[WPF自定义控件]Window(窗体)的UI元素及行为
1. 前言
本来打算写一篇《自定义Window》的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定义Window需要用到的部分基础知识独立出来,于是就形成了这篇文章。
无论是桌面编程还是日常使用,Window(窗体)都是最常接触的UI元素之一,既然Window这么重要那么多了解一些也没有坏处。
2.标准Window

这篇文章主要讨论标准的Window,不包括奇形怪状的无边框、非矩形Window,即只讨论WindowStyle="SingleBorderWindow"(默认值)的Window。
一个标准的Window的基本构成如上图所示,它主要由非工作区(non-client area)和工作区(client area)组成。上图中中间白色的部分即client area,在WPF对应下面代码中注释的部分:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.MarkupAndCodeBehindWindow">
<!-- Client area (for content) -->
</Window>
标准window中除client之外的部分称为non-client area,通常称之为chrome,它提供了提供了标准的窗口功能和行为,具体包含以下部分:
- 边框
- 阴影
- 标题栏
- Icon
- 标题
- SystemMenu
最小化、最大化和还原按钮关闭按钮- 大小调整手柄
边框
标准Window肯定会有边框的,在Windows 7上因为有Aero效果所以看上去很棒,现在偶尔用用Windows 7还是觉得很漂亮。但就如上图所示圆角不够平滑,如果电脑不是高分屏的话应该会更明显,例如这样:

因为圆角总是很难处理所以我不是很喜欢圆角的设计。
Windows 10的边框就时髦很多,如果在“个性化>颜色”设置页面取消标题栏和窗口边框,看上去就像是无边框(其实是把边框做成白色的了):

阴影
阴影用于体现UI的深度,属于装饰元素,Windows 的窗体通常都带有阴影,除非在“系统属性->高级->性能选项->视觉效果”里关闭“在窗口下显示阴影”选项。
标题栏
只要是标准的Window就应该有标题栏。一些浏览器看上去没有标题栏;当Fluent Design System出来后流行将内容扩展到标题栏,越来越多的应用看上去没有了标题栏。其实标题栏总是存在,能拖动,点击右键会弹出SystemMenu,并且最右边有关闭按钮的部分就是标题栏了。
双击标题栏还可以执行最大化或还原操作。
有一点细节可能不太容易注意到,当Window处于最大化状态时标题栏比较矮。在100% DPI时标题栏的高度为30像素,最大化时变为22像素,这时候右上角的几个按钮缩小了,其它元素的Margin也减少了一些。
Icon
Icon是指标题栏左边的窗体图标,这倒真的很常消失。在100% DPI的情况下它是个16 * 16 像素的图片。
顺便一提双击Icon会关闭Window,但我想一般都会用右边的关闭按钮的吧。
标题
标准Window的标题位于Icon右边。如果Window边框是深色,标题文字颜色为白色;反之则为黑色。

SystemMenu
在标题栏上点击鼠标右键出现的ContextMenu即是SystemMenu,它包括调整大小、移动和关闭操作。在Icon上点击鼠标左键,或者按Alt+空格都会在标题栏左下方弹出SystemMenu。
不过很少见到有人用SystemMenu,我也只是用它来确定标题栏的范围而已。
最小化、最大化和还原按钮
当Window的ResizeMode设置为NoResize以外的值时(即CanMinimize、CanResize和CanResizeWithGrip)这三个按钮才会出现,如果ResizeMode设置为CanMinimize则最大化和还原都会被禁用。
关闭按钮
因为关闭按钮基本上一定会存在所以把它独立出来,只是ResizeMode设置为NoResize时关闭按钮会比较小。在Windows 10中最大化时关闭按钮贴着右上角,这样比较方便鼠标操作。
调整大小
当Window的ResizeMode设置为CanResize或CanResizeWithGrip时Window可以使用最大化和还原按钮或SystemMenu调整大小,也可以通过拖动边框调整大小。
大小调整手柄
当Window的ResizeMode设置为CanResizeWithGrip并且WindowState = Normal时右下角会出现大小调整手柄,外观为组成三角形的一些点。除了让可以操作的区域变大一些,还可以用来提示Window是可以调整大小的。
拖动
有些Window会做成整个Window都可以通过拖动来改变位置,标准Window则只有标题栏可以拖动。
激活
激活或非激活的Window之间的区别主要体现在标题栏、边框及标题文字的颜色。在标题栏使用了AcrylicBrush的UWP应用还体现在非激活时AcrylicBrush变成纯色不透明的Brush。

焦点
一个Window中只有client area中的内容可以获得键盘焦点,而且tab键只会让键盘焦点在Window的内容中循环。当一个Window从非激活状态会到激活状态,之前获得键盘焦点的元素将重新获得键盘焦点。
动画

Window在最大化、最小化、还原有缩放的动画,这个动画可以清晰地指示Window的最终位置。当任务栏内容很多的时候,向下缩放到任务栏对应位置的动画尤其重要。
FlashWindow
如果一个Window设置了Owner并且以ShowDialog的方式打开,点击它的Owner将对这个Window调用FlashWindowEx功能,即闪烁几下,并且还有提示音。除了这种方式还可以用编程的方式调用FlashWindow功能。

Window的大小
最后要说的是Window的大小。Window的实际大小并不是表面上看到的大小。在Windows 10,以1920 * 1080 分辨率,100% DPI为例,打开以下XAML定义的一个Window:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow"
Height="600"
Width="800">
<Grid x:Name="LayoutRoot">
</Grid>
</Window>
通过实时可视化树可以看到,Window本身的小时确实是800 * 600,但LayoutRoot的大小只有784 * 561。将Window最大化后Window的大小变为1936 * 1066,而LayoutRoot的大小变为1920 * 1027。
如果将Window设置为启动位置在左上角:
WindowStartupLocation="Manual"
Top="0"
Left="0"

结果它并不会完全贴着左上角,而是左边有一点空间,上面没有。
通过Inspect看到的Window如下,黄色边框为它的实际范围:

可以看到系统理解的Window范围和我们看到的不同,这是Window设计的问题,有几个值用于计算chrome的尺寸:
| 属性 | 值(像素) | 描述 |
|---|---|---|
| SM_CXFRAME/SM_CYFRAME | 4 | The thickness of the sizing border around the perimeter of a window that can be resized, in pixels. SM_CXSIZEFRAME is the width of the horizontal border, and SM_CYSIZEFRAME is the height of the vertical border.This value is the same as SM_CXFRAME. |
| SM_CXPADDEDBORDER | 4 | The amount of border padding for captioned windows, in pixels.Windows XP/2000: This value is not supported. |
| SM_CYCAPTION | 23 | The height of a caption area, in pixels. |
在有标题的标准Window,chrome的顶部尺寸为SM_CYFRAME + SM_CXPADDEDBORDER + SM_CYCAPTION = 31,左右两边尺寸为SM_CXFRAME + SM_CXPADDEDBORDER = 8,底部尺寸为SM_CYFRAME + SM_CXPADDEDBORDER = 8。
最大化情况下Border和ResizeBorder都超出屏幕范围而且被隐藏了,所以Window的尺寸会超过显示器工作区的尺寸,这时候标题栏也会相应地变矮。在Windows 10,系统认为Window有4像素的ResizeBorder,但因为Windows 10是窄边框设计,而且在普通状态下和最大化状态下的标题栏高度还不一样,导致用UISpy观察Window和我们看到的Window不一致,也常常导致位置计算上的问题。
注意,上面的尺寸计算都是基于100 % DPI,在不同DPI的情况下还需要将DPI的值纳入计算。
3. 结语
标准Window的外观和行为基本上已经列出来了(其实还有很多,例如按住标题栏抖一抖可以缩小其它所有窗口这种功能,但这些不影响自定义Window的行为就不一一列出了),更多的内容请见下面给出的参考链接。
顺便一提设置SizeToContent="WidthAndHeight"并且 WindowState="Maximized"的Window行为很怪异,最好不要这样设置。
4. 参考
WPF Windows 概述 _ Microsoft Docs
SystemParameters Class (System.Windows) Microsoft Docs
[WPF自定义控件]Window(窗体)的UI元素及行为的更多相关文章
- [WPF自定义控件]?Window(窗体)的UI元素及行为
原文:[WPF自定义控件]?Window(窗体)的UI元素及行为 1. 前言 本来打算写一篇<自定义Window>的文章,但写着写着发觉内容太多,所以还是把使用WindowChrome自定 ...
- 在WPF中减少逻辑与UI元素的耦合
原文:在WPF中减少逻辑与UI元素的耦合 在WPF中减少逻辑与UI元素的耦合 周银辉 1, 避免在逻辑中引用界面元素,别把后台数据强加给UI 一个糟糕的案例 比如说主界 ...
- Wpf从资源中重用UI元素
在我的界面上有几个选项卡,每个选项卡中都有下面的元素: <StackPanel Orientation="Horizontal"> <Button Content ...
- WPF 中使用附加属性,将任意 UI 元素或控件裁剪成圆形(椭圆)
不知从什么时候开始,头像流行使用圆形了,于是各个平台开始追逐显示圆形裁剪图像的技术.WPF 作为一个优秀的 UI 框架,当然有其内建的机制支持这种圆形裁剪. 不过,内建的机制仅支持画刷,而如果被裁剪的 ...
- WPF FindName()查找命名注册的元素
一.查找xaml中命名注册的元素 <Button x:Name="btn1" Content="显示内容" HorizontalAlignment=&qu ...
- 拒绝卡顿——在WPF中使用多线程更新UI
原文:拒绝卡顿--在WPF中使用多线程更新UI 有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例: public partial class MainW ...
- WPF中不规则窗体与WebBrowser控件的兼容问题解决办法
原文:WPF中不规则窗体与WebBrowser控件的兼容问题解决办法 引言 这几天受委托开发一个网络电视项目,要求初步先使用内嵌网页形式实现视频播放和选单,以后再考虑将网页中的所有功能整合进桌面程序. ...
- WPF教程十四:了解元素的渲染OnRender()如何使用
上一篇分析了WPF元素中布局系统的MeasureOverride()和ArrangeOverride()方法.本节将进一步深入分析和研究元素如何渲染它们自身. 大多数WPF元素通过组合方式创建可视化外 ...
- CSharpGL(6)在OpenGL中绘制UI元素
CSharpGL(6)在OpenGL中绘制UI元素 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo,更适合入 ...
随机推荐
- 【Tsinsen】A1280. 最长双回文串
Bryce1010模板 http://www.tsinsen.com/A1280### 题目分析:记录一个点向后和向前的最长回文串,然后就是max(Llen[i]+Rlen[i+1])了. #incl ...
- 洛谷 P2147 [SDOI2008]洞穴勘测
以下这个做法应该是叫线段树分治... 根据修改操作预处理出每条边存在的时间区间[l,r](以操作序号为时间),然后把所有形式化后的修改挂到线段树节点上. 处理完修改后,dfs一遍线段树,进入某个节点时 ...
- Building Forest CodeForces - 195E
Building Forest CodeForces - 195E 这题意真是难懂啊...话说"An oriented weighted forest is an acyclic weigh ...
- 题解报告:hihoCoder #1050 : 树中的最长路
描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中,小Ho发现他不仅仅可以拼凑成一棵二叉树!还可以拼凑成一棵多叉树——好吧,其实就是更为平常的树而已. 但 ...
- [转]写给Git初学者的7个建议
本文转自:http://www.open-open.com/news/view/b7227e 阅读目录 第一条:花时间去学习 Git 的基本操作 第二条:从简单的 Git 工作流开始 第四条:理解分支 ...
- php 生成饼状图,折线图,条形图 通用类 2
生成饼状图,折线图,条形图通用的php类,这里使用的是国外的 HighCharts,前台页面别忘了调用HighCahrt js HighCharts中文网站 http://www.hcharts. ...
- Handler引起的内存泄露
一般我都写handler的时候是这样的: public class MyActivity extends Activity{ private final Handler myHandler = n ...
- [Github筆記] 清除所有 Commit 紀錄
# 把原來的 git 移除掉 sudo rm .git -r # 初始化 git init git remote add origin https://github.com/username/repo ...
- Outlook 数据文件(.pst 和 .ost)简介
使用 Microsoft Outlook 时,电子邮件.日历.任务和其他项目保存在邮件服务器或计算机上,或者同时保存在这两个位置.如果 Outlook 项目保存在计算机上,则它们保存在 Outlook ...
- TensorFlow中屏蔽warning的方法
问题 使用sudo pip3 install tensorflow安装完CPU版tensorflow后,运行简单的测试程序,出现如下警告: I tensorflow/core/platform/cpu ...