File: System\Windows\Controls\Primitives\TickBar.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationFramework\PresentationFramework.csproj (PresentationFramework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
 
using System.ComponentModel;
using MS.Internal;
using System.Windows.Data;
using System.Windows.Media;
using MS.Internal.KnownBoxes;
 
 
// For typeconverter
 
namespace System.Windows.Controls.Primitives
{
    /// <summary>
    /// Enum which describes how to position the TickBar.
    /// </summary>
    public enum TickBarPlacement
    {
        /// <summary>
        /// Position this tick at the left of target element.
        /// </summary>
        Left,
 
        /// <summary>
        /// Position this tick at the top of target element.
        /// </summary>
        Top,
 
        /// <summary>
        /// Position this tick at the right of target element.
        /// </summary>
        Right,
 
        /// <summary>
        /// Position this tick at the bottom of target element.
        /// </summary>
        Bottom,
 
        // NOTE: if you add or remove any values in this enum, be sure to update TickBar.IsValidTickBarPlacement()
    };
 
    /// <summary>
    /// TickBar is an element that use for drawing Slider's Ticks.
    /// </summary>
    [Localizability(LocalizationCategory.None, Readability = Readability.Unreadable)] // cannot be read & localized as string    
    public class TickBar : FrameworkElement
    {
        //-------------------------------------------------------------------
        //
        //  Constructors
        //
        //-------------------------------------------------------------------
        #region Constructors
 
        static TickBar()
        {
            SnapsToDevicePixelsProperty.OverrideMetadata(typeof(TickBar), new FrameworkPropertyMetadata(true));
        }
 
        /// <summary>
        ///     Default constructor for TickBar class
        /// </summary>
        /// <remarks>
        ///     Automatic determination of current Dispatcher. Use alternative constructor
        ///     that accepts a Dispatcher for best performance.
        /// </remarks>
        public TickBar() : base()
        {
        }
 
        #endregion
 
        /// <summary>
        /// Fill property
        /// </summary>
        public static readonly DependencyProperty FillProperty
            = DependencyProperty.Register(
                "Fill",
                typeof(Brush),
                typeof(TickBar),
                new FrameworkPropertyMetadata(
                    (Brush)null,
                    FrameworkPropertyMetadataOptions.AffectsRender,
                    null,
                    null)
                );
 
        /// <summary>
        /// Fill property
        /// </summary>
        public Brush Fill
        {
            get
            {
                return (Brush)GetValue(FillProperty);
            }
            set
            {
                SetValue(FillProperty, value);
            }
        }
 
        /// <summary>
        ///     The DependencyProperty for the <see cref="Minimum"/> property.
        /// </summary>
        public static readonly DependencyProperty MinimumProperty =
                RangeBase.MinimumProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                0.0,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        ///     Logical position where the Minimum Tick will be drawn
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double Minimum
        {
            get { return (double) GetValue(MinimumProperty); }
            set { SetValue(MinimumProperty, value); }
        }
 
        /// <summary>
        ///     The DependencyProperty for the <see cref="Maximum"/>  property.
        /// </summary>
        public static readonly DependencyProperty MaximumProperty =
                RangeBase.MaximumProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                100.0,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        ///     Logical position where the Maximum Tick will be drawn
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double Maximum
        {
            get { return (double) GetValue(MaximumProperty); }
            set { SetValue(MaximumProperty, value); }
        }
 
        /// <summary>
        ///     The DependencyProperty for the <see cref="SelectionStart"/>  property.
        /// </summary>
        public static readonly DependencyProperty SelectionStartProperty =
                Slider.SelectionStartProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                -1.0d,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        ///     Logical position where the SelectionStart Tick will be drawn
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double SelectionStart
        {
            get { return (double) GetValue(SelectionStartProperty); }
            set { SetValue(SelectionStartProperty, value); }
        }
 
        /// <summary>
        ///     The DependencyProperty for the <see cref="SelectionEnd"/>  property.
        /// </summary>
        public static readonly DependencyProperty SelectionEndProperty =
                Slider.SelectionEndProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                -1.0d,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        ///     Logical position where the SelectionEnd Tick will be drawn
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double SelectionEnd
        {
            get { return (double) GetValue(SelectionEndProperty); }
            set { SetValue(SelectionEndProperty, value); }
        }
 
        /// <summary>
        ///     The DependencyProperty for the <see cref="IsSelectionRangeEnabled"/>  property.
        /// </summary>
        public static readonly DependencyProperty IsSelectionRangeEnabledProperty =
                Slider.IsSelectionRangeEnabledProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                BooleanBoxes.FalseBox,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        ///     IsSelectionRangeEnabled specifies whether to draw SelectionStart Tick and SelectionEnd Tick or not.
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public bool IsSelectionRangeEnabled
        {
            get { return (bool) GetValue(IsSelectionRangeEnabledProperty); }
            set { SetValue(IsSelectionRangeEnabledProperty, BooleanBoxes.Box(value)); }
        }
 
        /// <summary>
        /// DependencyProperty for <see cref="TickFrequency" /> property.
        /// </summary>
        public static readonly DependencyProperty TickFrequencyProperty =
                Slider.TickFrequencyProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                1.0,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        /// TickFrequency property defines how the tick will be drawn.
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double TickFrequency
        {
            get { return (double) GetValue(TickFrequencyProperty); }
            set { SetValue(TickFrequencyProperty, value); }
        }
 
        /// <summary>
        /// DependencyProperty for <see cref="Ticks" /> property.
        /// </summary>
        public static readonly DependencyProperty TicksProperty =
                Slider.TicksProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                new FreezableDefaultValueFactory(DoubleCollection.Empty),
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        /// The Ticks property contains collection of value of type Double which
        /// are the logical positions use to draw the ticks.
        /// The property value is a <see cref="DoubleCollection" />.
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public DoubleCollection Ticks
        {
            get { return (DoubleCollection) GetValue(TicksProperty); }
            set { SetValue(TicksProperty, value); }
        }
 
        /// <summary>
        /// DependencyProperty for IsDirectionReversed property.
        /// </summary>
        public static readonly DependencyProperty IsDirectionReversedProperty =
                Slider.IsDirectionReversedProperty.AddOwner(
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                BooleanBoxes.FalseBox,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        /// The IsDirectionReversed property defines the direction of value incrementation.
        /// By default, if Tick's orientation is Horizontal, ticks will be drawn from left to right.
        /// (And, bottom to top for Vertical orientation).
        /// If IsDirectionReversed is 'true' the direction of the drawing will be in opposite direction.
        /// Ticks property contains collection of value of type Double which
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public bool IsDirectionReversed
        {
            get { return (bool) GetValue(IsDirectionReversedProperty); }
            set { SetValue(IsDirectionReversedProperty, BooleanBoxes.Box(value)); }
        }
 
        /// <summary>
        /// DependencyProperty for <see cref="Placement" /> property.
        /// </summary>
        public static readonly DependencyProperty PlacementProperty =
                DependencyProperty.Register(
                        "Placement",
                        typeof(TickBarPlacement),
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                TickBarPlacement.Top,
                                FrameworkPropertyMetadataOptions.AffectsRender),
                        new ValidateValueCallback(IsValidTickBarPlacement));
 
        /// <summary>
        /// Placement property specified how the Tick will be placed.
        /// This property affects the way ticks are drawn.
        /// This property has type of <see cref="TickBarPlacement" />.
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public TickBarPlacement Placement
        {
            get { return (TickBarPlacement) GetValue(PlacementProperty); }
            set { SetValue(PlacementProperty, value); }
        }
 
        private static bool IsValidTickBarPlacement(object o)
        {
            TickBarPlacement placement = (TickBarPlacement)o;
            return placement == TickBarPlacement.Left ||
                   placement == TickBarPlacement.Top ||
                   placement == TickBarPlacement.Right ||
                   placement == TickBarPlacement.Bottom;
        }
 
        /// <summary>
        /// DependencyProperty for ReservedSpace property.
        /// </summary>
        public static readonly DependencyProperty ReservedSpaceProperty =
                DependencyProperty.Register(
                        "ReservedSpace",
                        typeof(double),
                        typeof(TickBar),
                        new FrameworkPropertyMetadata(
                                0d,
                                FrameworkPropertyMetadataOptions.AffectsRender));
 
        /// <summary>
        /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or
        /// tob and bottom spacing (for vertical orienation).
        /// The space on both sides of TickBar is half of specified ReservedSpace.
        /// This property has type of <see cref="double" />.
        /// </summary>
        [Bindable(true), Category("Appearance")]
        public double ReservedSpace
        {
            get { return (double) GetValue(ReservedSpaceProperty); }
            set { SetValue(ReservedSpaceProperty, value); }
        }
 
        /// <summary>
        /// Draw ticks.
        /// Ticks can be draw in 8 diffrent ways depends on Placment property and IsDirectionReversed property.
        ///
        /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and
        /// SelectionStart and SelectionEnd are valid.
        ///
        /// The primary ticks (for Mininum and Maximum value) height will be 100% of TickBar's render size (use Width or Height
        /// depends on Placement property).
        ///
        /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size.
        ///
        /// Brush that use to fill ticks is specified by Shape.Fill property.
        ///
        /// Pen that use to draw ticks is specified by Shape.Pen property.
        /// </summary>
        protected override void OnRender(DrawingContext dc)
        {
            Size size = new Size(ActualWidth,ActualHeight);
            double range = Maximum - Minimum;
            double tickLen = 0.0d;  // Height for Primary Tick (for Mininum and Maximum value)
            double tickLen2;        // Height for Secondary Tick
            double logicalToPhysical = 1.0;
            double progression = 1.0d;
            Point startPoint = new Point(0d,0d);
            Point endPoint = new Point(0d, 0d);
 
            // Take Thumb size in to account
            double halfReservedSpace = ReservedSpace * 0.5;
 
            switch(Placement)
            {
                case TickBarPlacement.Top:
                    if (DoubleUtil.GreaterThanOrClose(ReservedSpace, size.Width))
                    {
                        return;
                    }
                    size.Width -= ReservedSpace;
                    tickLen = - size.Height;
                    startPoint = new Point(halfReservedSpace, size.Height);
                    endPoint = new Point(halfReservedSpace + size.Width, size.Height);
                    logicalToPhysical = size.Width / range;
                    progression = 1;
                    break;
 
                case TickBarPlacement.Bottom:
                    if (DoubleUtil.GreaterThanOrClose(ReservedSpace, size.Width))
                    {
                        return;
                    }
                    size.Width -= ReservedSpace;
                    tickLen = size.Height;
                    startPoint = new Point(halfReservedSpace, 0d);
                    endPoint = new Point(halfReservedSpace + size.Width, 0d);
                    logicalToPhysical = size.Width / range;
                    progression = 1;
                    break;
 
                case TickBarPlacement.Left:
                    if (DoubleUtil.GreaterThanOrClose(ReservedSpace, size.Height))
                    {
                        return;
                    }
                    size.Height -= ReservedSpace;
                    tickLen = -size.Width;
                    startPoint = new Point(size.Width, size.Height + halfReservedSpace);
                    endPoint = new Point(size.Width, halfReservedSpace);
                    logicalToPhysical = size.Height / range * -1;
                    progression = -1;
                    break;
 
                case TickBarPlacement.Right:
                    if (DoubleUtil.GreaterThanOrClose(ReservedSpace, size.Height))
                    {
                        return;
                    }
                    size.Height -= ReservedSpace;
                    tickLen = size.Width;
                    startPoint = new Point(0d, size.Height + halfReservedSpace);
                    endPoint = new Point(0d, halfReservedSpace);
                    logicalToPhysical = size.Height / range * -1;
                    progression = -1;
                    break;
            };
 
            tickLen2 = tickLen * 0.75;
 
            // Invert direciton of the ticks
            if (IsDirectionReversed)
            {
                progression = -progression;
                logicalToPhysical *= -1;
 
                // swap startPoint & endPoint
                Point pt = startPoint;
                startPoint = endPoint;
                endPoint = pt;
            }
 
            Pen pen = new Pen(Fill, 1.0d);
 
            bool snapsToDevicePixels = SnapsToDevicePixels;
            DoubleCollection xLines = snapsToDevicePixels ? new DoubleCollection() : null;
            DoubleCollection yLines = snapsToDevicePixels ? new DoubleCollection() : null;
 
            // Is it Vertical?
            if ((Placement == TickBarPlacement.Left) || (Placement == TickBarPlacement.Right))
            {
                // Reduce tick interval if it is more than would be visible on the screen
                double interval = TickFrequency;
                if (interval > 0.0)
                {
                    double minInterval = (Maximum - Minimum) / size.Height;
                    if (interval < minInterval)
                    {
                        interval = minInterval;
                    }
                }
 
                // Draw Min & Max tick
                dc.DrawLine(pen, startPoint, new Point(startPoint.X + tickLen, startPoint.Y));
                dc.DrawLine(pen, new Point(startPoint.X, endPoint.Y),
                                 new Point(startPoint.X + tickLen, endPoint.Y));
 
                if (snapsToDevicePixels)
                {
                    xLines.Add(startPoint.X);
                    yLines.Add(startPoint.Y - 0.5);
                    xLines.Add(startPoint.X + tickLen);
                    yLines.Add(endPoint.Y - 0.5);
                    xLines.Add(startPoint.X + tickLen2);
                }
 
                // This property is rarely set so let's try to avoid the GetValue
                // caching of the mutable default value
                DoubleCollection ticks = null;
                bool hasModifiers;
                if (GetValueSource(TicksProperty, null, out hasModifiers)
                    != BaseValueSourceInternal.Default || hasModifiers)
                {
                    ticks = Ticks;
                }
 
                // Draw ticks using specified Ticks collection
                if ((ticks != null) && (ticks.Count > 0))
                {
                    for (int i = 0; i < ticks.Count; i++)
                    {
                        if (DoubleUtil.LessThanOrClose(ticks[i],Minimum) || DoubleUtil.GreaterThanOrClose(ticks[i],Maximum))
                        {
                            continue;
                        }
 
                        double adjustedTick = ticks[i] - Minimum;
 
                        double y = adjustedTick * logicalToPhysical + startPoint.Y;
                        dc.DrawLine(pen,
                            new Point(startPoint.X, y),
                            new Point(startPoint.X + tickLen2, y));
 
                        if (snapsToDevicePixels)
                        {
                            yLines.Add(y - 0.5);
                        }
                    }
                }
                // Draw ticks using specified TickFrequency
                else if (interval > 0.0)
                {
                    for (double i = interval; i < range; i += interval)
                    {
                        double y = i * logicalToPhysical + startPoint.Y;
 
                        dc.DrawLine(pen,
                            new Point(startPoint.X, y),
                            new Point(startPoint.X + tickLen2, y));
 
                        if (snapsToDevicePixels)
                        {
                            yLines.Add(y - 0.5);
                        }
                    }
                }
 
                // Draw Selection Ticks
                if (IsSelectionRangeEnabled)
                {
                    double y0 = (SelectionStart - Minimum) * logicalToPhysical + startPoint.Y;
                    Point pt0 = new Point(startPoint.X, y0);
                    Point pt1 = new Point(startPoint.X + tickLen2, y0);
                    Point pt2 = new Point(startPoint.X + tickLen2, y0 + Math.Abs(tickLen2) * progression);
 
                    PathSegment[] segments = new PathSegment[] {
                        new LineSegment(pt2, true),
                        new LineSegment(pt0, true),
                    };
                    PathGeometry geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) });
 
                    dc.DrawGeometry(Fill, pen, geo);
 
                    y0 = (SelectionEnd - Minimum) * logicalToPhysical + startPoint.Y;
                    pt0 = new Point(startPoint.X, y0);
                    pt1 = new Point(startPoint.X + tickLen2, y0);
                    pt2 = new Point(startPoint.X + tickLen2, y0 - Math.Abs(tickLen2) * progression);
 
                    segments = new PathSegment[] {
                        new LineSegment(pt2, true),
                        new LineSegment(pt0, true),
                    };
                    geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) });
                    dc.DrawGeometry(Fill, pen, geo);
                }
            }
            else  // Placement == Top || Placement == Bottom
            {
                // Reduce tick interval if it is more than would be visible on the screen
                double interval = TickFrequency;
                if (interval > 0.0)
                {
                    double minInterval = (Maximum - Minimum) / size.Width;
                    if (interval < minInterval)
                    {
                        interval = minInterval;
                    }
                }
 
                // Draw Min & Max tick
                dc.DrawLine(pen, startPoint, new Point(startPoint.X, startPoint.Y + tickLen));
                dc.DrawLine(pen, new Point(endPoint.X, startPoint.Y),
                                 new Point(endPoint.X, startPoint.Y + tickLen));
 
                if (snapsToDevicePixels)
                {
                    xLines.Add(startPoint.X - 0.5);
                    yLines.Add(startPoint.Y);
                    xLines.Add(startPoint.X - 0.5);
                    yLines.Add(endPoint.Y + tickLen);
                    yLines.Add(endPoint.Y + tickLen2);
                }
 
                // This property is rarely set so let's try to avoid the GetValue
                // caching of the mutable default value
                DoubleCollection ticks = null;
                bool hasModifiers;
                if (GetValueSource(TicksProperty, null, out hasModifiers)
                    != BaseValueSourceInternal.Default || hasModifiers)
                {
                    ticks = Ticks;
                }
 
                // Draw ticks using specified Ticks collection
                if ((ticks != null) && (ticks.Count > 0))
                {
                    for (int i = 0; i < ticks.Count; i++)
                    {
                        if (DoubleUtil.LessThanOrClose(ticks[i],Minimum) || DoubleUtil.GreaterThanOrClose(ticks[i],Maximum))
                        {
                            continue;
                        }
                        double adjustedTick = ticks[i] - Minimum;
 
                        double x = adjustedTick * logicalToPhysical + startPoint.X;
                        dc.DrawLine(pen,
                            new Point(x, startPoint.Y),
                            new Point(x, startPoint.Y + tickLen2));
 
                        if (snapsToDevicePixels)
                        {
                            xLines.Add(x - 0.5);
                        }
                    }
                }
                // Draw ticks using specified TickFrequency
                else if (interval > 0.0)
                {
                    for (double i = interval; i < range; i += interval)
                    {
                        double x = i * logicalToPhysical + startPoint.X;
                        dc.DrawLine(pen,
                            new Point(x, startPoint.Y),
                            new Point(x, startPoint.Y + tickLen2));
 
                        if (snapsToDevicePixels)
                        {
                            xLines.Add(x - 0.5);
                        }
                    }
                }
 
                // Draw Selection Ticks
                if (IsSelectionRangeEnabled)
                {
                    double x0 = (SelectionStart - Minimum) * logicalToPhysical + startPoint.X;
                    Point pt0 = new Point(x0, startPoint.Y);
                    Point pt1 = new Point(x0, startPoint.Y + tickLen2);
                    Point pt2 = new Point(x0 + Math.Abs(tickLen2) * progression, startPoint.Y + tickLen2);
 
                    PathSegment[] segments = new PathSegment[] {
                        new LineSegment(pt2, true),
                        new LineSegment(pt0, true),
                    };
                    PathGeometry geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) });
 
                    dc.DrawGeometry(Fill, pen, geo);
 
                    x0 = (SelectionEnd - Minimum) * logicalToPhysical + startPoint.X;
                    pt0 = new Point(x0, startPoint.Y);
                    pt1 = new Point(x0, startPoint.Y + tickLen2);
                    pt2 = new Point(x0 - Math.Abs(tickLen2) * progression, startPoint.Y + tickLen2);
 
                    segments = new PathSegment[] {
                        new LineSegment(pt2, true),
                        new LineSegment(pt0, true),
                    };
                    geo = new PathGeometry(new PathFigure[] { new PathFigure(pt1, segments, true) });
                    dc.DrawGeometry(Fill, pen, geo);
                }
            }
 
            if (snapsToDevicePixels)
            {
                xLines.Add(ActualWidth);
                yLines.Add(ActualHeight);
                VisualXSnappingGuidelines = xLines;
                VisualYSnappingGuidelines = yLines;
            }
            return;
        }
 
        private void BindToTemplatedParent(DependencyProperty target, DependencyProperty source)
        {
            if (!HasNonDefaultValue(target))
            {
                Binding binding = new Binding();
                binding.RelativeSource = RelativeSource.TemplatedParent;
                binding.Path = new PropertyPath(source);
                SetBinding(target, binding);
            }
        }
 
        /// <summary>
        /// TickBar sets bindings on its properties to its TemplatedParent if the
        /// properties are not already set.
        /// </summary>
        internal override void OnPreApplyTemplate()
        {
            base.OnPreApplyTemplate();
 
            Slider parent = TemplatedParent as Slider;
            if (parent != null)
            {
                BindToTemplatedParent(TicksProperty, Slider.TicksProperty);
                BindToTemplatedParent(TickFrequencyProperty, Slider.TickFrequencyProperty);
                BindToTemplatedParent(IsSelectionRangeEnabledProperty, Slider.IsSelectionRangeEnabledProperty);
                BindToTemplatedParent(SelectionStartProperty, Slider.SelectionStartProperty);
                BindToTemplatedParent(SelectionEndProperty, Slider.SelectionEndProperty);
                BindToTemplatedParent(MinimumProperty, Slider.MinimumProperty);
                BindToTemplatedParent(MaximumProperty, Slider.MaximumProperty);
                BindToTemplatedParent(IsDirectionReversedProperty, Slider.IsDirectionReversedProperty);
 
                if (!HasNonDefaultValue(ReservedSpaceProperty) && parent.Track != null)
                {
                    Binding binding = new Binding();
                    binding.Source = parent.Track.Thumb;
 
                    if (parent.Orientation == Orientation.Horizontal)
                    {
                        binding.Path = new PropertyPath(Thumb.ActualWidthProperty);
                    }
                    else
                    {
                        binding.Path = new PropertyPath(Thumb.ActualHeightProperty);
                    }
 
                    SetBinding(ReservedSpaceProperty, binding);
                }
            }
        }
    }
}