項目中有不少的彈出窗口,按照美工的設計其外邊框(包括最大化,最小化,關閉等按鈕)自然不同於Window自身的,但每個彈出框的外邊框都是一樣的。對其中一個窗口而言,我們要取消其Window邊框,並在右上角擺上三個按鈕並編寫其點擊事件等,但若每個彈出窗口都按照這種方式做一遍就太土了。我們想避免重復勞動,最自然的聯想到了“繼承”。但WPF給我們找了若干麻煩,被挫敗了幾次。今天經過2小時的奮戰,終於搞定了,分享一下。
挫敗1,繼承時編譯錯誤
假設我們寫好的父窗口類為BaseWindow,對應BaseWindow.cs和BaseWindow.xaml, 要繼承它的窗口為Window1,對應Window1.cs和Window1.xaml,我們常常進行的動作是將VS為我們自動生成的代碼中的如下語句:
public partial class Window1 : Window
修改成:
public partial class Window1 : BaseWindow
但編譯後,你會得到一個錯誤:Window1有著不同的基類。
這是因為在window1.xaml中
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="InheritWindowDemo.Window1" Width="300" Height="300"> <Grid x:Name="LayoutRoot"/> </Window>
我們的Window繼承了Window類,打開Window1.g.cs也可以看到這一點(這是VS自動生成的一個中間文件,可以在Window1的InitializeComponent()方法上“轉到定義”來跳轉到該文件,也可以在Obj"Debug目錄下找到)。這就使得我們的Window1同時繼承Window和BaseWindow類,多繼承是不被允許的。
那麼自然地,需要修改Window1.xaml,將其中的根“Window”,修改成我們的BaseWindow:
<src:BaseWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="InheritWindowDemo.Window1" xmlns:src="clr-namespace:InheritWindowDemo" Height="300" Width="300"> <Grid> </Grid> </src:BaseWindow>
心想,這下可以編譯通過了吧,抱歉,不行,又得到另一個編譯錯誤:src:BaseWindow不能是Xaml文件的根,因為它是由Xaml定義的,目前我避免這個問題的辦法是讓BaseWindow僅僅在C#中定義(即,沒有BaseWindow.xaml,只有BaseWindow.cs)。
OK,編譯順利通過,繼承成功。
挫敗2,外邊框(包括最小化,最大化和關閉按鈕)放在哪裡
明顯,不能作為BaseWindow的內容,這是因為繼承了BaseWindow的子類窗口(比如Window1)會覆蓋BaseWindow的內容。
假設BaseWindow這樣編寫:
public BaseWindow() { Grid grid = new Grid(); Button minBtn = new Button(); Button maxBtn = new Button(); Button closeBtn =new Button(); //something to ini these buttons grid.Children.Add(minBtn); grid.Children.Add(maxBtn); grid.Children.Add(closeBtn); this.Content = grid; }
當子類Window1如下定義時:
<src:BaseWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="InheritWindowDemo.Window1" xmlns:src="clr-namespace:InheritWindowDemo" Height="300" Width="300"> <Grid> <TextBlock Text="hi , i am window1"/> </Grid> </src:BaseWindow>
這樣以來Window1中的Grid和TextBlock會覆蓋BaseWindow的內容而僅僅看到“hi,I am window1”的文本塊而沒有最小化最大化以及關閉按鈕了。
事實上,我們應該反過來想,Window也是一個控件,與其他控件一樣其外觀及其外觀中的視覺元素仍然是由其Style和ControlTemplate來定義的。想到這裡,一切就變得簡單了,我們應該將窗口外邊框(包括最小化,最大化和關閉按鈕)定義在其Template中,其他一些屬性(比如是否支持透明等)定義在Style中
其Template如下:
<ControlTemplate x:Key="BaseWindowControlTemplate" TargetType="{x:Type Window}"> <DockPanel LastChildFill="True"> <!--外邊框--> <Border Width="Auto" Height="Auto" DockPanel.Dock="Top" Background="#FF7097D0" CornerRadius="4,4,0,0" x:Name="borderTitle"> <StackPanel HorizontalAlignment="Right" Orientation="Horizontal"> <!--最小化按鈕--> <Button Content="Min" x:Name="btnMin" /> <!--最大化按鈕--> <Button Content="Max" x:Name="btnMax" /> <!--關閉按鈕--> <Button Content="Close" x:Name="btnClose" /> </StackPanel> </Border> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Width="Auto" Height="Auto" DockPanel.Dock="Top" CornerRadius="0,0,4,4"> <AdornerDecorator> <ContentPresenter /> </AdornerDecorator> </Border> </DockPanel> </ControlTemplate>
其Style如下:
<Style x:Key="BaseWindowStyle" TargetType="{x:Type Window}"> <Setter Property="Template" Value="{StaticResource BaseWindowControlTemplate}"/> <Setter Property="AllowsTransparency" Value="True" /> <Setter Property="WindowStyle" Value="None" /> <Setter Property="BorderBrush" Value="#FF7097D0" /> <Setter Property="BorderThickness" Value="4,0,4,4" /> <!—Something else--> </Style>
然後在BaseWindow的構造函數中指定其Style為我們定義的樣式:
private void InitializeStyle() { this.Style = (Style) App.Current.Resources["BaseWindowStyle"]; }
這樣一來,所有繼承了BaseWindow的窗體,都有我們統一定義的外觀了。
挫敗3,讓外邊框(包括最小化,最大化和關閉按鈕)響應事件
只有外觀還不夠,至少得有鼠標事件吧。那最小化事件來說,要做的事情是找到定義在ControlTemplate中的btnMin這個Button控件,然後當其被點擊時該ControlTemplate被應用到的那個窗體被最小化。
FrameworkTemplate.FindName(string name, FrameworkElement templatedParent)方法可以做幫助我們找到指定的FrameworkTemplate被應用到templatedParent上後具有name名稱的控件。
ControlTemplate baseWindowTemplate = (ControlTemplate)App.Current.Resources["BaseWindowControlTemplate"]; Button minBtn = (Button)baseWindowTemplate.FindName("btnMin", this); minBtn.Click += delegate { this.WindowState = WindowState.Minimized; };
其他事件同理:)不過值得提醒的是,上面這樣的代碼應該在窗體的Style和Template被應用之後,比如你可以在Loaded後編寫使用上面的代碼而不是直接放在構造方法中,否則FrameworkTemplate.FindName()方法將返回null。