1. 程式人生 > >[WPF自定義控制元件庫]使用WindowChrome自定義RibbonWindow

[WPF自定義控制元件庫]使用WindowChrome自定義RibbonWindow

1. 為什麼要自定義RibbonWindow

自定義Window有可能是設計或功能上的要求,可以是非必要的,而自定義RibbonWindow則不一樣:

  • 如果程式使用了自定義樣式的Window,為了統一外觀需要把RibbonWindow一起修改樣式。
  • 為了解決RibbonWindow的BUG。

如上圖所示,在Windows 10 上執行開啟RibbonWindow,可以看到標題欄的內容(包括分隔符)沒有居中對齊,缺少下邊框。

在最大化的時候標題欄內容甚至超出螢幕範圍。

WPF提供的Ribbon是個很古老很古老的控制元件,附帶的RibbonWindow也十分古老。RibbonWindow在以前應該可以執行良好,但多年沒有更新,在.NET 4.5(或者說是WIN7平臺,我沒仔細考究)後就出現了這個問題。作為專業軟體這可能沒法接受,而這個問題微軟好像也沒打算修復。以前的做法通常是使用Fluent.Ribbon之類的第三方元件,因為我已經在Kino.Toolkit.Wpf中提供了使用WindowChrome自定義的Window,為了統一外觀於是順手自定義一個ExtendedRibbonWindow。

2. 問題產生的原因

RibbonWindow是派生自Window,並使用了WindowChrome,它的ControlTemplate大概是這樣:

<Grid>
    <Border Name="PART_ClientAreaBorder"
            Background="{TemplateBinding Control.Background}"
            BorderBrush="{TemplateBinding Control.BorderBrush}"
            BorderThickness="{TemplateBinding Control.BorderThickness}"
            Margin="{Binding Path=(SystemParameters.WindowNonClientFrameThickness)}" />
    <Border BorderThickness="{Binding Path=(WindowChrome.WindowChrome).ResizeBorderThickness, RelativeSource={RelativeSource TemplatedParent}}">
        <Grid>
            <Image Name="PART_Icon"
                   WindowChrome.IsHitTestVisibleInChrome="True"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Width="{Binding Path=(SystemParameters.SmallIconWidth)}"
                   Height="{Binding Path=(SystemParameters.SmallIconHeight)}" />
            <AdornerDecorator>
                <ContentPresenter Name="PART_RootContentPresenter" />
            </AdornerDecorator>
            <ResizeGrip Name="WindowResizeGrip"
                        WindowChrome.ResizeGripDirection="BottomRight"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Bottom"
                        Visibility="Collapsed"
                        IsTabStop="False" />
        </Grid>
    </Border>
</Grid>

Ribbon的Chrome部分完全依賴於WindowChrome,PART_ClientAreaBorder負責為ClientArea提供背景顏色。在PART_ClientAreaBorder後面的另一個Border才是真正的ClientArea部分,它用於放置Ribbon。因為Ribbon的一些按鈕位於標題欄,所以Ribbon必須佔用標題欄的位置,並且由Ribbon顯示原本應該由Window顯示的標題。WindowChrome的標題欄高度是SystemParameters.WindowNonClientFrameThickness.Top,在Windows 10,100% DPI的情況下為27畫素。而Ribbon標題欄部分使用了SystemParameters.WindowCaptionHeight作為高度,這個屬性的值為23,所以才會出現對不齊的問題。

<DockPanel Grid.Column="0" Grid.ColumnSpan="3" LastChildFill="True" Height="{Binding Path=(SystemParameters.WindowCaptionHeight)}">

而最大化的時候完全沒有調整Ribbon的Margin,並且WindowChrome本身在最大化就會有問題。所以不能直接使用WindowChrome,而應該使用自定義的UI覆蓋WindowChrome的內容。

3. 自定義RibbonWindow

我在Kino.Toolkit.Wpf提供了一個自定義RibbonWindow,基本上程式碼和ControlTempalte與自定義Window一樣,執行效果如上圖所示。在自定義RibbonWindow裡我添加了RibbonStyle屬性,預設值是一個解決Ribbon標題欄問題的Ribbon樣式,裡面使用SystemParameters.WindowNonClientFrameThickness作為標題的高度。

<DockPanel Grid.Column="0"
           Grid.ColumnSpan="3"
           Margin="0,-1,0,0"
           Height="{Binding Path=(SystemParameters.WindowNonClientFrameThickness).Top}"
           LastChildFill="True">

RibbonWindow還添加了一個StyleTypedProperty:

[StyleTypedProperty(Property = nameof(RibbonStyle), StyleTargetType = typeof(Ribbon))]

StyleTypedProperty 應用於類定義並確定型別為 TargetType 的屬性的 Style。使用了這個屬性的控制元件可以在Blend中使用 "右鍵"->"編輯其他模板"->"編輯RibbonSytle" 建立Ribbon的Style。

不過雖然我這麼貼心地加上這個Attribute,但我的Blend複製Ribbon模板總是報錯。

4. 結語

我也見過一些很專業的軟體沒處理RibbonWindow,反正外觀上的問題忍一忍就過去了,實在受不了可以買一個有現代化風格的控制元件庫,只是為了標題欄對不齊這種小事比較難說服上面同意引入一個新的元件。除了使用我提供的解決方案,stackoverflow也由不少關於這個問題的討論及解決方案可供參考,例如這個:

c# - WPF RibbonWindow + Ribbon = Title outside screen - Stack Overflow

順便一提,ExtendedRibbonWindow需要繼承RibbonWindow,所以沒法直接整合ExtendedWindow。因為ExtendedWindow很多功能都試用附加屬性和控制元件程式碼分離,所以ExtendedRibbonWindow需要重複的程式碼不會太多。

5. 參考

RibbonWindow Class (System.Windows.Controls.Ribbon) Microsoft Docs

Ribbon Class (System.Windows.Controls.Ribbon) Microsoft Docs

WindowChrome Class (System.Windows.Shell) Microsoft Docs

SystemParameters Class (System.Windows) Microsoft Docs

StyleTypedPropertyAttribute Class (System.Windows) Microsoft Docs

6. 原始碼

Kino.Toolkit.Wpf_Window at master