I'm trying to display a DateTime formatted like 2019-10-07 17:00 in a TextBlock. The text should be underlined and dashed. To do this I'm using the following xaml
<TextBlock Text="2019-10-07 17:00">
<TextBlock.TextDecorations>
<TextDecoration Location="Underline">
<TextDecoration.Pen>
<Pen Brush="Black">
<Pen.DashStyle>
<DashStyle Dashes="5"/>
</Pen.DashStyle>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
</TextBlock.TextDecorations>
</TextBlock>
However, this produces some very unexpected results where it seems like each hyphen causes the dashed underline to restart its rendering. Notice the dash-pattern which looks almost random efter each hyphen.
If I change the "minus-sign-hyphen" to "non-breaking-hyphen" which looks very similar (- vs ‐), the rendering works as expected.
<TextBlock Text="2019‐10‐07 17:00" ...>
This buggy rendering of the dashed underline happends everytime I add a minus-sign-hyphen to the text but not with any other character that I could find. Has anyone else noticed this and does anyone have a solution? If not, what might be the reason for this weird behavior?
Given your format, the size should always be roughly the same so you could use another textblock like so and just let it overlay the other box
<TextBlock Text="This is a really lon" Foreground="Transparent" IsHitTestVisible="False">
<TextBlock.TextDecorations>
<TextDecoration Location="Underline">
<TextDecoration.Pen>
<Pen Brush="Black">
<Pen.DashStyle>
<DashStyle Dashes="5"/>
</Pen.DashStyle>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
</TextBlock.TextDecorations>
</TextBlock>
<TextBlock Text="2019-10-07 17:00" />
This is probably a result of some weird dash-hack found in the WPF glyph rendering code. In the .NET source you will find the AdjustAdvanceForDisplayLayout() method and its comment:
// AdvanceHeight is used to compute the bounding box. In some case, eg. the dash
// character '-', the bounding box is computed to be empty in Display
// TextFormattingMode (because the metrics are rounded to be pixel aligned) and so the
// dash is not rendered!
Setting TextOptions.TextFormattingMode="Display" on the TextBlock will produce a slightly different artifact:
This tells us that we did indeed hit this "workaround" (see GlyphRun.cs line 1326).
So the question is if we can somehow get a third variant, without any of these artifacts. So far, I have not succeeded but I did try to find where this hyphen check occurs. It seems to happen in native code. See TextFormatterContext.cs and LoCreateContext.
I don't have an answer to why this odd behavior occurs. It looks like the dashes created by the Pen are mapped to the decorated text of the TextDecoration. This makes sense as the dashes or TextDecoration in general will automatically adjust to the e.g. font size. The minus character seems to produce a different spacing. Maybe this behavior doesn't occur when using a monospace font.
Anyway, you could create a tiled DrawingBrush and assign it to the Pen.Brush property to create the dashed line. You can play around with the DrawingBrush.ViewPort to alter the position or the length of the dashes.
The Viewport consists of four values and is actually a Rect that describes the tile's position and dimension: x, y, width, height. Bigger values for width and height create longer dashes.
The result is an even drawing of dashes and spaces:
<TextBlock Text="2019-10-07 17:00">
<TextBlock.TextDecorations>
<TextDecoration Location="Underline">
<TextDecoration.Pen>
<Pen>
<Pen.Brush>
<DrawingBrush Viewport="0,0,10,10"
ViewportUnits="Absolute"
TileMode="Tile">
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Black">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,5,5" />
<RectangleGeometry Rect="5,5,5,5" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Pen.Brush>
</Pen>
</TextDecoration.Pen>
</TextDecoration>
</TextBlock.TextDecorations>
</TextBlock>
The downside of this approach is that the size and position of the dashes is no longer adaptive to the size of the font.
In the end, we built a custom control called DashTextBlock to solve this issue. It derives from TextBox and is styled like a TextBlock with an added TextDecoration that uses a Pen with a LinearGradientBrush that is set up according to whatever what specified as "dash-properties" and the thickness of DashThickness.
To achieve this it uses the TextBox method GetRectFromCharacterIndex to figure out how to setup the LinearGradientBrush.
TextBox.GetRectFromCharacterIndex Method
Returns the rectangle for an edge of the character at the specified index.
It produces results like this
Sample usage
<StackPanel>
<controls:DashTextBlock Text="Testing DashTextBlock"
DashThickness="1"
DashColor="Blue">
<controls:DashTextBlock.DashStyle>
<DashStyle Dashes="4,4,4,4" Offset="0" />
</controls:DashTextBlock.DashStyle>
</controls:DashTextBlock>
<controls:DashTextBlock Text="Testing DashTextBlock"
Margin="0 5 0 0"
DashThickness="2"
DashColor="Orange">
<controls:DashTextBlock.DashStyle>
<DashStyle Dashes="8 4 8 4" Offset="0" />
</controls:DashTextBlock.DashStyle>
</controls:DashTextBlock>
</StackPanel>
DashTextBlock
public class DashTextBlock : TextBox
{
public static readonly DependencyProperty DashColorProperty =
DependencyProperty.Register("DashColor",
typeof(Color),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(Colors.Black, OnDashColorChanged));
public static readonly DependencyProperty DashThicknessProperty =
DependencyProperty.Register("DashThickness",
typeof(double),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(1.0, OnDashThicknessChanged));
public static readonly DependencyProperty DashStyleProperty =
DependencyProperty.Register("DashStyle",
typeof(DashStyle),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(DashStyles.Solid, OnDashStyleChanged));
private static readonly DependencyProperty FontSizeCallbackProperty =
DependencyProperty.Register("FontSizeCallback",
typeof(double),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(0.0, OnFontSizeCallbackChanged));
public static readonly DependencyProperty TextLengthProperty =
DependencyProperty.Register("TextLength",
typeof(double),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(0.0));
public static readonly DependencyProperty DashEnabledProperty =
DependencyProperty.Register("DashEnabled",
typeof(bool),
typeof(DashTextBlock),
new FrameworkPropertyMetadata(true, OnDashEnabledChanged));
private static void OnDashColorChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DashTextBlock dashTextBlock = source as DashTextBlock;
dashTextBlock.DashColorChanged();
}
private static void OnDashThicknessChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DashTextBlock dashTextBlock = source as DashTextBlock;
dashTextBlock.DashThicknessChanged();
}
private static void OnDashStyleChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DashTextBlock dashTextBlock = source as DashTextBlock;
dashTextBlock.DashStyleChanged();
}
private static void OnFontSizeCallbackChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DashTextBlock dashTextBlock = source as DashTextBlock;
dashTextBlock.FontSizeChanged();
}
private static void OnDashEnabledChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DashTextBlock dashTextBlock = source as DashTextBlock;
dashTextBlock.DashEnabledChanged();
}
private static Pen _transparentPen;
static DashTextBlock()
{
_transparentPen = new Pen(Brushes.Transparent, 0);
_transparentPen.Freeze();
DefaultStyleKeyProperty.OverrideMetadata(typeof(DashTextBlock), new FrameworkPropertyMetadata(typeof(DashTextBlock)));
}
private TextDecoration _dashDecoration = new TextDecoration();
public DashTextBlock()
{
Binding fontSizeCallbackBinding = new Binding();
fontSizeCallbackBinding.Source = this;
fontSizeCallbackBinding.Path = new PropertyPath(TextBlock.FontSizeProperty);
this.SetBinding(FontSizeCallbackProperty, fontSizeCallbackBinding);
TextChanged += DashTextBlock_TextChanged;
this.LayoutUpdated += DashTextBlock_LayoutUpdated;
}
private void DashTextBlock_LayoutUpdated(object sender, EventArgs e)
{
if (IsLoaded)
{
var textRect = GetRectFromCharacterIndex(Text.Length);
double availableWidth = textRect.Right;
if (textRect.IsEmpty == false &&
availableWidth > 0)
{
this.LayoutUpdated -= DashTextBlock_LayoutUpdated;
UpdateTextWithDashing();
}
}
}
public Color DashColor
{
get { return (Color)GetValue(DashColorProperty); }
set { SetValue(DashColorProperty, value); }
}
public double DashThickness
{
get { return (double)GetValue(DashThicknessProperty); }
set { SetValue(DashThicknessProperty, value); }
}
public DashStyle DashStyle
{
get { return (DashStyle)GetValue(DashStyleProperty); }
set { SetValue(DashStyleProperty, value); }
}
private double FontSizeCallback
{
get { return (double)GetValue(FontSizeCallbackProperty); }
set { SetValue(FontSizeCallbackProperty, value); }
}
public double TextLength
{
get { return (double)GetValue(TextLengthProperty); }
set { SetValue(TextLengthProperty, value); }
}
public bool DashEnabled
{
get { return (bool)GetValue(DashEnabledProperty); }
set { SetValue(DashEnabledProperty, value); }
}
private void DashTextBlock_TextChanged(object sender, TextChangedEventArgs e)
{
UpdateTextWithDashing();
}
private void FontSizeChanged()
{
//UpdateTextWithDashing();
}
private void DashEnabledChanged()
{
UpdateTextWithDashing();
}
private void DashColorChanged()
{
UpdateTextWithDashing();
}
private void DashStyleChanged()
{
UpdateTextWithDashing();
}
private void DashThicknessChanged()
{
UpdateTextWithDashing();
}
public void UpdateTextWithDashing()
{
AddDashDecoration();
_dashDecoration.Pen = CreatePenFromProperties();
}
private Pen CreatePenFromProperties()
{
if (!DashEnabled)
{
return _transparentPen;
}
if (DashStyle.Dashes.Count < 2 ||
IsLoaded == false ||
Text.Length == 0)
{
return new Pen(new SolidColorBrush(DashColor), DashThickness);
}
double length = 0.0;
foreach (var dash in DashStyle.Dashes)
{
length += dash;
}
double stepLength = 1.0 / length;
TextBox textBox = this as TextBox;
Rect textRect = Rect.Empty;
for (int l = (textBox.Text.Length - 1); l >= 0; l--)
{
if (textBox.Text[l] != ' ')
{
try
{
textRect = textBox.GetRectFromCharacterIndex(l + 1);
}
catch
{
// See possible bug here:
// https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/VirtualizingStackPanel.cs,8060
// TODO: Revisit after migrate to .NET 5
}
break;
}
}
double target = FontSize;
double availableWidth = textRect.Right;
if (textRect.IsEmpty == false &&
availableWidth > 0)
{
TextLength = availableWidth;
double current = 0;
bool count = true;
bool foundTargetLength = false;
double savedDashes = 0.0;
while (!foundTargetLength)
{
for (int i = 0; i < DashStyle.Dashes.Count; i++)
{
var dash = DashStyle.Dashes[i];
savedDashes += dash;
double increase = (target * (dash * stepLength));
double preDiff = availableWidth - current;
current += increase;
double postDiff = current - availableWidth;
if (current > availableWidth)
{
if (!count)
{
if (postDiff < preDiff || Text.Length <= 2)
{
if ((i + 1) < DashStyle.Dashes.Count)
{
savedDashes += DashStyle.Dashes[i + 1];
}
else
{
savedDashes += DashStyle.Dashes[0];
}
}
else
{
if (i == 0)
{
savedDashes -= DashStyle.Dashes.Last();
}
else
{
savedDashes -= DashStyle.Dashes[i - 1];
}
}
}
foundTargetLength = true;
target = availableWidth / (savedDashes * stepLength);
break;
}
count = !count;
}
}
}
LinearGradientBrush dashBrush = new LinearGradientBrush();
dashBrush.StartPoint = new Point(0, 0);
dashBrush.EndPoint = new Point(target, 0);
dashBrush.MappingMode = BrushMappingMode.Absolute;
dashBrush.SpreadMethod = GradientSpreadMethod.Repeat;
double offset = 0.0;
bool isFill = true;
foreach (var dash in DashStyle.Dashes)
{
GradientStop gradientStop = new GradientStop();
gradientStop.Offset = offset;
gradientStop.Color = isFill ? DashColor : Colors.Transparent;
dashBrush.GradientStops.Add(gradientStop);
offset += (dash * stepLength);
gradientStop = new GradientStop();
gradientStop.Offset = offset;
gradientStop.Color = isFill ? DashColor : Colors.Transparent;
dashBrush.GradientStops.Add(gradientStop);
isFill = !isFill;
}
Pen dashPen = new Pen(dashBrush, DashThickness);
return dashPen;
}
private void AddDashDecoration()
{
foreach (TextDecoration textDecoration in TextDecorations)
{
if (textDecoration == _dashDecoration)
{
return;
}
}
TextDecorations.Add(_dashDecoration);
}
}
Style
<Style TargetType="{x:Type controls:DashTextBlock}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:DashTextBlock}">
<Border x:Name="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0"
Background="{TemplateBinding Background}"
SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"
Focusable="False"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" TargetName="border" Value="0.56"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Related
I am relatively new to custom controls (writing control from scratch in code - not merely styling existing controls). I am having a go at replicating the YouTube video control, you know the one...
To start with I want to develop the "timeline" (the transparent grey bar, which displays the current position of the video and allows the user to drag to change position). With the preview panel and all the rest coming later on...
I currently have the control partially rendered and the hover animations and scale working very well...
However, I am struggling to write the correct code to allow me to drag the "thumb". When I try and handle my left click on the Ellipse that is representing my thumb, the leave event of the containing Canvas fires, in accordance with the WPF documentation, so no complaints, I just don;t know how to achieve what I want and indeed if what I have done already is the correct approach.
The code:
[ToolboxItem(true)]
[DisplayName("VideoTimeline")]
[Description("Controls which allows the user navigate video media. In addition is can display a " +
"waveform repesenting the audio channels for the loaded video media.")]
//[TemplatePart(Name = "PART_ThumbCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_TimelineCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_WaveformCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_PreviewCanvas", Type = typeof(Canvas))]
[TemplatePart(Name = "PART_Thumb", Type = typeof(Ellipse))] // Is this the right thing to be doing?
public class VideoTimeline : Control
{
private Canvas thumbCanvas;
private Canvas timelineCanvas;
private Canvas waveformCanvas;
private Canvas previewCanvas;
private Rectangle timelineOuterBox = new Rectangle();
private Rectangle timelineProgressBox = new Rectangle();
private Rectangle timelineSelectionBox = new Rectangle();
private Ellipse timelineThumb = new Ellipse();
private Path previewWindow = new Path();
private Point mouseDownPosition;
private Point currentMousePosition;
private const int TIMELINE_ANIMATION_DURATION = 400;
private const string HIGHLIGHT_FILL = "#878787";
private double __timelineWidth;
#region Initialization.
static VideoTimeline()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(VideoTimeline),
new FrameworkPropertyMetadata(typeof(VideoTimeline)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//thumbCanvas = GetTemplateChild("PART_ThumbCanvas") as Canvas;
//thumbCanvas.Background = new SolidColorBrush(Colors.Transparent);
//thumbCanvas.Children.Add(timelineThumb);
timelineThumb = EnforceInstance<Ellipse>("PART_Thumb");
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineCanvas = GetTemplateChild("PART_TimelineCanvas") as Canvas;
timelineCanvas.Background = new SolidColorBrush(Colors.Transparent);
timelineCanvas.Children.Add(timelineOuterBox);
timelineCanvas.Children.Add(timelineSelectionBox);
timelineCanvas.Children.Add(timelineProgressBox);
timelineCanvas.Children.Add(timelineThumb);
previewCanvas = GetTemplateChild("PART_PreviewCanvas") as Canvas;
previewCanvas.Background = new SolidColorBrush(Colors.Transparent);
previewCanvas.Children.Add(previewWindow);
}
private T EnforceInstance<T>(string partName) where T : FrameworkElement, new()
{
return GetTemplateChild(partName) as T ?? new T();
}
protected override void OnTemplateChanged(ControlTemplate oldTemplate, ControlTemplate newTemplate)
{
base.OnTemplateChanged(oldTemplate, newTemplate);
if (timelineCanvas != null)
timelineCanvas.Children.Clear();
SetDefaultMeasurements();
}
#endregion // Initialization.
#region Event Overrides.
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
//UpdateWaveformCacheScaling();
SetDefaultMeasurements();
UpdateAllRegions();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
if (c != null)
{
CaptureMouse();
mouseDownPosition = e.GetPosition(c);
if (c.Name == "PART_TimelineCanvas")
{
Trace.WriteLine("OnMouseLeftDown over TimeLine");
}
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
ReleaseMouseCapture();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
currentMousePosition = e.GetPosition(thumbCanvas);
if (Mouse.Captured == null)
{
Canvas c = e.OriginalSource as Canvas;
if (c == null)
c = Utils.FindParent<Canvas>(e.OriginalSource as FrameworkElement);
}
}
#endregion // Event Overrides.
#region Drawing Methods and Events.
private void UpdateAllRegions()
{
UpdateTimelineCanvas();
}
private void UpdateTimelineCanvas()
{
if (timelineCanvas == null)
return;
SetDefaultMeasurements();
// Bounding timeline box.
timelineOuterBox.Fill = new SolidColorBrush(
(Color)ColorConverter.ConvertFromString("#878787")) { Opacity = 0.25 };
timelineOuterBox.StrokeThickness = 0.0;
timelineOuterBox.Width = __timelineWidth;
timelineOuterBox.Height = TimelineThickness;
timelineOuterBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineOuterBox.SnapsToDevicePixels = true;
// Selection timeline box.
timelineSelectionBox.Fill = TimelineSelectionBrush;
timelineSelectionBox.Width = 0.0;
timelineSelectionBox.Height = TimelineThickness;
timelineSelectionBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineSelectionBox.SnapsToDevicePixels = true;
// Progress timeline box.
timelineProgressBox.Fill = TimelineProgressBrush;
timelineProgressBox.StrokeThickness = 0.0;
timelineProgressBox.Width = 0.0;
timelineProgressBox.Height = TimelineThickness;
timelineProgressBox.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
timelineProgressBox.SnapsToDevicePixels = true;
// Animation and selection.
timelineCanvas.MouseEnter -= TimelineCanvas_MouseEnter;
timelineCanvas.MouseEnter += TimelineCanvas_MouseEnter;
timelineCanvas.MouseLeave -= TimelineCanvas_MouseLeave;
timelineCanvas.MouseLeave += TimelineCanvas_MouseLeave;
timelineCanvas.MouseMove -= TimelineCanvas_MouseMove;
timelineCanvas.MouseMove += TimelineCanvas_MouseMove;
timelineCanvas.MouseDown -= TimelineCanvas_MouseDown;
timelineCanvas.MouseDown += TimelineCanvas_MouseDown;
// The draggable thumb.
timelineThumb.Fill = TimelineThumbBrush;
//timelineThumb.Stroke = new SolidColorBrush(Colors.Black);
//timelineThumb.StrokeThickness = 0.5;
timelineThumb.VerticalAlignment = VerticalAlignment.Center;
timelineThumb.Height = timelineThumb.Width = 0.0;
timelineThumb.Margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
timelineCanvas.RenderSize.Height / 2, 0, 0);
timelineThumb.SnapsToDevicePixels = true;
timelineThumb.MouseLeftButtonDown -= TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonDown += TimelineThumb_MouseLeftButtonDown;
timelineThumb.MouseLeftButtonUp -= TimelineThumb_MouseLeftButtonUp;
timelineThumb.MouseLeftButtonUp += TimelineThumb_MouseLeftButtonUp;
// Preview window.
}
private void TimelineCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
Trace.WriteLine("POON");
}
private void SetDefaultMeasurements()
{
if (timelineCanvas != null)
__timelineWidth = timelineCanvas.RenderSize.Width - 2 * 2 * TimelineThickness;
}
private void TimelineCanvas_MouseEnter(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(0,
(timelineCanvas.RenderSize.Height - 2 * TimelineExpansionFactor * TimelineThickness) / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, TimelineThickness * TimelineExpansionFactor * 2, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness * TimelineExpansionFactor, margin, easing);
double selectionWidth = (currentMousePosition.X / RenderSize.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth;
Trace.WriteLine("MouseENTER Canvas");
}
private void TimelineCanvas_MouseLeave(object sender, MouseEventArgs e)
{
timelineThumb.ResetAnimation(Ellipse.WidthProperty, Ellipse.HeightProperty);
timelineProgressBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineSelectionBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
timelineOuterBox.ResetAnimation(Rectangle.HeightProperty, Rectangle.MarginProperty);
CircleEase easing = new CircleEase();
easing.EasingMode = EasingMode.EaseOut;
// Thumb animation.
Thickness margin = new Thickness(TimelineExpansionFactor * TimelineThickness, timelineCanvas.RenderSize.Height / 2, 0, 0);
EllpiseDiameterAnimation(timelineThumb, 0.0, margin, easing);
// Timeline animation.
margin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - TimelineThickness) / 2, 0, 0);
TimelineHeightAnimation(timelineProgressBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineSelectionBox, TimelineThickness, margin, easing);
TimelineHeightAnimation(timelineOuterBox, TimelineThickness, margin, easing);
if (!isDraggingThumb)
timelineSelectionBox.Width = 0.0;
Trace.WriteLine("MouseLeave Canvas");
}
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
Thickness thumbMargin = new Thickness(TimelineExpansionFactor * TimelineThickness,
(timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
timelineThumb.Margin = thumbMargin;
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
#endregion // Drawing Methods and Events.
#region Animation Methods.
private void EllpiseDiameterAnimation(Ellipse ellipse, double diameter, Thickness margin, IEasingFunction easing)
{
AnimationTimeline widthAnimation = ShapeWidthAnimation(ellipse, diameter, easing);
AnimationTimeline heightAnimation = ShapeHeightAnimation(ellipse, diameter, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(ellipse, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(widthAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Children.Add(marginAnimation);
storyboard.Begin(this);
}
private void TimelineHeightAnimation(Rectangle rectangle, double height, Thickness margin, IEasingFunction easing)
{
AnimationTimeline heightAnimation = ShapeHeightAnimation(rectangle, height, easing);
AnimationTimeline marginAnimation = ShapeMarginAnimation(rectangle, margin, easing);
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(marginAnimation);
storyboard.Children.Add(heightAnimation);
storyboard.Begin(this);
}
private AnimationTimeline ShapeMarginAnimation(Shape shape, Thickness margin, IEasingFunction easing)
{
ThicknessAnimation marginAnimation = new ThicknessAnimation(
margin, TimeSpan.FromMilliseconds((TIMELINE_ANIMATION_DURATION)));
if (easing != null)
marginAnimation.EasingFunction = easing;
Storyboard.SetTarget(marginAnimation, shape);
Storyboard.SetTargetProperty(marginAnimation, new PropertyPath(Rectangle.MarginProperty));
return marginAnimation;
}
private AnimationTimeline ShapeWidthAnimation(Shape shape, double width, IEasingFunction easing)
{
DoubleAnimation widthAnimation = new DoubleAnimation(
width, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
widthAnimation.EasingFunction = easing;
Storyboard.SetTarget(widthAnimation, shape);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Shape.WidthProperty));
return widthAnimation;
}
private AnimationTimeline ShapeHeightAnimation(Shape shape, double height, IEasingFunction easing)
{
DoubleAnimation heightAnimation = new DoubleAnimation(
height, TimeSpan.FromMilliseconds(TIMELINE_ANIMATION_DURATION));
if (easing != null)
heightAnimation.EasingFunction = easing;
Storyboard.SetTarget(heightAnimation, shape);
Storyboard.SetTargetProperty(heightAnimation, new PropertyPath(Shape.HeightProperty));
return heightAnimation;
}
#endregion // Animation Methods.
// Lots of DependencyProperties here...
}
The XAML style
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MediaControlBuilder">
<Style TargetType="{x:Type local:VideoTimeline}">
<Setter Property="TimelineProgressBrush" Value="DarkOrange"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:VideoTimeline}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<!--<RowDefinition Height="*"/>-->
<!--<RowDefinition Height="15"/>-->
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
<!--<RowDefinition Height="*"/>-->
</Grid.RowDefinitions>
<Canvas Name="PART_PreviewCanvas"
Grid.Row="0"
ClipToBounds="True"/>
<Canvas Name="PART_ThumbCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_TimelineCanvas"
Grid.Row="1"
ClipToBounds="True"/>
<Canvas Name="PART_WaveformCanvas"
Grid.Row="1"
ClipToBounds="True"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
My questions are:
Is my approach for drawing the draggable thumb correct?
How can I actually change the code to get the dragging of my "thumb" to work?
Thanks for your time.
Ps. the GitHub project with the working code is here so you can reproduce the problem I am having. If anyone wants to help me develop this control, that would be awesome!
Pps. I am aware I could override a slider to get my functionality for the "timeline", but this is just the first part of a much more comprehensive control and hence needs to be written from scratch.
I'm not sure but I think that can resolve your problem :
private void TimelineCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point relativePosition = e.GetPosition(timelineOuterBox);
double selectionWidth = (relativePosition.X / timelineOuterBox.Width) * timelineOuterBox.Width;
timelineSelectionBox.Width = selectionWidth.Clamp(0.0, timelineOuterBox.Width);
if (isDraggingThumb)
{
timelineProgressBox.Width = timelineSelectionBox.Width;
//Thickness thumbMargin = new Thickness(TimelineThickness * TimelineExpansionFactor,
// (timelineCanvas.RenderSize.Height - (TimelineThickness * TimelineExpansionFactor)) / 2, 0, 0);
//timelineThumb.Margin = thumbMargin;
Canvas.SetLeft(timelineThumb, timelineProgressBox.Width);
}
}
private bool isDraggingThumb = false;
private void TimelineThumb_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//CaptureMouse();
isDraggingThumb = true;
Trace.WriteLine("Dragging Thumb");
}
private void TimelineThumb_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
//ReleaseMouseCapture();
isDraggingThumb = false;
Trace.WriteLine("STOPPED Dragging Thumb");
}
You can stop the bubbling by handling the event args, and the leave event won't be fired.
To change the position of the thumb, you have to set the Left attached property of the Canvas.
Additionnaly you will have to reset isdraggingThumb :
/// <summary>
/// Invoked when an unhandled MouseLeftButtonUp routed event reaches an element in
/// its route that is derived from this class. Implement this method to add class
/// handling for this event.
/// </summary>
/// <param name="e">The MouseButtonEventArgs that contains the event data. The event
/// data reports that the left mouse button was released.</param>
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
isDraggingThumb = false;
When creating new custom controls you should not "write control from scratch in code".
Better is to base your new implementation of an existing control. In your case you want to create a custom slider control so your custom control could inherit from Slider leveraging existing functionality like thumb dragging logic and Start, End, Value properties.
When extending an existing control you start with original control's default template, it can be obtained with VS. A slider will have an element that you should be particularly interested in:
<Track x:Name="PART_Track" Grid.Column="1">
<Track.DecreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.DecreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="{x:Static Slider.IncreaseLarge}" Style="{StaticResource RepeatButtonTransparent}"/>
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb x:Name="Thumb" Focusable="False" Height="11" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbVerticalDefault}" VerticalAlignment="Top" Width="18"/>
</Track.Thumb>
</Track>
By using the required elements in the template your base control will take care of all basic slider capabilities. Starting from this, you can alter the base control functionality, style the slider parts the way you want and add any new functionality.
If you don't want to expose Slider properties that are not applicable for your Timeline control, like Minimum, just use a Slider control in your template.
I believe your question is focused on the timeline slider. There is no need to create your own. Just use the Slider control. You can restyle to make the fill red and the remaining semi-transparent. Then you can bind the Value to the Position of the MediaElement control and the Maximum of the Slider to the Duration.
<Slider Value="{Binding Position.Milliseconds, ElementName=MediaPlayer}"
Maximum="{Binding Duration.TimeSpan.Milliseconds, , ElementName=MediaPlayer}"
Style="{StaticResource YouTubeSliderStyle}" />
When the value changes you can update the Position of the MediaElement. You only want to do this when the user changes the value (not when it changes due to Position updating). To accomplish this you can listen to then mousedown/up and keydown/up events. During these events you can then (un)subscribe to the ValueChanged event and update the position.
private void UpdatePosition(long time)
{
MediaPlayer.Position = TimeSpan.FromMilliseconds(time);
}
Update: Ways to show/hide the thumb.
You can show or hide the thumb two ways. The first is to create anew Slider control and show/hide the thumb when the mouse is over.
class YouTubeSlider : Slider
{
private Thumb _thumb;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_thumb = (Thumb)GetTemplateChild("Thumb");
_thumb.Opacity = 0;
}
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
_thumb.Opacity = 1;
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
_thumb.Opacity = 0;
}
}
The second is to handle it within the style of the control. (some parts have been removed for brevity)
<ControlTemplate x:Key="SliderHorizontal" TargetType="{x:Type Slider}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<!-- Elements -->
<Track.Thumb>
<Thumb x:Name="Thumb" Opacity="0" Focusable="False" Height="18" OverridesDefaultStyle="True" Template="{StaticResource SliderThumbHorizontalDefault}" VerticalAlignment="Center" Width="11"/>
</Track.Thumb>
<!-- closing tags -->
</Border>
<ControlTemplate.Triggers>
<!-- missing triggers -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" TargetName="Thumb" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
I've created a bar chart via the below code however I'd like to add the red line at some X value as seen in the image below. Is this possible? And if so, how could I accomplish this?
<charting:Chart x:Name="BarChart" HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Visible" Width="280" Height="500" Margin="-5,-35,0,20" FontSize="10">
<charting:Chart.LegendStyle>
<Style TargetType="datavis:Legend">
<Setter Property="Width" Value="0"/>
</Style>
</charting:Chart.LegendStyle>
<charting:BarSeries Visibility="Visible"/>
<charting:Chart.Palette>
<charting:ResourceDictionaryCollection>
<ResourceDictionary>
<SolidColorBrush x:Key="Background" Color="#2574a9" />
<Style x:Key="DataPointStyle" TargetType="Control">
<Setter Property="Background" Value="{StaticResource Background}" />
</Style>
<Style x:Key="DataShapeStyle" TargetType="Shape">
<Setter Property="Stroke" Value="{StaticResource Background}" />
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="StrokeMiterLimit" Value="1" />
<Setter Property="Fill" Value="{StaticResource Background}" />
</Style>
</ResourceDictionary>
</charting:ResourceDictionaryCollection>
</charting:Chart.Palette>
</charting:Chart>
I wrote a demo to add a line according to the special X value. This feature is implemented by calculating the position of the special X value, and add the Line element. It works and you can test.
XAML Code
<Grid x:Name="rootgrid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<charting:Chart
x:Name="BarChart"
Title="Bar Chart"
Width="800"
Height="400">
<charting:BarSeries
x:Name="bar"
Title="Population"
DependentValueBinding="{Binding Value}"
IndependentValueBinding="{Binding Name}"
IsSelectionEnabled="True" />
</charting:Chart>
<StackPanel x:Name="InsertLine" >
<TextBox x:Name="txtnumber" Header="The location for insert a new line" />
<Button
x:Name="getaxi"
Click="getaxi_Click"
Content="Insert the new line" />
</StackPanel>
</Grid>
Code behind
public sealed partial class BarChat : Page
{
private Random _random = new Random();
private LinearAxis linearaxis;
private CategoryAxis categoryaxis;
private double linearaximaximum;
private double linearaximinimum;
private Line newline;
public BarChat()
{
this.InitializeComponent();
Window.Current.SizeChanged += Current_SizeChanged; ;
var items1 = new List<NameValueItem>();
for (int i = 0; i < 3; i++)
{
items1.Add(new NameValueItem { Name = "Name" + i, Value = _random.Next(1, 100) });
}
this.RunIfSelected(this.BarChart, () => ((BarSeries)this.BarChart.Series[0]).ItemsSource = items1);
}
private void Current_SizeChanged(object sender, Windows.UI.Core.WindowSizeChangedEventArgs e)
{
if (rootgrid.Children.Contains(newline))
{
clearline();
if (txtnumber.Text != null)
{
var insertvalue = Convert.ToDouble(txtnumber.Text);
positionline(insertvalue);
}
}
}
public void barchartinihital()
{
var Actualaxes = BarChart.ActualAxes;
linearaxis = Actualaxes[1] as LinearAxis;
categoryaxis = Actualaxes[0] as CategoryAxis;
linearaximaximum = Convert.ToDouble(linearaxis.ActualMaximum);
linearaximinimum = Convert.ToDouble(linearaxis.ActualMinimum);
}
private void RunIfSelected(UIElement element, Action action)
{
action.Invoke();
}
private async void getaxi_Click(object sender, RoutedEventArgs e)
{
clearline();
barchartinihital();
if (txtnumber.Text != null)
{
double insertvalue = Convert.ToDouble(txtnumber.Text);
if (insertvalue > linearaximaximum || insertvalue < linearaximinimum)
{
await new Windows.UI.Popups.MessageDialog("Please input a value in chart arrange").ShowAsync();
}
else
{
positionline(insertvalue);
}
}
}
public void positionline(double insertvalue)
{
var interval = linearaxis.ActualInterval;
double perinterval = Convert.ToDouble(linearaxis.ActualWidth / (linearaximaximum - linearaximinimum));
var lineX = perinterval * (insertvalue - linearaximinimum);
var lineheight = categoryaxis.ActualHeight;
var ttv = bar.TransformToVisual(Window.Current.Content);
Point screenCoords = ttv.TransformPoint(new Point(0, 0));
var chartx = screenCoords.X;
var charty = screenCoords.Y;
newline = new Line();
newline.X1 = Convert.ToDouble(lineX) + chartx;
newline.X2 = Convert.ToDouble(lineX) + chartx;
newline.Y1 = charty;
newline.Y2 = charty + lineheight;
newline.Stroke = new SolidColorBrush(Colors.Red);
newline.StrokeThickness = 2;
rootgrid.Children.Add(newline);
}
public void clearline()
{
if (rootgrid.Children.Contains(newline))
{
{
rootgrid.Children.Remove(newline);
}
}
}
}
I’m trying to create virtualizing uniform grid, so I took some code from here which creates a panel where I can set the number of columns,
and it works fine, and it’s about 3-4 times faster than standard uniform grid (I tested it on a complicated item template for listbox with lots of records).
So I thought I change the base control from Panel to VirtualizingStackPanel but as soon as I do this, no records show. Any Ideas why?
Here is my working code:
Change below to derive from VirtualizingPanel or VirtualizingStackPanel, and it won't work anymore :(
public class MyUniformGrid : Panel // VirtualizingStackPanel
{
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns", typeof(int), typeof(MyUniformGrid), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure));
public int Columns
{
set { SetValue(ColumnsProperty, value); }
get { return (int)GetValue(ColumnsProperty); }
}
private int Rows => (InternalChildren.Count + Columns - 1) / Columns;
protected override Size MeasureOverride(Size sizeAvailable)
{
var sizeChild = new Size(sizeAvailable.Width / Columns, sizeAvailable.Height / Rows);
double maxwidth = 0;
double maxheight = 0;
foreach (UIElement child in InternalChildren)
{
child.Measure(sizeChild);
maxwidth = Math.Max(maxwidth, child.DesiredSize.Width);
maxheight = Math.Max(maxheight, child.DesiredSize.Height);
}
return new Size(Columns * maxwidth, Rows * maxheight);
}
protected override Size ArrangeOverride(Size sizeFinal)
{
var sizeChild = new Size(sizeFinal.Width / Columns, sizeFinal.Height / Rows);
for (var index = 0; index < InternalChildren.Count; index++)
{
var row = index / Columns;
var col = index % Columns;
var rectChild = new Rect(new Point(col * sizeChild.Width, row * sizeChild.Height), sizeChild);
InternalChildren[index].Arrange(rectChild);
}
return sizeFinal;
}
}
xaml
<ListBox x:Name="MyListBox">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<uniformGridDemo:MyUniformGrid Columns="2" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
code behind
public MainWindow()
{
InitializeComponent();
var list = new List<int>();
for (int i = 0; i < 1000; i++)
{
list.Add(i);
}
MyListBox.ItemsSource = list;
}
Thank you
I'm not an expert on VirtualizingStackPanel, but if you just want to resolve the item display problem, below would work.
public class MyUniformGrid : VirtualizingStackPanel
{
public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns", typeof(int), typeof(MyUniformGrid), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure));
public int Columns
{
set { SetValue(ColumnsProperty, value); }
get { return (int)GetValue(ColumnsProperty); }
}
private int Rows
{
get
{
return (InternalChildren.Count + Columns - 1) / Columns;
}
}
protected override Size MeasureOverride(Size sizeAvailable)
{
base.MeasureOverride(sizeAvailable);
var sizeChild = new Size(sizeAvailable.Width / Columns, sizeAvailable.Height / Rows);
double maxwidth = 0;
double maxheight = 0;
foreach (UIElement child in this.InternalChildren)
{
child.Measure(sizeChild);
maxwidth = Math.Max(maxwidth, child.DesiredSize.Width);
maxheight = Math.Max(maxheight, child.DesiredSize.Height);
}
return new Size(Columns * maxwidth, Rows * maxheight);
}
protected override Size ArrangeOverride(Size sizeFinal)
{
base.ArrangeOverride(sizeFinal);
var sizeChild = new Size(sizeFinal.Width / Columns, sizeFinal.Height / Rows);
for (var index = 0; index < InternalChildren.Count; index++)
{
var row = index / Columns;
var col = index % Columns;
var rectChild = new Rect(new Point(col * sizeChild.Width, row * sizeChild.Height), sizeChild);
InternalChildren[index].Arrange(rectChild);
}
return sizeFinal;
}
}
But VirtualizingStackPanel is much complex implementation then overriding MeasureOverride & ArrangeOverride methods and arranging items. you need to process the scroll info and respect to that addition & removal of items in panel needs to be handled. So even after displayed items completely you may not get the desired performance.
I had implemented something similar in Silverlight by applying styles to ListBox and replacing the ItemsPanelTemplate with a WrapPanel. I've used the same in Windows Phone and WPF projects with no issues.
Styles
<!--Wrapping ListBox Styles-->
<Style x:Key="StretchedItemContainerStyle" TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<Style x:Key="ListBox_HorizontalWrapStyle" TargetType="ListBox">
<Setter Property="ItemContainerStyle" Value="{StaticResource StretchedItemContainerStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" Margin="0"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<ScrollViewer VerticalScrollBarVisibility="Auto" BorderBrush="{x:Null}" >
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--End Wrapping ListBox Styles-->
Xaml
<ListBox x:Name="MyListBox" Style="StaticResource ListBox_HorizontalWrapStyle">
You should also be able to style the WrapPanel for what ever other performance gains can be attained from it.
I'm trying to add several seperate textblocks into a grid dynamically so that adding an element to grid will populate the next open cell.
1 2
3 4
5 6
7 8
...
and so on
When any element is removed, every element following should be shifted to fill in any empty cells so that if 2, 5, 6 are removed (one at a time) it will look like this:
1 3
4 7
8 ...
My XAML and Code are as follows:
<StackPanel x:Name="NumbersStackPanel">
<TextBlock Text="Numbers: "/>
<Grid x:Name="NumbersGrid">
<TextBox x:Name="SearchNumbers"/>
</Grid>
</StackPanel>
CS:
TextBlock newTextBlock = new TextBlock();
newTextBlock.Visibility = Windows.UI.Xaml.Visibility.Visible;
newTextBlock.Foreground = new SolidColorBrush(Colors.Black);
newTextBlock.FontWeight = Windows.UI.Text.FontWeights.SemiBold;
newTextBlock.FontFamily = new Windows.UI.Xaml.Media.FontFamily("Segoe UI Semilight");
newTextBlock.Margin = new Thickness(0, 5, 4, 0);
newTextBlock.TextWrapping = TextWrapping.WrapWholeWords;
newTextBlock.FontSize = 18;
newTextBlock.Text = NumbersModelObj.Number + "; ";
newTextBlock.Tag = NumbersModelObj.NumberId;
textArray.Add(newTextBlock);
NumbersGrid.Children.Insert(NumbersCount, newTextBlock);
NumbersCount ++;
I've tried nested for loops given the value of elements (NumbersCount) but have not been successful in adding more than 2 elements to different cells into different cells in the grid
Are there any relatively simple/clean solutions for achieving this?
I've since found exactly what I was looking for here:
http://windowsapptutorials.com/windows-phone/ui/wrap-grid-with-variable-sized-items/
Hopefully this helps someone down the line.
What you're looking for is a called a UniformGrid. Unfortunately, it was not ported from desktop WPF to Windows Phone WPF.
However, I did some Googling and found this:
UniformGrid.cs
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace App1 // replace with your namespace, of course
{
public class UniformGrid : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var itemWidth = availableSize.Width / Columns;
foreach (FrameworkElement child in Children)
{
child.Measure(new Size(120, 120));
}
return new Size(availableSize.Width, availableSize.Width);
}
protected override Size ArrangeOverride(Size finalSize)
{
Size cellSize = new Size(finalSize.Width / Columns, finalSize.Width / Columns);
int row = 0, col = 0;
foreach (UIElement child in Children)
{
child.Arrange(new Rect(new Point(cellSize.Width * col, cellSize.Height * row), cellSize));
if (++col == Columns)
{
row++;
col = 0;
}
}
return finalSize;
}
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public int Rows
{
get { return (int)GetValue(RowsProperty); }
set { SetValue(RowsProperty, value); }
}
public static readonly DependencyProperty ColumnsProperty =
DependencyProperty.Register("Columns", typeof(int), typeof(UniformGrid), new PropertyMetadata(1, OnColumnsChanged));
public static readonly DependencyProperty RowsProperty =
DependencyProperty.Register("Rows", typeof(int), typeof(UniformGrid), new PropertyMetadata(1, OnRowsChanged));
static void OnColumnsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
int cols = (int)e.NewValue;
if (cols < 1)
((UniformGrid)obj).Columns = 1;
}
static void OnRowsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
int rows = (int)e.NewValue;
if (rows < 1)
((UniformGrid)obj).Rows = 1;
}
}
}
Resource:
https://social.msdn.microsoft.com/Forums/windowsapps/en-US/3254c8ff-a7ad-4346-b353-457cd6ac7a58/uwpcreating-a-uniformgrid-for-listview?forum=wpdevelop
How to use it:
Import UniformGrid.cs into your project; place it wherever you feel is appropriate.
Update the namespace appropriately (remember this namespace for when you use it in the Xaml).
Here's an example:
MainPage.xaml
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:custom="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel x:Name="NumbersStackPanel" Orientation="Vertical">
<TextBlock Text="Numbers: "/>
<custom:UniformGrid x:Name="NumbersGrid" Columns="2">
<custom:UniformGrid.Resources>
<Style TargetType="TextBox">
<Setter Property="Visibility" Value="Visible" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontFamily" Value="Segoe UI Semilight" />
<Setter Property="Margin" Value="0, 5, 4, 0" />
<Setter Property="TextWrapping" Value="WrapWholeWords" />
<Setter Property="FontSize" Value="18" />
</Style>
</custom:UniformGrid.Resources>
<TextBox Text="1" />
<TextBox Text="2" />
<TextBox Text="3" />
</custom:UniformGrid>
</StackPanel>
</Grid>
</Page>
Note: I'd recommend doing the formatting of textboxes in XAML, as shown. The style, as implemented, targets all TextBoxes within the UniformGrid.
MainPage.xaml.cs
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace App1
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
AddText("hello");
RemoveText("2");
AddText("Goodbye");
RemoveText("hello");
RemoveText("1");
}
private void AddText(string text)
{
TextBox tb = new TextBox();
tb.Text = text;
NumbersGrid.Children.Add(tb);
}
private void RemoveText(string text)
{
foreach(UIElement child in NumbersGrid.Children)
{
TextBox tb = (TextBox)child;
if (tb.Text.Equals(text))
{
NumbersGrid.Children.Remove(tb);
}
}
}
}
}
I'm trying to make a ColorableImage custom control, but I am not sure if what I am trying to do is possible. The class itself is rather straightforward:
public class ColorableImage : Image
{
private bool ChangedByColor { get; set; }
public static readonly DependencyProperty ColorProperty;
public static readonly DependencyProperty UncoloredSourceProperty;
static ColorableImage()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ColorableImage), new FrameworkPropertyMetadata(typeof(ColorableImage)));
SourceProperty.OverrideMetadata(typeof(ColorableImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(SourceChanged)));
ColorProperty = DependencyProperty.Register("Color", typeof(Color), typeof(ColorableImage), new FrameworkPropertyMetadata(new PropertyChangedCallback(ColorChanged)));
UncoloredSourceProperty = DependencyProperty.Register("UncoloredSource", typeof(ImageSource), typeof(ColorableImage));
}
public Color Color
{
get { return (Color)GetValue(ColorProperty); }
set { SetValue(ColorProperty, value); }
}
public ImageSource UncoloredSource
{
get { return (ImageSource)GetValue(UncoloredSourceProperty); }
protected set { SetValue(UncoloredSourceProperty, value); }
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorableImage cimg = (ColorableImage)d;
if (!cimg.ChangedByColor)
{
cimg.UncoloredSource = cimg.Source;
if (cimg.Color != null && !cimg.Color.ToString().Equals("#00000000"))
{
ColorChanged(d, e);
}
}
}
private static void ColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ColorableImage cimg = (ColorableImage)d;
// This will call the SourceChanged which we don't want (since it will change UncoloredImage)
cimg.ChangedByColor = true;
cimg.Source = ColorSource(cimg.Source, cimg.Color);
cimg.ChangedByColor = false;
}
private static ImageSource ColorSource(ImageSource source, Color color)
{
BitmapSource bitmapSource = (BitmapSource)source;
if (bitmapSource.Format.BitsPerPixel != 32 && bitmapSource.Format != System.Windows.Media.PixelFormats.Bgra32)
{
bitmapSource = new FormatConvertedBitmap(bitmapSource, System.Windows.Media.PixelFormats.Bgra32, null, 0);
}
WriteableBitmap wbmp = new WriteableBitmap(bitmapSource);
// Get necessary image info to copy into the pixel array
var bytesPerPixel = (wbmp.Format.BitsPerPixel + 7) / 8;
var stride = wbmp.PixelWidth * bytesPerPixel;
var arraySize = stride * wbmp.PixelHeight;
// Copy the image into the pixel array
var pixelArray = new byte[arraySize];
wbmp.CopyPixels(pixelArray, stride, 0);
// Convert the System.Windows.Media.Color to a System.Drawing.Color
System.Drawing.Color newColor = System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
// Put the new pixels into the pixel array
for (int i = 0; i < pixelArray.Length; i += bytesPerPixel)
{
// Only color those pixels that have no transparency
// This is a very simple alogirthm; you can pretty much only have one color in the image, with the rest of it being completely transparent; it is primarily meant for small, simple icons
if (pixelArray[i + 3] == 255)
{
pixelArray[i] = newColor.B;
pixelArray[i + 1] = newColor.G;
pixelArray[i + 2] = newColor.R;
pixelArray[i + 3] = newColor.A;
}
}
// Remake the image
wbmp.WritePixels(new Int32Rect(0, 0, wbmp.PixelWidth, wbmp.PixelHeight), pixelArray, stride, 0);
return wbmp;
}
}
The coloring algorithm is simple and the idea is to be able to replace every pixel that is not transparent with a given Color, which is a dependency property on the control. There is also a read-only property on the control called UncoloredSource which will give back the original image source, since the actual Source has changed due to coloring.
However, I think what I am doing is breaking some kind of internal binding the Image control has on its Source property. I am testing this with ListViewItems as follows:
<ControlTemplate x:Key="ListBoxItemControlTemplate1" TargetType="{x:Type ListBoxItem}">
<local:ColorableImage x:Name="img" Source="plus-black.png" Color="Red"></local:ColorableImage>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="img" Property="Color" Value="Blue" />
<Setter TargetName="img" Property="Source" Value="minus-black.png" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
...
<ListView>
<ListViewItem Template="{DynamicResource ListBoxItemControlTemplate1}" />
<ListViewItem Template="{DynamicResource ListBoxItemControlTemplate1}" />
<ListViewItem Template="{DynamicResource ListBoxItemControlTemplate1}" />
</ListView>
The image changes color successfully, but what I've found is that the SourceChanged event never fires again after Color is changed. My only guess is this is because I am reassigning the Source property in the code-behind in ColorChanged, which messes up the trigger... But I am kind of surprised that this would mess it up.
Also, if I switch around the trigger a little bit, this confirms what I think is happening. If I set no initial color, and then in the trigger change the Source first and Color second, then the source and color change, but when I select a second list item, the previous list item disappears completely, and when I select a third, the second disappears completely, etc., which I think means the trigger can no longer access the old source to revert it (unsure why).