[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
對話方塊概述 _ Microsoft Docs
SystemParameters Class (System.Windows) Microsoft D