using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; namespace Melskin.Controls { public class TutorialGuide : Control { public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TutorialGuide), new PropertyMetadata("Tutorial")); public string Title { get => (string)GetValue(TitleProperty); set => SetValue(TitleProperty, value); } public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(TutorialGuide), new PropertyMetadata(string.Empty)); public string Description { get => (string)GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } public static readonly DependencyProperty StepTextProperty = DependencyProperty.Register(nameof(StepText), typeof(string), typeof(TutorialGuide), new PropertyMetadata("1 / 1")); public string StepText { get => (string)GetValue(StepTextProperty); set => SetValue(StepTextProperty, value); } // 路由事件 public static readonly RoutedEvent NextEvent = EventManager.RegisterRoutedEvent(nameof(Next), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TutorialGuide)); public event RoutedEventHandler Next { add => AddHandler(NextEvent, value); remove => RemoveHandler(NextEvent, value); } public static readonly RoutedEvent BackEvent = EventManager.RegisterRoutedEvent(nameof(Back), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TutorialGuide)); public event RoutedEventHandler Back { add => AddHandler(BackEvent, value); remove => RemoveHandler(BackEvent, value); } public static readonly RoutedEvent CloseEvent = EventManager.RegisterRoutedEvent(nameof(Close), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(TutorialGuide)); public event RoutedEventHandler Close { add => AddHandler(CloseEvent, value); remove => RemoveHandler(CloseEvent, value); } public static readonly DependencyProperty PlacementProperty = DependencyProperty.Register("Placement", typeof(PlacementMode), typeof(TutorialGuide), new PropertyMetadata(PlacementMode.Top)); public PlacementMode Placement { get => (PlacementMode)GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); } static TutorialGuide() { DefaultStyleKeyProperty.OverrideMetadata(typeof(TutorialGuide), new FrameworkPropertyMetadata(typeof(TutorialGuide))); } public override void OnApplyTemplate() { base.OnApplyTemplate(); if (GetTemplateChild("PART_NextBtn") is Button next) next.Click += (s, e) => RaiseEvent(new RoutedEventArgs(NextEvent)); if (GetTemplateChild("PART_BackBtn") is Button back) back.Click += (s, e) => RaiseEvent(new RoutedEventArgs(BackEvent)); if (GetTemplateChild("PART_CloseBtn") is Button close) close.Click += (s, e) => RaiseEvent(new RoutedEventArgs(CloseEvent)); } } public class TutorialHighlighter : Control { static TutorialHighlighter() { DefaultStyleKeyProperty.OverrideMetadata(typeof(TutorialHighlighter), new FrameworkPropertyMetadata(typeof(TutorialHighlighter))); } // 目标控件属性 public static readonly DependencyProperty TargetElementProperty = DependencyProperty.Register("TargetElement", typeof(FrameworkElement), typeof(TutorialHighlighter), new PropertyMetadata(null, OnTargetChanged)); public FrameworkElement TargetElement { get => (FrameworkElement)GetValue(TargetElementProperty); set => SetValue(TargetElementProperty, value); } private static void OnTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TutorialHighlighter helper) helper.UpdatePosition(); } public void UpdatePosition() { if (TargetElement == null) return; // 获取 TutorialLayer (也就是当前的父容器 Canvas) var parentCanvas = VisualTreeHelper.GetParent(this) as UIElement; if (parentCanvas == null) return; // 关键修改:相对于 parentCanvas 计算坐标,而不是相对于 Window Point location = TargetElement.TranslatePoint(new Point(0, 0), parentCanvas); // 加上偏移量(让边框比目标稍微大一点,包围住目标) double offset = 4; Canvas.SetLeft(this, location.X - offset); Canvas.SetTop(this, location.Y - offset); this.Width = TargetElement.ActualWidth + (offset * 2); this.Height = TargetElement.ActualHeight + (offset * 2); } } public class TutorialLayer : Canvas { private TutorialHighlighter _highlighter; private TutorialGuide _guide; public TutorialLayer() { // 初始化两个组件 _highlighter = new TutorialHighlighter(); _guide = new TutorialGuide(); this.Children.Add(_highlighter); this.Children.Add(_guide); // 初始隐藏 this.Visibility = Visibility.Collapsed; } public void ShowStep(FrameworkElement target, string title, string content, string step) { this.Visibility = Visibility.Visible; // 1. 更新文字 _guide.Title = title; _guide.Description = content; _guide.StepText = step; // 2. 强制刷新布局(确保目标控件的 ActualWidth/Height 已计算) target.UpdateLayout(); // 3. 定位高亮框 (Highlighter) _highlighter.TargetElement = target; _highlighter.UpdatePosition(); // 4. 定位提示框 (Guide) // 获取高亮框在当前 Canvas 里的位置 double hLeft = Canvas.GetLeft(_highlighter); double hTop = Canvas.GetTop(_highlighter); double hWidth = _highlighter.Width; double hHeight = _highlighter.Height; // 默认逻辑:放在目标右侧。如果右侧放不下,放在左侧。 double guideLeft = hLeft + hWidth + 10; double guideTop = hTop; if (guideLeft + 350 > this.ActualWidth) // 350 是 TutorialGuide 的宽度 { guideLeft = hLeft - 350 - 10; // 放左边 } Canvas.SetLeft(_guide, guideLeft); Canvas.SetTop(_guide, guideTop); } public void Close() => this.Visibility = Visibility.Collapsed; } }