实现自定义时间轴控件

需求:

(1)实现一个以分钟为单位的自定义时间轴控件。

(2)滚轮缩放时间轴大小

(3)能在时间轴上添加不同的可视对象

 

实现思路:我采用自定义控件,OnRender绘制刻度,以及呈现标签。

(1)定义刻度等级

  /// <summary>
    /// 刻度等级
    /// </summary>
    public enum TimeLevel
    {
        TwoHour,//两小时
        OneHour,//一小时
        HaflHour,//半小时
        OneThirdHour,//1/3小时
        OneSixthHour,//1/6小时
        OnTwelfthHour//1/12小时
    }
 /// <summary>
        /// 根据等级计算一个间隔代表多少分钟
        /// </summary>
        /// <param name="timeLevel"></param>
        /// <returns></returns>
        private int GetPerItemMinuteByTimeLevel()
        {
            int minute = 0;
            switch (CurrentTimeLevel)
            {
                case TimeLevel.TwoHour:
                    minute = 24;
                    break;
                case TimeLevel.OneHour:
                    minute = 12;
                    break;
                case TimeLevel.HaflHour:
                    minute = 6;
                    break;
                case TimeLevel.OneThirdHour:
                    minute = 4;
                    break;
                case TimeLevel.OneSixthHour:
                    minute = 2;
                    break;
                case TimeLevel.OnTwelfthHour:
                    minute = 1;
                    break;
            }
            return minute;
        }

 

(2)以中心为基准为当前刻度时间,寻找第一个标准刻度。比如当前是11:11:20秒,需要找出一个刻度标准位置和起始时间。当前刻度单位为分钟,计算公式为:

 minute =GetPerItemMinuteByTimeLevel();

 var offsetPosition = (((CurrentDate.Hour * 60) % minute + (CurrentDate.Minute % minute)) % minute + (CurrentDate.Second / 60.0f)) / (float)minute * itemWidth;

起始时间计算方式类似。具体如下:

/// <summary>
        /// 计算第一个刻度出现的位置和时间
        /// </summary>
        /// <returns></returns>
        private Tuple<float, DateTime> GetStartPositionByTimeLevel()
        {
            float startPos = 0;
            var minute = GetPerItemMinuteByTimeLevel();
            //一个计量刻度长度
            var itemWidth = _ruleWidth + _intervalWidth;
            var midPosition = ActualWidth / 2.0f;
            //当前分钟为单位偏移量,需要把小时对分钟刻度取余
            var offsetPosition = (((CurrentDate.Hour * 60) % minute + (CurrentDate.Minute % minute)) % minute + (CurrentDate.Second / 60.0f)) / (float)minute * itemWidth;
            //计算第一个刻度出现的位置
            startPos = (float)(midPosition - offsetPosition);
            //第一个刻度的时间
            var offsetMinute = (((CurrentDate.Hour * 60) % minute + CurrentDate.Minute) % minute) / minute * minute;
            var startTime = (((CurrentDate.Hour * 60) % minute + CurrentDate.Minute) % minute + CurrentDate.Second / 60.0);
            var fisrtRuleDatetime = CurrentDate.AddMinutes(-startTime).AddMinutes(offsetMinute);
            return new Tuple<float, DateTime>(startPos, fisrtRuleDatetime);
        }

根据当前的时间找到标准第一个刻度出现的位置,接下来就是根据间隔和个数填充绘制了。

完整代码

TimeLine

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace TimeLineDemo
{
    public class TimeLineItemSelectedEventArgs : RoutedEventArgs
    {
        public TimeLineItemSelectedEventArgs(RoutedEvent routedEvent, object source)
            : base(routedEvent, source) { }

        public TimeLineItem TimeLineItem { get; set; }
    }

    public class TimeLineItem
    {
        public DateTime StartTime { get; set; }

        public DateTime EndTime { get; set; }

        /// <summary>
        /// 是否使用固定长度
        /// </summary>
        public bool IsUseRenderWidth { get; set; }

        /// <summary>
        /// 使用固定长度值
        /// </summary>
        public double RenderWidth { get; set; }

        /// <summary>
        /// 添加显示内容
        /// </summary>
        public Brush ContentBrush { get; set; }
    }

    /// <summary>
    /// 刻度等级
    /// </summary>
    public enum TimeLevel
    {
        TwoHour,//两小时
        OneHour,//一小时
        HaflHour,//半小时
        OneThirdHour,//1/3小时
        OneSixthHour,//1/6小时
        OnTwelfthHour//1/12小时
    }

    public class TimeLine : Control
    {
        private float _intervalWidth = 10;//刻度间距
        private float _ruleWidth = 1.5f;//刻度宽度
        private float _minIntervalWidth = 10;//间距最小值
        private float _maxIntervalWidth = 50;//间距最大值
        Brush _ruleBrush = null;//刻度画刷
        private readonly List<TimeLineItem> _timeLineItems = new List<TimeLineItem>();

        public DateTime CurrentDate
        {
            get { return (DateTime)GetValue(CurrentDateProperty); }
            set { SetValue(CurrentDateProperty, value); }
        }
        // Using a DependencyProperty as the backing store for CurrentDate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CurrentDateProperty =
            DependencyProperty.Register("CurrentDate", typeof(DateTime), typeof(TimeLine), new PropertyMetadata(CurrentDateChangedCallback));

        public TimeLevel CurrentTimeLevel
        {
            get { return (TimeLevel)GetValue(CurrentTimeLevelProperty); }
            set { SetValue(CurrentTimeLevelProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CurrentTimeLevel.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CurrentTimeLevelProperty =
            DependencyProperty.Register("CurrentTimeLevel", typeof(TimeLevel), typeof(TimeLine), new PropertyMetadata(TimeLevel.OneHour, TimeLevelChangedCallback));


        private static void TimeLevelChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TimeLine timeLine = d as TimeLine;
            timeLine.InvalidateVisual();
        }

        public static readonly RoutedEvent TimeLineItemSelectedEvent =
           EventManager.RegisterRoutedEvent("TimeLineItemSelected", RoutingStrategy.Bubble,
               typeof(EventHandler<TimeLineItemSelectedEventArgs>), typeof(TimeLine));

        public event RoutedEventHandler TimeLineItemSelected
        {
            add { this.AddHandler(TimeLineItemSelectedEvent, value); }
            remove { this.RemoveHandler(TimeLineItemSelectedEvent, value); }
        }

        private static void CurrentDateChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TimeLine timeLine = d as TimeLine;
            timeLine.InvalidateVisual();
        }


        static TimeLine()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TimeLine), new FrameworkPropertyMetadata(typeof(TimeLine)));
        }

        public TimeLine()
        {
            PreviewMouseLeftButtonDown += TimeLine_PreviewMouseLeftButtonDown;
            PreviewMouseLeftButtonUp += TimeLine_PreviewMouseLeftButtonUp;
            MouseLeftButtonDown += TimeLine_MouseLeftButtonDown;
            MouseMove += TimeLine_MouseMove;
            MouseWheel += TimeLine_MouseWheel;
            _ruleBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#27cfff"));
        }

        private void TimeLine_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Mouse.Capture(this);
            var point = e.GetPosition(this);
            var itemWidth = _ruleWidth + _intervalWidth;
            var minute = (point.X - ActualWidth / 2) / itemWidth * GetPerItemMinuteByTimeLevel();
            var clickTime = CurrentDate.AddMinutes(minute);
            foreach (var item in _timeLineItems)
            {
                if (item.StartTime <= clickTime && clickTime <= item.EndTime)
                {
                    TimeLineItemSelectedEventArgs eventArgs = new TimeLineItemSelectedEventArgs(TimeLineItemSelectedEvent, this)
                    { TimeLineItem = item };
                    RaiseEvent(eventArgs);
                }
            }
        }

        private void TimeLine_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            _intervalWidth = _intervalWidth + e.Delta * 0.005f;
            _intervalWidth = _intervalWidth < _minIntervalWidth ? _minIntervalWidth : _intervalWidth;
            _intervalWidth = _intervalWidth > _maxIntervalWidth ? _maxIntervalWidth : _intervalWidth;
            InvalidateVisual();
        }

        private void TimeLine_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                this.Cursor = Cursors.Hand;
                var offset = e.GetPosition(this) - startMousePoint;
                var itemWidth = _ruleWidth + _intervalWidth;
                var minute = offset.X / itemWidth * GetPerItemMinuteByTimeLevel();
                CurrentDate = startDate.AddMinutes(minute);
            }
        }

        private void TimeLine_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            ReleaseMouseCapture();
        }

        Point startMousePoint;
        DateTime startDate;
        private void TimeLine_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Mouse.Capture(this);
            startMousePoint = e.GetPosition(this);
            startDate = CurrentDate;
        }

        protected override void OnRender(DrawingContext drawingContext)
        {
            //绘制背景
            drawingContext.DrawRectangle(Brushes.Black, new Pen(), new Rect(new Size(ActualWidth, ActualHeight)));
            //一个计量刻度长度
            var itemWidth = _ruleWidth + _intervalWidth;
            //计算当前时间刻度个数,+15确保拖动不会出现空白
            var count = (int)(ActualWidth / itemWidth) + 15;
            //标准化第一个刻度出现的位置和时间
            var startRule = GetStartPositionByTimeLevel();
            //每个间隔代表分钟
            var minute = GetPerItemMinuteByTimeLevel();
            //控件起始刻度出现位置
            var startPosition = startRule.Item1 - (count / 2) * itemWidth;
            var tempTime = startRule.Item2.AddMinutes(-count / 2 * minute);
            var linePen = new Pen() { Brush = _ruleBrush };
            //从左往右进行绘制
            for (int i = 0; i < count; i++)
            {
                var pos = new Point(startPosition + i * itemWidth, 45);
                var size = new Size(2, 15);
                if (IsShowText(tempTime))//绘制文本
                {
                    pos = new Point(pos.X, pos.Y - 15);
                    size = new Size(2, 30);
                    var text = GetFormattedText($"{tempTime.Hour.ToString("00")}:{tempTime.Minute.ToString("00")}");
                    drawingContext.DrawText(text, new Point(pos.X - 23.5, 5));
                }
                tempTime = tempTime.AddMinutes(minute);
                drawingContext.DrawRectangle(_ruleBrush,
                   linePen,
                   new Rect(pos, size));
            }

            //VisualBrush List Render
            if (_timeLineItems.Count > 0)
            {
                double startTimePos;
                double endTimePos;
                Point startLineItemPos;
                Size lineItemSize;
                foreach (var item in _timeLineItems)
                {
                    startTimePos = GetPostionByTime(item.StartTime);
                    endTimePos = GetPostionByTime(item.EndTime);
                    if (startTimePos > 0 || startTimePos < ActualWidth || endTimePos > 0 || endTimePos < ActualWidth)
                    {
                        startLineItemPos = new Point(startTimePos, 0);
                        lineItemSize = new Size(Math.Abs(endTimePos - startTimePos), ActualHeight);
                        if (item.IsUseRenderWidth)
                        {
                            startLineItemPos = new Point(GetPostionByTime(item.StartTime.AddMinutes((item.EndTime - item.StartTime).TotalMinutes / 2)) - item.RenderWidth / 2, 0);
                            lineItemSize = new Size(item.RenderWidth, ActualHeight);
                        }
                        drawingContext.DrawRectangle(item.ContentBrush, new Pen()
                        {
                            Brush = item.ContentBrush
                        }, new Rect(startLineItemPos, lineItemSize));
                    }
                }
            }

            //Pointer
            drawingContext.DrawRectangle
                (Brushes.Yellow,
                new Pen(),
                new Rect(new Point(ActualWidth / 2, 0), new Size(2, ActualHeight)));
        }

        /// <summary>
        /// 根据获取时间轴上的坐标
        /// </summary>
        /// <param name="time"></param>
        /// <returns></returns>
        private double GetPostionByTime(DateTime time)
        {
            var minute = GetPerItemMinuteByTimeLevel();
            var itemWidth = _ruleWidth + _intervalWidth;
            var pos = ActualWidth / 2 + (time - CurrentDate).TotalMinutes / (float)minute * itemWidth;
            return pos;
        }

        private bool IsShowText(DateTime dateTime)
        {
            var time = 0;
            bool isShow = false;
            switch (CurrentTimeLevel)
            {
                case TimeLevel.TwoHour:
                    time = 120;
                    break;
                case TimeLevel.OneHour:
                    time = 60;
                    break;
                case TimeLevel.HaflHour:
                    time = 30;
                    break;
                case TimeLevel.OneThirdHour:
                    time = 20;
                    break;
                case TimeLevel.OneSixthHour:
                    time = 10;
                    break;
                case TimeLevel.OnTwelfthHour:
                    time = 5;
                    break;
            }
            isShow = (dateTime.Minute + dateTime.Second) % time == 0;
            return isShow;
        }

        /// <summary>
        /// 计算第一个刻度出现的位置和时间
        /// </summary>
        /// <returns></returns>
        private Tuple<float, DateTime> GetStartPositionByTimeLevel()
        {
            float startPos = 0;
            var minute = GetPerItemMinuteByTimeLevel();
            //一个计量刻度长度
            var itemWidth = _ruleWidth + _intervalWidth;
            var midPosition = ActualWidth / 2.0f;
            //当前分钟为单位偏移量,需要把小时对分钟刻度取余
            var offsetPosition = (((CurrentDate.Hour * 60) % minute + (CurrentDate.Minute % minute)) % minute + (CurrentDate.Second / 60.0f)) / (float)minute * itemWidth;
            //计算第一个刻度出现的位置
            startPos = (float)(midPosition - offsetPosition);
            //第一个刻度的时间
            var offsetMinute = (((CurrentDate.Hour * 60) % minute + CurrentDate.Minute) % minute) / minute * minute;
            var startTime = (((CurrentDate.Hour * 60) % minute + CurrentDate.Minute) % minute + CurrentDate.Second / 60.0);
            var fisrtRuleDatetime = CurrentDate.AddMinutes(-startTime).AddMinutes(offsetMinute);
            return new Tuple<float, DateTime>(startPos, fisrtRuleDatetime);
        }

        /// <summary>
        /// 根据等级计算一个间隔代表多少分钟
        /// </summary>
        /// <param name="timeLevel"></param>
        /// <returns></returns>
        private int GetPerItemMinuteByTimeLevel()
        {
            int minute = 0;
            switch (CurrentTimeLevel)
            {
                case TimeLevel.TwoHour:
                    minute = 24;
                    break;
                case TimeLevel.OneHour:
                    minute = 12;
                    break;
                case TimeLevel.HaflHour:
                    minute = 6;
                    break;
                case TimeLevel.OneThirdHour:
                    minute = 4;
                    break;
                case TimeLevel.OneSixthHour:
                    minute = 2;
                    break;
                case TimeLevel.OnTwelfthHour:
                    minute = 1;
                    break;
            }
            return minute;
        }

        private FormattedText GetFormattedText(string text)
        {
            return new FormattedText(text,
                System.Globalization.CultureInfo.CurrentCulture,
                 FlowDirection.LeftToRight,
                 new Typeface("MicrosoftYaHei"),
                  20,
                   Brushes.White);
        }


        public void AddVisualBrushToLine(TimeLineItem timeLineItem)
        {
            if (_timeLineItems.Contains(timeLineItem)) { return; }
            _timeLineItems.Add(timeLineItem);
            InvalidateVisual();
        }
    }
}

MainWindow.xaml

<Window x:Class="TimeLineDemo.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:TimeLineDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" WindowState="Maximized">
    <Window.Resources>
        <SolidColorBrush x:Key="timeLineBrush" Color="#88ff0000"/>
        <ImageBrush x:Key="imageBrush"/>
    </Window.Resources>
    <Grid Background="Gray">
        <Canvas/>
        <local:TimeLine Width="1000" Height="60" x:Name="timeLine" ClipToBounds="True"/>
        <TextBlock Text="{Binding ElementName=timeLine,Path=CurrentDate}" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="450"/>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Top" Height="200">
            <Button Content="Add" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="2" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="1" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="1/12" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="1/6" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="1/3" Click="Button_Click" Width="100" Height="50"/>
            <Button Content="1/2" Click="Button_Click" Width="100" Height="50"/>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TimeLineDemo
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            timeLine.CurrentDate = DateTime.Now;
            timeLine.TimeLineItemSelected += timeLine_TimeLineItemSelected;
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var btn = sender as Button;
            TimeLevel timeLevel = TimeLevel.OneHour;
            switch (btn.Content.ToString())
            {
                case "Add":
                    AddVisualBrushToTimeLine();
                    break;
                case "2":
                    timeLevel = TimeLevel.TwoHour;
                    break;
                case "1":
                    timeLevel = TimeLevel.OneHour;
                    break;
                case "1/12":
                    timeLevel = TimeLevel.OnTwelfthHour;
                    break;
                case "1/3":
                    timeLevel = TimeLevel.OneThirdHour;
                    break;
                case "1/6":
                    timeLevel = TimeLevel.OneSixthHour;
                    break;
                case "1/2":
                    timeLevel = TimeLevel.HaflHour;
                    break;

            }

            timeLine.CurrentTimeLevel = timeLevel;
        }

        private void AddVisualBrushToTimeLine()
        {
            Random random = new Random();
            var minute = random.Next(0, 60);
            MessageBox.Show(minute.ToString());
            TimeLineItem timeLineItem = new TimeLineItem()
            {
                StartTime = DateTime.Now.AddMinutes(-minute),
                EndTime = DateTime.Now.AddMinutes(minute),
                ContentBrush = (Brush)FindResource("timeLineBrush")
            };
            TimeLineItem timeLineItemIcon = new TimeLineItem()
            {
                StartTime = DateTime.Now.AddMinutes(-4),
                EndTime = DateTime.Now.AddMinutes(4),
                ContentBrush = (Brush)FindResource("imageBrush"),
                IsUseRenderWidth = true,
                RenderWidth = 10
            };
            timeLine.AddVisualBrushToLine(timeLineItem);
            timeLine.AddVisualBrushToLine(timeLineItemIcon);
        }

        private void timeLine_TimeLineItemSelected(object sender, RoutedEventArgs e)
        {
            TimeLineItemSelectedEventArgs timeLineItemSelectedEventArgs = e as TimeLineItemSelectedEventArgs;
            if (timeLineItemSelectedEventArgs != null && timeLineItemSelectedEventArgs.TimeLineItem != null)
            {
                //timeLineItemSelectedEventArgs.TimeLineItem;
                MessageBox.Show("选中!");
            }
        }
    }
}

运行效果:

 

posted @ 2022-03-30 11:32  荀幽  阅读(410)  评论(0编辑  收藏  举报