由於內容陳舊,已經寫了新的文章代替這篇,請參考新的文章:
-
Window(窗體)的UI元素及行為:這篇文章主要討論標准Window的 UI 元素和行為。
-
使用WindowChrome自定義Window Style:介紹使用 WindowChrome 自定義 Window 的原理及各種細節。
-
使用WindowChrome的問題:介紹如何處理使用WindowChrome自定義 Window 會遇到的各種問題。
-
使用WindowChrome自定義RibbonWindow:提供了一個自定義的 RibbonWindow 以解決原生 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.SmallIconHeight
和SystemParameters.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. 源碼#
作者:Dino.C
出處:https://www.cnblogs.com/dino623/p/CustomWindowStyle.html
版權:本文采用「CC BY 4.0」知識共享許可協議進行許可。