[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以外的值時(即CanMinimizeCanResizeCanResizeWithGrip)這三個按鈕纔會出現,如果ResizeMode設置爲CanMinimize最大化還原都會被禁用。

關閉按鈕

因爲關閉按鈕基本上一定會存在所以把它獨立出來,只是ResizeMode設置爲NoResize關閉按鈕會比較小。在Windows 10中最大化時關閉按鈕貼着右上角,這樣比較方便鼠標操作。

調整大小

當Window的ResizeMode設置爲CanResizeCanResizeWithGrip時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 Docs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章