[WPF]使用WindowChrome自定義Window Style


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

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

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。

Copy
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:

Copy

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

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

3.3 Window基本布局#

Copy
<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 布局標題欄#

Copy
<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",標題欄上的內容要設置這個附加屬性才能響應鼠標操作。

Copy
<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#

Copy
<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即可:

Copy
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中添加以下代碼,簡單粗暴,但可能引發其它問題:

Copy
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

作者:Dino.C

出處:https://www.cnblogs.com/dino623/p/CustomWindowStyle.html

版權:本文采用「CC BY 4.0」知識共享許可協議進行許可。

 

 
分類:  WPF
標簽:  WPF
18
0
 
 
 
 


免責聲明!

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



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