[WPF]使用WindowChrome自定义Window Style
由于内容陈旧,已经写了新的文章代替这篇,请参考新的文章:
这篇文章主要讨论标准Window的UI元素和行为。无论是桌面编程还是日常使用,Window(窗体)都是最常接触的UI元素之一,既然Window这么重要那么多了解一些也没有坏处。
介绍使用WindowChrome自定义Window的原理及各种细节。
使用WindowChrome自定义Window会遇到很多问题,例如最大化的尺寸问题,这篇文章介绍如何处理这些细节。
因为WPF原生的RibbonWindow有不少UI上的Bug,所以我提供了一个自定义的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. 源码
[WPF]使用WindowChrome自定义Window Style的更多相关文章
- [WPF自定义控件]使用WindowChrome自定义Window Style
1. 为什么要自定义Window 对稍微有点规模的桌面软件来说自定义的Window几乎是标配了,一来设计师总是克制不住自己想想软件更个性化,为了UI的和谐修改Window也是必要的:二来多一行的空间可 ...
- [WPF自定义控件]?使用WindowChrome自定义Window Style
原文:[WPF自定义控件]?使用WindowChrome自定义Window Style 1. 为什么要自定义Window 对稍微有点规模的桌面软件来说自定义的Window几乎是标配了,一来设计师总是克 ...
- WPF 使用WindowChrome自定义窗体 保留原生窗体特性
本文大幅度借鉴dino.c大佬的文章 https://www.cnblogs.com/dino623/p/uielements_of_window.html https://www.cnblogs.c ...
- WPF自定义Window样式(1)
1. 引言 WPF是制作界面的一大利器.最近在做一个项目,用的就是WPF.既然使用了WPF了,那么理所当然的,需要自定义窗体样式.所使用的代码是在网上查到的,遗憾的是,整理完毕后,再找那篇帖子却怎么也 ...
- [WPF 自定义控件]使用WindowChrome自定义RibbonWindow
1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自定义RibbonWindow则不一样: 如果程序使用了自定义样式的Window,为了统一 ...
- [WPF自定义控件库]使用WindowChrome自定义RibbonWindow
原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...
- WPF自定义Window样式(2)
1. 引言 在上一篇中,介绍了如何建立自定义窗体.接下来,我们需要考虑将该自定义窗体基类放到类库中去,只有放到类库中,我们才能在其他地方去方便的引用该基类. 2. 创建类库 接上一篇的项目,先添加一个 ...
- [WPF自定义控件库]为Form和自定义Window添加FunctionBar
1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...
- [WPF疑难] 继承自定义窗口
原文 [WPF疑难] 继承自定义窗口 [WPF疑难] 继承自定义窗口 周银辉 项目中有不少的弹出窗口,按照美工的设计其外边框(包括最大化,最小化,关闭等按钮)自然不同于Window自身的,但每个弹出框 ...
随机推荐
- Linux系统运维工程该具备哪些素质
记得在上高中时,物理老师总是会对我们一句话:"学习是件苦差事."工作后发现,其实做运维也是件苦差事.最为一名运维工程师,深知这一行的艰辛,但和IT行业其他职务一样,那就是付出的越多 ...
- [bzoj1592] Making the Grade
[bzoj1592] Making the Grade 题目 FJ打算好好修一下农场中某条凹凸不平的土路.按奶牛们的要求,修好后的路面高度应当单调上升或单调下降,也就是说,高度上升与高度下降的路段不能 ...
- [补档]暑假集训D1总结
归来 今天就这样回来了,虽然心里极其不想回来(暑假!@#的只有一天啊喂),但还是回来了,没办法,虽然不喜欢这个地方,但是机房却也是少数能给我安慰的地方,心再累,也没有办法了,不如好好集训= = %da ...
- T-SQL笔记总结(1)
--1.创建一个数据库 createdatabase School; --删除数据库 dropdatabase School; --创建一个数据库的时候,指定一些数据库的相关参数,比如大小,增长方式, ...
- NYOJ--513--A+B Problem IV(大数)
A+B Problem IV 时间限制:1000 ms | 内存限制:65535 KB 难度:3 描述 acmj最近发现在使用计算器计算高精度的大数加法时很不方便,于是他想着能不能写个程序把这 ...
- Head First 设计模式目录
这确实是本好书啊,看其他的书,都会有种看了就忘,看着看着就会有种昏昏欲睡的感脚,然而,这本书却能让我在看了之后记住自己看了些什么. 并且在本书的开头,作者也在一个劲的告诉你如何让自己来记住自己看了什么 ...
- 微信小程序(有始有终,全部代码)开发---跑步App+音乐播放器 Bug修复
开篇语 昨晚发了一篇: <简年15: 微信小程序(有始有终,全部代码)开发---跑步App+音乐播放器 > 然后上午起来吃完午饭之后,我就准备继续开工的,但是突然的,想要看B站.然后在一股 ...
- MyEclipse去除网上复制下来的代码带有的行号
正则表达式去除代码行号 作为开发人员,我们经常从网上复制一些代码,有些时候复制的代码前面是带有行号,如: MyEclipse本身自带有查找替换功能,并且支持正则表达式替换,使用正则替换就可以很容易去除 ...
- css实现未知高度水平垂直居中
页面设计中,经常需要实现元素的水平垂直居中,css实现的方法有很多(列如: margin: auto.position定位.css表达式calc().使用css预处理.table等都可以实现水平居中) ...
- Python 字典和集合
泛映射类型 collections.abc 模块中有 Mapping 和 MutableMapping 这两个抽象基类,它们的作用是为 dict 和其他类似的类型定义形式接口(在Python 2.6 ...