【.NET深呼吸】將XAML放到WPF程序之外

上一篇水文中,老周說了一下純代碼編寫 WPF 的大概過程。不過,還是不夠的,本篇水文中咱們還要更進一步。

XAML 文件默認是作爲資源打包進程序中的,而純代碼編寫又導致一些常改動的東西變成硬編碼了。爲了取得二者平衡,咱們還要把一些經常修改的東西放到 XAML 文件中,不過 XAML 文件不編譯進程序裏,而是放到外部,運行階段加載。比如一些對象屬性、畫刷、樣式、字體之類的,直接改文件保存就行,修改之後不用重新編譯項目。

要在運行階段加載 XAML,咱們只需認識一個類就OK—— XamlReader,調用它的 Load 方法就能從 XAML 文件加載對象了。

下面老周就邊演示邊嘮叨一下相關的問題。

一、新建項目。可以參照上一篇中的做法,用控制檯應用程序項目,然後修改項目文件。也可以直接建 WPF 項目。都可以。

 

 

二、自定義窗口類,從 Window 派生。當然,你直接用 Window 類也可以的。

public class MyWindow : Window
{
    const string XAML_FILE = "MyWindow.xaml";

    public MyWindow()
    {
        Title = "加載外部XAML";
        Height = 150;
        Width = 225;
        // 從XAML文件加載
        using FileStream fsIn = new(XAML_FILE, FileMode.Open, FileAccess.Read);
        FrameworkElement layout = (FrameworkElement)XamlReader.Load(fsIn);
        // 兩個按鈕要處理事件
        Button btn1 = (Button)layout.FindName("btn1");
        Button btn2 = (Button)layout.FindName("btn2");
        btn1.Click += OnClick1;
        btn2.Click += OnClick2;
        Content = layout;
    }

    private void OnClick2(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("第二個按鈕");
    }

    private void OnClick1(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("第一個按鈕");
    }
}

這個不復雜,咱們關注加載 XAML 部分。通過文件流 FileStream 讀取文件,而後在 XamlReader.Load 中加載。Load 方法返回的是 object 類型的對象,咱們要適當地進行類型轉換。這個例子裏面其實加載上來的是 Grid 類,但這裏我只轉換爲 FrameworkElement 就可以了,畢竟我後面只用到了 FindName 方法。Find 出來的是兩個 Button 對象,最後處理一下 Click 事件。

 

三、在項目中添加 MyWindow.xaml 文件。

<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      Margin="12">
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
    <Button Name="btn1" Grid.Row="0" Margin="5,8">按鈕A</Button>
    <Button Name="btn2" Grid.Row="1" Margin="5,8">按鈕B</Button>
</Grid>

這裏順便說一下,保存 XAML 文件時最好用 UTF-8 編碼,不然可能會報錯。方法是在 VS 裏,【文件】-【XXX 另存爲】。在保存文件對話框中,點“保存”按鈕右邊的箭頭,選擇“編碼保存”。

編碼選 UTF-8 無簽名(或帶簽名的也行)。

另一種方法是用記事本打開,再以 UTF-8 保存。

 

四、在項目中添加 styles.xaml。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <Style TargetType="Button">
        <Setter Property="Background" Value="Red"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Padding" Value="5"/>
        <Setter Property="FontFamily" Value="楷體"/>
        <Setter Property="FontSize" Value="17"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="BorderBrush" Value="Yellow"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="5">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          Margin="{TemplateBinding Padding}"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="Green"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background" Value="DarkSlateGray"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

裏面包含一個 Button 控件模板。

 

五、在 Main 方法中,初始化 Application 類,並且從外部 XAML 中加載資源字典。

[STAThread]
static void Main(string[] args)
{
    Application app = new();
    using FileStream extFile = new FileStream("styles.xaml", FileMode.Open, FileAccess.Read);
    ResourceDictionary dic = (ResourceDictionary)XamlReader.Load(extFile);
    app.Resources = dic;
    app.Run(new MyWindow());
}

由於是在 app 處加載的資源,所以按鈕樣式會應用到整個程序。

 

六、打開項目文件(*.csproj),我們要做點手腳。

<ItemGroup>
    <Page Remove="*.xaml"/>
    <None Include="*.xaml"/>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <Exec Command="copy /y *.xaml $(OutDir)" />
</Target>

老周解釋一下加了顏色的部分。

1、Page 表示XAML文件最終生成二進制文件,且塞進目標程序集中。加了 Remove 表示排除這一行爲。說人話就是:本項目不編譯 XAML 文件。

2、None 表示該項目中 XAML 文件“啥也不是”,編譯時不做任何處理。

3、PostBuild 任務指定一條命令,在生成項目之後執行。此處是把項目目錄下的 XAML 文件複製到輸出目錄。$(OutDir) 在 VS 中表示宏(也是 MSBuild 的屬性)。在命令實際執行時,替換爲實際目錄路徑,如 bin\Debug\net7.0-windows。

也可以用 $(TargetDir),不過 TargetDir 替換的是完整路徑,OutDir 是用相對路徑的。

 

現在生成一下項目,若沒有問題,在輸出目錄下除了程序文件,還有那兩個 XAML 文件。運行一下。

關閉程序,用記事本打開 styles.xaml 文件,把按鈕的背景色改成橙色。

保存並關閉文件,重新運行程序。

咱們並沒有重新編譯程序。接下來用記事本打開 MyWindow.xaml 文件,改一下按鈕上的文本。

保存並關閉文件,不用編譯代碼,再次運行程序。

 

這樣就很方便修改了,不必每次都重新編譯。 

下一篇老周還會說說純代碼寫 WPF 的模板問題。三維圖形就看心情了。因爲 3D 圖形的構造和一般控件應用差不多,就是用代碼建立 WPF 對象樹。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章