由于内容陈旧,已经写了新的文章代替这篇,请参考新的文章:
-
Window(窗体)的UI元素及行为:这篇文章主要讨论标准Window的 UI 元素和行为。
-
使用WindowChrome自定义Window Style:介绍使用 WindowChrome 自定义 Window 的原理及各种细节。
-
使用WindowChrome的问题:介绍如何处理使用WindowChrome自定义 Window 会遇到的各种问题。
-
使用WindowChrome自定义RibbonWindow:提供了一个自定义的 RibbonWindow 以解决原生 RibbonWindow 的问题。
以下为原内容-----------------------------------------------------------------------------
1. 前言#
做了WPF开发多年,一直未曾自己实现一个自定义Window Style,无论是《WPF编程宝典》或是各种博客都建议使用WindowStyle="None"
和AllowsTransparency="True"
,于是想当然以为这样就可以了。最近来了兴致想自己实现一个,才知道WindowStyle="None"
的方式根本不好用,原因有几点:
- 如果Window没有阴影会很难看,但自己添加DropShadowEffect又十分影响性能。
- 需要自定义弹出、关闭、最大化、最小化动画,而自己做肯定不如Windows自带动画高效。
- 需要实现Resize功能。
- 其它BUG。
光是性能问题就足以放弃WindowStyle="None"
的实现方式,幸好还有使用WindowChrome的实现方式,但一时之间也找不到理想的实现,连MSDN上的文档( WindowChrome Class )都太过时,.NET 4.5也没有SystemParameters2这个类,只好参考一些开源项目(如 Modern UI for WPF )自己实现了。
2. Window基本功能#
Window的基本功能如上图所示。注意除了标准的“最小化”、“最大化/还原”、"关闭"按钮外,Icon上单击还应该能打开窗体的系统菜单,双击则直接关闭窗体。
我想实现类似Office 2016的Window效果:阴影、自定义窗体颜色。阴影、动画效果保留系统默认的就可以了,基本上会很耐看。
大多数自定义Window都有圆角,但我并不喜欢,低DPI的情况下只有几个像素组成的圆角通常都不会很圆滑(如下图),所以保留直角。
3. 实现#
3.1 定义CustomWindow控件#
首先,为了方便以后的扩展,我定义了一个名为CustomWindow的模板化控件派生自Window。
public class CustomWindow : Window { public CustomWindow() { DefaultStyleKey = typeof(CustomWindow); CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindow)); CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindow, CanResizeWindow)); CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindow, CanMinimizeWindow)); CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, RestoreWindow, CanResizeWindow)); CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu)); } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); if (e.ButtonState == MouseButtonState.Pressed) DragMove(); } protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); if (SizeToContent == SizeToContent.WidthAndHeight) InvalidateMeasure(); } #region Window Commands private void CanResizeWindow(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = ResizeMode == ResizeMode.CanResize || ResizeMode == ResizeMode.CanResizeWithGrip; } private void CanMinimizeWindow(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = ResizeMode != ResizeMode.NoResize; } private void CloseWindow(object sender, ExecutedRoutedEventArgs e) { this.Close(); //SystemCommands.CloseWindow(this); } private void MaximizeWindow(object sender, ExecutedRoutedEventArgs e) { SystemCommands.MaximizeWindow(this); } private void MinimizeWindow(object sender, ExecutedRoutedEventArgs e) { SystemCommands.MinimizeWindow(this); } private void RestoreWindow(object sender, ExecutedRoutedEventArgs e) { SystemCommands.RestoreWindow(this); } private void ShowSystemMenu(object sender, ExecutedRoutedEventArgs e) { var element = e.OriginalSource as FrameworkElement; if (element == null) return; var point = WindowState == WindowState.Maximized ? new Point(0, element.ActualHeight) : new Point(Left + BorderThickness.Left, element.ActualHeight + Top + BorderThickness.Top); point = element.TransformToAncestor(this).Transform(point); SystemCommands.ShowSystemMenu(this, point); } #endregion }
主要是添加了几个CommandBindings,用于给标题栏上的按钮绑定。
3.2 使用WindowChrome#
对于WindowChrome,MSDN是这样描述的:
若要自定义窗口,同时保留其标准功能,可以使用WindowChrome类。 WindowChrome类窗口框架的功能分离开来视觉对象,并允许您控制的客户端和应用程序窗口的非工作区之间的边界。
在CustomWindow的DefaultStyle中添加如下Setting:
<Setter Property="WindowChrome.WindowChrome"> <Setter.Value> <WindowChrome CornerRadius="0" GlassFrameThickness="1" UseAeroCaptionButtons="False" NonClientFrameEdges="None" /> </Setter.Value> </Setter>
这样除了包含阴影的边框,整个Window的内容就可以由用户定义了。
3.3 Window基本布局#
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" x:Name="WindowBorder"> <Grid x:Name="LayoutRoot" Background="{TemplateBinding Background}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid x:Name="PART_WindowTitleGrid" Grid.Row="0" Height="26.4" Background="{TemplateBinding BorderBrush}"> .... </Grid> <AdornerDecorator Grid.Row="1" KeyboardNavigation.IsTabStop="False"> <ContentPresenter x:Name="MainContentPresenter" KeyboardNavigation.TabNavigation="Cycle" /> </AdornerDecorator> <ResizeGrip x:Name="ResizeGrip" HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="1" IsTabStop="False" Visibility="Hidden" WindowChrome.ResizeGripDirection="BottomRight" /> </Grid> </Border>
Window的标准布局很简单,大致上就是标题栏和内容。
PART_WindowTitleGrid是标题栏,具体内容下一节再讨论。
ContentPresenter的内容即Window的Client Area的范围。
ResizeGrip是当ResizeMode = ResizeMode.CanResizeWithGrip;
时出现的Window右下角的大小调整手柄,基本上用于提示窗口可以通过拖动边框改调整小。
AdornerDecorator 为可视化树中的子元素提供 AdornerLayer,如果没有它的话一些装饰效果不能显示(例如下图Button控件的Focus效果),Window的 ContentPresenter 外面套个 AdornerDecorator 是 必不能忘的。
3.4 布局标题栏#
<Button x:Name="Minimize" ToolTip="Minimize" WindowChrome.IsHitTestVisibleInChrome="True" Command="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}" ContentTemplate="{StaticResource MinimizeWhite}" Style="{StaticResource TitleBarButtonStyle}" IsTabStop="False" />
标题栏上的按钮实现如上,将Command绑定到SystemCommands,并且设置WindowChrome.IsHitTestVisibleInChrome="True"
,标题栏上的内容要设置这个附加属性才能响应鼠标操作。
<Button VerticalAlignment="Center" Margin="7,0,5,0" Content="{TemplateBinding Icon}" Height="{x:Static SystemParameters.SmallIconHeight}" Width="{x:Static SystemParameters.SmallIconWidth}" WindowChrome.IsHitTestVisibleInChrome="True" IsTabStop="False"> <Button.Template> <ControlTemplate TargetType="{x:Type Button}"> <Image Source="{TemplateBinding Content}" /> </ControlTemplate> </Button.Template> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <i:InvokeCommandAction Command="{x:Static SystemCommands.ShowSystemMenuCommand}" /> </i:EventTrigger> <i:EventTrigger EventName="MouseDoubleClick"> <i:InvokeCommandAction Command="{x:Static SystemCommands.CloseWindowCommand}" /> </i:EventTrigger> </i:Interaction.Triggers> </Button>
标题栏上的Icon也是一个按钮,单机打开SystemMenu,双击关闭Window。Height和Widht的值分别使用了SystemParameters.SmallIconHeight
和SystemParameters.SmallIconWidth
,SystemParameters包含可用来查询系统设置的属性,能使用SystemParameters的地方尽量使用总是没错的。
按钮的样式没实现得很好,这点暂时将就一下,以后改进吧。
3.5 处理Triggers#
<ControlTemplate.Triggers> <Trigger Property="IsActive" Value="False"> <Setter Property="BorderBrush" Value="#FF6F7785" /> </Trigger> <Trigger Property="WindowState" Value="Maximized"> <Setter TargetName="Maximize" Property="Visibility" Value="Collapsed" /> <Setter TargetName="Restore" Property="Visibility" Value="Visible" /> <Setter TargetName="LayoutRoot" Property="Margin" Value="7" /> </Trigger> <Trigger Property="WindowState" Value="Normal"> <Setter TargetName="Maximize" Property="Visibility" Value="Visible" /> <Setter TargetName="Restore" Property="Visibility" Value="Collapsed" /> </Trigger> <Trigger Property="ResizeMode" Value="NoResize"> <Setter TargetName="Minimize" Property="Visibility" Value="Collapsed" /> <Setter TargetName="Maximize" Property="Visibility" Value="Collapsed" /> <Setter TargetName="Restore" Property="Visibility" Value="Collapsed" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="ResizeMode" Value="CanResizeWithGrip" /> <Condition Property="WindowState" Value="Normal" /> </MultiTrigger.Conditions> <Setter TargetName="ResizeGrip" Property="Visibility" Value="Visible" /> </MultiTrigger> </ControlTemplate.Triggers>
虽然我平时喜欢用VisualState的方式实现模板化控件UI再状态之间的转变,但有时还是Trigger方便快捷,尤其是不需要做动画的时候。
注意当WindowState=Maximized时要将LayoutRoot的Margin设置成7,如果不这样做在最大化时Window边缘部分会被遮蔽,很多使用WindowChrome自定义Window的方案都没有处理这点。
3.6 处理导航#
另一点需要注意的是键盘导航。一般来说Window中按Tab键,焦点会在Window的内容间循环,不要让标题栏的按钮获得焦点,也不要让ContentPresenter 的各个父元素获得焦点,所以在ContentPresenter 上设置KeyboardNavigation.TabNavigation="Cycle"
。为了不让标题栏上的各个按钮获得焦点,在各个按钮上还设置了IsTabStop="False"
,
3.7 DragMove#
有些人喜欢不止标题栏,按住Window的任何空白部分都可以拖动Window,只需要在代码中添加DragMove即可:
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); if (e.ButtonState == MouseButtonState.Pressed) DragMove(); }
3.8 移植TransitioningContentControl#
索性让Window打开时内容也添加一些动画。我将Silverlight Toolkit的TransitioningContentControl
复制过来,只改了一点动画,并且在OnApplyTemplate()
最后添加了这句:VisualStateManager.GoToState(this, Transition, true);
。最后将Window中的ContentPresenter 替换成这个控件,效果还不错(实际效果挺流畅的,可是GIF看起来不怎么样):
3.9 SizeToContent问题#
有个比较麻烦的问题,当设置SizeToContent="WidthAndHeight"
,打开Window会出现以下错误。
看上去是内容的Size和Window的Size计算错误,目前的解决方法是在CustomWindow中添加以下代码,简单粗暴,但可能引发其它问题:
protected override void OnContentRendered(EventArgs e) { base.OnContentRendered(e); if (SizeToContent == SizeToContent.WidthAndHeight) InvalidateMeasure(); }
5. 结语#
第一次写Window样式,想不到遇到这么多需要注意的地方。
目前只是个很简单的Demo,没有添加额外的功能,希望对他人有帮助吧。
编码在Window10上完成,只在Windows7上稍微测试了一下,不敢保证兼容性。
如有错漏请指出。
6. 参考#
Window Styles and Templates
WindowChrome 类
SystemParameters 类
mahapps.metro
Modern UI for WPF
7. 源码#
作者:Dino.C
出处:https://www.cnblogs.com/dino623/p/CustomWindowStyle.html
版权:本文采用「CC BY 4.0」知识共享许可协议进行许可。