147 lines
6.7 KiB
C#
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;
|
|
}
|
|
} |