// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) 2020 OxyPlot contributors // // // The tracker control. // // -------------------------------------------------------------------------------------------------------------------- namespace OxyPlot.Wpf { using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; /// /// The tracker control. /// public class TrackerControl : ContentControl { /// /// Identifies the dependency property. /// public static readonly DependencyProperty HorizontalLineVisibilityProperty = DependencyProperty.Register( nameof(HorizontalLineVisibility), typeof(Visibility), typeof(TrackerControl), new PropertyMetadata(Visibility.Visible)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty VerticalLineVisibilityProperty = DependencyProperty.Register( nameof(VerticalLineVisibility), typeof(Visibility), typeof(TrackerControl), new PropertyMetadata(Visibility.Visible)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty LineThicknessProperty = DependencyProperty.Register( nameof(LineThickness), typeof(double), typeof(TrackerControl), new PropertyMetadata(1.0)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty LineStrokeProperty = DependencyProperty.Register( nameof(LineStroke), typeof(Brush), typeof(TrackerControl), new PropertyMetadata(null)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty LineExtentsProperty = DependencyProperty.Register( nameof(LineExtents), typeof(OxyRect), typeof(TrackerControl), new PropertyMetadata(new OxyRect())); /// /// Identifies the dependency property. /// public static readonly DependencyProperty LineDashArrayProperty = DependencyProperty.Register( nameof(LineDashArray), typeof(DoubleCollection), typeof(TrackerControl), new PropertyMetadata(null)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty BorderEdgeModeProperty = DependencyProperty.Register( nameof(BorderEdgeMode), typeof(EdgeMode), typeof(TrackerControl)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty ShowPointerProperty = DependencyProperty.Register( nameof(ShowPointer), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register( nameof(CornerRadius), typeof(double), typeof(TrackerControl), new PropertyMetadata(0.0)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty DistanceProperty = DependencyProperty.Register( nameof(Distance), typeof(double), typeof(TrackerControl), new PropertyMetadata(7.0)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty CanCenterHorizontallyProperty = DependencyProperty.Register( nameof(CanCenterHorizontally), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty CanCenterVerticallyProperty = DependencyProperty.Register( nameof(CanCenterVertically), typeof(bool), typeof(TrackerControl), new PropertyMetadata(true)); /// /// Identifies the dependency property. /// public static readonly DependencyProperty PositionProperty = DependencyProperty.Register( nameof(Position), typeof(ScreenPoint), typeof(TrackerControl), new PropertyMetadata(new ScreenPoint(), PositionChanged)); /// /// The path part string. /// private const string PartPath = "PART_Path"; /// /// The content part string. /// private const string PartContent = "PART_Content"; /// /// The content container part string. /// private const string PartContentcontainer = "PART_ContentContainer"; /// /// The horizontal line part string. /// private const string PartHorizontalline = "PART_HorizontalLine"; /// /// The vertical line part string. /// private const string PartVerticalline = "PART_VerticalLine"; /// /// The content. /// private ContentPresenter content; /// /// The horizontal line. /// private Line horizontalLine; /// /// The path. /// private Path path; /// /// The content container. /// private Grid contentContainer; /// /// The vertical line. /// private Line verticalLine; /// /// Initializes static members of the class. /// static TrackerControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(TrackerControl), new FrameworkPropertyMetadata(typeof(TrackerControl))); } /// /// Gets or sets BorderEdgeMode. /// public EdgeMode BorderEdgeMode { get => (EdgeMode)this.GetValue(BorderEdgeModeProperty); set => this.SetValue(BorderEdgeModeProperty, value); } /// /// Gets or sets HorizontalLineVisibility. /// public Visibility HorizontalLineVisibility { get => (Visibility)this.GetValue(HorizontalLineVisibilityProperty); set => this.SetValue(HorizontalLineVisibilityProperty, value); } /// /// Gets or sets VerticalLineVisibility. /// public Visibility VerticalLineVisibility { get => (Visibility)this.GetValue(VerticalLineVisibilityProperty); set => this.SetValue(VerticalLineVisibilityProperty, value); } /// /// Gets or sets LineThickness. /// public double LineThickness { get => (double)this.GetValue(LineThicknessProperty); set => this.SetValue(LineThicknessProperty, value); } /// /// Gets or sets LineStroke. /// public Brush LineStroke { get => (Brush)this.GetValue(LineStrokeProperty); set => this.SetValue(LineStrokeProperty, value); } /// /// Gets or sets LineExtents. /// public OxyRect LineExtents { get => (OxyRect)this.GetValue(LineExtentsProperty); set => this.SetValue(LineExtentsProperty, value); } /// /// Gets or sets LineDashArray. /// public DoubleCollection LineDashArray { get => (DoubleCollection)this.GetValue(LineDashArrayProperty); set => this.SetValue(LineDashArrayProperty, value); } /// /// Gets or sets a value indicating whether to show a 'pointer' on the border. /// public bool ShowPointer { get => (bool)this.GetValue(ShowPointerProperty); set => this.SetValue(ShowPointerProperty, value); } /// /// Gets or sets the corner radius (only used when ShowPoint=false). /// public double CornerRadius { get => (double)this.GetValue(CornerRadiusProperty); set => this.SetValue(CornerRadiusProperty, value); } /// /// Gets or sets the distance of the content container from the trackers Position. /// public double Distance { get => (double)this.GetValue(DistanceProperty); set => this.SetValue(DistanceProperty, value); } /// /// Gets or sets a value indicating whether the tracker can center its content box horizontally. /// public bool CanCenterHorizontally { get => (bool)this.GetValue(CanCenterHorizontallyProperty); set => this.SetValue(CanCenterHorizontallyProperty, value); } /// /// Gets or sets a value indicating whether the tracker can center its content box vertically. /// public bool CanCenterVertically { get => (bool)this.GetValue(CanCenterVerticallyProperty); set => this.SetValue(CanCenterVerticallyProperty, value); } /// /// Gets or sets Position of the tracker. /// public ScreenPoint Position { get => (ScreenPoint)this.GetValue(PositionProperty); set => this.SetValue(PositionProperty, value); } /// /// When overridden in a derived class, is invoked whenever application code or internal processes call . /// public override void OnApplyTemplate() { base.OnApplyTemplate(); this.path = this.GetTemplateChild(PartPath) as Path; this.content = this.GetTemplateChild(PartContent) as ContentPresenter; this.contentContainer = this.GetTemplateChild(PartContentcontainer) as Grid; this.horizontalLine = this.GetTemplateChild(PartHorizontalline) as Line; this.verticalLine = this.GetTemplateChild(PartVerticalline) as Line; if (this.contentContainer == null) { throw new InvalidOperationException($"The TrackerControl template must contain a content container with name +'{PartContentcontainer}'"); } if (this.path == null) { throw new InvalidOperationException($"The TrackerControl template must contain a Path with name +'{PartPath}'"); } if (this.content == null) { throw new InvalidOperationException($"The TrackerControl template must contain a ContentPresenter with name +'{PartContent}'"); } this.UpdatePositionAndBorder(); } /// /// Called when the position is changed. /// /// The sender. /// The instance containing the event data. private static void PositionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { ((TrackerControl)sender).OnPositionChanged(e); } /// /// Called when the position is changed. /// /// The dependency property changed event args. // ReSharper disable once UnusedParameter.Local private void OnPositionChanged(DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs) { this.UpdatePositionAndBorder(); } /// /// Update the position and border of the tracker. /// private void UpdatePositionAndBorder() { if (this.contentContainer == null) { return; } Canvas.SetLeft(this.contentContainer, this.Position.X); Canvas.SetTop(this.contentContainer, this.Position.Y); FrameworkElement parent = this; while (!(parent is Canvas) && parent != null) { parent = VisualTreeHelper.GetParent(parent) as FrameworkElement; } if (parent == null) { return; } // throw new InvalidOperationException("The TrackerControl must have a Canvas parent."); double canvasWidth = parent.ActualWidth; double canvasHeight = parent.ActualHeight; this.content.Measure(new Size(canvasWidth, canvasHeight)); this.content.Arrange(new Rect(0, 0, this.content.DesiredSize.Width, this.content.DesiredSize.Height)); double contentWidth = this.content.DesiredSize.Width; double contentHeight = this.content.DesiredSize.Height; // Minimum allowed margins around the tracker const double MarginLimit = 10; var ha = HorizontalAlignment.Center; if (this.CanCenterHorizontally) { if (this.Position.X - (contentWidth / 2) < MarginLimit) { ha = HorizontalAlignment.Left; } if (this.Position.X + (contentWidth / 2) > canvasWidth - MarginLimit) { ha = HorizontalAlignment.Right; } } else { ha = this.Position.X < canvasWidth / 2 ? HorizontalAlignment.Left : HorizontalAlignment.Right; } var va = VerticalAlignment.Center; if (this.CanCenterVertically) { if (this.Position.Y - (contentHeight / 2) < MarginLimit) { va = VerticalAlignment.Top; } if (ha == HorizontalAlignment.Center) { va = VerticalAlignment.Bottom; if (this.Position.Y - contentHeight < MarginLimit) { va = VerticalAlignment.Top; } } if (va == VerticalAlignment.Center && this.Position.Y + (contentHeight / 2) > canvasHeight - MarginLimit) { va = VerticalAlignment.Bottom; } if (va == VerticalAlignment.Top && this.Position.Y + contentHeight > canvasHeight - MarginLimit) { va = VerticalAlignment.Bottom; } } else { va = this.Position.Y < canvasHeight / 2 ? VerticalAlignment.Top : VerticalAlignment.Bottom; } double dx = ha == HorizontalAlignment.Center ? -0.5 : ha == HorizontalAlignment.Left ? 0 : -1; double dy = va == VerticalAlignment.Center ? -0.5 : va == VerticalAlignment.Top ? 0 : -1; this.path.Data = this.ShowPointer ? this.CreatePointerBorderGeometry(ha, va, contentWidth, contentHeight, out Thickness margin) : this.CreateBorderGeometry(ha, va, contentWidth, contentHeight, out margin); this.content.Margin = margin; this.contentContainer.Measure(new Size(canvasWidth, canvasHeight)); var contentSize = this.contentContainer.DesiredSize; this.contentContainer.RenderTransform = new TranslateTransform { X = dx * contentSize.Width, Y = dy * contentSize.Height }; var pos = this.Position; if (this.horizontalLine != null) { if (this.LineExtents.Width > 0) { this.horizontalLine.X1 = this.LineExtents.Left; this.horizontalLine.X2 = this.LineExtents.Right; } else { this.horizontalLine.X1 = 0; this.horizontalLine.X2 = canvasWidth; } this.horizontalLine.Y1 = pos.Y; this.horizontalLine.Y2 = pos.Y; } if (this.verticalLine != null) { if (this.LineExtents.Width > 0) { this.verticalLine.Y1 = this.LineExtents.Top; this.verticalLine.Y2 = this.LineExtents.Bottom; } else { this.verticalLine.Y1 = 0; this.verticalLine.Y2 = canvasHeight; } this.verticalLine.X1 = pos.X; this.verticalLine.X2 = pos.X; } } /// /// Create the border geometry. /// /// The horizontal alignment. /// The vertical alignment. /// The width. /// The height. /// The margin. /// The border geometry. private Geometry CreateBorderGeometry( HorizontalAlignment ha, VerticalAlignment va, double width, double height, out Thickness margin) { double m = this.Distance; var rect = new Rect( ha == HorizontalAlignment.Left ? m : 0, va == VerticalAlignment.Top ? m : 0, width, height); margin = new Thickness( ha == HorizontalAlignment.Left ? m : 0, va == VerticalAlignment.Top ? m : 0, ha == HorizontalAlignment.Right ? m : 0, va == VerticalAlignment.Bottom ? m : 0); return new RectangleGeometry { Rect = rect, RadiusX = this.CornerRadius, RadiusY = this.CornerRadius }; } /// /// Create a border geometry with a 'pointer'. /// /// The horizontal alignment. /// The vertical alignment. /// The width. /// The height. /// The margin. /// The border geometry. private Geometry CreatePointerBorderGeometry( HorizontalAlignment ha, VerticalAlignment va, double width, double height, out Thickness margin) { Point[] points = null; double m = this.Distance; margin = new Thickness(); if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Bottom) { double x0 = 0; double x1 = width; double x2 = (x0 + x1) / 2; double y0 = 0; double y1 = height; margin = new Thickness(0, 0, 0, m); points = new[] { new Point(x0, y0), new Point(x1, y0), new Point(x1, y1), new Point(x2 + (m / 2), y1), new Point(x2, y1 + m), new Point(x2 - (m / 2), y1), new Point(x0, y1) }; } if (ha == HorizontalAlignment.Center && va == VerticalAlignment.Top) { double x0 = 0; double x1 = width; double x2 = (x0 + x1) / 2; double y0 = m; double y1 = m + height; margin = new Thickness(0, m, 0, 0); points = new[] { new Point(x0, y0), new Point(x2 - (m / 2), y0), new Point(x2, 0), new Point(x2 + (m / 2), y0), new Point(x1, y0), new Point(x1, y1), new Point(x0, y1) }; } if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Center) { double x0 = m; double x1 = m + width; double y0 = 0; double y1 = height; double y2 = (y0 + y1) / 2; margin = new Thickness(m, 0, 0, 0); points = new[] { new Point(0, y2), new Point(x0, y2 - (m / 2)), new Point(x0, y0), new Point(x1, y0), new Point(x1, y1), new Point(x0, y1), new Point(x0, y2 + (m / 2)) }; } if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Center) { double x0 = 0; double x1 = width; double y0 = 0; double y1 = height; double y2 = (y0 + y1) / 2; margin = new Thickness(0, 0, m, 0); points = new[] { new Point(x1 + m, y2), new Point(x1, y2 + (m / 2)), new Point(x1, y1), new Point(x0, y1), new Point(x0, y0), new Point(x1, y0), new Point(x1, y2 - (m / 2)) }; } if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Top) { m *= 0.67; double x0 = m; double x1 = m + width; double y0 = m; double y1 = m + height; margin = new Thickness(m, m, 0, 0); points = new[] { new Point(0, 0), new Point(m * 2, y0), new Point(x1, y0), new Point(x1, y1), new Point(x0, y1), new Point(x0, m * 2) }; } if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Top) { m *= 0.67; double x0 = 0; double x1 = width; double y0 = m; double y1 = m + height; margin = new Thickness(0, m, m, 0); points = new[] { new Point(x1 + m, 0), new Point(x1, y0 + m), new Point(x1, y1), new Point(x0, y1), new Point(x0, y0), new Point(x1 - m, y0) }; } if (ha == HorizontalAlignment.Left && va == VerticalAlignment.Bottom) { m *= 0.67; double x0 = m; double x1 = m + width; double y0 = 0; double y1 = height; margin = new Thickness(m, 0, 0, m); points = new[] { new Point(0, y1 + m), new Point(x0, y1 - m), new Point(x0, y0), new Point(x1, y0), new Point(x1, y1), new Point(x0 + m, y1) }; } if (ha == HorizontalAlignment.Right && va == VerticalAlignment.Bottom) { m *= 0.67; double x0 = 0; double x1 = width; double y0 = 0; double y1 = height; margin = new Thickness(0, 0, m, m); points = new[] { new Point(x1 + m, y1 + m), new Point(x1 - m, y1), new Point(x0, y1), new Point(x0, y0), new Point(x1, y0), new Point(x1, y1 - m) }; } if (points == null) { return null; } var pc = new PointCollection(points.Length); foreach (var p in points) { pc.Add(p); } var segments = new PathSegmentCollection { new PolyLineSegment { Points = pc } }; var pf = new PathFigure { StartPoint = points[0], Segments = segments, IsClosed = true }; return new PathGeometry { Figures = new PathFigureCollection { pf } }; } } }