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