[WPF]使用WindowChrome自定義Window Style


由於內容陳舊,已經寫了新的文章代替這篇,請參考新的文章:

Window(窗體)的UI元素及行為

這篇文章主要討論標准Window的UI元素和行為。無論是桌面編程還是日常使用,Window(窗體)都是最常接觸的UI元素之一,既然Window這么重要那么多了解一些也沒有壞處。

使用WindowChrome自定義Window Style

介紹使用WindowChrome自定義Window的原理及各種細節。

使用WindowChrome的問題

使用WindowChrome自定義Window會遇到很多問題,例如最大化的尺寸問題,這篇文章介紹如何處理這些細節。

使用WindowChrome自定義RibbonWindow

因為WPF原生的RibbonWindow有不少UI上的Bug,所以我提供了一個自定義的RibbonWindow以解決這些問題。

以下為原內容-----------------------------------------------------------------------------

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM