Files
ShrlAlgoToolkit/Melskin/Controls/TutorialPrompt.xaml.cs
2026-02-22 20:03:42 +08:00

147 lines
6.7 KiB
C#

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;
}
}