項目中有不少的彈出窗口,按照美工的設計其外邊框(包括最大化,最小化,關閉等按鈕)自然不同於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。