WPF自定義介面WindowChrome

醜萌氣質狗發表於2021-12-28

WPF自定義介面WindowChrome

預設WPF的介面其實也還行,就是滿足不了日漸增長的需求,介面還是需要有更高的自定義程度,包括標題欄也要能夠塞下更多的操作控制元件。

預設視窗介紹

新建WPF專案,給裡面內容設定一點顏色:

image-20211223154246966

預設建立的介面(Win10上的效果),能夠看到兩塊區域,一塊是以顏色#0078D4內容區,一塊是頂部白色的非內容區,按照官方的說法它們依次是客戶區非客戶區

客戶區就是我們的主體內容,目前裡面有一排文字,這沒什麼好說的, 你想放什麼內容,就往這裡放就行了。

非客戶區裡面的東西稍微有一點多,一點一點來列吧:

左上角的應用圖示標題

image-20211223155013699

右上角的最小化最大化關閉按鈕:

image-20211223155128648

還有一個就是我們窗體的邊框線,黑色的那條。

以上都是我們能夠看到的,還有一些看不見的視窗行為

縮放標題欄的拖動以及滑鼠拖到視窗到桌面邊緣位置,可以對視窗進行一個大小的調整:

動畫

點選應用圖示之後彈出的視窗操作:

image-20211223160124239

以及在視窗拖動和在拖動的過程中中,視窗會自動擴充到對應的螢幕:

動畫1

其實呢, 還有一些行為,這裡就不做敘述了,下面我們來看看如何自定義

自定義介面方式

WPF中自定義的介面的方式可以分為兩種,一種是使用 AllowsTransparency="True"WindowStyle="None",這種呢就相當於直接把原生非客戶區給幹掉了,然後我們在內容區域自己去實現非客戶區,就會導致視窗自定的行為如:縮放拖動停靠邊界放大。。。這些功能全都沒有了,如果需要的話,是需要自己手動程式碼新增的。

第二種呢就是使用我們的WindowChrome來自定義介面, 這種方式保留了一個視窗基本的行為,只需要我們重新規劃一下客戶區非客戶區就行了。

兩種方式都不復雜,不過介面客戶區非客戶區都是需要自己去定義的,但是WindowChrome的方式保留了視窗的一些基本行為,顯示效能也會比第一種強。

至於二者視窗效能的對比,可以看下這個大佬的文章:
(walterlv 呂毅)WPF 製作高效能的透明背景異形視窗(使用 WindowChrome 而不要使用 AllowsTransparency=True)

使用WindowChrome

兩種方式自定義介面沒什麼太大區別, 這裡就以WindowChrome來進行舉例。

使用WindowChrome的方式也很簡單,直接宣告一下就行了:

    <WindowChrome.WindowChrome>
        <WindowChrome />
    </WindowChrome.WindowChrome>
    <Grid Background="#0078D4">
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="50"
            FontWeight="Bold"
            Foreground="White"
            Text="醜萌氣質狗" />
    </Grid>

得到如下的介面:

image-20211223170623630

這個介面擁有預設視窗的所有視窗行為,就是沒有應用圖示、標題、操作按鈕(最大化、最小化、關閉),其實不是沒有,只是被當前Window的預設樣式給遮擋了

只需要去除當前Window的樣式,就可以看到WindowChrome的樣式:

    <Window.Template>
        <ControlTemplate TargetType="{x:Type Window}">
            <Border />
        </ControlTemplate>
    </Window.Template>

image-20211227154025409

作為對比看一下預設的Window是什麼樣子的:

image-20211227154216344

可以看到,僅僅在關閉的地方,WindowChrome的樣式就和預設的Window不一致,操作欄都無法靠邊界,而且嘗試修改這些樣式也不好調整到我們想要的樣子,所以我們直接拋棄原來的樣式,重寫WindowChrome,自己定義客戶區非客戶區,如果對WindowChrome的樣式調整有興趣,可以看一下:

(walterlv 呂毅)WPF 使用 WindowChrome,在自定義視窗標題欄的同時最大程度保留原生視窗樣式(類似 UWP/Chrome)

除了需要這些東西以外, 還需要新增我們自定義的操作按鈕,下面我們就要重寫一下視窗的樣式,自定義裡面的客戶區非客戶區

自定義WindowChrome說明

在重寫樣式之前呢,先來介紹幾個WindowChrome的幾個屬性:

CaptionHeight:標題欄的操作高度,一般跟隨非客戶區高度,設定為0表示介面無法響應滑鼠的任何操作(拖動視窗, 雙擊標題欄的最大化最小化。。。 僅僅是行為區域,並不影響外觀)。

GlassFrameThickness:用來設定距離區域的厚度,設定為-1,可以使得區域覆蓋整個介面

ResizeBorderThickness:可以設定視窗縮放的邊框厚度,不便於設定過大。下圖是設定為50的效果:

image-20211227164038836

可以看到侵入內容區這麼多,依然可以進行視窗縮放。

還有一個很重要的屬性,不是給WindowChrome設定的,是給介面中的元素設定的,如果你的元素在CaptionHeight的高度內,是無法響應滑鼠事件的,此時需要給元素設定屬性WindowChrome.IsHitTestVisibleInChrome="True",很常見的問題,在自定義標題按鈕的時候,會發現自己點選不了標題按鈕!

自定義WindowChrome

既然是要自定義客戶區非客戶區,那麼我們稍微設計一下(下面示例所有屬性均寫死,自行更改)

根元素使用Border,我們設定它的邊框和顏色,內部使用Grid元素,進行上下兩行分割,第一行客戶區, 第二行非客戶區,第一行的高度改為Auto,第二行預設使用剩下全部區域。

為了撐開非客戶區,這裡繫結了一個視窗的標題。

大概程式碼如下

<Window.Template>
        <ControlTemplate TargetType="{x:Type Window}">
            <Border
                x:Name="border"
                Background="White"
                BorderBrush="#0078d4"
                BorderThickness="1"
                UseLayoutRounding="True">
                <Grid>
                    <!--  上下區域 上面是非客戶區,下面是客戶區  -->
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    <!--  非客戶區  -->
                    <Grid
                        Grid.Row="0"
                        Background="#0078d4">
                        <TextBlock
                            Padding="10,0,0,0"
                            VerticalAlignment="Center"
                            Text="{TemplateBinding Title}" />
                    </Grid>

                    <!--  客戶區  -->
                    <AdornerDecorator Grid.Row="1">
                        <ContentPresenter ClipToBounds="True" />
                    </AdornerDecorator>
                </Grid>
            </Border>
        </ControlTemplate>
</Window.Template>

呈現的效果如下(用裝飾器AdornerDecorator包裝了一下客戶區,為了後期方便新增遮罩層,有文章說明,目前沒寫,後續弄好了連結再貼過來吧):

image-20211227171937366

客戶區沒啥說的,你自己想怎麼寫就怎麼寫, 主要需要說一下非客戶區的圖示、標題(已經有了)、操作按鈕、自定義按鈕,直接貼一下程式碼,樣式這麼就不加了,採用自己的樣式,主要說明下使用系統的幾個命令。

                    <!--  非客戶區  -->
                    <Grid
                        Grid.Row="0"
                        Background=" #0078d4">
                        <!--  左右區域 左邊是窗體圖示文字 右邊是操作按鈕、最小化、最大化、關閉  -->
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <!--  圖片 標題  -->
                        <StackPanel
                            HorizontalAlignment="Left"
                            VerticalAlignment="Center"
                            Orientation="Horizontal"
                            WindowChrome.IsHitTestVisibleInChrome="True">
                            <Button
                                Width="16"
                                Height="16"
                                Margin="10,0,0,0"
                                Command="{x:Static SystemCommands.ShowSystemMenuCommand}"
                                Content="我是圖示" />
                            <TextBlock
                                Margin="5,0,0,0"
                                VerticalAlignment="Center"
                                Text="{TemplateBinding Title}" />
                        </StackPanel>

                        <!--  操作按鈕  -->
                        <StackPanel
                            Grid.Column="1"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Top"
                            Orientation="Horizontal"
                            WindowChrome.IsHitTestVisibleInChrome="True">
                            <Button>
                                <Path
                                    Width="12"
                                    Height="12"
                                    Data="F1 M 17.412109 9.648438 C 17.412109 9.707031 17.413736 9.765625 17.416992 9.824219 C 17.420246 9.882812 17.421875 9.941406 17.421875 10 C 17.421875 10.058594 17.420246 10.117188 17.416992 10.175781 C 17.413736 10.234375 17.412109 10.292969 17.412109 10.351562 L 19.941406 11.923828 L 18.388672 15.664062 L 15.488281 15 C 15.332031 15.169271 15.169271 15.332031 15 15.488281 L 15.664062 18.388672 L 11.923828 19.941406 L 10.351562 17.412109 C 10.292969 17.412109 10.234375 17.413736 10.175781 17.416992 C 10.117188 17.420248 10.058594 17.421875 10 17.421875 C 9.941406 17.421875 9.882812 17.420248 9.824219 17.416992 C 9.765625 17.413736 9.707031 17.412109 9.648438 17.412109 L 8.076172 19.941406 L 4.335938 18.388672 L 5 15.488281 C 4.830729 15.332031 4.667969 15.169271 4.511719 15 L 1.611328 15.664062 L 0.058594 11.923828 L 2.587891 10.351562 C 2.587891 10.292969 2.586263 10.234375 2.583008 10.175781 C 2.579752 10.117188 2.578125 10.058594 2.578125 10 C 2.578125 9.941406 2.579752 9.882812 2.583008 9.824219 C 2.586263 9.765625 2.587891 9.707031 2.587891 9.648438 L 0.058594 8.076172 L 1.611328 4.335938 L 4.511719 5 C 4.667969 4.830729 4.830729 4.667969 5 4.511719 L 4.335938 1.611328 L 8.076172 0.058594 L 9.648438 2.587891 C 9.707031 2.587891 9.765625 2.586264 9.824219 2.583008 C 9.882812 2.579754 9.941406 2.578125 10 2.578125 C 10.058594 2.578125 10.117188 2.579754 10.175781 2.583008 C 10.234375 2.586264 10.292969 2.587891 10.351562 2.587891 L 11.923828 0.058594 L 15.664062 1.611328 L 15 4.511719 C 15.169271 4.667969 15.332031 4.830729 15.488281 5 L 18.388672 4.335938 L 19.941406 8.076172 Z M 16.269531 10.917969 C 16.282551 10.761719 16.295572 10.607097 16.308594 10.454102 C 16.321613 10.301107 16.328125 10.146484 16.328125 9.990234 C 16.328125 9.840495 16.321613 9.6875 16.308594 9.53125 C 16.295572 9.375 16.282551 9.222006 16.269531 9.072266 L 18.574219 7.636719 L 17.734375 5.605469 L 15.087891 6.220703 C 14.886067 5.973309 14.679361 5.745443 14.467773 5.537109 C 14.256184 5.328776 14.026691 5.120443 13.779297 4.912109 L 14.394531 2.265625 L 12.363281 1.425781 L 10.917969 3.730469 C 10.768229 3.717449 10.615234 3.704428 10.458984 3.691406 C 10.302734 3.678387 10.149739 3.671875 10 3.671875 C 9.84375 3.671875 9.689127 3.678387 9.536133 3.691406 C 9.383138 3.704428 9.228516 3.717449 9.072266 3.730469 L 7.636719 1.425781 L 5.605469 2.265625 L 6.220703 4.912109 C 5.973307 5.113933 5.745442 5.320639 5.537109 5.532227 C 5.328776 5.743816 5.120442 5.973309 4.912109 6.220703 L 2.265625 5.605469 L 1.425781 7.636719 L 3.730469 9.082031 C 3.717448 9.238281 3.704427 9.392904 3.691406 9.545898 C 3.678385 9.698894 3.671875 9.853516 3.671875 10.009766 C 3.671875 10.159506 3.678385 10.3125 3.691406 10.46875 C 3.704427 10.625 3.717448 10.777995 3.730469 10.927734 L 1.425781 12.363281 L 2.265625 14.394531 L 4.912109 13.779297 C 5.113932 14.026693 5.320638 14.254558 5.532227 14.462891 C 5.743815 14.671225 5.973307 14.879558 6.220703 15.087891 L 5.605469 17.734375 L 7.636719 18.574219 L 9.082031 16.269531 C 9.231771 16.282553 9.384766 16.295572 9.541016 16.308594 C 9.697266 16.321615 9.85026 16.328125 10 16.328125 C 10.15625 16.328125 10.310872 16.321615 10.463867 16.308594 C 10.616861 16.295572 10.771484 16.282553 10.927734 16.269531 L 12.363281 18.574219 L 14.394531 17.734375 L 13.779297 15.087891 C 14.026691 14.886068 14.254557 14.679362 14.462891 14.467773 C 14.671224 14.256186 14.879557 14.026693 15.087891 13.779297 L 17.734375 14.394531 L 18.574219 12.363281 Z M 10 6.328125 C 10.507812 6.328125 10.9847 6.424154 11.430664 6.616211 C 11.876627 6.80827 12.265624 7.070313 12.597656 7.402344 C 12.929687 7.734376 13.19173 8.123373 13.383789 8.569336 C 13.575846 9.0153 13.671875 9.492188 13.671875 10 C 13.671875 10.507812 13.575846 10.984701 13.383789 11.430664 C 13.19173 11.876628 12.929687 12.265625 12.597656 12.597656 C 12.265624 12.929688 11.876627 13.191732 11.430664 13.383789 C 10.9847 13.575847 10.507812 13.671875 10 13.671875 C 9.492188 13.671875 9.015299 13.575847 8.569336 13.383789 C 8.123372 13.191732 7.734375 12.929688 7.402344 12.597656 C 7.070312 12.265625 6.808268 11.876628 6.616211 11.430664 C 6.424153 10.984701 6.328125 10.507812 6.328125 10 C 6.328125 9.492188 6.424153 9.0153 6.616211 8.569336 C 6.808268 8.123373 7.070312 7.734376 7.402344 7.402344 C 7.734375 7.070313 8.123372 6.80827 8.569336 6.616211 C 9.015299 6.424154 9.492188 6.328125 10 6.328125 Z M 10 12.578125 C 10.358072 12.578125 10.693359 12.511394 11.005859 12.37793 C 11.318359 12.244467 11.591797 12.060547 11.826172 11.826172 C 12.060547 11.591797 12.244466 11.318359 12.37793 11.005859 C 12.511393 10.693359 12.578125 10.358073 12.578125 10 C 12.578125 9.641928 12.511393 9.306641 12.37793 8.994141 C 12.244466 8.681641 12.060547 8.408203 11.826172 8.173828 C 11.591797 7.939453 11.318359 7.755534 11.005859 7.62207 C 10.693359 7.488607 10.358072 7.421875 10 7.421875 C 9.641927 7.421875 9.306641 7.488607 8.994141 7.62207 C 8.681641 7.755534 8.408203 7.939453 8.173828 8.173828 C 7.939453 8.408203 7.755533 8.681641 7.62207 8.994141 C 7.488606 9.306641 7.421875 9.641928 7.421875 10 C 7.421875 10.358073 7.488606 10.693359 7.62207 11.005859 C 7.755533 11.318359 7.939453 11.591797 8.173828 11.826172 C 8.408203 12.060547 8.681641 12.244467 8.994141 12.37793 C 9.306641 12.511394 9.641927 12.578125 10 12.578125 Z "
                                    Fill="{Binding (TextBlock.Foreground), RelativeSource={RelativeSource AncestorType=Button}}"
                                    RenderTransformOrigin="0.5,0.5"
                                    Stretch="Uniform" />
                            </Button>
                            <Button
                                x:Name="btnMinimizeButton"
                                Command="{x:Static SystemCommands.MinimizeWindowCommand}">
                                <Path
                                    Width="10"
                                    Height="10"
                                    Data="M0,4 L10,4 L10,5 L0,5 z"
                                    Fill="{Binding (TextBlock.Foreground), RelativeSource={RelativeSource AncestorType=Button}}"
                                    RenderTransformOrigin="0.5,0.5"
                                    Stretch="Uniform" />
                            </Button>
                            <Button
                                x:Name="btnMaximizeButton"
                                Command="{x:Static SystemCommands.MaximizeWindowCommand}">
                                <Path
                                    Width="10"
                                    Height="10"
                                    Data="M1,1 L1,9 L9,9 L9,1 z M0,0 L10,0 L10,10 L0,10 z"
                                    Fill="{Binding (TextBlock.Foreground), RelativeSource={RelativeSource AncestorType=Button}}"
                                    RenderTransformOrigin="0.5,0.5"
                                    Stretch="Uniform" />
                            </Button>
                            <Button
                                x:Name="btnRestoreButton"
                                Command="{x:Static SystemCommands.RestoreWindowCommand}"
                                Visibility="Collapsed">
                                <Path
                                    Width="10"
                                    Height="10"
                                    Data="M1,3 L1,9 L7,9 L7,3 z M3,1 L3,2 L8,2 L8,7 L9,7 L9,1 z M2,0 L10,0 L10,8 L8,8 L8,10 L0,10 L0,2 L2,2 z"
                                    Fill="{Binding (TextBlock.Foreground), RelativeSource={RelativeSource AncestorType=Button}}"
                                    RenderTransformOrigin="0.5,0.5"
                                    Stretch="Uniform" />
                            </Button>
                            <Button
                                x:Name="btnCloseButton"
                                Command="{x:Static SystemCommands.CloseWindowCommand}">
                                <Path
                                    Width="10"
                                    Height="10"
                                    Data="M0.7,0 L5,4.3 L9.3,0 L10,0.7 L5.7,5 L10,9.3 L9.3,10 L5,5.7 L0.7,10 L0,9.3 L4.3,5 L0,0.7 z"
                                    Fill="{Binding (TextBlock.Foreground), RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
                                    RenderTransformOrigin="0.5,0.5"
                                    Stretch="Uniform" />
                            </Button>
                        </StackPanel>
                    </Grid>

樣式出完的效果是這樣的:

image-20211228100417631

這裡記得需要給按鈕的父元件新增WindowChrome.IsHitTestVisibleInChrome="True",也可以給按鈕本身新增,看自己的需要,不然會導致按鈕無法點選。

雖然新增了屬性,但是發現圖中依然有按鈕是置灰的,那是因為我們給按鈕繫結了系統的命令,但是了這個命令並沒有初始化,所以需要到後臺,進行命令的初始化

        public MainWindow()
        {
            InitializeComponent();

            CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (_, __) => { SystemCommands.CloseWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (_, __) => { SystemCommands.MinimizeWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (_, __) => { SystemCommands.MaximizeWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (_, __) => { SystemCommands.RestoreWindow(this); }));
            CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu));

        }

        private void ShowSystemMenu(object sender, ExecutedRoutedEventArgs e)
        {
            var element = e.OriginalSource as FrameworkElement;
            if (element == null)
                return;

            var position = WindowState == WindowState.Maximized ? new Point(0, element.ActualHeight)
                : new Point(Left + BorderThickness.Left, element.ActualHeight + Top + BorderThickness.Top);
            position = element.TransformToAncestor(this).Transform(position);
            SystemCommands.ShowSystemMenu(this, position);
        }

現在視窗是正常了,但是放大之後並沒有更換放大區域的圖示,可以看到,我們擺放按鈕的時候, 有一個按鈕被隱藏了, 這就是我們的還原介面按鈕,所以需要新增一些觸發器,讓介面在放大之後,可以更換一下圖示,並且我們在放大介面的時候, 明顯能夠感覺到標題欄超出了螢幕外圍(視窗最大化的時候設定BorderThickness=8解決此問題):

image-20211228104516111

上面說的這些都可以放在樣式和樣式的觸發器中去實現,具體的步驟就不說明了,直接貼一下完整的程式碼,供學習參考(因為很多東西都寫死了,所以根據自己的需要,自行進行動態更改)

完整程式碼的效果:

image-20211228113237884

完整程式碼下載

最後說一句

AllowsTransparency="True"的這種方式依然是適用的,重在開發快速高效,有時候公司的專案,並不需要過多的預設視窗行為,還有就是有些老哥就是喜歡自己新增這些視窗行為,任何一種方式都有有利有弊,效能之戰也無終章。

在滿足工作要求的時候,我們也要有更高的追求!

沒有銀彈!

相關文章