返回提交历史
Modified
XFEToolBox.Client.Wpf.Test/Program.cs
+155
-0
Added
XFEToolBox.Client.Wpf.Test/SettingsControlsTests.cs
+136
-0
Added
XFEToolBox.WpfCore/Controls/SettingsCard.cs
+150
-0
Added
XFEToolBox.WpfCore/Controls/SettingsExpander.cs
+125
-0
Added
XFEToolBox.WpfCore/Controls/XamlCodeViewer.cs
+216
-0
Added
XFEToolBox.WpfCore/Resources/Style/SettingsControlsStyle.xaml
+272
-0
Modified
XFEToolBox.WpfCore/Resources/Style/StandardControlsStyle.xaml
+76
-23
Modified
XFEToolBox.WpfCore/Resources/Style/ToolThemeResources.xaml
+1
-0
Modified
XFEToolBox/Profiles/CrossVersionProfiles/ConsoleProfile.cs
+5
-0
Modified
XFEToolBox/ViewModel/Pages/ConsolePageViewModel.cs
+1
-1
Modified
XFEToolBox/ViewModel/Pages/SettingPageViewModel.cs
+23
-2
Modified
XFEToolBox/Views/Pages/SettingPage.xaml
+138
-112
Modified
XFEToolBox/Views/Windows/ControlGalleryWindow.Catalog.cs
+18
-0
Modified
XFEToolBox/Views/Windows/ControlGalleryWindow.Scenarios.Advanced.cs
+22
-0
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
XFEstudio/XFEToolBox
设置页面结构升级与代码高亮控件集成
新增 SettingsCard/SettingsExpander 控件及样式,统一设置项结构和分组折叠,提升页面一致性。SettingPage 全面替换为新结构,远程调试项支持分组与联动显示。新增 XamlCodeViewer 控件,实现只读 XAML 语法高亮,控件库示例区集成。补充自动化测试,完善递归加载逻辑。控制台配置支持独立远程密码。统一滚动条样式,修复 TimePicker 问题。版本号升级至 1.0.1。
769f049
代码差异
20 个文件
+1369
-146
@@ -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));
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
@@ -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>
@@ -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]
@@ -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;
@@ -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
@@ -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
"单行溢出时自动或悬停滚动的文本。",
@@ -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("长路径会在溢出时滚动");