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行為很怪異,最好不要這樣設置。