[WPF]使用WindowChrome自定義Window Style

来源:http://www.cnblogs.com/dino623/archive/2017/09/08/CustomWindowStyle.html
-Advertisement-
Play Games

1. 前言 做了WPF開發多年,一直未曾自己實現一個自定義Window Style,無論是《WPF編程寶典》或是各種博客都建議使用WindowStyle="None" 和 AllowsTransparency="True",於是想當然以為這樣就可以了。最近來了興緻想自己實現一個,才知道WindowS ...


1. 前言

做了WPF開發多年,一直未曾自己實現一個自定義Window Style,無論是《WPF編程寶典》或是各種博客都建議使用WindowStyle="None" AllowsTransparency="True",於是想當然以為這樣就可以了。最近來了興緻想自己實現一個,才知道WindowStyle="None" 的方式根本不好用,原因有幾點:

  • 如果Window沒有陰影會很難看,但自己添加DropShadowEffect又十分影響性能。
  • 需要自定義彈出、關閉、最大化、最小化動畫,而自己做肯定不如Windows自帶動畫高效。
  • 需要實現Resize功能。
  • 其它BUG。

光是性能問題就足以放棄WindowStyle="None" 的實現方式,幸好還有使用WindowChrome的實現方式,但一時之間也找不到理想的實現,連MSDN上的文檔( WindowChrome Class )都太過時,.NET 4.5也沒有SystemParameters2這個類,只好參考一些開源項目(如 Modern UI for WPF )自己實現了。

2. Window基本功能

Window的基本功能如上圖所示。註意除了標準的“最小化”、“最大化/還原”、"關閉"按鈕外,Icon上單擊還應該能打開窗體的系統菜單,雙擊則直接關閉窗體。

我想實現類似Office 2016的Window效果:陰影、自定義窗體顏色。陰影、動畫效果保留系統預設的就可以了,基本上會很耐看。

大多數自定義Window都有圓角,但我並不喜歡,低DPI的情況下只有幾個像素組成的圓角通常都不會很圓滑(如下圖),所以保留直角。

另外,激活、非激活狀態下標題欄顏色變更:

最終效果如下:

3. 實現

3.1 定義CustomWindow控制項

首先,為了方便以後的擴展,我定義了一個名為CustomWindow的模板化控制項派生自Window。

public class CustomWindow : Window
{
    public CustomWindow()
    {
        DefaultStyleKey = typeof(CustomWindow);
        CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, CloseWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, MaximizeWindow, CanResizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, MinimizeWindow, CanMinimizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, RestoreWindow, CanResizeWindow));
        CommandBindings.Add(new CommandBinding(SystemCommands.ShowSystemMenuCommand, ShowSystemMenu));
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);
        if (e.ButtonState == MouseButtonState.Pressed)
            DragMove();
    }

    protected override void OnContentRendered(EventArgs e)
    {
        base.OnContentRendered(e);
        if (SizeToContent == SizeToContent.WidthAndHeight)
            InvalidateMeasure();
    }

    #region Window Commands

    private void CanResizeWindow(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = ResizeMode == ResizeMode.CanResize || ResizeMode == ResizeMode.CanResizeWithGrip;
    }

    private void CanMinimizeWindow(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = ResizeMode != ResizeMode.NoResize;
    }

    private void CloseWindow(object sender, ExecutedRoutedEventArgs e)
    {
        this.Close();
        //SystemCommands.CloseWindow(this);
    }

    private void MaximizeWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.MaximizeWindow(this);
    }

    private void MinimizeWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.MinimizeWindow(this);
    }

    private void RestoreWindow(object sender, ExecutedRoutedEventArgs e)
    {
        SystemCommands.RestoreWindow(this);
    }


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

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

    #endregion
}

主要是添加了幾個CommandBindings,用於給標題欄上的按鈕綁定。

3.2 使用WindowChrome

對於WindowChrome,MSDN是這樣描述的:

若要自定義視窗,同時保留其標準功能,可以使用WindowChrome類。 WindowChrome類視窗框架的功能分離開來視覺對象,並允許您控制的客戶端和應用程式視窗的非工作區之間的邊界。

在CustomWindow的DefaultStyle中添加如下Setting:


<Setter Property="WindowChrome.WindowChrome">
    <Setter.Value>
        <WindowChrome CornerRadius="0"
                      GlassFrameThickness="1"
                      UseAeroCaptionButtons="False"
                      NonClientFrameEdges="None" />
    </Setter.Value>
</Setter>

這樣除了包含陰影的邊框,整個Window的內容就可以由用戶定義了。

3.3 Window基本佈局

<Border BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}"
        x:Name="WindowBorder">
    <Grid x:Name="LayoutRoot"
          Background="{TemplateBinding Background}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid x:Name="PART_WindowTitleGrid"
              Grid.Row="0"
              Height="26.4"
              Background="{TemplateBinding BorderBrush}">
           ....
        </Grid>
        <AdornerDecorator Grid.Row="1" KeyboardNavigation.IsTabStop="False">
            <ContentPresenter x:Name="MainContentPresenter"
                              KeyboardNavigation.TabNavigation="Cycle" />
        </AdornerDecorator>
        <ResizeGrip x:Name="ResizeGrip"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom"
                    Grid.Row="1"
                    IsTabStop="False"
                    Visibility="Hidden"
                    WindowChrome.ResizeGripDirection="BottomRight" />
    </Grid>
</Border>

Window的標準佈局很簡單,大致上就是標題欄和內容。
PART_WindowTitleGrid是標題欄,具體內容下一節再討論。

ContentPresenter的內容即Window的Client Area的範圍。

ResizeGrip是當ResizeMode = ResizeMode.CanResizeWithGrip;時出現的Window右下角的大小調整手柄,基本上用於提示視窗可以通過拖動邊框改調整小。

AdornerDecorator 為可視化樹中的子元素提供 AdornerLayer,如果沒有它的話一些裝飾效果不能顯示(例如下圖Button控制項的Focus效果),Window的 ContentPresenter 外面套個 AdornerDecorator 是 必不能忘的。

3.4 佈局標題欄

<Button x:Name="Minimize"
        ToolTip="Minimize"
        WindowChrome.IsHitTestVisibleInChrome="True"
        Command="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}"
        ContentTemplate="{StaticResource MinimizeWhite}"
        Style="{StaticResource TitleBarButtonStyle}"
        IsTabStop="False" />

標題欄上的按鈕實現如上,將Command綁定到SystemCommands,並且設置WindowChrome.IsHitTestVisibleInChrome="True",標題欄上的內容要設置這個附加屬性才能響應滑鼠操作。

<Button VerticalAlignment="Center"
        Margin="7,0,5,0"
        Content="{TemplateBinding Icon}"
        Height="{x:Static SystemParameters.SmallIconHeight}"
        Width="{x:Static SystemParameters.SmallIconWidth}"
        WindowChrome.IsHitTestVisibleInChrome="True"
        IsTabStop="False">
    <Button.Template>
        <ControlTemplate TargetType="{x:Type Button}">
            <Image Source="{TemplateBinding Content}" />
        </ControlTemplate>
    </Button.Template>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction Command="{x:Static SystemCommands.ShowSystemMenuCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{x:Static SystemCommands.CloseWindowCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

標題欄上的Icon也是一個按鈕,單機打開SystemMenu,雙擊關閉Window。Height和Widht的值分別使用了SystemParameters.SmallIconHeightSystemParameters.SmallIconWidth,SystemParameters包含可用來查詢系統設置的屬性,能使用SystemParameters的地方儘量使用總是沒錯的。

按鈕的樣式沒實現得很好,這點暫時將就一下,以後改進吧。

3.5 處理Triggers

<ControlTemplate.Triggers>
    <Trigger Property="IsActive"
             Value="False">
        <Setter Property="BorderBrush"
                Value="#FF6F7785" />
    </Trigger>
    <Trigger Property="WindowState"
             Value="Maximized">
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Visible" />
        <Setter TargetName="LayoutRoot"
                Property="Margin"
                Value="7" />
    </Trigger>
    <Trigger Property="WindowState"
             Value="Normal">
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Visible" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Collapsed" />
    </Trigger>
    <Trigger Property="ResizeMode"
             Value="NoResize">
        <Setter TargetName="Minimize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Maximize"
                Property="Visibility"
                Value="Collapsed" />
        <Setter TargetName="Restore"
                Property="Visibility"
                Value="Collapsed" />
    </Trigger>

    <MultiTrigger>
        <MultiTrigger.Conditions>
            <Condition Property="ResizeMode"
                       Value="CanResizeWithGrip" />
            <Condition Property="WindowState"
                       Value="Normal" />
        </MultiTrigger.Conditions>
        <Setter TargetName="ResizeGrip"
                Property="Visibility"
                Value="Visible" />
    </MultiTrigger>
</ControlTemplate.Triggers>

雖然我平時喜歡用VisualState的方式實現模板化控制項UI再狀態之間的轉變,但有時還是Trigger方便快捷,尤其是不需要做動畫的時候。
註意當WindowState=Maximized時要將LayoutRoot的Margin設置成7,如果不這樣做在最大化時Window邊緣部分會被遮蔽,很多使用WindowChrome自定義Window的方案都沒有處理這點。

3.6 處理導航

另一點需要註意的是鍵盤導航。一般來說Window中按Tab鍵,焦點會在Window的內容間迴圈,不要讓標題欄的按鈕獲得焦點,也不要讓ContentPresenter 的各個父元素獲得焦點,所以在ContentPresenter 上設置KeyboardNavigation.TabNavigation="Cycle"。為了不讓標題欄上的各個按鈕獲得焦點,在各個按鈕上還設置了IsTabStop="False"

3.7 DragMove

有些人喜歡不止標題欄,按住Window的任何空白部分都可以拖動Window,只需要在代碼中添加DragMove即可:

protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    base.OnMouseLeftButtonDown(e);
    if (e.ButtonState == MouseButtonState.Pressed)
        DragMove();
}

3.8 移植TransitioningContentControl

索性讓Window打開時內容也添加一些動畫。我將Silverlight Toolkit的TransitioningContentControl複製過來,只改了一點動畫,並且在OnApplyTemplate()最後添加了這句:VisualStateManager.GoToState(this, Transition, true);。最後將Window中的ContentPresenter 替換成這個控制項,效果還不錯(實際效果挺流暢的,可是GIF看起來不怎麼樣):

3.9 SizeToContent問題

有個比較麻煩的問題,當設置SizeToContent="WidthAndHeight",打開Window會出現以下錯誤。

看上去是內容的Size和Window的Size計算錯誤,目前的解決方法是在CustomWindow中添加以下代碼,簡單粗暴,但可能引發其它問題:

protected override void OnContentRendered(EventArgs e)
{
    base.OnContentRendered(e);
    if (SizeToContent == SizeToContent.WidthAndHeight)
        InvalidateMeasure();
}

5. 結語

第一次寫Window樣式,想不到遇到這麼多需要註意的地方。
目前只是個很簡單的Demo,沒有添加額外的功能,希望對他人有幫助吧。
編碼在Window10上完成,只在Windows7上稍微測試了一下,不敢保證相容性。
如有錯漏請指出。

6. 參考

Window Styles and Templates
WindowChrome 類
SystemParameters 類
mahapps.metro
Modern UI for WPF

7. 源碼

GitHub - WindowDemo


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Centos7 開始,網路由 NetworkManager 服務負責管理,相對於舊的 /etc/init.d/network 腳本,NetworkManager是動態的、事件驅動的網路管理服務。舊的 /etc/init.d/network 以及 ifup,ifdown 等依然存在,但是處於備用狀態, ...
  • 問題現象:由於重裝linux,並且加了固態硬碟,直接將系統裝在固態硬碟中。啟動伺服器的時候, 便看不到原來機械硬碟的掛載目錄了,不知如何訪問機械硬碟了。直接用命令 mount /dev/sda3 /store 掛載, 提示 mount: unknown filesystem type 'LVM2_m ...
  • Linux對於記憶體的管理涉及到非常多的方面,這篇文章首先從對進程虛擬地址空間的管理說起。(所依據的代碼是2.6.32.60) 無論是內核線程還是用戶進程,對於內核來說,無非都是task_struct這個數據結構的一個實例而已,task_struct被稱為進程描述符(process descripto ...
  • 首先找出所有可選的佈局(layout)方案: 可以看到 us 下有很多常見的佈局方案(以下為節選): 但文檔可能不全,比如早在 13 年加入的 norman 方案就未在 man 手冊列出。 可以通過搜索 symbol 文件找到: 更改佈局方案: 加上 / 選項會顯示輸出信息: 切換回主流的 QWER ...
  • 轉:http://blog.csdn.net/hongchangfirst/article/details/7075026 大家都知道進程,可是知道linux是怎麼管理其進程的嗎?每一個進程都有一個進程描述符,具體是task_struct結構體存儲相關的信息,在linux/sched.h文件里定義, ...
  • 一、分佈位置上的區別: kmalloc()和__get_free_pages()函數申請的記憶體位於物理記憶體的映射區域,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,因此存在簡單的線性關係;(3G+896M)(低端記憶體); vmalloc函數申請的虛擬記憶體與物理記憶體之間也沒有簡單的換 ...
  • 添加樣式: 在html中,需要創建2層div來實現。一個div包含另一個div: 效果: ...
  • 表格控制項 Spread Studio 發佈了全新的 V11 CTP 版本。在此版本中,Spread For WinForms 引入了 Spread Common,也帶來了 Spread 性能的巨大提升和記憶體消耗的急劇下降。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...