diff --git a/Melskin/Controls/Button.xaml b/Melskin/Controls/Button.xaml index 8f67c8f..ccb3e60 100644 --- a/Melskin/Controls/Button.xaml +++ b/Melskin/Controls/Button.xaml @@ -526,6 +526,7 @@ + diff --git a/Melskin/Controls/Card.xaml b/Melskin/Controls/Card.xaml index f85aca6..0d9444d 100644 --- a/Melskin/Controls/Card.xaml +++ b/Melskin/Controls/Card.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:Melskin.Controls" xmlns:decorations="clr-namespace:Melskin.Controls.Decorations"> - + + \ No newline at end of file diff --git a/Melskin/Controls/CheckBox.xaml b/Melskin/Controls/CheckBox.xaml index a38952c..20a0e3d 100644 --- a/Melskin/Controls/CheckBox.xaml +++ b/Melskin/Controls/CheckBox.xaml @@ -186,7 +186,7 @@ - + diff --git a/Melskin/Controls/ChooseBox.xaml b/Melskin/Controls/ChooseBox.xaml index 97a05d9..5d10e6a 100644 --- a/Melskin/Controls/ChooseBox.xaml +++ b/Melskin/Controls/ChooseBox.xaml @@ -148,7 +148,7 @@ - + @@ -162,7 +162,7 @@ x:Name="ContentBorder" MinWidth="{TemplateBinding MinWidth}" MinHeight="{TemplateBinding MinHeight}" - Padding="0" + Padding="{TemplateBinding Padding}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="{TemplateBinding Background}" @@ -178,6 +178,7 @@ Name="PlaceholderTextBlock" Margin="5,0,0,0" VerticalAlignment="Center" + Focusable="False" FontSize="{TemplateBinding FontSize}" Foreground="{DynamicResource TextPlaceholderBrush}" Text="{TemplateBinding PlaceholderText}" @@ -185,7 +186,6 @@ - diff --git a/Melskin/Controls/MultiComboBox.xaml b/Melskin/Controls/MultiComboBox.xaml index 09e32d4..93b1737 100644 --- a/Melskin/Controls/MultiComboBox.xaml +++ b/Melskin/Controls/MultiComboBox.xaml @@ -73,7 +73,8 @@ - + + diff --git a/Melskin/Controls/MultiComboBox.xaml.cs b/Melskin/Controls/MultiComboBox.xaml.cs index d05dedd..4c732ad 100644 --- a/Melskin/Controls/MultiComboBox.xaml.cs +++ b/Melskin/Controls/MultiComboBox.xaml.cs @@ -286,7 +286,7 @@ public class MultiComboBox : Control /// /// 该属性包含了从数据源中筛选出的项,这些项将被实际显示在组合框中。通过设置此属性,可以控制哪些项最终展示给用户。当数据源发生变化或应用了新的筛选条件时,FilteredItems 会相应地更新。 /// - public IEnumerable FilteredItems + public IEnumerable? FilteredItems { get => (IEnumerable)GetValue(FilteredItemsProperty); set => SetValue(FilteredItemsProperty, value); diff --git a/Melskin/Controls/ProgressBar.xaml b/Melskin/Controls/ProgressBar.xaml index 1b232ae..77e0977 100644 --- a/Melskin/Controls/ProgressBar.xaml +++ b/Melskin/Controls/ProgressBar.xaml @@ -201,6 +201,196 @@ + + + + + + + + + + + + + + + + + + + + + @@ -210,50 +400,43 @@ + - - - - - - - - - - - + + + + - + + + + - - + + + + @@ -264,6 +447,8 @@ + + @@ -273,47 +458,66 @@ + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - - + + + + @@ -324,6 +528,87 @@ + + + + + + \ No newline at end of file diff --git a/Melskin/Controls/StepBar.xaml.cs b/Melskin/Controls/StepBar.xaml.cs new file mode 100644 index 0000000..d16d45e --- /dev/null +++ b/Melskin/Controls/StepBar.xaml.cs @@ -0,0 +1,124 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace Melskin.Controls +{ + public class StepBar : ItemsControl + { + // 使用 StepProgress 代替 SelectedIndex,允许值范围 0 到 Items.Count + public static readonly DependencyProperty StepProgressProperty = + DependencyProperty.Register("StepProgress", typeof(int), typeof(StepBar), + new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnStepProgressChanged)); + + public int StepProgress + { + get => (int)GetValue(StepProgressProperty); + set => SetValue(StepProgressProperty, value); + } + + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register("Orientation", typeof(Orientation), typeof(StepBar), + new PropertyMetadata(Orientation.Horizontal)); + + public Orientation Orientation + { + get => (Orientation)GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + static StepBar() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(StepBar), new FrameworkPropertyMetadata(typeof(StepBar))); + } + + private static void OnStepProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is StepBar bar) bar.UpdateItemsStatus(); + } + + protected override DependencyObject GetContainerForItemOverride() => new StepBarItem(); + protected override bool IsItemItsOwnContainerOverride(object item) => item is StepBarItem; + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + base.PrepareContainerForItemOverride(element, item); + UpdateItemsStatus(); + } + + internal void UpdateItemsStatus() + { + for (int i = 0; i < Items.Count; i++) + { + if (ItemContainerGenerator.ContainerFromIndex(i) is StepBarItem container) + { + container.Index = i + 1; + container.IsLast = (i == Items.Count - 1); + container.Orientation = this.Orientation; + + // 核心逻辑: + // i < Progress -> 已完成 (Completed) + // i == Progress -> 进行中 (Active) + // i > Progress -> 等待 (Waiting) + if (i < StepProgress) + container.Status = StepStatus.Completed; + else if (i == StepProgress) + container.Status = StepStatus.Active; + else + container.Status = StepStatus.Waiting; + } + } + } + } + + public class StepBarItem : ContentControl + { + public static readonly DependencyProperty StatusProperty = DependencyProperty.Register("Status", typeof(StepStatus), typeof(StepBarItem), new PropertyMetadata(StepStatus.Waiting)); + public StepStatus Status { get => (StepStatus)GetValue(StatusProperty); set => SetValue(StatusProperty, value); } + + public static readonly DependencyProperty IndexProperty = DependencyProperty.Register("Index", typeof(int), typeof(StepBarItem), new PropertyMetadata(1)); + public int Index { get => (int)GetValue(IndexProperty); set => SetValue(IndexProperty, value); } + + public static readonly DependencyProperty IsLastProperty = DependencyProperty.Register("IsLast", typeof(bool), typeof(StepBarItem), new PropertyMetadata(false)); + public bool IsLast { get => (bool)GetValue(IsLastProperty); set => SetValue(IsLastProperty, value); } + + public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(StepBarItem), new PropertyMetadata(Orientation.Horizontal)); + public Orientation Orientation { get => (Orientation)GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } + + static StepBarItem() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(StepBarItem), new FrameworkPropertyMetadata(typeof(StepBarItem))); + } + + protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) + { + base.OnMouseLeftButtonDown(e); + var parent = ItemsControl.ItemsControlFromItemContainer(this) as StepBar; + if (parent != null) + { + int myIndex = parent.ItemContainerGenerator.IndexFromContainer(this); + + // 点击逻辑: + // 1. 如果点的是当前正在进行的步 -> 完成这一步,进度+1 + // 2. 如果点的是已经完成的步 -> 回退到那一步 + // 3. 如果点的是还没开始的步 -> 直接跳到那一步 + if (parent.StepProgress == myIndex) + { + parent.StepProgress = myIndex + 1; + } + else + { + parent.StepProgress = myIndex; + } + } + } + } +} + +public enum StepStatus +{ + Completed, + Active, + Waiting +} diff --git a/Melskin/Controls/TextBox.xaml b/Melskin/Controls/TextBox.xaml index 9e4c83a..f57a650 100644 --- a/Melskin/Controls/TextBox.xaml +++ b/Melskin/Controls/TextBox.xaml @@ -379,6 +379,7 @@ + diff --git a/Melskin/Controls/TutorialPrompt.xaml b/Melskin/Controls/TutorialPrompt.xaml new file mode 100644 index 0000000..9fa9010 --- /dev/null +++ b/Melskin/Controls/TutorialPrompt.xaml @@ -0,0 +1,149 @@ + + + + + \ No newline at end of file diff --git a/Melskin/Controls/TutorialPrompt.xaml.cs b/Melskin/Controls/TutorialPrompt.xaml.cs new file mode 100644 index 0000000..55ca87e --- /dev/null +++ b/Melskin/Controls/TutorialPrompt.xaml.cs @@ -0,0 +1,147 @@ +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; + } +} \ No newline at end of file diff --git a/Melskin/Converters/Internal/ProgressToAngleConverter.cs b/Melskin/Converters/Internal/ProgressToAngleConverter.cs new file mode 100644 index 0000000..9c49868 --- /dev/null +++ b/Melskin/Converters/Internal/ProgressToAngleConverter.cs @@ -0,0 +1,62 @@ +using System.Globalization; +using System.Windows.Data; + +namespace Melskin.Converters.Internal +{ + internal class ProgressToAngleConverter : IMultiValueConverter + { + public readonly static ProgressToAngleConverter Instance = new ProgressToAngleConverter(); + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + // 防御性检查:检查 UnsetValue 或 Null + if (values == null || values.Length < 2 || + values[0] == System.Windows.DependencyProperty.UnsetValue || values[0] == null || + values[1] == System.Windows.DependencyProperty.UnsetValue || values[1] == null) + { + return System.Windows.Media.Geometry.Empty; + } + + double value = System.Convert.ToDouble(values[0]); + double max = System.Convert.ToDouble(values[1]); + + if (max <= 0) return System.Windows.Media.Geometry.Empty; + + // 确保百分比在 0-0.9999 之间 + double percent = (value / max) <= 0 ? 0 : (value / max) >= 1 ? 0.9999 : value / max; + double angle = percent * 360; + + double size = 100; + double stroke = 10; + double center = size / 2; + double radius = (size - stroke) / 2; // 半径 45 + + double rad = (angle - 90) * Math.PI / 180; + double x = center + radius * Math.Cos(rad); + double y = center + radius * Math.Sin(rad); + + bool isLargeArc = angle > 180; + + Point startPoint = new Point(center, center - radius); + Point endPoint = new Point(x, y); + + PathFigure figure = new PathFigure { StartPoint = startPoint, IsClosed = false }; + figure.Segments.Add(new ArcSegment( + endPoint, + new Size(radius, radius), + 0, + isLargeArc, + SweepDirection.Clockwise, + true)); + + return new PathGeometry(new[] { figure }); + } + + + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Melskin/Melskin.csproj b/Melskin/Melskin.csproj index eb4f607..e0c1f98 100644 --- a/Melskin/Melskin.csproj +++ b/Melskin/Melskin.csproj @@ -16,7 +16,7 @@ WPF;Theme;Controls MIT False - True + False WPF 自定义控件库,Meld Skin AnyCPU;x64 diff --git a/Melskin/Themes/Dark.xaml b/Melskin/Themes/Dark.xaml index cfc6208..9c9204a 100644 --- a/Melskin/Themes/Dark.xaml +++ b/Melskin/Themes/Dark.xaml @@ -131,7 +131,7 @@ #24272C - #57606f + #E61A1A1A #2C3036 @@ -146,10 +146,10 @@ 用于控件的不同交互状态(正常、悬停、按压、选中、禁用) --> - #282828 + #1E2124 - #333333 + #2C2F33 #3D3D3D diff --git a/Melskin/Themes/Styles.xaml b/Melskin/Themes/Styles.xaml index 00134e7..7055ca1 100644 --- a/Melskin/Themes/Styles.xaml +++ b/Melskin/Themes/Styles.xaml @@ -42,6 +42,8 @@ + + diff --git a/MelskinTest/MainWindow.xaml b/MelskinTest/MainWindow.xaml index fd60077..d68bac0 100644 --- a/MelskinTest/MainWindow.xaml +++ b/MelskinTest/MainWindow.xaml @@ -466,6 +466,10 @@ + + + + @@ -679,8 +683,16 @@ - + + + + + + + + + - + - + Value="50" /> + + + + + + + + + + + + + + + + + + + +