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>
Related
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>
I know there are several posts on stack and others websites, but it seems I still make something wrong. When I zoom with MouseWheel event, the zoom is always not centered, but the left side of my canvas always stays on the left on my ViewBox, so when I zoom in, I only can see the left of my canvas.
XAML code :
<Grid x:Name="MainGrid">
<Viewbox x:Name="ViewBoxDessin" Stretch="None" HorizontalAlignment="Center" VerticalAlignment="Center">
<Canvas x:Name="monDessin" Background="WhiteSmoke" MouseWheel="monDessin_MouseWheel">
<Canvas.LayoutTransform>
<ScaleTransform x:Name="st" ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5" />
</Canvas.LayoutTransform>
</Canvas>
</Viewbox>
<Viewbox x:Name="ViewBoxDessin2" Stretch="None">
<Canvas x:Name="monDessin2">
<Canvas.LayoutTransform>
<ScaleTransform x:Name="st2" ScaleX="1" ScaleY="1" CenterX=".5" CenterY=".5" />
</Canvas.LayoutTransform>
</Canvas>
</Viewbox>
</Grid>
Code behind
public AfficheGraphiquePiece()
{
InitializeComponent();
MakeMyDrawing();
ViewBoxDessin.Width = System.Windows.SystemParameters.PrimaryScreenWidth;
ViewBoxDessin.Height = System.Windows.SystemParameters.PrimaryScreenHeight;
double ech_x = monDessin.Width / System.Windows.SystemParameters.PrimaryScreenWidth;
double ech_y = monDessin.Height / System.Windows.SystemParameters.PrimaryScreenHeight;
double ech = Math.Min(ech_x, ech_y);
this.ech_full = ech;
st.ScaleX = ech;
st.ScaleY = -ech;
st2.ScaleX = ech;
st2.ScaleY = ech;
}
private void monDessin_MouseWheel(object sender, MouseWheelEventArgs e)
{
double zoom = e.Delta > 0 ? 1.1 : 0.9;
if(st.ScaleX<this.ech_full*1.1 && zoom<1)
{
st.ScaleX = this.ech_full;
st.ScaleY = -this.ech_full;
}
else
{
st.ScaleX *= zoom;
st.ScaleY *= zoom;
double coor_x = Mouse.GetPosition(monDessin).X;
double coor_y = Mouse.GetPosition(monDessin).Y;
st.CenterX = coor_x;
st.CenterY = coor_y;
}
}
Excuse me, didn't remove some code, and it could make confusion, just replaced it by a function MakeMyDrawing()
Well, after Clemens advise, and help of that link for matrix use, I could do the following :
XAML :
<Grid x:Name="MainGrid">
<Canvas x:Name="monDessin" Background="WhiteSmoke" MouseWheel="monDessin_MouseWheel" MouseLeftButtonDown="image_MouseLeftButtonDown" MouseMove="image_MouseMove" MouseLeftButtonUp="image_MouseLeftButtonUp" MouseLeave="image_MouseLeave" >
<Canvas.RenderTransform >
<MatrixTransform/>
</Canvas.RenderTransform>
</Canvas>
<Canvas x:Name="monDessin2">
<Canvas.RenderTransform >
<MatrixTransform/>
</Canvas.RenderTransform>
</Canvas>
</Grid>
Code behind
public AfficheGraphiquePiece(Repere rep)
{
InitializeComponent();
ClassGraphique monGraphe = new ClassGraphique(monDessin);
ClassGraphique monGraphe2 = new ClassGraphique(monDessin2);
MakeMyDrawing();
double screenWidth = System.Windows.SystemParameters.PrimaryScreenWidth;
double screenHeight = System.Windows.SystemParameters.PrimaryScreenHeight;
double ech_x = screenWidth/ monDessin.Width ;
double ech_y = screenHeight/ monDessin.Height;
double ech = Math.Min(ech_x, ech_y)*0.9;
this.ech_full = ech;
this.echelleNow = this.ech_full;
MatrixTransform maTrans =(MatrixTransform)monDessin.RenderTransform;
var mat = maTrans.Matrix;
mat.ScaleAt(ech, -ech, 0.1* screenWidth, (screenHeight-monDessin.Height*ech)/2-0.1*screenHeight);
MatrixTransform maTrans2 = (MatrixTransform)monDessin2.RenderTransform;
var mat2 = maTrans2.Matrix;
mat2.ScaleAt(ech, ech, 0.1 * screenWidth, screenHeight*ech-((screenHeight - monDessin.Height * ech) / 2 - 0.1 * screenHeight));
maTrans.Matrix = mat;
maTrans2.Matrix = mat2;
}
private void monDessin_MouseWheel(object sender, MouseWheelEventArgs e)
{
try
{
var position = e.GetPosition(monDessin);
MatrixTransform transform = (MatrixTransform)monDessin.RenderTransform;
MatrixTransform transform2 = (MatrixTransform)monDessin2.RenderTransform;
var matrix = transform.Matrix;
var matrix2 = transform2.Matrix;
var scale = e.Delta >= 0 ? 1.1 : (1.0 / 1.1);
this.echelleNow *= scale;
matrix.ScaleAtPrepend(scale, scale, position.X, position.Y);
matrix2.ScaleAtPrepend(scale, scale, position.X,monDessin.Height-position.Y);
monDessin.RenderTransform = new MatrixTransform(matrix);
monDessin2.RenderTransform = new MatrixTransform(matrix2);
}
catch { }
}
Here is a very basic example for zooming and panning a Canvas with fixed initial size. The MatrixTransform in the RenderTransform of the inner Canvas provides the necessary transformations, while the outer Canvas handles mouse input and sets an initial scaling.
<Canvas Background="Transparent"
SizeChanged="ViewportSizeChanged"
MouseLeftButtonDown="ViewportMouseLeftButtonDown"
MouseLeftButtonUp="ViewportMouseLeftButtonUp"
MouseMove="ViewportMouseMove"
MouseWheel="ViewportMouseWheel">
<Canvas x:Name="canvas" Width="1000" Height="600">
<Canvas.RenderTransform>
<MatrixTransform x:Name="transform"/>
</Canvas.RenderTransform>
<Ellipse Fill="Red" Width="100" Height="100" Canvas.Left="100" Canvas.Top="100"/>
<Ellipse Fill="Green" Width="100" Height="100" Canvas.Right="100" Canvas.Bottom="100"/>
</Canvas>
</Canvas>
Code behind:
private Point? mousePos;
private void ViewportSizeChanged(object sender, SizeChangedEventArgs e)
{
((MatrixTransform)canvas.RenderTransform).Matrix = new Matrix(
e.NewSize.Width / canvas.ActualWidth,
0, 0,
e.NewSize.Height / canvas.ActualHeight,
0, 0);
}
private void ViewportMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var viewport = (UIElement)sender;
viewport.CaptureMouse();
mousePos = e.GetPosition(viewport);
}
private void ViewportMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
((UIElement)sender).ReleaseMouseCapture();
mousePos = null;
}
private void ViewportMouseMove(object sender, MouseEventArgs e)
{
if (mousePos.HasValue)
{
var pos = e.GetPosition((UIElement)sender);
var matrix = transform.Matrix;
matrix.Translate(pos.X - mousePos.Value.X, pos.Y - mousePos.Value.Y);
transform.Matrix = matrix;
mousePos = pos;
}
}
private void ViewportMouseWheel(object sender, MouseWheelEventArgs e)
{
var pos = e.GetPosition((UIElement)sender);
var matrix = transform.Matrix;
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
matrix.ScaleAt(scale, scale, pos.X, pos.Y);
transform.Matrix = matrix;
}
I'm currently working on a C# WPF application which contains a fullscreen Grid with controls that are dynamically added to it at runtime. I have code in place to allow the user to move these controls across the Grid with mouse events. What I now want to do is allow the user to also resize the controls (while keeping the aspect ratio), also at runtime. I have seen various tutorials describing how to do this using a Canvas (and Thumb controls), but none on a Grid. As I cannot use a Canvas in my application, is there an efficient way to implement this on a grid? To give you an idea of what my code looks like, I placed my mouse events below:
[Edited below]
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
//Orientation variables:
public static Point _anchorPoint;
public static Point _currentPoint;
private static double _originalTop;
private static double _originalLeft;
private static Point _startPoint;
private static bool _isDown = false;
private static bool _isInDrag = false;
private static bool _isDragging = false;
public static UIElement selectedElement = null;
public static bool elementIsSelected = false;
public static Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform>();
public static AdornerLayer aLayer;
public MainWindow()
{
InitializeComponent();
}
//Control events:
public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) //HUD element left mouse button up
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
_isInDrag = false;
e.Handled = true;
aLayer = AdornerLayer.GetAdornerLayer(selectedElement);
aLayer.Add(new ResizingAdorner(selectedElement));
}
}
public static void HUD_MouseDown(object sender, MouseButtonEventArgs e)
{
if (elementIsSelected)
{
elementIsSelected = false;
_isDown = false;
if (selectedElement != null)
{
aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
selectedElement = null;
}
}
if (e.Source != mw.PACSGrid)
{
_isDown = true;
_startPoint = e.GetPosition(mw.PACSGrid);
selectedElement = e.Source as UIElement;
_originalLeft = VisualWorker.GetLeft(selectedElement);
_originalTop = VisualWorker.GetTop(selectedElement);
aLayer = AdornerLayer.GetAdornerLayer(selectedElement);
aLayer.Add(new ResizingAdorner(selectedElement));
elementIsSelected = true;
e.Handled = true;
}
}
public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) //HUD element left mouse button down
{
if (elementIsSelected)
{
aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
selectedElement = sender as UIElement;
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
_isInDrag = true;
e.Handled = true;
}
}
public static void Control_MouseMove(object sender, MouseEventArgs e) //Drag & drop HUD element
{
if (_isInDrag) // The user is currently dragging the HUD element...
{
_currentPoint = e.GetPosition(null);
TranslateTransform tt = new TranslateTransform();
bool isMoved = false;
if (PointDict.ContainsKey(sender))
{
tt = PointDict[sender];
isMoved = true;
}
tt.X += _currentPoint.X - _anchorPoint.X;
tt.Y += (_currentPoint.Y - _anchorPoint.Y);
_anchorPoint = _currentPoint;
(sender as UIElement).RenderTransform = tt;
if (isMoved)
{
PointDict.Remove(sender);
}
PointDict.Add(sender, tt);
}
}
}
// Adorner Class:
public class ResizingAdorner : Adorner
{
Thumb topLeft, topRight, bottomLeft, bottomRight;
// To store and manage the adorner's visual children.
VisualCollection visualChildren;
// Initialize the ResizingAdorner.
public ResizingAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Call a helper method to initialize the Thumbs
// with a customized cursors.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);
// Add handlers for resizing.
bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
topRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
}
// Handler for resizing from the bottom-right.
void HandleBottomRight(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
}
// Handler for resizing from the top-right.
void HandleTopRight(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
//adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double height_old = adornedElement.Height;
double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double top_old = VisualWorker.GetTop(adornedElement);
//double top_old = Canvas.GetTop(adornedElement);
adornedElement.Height = height_new;
//Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
VisualWorker.SetTop(adornedElement, top_old - (height_new - height_old));
}
// Handler for resizing from the top-left.
void HandleTopLeft(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
//adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
//adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double width_old = adornedElement.Width;
double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
double left_old = VisualWorker.GetLeft(adornedElement);
//double left_old = Canvas.GetLeft(adornedElement);
adornedElement.Width = width_new;
VisualWorker.SetLeft(adornedElement, left_old - (width_new - width_old));
double height_old = adornedElement.Height;
double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double top_old = VisualWorker.GetTop(adornedElement);
//double top_old = Canvas.GetTop(adornedElement);
adornedElement.Height = height_new;
//Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
VisualWorker.SetTop(adornedElement, top_old - (height_new - height_old));
}
// Handler for resizing from the bottom-left.
void HandleBottomLeft(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
//adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
double width_old = adornedElement.Width;
double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
double left_old = VisualWorker.GetLeft(adornedElement);
//double left_old = Canvas.GetLeft(adornedElement);
adornedElement.Width = width_new;
//Canvas.SetLeft(adornedElement, left_old - (width_new - width_old));
VisualWorker.SetLeft(adornedElement, left_old - (width_new - width_old));
}
// Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{
// desiredWidth and desiredHeight are the width and height of the element that's being adorned.
// These will be used to place the ResizingAdorner at the corners of the adorned element.
double desiredWidth = AdornedElement.DesiredSize.Width;
double desiredHeight = AdornedElement.DesiredSize.Height;
// adornerWidth & adornerHeight are used for placement as well.
double adornerWidth = this.DesiredSize.Width;
double adornerHeight = this.DesiredSize.Height;
topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
topRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
bottomLeft.Arrange(new Rect(-adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
bottomRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
// Return the final size.
return finalSize;
}
// Helper method to instantiate the corner Thumbs, set the Cursor property,
// set some appearance properties, and add the elements to the visual tree.
void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
{
if (cornerThumb != null) return;
cornerThumb = new Thumb();
// Set some arbitrary visual characteristics.
cornerThumb.Cursor = customizedCursor;
cornerThumb.Height = cornerThumb.Width = 10;
cornerThumb.Opacity = 1;
cornerThumb.Background = new ImageBrush(new BitmapImage(new Uri(#"pack://application:,,,/Images/Thumb 1.jpg")));
visualChildren.Add(cornerThumb);
}
// This method ensures that the Widths and Heights are initialized. Sizing to content produces
// Width and Height values of Double.NaN. Because this Adorner explicitly resizes, the Width and Height
// need to be set first. It also sets the maximum size of the adorned element.
void EnforceSize(FrameworkElement adornedElement)
{
if (adornedElement.Width.Equals(Double.NaN))
adornedElement.Width = adornedElement.DesiredSize.Width;
if (adornedElement.Height.Equals(Double.NaN))
adornedElement.Height = adornedElement.DesiredSize.Height;
FrameworkElement parent = adornedElement.Parent as FrameworkElement;
if (parent != null)
{
adornedElement.MaxHeight = parent.ActualHeight;
adornedElement.MaxWidth = parent.ActualWidth;
}
}
// Override the VisualChildrenCount and GetVisualChild properties to interface with
// the adorner's visual collection.
protected override int VisualChildrenCount { get { return visualChildren.Count; } }
protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
}
// Canvas alternative class:
public class VisualWorker
{
public static void SetTop(UIElement uie, double top)
{
var frame = uie as FrameworkElement;
frame.Margin = new Thickness(frame.Margin.Left, top, frame.Margin.Right, frame.Margin.Bottom);
}
public static void SetLeft(UIElement uie, double left)
{
var frame = uie as FrameworkElement;
frame.Margin = new Thickness(left, frame.Margin.Top, frame.Margin.Right, frame.Margin.Bottom);
}
public static double GetTop(UIElement uie)
{
return (uie as FrameworkElement).Margin.Top;
}
public static double GetLeft(UIElement uie)
{
return (uie as FrameworkElement).Margin.Left;
}
}
MainWindow.xaml (example):
<Window x:Name="MW" x:Class="MyProgram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyProgram"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None" MouseLeave="HUD_MouseLeave">
<Grid x:Name="MyGrid MouseDown="HUD_MouseDown" />
<Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
<TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
Edit: I have found that using TranslateTransform does not change a control's margin. I need the margin to change appropriately in order for this to work.
Edit 2: Now I have added [modified] code for a resizing adorner (found here). It almost actually works. The problem is that I'm experiencing odd behavior with the adorners. On one of my controls (in the top left corner), all 4 appear in their appropriate corners. But the rest of my controls only get 1-2 adorners and they're not spaced correctly. The behavior with those is stranger as well. Yes, I realize this is a lot of code but I'd suspect the problem would be in the ResizingAdorner class.
Edit 3: Added 'boilerplate' code for those who want to copy&paste. It should compile with no problems. Let me know if you have any issues.
Edit 4 (1/10/2018): Still no good answer. It seems that the Thumb controls on the adorner only align themselves correctly if the control's Margin is 0,0. When moved from that position, the adorners space out from the element.
Edit 5 (1/15/2018): The adorner class was originally designed for a Canvas and running it on a Grid may contribute to the problem. My best guess is that the ArrangeOverride method was messed up because of it (that's where the thumbs are placed on their UIElement).
Resize:
Maybe this is a starter...
XAML:
<Grid x:Name="Content">
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Background="Blue" Width="20" Height="20" x:Name="BorderToResize"/>
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Background="Red" Width="10" Height="10" MouseLeftButtonDown="OnLeftMouseButtonDown" MouseLeftButtonUp="OnLeftMouseButtonUp" MouseMove="OnMouseMove" x:Name="BorderThumb">
<Border.RenderTransform>
<TranslateTransform X="15" Y="15" />
</Border.RenderTransform>
</Border>
Code Behind:
private void OnLeftMouseButtonDown(object sender, MouseButtonEventArgs e) {
(sender as UIElement).CaptureMouse();
}
private void OnLeftMouseButtonUp(object sender, MouseButtonEventArgs e) {
(sender as UIElement).ReleaseMouseCapture();
}
private void OnMouseMove(object sender, MouseEventArgs e) {
if ((sender as UIElement).IsMouseCaptureWithin) {
var pos = e.GetPosition(Content);
BorderThumb.RenderTransform = new TranslateTransform(pos.X, pos.Y);
BorderToResize.Height = pos.Y;
BorderToResize.Width = pos.X;
}
}
Today, after some trial and error, I was able to figure out how to fix the Adorner bug. As I posted, the corner Thumbs that are used to resize the elements weren't aligning to the controls properly. The original code was intended for use on a Canvas rather than the Grid container I am using. The fault was in some double values in the ArrangeOverride method (which arranges the Thumbs). The original code was:
double desiredWidth = AdornedElement.DesiredSize.Width;
double desiredHeight = AdornedElement.DesiredSize.Height;
// adornerWidth & adornerHeight are used for placement as well.
double adornerWidth = this.DesiredSize.Width;
double adornerHeight = this.DesiredSize.Height;
//Arrange method calls below....
...which I modified into:
double desiredWidth = (AdornedElement as FrameworkElement).ActualWidth;
double desiredHeight = (AdornedElement as FrameworkElement).ActualHeight;
// adornerWidth & adornerHeight are used for placement as well.
double adornerWidth = (AdornedElement as FrameworkElement).Width;
double adornerHeight = (AdornedElement as FrameworkElement).Height;
//Arrange the thumbs:
topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
topRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
bottomLeft.Arrange(new Rect(-adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
bottomRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
It seems the desiredWidth and desiredHeight double variables specifically used to arrange the Thumbs weren't providing the actual dimensions for the control. Using the code with this change is functional, but still quite glitchy for the top left, top right, and bottom left adorners. That's something I believe I can tweak myself. Thanks to all those who provided feedback.
Link to original (canvas) code
I'm fairly new to C# and programming in general.
I'm building a dummy project to understand how drag and drop works. At first, I begun moving around rectangles, limiting their movements inside the canvas, and they moved just fine. After that, I tried to make it a bit more complex by adding PointerEntered/PointerExited and replacing the rectangles with images. Re-sizing the images when the events PointerEntered/PointerExited occur works, but when I try to drag my images nothing happens. I tried several things I've seen around SO and msdn but it still fails.
Here is my code:
MainPage.xaml
<Canvas x:Name="MyCanvas" Width="800" Height="600">
<Canvas.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="#FF5E8B83" Offset="1"/>
</LinearGradientBrush>
</Canvas.Background>
<TextBlock x:Name="someTB" FontSize="30" Canvas.Left="262" Canvas.Top="25" Height="28" Width="171"/>
<Image x:Name="cauldron" Source="Assets/cauldron-md.png" AllowDrop="True" PointerExited="draggableItem_PointerExited" PointerEntered="draggableItem_PointerEntered" Width="210" Height="184" Canvas.Left="455" Canvas.Top="159"/>
<Image x:Name="antidote" Source="Assets/alchemy-green-potion-no-label.png" ManipulationDelta="dragableItem_ManipulationDelta" PointerExited="draggableItem_PointerExited" PointerEntered="draggableItem_PointerEntered" Width="45" Height="89" Canvas.Left="72" Canvas.Top="263"/>
<Image x:Name="poison" Source="Assets/alchemy-tree-poison-md-standing.png" ManipulationDelta="dragableItem_ManipulationDelta" PointerExited="draggableItem_PointerExited" PointerEntered="draggableItem_PointerEntered" Width="45" Height="89" Canvas.Left="202" Canvas.Top="159"/>
</Canvas>
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
double originalWidth;
double originalHeight;
public MainPage()
{
this.InitializeComponent();
// Add clipping area to the canvas
MyCanvas.Clip = new RectangleGeometry();
MyCanvas.Clip.Rect = new Rect(0, 0, MyCanvas.ActualWidth, MyCanvas.ActualHeight);
}
private Boolean DetectCollisions(FrameworkElement rect1, FrameworkElement rect2)
{
var r1 = new Rect(Canvas.GetLeft(rect1), Canvas.GetTop(rect1), rect1.ActualWidth, rect1.ActualHeight);
var r2 = new Rect(Canvas.GetLeft(rect2), Canvas.GetTop(rect2), rect2.ActualWidth, rect2.ActualHeight);
r1.Intersect(r2);
if (r1 != Rect.Empty)
{
return true;
}
return false;
}
private void dragableItem_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
{
Image dragableItem = sender as Image;
dragableItem.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
dragableItem.RenderTransform = new CompositeTransform();
var transform = (CompositeTransform)dragableItem.RenderTransform;
var newPosX = Canvas.GetLeft(dragableItem) + transform.TranslateX + e.Delta.Translation.X;
var newPosY = Canvas.GetTop(dragableItem) + transform.TranslateY + e.Delta.Translation.Y;
if (!isBoundary(newPosX, MyCanvas.ActualWidth - dragableItem.ActualWidth, 0))
Canvas.SetLeft(dragableItem, newPosX);
if (!isBoundary(newPosY, MyCanvas.ActualHeight - dragableItem.ActualHeight, 0))
Canvas.SetTop(dragableItem, newPosY);
if (DetectCollisions(dragableItem, cauldron) == true)
{
cauldron.Source = new BitmapImage(new Uri(#"Assets/cauldron-md-autoTone.png"));
}
}
bool isBoundary(double value, double max, double min)
{
return value > max ? true : value < min ? true : false;
}
private void draggableItem_PointerEntered(object sender, PointerRoutedEventArgs e)
{
Image thatPic = sender as Image;
if (thatPic != null)
{
originalWidth = thatPic.ActualWidth;
originalHeight = thatPic.ActualHeight;
thatPic.Width = thatPic.ActualWidth + 10;
thatPic.Height = thatPic.ActualHeight + 10;
}
}
private void draggableItem_PointerExited(object sender, PointerRoutedEventArgs e)
{
Image thatPic = sender as Image;
if (thatPic != null)
{
thatPic.Width = originalWidth;
thatPic.Height = originalHeight;
}
}
}
1) You need to add ManipulationMode="All" to the Image controls.
2) The Image "cauldron" is missing the ManipulationDelta event handler assignment.
Using the following XAML:
<Grid x:Name="grid" Background="LightBlue" ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Viewbox x:Name="imgViewbox" >
<InkCanvas Grid.Row="0" Name="inkCanvas" Background="Red" >
<Image Source="Images/pic.png" HorizontalAlignment="Left" x:Name="imgObject" VerticalAlignment="Top" />
<Label>Testing</Label>
</InkCanvas>
</Viewbox>
</Grid>
I am trying to rotate around the center of the image and also use the wheel mouse to zoom. I have set up this transform group and event:
public MainWindow() {
InitializeComponent();
DataContext = new MainWindowViewModel();
transformGroup = new TransformGroup();
scaleTransform = new ScaleTransform();
rotateTransform = new RotateTransform();
translateTransform = new TranslateTransform();
transformGroup.Children.Add(rotateTransform);
transformGroup.Children.Add(scaleTransform);
transformGroup.Children.Add(translateTransform);
imgViewbox.RenderTransform = transformGroup;
imgViewbox.MouseWheel += ImageViewboxMouseWheel;
}
Rotate is simple:
void Rotate(object sender, RoutedEventArgs e) {
//imgViewbox.RenderTransformOrigin = new Point(0.5,0.5);
rotateTransform.Angle += 90;
}
but zoom is doing all sorts of weird stuff jumping around the screen. The code for zoom is here:
void ImageViewboxMouseWheel(object sender, MouseWheelEventArgs e) {
//imgViewbox.RenderTransformOrigin = new Point(0, 0);
double zoomFactor = DefaultZoomFactor;
if (e.Delta <= 0) zoomFactor = 1.0 / DefaultZoomFactor;
// DoZoom requires both the logical and physical location of the mouse pointer
var physicalPoint = e.GetPosition(imgViewbox);
if (transformGroup.Inverse != null) {
DoZoom(zoomFactor, transformGroup.Inverse.Transform(physicalPoint), physicalPoint);
}
else {
throw new ArgumentException("Missing Inverse");
}
//Set the center point of the ScaleTransform object to the cursor location.
scaleTransform.CenterX = e.GetPosition(imgViewbox).X;
scaleTransform.CenterY = e.GetPosition(imgViewbox).Y;
Debug.WriteLine(string.Format("IVMW Center {0},{1}", scaleTransform.CenterX, scaleTransform.CenterY));
}
public void DoZoom(double deltaZoom, Point mousePosition, Point physicalPosition) {
double currentZoom = scaleTransform.ScaleX;
currentZoom *= deltaZoom;
translateTransform.X = -1*(mousePosition.X*currentZoom - physicalPosition.X);
translateTransform.Y = -1*(mousePosition.X*currentZoom - physicalPosition.Y);
scaleTransform.ScaleX = currentZoom;
scaleTransform.ScaleY = currentZoom;
}
I have removed as much as I can, animations and such. Hopefully leaving only the key parts. I believe that the major problem is the scaleTransform.Center[X|Y] as the numbers that are being returned are all over the quadrant even when I try to click exactly in the same location. The RenderTransformOrigin doesn't seem to make any difference with the Center position but I am aware that I need it to rotate around center.
What am I doing wrong?
You need to offset the jump you get from changing the ScaleTranform's CenterX/Y in the TranslateTransform, here is a snippet from a pan & zoom control i wrote:
private void This_MouseWheel(object sender, MouseWheelEventArgs e)
{
if (IsZoomEnabled)
{
Point cursorPos = e.GetPosition(this);
Point newCenter = _scaleT.Inverse.Transform(_translateT.Inverse.Transform(cursorPos));
Point oldCenter = new Point(_scaleT.CenterX, _scaleT.CenterY);
Vector oldToNewCenter = newCenter - oldCenter;
_scaleT.CenterX = newCenter.X;
_scaleT.CenterY = newCenter.Y;
_translateT.X += oldToNewCenter.X * (_scaleT.ScaleX - 1.0);
_translateT.Y += oldToNewCenter.Y * (_scaleT.ScaleY - 1.0);
...
Hopefully you can adapt this to your code. Where the new center is calculated you might need to take your RotateTransform into account.