【WPF】填坑 - WindowChrome 自定义窗口完美实现
- 概述
- Demo 说明
- 基本样式资源
- 布局
- ShellView Style
- 界面元素修正
- Command Binding
- Command 实现
- 效果
概述
前面写过一篇关于在 WPF 中通过对 WindowChrome 的操作实现自定义窗口并保留一部分的系统功能。
【WPF】WindowChrome 自定义窗口完美实现
有小伙伴看过之后反应,其中有些功能不够完善,本篇来对前面填坑。
Demo 说明
- 基于 .net6 WPF MVVM 模式
- 不在 MainWindow 上进行修改而是新建一个 ShellView 的窗口作为主窗口
- 必要的 NuGet 包
CommunityToolkit.Mvvm
Microsoft.Xaml.Behaviors.Wpf
基本样式资源
<!--颜色-->
<Brush x:Key="TitleBar">#F0F8FF</Brush>
<Brush x:Key="themeColor">#696969</Brush>
<Brush x:Key="MouseOverBackground">#87CEEB</Brush>
<Brush x:Key="MouseOverForeground">#F0F8FF</Brush>
<!--字体图标-->
<Geometry x:Key="Minimize">M928.2 548h-832c-17.7 0-32-14.3-32-32s14.3-32 32-32h832c17.7 0 32 14.3 32 32s-14.3 32-32 32z</Geometry>
<Geometry x:Key="Maximize">M812.3 959.4H213.7c-81.6 0-148-66.4-148-148V212.9c0-81.6 66.4-148 148-148h598.5c81.6 0 148 66.4 148 148v598.5C960.3 893 893.9 959.4 812.3 959.4zM213.7 120.9c-50.7 0-92 41.3-92 92v598.5c0 50.7 41.3 92 92 92h598.5c50.7 0 92-41.3 92-92V212.9c0-50.7-41.3-92-92-92H213.7z</Geometry>
<Geometry x:Key="Restore">M714.666667 256H138.666667a53.393333 53.393333 0 0 0-53.333334 53.333333v576a53.393333 53.393333 0 0 0 53.333334 53.333334h576a53.393333 53.393333 0 0 0 53.333333-53.333334V309.333333a53.393333 53.393333 0 0 0-53.333333-53.333333z m10.666666 629.333333a10.666667 10.666667 0 0 1-10.666666 10.666667H138.666667a10.666667 10.666667 0 0 1-10.666667-10.666667V309.333333a10.666667 10.666667 0 0 1 10.666667-10.666666h576a10.666667 10.666667 0 0 1 10.666666 10.666666z m213.333334-746.666666v565.333333a21.333333 21.333333 0 0 1-42.666667 0V138.666667a10.666667 10.666667 0 0 0-10.666667-10.666667H320a21.333333 21.333333 0 0 1 0-42.666667h565.333333a53.393333 53.393333 0 0 1 53.333334 53.333334z</Geometry>
<Geometry x:Key="Close">M952.311261 921.328619 542.892591 510.919389 950.154131 102.671381c8.53028-8.55177 8.53028-22.416546 0-30.967292-8.532327-8.55177-22.360264-8.55177-30.892591 0l-407.262564 408.248008L104.737436 71.704089c-8.53028-8.55177-22.36231-8.55177-30.892591 0-8.529257 8.55177-8.529257 22.416546 0 30.967292l407.262564 408.248008L71.687716 921.328619c-8.529257 8.55177-8.529257 22.416546 0 30.967292 4.26514 4.27435 9.856485 6.41306 15.446807 6.41306 5.590322 0 11.181667-2.13871 15.446807-6.41306l409.41867-410.409231 409.41867 410.409231c4.266164 4.27435 9.855462 6.41306 15.446807 6.41306 5.591345 0 11.17962-2.13871 15.446807-6.41306C960.841541 943.745165 960.841541 929.879366 952.311261 921.328619z</Geometry>
<!--图片-->
<ImageSource x:Key="icon">pack://SiteOfOrigin:,,,/icon.ico</ImageSource>
图片自己换,不在这里提供
布局
主体分为,TitleBar 和 Content 两部分,也就是非用户区和用户区
ShellView Style
(SystemParameters.WindowNonClientFrameThickness).Top
获取系统窗口标题栏可触发窗口移动的高度,标题栏上的按钮也通过这里高度
SystemParameters.SmallIconWidth
SystemParameters.SmallIconHeight
图标的宽和高,一般为16*16 Pixel
WindowChrome 的处理,这一块相对来说其实比较固定,下面这样处理即可 不保留三大键
<WindowChrome
CaptionHeight="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
GlassFrameThickness="1"
NonClientFrameEdges="None"
ResizeBorderThickness="5"
UseAeroCaptionButtons="False" />
通过自定义 Template 实现前面的布局
<Style x:Key="ShellViewStyle" TargetType="{x:Type Window}">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome
CaptionHeight="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
GlassFrameThickness="1"
NonClientFrameEdges="None"
ResizeBorderThickness="5"
UseAeroCaptionButtons="False" />
</Setter.Value>
</Setter>
<Setter Property="Icon" Value="{DynamicResource icon}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Window}">
<Border
x:Name="WindowBorder"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid
x:Name="WindowTitlePanel"
Grid.Row="0"
Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
Background="{DynamicResource TitleBar}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Image
Width="{x:Static SystemParameters.SmallIconWidth}"
Height="{x:Static SystemParameters.SmallIconHeight}"
Margin="5,0"
Source="{TemplateBinding Icon}"
WindowChrome.IsHitTestVisibleInChrome="True"/>
<ContentControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Title}"
FontSize="{x:Static SystemFonts.CaptionFontSize}"
IsTabStop="False" />
</StackPanel>
<StackPanel
x:Name="WindowCommandButtonsPanel"
Grid.Column="1"
HorizontalAlignment="Right"
Orientation="Horizontal"
WindowChrome.IsHitTestVisibleInChrome="True">
<Button
x:Name="MinimizeButton"
Style="{DynamicResource MinimizeButtonStyle}" />
<Grid Margin="1,0">
<Button
x:Name="RestoreButton"
Style="{DynamicResource RestoreButtonStyle}"
Visibility="Collapsed" />
<Button
x:Name="MaximizeButton"
Style="{DynamicResource MaximizeButtonStyle}" />
</Grid>
<Button
x:Name="CloseButton"
Style="{DynamicResource CloseButtonStyle}" />
</StackPanel>
</Grid>
<Grid Grid.Row="1" Background="{TemplateBinding Background}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="28" />
</Grid.RowDefinitions>
<AdornerDecorator KeyboardNavigation.IsTabStop="False">
<ContentControl
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Content="{TemplateBinding Content}"
KeyboardNavigation.TabNavigation="Cycle" />
</AdornerDecorator>
<Grid Grid.Row="1" Background="{DynamicResource themeColor}">
<ResizeGrip
x:Name="ResizeGrip"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
IsTabStop="False"
Visibility="Collapsed" />
</Grid>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
界面元素修正
- 窗口在最大化后会溢出 8 Pixel 的边距因此在最大化是需要处理边距
- 最大化按钮和还原按钮在 Window 的 WindowState 属性改变时需要做显示/隐藏处理
<ControlTemplate.Triggers>
<Trigger Property="WindowState" Value="Maximized">
<Setter TargetName="LayoutRoot" Property="Margin" Value="8" />
<Setter TargetName="RestoreButton" Property="Visibility" Value="Visible" />
<Setter TargetName="MaximizeButton" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="WindowState" Value="Normal">
<Setter TargetName="ResizeGrip" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
Command Binding
点击图标显示 SystemMenu 菜单模态框,通过 Behaviors 将鼠标事件 Binding 到 ViewModel 中的 Command
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding MouseLeftSystemMenuCommand}" />
</i:EventTrigger>
<i:EventTrigger EventName="MouseRightButtonDown">
<i:InvokeCommandAction Command="{Binding MouseRightSystemMenuCommand}"
rgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
最小化、最大化、还原、关闭按钮 Binding Command
<Button
x:Name="MinimizeButton"
Command="{Binding MinimizeWindowCommand}"/>
<Button
x:Name="RestoreButton"
Command="{Binding RestoreWindowCommand}"/>
<Button
x:Name="MaximizeButton"
Command="{Binding MaximizeWindowCommand}"/>
<Button
x:Name="CloseButton"
Command="{Binding CloseWindowCommand}"/>
Command 实现
Command 尽可能的使用 WPF 提供的系统命令
public partial class ShellViewModel : ObservableObject
{
private readonly Window _window;
public ShellViewModel()
{
_window = Application.Current.MainWindow;
}
[RelayCommand]
private void CloseWindow()
{
SystemCommands.CloseWindow(_window);
Application.Current.Shutdown();
}
[RelayCommand]
private void MinimizeWindow() => SystemCommands.MinimizeWindow(_window);
[RelayCommand]
private void MaximizeWindow() => SystemCommands.MaximizeWindow(_window);
[RelayCommand]
private void RestoreWindow() => SystemCommands.RestoreWindow(_window);
[RelayCommand]
private void MouseLeftSystemMenu()
{
var pointing = _window.PointToScreen(new Point(0, 0));
if (_window.WindowState is WindowState.Maximized)
pointing.X += 10;
else
pointing.X += 2;
pointing.Y += SystemParameters.WindowNonClientFrameThickness.Top + 1;
SystemCommands.ShowSystemMenu(_window, pointing);
}
[RelayCommand]
private void MouseRightSystemMenu(MouseEventArgs e)
{
FrameworkElement? element = e.OriginalSource as FrameworkElement;
var pointing = _window.PointToScreen(Mouse.GetPosition(element));
pointing.X += 5;
pointing.Y += 5;
SystemCommands.ShowSystemMenu(_window, pointing);
}
}
效果
通过 WindowChrome 对窗口进行高度自定义化,可以最大限度地保留系统功能,比如窗口动画,点击任务栏图标的一些操作等,最主要的我想还是窗口的性能是最接近原生性能的。
这篇文章也只是更加完善了前面文章的不足,后面有更完善的部分,同样会以文章的形式更新出来,供大家参考。