实现自定义时间轴控件
需求:
(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("选中!"); } } } }
运行效果: