using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using HandyControl.Data; using HandyControl.Interactivity; using HandyControl.Tools; namespace HandyControl.Controls; /// /// 时间条 /// [TemplatePart(Name = ElementBorderTop, Type = typeof(Border))] [TemplatePart(Name = ElementTextBlockMove, Type = typeof(TextBlock))] [TemplatePart(Name = ElementTextBlockSelected, Type = typeof(TextBlock))] [TemplatePart(Name = ElementCanvasSpe, Type = typeof(Canvas))] [TemplatePart(Name = ElementHotspots, Type = typeof(Panel))] public class TimeBar : Control { #region Constants private const string ElementBorderTop = "PART_BorderTop"; private const string ElementTextBlockMove = "PART_TextBlockMove"; private const string ElementTextBlockSelected = "PART_TextBlockSelected"; private const string ElementCanvasSpe = "PART_CanvasSpe"; private const string ElementHotspots = "PART_Hotspots"; #endregion Constants #region Data private Border _borderTop; private TextBlock _textBlockMove; private TextBlock _textBlockSelected; private Canvas _canvasSpe; private Panel _panelHotspots; #endregion Data [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [Bindable(true)] public Collection Hotspots { get; } public static readonly DependencyProperty HotspotsBrushProperty = DependencyProperty.Register( nameof(HotspotsBrush), typeof(Brush), typeof(TimeBar), new PropertyMetadata(default(Brush))); public Brush HotspotsBrush { get => (Brush) GetValue(HotspotsBrushProperty); set => SetValue(HotspotsBrushProperty, value); } /// /// 是否显示刻度字符串 /// public static readonly DependencyProperty ShowSpeStrProperty = DependencyProperty.Register( nameof(ShowSpeStr), typeof(bool), typeof(TimeBar), new PropertyMetadata(ValueBoxes.FalseBox)); /// /// 是否显示刻度字符串 /// public bool ShowSpeStr { get => (bool) GetValue(ShowSpeStrProperty); set => SetValue(ShowSpeStrProperty, ValueBoxes.BooleanBox(value)); } public static readonly DependencyProperty TimeFormatProperty = DependencyProperty.Register( nameof(TimeFormat), typeof(string), typeof(TimeBar), new PropertyMetadata("yyyy-MM-dd HH:mm:ss")); public string TimeFormat { get => (string) GetValue(TimeFormatProperty); set => SetValue(TimeFormatProperty, value); } /// /// 刻度字符串 /// internal static readonly DependencyProperty SpeStrProperty = DependencyProperty.Register( nameof(SpeStr), typeof(string), typeof(TimeBar), new PropertyMetadata(Properties.Langs.Lang.Interval1h)); /// /// 刻度字符串 /// internal string SpeStr { get => (string) GetValue(SpeStrProperty); set => SetValue(SpeStrProperty, value); } /// /// 选中时间 /// public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register( nameof(SelectedTime), typeof(DateTime), typeof(TimeBar), new PropertyMetadata(default(DateTime), OnSelectedTimeChanged)); private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TimeBar { _textBlockSelected: { } } timeBar) { timeBar.OnSelectedTimeChanged((DateTime) e.NewValue); } } /// /// 选中时间 /// public DateTime SelectedTime { get => (DateTime) GetValue(SelectedTimeProperty); set => SetValue(SelectedTimeProperty, value); } private void OnSelectedTimeChanged(DateTime time) { _textBlockSelected.Text = time.ToString(TimeFormat); if (!(_isDragging || _borderTopIsMouseLeftButtonDown)) { _totalOffsetX = (_starTime - SelectedTime).TotalMilliseconds / _timeSpeList[SpeIndex] * _itemWidth; } UpdateSpeBlock(); UpdateMouseFollowBlockPos(); } /// /// 时间改变事件 /// public static readonly RoutedEvent TimeChangedEvent = EventManager.RegisterRoutedEvent("TimeChanged", RoutingStrategy.Bubble, typeof(EventHandler>), typeof(TimeBar)); /// /// 刻度集合 /// private readonly List _speBlockList = new(); /// /// 初始化时时间 /// private readonly DateTime _starTime; /// /// 时间段集合 /// private readonly List _timeSpeList = new() { 7200000, 3600000, 1800000, 600000, 300000, 60000, 30000 }; /// /// 顶部border是否被按下 /// private bool _borderTopIsMouseLeftButtonDown; /// /// 控件是否处于拖动中 /// private bool _isDragging; /// /// 刻度单项宽度 /// private double _itemWidth; /// /// 鼠标按下拖动时选中的时间 /// private DateTime _mouseDownTime; /// /// 显示的刻度数目 /// private int _speCount = 13; /// /// 刻度区间编号 /// private int _speIndex = 1; /// /// 刻度单次偏移 /// private double _tempOffsetX; /// /// 刻度总偏移 /// private double _totalOffsetX; private readonly bool _isLoaded; private readonly SortedSet _dateTimeRanges; public TimeBar() { _starTime = DateTime.Now; SelectedTime = new DateTime(_starTime.Year, _starTime.Month, _starTime.Day, 0, 0, 0); _starTime = SelectedTime; _isLoaded = true; var hotspots = new ObservableCollection(); _dateTimeRanges = new SortedSet(ComparerGenerator.GetComparer()); hotspots.CollectionChanged += Items_CollectionChanged; Hotspots = hotspots; } private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add) { foreach (DateTimeRange item in e.NewItems) { _dateTimeRanges.Add(item); } } else if (e.Action == NotifyCollectionChangedAction.Remove) { foreach (DateTimeRange item in e.OldItems) { _dateTimeRanges.Remove(item); } } else if (e.Action == NotifyCollectionChangedAction.Replace) { foreach (DateTimeRange item in e.OldItems) { _dateTimeRanges.Remove(item); } foreach (DateTimeRange item in e.NewItems) { _dateTimeRanges.Add(item); } } else if (e.Action == NotifyCollectionChangedAction.Reset) { _dateTimeRanges.Clear(); } } public override void OnApplyTemplate() { if (_borderTop != null) { _borderTop.MouseLeftButtonDown -= BorderTop_OnMouseLeftButtonDown; _borderTop.PreviewMouseLeftButtonUp -= BorderTop_OnPreviewMouseLeftButtonUp; } base.OnApplyTemplate(); _borderTop = GetTemplateChild(ElementBorderTop) as Border; _textBlockMove = GetTemplateChild(ElementTextBlockMove) as TextBlock; _textBlockSelected = GetTemplateChild(ElementTextBlockSelected) as TextBlock; _canvasSpe = GetTemplateChild(ElementCanvasSpe) as Canvas; _panelHotspots = GetTemplateChild(ElementHotspots) as Panel; CheckNull(); _borderTop.MouseLeftButtonDown += BorderTop_OnMouseLeftButtonDown; _borderTop.PreviewMouseLeftButtonUp += BorderTop_OnPreviewMouseLeftButtonUp; var behavior = new MouseDragElementBehaviorEx { LockY = true, }; behavior.DragBegun += DragElementBehavior_OnDragBegun; behavior.Dragging += MouseDragElementBehavior_OnDragging; behavior.DragFinished += MouseDragElementBehavior_OnDragFinished; var collection = Interaction.GetBehaviors(_borderTop); collection.Add(behavior); if (_isLoaded) { Update(); } _textBlockSelected.Text = SelectedTime.ToString(TimeFormat); } private void CheckNull() { if (_borderTop == null || _textBlockMove == null || _textBlockSelected == null || _canvasSpe == null) throw new Exception(); } /// /// 刻度区间编号 /// public int SpeIndex { get => _speIndex; private set { if (_speIndex == value) return; if (value < 0) { SpeStr = Properties.Langs.Lang.Interval2h; _speIndex = 0; return; } if (value > 6) { SpeStr = Properties.Langs.Lang.Interval30s; _speIndex = 6; return; } SetSpeTimeFormat("HH:mm"); switch (value) { case 0: SpeStr = Properties.Langs.Lang.Interval2h; break; case 1: SpeStr = Properties.Langs.Lang.Interval1h; break; case 2: SpeStr = Properties.Langs.Lang.Interval30m; break; case 3: SpeStr = Properties.Langs.Lang.Interval10m; break; case 4: SpeStr = Properties.Langs.Lang.Interval5m; break; case 5: SpeStr = Properties.Langs.Lang.Interval1m; break; case 6: SetSpeTimeFormat("HH:mm:ss"); SpeStr = Properties.Langs.Lang.Interval30s; break; } _speIndex = value; } } /// /// 时间改变事件 /// public event EventHandler> TimeChanged { add => AddHandler(TimeChangedEvent, value); remove => RemoveHandler(TimeChangedEvent, value); } /// /// 设置刻度时间格式 /// /// private void SetSpeTimeFormat(string format) { foreach (var item in _speBlockList) item.TimeFormat = format; } /// /// 更新刻度 /// private void UpdateSpeBlock() { var rest = (_totalOffsetX + _tempOffsetX) % _itemWidth; for (var i = 0; i < _speCount; i++) { var item = _speBlockList[i]; item.MoveX(rest + (_itemWidth - item.Width) / 2); } var sub = rest <= 0 ? _speCount / 2 : _speCount / 2 - 1; for (var i = 0; i < _speCount; i++) _speBlockList[i].Time = TimeConvert(SelectedTime).AddMilliseconds((i - sub) * _timeSpeList[_speIndex]); if (_panelHotspots != null && _dateTimeRanges.Any()) { UpdateHotspots(); } } /// /// 时间转换 /// /// /// private DateTime TimeConvert(DateTime time) { return _speIndex switch { 0 => new DateTime(time.Year, time.Month, time.Day, time.Hour / 2 * 2, 0, 0), 1 => new DateTime(time.Year, time.Month, time.Day, time.Hour, 0, 0), 2 => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute / 30 * 30, 0), 3 => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute / 10 * 10, 0), 4 => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute / 5 * 5, 0), 5 => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, 0), 6 => new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second / 30 * 30), _ => time }; } /// /// 鼠标滚轮滚动时改变刻度区间 /// /// protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); if (Mouse.LeftButton == MouseButtonState.Pressed) return; SpeIndex += e.Delta > 0 ? 1 : -1; _totalOffsetX = (_starTime - SelectedTime).TotalMilliseconds / _timeSpeList[SpeIndex] * _itemWidth; UpdateSpeBlock(); UpdateMouseFollowBlockPos(); e.Handled = true; } private void MouseDragElementBehavior_OnDragging(object sender, MouseEventArgs e) { _isDragging = true; _tempOffsetX = _borderTop.RenderTransform.Value.OffsetX; SelectedTime = _mouseDownTime - TimeSpan.FromMilliseconds(_tempOffsetX / _itemWidth * _timeSpeList[_speIndex]); _borderTopIsMouseLeftButtonDown = false; } private void MouseDragElementBehavior_OnDragFinished(object sender, MouseEventArgs e) { _tempOffsetX = 0; _totalOffsetX = (_totalOffsetX + _borderTop.RenderTransform.Value.OffsetX) % ActualWidth; _borderTop.RenderTransform = new TranslateTransform(); RaiseEvent(new FunctionEventArgs(TimeChangedEvent, this) { Info = SelectedTime }); _isDragging = false; } private void DragElementBehavior_OnDragBegun(object sender, MouseEventArgs e) => _mouseDownTime = SelectedTime; protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); Update(); } /// /// 更新 /// private void Update() { if (_canvasSpe == null) return; _speBlockList.Clear(); _canvasSpe.Children.Clear(); _speCount = (int) (ActualWidth / 800 * 9) | 1; var itemWidthOld = _itemWidth; _itemWidth = ActualWidth / _speCount; _totalOffsetX = _itemWidth / itemWidthOld * _totalOffsetX % ActualWidth; if (double.IsNaN(_totalOffsetX)) { _totalOffsetX = 0; } var rest = (_totalOffsetX + _tempOffsetX) % _itemWidth; var sub = rest <= 0 || double.IsNaN(rest) ? _speCount / 2 : _speCount / 2 - 1; for (var i = 0; i < _speCount; i++) { var block = new SpeTextBlock { Time = TimeConvert(SelectedTime).AddMilliseconds((i - sub) * _timeSpeList[_speIndex]), TextAlignment = TextAlignment.Center, TimeFormat = "HH:mm" }; _speBlockList.Add(block); _canvasSpe.Children.Add(block); } if (_speIndex == 6) { SetSpeTimeFormat("HH:mm:ss"); } ShowSpeStr = ActualWidth > 320; for (var i = 0; i < _speCount; i++) { var item = _speBlockList[i]; item.X = _itemWidth * i; item.MoveX((_itemWidth - item.Width) / 2); } UpdateSpeBlock(); UpdateMouseFollowBlockPos(); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); UpdateMouseFollowBlockPos(); } /// /// 更新鼠标跟随块位置 /// private void UpdateMouseFollowBlockPos() { var p = Mouse.GetPosition(this); var mlliseconds = (p.X - ActualWidth / 2) / _itemWidth * _timeSpeList[_speIndex]; if (double.IsNaN(mlliseconds) || double.IsInfinity(mlliseconds)) return; _textBlockMove.Text = mlliseconds < 0 ? (SelectedTime - TimeSpan.FromMilliseconds(-mlliseconds)).ToString(TimeFormat) : (SelectedTime + TimeSpan.FromMilliseconds(mlliseconds)).ToString(TimeFormat); _textBlockMove.Margin = new Thickness(p.X - _textBlockMove.ActualWidth / 2, 2, 0, 0); } private void UpdateHotspots() { var mlliseconds = ActualWidth * 0.5 / _itemWidth * _timeSpeList[_speIndex]; if (double.IsNaN(mlliseconds) || double.IsInfinity(mlliseconds)) return; _panelHotspots.Children.Clear(); foreach (var rect in GetHotspotsRectangle(mlliseconds)) { _panelHotspots.Children.Add(rect); } } private void BorderTop_OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) => _borderTopIsMouseLeftButtonDown = true; private void BorderTop_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (_borderTopIsMouseLeftButtonDown) { _borderTopIsMouseLeftButtonDown = false; var p = Mouse.GetPosition(this); _tempOffsetX = ActualWidth / 2 - p.X; SelectedTime -= TimeSpan.FromMilliseconds(_tempOffsetX / _itemWidth * _timeSpeList[_speIndex]); _totalOffsetX = (_totalOffsetX + _tempOffsetX) % ActualWidth; _tempOffsetX = 0; UpdateMouseFollowBlockPos(); RaiseEvent(new FunctionEventArgs(TimeChangedEvent, this) { Info = SelectedTime }); } } private IEnumerable GetHotspotsRectangle(double mlliseconds) { var timeSpan = TimeSpan.FromMilliseconds(mlliseconds); var selectedTime = SelectedTime; var start = selectedTime - timeSpan; var end = selectedTime + timeSpan; var set = _dateTimeRanges.GetViewBetween(new DateTimeRange(start), new DateTimeRange(end)); var unitLength = ActualWidth / mlliseconds * 0.5; foreach (var range in set) { var width = range.TotalMilliseconds * unitLength; var sub = range.Start - start; var x = sub.TotalMilliseconds * unitLength; yield return new Rectangle { Fill = HotspotsBrush, Height = 4, Width = width, Margin = new Thickness(x, 0, 0, 0), HorizontalAlignment = HorizontalAlignment.Left }; } } }