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

XFEToolBox

【WPF】XFE工具箱

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

XFEstudio/XFEToolBox

优化TabView与NavigationView圆角边框一致性

本次优化统一了WPF TabView 和 NavigationView 控件的圆角边框渲染与裁剪逻辑,提升视觉一致性。新增 UniformRoundedBorder 控件,解决原生 Border 圆角像素差异问题,并适配缩放与 DPI 变化。重构相关样式,调整边距、边框层级与内容对齐方式,确保选中状态下边框完整显示。NavigationView 采用双层 UniformRoundedBorder 实现裁剪与描边分离,导航项区域整体内缩避免边界裁剪。补充自动化测试,验证控件边框、圆角、内容对齐及滚动裁剪的像素级一致性。修正“第二调试模式”文案为“远程调试模式”,并更新测试结果文件。

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

代码差异

7 个文件 +371 -33
Modified XFEToolBox.Client.Wpf.Test/Program.cs +210 -0
@@ -4,15 +4,169 @@ using System.Windows;
4 4 using System.Windows.Controls;
5 5 using System.Windows.Interop;
6 6 using System.Windows.Media;
7 using System.Windows.Media.Imaging;
7 8 using System.Windows.Shell;
8 9 using System.Windows.Threading;
9 10 using XFEToolBox.Client.Utilities;
11 using XFEToolBox.WpfCore.Controls;
10 12 using XFEToolBox.WpfCore.Windowing;
11 13
12 14 namespace XFEToolBox.Client.Wpf.Test;
13 15
14 16 public class Program
15 17 {
18 [Test]
19 public static void TabAndNavigationOutlinesStayInsideTheirLayoutBounds()
20 {
21 Exception? failure = null;
22 var thread = new Thread(() =>
23 {
24 try
25 {
26 var dispatcher = Dispatcher.CurrentDispatcher;
27 var tabView = new TabView { Width = 420, Height = 210, SelectedIndex = 0 };
28 tabView.Items.Add(new TabItem { Header = "概览", Content = new TextBlock { Text = "概览内容" } });
29 tabView.Items.Add(new TabItem { Header = "日志", Content = new TextBlock { Text = "日志内容" } });
30 tabView.Items.Add(new TabItem { Header = "设置", Content = new TextBlock { Text = "设置内容" } });
31
32 var navigationView = new NavigationView
33 {
34 Width = 420,
35 Height = 230,
36 NavigationWidth = new GridLength(160),
37 SelectedIndex = 0
38 };
39 navigationView.Items.Add(new TabItem { Header = "常规", Content = new TextBlock { Text = "常规设置" } });
40 navigationView.Items.Add(new TabItem { Header = "网络", Content = new TextBlock { Text = "网络设置" } });
41 navigationView.Items.Add(new TabItem { Header = "高级", Content = new TextBlock { Text = "高级设置" } });
42 navigationView.Items.Add(new TabItem { Header = "外观", Content = new TextBlock { Text = "外观设置" } });
43 navigationView.Items.Add(new TabItem { Header = "通知", Content = new TextBlock { Text = "通知设置" } });
44 navigationView.Items.Add(new TabItem { Header = "隐私", Content = new TextBlock { Text = "隐私设置" } });
45
46 var panel = new StackPanel();
47 panel.Children.Add(tabView);
48 panel.Children.Add(navigationView);
49 var window = new Window
50 {
51 Width = 480,
52 Height = 500,
53 Left = -10_000,
54 Top = -10_000,
55 ShowInTaskbar = false,
56 WindowStartupLocation = WindowStartupLocation.Manual,
57 Content = panel
58 };
59 window.Resources.MergedDictionaries.Add(new ResourceDictionary
60 {
61 Source = new Uri("/XFEToolBox.WpfCore;component/Resources/Style/ToolThemeResources.xaml", UriKind.Relative)
62 });
63 window.Show();
64 PumpDispatcher(dispatcher);
65
66 for (var index = 0; index < tabView.Items.Count; index++)
67 {
68 tabView.SelectedIndex = index;
69 PumpDispatcher(dispatcher);
70 window.UpdateLayout();
71
72 var item = (TabItem)tabView.Items[index];
73 item.ApplyTemplate();
74 var outline = (Border?)item.Template.FindName("SelectedOutline", item);
75 var headerContent = (ContentPresenter?)item.Template.FindName("HeaderContent", item);
76 var connector = (Border?)item.Template.FindName("ContentConnector", item);
77
78 Ensure(outline is { Visibility: Visibility.Visible, ActualWidth: >= 1 }, $"第 {index + 1} 个页签轮廓没有显示。");
79 Ensure(outline!.BorderThickness == new Thickness(1, 1, 1, 0), $"第 {index + 1} 个页签轮廓缺少侧边框。");
80 Ensure(connector is { Visibility: Visibility.Visible }, $"第 {index + 1} 个页签没有与内容面板连接。");
81
82 var itemRight = item.TranslatePoint(new Point(item.ActualWidth, 0), window).X;
83 var edgeRight = outline!.TranslatePoint(new Point(outline.ActualWidth, 0), window).X;
84 Ensure(edgeRight <= itemRight - 4,
85 $"第 {index + 1} 个页签右边框仍位于可裁剪边界:{edgeRight:N2} >= {itemRight:N2}。");
86 Ensure(headerContent is not null, $"第 {index + 1} 个页签缺少标题内容。");
87 var outlineLeft = outline.TranslatePoint(new Point(0, 0), window).X;
88 var headerLeft = headerContent!.TranslatePoint(new Point(0, 0), window).X;
89 var outlineCenter = outlineLeft + outline.ActualWidth / 2;
90 var headerCenter = headerLeft + headerContent.ActualWidth / 2;
91 Ensure(Math.Abs(outlineCenter - headerCenter) <= 0.5,
92 $"第 {index + 1} 个页签标题没有居中:{headerCenter:N2} != {outlineCenter:N2}。");
93 }
94
95 tabView.SelectedIndex = 0;
96 PumpDispatcher(dispatcher);
97 window.UpdateLayout();
98 var firstItem = (TabItem)tabView.Items[0];
99 var firstOutline = (Border?)firstItem.Template.FindName("SelectedOutline", firstItem);
100 var contentFrame = (Border?)tabView.Template.FindName("ContentFrame", tabView);
101 Ensure(firstOutline is not null && contentFrame is not null, "TabView 缺少用于对齐的轮廓或内容面板。");
102 var firstOutlineLeft = firstOutline!.TranslatePoint(new Point(0, 0), window).X;
103 var contentFrameLeft = contentFrame!.TranslatePoint(new Point(0, 0), window).X;
104 Ensure(Math.Abs(firstOutlineLeft - contentFrameLeft) <= 0.5,
105 $"首个页签与内容面板左侧没有对齐:{firstOutlineLeft:N2} != {contentFrameLeft:N2}。");
106
107 navigationView.ApplyTemplate();
108 window.UpdateLayout();
109 var navigationFrameHost = (Grid?)navigationView.Template.FindName("NavigationFrameHost", navigationView);
110 var navigationClip = (UniformRoundedBorder?)navigationView.Template.FindName("NavigationClip", navigationView);
111 var navigationFrame = (Border?)navigationView.Template.FindName("NavigationFrame", navigationView);
112 Ensure(navigationFrameHost is not null && navigationClip is not null && navigationFrame is not null,
113 "NavigationView 缺少独立的圆角裁剪层或描边层。");
114 Ensure(navigationFrame!.ActualWidth > 0 && navigationFrame.ActualHeight > 0, "NavigationView 导航边框没有完成布局。");
115 Ensure(navigationFrame.BorderThickness == new Thickness(1), "NavigationView 四侧边框厚度不完整。");
116 Ensure(navigationFrame.CornerRadius == new CornerRadius(15), "NavigationView 四角没有保持统一圆角。");
117 Ensure(navigationFrameHost!.Margin == new Thickness(2), "NavigationView 边框没有保留防裁剪间距。");
118 Ensure(navigationClip!.CornerRadius == navigationFrame.CornerRadius,
119 "NavigationView 裁剪层与描边层的圆角半径不一致。");
120 Ensure(Math.Abs(navigationClip.ActualWidth - navigationFrame.ActualWidth) <= 0.5 &&
121 Math.Abs(navigationClip.ActualHeight - navigationFrame.ActualHeight) <= 0.5,
122 "NavigationView 裁剪层与描边层的尺寸不一致。");
123 Ensure(navigationFrame is UniformRoundedBorder && navigationClip.Clip is RectangleGeometry,
124 "NavigationView 没有使用圆角裁剪,内部内容可能覆盖下半部圆角。");
125 EnsureVerticallyMirroredCorners(navigationFrame, 18);
126 for (var index = 0; index < navigationView.Items.Count; index++)
127 {
128 var item = (TabItem)navigationView.Items[index];
129 item.ApplyTemplate();
130 var surface = (Border?)item.Template.FindName("Surface", item);
131 var headerContent = (ContentPresenter?)item.Template.FindName("HeaderContent", item);
132 Ensure(surface is not null && headerContent is not null, $"第 {index + 1} 个导航项缺少表面或标题内容。");
133 var surfaceLeft = surface!.TranslatePoint(new Point(0, 0), item).X;
134 var headerLeft = headerContent!.TranslatePoint(new Point(0, 0), item).X;
135 var surfaceCenter = surfaceLeft + surface.ActualWidth / 2;
136 var headerCenter = headerLeft + headerContent.ActualWidth / 2;
137 Ensure(Math.Abs(surfaceCenter - headerCenter) <= 0.5,
138 $"第 {index + 1} 个导航项标题没有居中:{headerCenter:N2} != {surfaceCenter:N2}。");
139 }
140 var navigationScroller = (ScrollViewer?)navigationView.Template.FindName("NavigationScroller", navigationView);
141 Ensure(navigationScroller is not null, "NavigationView 缺少独立导航滚动区域。");
142 navigationScroller!.ScrollToEnd();
143 PumpDispatcher(dispatcher);
144 window.UpdateLayout();
145 Ensure(navigationScroller.VerticalOffset > 0, "NavigationView 滚动状态没有被覆盖测试。");
146 Ensure(navigationClip.Clip is RectangleGeometry, "NavigationView 滚动后丢失圆角裁剪。");
147
148 window.Close();
149 dispatcher.InvokeShutdown();
150 }
151 catch (Exception exception)
152 {
153 failure = exception;
154 }
155 })
156 {
157 IsBackground = true
158 };
159
160 thread.SetApartmentState(ApartmentState.STA);
161 thread.Start();
162 Ensure(thread.Join(TimeSpan.FromSeconds(15)), "页签与导航边框测试超时。");
163 if (failure is not null)
164 {
165 Console.WriteLine(failure);
166 throw new InvalidOperationException("页签或导航边框可能再次被裁剪。", failure);
167 }
168 }
169
16 170 [Test]
17 171 public static void FramelessMaximizedWindowStaysInsideMonitorWorkArea()
18 172 {
@@ -211,6 +365,62 @@ public class Program
211 365 Dispatcher.PushFrame(frame);
212 366 }
213 367
368 private static void EnsureVerticallyMirroredCorners(FrameworkElement element, int sampleSize)
369 {
370 var width = Math.Max(1, (int)Math.Ceiling(element.ActualWidth));
371 var height = Math.Max(1, (int)Math.Ceiling(element.ActualHeight));
372 var bitmap = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
373 bitmap.Render(element);
374
375 var stride = width * 4;
376 var pixels = new byte[stride * height];
377 bitmap.CopyPixels(pixels, stride, 0);
378 var size = Math.Min(sampleSize, Math.Min(width, height) / 2);
379 var maximumDifference = 0;
380 var differenceLocation = string.Empty;
381 var differenceValues = string.Empty;
382
383 for (var y = 0; y < size; y++)
384 {
385 for (var x = 0; x < size; x++)
386 {
387 var leftDifference = PixelDifference(pixels, stride, x, y, x, height - 1 - y);
388 if (leftDifference > maximumDifference)
389 {
390 maximumDifference = leftDifference;
391 differenceLocation = $"left ({x},{y})/({x},{height - 1 - y})";
392 differenceValues = $"{PixelValue(pixels, stride, x, y)}/{PixelValue(pixels, stride, x, height - 1 - y)}";
393 }
394 var rightDifference = PixelDifference(pixels, stride, width - 1 - x, y, width - 1 - x, height - 1 - y);
395 if (rightDifference > maximumDifference)
396 {
397 maximumDifference = rightDifference;
398 differenceLocation = $"right ({width - 1 - x},{y})/({width - 1 - x},{height - 1 - y})";
399 differenceValues = $"{PixelValue(pixels, stride, width - 1 - x, y)}/{PixelValue(pixels, stride, width - 1 - x, height - 1 - y)}";
400 }
401 }
402 }
403
404 Ensure(maximumDifference <= 1,
405 $"NavigationView 上下圆角的像素差异过大:{maximumDifference},位置 {differenceLocation},像素 {differenceValues},布局 {element.ActualWidth:N3}×{element.ActualHeight:N3},位图 {width}×{height}。");
406 }
407
408 private static int PixelDifference(byte[] pixels, int stride, int firstX, int firstY, int secondX, int secondY)
409 {
410 var first = firstY * stride + firstX * 4;
411 var second = secondY * stride + secondX * 4;
412 var difference = 0;
413 for (var channel = 0; channel < 4; channel++)
414 difference = Math.Max(difference, Math.Abs(pixels[first + channel] - pixels[second + channel]));
415 return difference;
416 }
417
418 private static string PixelValue(byte[] pixels, int stride, int x, int y)
419 {
420 var offset = y * stride + x * 4;
421 return $"[{pixels[offset]},{pixels[offset + 1]},{pixels[offset + 2]},{pixels[offset + 3]}]";
422 }
423
214 424 private const uint MonitorDefaultToNearest = 0x00000002;
215 425
216 426 [DllImport("user32.dll")]
Added XFEToolBox.WpfCore/Controls/UniformRoundedBorder.cs +101 -0
@@ -0,0 +1,101 @@
1 using System.Windows;
2 using System.Windows.Controls;
3 using System.Windows.Media;
4
5 namespace XFEToolBox.WpfCore.Controls;
6
7 /// <summary>
8 /// 使用同一几何算法绘制并裁剪四个等半径圆角,避免原生 Border 在缩放后的上下圆角差异。
9 /// </summary>
10 public sealed class UniformRoundedBorder : Border
11 {
12 protected override void OnRender(DrawingContext drawingContext)
13 {
14 if (!TryGetUniformCornerRadius(out var radius) || !TryGetUniformBorderThickness(out var thickness))
15 {
16 base.OnRender(drawingContext);
17 return;
18 }
19
20 var bounds = new Rect(0, 0, ActualWidth, ActualHeight);
21 if (bounds.IsEmpty)
22 return;
23
24 if (Background is not null)
25 drawingContext.DrawGeometry(Background, null, CreateGeometry(bounds, radius));
26
27 if (BorderBrush is null || thickness <= 0)
28 return;
29
30 var outerGeometry = CreateGeometry(bounds, radius);
31 var innerWidth = ActualWidth - thickness * 2;
32 var innerHeight = ActualHeight - thickness * 2;
33 if (innerWidth <= 0 || innerHeight <= 0)
34 {
35 drawingContext.DrawGeometry(BorderBrush, null, outerGeometry);
36 return;
37 }
38
39 // A filled even-odd ring stays symmetric at the first and last pixel rows. A centered Pen stroke
40 // follows WPF's half-open raster edge rules and produces visibly different top/bottom antialiasing.
41 var borderGeometry = new GeometryGroup { FillRule = FillRule.EvenOdd };
42 borderGeometry.Children.Add(outerGeometry);
43 borderGeometry.Children.Add(CreateGeometry(
44 new Rect(thickness, thickness, innerWidth, innerHeight),
45 Math.Max(0, radius - thickness)));
46 borderGeometry.Freeze();
47 drawingContext.DrawGeometry(BorderBrush, null, borderGeometry);
48 }
49
50 protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
51 {
52 base.OnRenderSizeChanged(sizeInfo);
53 UpdateClip();
54 }
55
56 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
57 {
58 base.OnPropertyChanged(e);
59 if (e.Property == CornerRadiusProperty)
60 UpdateClip();
61 }
62
63 private void UpdateClip()
64 {
65 // Clip is only needed for hosted content. Applying the clip to an outline-only instance also clips
66 // its own one-pixel stroke at the bottom/right raster boundary and makes mirrored corners differ.
67 if (Child is null || ActualWidth <= 0 || ActualHeight <= 0 || !TryGetUniformCornerRadius(out var radius))
68 {
69 Clip = null;
70 return;
71 }
72
73 Clip = CreateGeometry(new Rect(0, 0, ActualWidth, ActualHeight), radius);
74 }
75
76 private bool TryGetUniformCornerRadius(out double radius)
77 {
78 radius = CornerRadius.TopLeft;
79 return AreClose(radius, CornerRadius.TopRight) &&
80 AreClose(radius, CornerRadius.BottomRight) &&
81 AreClose(radius, CornerRadius.BottomLeft);
82 }
83
84 private bool TryGetUniformBorderThickness(out double thickness)
85 {
86 thickness = BorderThickness.Left;
87 return AreClose(thickness, BorderThickness.Top) &&
88 AreClose(thickness, BorderThickness.Right) &&
89 AreClose(thickness, BorderThickness.Bottom);
90 }
91
92 private static Geometry CreateGeometry(Rect bounds, double requestedRadius)
93 {
94 var radius = Math.Min(Math.Max(0, requestedRadius), Math.Min(bounds.Width, bounds.Height) / 2);
95 var geometry = new RectangleGeometry(bounds, radius, radius);
96 geometry.Freeze();
97 return geometry;
98 }
99
100 private static bool AreClose(double left, double right) => Math.Abs(left - right) < 0.001;
101 }
Modified XFEToolBox.WpfCore/Resources/Style/StandardControlsStyle.xaml +37 -22
@@ -672,26 +672,39 @@
672 672
673 673 <Style x:Key="TabViewItemStyle" TargetType="TabItem">
674 674 <Setter Property="Padding" Value="15,10"/><Setter Property="MinHeight" Value="42"/>
675 <Setter Property="Margin" Value="1,0,5,0"/>
675 <Setter Property="Margin" Value="0,0,5,0"/>
676 676 <Setter Property="Panel.ZIndex" Value="0"/>
677 677 <Setter Property="Foreground" Value="{DynamicResource ToolTextSecondaryBrush}"/>
678 678 <Setter Property="Background" Value="Transparent"/><Setter Property="BorderBrush" Value="Transparent"/>
679 <Setter Property="BorderThickness" Value="1,1,1,0"/>
679 <Setter Property="BorderThickness" Value="1"/>
680 680 <Setter Property="FocusVisualStyle" Value="{x:Null}"/><Setter Property="Cursor" Value="Hand"/>
681 681 <Setter Property="Template"><Setter.Value><ControlTemplate TargetType="TabItem">
682 <Grid Panel.ZIndex="2">
682 <Grid Panel.ZIndex="2" SnapsToDevicePixels="True" UseLayoutRounding="True">
683 683 <Border x:Name="Surface" Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}"
684 684 BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"
685 685 CornerRadius="11,11,0,0">
686 <ContentPresenter ContentSource="Header" HorizontalAlignment="Center" VerticalAlignment="Center"/>
686 <ContentPresenter x:Name="HeaderContent" ContentSource="Header" Margin="0,0,5,0"
687 HorizontalAlignment="Center" VerticalAlignment="Center"/>
687 688 </Border>
689 <!-- This overlay does not participate in the auto-size measurement. Its trailing edge is
690 deliberately inset so TabPanel/DPI clipping cannot remove the right side. -->
691 <Border x:Name="SelectedOutline" Margin="0,0,5,0"
692 BorderBrush="{DynamicResource MainColor}" BorderThickness="1,1,1,0"
693 CornerRadius="11,11,0,0" Visibility="Collapsed"
694 IsHitTestVisible="False" Panel.ZIndex="4"/>
695 <Border x:Name="ContentConnector" Height="2" Margin="1,0,6,0"
696 VerticalAlignment="Bottom" Background="Transparent" Visibility="Collapsed"
697 IsHitTestVisible="False" Panel.ZIndex="3"/>
688 698 </Grid>
689 699 <ControlTemplate.Triggers>
690 700 <Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
691 701 <Trigger Property="IsSelected" Value="True">
692 702 <Setter Property="Panel.ZIndex" Value="10"/>
693 703 <Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlBackgroundBrush}"/>
694 <Setter TargetName="Surface" Property="BorderBrush" Value="{DynamicResource ToolControlBorderBrush}"/>
704 <Setter TargetName="Surface" Property="BorderBrush" Value="Transparent"/>
705 <Setter TargetName="SelectedOutline" Property="Visibility" Value="Visible"/>
706 <Setter TargetName="ContentConnector" Property="Background" Value="{DynamicResource ToolControlBackgroundBrush}"/>
707 <Setter TargetName="ContentConnector" Property="Visibility" Value="Visible"/>
695 708 <Setter Property="Foreground" Value="{DynamicResource MainColor}"/>
696 709 <Setter Property="FontWeight" Value="SemiBold"/>
697 710 </Trigger>
@@ -711,10 +724,10 @@
711 724 <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
712 725 <ScrollViewer x:Name="HeaderScroller" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
713 726 CanContentScroll="False" PanningMode="HorizontalOnly" Focusable="False"
714 Margin="1,1,1,0" Panel.ZIndex="2">
727 Margin="0,1,0,0" Panel.ZIndex="2">
715 728 <TabPanel IsItemsHost="True" Background="Transparent" KeyboardNavigation.TabIndex="1"/>
716 729 </ScrollViewer>
717 <Border Grid.Row="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
730 <Border x:Name="ContentFrame" Grid.Row="1" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
718 731 BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"
719 732 CornerRadius="0,14,14,14" Margin="0,-1,0,0" Panel.ZIndex="1">
720 733 <ContentPresenter x:Name="PART_SelectedContentHost" ContentSource="SelectedContent"
@@ -726,20 +739,19 @@
726 739
727 740 <Style x:Key="NavigationViewItemStyle" TargetType="TabItem">
728 741 <Setter Property="Padding" Value="14,11"/><Setter Property="Margin" Value="1,0,1,5"/>
729 <Setter Property="MinHeight" Value="42"/><Setter Property="HorizontalContentAlignment" Value="Stretch"/>
742 <Setter Property="MinHeight" Value="42"/><Setter Property="HorizontalContentAlignment" Value="Center"/>
730 743 <Setter Property="Foreground" Value="{DynamicResource ToolTextSecondaryBrush}"/>
731 744 <Setter Property="Background" Value="Transparent"/><Setter Property="FocusVisualStyle" Value="{x:Null}"/>
732 745 <Setter Property="Cursor" Value="Hand"/>
733 746 <Setter Property="Template"><Setter.Value><ControlTemplate TargetType="TabItem">
734 <Border x:Name="Surface" Background="{TemplateBinding Background}" CornerRadius="11">
747 <controls:UniformRoundedBorder x:Name="Surface" Background="{TemplateBinding Background}" CornerRadius="11">
735 748 <Grid>
736 <Grid.ColumnDefinitions><ColumnDefinition Width="4"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
737 749 <Border x:Name="SelectedMark" Width="3" Height="22" HorizontalAlignment="Left" VerticalAlignment="Center"
738 750 Background="Transparent" CornerRadius="2"/>
739 <ContentPresenter Grid.Column="1" ContentSource="Header" Margin="{TemplateBinding Padding}"
751 <ContentPresenter x:Name="HeaderContent" ContentSource="Header" Margin="{TemplateBinding Padding}"
740 752 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
741 753 </Grid>
742 </Border>
754 </controls:UniformRoundedBorder>
743 755 <ControlTemplate.Triggers>
744 756 <Trigger Property="IsMouseOver" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolControlHoverBrush}"/></Trigger>
745 757 <Trigger Property="IsSelected" Value="True"><Setter TargetName="Surface" Property="Background" Value="{DynamicResource ToolAccentSoftBrush}"/><Setter TargetName="SelectedMark" Property="Background" Value="{DynamicResource MainColor}"/><Setter Property="Foreground" Value="{DynamicResource ToolTextPrimaryBrush}"/><Setter Property="FontWeight" Value="SemiBold"/></Trigger>
@@ -759,16 +771,19 @@
759 771 <ColumnDefinition Width="12"/>
760 772 <ColumnDefinition Width="*"/>
761 773 </Grid.ColumnDefinitions>
762 <Grid Margin="1">
763 <controls:RoundedClipBorder Background="{DynamicResource ToolSurfaceBrush}"
764 CornerRadius="14" Padding="6">
765 <ScrollViewer Background="Transparent" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
766 CanContentScroll="True" PanningMode="VerticalOnly" Focusable="False">
767 <TabPanel IsItemsHost="True" Background="Transparent" KeyboardNavigation.TabIndex="1"/>
768 </ScrollViewer>
769 </controls:RoundedClipBorder>
770 <Border BorderBrush="{DynamicResource ToolControlBorderBrush}" BorderThickness="1"
771 CornerRadius="15" Background="Transparent" IsHitTestVisible="False" Panel.ZIndex="10"/>
774 <!-- The clip and outline share exactly the same bounds and radius. Keeping the host two
775 pixels inside the layout prevents either outline edge from landing on a clipped boundary. -->
776 <Grid x:Name="NavigationFrameHost" Margin="2" SnapsToDevicePixels="True" UseLayoutRounding="True">
777 <controls:UniformRoundedBorder x:Name="NavigationClip" Background="{DynamicResource ToolSurfaceBrush}"
778 BorderThickness="0" CornerRadius="15" Padding="6">
779 <ScrollViewer x:Name="NavigationScroller" Background="Transparent" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
780 CanContentScroll="True" PanningMode="VerticalOnly" Focusable="False">
781 <TabPanel IsItemsHost="True" Background="Transparent" KeyboardNavigation.TabIndex="1"/>
782 </ScrollViewer>
783 </controls:UniformRoundedBorder>
784 <controls:UniformRoundedBorder x:Name="NavigationFrame" BorderBrush="{DynamicResource ToolControlHoverBorderBrush}"
785 BorderThickness="1" CornerRadius="15" Background="Transparent"
786 RenderOptions.EdgeMode="Aliased" IsHitTestVisible="False" Panel.ZIndex="10"/>
772 787 </Grid>
773 788 <Border Grid.Column="2" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
774 789 BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
Modified XFEToolBox/Profiles/CrossVersionProfiles/ConsoleProfile.cs +2 -2
@@ -21,12 +21,12 @@ public partial class ConsoleProfile : XFEProfile
21 21 [ProfileProperty]
22 22 private bool localHostOnly = true;
23 23 /// <summary>
24 /// 是否由工具箱主动连接调试程序服务器(第二调试模式)
24 /// 是否由工具箱主动连接调试程序服务器
25 25 /// </summary>
26 26 [ProfileProperty]
27 27 private bool connectToRemoteServer = false;
28 28 /// <summary>
29 /// 第二调试模式下的远程调试程序服务器地址
29 /// 远程调试模式下的远程调试程序服务器地址
30 30 /// </summary>
31 31 [ProfileProperty]
32 32 private string remoteServerAddress = "ws://localhost:3280/";
Modified XFEToolBox/Views/Pages/SettingPage.xaml +1 -1
@@ -81,7 +81,7 @@
81 81 <Border Background="{DynamicResource MainColor}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Height="1"/>
82 82 <StackPanel Grid.Row="1">
83 83 <controls:SwitchButton Margin="0,14,0,4" Command="{Binding SwitchedCommand}" CommandParameter="{Binding RelativeSource={x:Static RelativeSource.Self}}" Tag="XFEToolBox.Client.Profiles.CrossVersionProfiles.ConsoleProfile.ConnectToRemoteServer">
84 <TextBlock Text="第二调试模式:工具箱主动连接调试程序服务器"/>
84 <TextBlock Text="远程调试模式(连接调试程序服务器)"/>
85 85 </controls:SwitchButton>
86 86 <Grid Margin="0,8,0,4">
87 87 <Grid.ColumnDefinitions>
Modified XfeTestArtifacts/test-results.json +17 -6
@@ -1,21 +1,32 @@
1 1 {
2 "startedAt": "2026-08-17T20:08:53.5334231+00:00",
3 "duration": "00:00:00.7109143",
2 "startedAt": "2026-08-17T21:40:51.2224379+00:00",
3 "duration": "00:00:01.2041096",
4 4 "results": [
5 5 {
6 6 "id": "XFEToolBox.Client.Wpf.Test.Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea#0",
7 7 "displayName": "Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea",
8 8 "outcome": 0,
9 "bodyDuration": "00:00:00.7006523",
10 "totalDuration": "00:00:00.7020166",
9 "bodyDuration": "00:00:00.3877852",
10 "totalDuration": "00:00:00.3879686",
11 "attempts": 1,
12 "message": null,
13 "stackTrace": null,
14 "output": ""
15 },
16 {
17 "id": "XFEToolBox.Client.Wpf.Test.Program.TabAndNavigationOutlinesStayInsideTheirLayoutBounds#0",
18 "displayName": "Program.TabAndNavigationOutlinesStayInsideTheirLayoutBounds",
19 "outcome": 0,
20 "bodyDuration": "00:00:00.8030758",
21 "totalDuration": "00:00:00.8043573",
11 22 "attempts": 1,
12 23 "message": null,
13 24 "stackTrace": null,
14 25 "output": ""
15 26 }
16 27 ],
17 "total": 1,
18 "passed": 1,
28 "total": 2,
29 "passed": 2,
19 30 "failed": 0,
20 31 "skipped": 0
21 32 }
Modified XfeTestArtifacts/test-results.xml +3 -2
@@ -1,3 +1,4 @@
1 <testsuite name="XFEExtension.NetCore.XUnit" tests="1" failures="0" skipped="0" time="0.710914">
2 <testcase name="Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea" time="0.702017" />
1 <testsuite name="XFEExtension.NetCore.XUnit" tests="2" failures="0" skipped="0" time="1.204110">
2 <testcase name="Program.FramelessMaximizedWindowStaysInsideMonitorWorkArea" time="0.387969" />
3 <testcase name="Program.TabAndNavigationOutlinesStayInsideTheirLayoutBounds" time="0.804357" />
3 4 </testsuite>