実践!Windowsアプリを作る
第3章 機能更新

第1章 WPFアプリケーション」ではWPFのアプリケーションとして指定フォルダ内のファイルを指定のファイル名でコピーするだけのシンプルなアプリケーションを作成しました。

第2章 WPFデザイン」ではMahApps.MetroMaterialDesignThemesを使用して、アプリケーションのデザインをブラッシュアップしました。

今回は、もう少し機能に手を加えて行きたいと思います。

AboutBoxの追加

通常のWindowsのアプリケーションでは、"AboutBox"と呼ばれる、アプリケーションの情報等を表示するためのダイアログボックスを表示する機能が備わっています。
なので、先ずは"AboutBox"を表示する機能を追加します。

表示する情報は以下の通りです。

  • アプリケーションの説明
  • 著作権表示
  • ライセンス
  • バージョン
  • 使用しているパッケージとそのライセンス

AboutBox作成

先ずは表示するダイアロボックスを作成します。

とは云え、ダイアログボックスと通常のウィンドウは同じ手順で作成できます。
基本的には、Windowクラスを継承したクラスを新規に作成し、インスタンスを作成後、"ShowDialog"メソッドでダイアログボックスを表示します。

クラス作成は以下のようにします。

  1. 「ソリューションエクスプローラー」で"CopyFilesWithSpecifiedName"を右クリック
  2. “追加"→"ウィンドウ(WPF)…"を選択
  3. 「新しい項目の追加」で"ウィンドウ(WPF)"が選択されているのを確認
  4. “名前:"に"AboutBox"と記入し、"追加"ボタンをクリック

Window作成の選択AboutBoxの作成

AboutBoxのデザイン

“AboutBox"のデザインは"MainWindow"と同様に"AboutBox.xaml"ファイルで行います。

なお、ダイアログボックスですのでタイトルバーは表示しません。
ですので、MahApps.Metroを使用せず、MaterialDesignThemesのみを使用します。

最終的な"AboutBox"は以下のようにします。

AboutBoxのデザイン

“AboutBox.xaml"は以下のようになります。

<Window x:Class="CopyFilesWithSpecifiedName.AboutBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        xmlns:System="clr-namespace:System;assembly=System.Runtime"
        mc:Ignorable="d"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        TextElement.FontWeight="Medium"
        TextElement.FontSize="14"
        FontFamily="{materialDesign:MaterialDesignFont}"
        WindowStyle="None" ResizeMode="NoResize" Background="Transparent" AllowsTransparency="True"
        MouseLeftButtonDown="Window_MouseLeftButtonDown" Loaded="Window_Loaded"
        Title="AboutBox" Height="480" Width="640">
    <Border BorderBrush="Black" BorderThickness="1" CornerRadius="10,10,10,10" Background="White">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid Margin="10,10,10,7">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <materialDesign:PackIcon Kind="ContentCopy" HorizontalAlignment="Center" VerticalAlignment="Center" Width="32" Height="32" Foreground="BlueViolet"/>
                <TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" Text="CopyFilesWithSpecifiedName" Grid.Column="1" Margin="10,0,0,0" Foreground="BlueViolet"/>
            </Grid>
            <Grid Grid.Row="1" Margin="10,5,10,5">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <Label Content="詳細: "/>
                <TextBlock TextWrapping="Wrap" Grid.Column="1"><Run Text="指定したフォルダ内のファイルをコピーします。"/><LineBreak/><Run Text="コピーする際のファイル名は&quot;指定のファイル名+連番.拡張子&quot;となります。"/><LineBreak/><Run Text="コピー先のフォルダは選択できます。"/><LineBreak/><Run Text="コピー先のフォルダを指定しない場合、コピー元のフォルダにコピーします。"/></TextBlock>
                <TextBlock Text="バージョン : 1.0.0" Grid.Row="1" MinWidth="3" Grid.ColumnSpan="2"/>
                <TextBlock Text="ライセンス : MIT" Grid.Row="2" Grid.ColumnSpan="2"/>
                <TextBlock Text="Copyright (c) 2023 Yoshimasa Awata" Grid.Row="3" Grid.ColumnSpan="2"/>
                <Label Content="使用しているパッケージ :" Margin="0,10,0,1" Grid.Row="4" Grid.ColumnSpan="2" />
                <Button x:Name="LicenseButton" Content="ライセンス表示" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0,10,5"
                    Style="{StaticResource MaterialDesignRaisedLightButton}" FontSize="9" Click="LicenseButton_Click"/>
                <materialDesign:Card Grid.Row="5" Grid.ColumnSpan="2" Cursor="">
                    <ListView x:Name="PackageList" SelectionMode="Single" ScrollViewer.HorizontalScrollBarVisibility="Hidden" SelectionChanged="PackageList_SelectionChanged">
                        <ListView.View>
                            <GridView>
                                <GridView.ColumnHeaderContainerStyle>
                                    <Style TargetType="{x:Type GridViewColumnHeader}">
                                        <Setter Property="Background" Value="LightBlue"/>
                                    </Style>
                                </GridView.ColumnHeaderContainerStyle>
                                <GridViewColumn x:Name="PackageColumn" Header="パッケージ" DisplayMemberBinding="{Binding Name}"/>
                                <GridViewColumn x:Name="LicenseColumn" Header="ライセンス" DisplayMemberBinding="{Binding License}" Width="100"/>
                            </GridView>
                        </ListView.View>
                    </ListView>
                </materialDesign:Card>
                <TextBlock x:Name="AlartTextBox" Grid.Row="6" Grid.ColumnSpan="2" Foreground="Red"/>
            </Grid>
            <Button x:Name="OKButton" Content="OK" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,5,10,10"
            Style="{StaticResource MaterialDesignRaisedLightButton}" Click="OKButton_Click"/>
        </Grid>
    </Border>
</Window>

AboutBoxのMaterialDesignThemes使用準備

先ず、<Window>タグにMaterialDesignThemesを使用するための属性の追加を行います。

<Window x:Class="CopyFilesWithSpecifiedName.AboutBox"
            :
            :
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        TextElement.FontWeight="Medium"
        TextElement.FontSize="14"
        FontFamily="{materialDesign:MaterialDesignFont}"
        Title="AboutBox" Height="480" Width="640">
    <Grid>
        :
        :
    </Grid>
</Window>

AboutBoxのタイトルバー削除

“AboutBox"はダイアログボックスなのでタイトルバーは不要です。
なので、<Windows>タグの"WindowStyle"属性として"None"を追加してタイトルバーを削除します。

更にダイアログボックスのサイズは固定としたいので、<Windows>タグの"ResizeMode"属性に"NoResize"を指定してリサイズができないようにします。

また、タイトルバーを削除したため、タイトルバーをマウスでドラッグしてダイアログボックスの移動ができなくなりました。
移動ができないのは不便なため、ダイアログボックス全体をマウスでドラッグして移動ができるようにするために少々工夫をします。

  1. <Window>タグの属性として"MouseLeftButtonDown"イベントのメソッド"Window_MouseLeftButtonDown"を追加
  2. “AboutBox.xaml.cs"ファイルのAboutBoxクラスに"Window_MouseLeftButtonDown"メソッドを追加
<Window x:Class="CopyFilesWithSpecifiedName.AboutBox"
            :
            :
        WindowStyle="None" ResizeMode="NoResize"
        MouseLeftButtonDown="Window_MouseLeftButtonDown"
        Title="AboutBox" Height="480" Width="640">
    <Grid>
            :
            :
    </Grid>
</Window>
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (e.ButtonState == MouseButtonState.Pressed)
    {
        this.DragMove();
    }
}

これで、マウスの左ボタンをクリック、ドラッグしてダイアログボックスを移動できます。

“MouseLeftButtonDown"イベントに対応して"DragMove()"メソッドをコールするだけでも良いようですが、タイミングによってはマウスの左ボタンをリリース後も移動する等の不具合が出る可能性があるようなので、ボタンが押され続けている事を確認しています。

AboutBoxの枠線とコーナー追加

ダイアログボックスに枠線を付加するだけであれば、<Window>タグの属性として “Borderbrush"と “BorderThickness"を追加すればOKです。

ただ、今回はコーナーに丸みをつけたいので、<Border>コントロールを使用します。

先ず、コーナーを付けるとダイアログボックスのバックグラウンド色がそのまま残ってしまい、コーナーをつけた意味が無くなってしまいますので、<Window>タグの属性 “Background"を “Transparent"に設定します。
更に"AllowsTransparency"属性を"True"にします。

次に、<Window>タグの直接の子要素として<Border>タグを追加します。
なお従来の<Grid>要素は<Border>の子要素とします。

Border挿入

追加した<Border>タグの属性は以下のようにします。

<Window x:Class="CopyFilesWithSpecifiedName.AboutBox"
            :
            :
        Title="AboutBox" Height="480" Width="640">
    <Border BorderBrush="Black" BorderThickness="1" CornerRadius="10,10,10,10" Background="White">
        <Grid>
            :
            :
        </Grid>
    </Border>
</Window>

AboutBoxのアイコンとアプリケーション名

一番上の行にはアイコンとアプリケーション名"CopyFilesWithSpecifiedName"を表示しています。

アイコンはMaterialDesignThemesの"PackIcon"が提供する"ContentCopy"を使用し、色とサイズを変更しています。

<materialDesign:PackIcon Kind="ContentCopy" HorizontalAlignment="Center" VerticalAlignment="Center" Width="32" Height="32" Foreground="BlueViolet"/>

アプリケーション名については<TextBlock>タグを使用し、テキストスタイルをMaterialDesignThemesから選んでいます。

<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" Text="CopyFilesWithSpecifiedName" Grid.Column="1" Margin="10,0,0,0" Foreground="BlueViolet"/>

AboutBoxの複数行文字列

複数行の文字列を記述するためには<StackLabel>内に<TextBox><Label>を複数重ねて使用しても可能です。
それ以外にも<TextBlock>を使用する事も可能です。

<TextBlock>はユーザーの書き込みはできませんが、文字単位でスタイルを制御する事が可能ですので、非常にリッチな見栄えの文字列を提供できます。
加えて、イベントハンドラを作成する必要はあるものの、文字列内にハイパーリンクを埋め込む事ができます。

今回の"AboutBox"ではハイパーリンクは使用していませんが、<Run.../>タグで分割された文字列と<LineBreak/>による改行を使用しています。

TextBlockでハイパーリンクを使用する

今回のアプリケーションでは使用しませんが、<TextBlock>でハイパーリンクを使用する場合には子要素として以下のようなタグを挿入します。

<Hyperlink NavigateUri="https://www.google.com/" RequestNavigate="Hyperlink_RequestNavigate">Google</Hyperlink>

“NavigateUri"属性でリンク先のURIを指定し"RequestNavigate"属性でイベントハンドラとなるメソッドを指定します。
なお、イベントハンドラでは以下のようにして指定のURIをオープンします。

private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
    try
    {
        var startInfo = new System.Diagnostics.ProcessStartInfo(e.Uri.ToString());
        startInfo.UseShellExecute = true;
        System.Diagnostics.Process.Start(startInfo);
    }
    catch (System.ComponentModel.Win32Exception noBrowser)
    {
        if (noBrowser.ErrorCode == -2147467259)
        {
            MessageBox.Show(noBrowser.Message);
        }
    }
    catch (System.Exception other)
    {
        MessageBox.Show(other.Message);
    }
}

e.Uri.ToString()で指定のURIの文字列を取得できます。

AboutBoxのパッケージおよびライセンスリスト

“AboutBox"にアプリケーションで使用しているNuGetのパッケージ名とライセンスのリストを表示します。
そのため、<ListView>を使用します。

<ListBox>でも問題ないのですが、ヘッダーとして"パッケージ"と"ライセンス"と表示し、列をしっかりと分けたかったので、<ListView>にしました。

<ListView>コンポーネントの使い方は<ListBox>コンポーネントと概ね同様です。
ReadOnlyObservableCollection<T>クラスのインスタンスをListViewクラスの"ItemsSource"プロパティに割り当てる事でReadOnlyObservableCollection<T>の初期化時にセットしたObservableCollection<T>クラスのインスタンスの要素を<ListView>に反映させることができます。

なおReadOnlyObservableCollection<T>クラスはObservableCollection<T>クラスの要素を読み取り専用にするためのクラスです。

public partial class AboutBox : Window
{
    protected class Package
    {
        public string Name { get; set; }
        public string License { get; set; }
        public string Url { get; set; }
    }

    private static readonly ReadOnlyObservableCollection<Package> Packages = new ReadOnlyObservableCollection<Package>(
        new ObservableCollection<Package>() {
        new Package {Name = "WindowsAPICodePack-Core", License = "Custom", Url = "https://github.com/aybe/Windows-API-Code-Pack-1.1/blob/master/LICENCE"},
        new Package {Name = "WindowsAPICodePack-Shell", License = "Custom", Url = "https://github.com/aybe/Windows-API-Code-Pack-1.1/blob/master/LICENCE"},
        new Package {Name = "MahApps.Metro", License = "MIT", Url = "https://github.com/MahApps/MahApps.Metro/blob/develop/LICENSE"},
        new Package {Name = "MahApps.Metro.IconPacks", License = "MIT", Url = "https://github.com/MahApps/MahApps.Metro.IconPacks/blob/develop/LICENSE"},
        new Package {Name = "ControlzEx", License = "MIT", Url = "https://github.com/ControlzEx/ControlzEx/blob/develop/LICENSE"},
        new Package {Name = "Microsoft.Xaml.Behaviors.Wpf", License = "MIT", Url = "https://github.com/microsoft/XamlBehaviorsWpf/blob/master/LICENSE"},
        new Package {Name = "System.Text.Json", License = "MIT", Url = "https://www.nuget.org/packages/System.Text.Json/4.7.2/license"},
        new Package {Name = "MaterialDesignThemes", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
        new Package {Name = "MaterialDesignColors", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
        new Package {Name = "MaterialDesignThemes.MahApps", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
    });

    public AboutBox()
    {
        InitializeComponent();

        PackageList.ItemsSource = Packages;
        PackageList.SelectedIndex = 0;
    }
        :
        :
}

ListViewListBoxと大きく異なる点は、複数列を持つ事ができ、其々の列にヘッダーをつけられる事です。

以下は今回使用したListViewのXAMLです。
“PackageList"と云う名前を付けています。

<ListView x:Name="PackageList" Grid.Row="5" Grid.ColumnSpan="2" ScrollViewer.HorizontalScrollBarVisibility="Hidden">
    <ListView.View>
        <GridView>
            <GridViewColumn x:Name="PackageColumn" Header="パッケージ" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn x:Name="LicenseColumn" Header="ライセンス" DisplayMemberBinding="{Binding License}"/>
        </GridView>
    </ListView.View>
</ListView>

<GridViewColumn>タグの"Header"属性にヘッダーに表示する文字列を、"DisplayMemberBinding"属性にバインドするプロパティ名を指定します。

PackageListの選択

後に"PackageList"で選択したパッケージに関するライセンス情報をブラウザで表示するようにします。
なので、選択できるパッケージを1つに限定したいと思います。

そのため、<ListView...>タグの属性"SelectionMode"を"Single"に設定しています。

また、選択時に小細工をしたいので、"SelectionChanged"イベントに対するハンドラ"PackageList_SelectionChanged"メソッドを登録しておきます。

<ListView x:Name="PackageList" Grid.Row="5" Grid.ColumnSpan="2" SelectionMode="Single" ScrollViewer.HorizontalScrollBarVisibility="Hidden" SelectionChanged="PackageList_SelectionChanged">
    <ListView.View>
        <GridView>
            <GridViewColumn x:Name="PackageColumn" Header="パッケージ" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn x:Name="LicenseColumn" Header="ライセンス" DisplayMemberBinding="{Binding License}"/>
        </GridView>
    </ListView.View>
</ListView>

なお"PackageList_SelectionChanged"メソッドについては後で実装します。

PackageListの列の指定

“PackageList"の各列に表示するデータは、<GridViewColumn.../>タグで指定します。

<GridViewColumn.../>要素は<ListView>の子要素である<ListView.View>の更に子要素である<GridView>以下に配置します。

<GridViewColumn.../>タグでは、"Header"属性にヘッダーの名前を設定します。

また、"DisplayMemberBinding"属性に表示するデータを指定します。
今回の場合には、ListViewにセットしたReadOnlyObservableCollection<T>の型パラメーター"T"として以下のクラスを割り当てています。

protected class Package
{
    public string Name { get; set; }
    public string License { get; set; }
    public string Url { get; set; }
}
  • Name: パッケージ名
  • License: ライセンス [Custom, MIT…]
  • Url: ライセンス情報が記載されたURL

従って、<GridViewColumn.../>タグの"DisplayMemberBinding"属性にはPackageクラスのプロパティ"Name"と"License"をバインディングしています。

因みに"PackageList"の"ItemsSource"プロパティに割り当てるReadOnlyObservableCollection<Package>クラスのインスタンスは、"AboutBox.xaml.cs"中、AboutBoxクラスの定数として以下のように指定します。

private static readonly ReadOnlyObservableCollection<Package> Packages = new ReadOnlyObservableCollection<Package>(
    new ObservableCollection<Package>() {
    new Package {Name = "WindowsAPICodePack-Core", License = "Custom", Url = "https://github.com/aybe/Windows-API-Code-Pack-1.1/blob/master/LICENCE"},
    new Package {Name = "WindowsAPICodePack-Shell", License = "Custom", Url = "https://github.com/aybe/Windows-API-Code-Pack-1.1/blob/master/LICENCE"},
    new Package {Name = "MahApps.Metro", License = "MIT", Url = "https://github.com/MahApps/MahApps.Metro/blob/develop/LICENSE"},
    new Package {Name = "MahApps.Metro.IconPacks", License = "MIT", Url = "https://github.com/MahApps/MahApps.Metro.IconPacks/blob/develop/LICENSE"},
    new Package {Name = "ControlzEx", License = "MIT", Url = "https://github.com/ControlzEx/ControlzEx/blob/develop/LICENSE"},
    new Package {Name = "Microsoft.Xaml.Behaviors.Wpf", License = "MIT", Url = "https://github.com/microsoft/XamlBehaviorsWpf/blob/master/LICENSE"},
    new Package {Name = "System.Text.Json", License = "MIT", Url = "https://www.nuget.org/packages/System.Text.Json/4.7.2/license"},
    new Package {Name = "MaterialDesignThemes", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
    new Package {Name = "MaterialDesignColors", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
    new Package {Name = "MaterialDesignThemes.MahApps", License = "MIT", Url = "https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE"},
});

使用するパッケージは固定ですので"static readonly"とし各要素は初期化時に登録しておきます。

PackageListのヘッダーデザイン

<ListView>のヘッダーのデザインは子要素である<ListView.View>の更に子要素である<GridView>以下に配置した<GridView.ColumnHeaderContainerStyle>内で行います。

<ListView x:Name="PackageList" Grid.Row="5" Grid.ColumnSpan="2" SelectionMode="Single" ScrollViewer.HorizontalScrollBarVisibility="Hidden" SelectionChanged="PackageList_SelectionChanged">
    <ListView.View>
        <GridView>
            <GridView.ColumnHeaderContainerStyle>
                <Style TargetType="{x:Type GridViewColumnHeader}">
                    <Setter Property="Background" Value="LightBlue"/>
                </Style>
            </GridView.ColumnHeaderContainerStyle>
            <GridViewColumn x:Name="PackageColumn" Header="パッケージ" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn x:Name="LicenseColumn" Header="ライセンス" DisplayMemberBinding="{Binding License}" Width="100"/>
        </GridView>
    </ListView.View>
</ListView>

なお<Style>タグの属性"TargetType"には"GridViewColumnHeader"を指定します。

因みに<GridView.ColumnHeaderContainerStyle>タグが省かれた場合には、何故かヘッダーの高さが異様に大きく取られてしまいます。
それを避けるためには、ヘッダーのデザインを指定しない場合でも以下のように<GridView.ColumnHeaderContainerStyle>タグを追加しておきます。

<GridView.ColumnHeaderContainerStyle>
    <Style/>
</GridView.ColumnHeaderContainerStyle>

また、デフォルトではリストの列幅はリスト全体をカバーするようには広がってくれません。
リスト全体ひ広がって表示させるには、今回に限っては列幅を指定するのが手っ取り早いかと思います。

とりあえずライセンス表示をする列の幅を"100″に固定し、パッケージ名を表示する列を広げる事で対応します。

パッケージ名を表示する列の幅の指定は"AboutBox"が描画される前に指定したいので、"Window_Loaded"イベントハンドラ内で行います。

<Window x:Class="CopyFilesWithSpecifiedName.AboutBox"
            :
            :
         MouseLeftButtonDown="Window_MouseLeftButtonDown" Loaded="Window_Loaded"
        Title="AboutBox" Height="480" Width="640">
    <Border BorderBrush="Black" BorderThickness="1" CornerRadius="10,10,10,10" Background="White">
        <Grid>
            :
            :
        </Grid>
    </Border>
</Window>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    PackageColumn.Width = PackageList.ActualWidth - LicenseColumn.ActualWidth - SystemParameters.VerticalScrollBarWidth;
}
PackageListのライセンス表示

“PackageList"中、選択されたパッケージに対応したライセンスの情報をブラウザで表示するようにします。

そのためにライセンス表示用のボタンを設置します。

<Button x:Name="LicenseButton" Content="ライセンス表示" Grid.Row="4"  Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="10,0,10,5"
                    Style="{StaticResource MaterialDesignRaisedLightButton}" FontSize="9" Click="LicenseButton_Click"/>

なお"Click"イベントに対するハンドラ"LicenseButton_Click"は以下の通りです。

private void LicenseButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        var startInfo = new System.Diagnostics.ProcessStartInfo(Packages[PackageList.SelectedIndex].Url);
        startInfo.UseShellExecute = true;
        System.Diagnostics.Process.Start(startInfo);
        AlartTextBox.Text = "";
    }
    catch (System.ComponentModel.Win32Exception noBrowser)
    {
        if (noBrowser.ErrorCode == -2147467259)
        {
            AlartTextBox.Text = noBrowser.Message;
        }
    }
    catch (System.Exception other)
    {
        AlartTextBox.Text = other.Message;
    }
}

基本的にはブラウザでオープンするURIを指定して"System.Diagnostics.Process.Start"メソッドをコールすれば良かったはずなのですが、いつの間にか仕様が変わり、そのままでは例外が発生するようになりました。

新しい方法では以下のようにします。

  1. ブラウザでオープンするURIを指定してSystem.Diagnostics.ProcessStartInfoクラスのインスタンスを作成
  2. 作成したインスタンスの"UseShellExecute"プロパティを"true"に設定
  3. 作成したインスタンスを指定して"System.Diagnostics.Process.Start"メソッドをコール

なお"System.Diagnostics.Process.Start"メソッドは例外が発生するため例外処理を行います。

今回は"PackageList"の下に配置した<TextBlock>の"AlartTextBox"にエラーメッセージを表示させています。

<TextBlock x:Name="AlartTextBox" Grid.Row="6" Grid.ColumnSpan="2" Foreground="Red"/>

なお、"AlartTextBox"のエラーメッセージは、"PackageList"の選択を変更した際にコールされるイベントハンドラ、"PackageList_SelectionChanged"メソッドでクリアするようにしています。

private void PackageList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    AlartTextBox.Text = "";
}
PackageListの枠線追加

<ListView><ListBox>同様、デフォルトでは枠線が表示されません。

今回はコーナーと影をつけた枠線を表示したいのでMaterialDesignThemes<materialDesign:Card>コンポーネントを使用してみます。

<materialDesign:Card Grid.Row="5" Grid.ColumnSpan="2" Cursor="">
    <ListView x:Name="PackageList" SelectionMode="Single" ScrollViewer.HorizontalScrollBarVisibility="Hidden" SelectionChanged="PackageList_SelectionChanged">
        <ListView.View>
            <GridView>
                <GridView.ColumnHeaderContainerStyle>
                    <Style TargetType="{x:Type GridViewColumnHeader}">
                        <Setter Property="Background" Value="LightBlue"/>
                    </Style>
                </GridView.ColumnHeaderContainerStyle>
                <GridViewColumn x:Name="PackageColumn" Header="パッケージ" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn x:Name="LicenseColumn" Header="ライセンス" DisplayMemberBinding="{Binding License}" Width="100"/>
            </GridView>
        </ListView.View>
    </ListView>
</materialDesign:Card>

“PackageList"を配置していたGridの位置に<materialDesign:Card>を配置し、その子要素として従来のListViewを配置します。

AboutBoxのOKボタン

“MainWindow"のCloseボタン同様、"AboutBox"にもダイアログボックスを閉じるためのOKボタンを追加します。

<Button x:Name="OKButton" Content="OK" Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="5,5,10,10"
    Style="{StaticResource MaterialDesignRaisedLightButton}" Click="OKButton_Click" />

“Click"イベントに対するハンドラ"OKButton_Click"メソッドは以下の通りです。

private void OKButton_Click(object sender, RoutedEventArgs e)
{
    this.Close();
}

“MainWindow"のCloseボタンとは異なり、アプリケーションを終了する訳では無く、単純にウィンドウを閉じるだけです。

MainWindowにAboutBoxボタンの追加

通常、"AboutBox"は、メニューバーの"Help"以下の"About…"等のコマンドを選択すると表示されます。

ただ、今回のアプリケーションは、ファイルをコピーするだけの単純な機能しかありませんので、メニューを使用したコマンドは必要ありません。
なのでメニューを追加するメリットがありません。

代わりに、タイトルバーにAboutボタンを追加したいと思います。

幸い、MahApps.Metroにはタイトルバーの左右に簡単にボタンを追加する機能が備わっていますので、それを利用します。

“MainWindow.xaml"の<mah:MetroWindow...>タグ直下の子要素として<mah:MetroWindow.RightWindowCommands>タグの要素を追加します。

<mah:MetroWindow x:Class="CopyFilesWithSpecifiedName.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
            :
            :
        Title="CopyFilesWithSpecifiedName" Height="450" Width="800" ShowCloseButton="False">
    <mah:MetroWindow.RightWindowCommands>
        <mah:WindowCommands>
            <Button x:Name="AboutButton" Content="About" Click="AboutButton_Click"/>
        </mah:WindowCommands>
    </mah:MetroWindow.RightWindowCommands>
    <Grid>
        :
        :
    </Grid>
</mah:MetroWindow>

ボタンの要素は複数並べる事ができます。
また、タイトルバーの左にボタンを配置する場合には、<mah:MetroWindow.RightWindowCommands>タグの代わりに<mah:MetroWindow.LeftWindowCommands>タグを使用します。

ボタンの"Click"イベントに対するハンドラ"AboutButton_Click"は以下のようにします。

private void AboutButton_Click(object sender, RoutedEventArgs e)
{
    var about = new AboutBox();
    about.ShowDialog();
}

モーダルダイアログとして表示したいので、AboutBoxクラスのインスタンスを作成し"ShowDialog"メソッドを呼び出します。

ここまでのコードはこちらを参照してください。