XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

设置页面结构升级与代码高亮控件集成

新增 SettingsCard/SettingsExpander 控件及样式,统一设置项结构和分组折叠,提升页面一致性。SettingPage 全面替换为新结构,远程调试项支持分组与联动显示。新增 XamlCodeViewer 控件,实现只读 XAML 语法高亮,控件库示例区集成。补充自动化测试,完善递归加载逻辑。控制台配置支持独立远程密码。统一滚动条样式,修复 TimePicker 问题。版本号升级至 1.0.1。

769f049
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

20 个文件 +1369 -146
Modified XFEToolBox.Client.Wpf.Test/Program.cs +155 -0
@@ -2,6 +2,8 @@ using System.Diagnostics;
2 2 using System.Runtime.InteropServices;
3 3 using System.Windows;
4 4 using System.Windows.Controls;
5 using System.Windows.Controls.Primitives;
6 using System.Windows.Documents;
5 7 using System.Windows.Interop;
6 8 using System.Windows.Media;
7 9 using System.Windows.Media.Imaging;
@@ -15,6 +17,143 @@ namespace XFEToolBox.Client.Wpf.Test;
15 17
16 18 public class Program
17 19 {
20 [Test]
21 public static void TimePickerIncrementOneKeepsTheWholeScrollTrackUsable()
22 {
23 Exception? failure = null;
24 var thread = new Thread(() =>
25 {
26 try
27 {
28 var dispatcher = Dispatcher.CurrentDispatcher;
29 var picker = new TimePicker
30 {
31 Width = 300,
32 MinuteIncrement = 1,
33 SecondIncrement = 1,
34 ShowSecond = true,
35 SelectedTime = new TimeSpan(12, 24, 30),
36 IsDropDownOpen = true
37 };
38 var window = new Window
39 {
40 Width = 420,
41 Height = 360,
42 Left = -10_000,
43 Top = -10_000,
44 ShowInTaskbar = false,
45 WindowStartupLocation = WindowStartupLocation.Manual,
46 Content = picker
47 };
48 window.Resources.MergedDictionaries.Add(new ResourceDictionary
49 {
50 Source = new Uri("/XFEToolBox.WpfCore;component/Resources/Style/ToolThemeResources.xaml", UriKind.Relative)
51 });
52 window.Show();
53 PumpDispatcher(dispatcher);
54 window.UpdateLayout();
55
56 picker.ApplyTemplate();
57 var minuteList = (ListBox?)picker.Template.FindName("PART_MinuteList", picker);
58 Ensure(minuteList is not null && minuteList.Items.Count == 60,
59 "步长为 1 时分钟列没有生成完整的 60 个候选值。");
60 minuteList!.ApplyTemplate();
61 window.UpdateLayout();
62
63 var scrollViewer = FindVisualDescendant<ScrollViewer>(minuteList);
64 var scrollBar = FindVisualDescendants<ScrollBar>(minuteList)
65 .FirstOrDefault(candidate => candidate.Orientation == Orientation.Vertical && candidate.Visibility == Visibility.Visible);
66 Ensure(scrollViewer is not null && scrollBar is not null,
67 "步长为 1 时分钟列没有显示纵向滚动条。");
68
69 scrollBar!.ApplyTemplate();
70 var track = (Track?)scrollBar.Template.FindName("PART_Track", scrollBar);
71 Ensure(track is not null, "滚动条模板缺少 PART_Track,ScrollBar 无法同步完整滚动范围。");
72 Ensure(track!.ActualHeight > 0 && track.Thumb.ActualHeight > 0,
73 "滚动轨道或滑块没有完成布局。");
74
75 scrollViewer!.ScrollToEnd();
76 PumpDispatcher(dispatcher);
77 window.UpdateLayout();
78 Ensure(scrollViewer.VerticalOffset >= scrollViewer.ScrollableHeight - 0.5,
79 "分钟列无法滚动到最后一个候选值。");
80 Ensure(Math.Abs(track.Value - track.Maximum) <= 0.5,
81 $"滑块没有到达滚动范围底部:{track.Value:N2} / {track.Maximum:N2}。");
82 var thumbBottom = track.Thumb.TranslatePoint(
83 new Point(0, track.Thumb.ActualHeight), track).Y;
84 Ensure(thumbBottom <= track.ActualHeight + 0.5 && thumbBottom >= track.ActualHeight - 0.5,
85 $"滑块下半部被裁切或未到达轨道底部:{thumbBottom:N2} / {track.ActualHeight:N2}。");
86
87 scrollViewer.ScrollToTop();
88 PumpDispatcher(dispatcher);
89 window.UpdateLayout();
90 var thumbTop = track.Thumb.TranslatePoint(new Point(0, 0), track).Y;
91 Ensure(Math.Abs(track.Value - track.Minimum) <= 0.5 && Math.Abs(thumbTop) <= 0.5,
92 "滑块无法返回滚动范围顶部。");
93
94 window.Close();
95 dispatcher.InvokeShutdown();
96 }
97 catch (Exception exception)
98 {
99 failure = exception;
100 }
101 })
102 {
103 IsBackground = true
104 };
105
106 thread.SetApartmentState(ApartmentState.STA);
107 thread.Start();
108 Ensure(thread.Join(TimeSpan.FromSeconds(15)), "TimePicker 步长 1 的滚动测试超时。");
109 if (failure is not null)
110 {
111 Console.WriteLine(failure);
112 throw new InvalidOperationException("TimePicker 步长为 1 时滚动范围不完整。", failure);
113 }
114 }
115
116 [Test]
117 public static void XamlCodeViewerRendersDistinctSyntaxTokens()
118 {
119 Exception? failure = null;
120 var thread = new Thread(() =>
121 {
122 try
123 {
124 var viewer = new XamlCodeViewer
125 {
126 Text = "<!-- 示例 -->\n<controls:CommandPreviewBox Label=\"等价命令\" IsSyntaxHighlightingEnabled=\"True\" />"
127 };
128 var paragraph = viewer.Document.Blocks.OfType<Paragraph>().Single();
129 var runs = paragraph.Inlines.OfType<Run>().ToArray();
130 var distinctColors = runs
131 .Select(run => (run.Foreground as SolidColorBrush)?.Color)
132 .Where(color => color.HasValue)
133 .Distinct()
134 .Count();
135
136 Ensure(runs.Any(run => run.Text == "controls:CommandPreviewBox"), "XAML 元素名称没有被独立分词。");
137 Ensure(runs.Any(run => run.Text == "Label"), "XAML 属性名称没有被独立分词。");
138 Ensure(runs.Any(run => run.Text == "\"等价命令\""), "XAML 属性值没有被独立分词。");
139 Ensure(distinctColors >= 5, $"XAML 语法颜色不足:仅检测到 {distinctColors} 种颜色。");
140 }
141 catch (Exception exception)
142 {
143 failure = exception;
144 }
145 })
146 {
147 IsBackground = true
148 };
149
150 thread.SetApartmentState(ApartmentState.STA);
151 thread.Start();
152 Ensure(thread.Join(TimeSpan.FromSeconds(10)), "XAML 代码查看器测试超时。");
153 if (failure is not null)
154 throw new InvalidOperationException("XAML 代码查看器没有正确渲染语法颜色。", failure);
155 }
156
18 157 [Test]
19 158 public static void TabAndNavigationOutlinesStayInsideTheirLayoutBounds()
20 159 {
@@ -365,6 +504,22 @@ public class Program
365 504 Dispatcher.PushFrame(frame);
366 505 }
367 506
507 private static T? FindVisualDescendant<T>(DependencyObject root) where T : DependencyObject
508 => FindVisualDescendants<T>(root).FirstOrDefault();
509
510 private static IEnumerable<T> FindVisualDescendants<T>(DependencyObject root) where T : DependencyObject
511 {
512 for (var index = 0; index < VisualTreeHelper.GetChildrenCount(root); index++)
513 {
514 var child = VisualTreeHelper.GetChild(root, index);
515 if (child is T match)
516 yield return match;
517
518 foreach (var descendant in FindVisualDescendants<T>(child))
519 yield return descendant;
520 }
521 }
522
368 523 private static void EnsureVerticallyMirroredCorners(FrameworkElement element, int sampleSize)
369 524 {
370 525 var width = Math.Max(1, (int)Math.Ceiling(element.ActualWidth));
Added XFEToolBox.Client.Wpf.Test/SettingsControlsTests.cs +136 -0
@@ -0,0 +1,136 @@
1 using System.Windows;
2 using System.Windows.Controls;
3 using System.Windows.Controls.Primitives;
4 using System.Windows.Data;
5 using System.Windows.Threading;
6 using XFEToolBox.WpfCore.Controls;
7
8 namespace XFEToolBox.Client.Wpf.Test;
9
10 public static class SettingsControlsTests
11 {
12 [Test]
13 public static void RemoteInputsAreOnlyVisibleWhileRemoteModeIsEnabled()
14 {
15 Exception? failure = null;
16 var thread = new Thread(() =>
17 {
18 try
19 {
20 var dispatcher = Dispatcher.CurrentDispatcher;
21 var remoteModeSwitch = new SwitchButton { IsChecked = false, Margin = new Thickness(0) };
22 var addressEditor = new TextEditor { Text = "ws://localhost:3280/" };
23 var passwordEditor = new PasswordEditor { Password = "test-password" };
24 var addressCard = new SettingsCard
25 {
26 Header = "远程服务器地址",
27 Description = "支持 ws:// 与 wss:// 地址",
28 Content = addressEditor
29 };
30 var passwordCard = new SettingsCard
31 {
32 Header = "远程服务器连接密码",
33 Content = passwordEditor
34 };
35 var expander = new SettingsExpander
36 {
37 Header = "远程调试模式",
38 Description = "由工具箱主动连接调试程序服务器",
39 Content = remoteModeSwitch,
40 IsHeaderClickEnabled = false
41 };
42 expander.Items.Add(addressCard);
43 expander.Items.Add(passwordCard);
44 BindingOperations.SetBinding(expander, SettingsExpander.IsExpandedProperty, new Binding(nameof(ToggleButton.IsChecked))
45 {
46 Source = remoteModeSwitch,
47 Mode = BindingMode.OneWay
48 });
49
50 var expandedCount = 0;
51 var collapsedCount = 0;
52 expander.Expanded += (_, _) => expandedCount++;
53 expander.Collapsed += (_, _) => collapsedCount++;
54
55 var window = new Window
56 {
57 Width = 720,
58 Height = 420,
59 Left = -10_000,
60 Top = -10_000,
61 ShowInTaskbar = false,
62 WindowStartupLocation = WindowStartupLocation.Manual,
63 Content = expander
64 };
65 window.Resources.MergedDictionaries.Add(new ResourceDictionary
66 {
67 Source = new Uri("/XFEToolBox.WpfCore;component/Resources/Style/ToolThemeResources.xaml", UriKind.Relative)
68 });
69 window.Show();
70 PumpDispatcher(dispatcher);
71 window.UpdateLayout();
72
73 expander.ApplyTemplate();
74 addressCard.ApplyTemplate();
75 passwordCard.ApplyTemplate();
76 var itemsSite = (Border?)expander.Template.FindName("ItemsSite", expander);
77 Ensure(expander.Template is not null && addressCard.Template is not null && passwordCard.Template is not null,
78 "SettingsCard 或 SettingsExpander 没有加载统一主题模板。");
79 Ensure(itemsSite is not null, "SettingsExpander 模板缺少内部设置区域。");
80 var realizedItemsSite = itemsSite!;
81 Ensure(expander.Items.Count == 2, "SettingsExpander 没有保留内部设置项。");
82 Ensure(realizedItemsSite.Visibility == Visibility.Collapsed && !addressEditor.IsVisible && !passwordEditor.IsVisible,
83 "远程调试关闭时,远程服务器设置仍然可见或可输入。");
84
85 remoteModeSwitch.IsChecked = true;
86 PumpDispatcher(dispatcher);
87 window.UpdateLayout();
88 Ensure(expander.IsExpanded && realizedItemsSite.Visibility == Visibility.Visible,
89 "打开远程调试后,远程服务器设置没有展开。");
90 Ensure(addressEditor.IsVisible && passwordEditor.IsVisible && addressEditor.IsEnabled && passwordEditor.IsEnabled,
91 "打开远程调试后,远程服务器地址或密码仍不可输入。");
92
93 remoteModeSwitch.IsChecked = false;
94 PumpDispatcher(dispatcher);
95 window.UpdateLayout();
96 Ensure(!expander.IsExpanded && realizedItemsSite.Visibility == Visibility.Collapsed,
97 "关闭远程调试后,远程服务器设置没有收起。");
98 Ensure(expandedCount == 1 && collapsedCount == 1,
99 $"SettingsExpander 展开/收起事件次数异常:{expandedCount}/{collapsedCount}。");
100
101 window.Close();
102 dispatcher.InvokeShutdown();
103 }
104 catch (Exception exception)
105 {
106 failure = exception;
107 }
108 })
109 {
110 IsBackground = true
111 };
112
113 thread.SetApartmentState(ApartmentState.STA);
114 thread.Start();
115 Ensure(thread.Join(TimeSpan.FromSeconds(15)), "SettingsExpander 远程模式联动测试超时。");
116 if (failure is not null)
117 {
118 Console.WriteLine(failure);
119 throw new InvalidOperationException("SettingsExpander 没有正确限制远程设置的输入状态。", failure);
120 }
121 }
122
123 private static void Ensure(bool condition, string message)
124 {
125 if (!condition)
126 throw new InvalidOperationException(message);
127 }
128
129 private static void PumpDispatcher(Dispatcher dispatcher)
130 {
131 var frame = new DispatcherFrame();
132 _ = dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => frame.Continue = false));
133 Dispatcher.PushFrame(frame);
134 }
135
136 }
Added XFEToolBox.WpfCore/Controls/SettingsCard.cs +150 -0
@@ -0,0 +1,150 @@
1 using System.Windows;
2 using System.Windows.Controls;
3 using System.Windows.Input;
4
5 namespace XFEToolBox.WpfCore.Controls;
6
7 /// <summary>
8 /// 设置卡片内容的布局方向。
9 /// </summary>
10 public enum SettingsCardContentAlignment
11 {
12 /// <summary>操作控件显示在标题右侧。</summary>
13 Right,
14 /// <summary>仅显示内容并将其左对齐。</summary>
15 Left,
16 /// <summary>操作控件显示在标题和说明下方。</summary>
17 Vertical
18 }
19
20 /// <summary>
21 /// 以统一的标题、说明、图标和操作区域展示单项设置。
22 /// </summary>
23 public class SettingsCard : HeaderedContentControl
24 {
25 public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
26 nameof(Description), typeof(object), typeof(SettingsCard), new PropertyMetadata(null));
27
28 public static readonly DependencyProperty DescriptionTemplateProperty = DependencyProperty.Register(
29 nameof(DescriptionTemplate), typeof(DataTemplate), typeof(SettingsCard), new PropertyMetadata(null));
30
31 public static readonly DependencyProperty HeaderIconProperty = DependencyProperty.Register(
32 nameof(HeaderIcon), typeof(object), typeof(SettingsCard), new PropertyMetadata(null));
33
34 public static readonly DependencyProperty HeaderIconTemplateProperty = DependencyProperty.Register(
35 nameof(HeaderIconTemplate), typeof(DataTemplate), typeof(SettingsCard), new PropertyMetadata(null));
36
37 public static readonly DependencyProperty ContentAlignmentProperty = DependencyProperty.Register(
38 nameof(ContentAlignment), typeof(SettingsCardContentAlignment), typeof(SettingsCard),
39 new PropertyMetadata(SettingsCardContentAlignment.Right));
40
41 public static readonly DependencyProperty IsClickEnabledProperty = DependencyProperty.Register(
42 nameof(IsClickEnabled), typeof(bool), typeof(SettingsCard), new PropertyMetadata(false));
43
44 public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
45 nameof(Command), typeof(ICommand), typeof(SettingsCard), new PropertyMetadata(null));
46
47 public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
48 nameof(CommandParameter), typeof(object), typeof(SettingsCard), new PropertyMetadata(null));
49
50 public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register(
51 nameof(CommandTarget), typeof(IInputElement), typeof(SettingsCard), new PropertyMetadata(null));
52
53 public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
54 nameof(Click), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsCard));
55
56 public object? Description
57 {
58 get => GetValue(DescriptionProperty);
59 set => SetValue(DescriptionProperty, value);
60 }
61
62 public DataTemplate? DescriptionTemplate
63 {
64 get => (DataTemplate?)GetValue(DescriptionTemplateProperty);
65 set => SetValue(DescriptionTemplateProperty, value);
66 }
67
68 public object? HeaderIcon
69 {
70 get => GetValue(HeaderIconProperty);
71 set => SetValue(HeaderIconProperty, value);
72 }
73
74 public DataTemplate? HeaderIconTemplate
75 {
76 get => (DataTemplate?)GetValue(HeaderIconTemplateProperty);
77 set => SetValue(HeaderIconTemplateProperty, value);
78 }
79
80 public SettingsCardContentAlignment ContentAlignment
81 {
82 get => (SettingsCardContentAlignment)GetValue(ContentAlignmentProperty);
83 set => SetValue(ContentAlignmentProperty, value);
84 }
85
86 public bool IsClickEnabled
87 {
88 get => (bool)GetValue(IsClickEnabledProperty);
89 set => SetValue(IsClickEnabledProperty, value);
90 }
91
92 public ICommand? Command
93 {
94 get => (ICommand?)GetValue(CommandProperty);
95 set => SetValue(CommandProperty, value);
96 }
97
98 public object? CommandParameter
99 {
100 get => GetValue(CommandParameterProperty);
101 set => SetValue(CommandParameterProperty, value);
102 }
103
104 public IInputElement? CommandTarget
105 {
106 get => (IInputElement?)GetValue(CommandTargetProperty);
107 set => SetValue(CommandTargetProperty, value);
108 }
109
110 public event RoutedEventHandler Click
111 {
112 add => AddHandler(ClickEvent, value);
113 remove => RemoveHandler(ClickEvent, value);
114 }
115
116 protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
117 {
118 base.OnMouseLeftButtonUp(e);
119 if (IsClickEnabled && !e.Handled)
120 {
121 InvokeClick();
122 e.Handled = true;
123 }
124 }
125
126 protected override void OnKeyUp(KeyEventArgs e)
127 {
128 base.OnKeyUp(e);
129 if (IsClickEnabled && e.Key is Key.Enter or Key.Space)
130 {
131 InvokeClick();
132 e.Handled = true;
133 }
134 }
135
136 private void InvokeClick()
137 {
138 RaiseEvent(new RoutedEventArgs(ClickEvent, this));
139 if (Command is RoutedCommand routedCommand)
140 {
141 var commandTarget = CommandTarget ?? this;
142 if (routedCommand.CanExecute(CommandParameter, commandTarget))
143 routedCommand.Execute(CommandParameter, commandTarget);
144 }
145 else if (Command?.CanExecute(CommandParameter) == true)
146 {
147 Command.Execute(CommandParameter);
148 }
149 }
150 }
Added XFEToolBox.WpfCore/Controls/SettingsExpander.cs +125 -0
@@ -0,0 +1,125 @@
1 using System.Windows;
2 using System.Windows.Controls;
3
4 namespace XFEToolBox.WpfCore.Controls;
5
6 /// <summary>
7 /// 将相关的 <see cref="SettingsCard"/> 设置项组织为可折叠组。
8 /// </summary>
9 public class SettingsExpander : HeaderedItemsControl
10 {
11 public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
12 nameof(Description), typeof(object), typeof(SettingsExpander), new PropertyMetadata(null));
13
14 public static readonly DependencyProperty DescriptionTemplateProperty = DependencyProperty.Register(
15 nameof(DescriptionTemplate), typeof(DataTemplate), typeof(SettingsExpander), new PropertyMetadata(null));
16
17 public static readonly DependencyProperty HeaderIconProperty = DependencyProperty.Register(
18 nameof(HeaderIcon), typeof(object), typeof(SettingsExpander), new PropertyMetadata(null));
19
20 public static readonly DependencyProperty HeaderIconTemplateProperty = DependencyProperty.Register(
21 nameof(HeaderIconTemplate), typeof(DataTemplate), typeof(SettingsExpander), new PropertyMetadata(null));
22
23 public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(
24 nameof(Content), typeof(object), typeof(SettingsExpander), new PropertyMetadata(null));
25
26 public static readonly DependencyProperty ContentTemplateProperty = DependencyProperty.Register(
27 nameof(ContentTemplate), typeof(DataTemplate), typeof(SettingsExpander), new PropertyMetadata(null));
28
29 public static readonly DependencyProperty ItemsHeaderProperty = DependencyProperty.Register(
30 nameof(ItemsHeader), typeof(object), typeof(SettingsExpander), new PropertyMetadata(null));
31
32 public static readonly DependencyProperty ItemsFooterProperty = DependencyProperty.Register(
33 nameof(ItemsFooter), typeof(object), typeof(SettingsExpander), new PropertyMetadata(null));
34
35 public static readonly DependencyProperty IsHeaderClickEnabledProperty = DependencyProperty.Register(
36 nameof(IsHeaderClickEnabled), typeof(bool), typeof(SettingsExpander), new PropertyMetadata(true));
37
38 public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
39 nameof(IsExpanded), typeof(bool), typeof(SettingsExpander),
40 new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsExpandedChanged));
41
42 public static readonly RoutedEvent ExpandedEvent = EventManager.RegisterRoutedEvent(
43 nameof(Expanded), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsExpander));
44
45 public static readonly RoutedEvent CollapsedEvent = EventManager.RegisterRoutedEvent(
46 nameof(Collapsed), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SettingsExpander));
47
48 public object? Description
49 {
50 get => GetValue(DescriptionProperty);
51 set => SetValue(DescriptionProperty, value);
52 }
53
54 public DataTemplate? DescriptionTemplate
55 {
56 get => (DataTemplate?)GetValue(DescriptionTemplateProperty);
57 set => SetValue(DescriptionTemplateProperty, value);
58 }
59
60 public object? HeaderIcon
61 {
62 get => GetValue(HeaderIconProperty);
63 set => SetValue(HeaderIconProperty, value);
64 }
65
66 public DataTemplate? HeaderIconTemplate
67 {
68 get => (DataTemplate?)GetValue(HeaderIconTemplateProperty);
69 set => SetValue(HeaderIconTemplateProperty, value);
70 }
71
72 public object? Content
73 {
74 get => GetValue(ContentProperty);
75 set => SetValue(ContentProperty, value);
76 }
77
78 public DataTemplate? ContentTemplate
79 {
80 get => (DataTemplate?)GetValue(ContentTemplateProperty);
81 set => SetValue(ContentTemplateProperty, value);
82 }
83
84 public object? ItemsHeader
85 {
86 get => GetValue(ItemsHeaderProperty);
87 set => SetValue(ItemsHeaderProperty, value);
88 }
89
90 public object? ItemsFooter
91 {
92 get => GetValue(ItemsFooterProperty);
93 set => SetValue(ItemsFooterProperty, value);
94 }
95
96 public bool IsHeaderClickEnabled
97 {
98 get => (bool)GetValue(IsHeaderClickEnabledProperty);
99 set => SetValue(IsHeaderClickEnabledProperty, value);
100 }
101
102 public bool IsExpanded
103 {
104 get => (bool)GetValue(IsExpandedProperty);
105 set => SetValue(IsExpandedProperty, value);
106 }
107
108 public event RoutedEventHandler Expanded
109 {
110 add => AddHandler(ExpandedEvent, value);
111 remove => RemoveHandler(ExpandedEvent, value);
112 }
113
114 public event RoutedEventHandler Collapsed
115 {
116 add => AddHandler(CollapsedEvent, value);
117 remove => RemoveHandler(CollapsedEvent, value);
118 }
119
120 private static void OnIsExpandedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
121 {
122 var expander = (SettingsExpander)dependencyObject;
123 expander.RaiseEvent(new RoutedEventArgs((bool)eventArgs.NewValue ? ExpandedEvent : CollapsedEvent, expander));
124 }
125 }
Added XFEToolBox.WpfCore/Controls/XamlCodeViewer.cs +216 -0
@@ -0,0 +1,216 @@
1 using System.Globalization;
2 using System.Text.RegularExpressions;
3 using System.Windows;
4 using System.Windows.Controls;
5 using System.Windows.Documents;
6 using System.Windows.Media;
7
8 namespace XFEToolBox.WpfCore.Controls;
9
10 /// <summary>
11 /// 用于只读展示 XAML 代码的轻量语法高亮控件。
12 /// 保留 RichTextBox 的文本选择、键盘复制和滚动能力,不负责编辑或执行代码。
13 /// </summary>
14 public sealed class XamlCodeViewer : RichTextBox
15 {
16 private static readonly Regex TokenPattern = new(
17 "(?<Comment><!--[\\s\\S]*?-->)|" +
18 "(?<CData><!\\[CDATA\\[[\\s\\S]*?\\]\\]>)|" +
19 "(?<Declaration><\\?[A-Za-z_][A-Za-z0-9_.:-]*)|" +
20 "(?<Tag></?[A-Za-z_][A-Za-z0-9_.:-]*)|" +
21 "(?<TagClose>\\?>|/?>)|" +
22 "(?<Attribute>[A-Za-z_][A-Za-z0-9_.:-]*(?=\\s*=))|" +
23 "(?<String>\"[^\"]*\"|'[^']*')|" +
24 "(?<Entity>&(?:#[0-9]+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);)",
25 RegexOptions.Compiled);
26
27 private static readonly Brush PlainTextBrush = CreateBrush(0xEE, 0xEE, 0xFA);
28 private static readonly Brush PunctuationBrush = CreateBrush(0xB8, 0xA9, 0xFF);
29 private static readonly Brush ElementBrush = CreateBrush(0x79, 0xCF, 0xF2);
30 private static readonly Brush AttributeBrush = CreateBrush(0xD6, 0xB5, 0xFF);
31 private static readonly Brush StringBrush = CreateBrush(0xA8, 0xE6, 0xA3);
32 private static readonly Brush CommentBrush = CreateBrush(0x79, 0x7A, 0x91);
33 private static readonly Brush EntityBrush = CreateBrush(0xF5, 0xD7, 0x8E);
34 private static readonly Brush CDataBrush = CreateBrush(0xF4, 0xB8, 0xE4);
35
36 public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
37 nameof(Text), typeof(string), typeof(XamlCodeViewer),
38 new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsRender, OnSourceChanged));
39
40 public static readonly DependencyProperty IsSyntaxHighlightingEnabledProperty = DependencyProperty.Register(
41 nameof(IsSyntaxHighlightingEnabled), typeof(bool), typeof(XamlCodeViewer),
42 new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender, OnSourceChanged));
43
44 public XamlCodeViewer()
45 {
46 IsReadOnly = true;
47 IsReadOnlyCaretVisible = false;
48 AcceptsReturn = true;
49 VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
50 HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
51 SpellCheck.SetIsEnabled(this, false);
52 Loaded += (_, _) => RebuildDocument();
53 SizeChanged += (_, _) => UpdateDocumentPageWidth();
54 RebuildDocument();
55 }
56
57 /// <summary>要显示的 XAML 文本。</summary>
58 public string Text
59 {
60 get => (string)GetValue(TextProperty);
61 set => SetValue(TextProperty, value);
62 }
63
64 /// <summary>是否启用 XAML 语法着色;关闭后仍保持只读代码查看体验。</summary>
65 public bool IsSyntaxHighlightingEnabled
66 {
67 get => (bool)GetValue(IsSyntaxHighlightingEnabledProperty);
68 set => SetValue(IsSyntaxHighlightingEnabledProperty, value);
69 }
70
71 private static void OnSourceChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
72 {
73 if (dependencyObject is XamlCodeViewer viewer)
74 viewer.RebuildDocument();
75 }
76
77 private void RebuildDocument()
78 {
79 var source = Text ?? string.Empty;
80 var document = new FlowDocument
81 {
82 PagePadding = new Thickness(0),
83 PageWidth = CalculateDocumentPageWidth(source),
84 ColumnGap = 0,
85 FontFamily = FontFamily,
86 FontSize = FontSize,
87 Foreground = PlainTextBrush
88 };
89 var paragraph = new Paragraph
90 {
91 Margin = new Thickness(0),
92 LineHeight = Math.Max(17, FontSize * 1.55)
93 };
94 document.Blocks.Add(paragraph);
95
96 if (!IsSyntaxHighlightingEnabled || source.Length == 0)
97 {
98 paragraph.Inlines.Add(new Run(source) { Foreground = PlainTextBrush });
99 Document = document;
100 return;
101 }
102
103 var offset = 0;
104 foreach (Match match in TokenPattern.Matches(source))
105 {
106 if (match.Index > offset)
107 AddRun(paragraph, source[offset..match.Index], PlainTextBrush);
108
109 AddToken(paragraph, match);
110 offset = match.Index + match.Length;
111 }
112 if (offset < source.Length)
113 AddRun(paragraph, source[offset..], PlainTextBrush);
114
115 Document = document;
116 }
117
118 private void UpdateDocumentPageWidth()
119 {
120 if (Document is null)
121 return;
122
123 var width = CalculateDocumentPageWidth(Text ?? string.Empty);
124 if (Math.Abs(Document.PageWidth - width) > 0.5)
125 Document.PageWidth = width;
126 }
127
128 private double CalculateDocumentPageWidth(string source)
129 {
130 var availableWidth = Math.Max(1,
131 ActualWidth - Padding.Left - Padding.Right - BorderThickness.Left - BorderThickness.Right -
132 SystemParameters.VerticalScrollBarWidth - 4);
133 if (source.Length == 0)
134 return availableWidth;
135
136 var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
137 var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
138 var widestLine = source.Replace("\r", string.Empty, StringComparison.Ordinal)
139 .Split('\n')
140 .Select(line => new FormattedText(
141 line,
142 CultureInfo.CurrentUICulture,
143 FlowDirection.LeftToRight,
144 typeface,
145 FontSize,
146 PlainTextBrush,
147 pixelsPerDip).WidthIncludingTrailingWhitespace)
148 .DefaultIfEmpty(0)
149 .Max();
150 return Math.Max(availableWidth, widestLine + 4);
151 }
152
153 private static void AddToken(Paragraph paragraph, Match match)
154 {
155 if (match.Groups["Comment"].Success)
156 {
157 AddRun(paragraph, match.Value, CommentBrush, FontStyles.Italic);
158 return;
159 }
160 if (match.Groups["CData"].Success)
161 {
162 AddRun(paragraph, match.Value, CDataBrush);
163 return;
164 }
165 if (match.Groups["Tag"].Success || match.Groups["Declaration"].Success)
166 {
167 var prefixLength = match.Value.StartsWith("</", StringComparison.Ordinal) ||
168 match.Value.StartsWith("<?", StringComparison.Ordinal)
169 ? 2
170 : 1;
171 AddRun(paragraph, match.Value[..prefixLength], PunctuationBrush);
172 AddRun(paragraph, match.Value[prefixLength..], ElementBrush);
173 return;
174 }
175 if (match.Groups["TagClose"].Success)
176 {
177 AddRun(paragraph, match.Value, PunctuationBrush);
178 return;
179 }
180 if (match.Groups["Attribute"].Success)
181 {
182 AddRun(paragraph, match.Value, AttributeBrush);
183 return;
184 }
185 if (match.Groups["String"].Success)
186 {
187 AddRun(paragraph, match.Value, StringBrush);
188 return;
189 }
190 if (match.Groups["Entity"].Success)
191 {
192 AddRun(paragraph, match.Value, EntityBrush);
193 return;
194 }
195
196 AddRun(paragraph, match.Value, PlainTextBrush);
197 }
198
199 private static void AddRun(Paragraph paragraph, string text, Brush foreground, FontStyle? fontStyle = null)
200 {
201 if (text.Length == 0)
202 return;
203
204 var run = new Run(text) { Foreground = foreground };
205 if (fontStyle is { } style)
206 run.FontStyle = style;
207 paragraph.Inlines.Add(run);
208 }
209
210 private static Brush CreateBrush(byte red, byte green, byte blue)
211 {
212 var brush = new SolidColorBrush(Color.FromRgb(red, green, blue));
213 brush.Freeze();
214 return brush;
215 }
216 }
Added XFEToolBox.WpfCore/Resources/Style/SettingsControlsStyle.xaml +272 -0
Modified XFEToolBox.WpfCore/Resources/Style/StandardControlsStyle.xaml +76 -23
@@ -143,6 +143,47 @@
143 143 </Style>
144 144 <Style TargetType="TextBox" BasedOn="{StaticResource ToolBoxTextBoxStyle}"/>
145 145
146 <Style x:Key="ToolBoxXamlCodeViewerStyle" TargetType="controls:XamlCodeViewer">
147 <Setter Property="MinHeight" Value="96"/>
148 <Setter Property="Padding" Value="14"/>
149 <Setter Property="Foreground" Value="#EEEEFA"/>
150 <Setter Property="Background" Value="#202033"/>
151 <Setter Property="BorderBrush" Value="#34344B"/>
152 <Setter Property="BorderThickness" Value="1"/>
153 <Setter Property="CaretBrush" Value="#EEEEFA"/>
154 <Setter Property="SelectionBrush" Value="#6969BC"/>
155 <Setter Property="FontFamily" Value="Cascadia Mono, Consolas"/>
156 <Setter Property="FontSize" Value="11"/>
157 <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
158 <Setter Property="Template">
159 <Setter.Value>
160 <ControlTemplate TargetType="controls:XamlCodeViewer">
161 <Border x:Name="Surface" Padding="{TemplateBinding Padding}"
162 Background="{TemplateBinding Background}"
163 BorderBrush="{TemplateBinding BorderBrush}"
164 BorderThickness="{TemplateBinding BorderThickness}"
165 CornerRadius="11" SnapsToDevicePixels="True">
166 <ScrollViewer x:Name="PART_ContentHost"
167 HorizontalScrollBarVisibility="{TemplateBinding HorizontalScrollBarVisibility}"
168 VerticalScrollBarVisibility="{TemplateBinding VerticalScrollBarVisibility}"/>
169 </Border>
170 <ControlTemplate.Triggers>
171 <Trigger Property="IsMouseOver" Value="True">
172 <Setter TargetName="Surface" Property="BorderBrush" Value="#50506D"/>
173 </Trigger>
174 <Trigger Property="IsKeyboardFocusWithin" Value="True">
175 <Setter TargetName="Surface" Property="BorderBrush" Value="{DynamicResource MainColor}"/>
176 </Trigger>
177 <Trigger Property="IsEnabled" Value="False">
178 <Setter Property="Opacity" Value="0.55"/>
179 </Trigger>
180 </ControlTemplate.Triggers>
181 </ControlTemplate>
182 </Setter.Value>
183 </Setter>
184 </Style>
185 <Style TargetType="controls:XamlCodeViewer" BasedOn="{StaticResource ToolBoxXamlCodeViewerStyle}"/>
186
146 187 <Style x:Key="ToolBoxPasswordBoxStyle" TargetType="PasswordBox">
147 188 <Setter Property="MinHeight" Value="36"/><Setter Property="Padding" Value="11,7"/>
148 189 <Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/><Setter Property="Background" Value="{DynamicResource ToolControlBackgroundBrush}"/>
@@ -571,32 +612,44 @@
571 612 </ControlTemplate.Triggers>
572 613 </ControlTemplate></Setter.Value></Setter>
573 614 </Style>
615 <ControlTemplate x:Key="ToolBoxVerticalScrollBarTemplate" TargetType="ScrollBar">
616 <Grid>
617 <Border x:Name="TrackSurface" Background="{TemplateBinding Background}" CornerRadius="6"/>
618 <Track x:Name="PART_Track" Orientation="Vertical" IsDirectionReversed="True" Focusable="False">
619 <Track.DecreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageUpCommand}" Focusable="False" Opacity="0"/></Track.DecreaseRepeatButton>
620 <Track.Thumb><Thumb Style="{StaticResource ToolBoxScrollBarThumbStyle}"/></Track.Thumb>
621 <Track.IncreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageDownCommand}" Focusable="False" Opacity="0"/></Track.IncreaseRepeatButton>
622 </Track>
623 </Grid>
624 <ControlTemplate.Triggers>
625 <Trigger Property="IsMouseOver" Value="True"><Setter TargetName="TrackSurface" Property="Background" Value="#E7E7F7"/></Trigger>
626 <Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0"/></Trigger>
627 </ControlTemplate.Triggers>
628 </ControlTemplate>
629 <ControlTemplate x:Key="ToolBoxHorizontalScrollBarTemplate" TargetType="ScrollBar">
630 <Grid>
631 <Border x:Name="TrackSurface" Background="{TemplateBinding Background}" CornerRadius="6"/>
632 <Track x:Name="PART_Track" Orientation="Horizontal" IsDirectionReversed="False" Focusable="False">
633 <Track.DecreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageLeftCommand}" Focusable="False" Opacity="0"/></Track.DecreaseRepeatButton>
634 <Track.Thumb><Thumb Style="{StaticResource ToolBoxScrollBarThumbStyle}"/></Track.Thumb>
635 <Track.IncreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageRightCommand}" Focusable="False" Opacity="0"/></Track.IncreaseRepeatButton>
636 </Track>
637 </Grid>
638 <ControlTemplate.Triggers>
639 <Trigger Property="IsMouseOver" Value="True"><Setter TargetName="TrackSurface" Property="Background" Value="#E7E7F7"/></Trigger>
640 <Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0"/></Trigger>
641 </ControlTemplate.Triggers>
642 </ControlTemplate>
574 643 <Style x:Key="ToolBoxScrollBarStyle" TargetType="ScrollBar">
575 644 <Setter Property="Width" Value="12"/><Setter Property="MinWidth" Value="12"/><Setter Property="Height" Value="Auto"/><Setter Property="MinHeight" Value="0"/>
576 645 <Setter Property="Background" Value="#ECECFA"/><Setter Property="Margin" Value="1,3,1,3"/><Setter Property="IsTabStop" Value="False"/><Setter Property="FocusVisualStyle" Value="{x:Null}"/>
577 <Setter Property="Template"><Setter.Value><ControlTemplate TargetType="ScrollBar">
578 <Grid>
579 <Border x:Name="TrackSurface" Background="{TemplateBinding Background}" CornerRadius="6"/>
580 <Track x:Name="VerticalTrack" Orientation="Vertical" IsDirectionReversed="True" Focusable="False">
581 <Track.DecreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageUpCommand}" Focusable="False" Opacity="0"/></Track.DecreaseRepeatButton>
582 <Track.Thumb><Thumb MinHeight="34" Style="{StaticResource ToolBoxScrollBarThumbStyle}"/></Track.Thumb>
583 <Track.IncreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageDownCommand}" Focusable="False" Opacity="0"/></Track.IncreaseRepeatButton>
584 </Track>
585 <Track x:Name="HorizontalTrack" Orientation="Horizontal" IsDirectionReversed="False" Focusable="False" Visibility="Collapsed">
586 <Track.DecreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageLeftCommand}" Focusable="False" Opacity="0"/></Track.DecreaseRepeatButton>
587 <Track.Thumb><Thumb MinWidth="34" Style="{StaticResource ToolBoxScrollBarThumbStyle}"/></Track.Thumb>
588 <Track.IncreaseRepeatButton><RepeatButton Command="{x:Static ScrollBar.PageRightCommand}" Focusable="False" Opacity="0"/></Track.IncreaseRepeatButton>
589 </Track>
590 </Grid>
591 <ControlTemplate.Triggers>
592 <Trigger Property="Orientation" Value="Horizontal">
593 <Setter TargetName="VerticalTrack" Property="Visibility" Value="Collapsed"/><Setter TargetName="HorizontalTrack" Property="Visibility" Value="Visible"/>
594 <Setter Property="Width" Value="Auto"/><Setter Property="MinWidth" Value="0"/><Setter Property="Height" Value="12"/><Setter Property="MinHeight" Value="12"/><Setter Property="Margin" Value="3,1,3,1"/>
595 </Trigger>
596 <Trigger Property="IsMouseOver" Value="True"><Setter TargetName="TrackSurface" Property="Background" Value="#E7E7F7"/></Trigger>
597 <Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0"/></Trigger>
598 </ControlTemplate.Triggers>
599 </ControlTemplate></Setter.Value></Setter>
646 <Setter Property="Template" Value="{StaticResource ToolBoxVerticalScrollBarTemplate}"/>
647 <Style.Triggers>
648 <Trigger Property="Orientation" Value="Horizontal">
649 <Setter Property="Width" Value="Auto"/><Setter Property="MinWidth" Value="0"/><Setter Property="Height" Value="12"/><Setter Property="MinHeight" Value="12"/><Setter Property="Margin" Value="3,1,3,1"/>
650 <Setter Property="Template" Value="{StaticResource ToolBoxHorizontalScrollBarTemplate}"/>
651 </Trigger>
652 </Style.Triggers>
600 653 </Style>
601 654 <Style TargetType="ScrollBar" BasedOn="{StaticResource ToolBoxScrollBarStyle}"/>
602 655
Modified XFEToolBox.WpfCore/Resources/Style/ToolThemeResources.xaml +1 -0
@@ -5,6 +5,7 @@
5 5 <ResourceDictionary Source="/XFEToolBox.WpfCore;component/Resources/Style/Theme/LightTheme.xaml"/>
6 6 <ResourceDictionary Source="/XFEToolBox.WpfCore;component/Resources/Style/StandardControlsStyle.xaml"/>
7 7 <ResourceDictionary Source="/XFEToolBox.WpfCore;component/Resources/Style/UnifiedControlsStyle.xaml"/>
8 <ResourceDictionary Source="/XFEToolBox.WpfCore;component/Resources/Style/SettingsControlsStyle.xaml"/>
8 9 <ResourceDictionary Source="/XFEToolBox.WpfCore;component/Resources/Style/MyControlsStyle.xaml"/>
9 10 </ResourceDictionary.MergedDictionaries>
10 11 </ResourceDictionary>
Modified XFEToolBox/Profiles/CrossVersionProfiles/ConsoleProfile.cs +5 -0
@@ -31,6 +31,11 @@ public partial class ConsoleProfile : XFEProfile
31 31 [ProfileProperty]
32 32 private string remoteServerAddress = "ws://localhost:3280/";
33 33 /// <summary>
34 /// 远程调试模式下用于连接调试程序服务器的密码
35 /// </summary>
36 [ProfileProperty]
37 private string remoteServerPassword = "";
38 /// <summary>
34 39 /// 最大行数
35 40 /// </summary>
36 41 [ProfileProperty]
Modified XFEToolBox/ViewModel/Pages/ConsolePageViewModel.cs +1 -1
@@ -57,7 +57,7 @@ public partial class ConsolePageViewModel : ObservableObject
57 57 await TerminalClient.DisposeAsync();
58 58 TerminalClient = new XFEConsoleTerminalClient(
59 59 NormalizeRemoteAddress(ConsoleProfile.RemoteServerAddress),
60 ConsoleProfile.ConsolePassword,
60 ConsoleProfile.RemoteServerPassword,
61 61 Environment.MachineName,
62 62 $"XFEToolBox-{Environment.ProcessId}");
63 63 TerminalClient.Connected += TerminalClient_Connected;
Modified XFEToolBox/ViewModel/Pages/SettingPageViewModel.cs +23 -2
@@ -36,12 +36,33 @@ public partial class SettingPageViewModel(SettingPage viewPage) : ObservableObje
36 36 {
37 37 if (parent is null)
38 38 return;
39 LoadSettingProfile(parent, []);
40 }
41
42 private static void LoadSettingProfile(DependencyObject parent, HashSet<DependencyObject> visited)
43 {
44 if (!visited.Add(parent))
45 return;
46
47 ChildFound(parent);
48 if (parent is SettingsExpander settingsExpander)
49 {
50 if (settingsExpander.Content is DependencyObject headerContent)
51 LoadSettingProfile(headerContent, visited);
52 foreach (var item in settingsExpander.Items)
53 if (item is DependencyObject settingsItem)
54 LoadSettingProfile(settingsItem, visited);
55 }
56 else if (parent is SettingsCard { Content: DependencyObject cardContent })
57 {
58 LoadSettingProfile(cardContent, visited);
59 }
60
39 61 int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
40 62 for (int i = 0; i < childrenCount; i++)
41 63 {
42 64 var child = VisualTreeHelper.GetChild(parent, i);
43 ChildFound(child);
44 LoadSettingProfile(child);
65 LoadSettingProfile(child, visited);
45 66 }
46 67 }
47 68
Modified XFEToolBox/Views/Pages/SettingPage.xaml +138 -112
Modified XFEToolBox/Views/Windows/ControlGalleryWindow.Catalog.cs +18 -0
@@ -354,6 +354,24 @@ public partial class ControlGalleryWindow
354 354 ["不要把密码或令牌拼入可见命令", "执行命令前仍需独立校验参数"],
355 355 ["命令", "command", "copy", "终端"]),
356 356
357 Item(
358 "xaml-code-viewer", "XamlCodeViewer", "controls:XamlCodeViewer", "工具增强", "</>",
359 "只读展示带语法颜色的 XAML 代码。",
360 "XamlCodeViewer 用于文档、预览和诊断界面,保留文本选择、键盘复制及双向滚动能力,但不会编辑、解析或执行显示的 XAML。",
361 """
362 <controls:XamlCodeViewer Height="180"
363 IsSyntaxHighlightingEnabled="True">
364 <controls:XamlCodeViewer.Text><![CDATA[
365 <Grid Margin="16">
366 <TextBlock Text="XFE 工具页面" />
367 </Grid>
368 ]]></controls:XamlCodeViewer.Text>
369 </controls:XamlCodeViewer>
370 """,
371 [P("Text", "string", "要展示的 XAML 文本"), P("IsSyntaxHighlightingEnabled", "bool", "是否启用 XAML 语法着色")],
372 ["适合只读示例,不替代代码编辑器", "Ctrl+C 会复制当前选区,完整示例可由外部按钮复制"],
373 ["XAML", "代码预览", "syntax", "highlight", "viewer"]),
374
357 375 Item(
358 376 "scroll-text", "ScrollTextBlock", "controls:ScrollTextBlock", "工具增强", "↔",
359 377 "单行溢出时自动或悬停滚动的文本。",
Modified XFEToolBox/Views/Windows/ControlGalleryWindow.Scenarios.Advanced.cs +22 -0
@@ -335,6 +335,28 @@ public partial class ControlGalleryWindow
335 335 TextParameter("CopyButtonText", preview.CopyButtonText, value => preview.CopyButtonText = value)));
336 336 }
337 337
338 private static ScenarioPreviewResult BuildXamlCodeViewerScenarios()
339 {
340 var viewer = new ToolControls.XamlCodeViewer
341 {
342 Height = 150,
343 Text = """
344 <!-- 工具页面标题 -->
345 <Grid Margin="16">
346 <TextBlock Text="XFE 工具页面" FontWeight="SemiBold" />
347 </Grid>
348 """,
349 IsSyntaxHighlightingEnabled = true
350 };
351
352 return Scenarios(Scenario(
353 "只读 XAML 示例",
354 "切换语法着色或替换示例文本,验证标签、属性、字符串和注释的颜色层次。",
355 ScenarioPreviewStack(viewer),
356 TextParameter("Text", viewer.Text, value => viewer.Text = value),
357 ToggleParameter("IsSyntaxHighlightingEnabled", true, value => viewer.IsSyntaxHighlightingEnabled = value)));
358 }
359
338 360 private static ScenarioPreviewResult BuildScrollTextScenarios()
339 361 {
340 362 var status = ScenarioStatus("长路径会在溢出时滚动");
Modified XFEToolBox/Views/Windows/ControlGalleryWindow.Scenarios.cs +1 -0
Modified XFEToolBox/Views/Windows/ControlGalleryWindow.xaml +5 -5
Modified XFEToolBox/XFEToolBox.Client.csproj +1 -1
Modified XfeTestArtifacts/test-results.json +1 -1
Modified XfeTestArtifacts/test-results.xml +1 -1
Modified docs/tool-packages.md +22 -0