WPFを使用してWindowsアプリを作成

実践!Windowsアプリを作る

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

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

第3章 機能更新」では"AboutBox"の追加とリストボックスに対する機能追加を行いました。

今回は、オマケとしてMaterialDesignThemesを使用したメッセージボックスやダイアログの実装とアプリケーションのリリースを行いたいと思います。

メッセージボックス

第2章 WPFデザイン」ではMahApps.Metroの"ShowMessageAsync"メソッドを使用したメッセージボックス表示を行いました。
ただ、MahApps.Metroのメッセージボックスは親ウィンドウの横幅全体に広がって表示されますので、従来のメッセージボックスを見慣れたユーザーにとっては違和感があります。
まぁ、Modern UIとしては当たり前のデザインなんですが…。

今回は、メッセージボックスにMaterialDesignThemesを使用し、従来のメッセージボックスに多少は近いデザインを目指します。

ユーザーコントロールの追加

第3章 機能更新」の"AboutBox"ではウィンドウベースのダイアログボックスを作成しました。
基本的にはWindowクラスを継承し、"ShowDialog"メソッドによりモーダルダイアログとして表示していました。

今回のMaterialDesignThemesを使用したメッセージボックスはユーザーコントロールをベースとして作成します。
表示はMaterialDesignThemes.Wpf.DialogHostクラスの"Show"メソッドで行います。

と云う訳でメッセージを表示するユーザーコントロールを作成して行きます。

  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"プロジェクトを右クリック
  2. “追加"→"ユーザーコントロール(WPF)…"を選択
  3. 「新しい項目の追加」ウィンドウで"名前"に"ErrorDialog"と入力し"追加"ボタンをクリック
ユーザーコントロール追加

ユーザーコントロールのデザイン

追加するユーザーコントロールの最終的なデザインは以下のようになります。

メッセージダイアログの最終デザイン

“ErrorDialog.xaml"ファイルが追加されていますので内容を以下のように変更します。

<UserControl x:Class="CopyFilesWithSpecifiedName.ErrorDialog"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
             mc:Ignorable="d"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             d:DesignHeight="200" d:DesignWidth="400">
    <materialDesign:Card>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <StackPanel x:Name="InfoPanel" Orientation="Horizontal" HorizontalAlignment="Left" Margin="10,10,0,5" Visibility="Hidden">
                <materialDesign:PackIcon Kind="MessageAlertOutline" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Width="48" Height="48" Foreground="#DD0000FF" Margin="0,0,5,0" />
                <Label Content="Infomation" Margin="5,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="#DD0000FF" FontSize="48"/>
            </StackPanel>
            <StackPanel x:Name="WarningPanel" Orientation="Horizontal" HorizontalAlignment="Left" Margin="10,10,0,5" Visibility="Hidden">
                <materialDesign:PackIcon Kind="Alert" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Width="48" Height="48" Foreground="#DDFFFF00" Margin="0,0,5,0" />
                <Label Content="Warning" Margin="5,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="#DDFFFF00" FontSize="48"/>
            </StackPanel>
            <StackPanel x:Name="ErrorPanel" Orientation="Horizontal" HorizontalAlignment="Left" Margin="10,10,0,5" Visibility="Hidden">
                <materialDesign:PackIcon Kind="AlertDecagram" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Width="48" Height="48" Foreground="#DDFF0000" Margin="0,0,5,0" />
                <Label Content="Error" Margin="5,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Foreground="#DDFF0000" FontSize="48"/>
            </StackPanel>
            <TextBlock x:Name="Message" TextWrapping="Wrap" Text="TextBlock" Cursor="Hand" Grid.Row="1" Margin="60,5,60,5"/>
            <Button x:Name="OKButton" Content="OK" HorizontalAlignment="Right" Margin="0,5,10,10" Grid.Row="2" VerticalAlignment="Bottom" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"/>
        </Grid>
    </materialDesign:Card>
</UserControl>

先ず、MaterialDesignThemesを使用しますので、<UserControl>タグの"xmlns:materialDesign"属性に"http://materialdesigninxaml.net/winfx/xaml/themes"を設定しています。

次に、ウィンドウを3段に分け、

  1. 上段: アイコンと"Infomation", “Warning"もしくは"Error"の表示
  2. 中段: メッセージ
  3. 下段: OKボタン

を配置します。

<UserControl>直下に<materialDesign:Card>を配置し、その下に<Grid>を置くようにする事でメッセージボックスをMaterialDesignThemesのカード型にする事にしました。

上段: アイコンと表示

上段のアイコンにはMaterialDesignThemesの"IconPack"を使用します。

なお、アイコンと表示する文字列については、"Infomation"と"Warning", “Error"其々に対応する<StackPanel>コンポーネントを用意し、"Visibility"属性を"Hidden"として非表示にしておきます。
ダイアログ作成時の指定により何れかを"Visible"にして表示するようにします。

中段: メッセージ

中段には<TextBlock>コンポーネントでメッセージを表示します。

表示するメッセージについてはダイアログ作成時に指定するようにします。

下段: OKボタン

下段にはOKボタンを配置します。

OKボタンクリック時にダイアログを閉じるように、<Button>タグの"Command"属性に"{x:Static materialDesign:DialogHost.CloseDialogCommand}"を指定します。

因みにボタンのクリックに対するイベントハンドラも指定可能ですので、ダイアログを閉じる際に色々な操作をする事もできます。

また、OKボタンが無くてもダイアログボックス上でマウスクリックするだけでダイアログを閉じる方法もあります。

ユーザーコントロールのクラス

“ErrorDialog.xaml.cs"ファイルにUserControlクラスを継承したErrorDialogクラスが作成されています。

それを以下のように変更します。

public partial class ErrorDialog : UserControl
{
    public enum Type
    {
        Error,
        Warning,
        Info,
    }

    public ErrorDialog(string message, Type type = Type.Error)
    {
        InitializeComponent();

        Message.Text = message;

        if (type == Type.Warning)
        {
            WarningPanel.Visibility = Visibility.Visible;
        }
        else if (type == Type.Info)
        {
            InfoPanel.Visibility = Visibility.Visible;
        }
        else
        {
            ErrorPanel.Visibility = Visibility.Visible;
        }
    }
}

コンストラクタで表示するメッセージと"Infomation"と"Warning", “Error"の区別を指定します。

なお、"Infomation"と"Warning", “Error"の区別のために列挙子Typeを規定しています。

MainWindow側の変更

MaterialDesignThemesのダイアログを使用するためには、ダイアログを呼び出す側のXAMLにも追加の設定が必要です。

“MainWindow.xaml"ファイルの<Window>もしくは<mah:MetroWindow>タグの子要素の<Grid>を囲むように<materialDesign:DialogHost>タグを設置します。

<mah:MetroWindow x:Class="CopyFilesWithSpecifiedName.MainWindow"
            :
            :
        Title="CopyFilesWithSpecifiedName" Height="450" Width="800" ShowCloseButton="False">
            :
            :
    <materialDesign:DialogHost>
        <Grid>
            :
            :
        </Grid>
    </materialDesign:DialogHost>
</mah:MetroWindow>

なお、<materialDesign:DialogHost>タグの属性"CloseOnClickAway"に"True"を設定すると、表示されたダイアログボックスをマウスクリックするだけで閉じる事ができるようになります。
今回はOKボタンをクリックするとダイアログボックスを閉じるようにしているので"CloseOnClickAway"属性は設定していません。

メッセージボックスの表示

<materialDesign:DialogHost>タグを設定したウィンドウでは、そのクラスメソッド中でDialogHostクラスの"Show"メソッドをコールする事でダイアログを表示する事ができます。

なお"Show"メソッドの引数には作成したユーザーコントロールのインスタンスを指定します。

例えば"MainWindow.xaml.cs"ファイル中のMainWindowクラスのメソッド"CopyButton_Click"は、以前にMahApps.Metroを使用して表示していたメッセージボックスの部分を以下のように変更します。

private async void CopyButton_Click(object sender, RoutedEventArgs e)
{
    CopyButton.IsEnabled = false;
    var rc = fileList.CopyFiles();
    if (rc < 0)
    {
        await DialogHost.Show(new ErrorDialog(fileList.ErrMessage, ErrorDialog.Type.Error));
    }
    else
    {
        await DialogHost.Show(new ErrorDialog("コピーしました。", ErrorDialog.Type.Info));
    }
    CopyButton.IsEnabled = true;
}

更に"FromButton_Clicked"メソッド内でもエラーメッセージを表示する部分がありますので、変更しておきます。

private async void FromButton_Click(object sender, RoutedEventArgs e)
{
    using (var openFolderDialog = new CommonOpenFileDialog()
    {
        Title = "コピー元フォルダを選択してください",
        IsFolderPicker = true,
    })
    {
        if (openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
        {
            var sourceDir = openFolderDialog.FileName;
            var rc = fileList.SetSourceDir(sourceDir, ExcludeCheck.IsChecked);
            if (rc == FileList.Code.NG)
            {
                await DialogHost.Show(new ErrorDialog(fileList.Message, ErrorDialog.Type.Error));
            }
            else
            {
                FromTextBox.Text = sourceDir;
                // コピーするファイルがない場合もCopyボタンは無効
                CopyButton.IsEnabled = ((rc == FileList.Code.OK) && (fileList.FileNameList.Count > 0));
            }
        }
    }
}

ここまでのコードについては、こちらを参照して下さい。

ファイルコピー中のプログレスバー表示

現状、ファイルコピーはFileクラスの"Copy"メソッドがコピーを終了するまでアプリケーションの他の作業を止めてしまいます。
結果としてギガバイト級の大きなファイルをコピーするとアプリケーションがフリーズしてしまいます。
まぁ、そのまま放置してファイルコピーが終了するのを待てば問題ないのですが、ユーザーとしては不安になります。

そこで、ファイルコピー中にコピー状況を示すプログレスバー表示すると共にファイルのコピーを中止できるよう、キャンセルボタンを追加したいと思います。

プログレスバーダイアログの追加

表示するプログレスバーについてはメッセージボックスの作成と同様にユーザーコントロールで作成します。

  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"プロジェクトを右クリック
  2. “追加"→"ユーザーコントロール(WPF)…"を選択
  3. 「新しい項目の追加」ウィンドウで"名前"に"FileCopyProgress"と入力し"追加"ボタンをクリック

プログレスバーダイアログのデザイン

メッセージボックスのデザインと同様、XAMLを使用してデザインしていきます。

最終的なデザインは以下のようになります。

プログレスバーの最終デザイン

“FileCopyProgress.xaml"ファイルは以下のようにします。

<UserControl x:Class="CopyFilesWithSpecifiedName.FileCopyProgress"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
             mc:Ignorable="d"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             d:DesignHeight="200" d:DesignWidth="400">
    <materialDesign:Card>
        <Grid>
            <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="10,10,10,10">
                <StackPanel Cursor="" Orientation="Horizontal" HorizontalAlignment="Center">
                    <Label x:Name="FileNameLabel" Content="Label" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                    <Label Content="をコピーしています。" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </StackPanel>
                <StackPanel Cursor="" Orientation="Horizontal" HorizontalAlignment="Center">
                    <ProgressBar x:Name="CircleProgress" IsIndeterminate="True" Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" />
                    <ProgressBar x:Name="FileCopiedProgress" Margin="10,20,20,20" Width="300" HorizontalAlignment="Center" VerticalAlignment="Center" />
                </StackPanel>
                <Button x:Name="CancelButton" Content="Cancel" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"/>
            </StackPanel>
        </Grid>
    </materialDesign:Card>
</UserControl>

先ず、MaterialDesignThemesを使用しますので<UserControl>タグの属性"xmlns:materialDesign"に"http://materialdesigninxaml.net/winfx/xaml/themes"’を設定しています。

その後、<StackPanel>を用いて上段にファイル名、中段にプログレスバーを2つ、下段にキャンセルボタンを配置します。

なお、メッセージボックスの場合と同様に全体を<materialDesign:Card>で囲っています。

プログレスバー

プログレスバーは2つ配置しています。

1つは円を描いて回るデザインです。
“Style"属性に"{StaticResource MaterialDesignCircularProgressBar}"をセットするだけで実現できます。
くるくると回るようにするには"IsIndeterminate"属性を"True"に設定します。

もう1つは標準的な直線のプログレスバーです。

キャンセルボタン

ファイルコピーをキャンセルするためのボタンです。

キャンセル時にはプログレスバーダイアログを閉じる必要がありますので"Command"属性に"{x:Static materialDesign:DialogHost.CloseDialogCommand}"を設定しています。

プログレスバーダイアログのクラス

プログレスバーダイアログのクラスは"FileCopyProgress.xaml.cs"ファイル内にUserControlを継承したクラスFileCopyProgressとして定義されます。

public partial class FileCopyProgress : UserControl
{
    public FileCopyProgress()
    {
        InitializeComponent();

        timer.Elapsed += TimerEvent;
    }

    public void SetFileNameProgress(string fileName, int value)
    {
        this.Dispatcher.Invoke(new Action(() =>
        {
            FileNameLabel.Content = fileName;
            FileCopiedProgress.Value = value;
            CircleProgress.Value = value;
        }));
    }
}

プログレスバーダイアログに表示するファイル名とプログレスバーの進捗状況を示す値をアップデートするためのメソッド"SetFileNameProgress"を追加しています。

このメソッドは後ほどファイルコピー中にFileListクラスのメソッド"CopyFiles"からコールされるようにします。
なのでUIの操作はディスパッチャーに登録して任せています。

ディスパッチャーを経由せず、直接、UIを操作しようとすると例外が発生して「スレッドが違う!!」と怒られます。

ディスパッチャー経由で操作を行うには、UserControlクラスの"Dispatcher"プロパティの"Invoke"メソッドにデリゲート(ここではAction)を指定してコールします。
デリゲート内には実行したい操作を記述します。

ファイルコピーの非同期化

Fileクラスの"Copy"メソッドはファイルのコピーが終了するまでアプリケーションを止めてしまっています。
なので、先ずファイルをコピーする部分を非同期にし、アプリケーションを止めないようにします。

“FileList.cs"ファイル内、FileListクラスの"CopyFiles"メソッドについてFileクラスの"Copy"メソッドをコールしている部分をStreamクラスの"CopyToAsync"メソッドに置き換えます。

なお、コピーしているファイル名とコピーの進捗状況をプログレスバーに表示するため、引数にプログレスバーダイアログのオブジェクトを設定できるようにします。
更に、途中でFileCopyProgressクラスの"SetFileNameProgress"メソッドをコールしています。

public async Task<Code> CopyFiles(FileCopyProgress progress)
{
    Code result = Code.OK;
    int fileCount = 0;

    // コピー先フォルダの指定がない場合はコピー元フォルダにコピー
    var dir = (TargetDir == "") ? SourceDir : TargetDir;

    foreach (var file in FileNameList)
    {
        var targetFileName = Path.Join(dir, file.ToFile);

        progress.SetFileNameProgress(Path.GetFileName(file.FromFile), (++fileCount * 100 / FileNameList.Count));

        try
        {
            using (var source = File.OpenRead(file.FromFile))
            {
                using (var target = File.Open(targetFileName, FileMode.CreateNew))
                {
                    await source.CopyToAsync(target).ConfigureAwait(false);
                }
            }
        }
        catch (IOException)
        {
            // ファイルが既に存在する等のエラーはそのまま続行する
            continue;
        }
        catch (Exception e)
        {
            Message = e.Message;
            result = Code.NG;
            break;
        }
    }

    return result;
}

ファイルのコピーについては、先ずコピー元のファイルとコピー先のファイルをusingを使用してオープンします。
コピー先のファイルは上書きをしないために"FileMode.CreateNew"を用いてオープンします。

Streamクラスの"CopyToAsync"メソッドは非同期ですのでawaitを用いてメソッドの完了を待ちます。
“ConfigureAwait(false)"はデッドロック回避のためのおまじないです。
通常は無くても構いませんが念のため…。

内部で非同期メソッドを使用しているため"CopyFiles"メソッドの戻り値をCodeからTask<Code>に変更しasyncを付加します。

ファイルコピーのキャンセル

ファイルコピーの非同期化を行い、アプリケーションがフリーズする事は防止できるようになりました。
これで一応ファイルコピー中でもCloseボタンをクリックすればアプリケーションを終了する事ができます。

ただ、ファイルコーピーを無理やりキャンセルしている状態なので決して良い事ではありません。

なので"CopyFiles"メソッドにコードを更に追加しファイルコピーをキャンセルできるようにします。

ファイルコピーのキャンセルを行うには、先ず、"FileList.cs"ファイル内、FileListクラスの"CopyFiles"メソッドについてStreamクラスの"CopyToAsync"メソッドの第2引数にCancellationTokenSourceクラスの"Token"プロパティをセットします。
キャンセル時には、CancellationTokenSourceクラスの"Cancel"メソッドを外部からコールします。

なお、ファイルコピーをキャンセルした際にはStreamクラスの"CopyToAsync"メソッドは例外を発生します。
更にCancellationTokenSourceクラスの"IsCancellationRequested"プロパティが"true"にセットされます。

public enum Code
{
    OK,
    NG,
    Cancel,
}
    :
    :
private CancellationTokenSource? tokenSource = null;
private readonly object balanceLock = new object();
        :
        :
public async Task<Code> CopyFiles(FileCopyProgress progress)
{
    Code result = Code.OK;
    int fileCount = 0;

    // コピー先フォルダの指定がない場合はコピー元フォルダにコピー
    var dir = (TargetDir == "") ? SourceDir : TargetDir;

    lock (balanceLock)
    {
        tokenSource = new CancellationTokenSource();
    }

    foreach (var file in FileNameList)
    {
        var targetFileName = Path.Join(dir, file.ToFile);

        progress.SetFileNameProgress(Path.GetFileName(file.FromFile), (++fileCount * 100 / FileNameList.Count));

        try
        {
            using (var source = File.OpenRead(file.FromFile))
            {
                using (var target = File.Open(targetFileName, FileMode.CreateNew))
                {
                    await source.CopyToAsync(target, tokenSource.Token).ConfigureAwait(false);
                }
            }
        }
        catch (IOException)
        {
            // ファイルが既に存在する等のエラーはそのまま続行する
            continue;
        }
        catch (Exception e)
        {
            Message = e.Message;
            result = tokenSource.IsCancellationRequested ? Code.Cancel : Code.NG;
            break;
        }
    }

    lock (balanceLock)
    {
        tokenSource.Dispose();
        tokenSource = null;
    }

    return result;
}

public void CancelCopy()
{
    lock (balanceLock)
    {
        tokenSource?.Cancel();
    }
}

CancellationTokenSourceクラスの"Cancel"メソッドを外部からコールするため、新たに"CancelCopy"メソッドを追加しています。

また、CancellationTokenSourceクラスのインスタンス操作時にはタスク切り換え等による不整合が起きないようlockステートメントによる排他処理を行っています。

プログレスバーダイアログの表示

更に"MainWindow.xaml.cs"ファイル内、MainWindowクラスの"CopyButton_Click"メソッドで"CopyFiles"メソッドをコールしている部分について、ファイルのコピーを開始すると共にプログレスバーダイアログの表示を行うように変更します。

なお、内部で非同期メソッドを使用しているため宣言部分にasyncを付加します。

private async void CopyButton_Click(object sender, RoutedEventArgs e)
{
    CopyButton.IsEnabled = false;

    var progress = new FileCopyProgress();
    var copyTask = fileList.CopyFiles(progress);
    var progressTask = DialogHost.Show(progress);

    var taskDone = await Task.WhenAny(copyTask, progressTask);

    if (taskDone == progressTask)
    {
        fileList.CancelCopy();
        await copyTask;
        await DialogHost.Show(new ErrorDialog("キャンセルされました。", ErrorDialog.Type.Warning));
    }
    else
    {
        DialogHost.Close(null);
        await progressTask;

        var rc = copyTask.Result;
        if (rc == FileList.Code.NG)
        {
            await DialogHost.Show(new ErrorDialog(fileList.Message, ErrorDialog.Type.Error));
        }
        else
        {
            await DialogHost.Show(new ErrorDialog("コピーしました。", ErrorDialog.Type.Info));
        }
    }

    CopyButton.IsEnabled = true;
}

主にvar rc = await fileList.CopyFiles();としていた部分を変更します。

先ず、プログレスバーを表示するためのユーザーコントロールFileCopyProgressクラスのオブジェクトを作成します。
次に、作成したFileCopyProgressクラスのオブジェクトを引数にしてFileListクラスの"CopyFiles"メソッドをコールします。

通常であれば"CopyFiles"メソッドは非同期処理なのでメソッドの前にawaitを付加して処理の完了を待つのですが、今回は同時にプログレッシブバーダイアログの表示を行いたいので、awaitは付加せず、すぐ次の処理に移るようにします。

次の処理では、作成してあるFileCopyProgressクラスのオブジェクトを引数にしてDialogHostクラスの"Show"メソッドをコールします。
この処理も非同期処理ですがawaitを付加せず、すぐ次の処理に移るようにします。

“CopyFiles"メソッドおよび"Show"メソッドのタスクの終了はTaskクラスの"WhenAny"メソッドを使用して確認します。
“WhenAny"メソッドは、引数に指定したタスクの何れかが終了するのを待つためのタスクを作成、実行します。
なので、awaitを付加するとタスクの終了時には終了したタスクが返ります。

返ったタスクを確認する事でプログレスバー表示のキャンセルボタンがクリックされたか、ファイルコピーが終了したかの区別が付きます。

プログレスバー表示のキャンセル時

プログレスバー表示のキャンセルボタンがクリックされた場合にはawait Task.WhenAny(copyTask, progressTask);からは"progressTask"が戻ります。

その場合にはFileListクラスの"CancelCopy"メソッドをコールしてファイルコピーをキャンセルします。

await copyTask;によってファイルコピー終了を待った後、ファイルのコピーがキャンセルされた旨のメッセージボックスを表示します。

ファイルコピー終了時

ファイルコピーが先に終了した場合にはawait Task.WhenAny(copyTask, progressTask);からは"copyTask"が戻ります。

その場合には、DialogHostクラスの"Close"メソッドをコールしてプログレスバー表示をクローズします。
引数は"null"で問題ありません。

await progressTask;によってプログレスバー表示の終了を待った後、ファイルコピーの結果を確認してメッセージボックスを表示します。

まとめ

今回、追加の修正としてMaterialDesignThemesを使用したメッセージボックス表示とファイルコピーの非同期処理化、プログレスバー表示、ファイルコピーのキャンセル機能追加を行いました。

特にプログレスバー表示とファイルキャンセル機能追加ではマルチタスクを明示的に使用していますので一気に面倒になっています。
従来のマルチスレッドやマルチプロセスから比べれば遥かに簡便になっているようなのですが…。

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

おまけのおまけ(リリース版作成)

作成したプログラムのリリース版をWindows PCにインストールできるようにします。

簡単なプログラムで適切な設定を行っていればプロジェクトのビルドで作成された".exe"ファイルをコピーしただけで動作します。
ただ最近のプログラムは簡単な物であっても関連するライブラリは多岐にわたり、インストーラーによって必要なライブラリをチェックし、適切に組み込むことが大切となります。

今回のアプリケーションでも、.NET6は標準でWindowsには組み込まれておらず、追加でインストールする必要があります。
また、MahApps.MetroMaterialDesignThemes等、Nugetパッケージのライブラリも追加インストールする必要があります。

そのため、今回作成したアプリケーションのインストーラーを作成していきます。

なお、本格的なインストーラーとしては従来".msi"形式が使われてきましたが、.NET Framework 2.0からClickOnceと云う新しいインストーラーの形式が導入されています。

ClickOnceを作成する機能はVisualStudioにデフォルトで搭載されていますので、今回はClickOnceを使います。

ClickOnceの設定

ClickOnceによるインストーラー作成のため以下のように設定を行います。

  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"プロジェクトを右クリック
  2. “発行…"を選択
    発行の選択
  3. 「公開」ウィンドウの"ターゲット"で"ClickOnce"を選択し"次へ"をクリック
    ClickOnceをターゲットに選択
  4. 「公開」ウィンドウの"発行場所"はそのままでOKなので"次へ"をクリック
    ClickOnceのインストーラーの出力先
  5. 「公開」ウィンドウの"インストール場所"は"CD、DVD、またはUSBドライブから"を選択し"次へ"をクリック
    ClickOnceで作成したインストーラーの保管先
  6. 「公開」ウィンドウの"設定"で"オプション"をクリック
    ClickOnceの設定
  7. 「発行オプション」ウィンドウの"説明"で"発行者名"等の欄を適当に記入した後"OK"をクリック
    なお"説明"以外はデフォルトでOK
    ClickOnceのオプション設定
  8. 「公開」ウィンドウの"マニュフェストの署名"はそのままでOKなので"次へ"をクリック
    ClickOnceのマニュフェストなし画面
  9. 「公開」ウィンドウの"構成"は以下の設定を確認後"完了"をクリック
    ClickOnceの構成画面
  • 構成: Release|AnyCPU
  • ターゲットフレームワーク: net6.0-windows
  • 配置モード: フレームワーク依存
  • ターゲットランタイム: 移植可能

以上で"発行プロファイル…が作成されました。"と表示されればOKです。
“閉じる"をクリックしてウィンドウを閉じます。

「CopyFilesWithSpecifiedName.csprj:公開」ウィンドウの"発行"をクリックするとリリース版のビルドが行われます。

ClickOnceの実際の発行

発行場所"bin\publish\"フォルダ内に"setup.exe"と共にインストールに必要なファイルが作成されます。

他のWindows PCにアプリケーションをインストールする場合には"bin\publish\"以下のファイルを全てコピーし、"setup.exe"を実行します。

リリースしたインストーラーはこちらからダウンロードできます。

実践!Windowsアプリを作る

実践!Windowsアプリを作る

第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"メソッドを呼び出します。

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

実践!Windowsアプリを作る

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

ただ、実行して頂ければ判る通り、Windowアプリとしてのデザインは、お世辞にも良いとは言えません。
モノクロームの色彩、ワイヤーフレームかと勘違いしそうな2Dのデザイン等々…。
Windows Formsやそれ以前のMFCでもデフォルトでそれなりのデザインができていました。

まあ、設計思想としてWPFUWP等は「XAMLで比較的自由にデザインできるので、デフォルトのデザインは超シンプルにしておいて、デザイナーが好きにデザインしてね!」と云う事なのでしょう。

とは云え、絵心の無いプログラマーとしては、あまり手を掛けずにそれなりのデザインが出来上がってくれる方が良いわけで…。

と云う訳で、今回はあまり手を掛けずにそれなりのデザインのアプリケーションに仕上げていきたいと思います。

MahApps.Metro

MahApps.MetroWPFのアプリケーション全体をMetro風のデザインにしてくれるNuGetのパッケージです。

XAMLファイルを修正、追記が必要であったり、"MainWindow.xaml.cs"を微修正する必要はありますが、通常のデザインに必要な作業量から比べれば微々たるものです。
また、デザインがある程度決まっていますので、自身で一からデザインを考える必要も無く非常に楽です。

MahApps.Metroのインストール

基本的なインストール方法はこちらを参照してください。

  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"を右クリック
  2. “NuGetパッケージの管理…"を選択
  3. 「NuGetパッケージマネージャー」で「参照」を選択
  4. “MahApps.Metro"を選択し、"インストール"をクリック

MainWindow.xaml.csのMahApps.Metro対応

MahApps.Metroを使用するためには、先ず、Windowクラスを継承したMainWindowクラスの親クラスをMetroWindowクラスに変更する必要があります。
“MainWindow.xaml.cs"の変更は以下のようにします。

using MahApps.Metro.Controls;

namespace CopyFilesWithSpecifiedName
{
    public partial class MainWindow : MetroWindow
    {
            :
            :
        public MainWindow()
        {
            InitializeComponent();
                :
                :
        }
            :
            :
    }
}

usingディレクティブで"MahApps.Metro.Controls"を指定した上で、MainWindowのクラス宣言でMetroWindowクラスを継承するように指定するだけです。

MainWindow.xamlのMahApps.Metro対応

MahApps.Metroを使用するためには、ウィンドウのデザイン等を指定するXAMLファイル、ここでは"MainWindow.xaml"を変更します。

デフォルトではMainWindowWindowクラスを継承していましたので、<Window...>タグを使用していました。
MainWindwMetroWindowを継承するようになりましたのでタグもそれに合わせて変更します。

<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"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        :
        :
    </Grid>
</mah:MetroWindow>

<Window...>タグを<mah:MetroWindow...>タグへと変更し"xmlns:mah"属性に"http://metro.mahapps.com/winfx/xaml/controls"を追加指定します。

App.xamlのMahApps.Metro対応

最後に、"App.xaml"のMahApps.Metro対応を行います。

<Application.Resources>タグの子要素として以下のような<ResourceDictionary>を追加します。

<Application x:Class="CopyFilesWithSpecifiedName.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

なお、"pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml"の部分でデザインテーマを選択できます。

“Light.Blue"の部分を以下のように指定できます。

前半部分については、"Light"と"Dark"の2種類から選択できます。
後半部分については、"Red", “Green", “Blue", “Purple", “Orange", “Lime", “Emerald", “Teal", “Cyan", “Cobalt", “Indigo", “Violet", “Pink", “Magenta", “Crimson", “Amber", “Yellow", “Brown", “Olive", “Steel", “Mauve", “Taupe", “Sienna"の23種類から選択できます。

デフォルトのMahApps.Metroデザイン

とりあえず現時点でMahApps.Metroデザインに変わっているはずです。
実行結果は以下の通りです。

デフォルトのMahApps.Metroデザイン

とりあえずはタイトルバーとボーダーに色が付き、ボタンの角が丸みを帯びたデザインに変わっています。
マウスオーバーした際にはフラッシュしたり、ボタンクリック時にはボーダーが太くなったりと、それなりに見栄えが良くなっています。

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

ただ、ボタン等のコントロール類のデザインは多少良くなった程度なので、もう少し手を加えたいと思います。

Material Design In XAML Toolkit

Material Design In XAML ToolkitWPFのアプリケーション全体をマテリアルデザイン風にしてくれるNuGetのパッケージです。

XAMLファイルの修正、追記の必要はありますが、通常のデザインに必要な作業量から比べれば微々たるものです。
また、デザインがある程度決まっていますので、自身で一からデザインを考える必要も無く非常に楽です。

Material Design In XAML Toolkitのインストール

基本的なインストール方法はこちらを参照してください。

  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"を右クリック
  2. “NuGetパッケージの管理…"を選択
  3. 「NuGetパッケージマネージャー」で「参照」を選択
  4. “MaterialDesignThemes"を選択し、"インストール"をクリック

なお、Material Design In XAML ToolkitMahApps.Metroを併用する場合には、"MaterialDesignThemes"と同時に"MaterialDesignThemes.MahApps"をインストールします。
因みに"MaterialDesignThemes.MahApps"をインストールすると、"MaterialDesignThemes"と"MahApps.Metro"も同時にインストールしてくれます。

MainWindow.xamlのMaterial Design In XAML Toolkit対応

Material Design In XAML Toolkitを使用するためには、ウィンドウのデザイン等を指定するXAMLファイル、ここでは"MainWindow.xaml"を変更します。

基本的には<Window...>タグの属性として以下を追加します。

xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
Background="{DynamicResource MaterialDesignPaper}"
TextElement.FontWeight="Medium"
TextElement.FontSize="14"
FontFamily="{materialDesign:MaterialDesignFont}"

MahApps.Metroと併用する場合には、<mah:MetroWindow...>タグの属性として追加します。

<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"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
        mc:Ignorable="d"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
        TextElement.Foreground="{DynamicResource MaterialDesignBody}"
        Background="{DynamicResource MaterialDesignPaper}"
        TextElement.FontWeight="Medium"
        TextElement.FontSize="14"
        FontFamily="{materialDesign:MaterialDesignFont}"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        :
        :
    </Grid>
</mah:MetroWindow>

<Window...>タグを<mah:MetroWindow...>タグへと変更し"xmlns:mah"属性として"http://metro.mahapps.com/winfx/xaml/controls"を追加指定します。

App.xamlのMaterial Design In XAML Toolkit対応

最後に、"App.xaml"のMaterial Design In XAML Toolkit対応を行います。

先ず、<Application...>タグの"xmlns:materialDesign"属性として “http://materialdesigninxaml.net/winfx/xaml/themes"を追加します。
更に<Application.Resources>タグの子要素として以下のような<ResourceDictionary>を追加します。

<Application x:Class="CopyFilesWithSpecifiedName.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!-- Material Design -->
                <materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

更にMahApps.Metroと併用する場合には、MahApps.Metro<ResourceDictionaryの記述と共にMaterial Design In XAML ToolkitMahApps.Metroの互換性を保つための指定を追加します。

<Application x:Class="CopyFilesWithSpecifiedName.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:CopyFilesWithSpecifiedName"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!-- MahApps -->
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <!-- Theme setting -->
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
                <!-- Material Design -->
                <materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
                <!-- Material Design: MahApps Compatibility -->
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.MahApps;component/Themes/MaterialDesignTheme.MahApps.Flyout.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <!-- MahApps Brushes -->
            <SolidColorBrush x:Key="HighlightBrush" Color="{DynamicResource Primary700}"/>
            <SolidColorBrush x:Key="AccentBaseColorBrush" Color="{DynamicResource Primary600}" />
            <SolidColorBrush x:Key="AccentColorBrush" Color="{DynamicResource Primary500}"/>
            <SolidColorBrush x:Key="AccentColorBrush2" Color="{DynamicResource Primary400}"/>
            <SolidColorBrush x:Key="AccentColorBrush3" Color="{DynamicResource Primary300}"/>
            <SolidColorBrush x:Key="AccentColorBrush4" Color="{DynamicResource Primary200}"/>
            <SolidColorBrush x:Key="WindowTitleColorBrush" Color="{DynamicResource Primary700}"/>
            <SolidColorBrush x:Key="AccentSelectedColorBrush" Color="{DynamicResource Primary500Foreground}"/>
            <LinearGradientBrush x:Key="ProgressBrush" EndPoint="0.001,0.5" StartPoint="1.002,0.5">
                <GradientStop Color="{DynamicResource Primary700}" Offset="0"/>
                <GradientStop Color="{DynamicResource Primary300}" Offset="1"/>
            </LinearGradientBrush>
            <SolidColorBrush x:Key="CheckmarkFill" Color="{DynamicResource Primary500}"/>
            <SolidColorBrush x:Key="RightArrowFill" Color="{DynamicResource Primary500}"/>
            <SolidColorBrush x:Key="IdealForegroundColorBrush" Color="{DynamicResource Primary500Foreground}"/>
            <SolidColorBrush x:Key="IdealForegroundDisabledBrush" Color="{DynamicResource Primary500}" Opacity="0.4"/>
            <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchBrush.Win10" Color="{DynamicResource Primary500}" />
            <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.OnSwitchMouseOverBrush.Win10" Color="{DynamicResource Primary400}" />
            <SolidColorBrush x:Key="MahApps.Metro.Brushes.ToggleSwitchButton.ThumbIndicatorCheckedBrush.Win10" Color="{DynamicResource Primary500Foreground}" />
        </ResourceDictionary>
    </Application.Resources>
</Application>

互換性のため、<ResourceDictionary...>は2つだけ追加となりますが、<SolidColorBrush...>等、結構多くの設定の追加が必要です。

Material Design In XAML ToolkitとMahApps.Metroデザイン

とりあえず現時点でMaterial Design In XAML ToolkitMahApps.Metroを併用したデザインに変わっているはずです。
実行結果は以下の通りです。

MaterialDesignThemesとMahApps.Metroを同時に使用したデザイン

とりあえずボタン類に色がついてかなり見栄えが良くなりました。

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

デザインの微修正

Material Design In XAML ToolkitMahApps.Metroを併用した事で、ある程度の見栄えにする事ができました。
ただ、もう少し手を加える事ができるかと思います。

タイトルバーの修正

タイトルバーについてはMahApps.Metroによって色がついて多少は良くなってはいますが、もう少し手を入れていきたいと思います。

タイトル変更

現状、タイトルバーのタイトルは"MainWindow"となっています。
これをアプリケーション名である"CopyFilesWithSpecifiedName"に変更します。

タイトルバーのタイトルは、"MainWindow.xaml"ファイル中、<Window...>もしくは<mah:MetroWindow>タグの"Title"属性を変更します。

<mah:MetroWindow x:Class="CopyFilesWithSpecifiedName.MainWindow"
            :
            :
        Title="CopyFilesWithSpecifiedName" Height="450" Width="800">
    <Grid>
        :
        :
    </Grid>
</mah:MetroWindow>

クローズボタン

Closeボタンを実装していますのでタイトルバーの右端のクローズボタンは不要です。
消しておきましょう。

“MainWindow.xaml"ファイル中、<Window...>もしくは<mah:MetroWindow>タグに"ShowCloseButton"属性として"False"を追加指定します。

<mah:MetroWindow x:Class="CopyFilesWithSpecifiedName.MainWindow"
            :
            :
        Title="CopyFilesWithSpecifiedName" Height="450" Width="800" ShowCloseButton="False">
    <Grid>
        :
        :
    </Grid>
</mah:MetroWindow>

タイトルバーの色とボタンの色

現状、タイトルバーの色はMahApps.Metroのテーマで設定された"Light.Blue"、ボタン等のコントロールの色はMaterial Design In XAML ToolkitのPrimaryColorで設定された"DeepPurple"となっています。

少々、ボタンの色が鮮やかすぎる感じですので、タイトルバーの色に寄せたいと思います。
同じ色にする事も可能ですが、設定が面倒ですので、あくまでも似た色に変更と云う事で…。

基本的にはMaterial Design In XAML Toolkit<materialDesign:BundledTheme.../>タグの"PrimaryColor"属性を変更するだけです。
今回は"LightBlue"に変更しておきます。

<Application x:Class="CopyFilesWithSpecifiedName.App"
                  :
                  :
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                    :
                    :
                <!-- Material Design -->
                <materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="LightBlue" SecondaryColor="Lime" />
                    :
                    :
            </ResourceDictionary.MergedDictionaries>
                :
                :
        </ResourceDictionary>
    </Application.Resources>
</Application>

Copyボタンの修正

Copyボタンが味気ないので、もう少し手を加えてみます。
ボタンの形状を両端が丸くなった形とし、内部にIconと"Copy"の文字を表示します。
ついでにツールチップも表示するようにしましょう。

ボタンの外観はこんな感じにします。

Copyボタン

本来、ボタンの形状を変えたりするのはXAMLを使用すれば可能ですし、Iconも自作等を行ってプロジェクトに読み込ませることもできます。
ただ、MaterialDesignThemesに用意されたものを使えば容易ですので、今回はMaterialDesignThemesを使っていきます。

MaterialDesign DemoApp

MaterialDesignThemesは非常に便利ですが、用意されたデザインが豊富なので、何をどのように指定したらいいのか判り難い場合が殆どです。
そのため、サンプルとなるデザインがデモアプリとして用意されています。

MaterialDesignInXamlToolkitのページの"Releases"から"DemoApp.zip"をダウンロードします。

ダウンロードしたzipファイルを解凍し、"demo-app"->"Release"->"net6.0-windows"内の"MaterialDesignDemo.exe"を実行すると、以下のような画面が表示されます。

MaterialDesignThemesデモアプリ

“Welcome to Material Design In Xaml Toolkit"の下の"EXPLORE"ボタンをクリックすると、左にナビゲーションウィンドウが表示されます。

MaterialDesignThemesのデモアプリのNavigationウィンドウ

例えば"Buttons"を選択すると、様々な種類のボタンが表示されます。

MaterialDesignThemesのデモアプリのボタン

ボタンの横の"</>"をクリックすると、サンプルのボタンを表示するためのタグが表示されますので、参考にする事ができます。

MaterialDesignThemesのデモアプリでサンプルタグの表示

Copyボタン形状指定

今回の表示したいCopyボタンに近い物としては、"Buttons-With Custom Corner Radius"中の"25/50 Radius"ですが、そのタグは以下のようになります。

<Grid
  Width="{StaticResource GridWidth}">
  <Button
    Height="{StaticResource ButtonHeight}"
    materialDesign:ButtonAssist.CornerRadius="25"
    IsEnabled="{Binding DataContext.ControlsEnabled, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
    Style="{StaticResource MaterialDesignRaisedDarkButton}"
    ToolTip="MaterialDesignRaisedDarkButton with Round Corners">
    <TextBlock
      Text="25/50 Radius" />
  </Button>
</Grid>

このコード中、ボタンの両端を丸くするための指定は"materialDesign:ButtonAssist.CornerRadius"属性の部分ですので、Copyボタンのタグに属性の値"25″を追加します。

<Button x:Name="CopyButton" Content=" ⇒ " Margin="5,0,5,0" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" IsEnabled="False" Click="CopyButton_Click"
        materialDesign:ButtonAssist.CornerRadius="25"/>

本来、ボタンのコーナーの丸みは、<Button>タグ中に<Broder>タグを指定し、その属性として"CornerRadius"を設定する必要があり、結構面倒なのですが、MaterialDesignThemesを使用すれば<Button>タグの属性1つで設定できるので大変便利です。

Copyボタンのツールチップの追加

ツールチップについては、MaterialDesignThemesMahApps.Metroは関係無く、単純に<Button>タグに"ToolsTip"属性を設定すれば良いだけです。

<Button x:Name="CopyButton" Content=" ⇒ " Margin="5,0,5,0" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" IsEnabled="False" Click="CopyButton_Click"
        materialDesign:ButtonAssist.CornerRadius="25"
         ToolTip="ファイルを指定の名前でコピーします。" Cursor=""/>

理由は不明なのですが"Cursor"属性が自動的に付加されます。
特に問題はなさそうなので、そのままスルーして下さい。

なお、このままですと、ボタンが"Disable"の時にツールチップが表示されません。
今回は、Copyボタンが"Disable"の時にもツールチップを表示させたいので追加で"ToolTipService.ShowOnDisabled"属性に"True"を指定します。

<Button x:Name="CopyButton" Content=" ⇒ " Margin="5,0,5,0" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" IsEnabled="False" Click="CopyButton_Click"
        materialDesign:ButtonAssist.CornerRadius="25"
         ToolTip="ファイルを指定の名前でコピーします。" ToolTipService.ShowOnDisabled="True" Cursor=""/>

CopyボタンにIconと文字列を追加

現状、Copyボタンには" ⇒ “の表示を行っていますが、これをIconと"Copy"の文字列を表示したいと思います。

具体的には、<Button>の子要素として<StackPanel>内にIconと<TextBlock>を配置します。

先ずIconですが、MaterialDesignThemesには豊富なIconが用意されています。

先ほどの"MaterialDesignDemo.exe"で左のナビゲーションウィンドウで"Icon Pack"を選択すると、利用できるIconがリストアップされます。
ただ、数が多いので、左下の"Search"で検索すると良いでしょう。

今回は"File"で検索し"FileMoveOutline"Iconをクリックします。
“Usage:"にIconのタグが表示されますので、"Copy To Clipboard"ボタンでタグをコピーし、"MainWindow.xaml"ファイルに反映します。

MaterialDesignThemesのデモアプリでファイル移動ICONを選択

Iconと文字列をセットしたCopyボタンのタグは以下のようになります。
なお"Content"属性は不要となりますので削除しておきます。

<Button x:Name="CopyButton" Margin="5,0,5,0" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" IsEnabled="False" Click="CopyButton_Click"
        materialDesign:ButtonAssist.CornerRadius="25"
         ToolTip="ファイルを指定の名前でコピーします。" ToolTipService.ShowOnDisabled="True" Cursor="">
    <StackPanel Orientation="Horizontal">
        <materialDesign:PackIcon Kind="FileDocumentArrowRightOutline" />
        <TextBlock Text="Copy"/>
    </StackPanel>
</Button>

Closeボタンの修正

Closeボタンについては、表示する文字列にIconを追加します。

MaterialDesignThemesの"Icon Pack"から選択しようと思ったのですが、ピンとくるIconが無かったので、MahApps.Metroが提供するIconを使用する事にします。

以下のようなボタンにしたいと思います。

CloseボタンにICONを追加

MahApps.Metro.IconPacks

MahApps.Metroは、IconとしてMahApps.Metro.IconPacksを提供しています。

MahApps.Metroと同様に「NuGetパッケージマネージャー」でMahApps.Metro.IconPacksをインストールします。

NuGetからMahApps IconPacksパッケージを導入

MahApps.Metro.IconPacksMaterialDesignThemesの"Icon Pack"以上にIconの数が多いため、選択には専用のブラウザが提供されています。

こちらから"Releases"をクリックし、"IconPacks.Browser-net50-v1.0.0.zip"をダウンロードします。
ダウンロードしたファイルを解凍し、"IconPacks.Browser.exe"を実行すると、Iconのリストが表示されます。

MahApps.MetroのIconPackのBrowser

Closeボタン用Iconの選択

MahApps.Metro.IconPacksが提供するIconの数は多いので、"IconPacks.Browser.exe"の右上の検索欄を使用し、候補を絞る事ができます。

今回は、"Exit"で候補を絞り、適当なIconを選択します。
上部に"<> {} …"等の記号が表示されますので、XAML用には"<>"をクリックし、タグをコピーします。

MahApps.Metro.IconPacksの適用

MahApps.Metro.IconPacksを使用するためには、先ず"MainWindow.xaml"ファイル中、<Window...>もしくは<mah:MetroWindow>タグに"xmlns:iconPacks"属性として"http://metro.mahapps.com/winfx/xaml/iconpacks"を追加します。

その後、<Button>の子要素として"IconPacks.Browser.exe"でコピーしたタグを追加します。

結果として"MainWindow.xaml"ファイルのCloseボタンは以下のようなコードとなります。
なお、<Button>タグの"Content"属性は不要となりますので削除します。

<mah:MetroWindow x:Class="CopyFilesWithSpecifiedName.MainWindow"
            :
            :
        xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
        Title="CopyFilesWithSpecifiedName" Height="450" Width="800" ShowCloseButton="False">
    <Grid>
            :
            :
        <Button x:Name="CloseButton" Margin="0,5,5,5" Grid.Row="3" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Click="CloseButton_Click">
            <StackPanel Orientation="Horizontal">
                <iconPacks:PackIconVaadinIcons Kind="Exit" />
                <TextBlock Text="Exit"/>
            </StackPanel>
        </Button>
    </Grid>
</mah:MetroWindow>

リストボックスの装飾

リストボックスの枠線とバックグラウンド色をプロパティで設定リストボックスについては、MahApps.Metroを導入した時点で枠線が削除されたデザインになったため、ファイルが追加されるまではその範囲が判りません。
これでは少々不便なため、リストボックスの範囲が判るように枠線とバックグラウンド色を追加してみます。

  1. 「ドキュメントアウトライン」で"FromListBox"を選択
  2. 「プロパティ」で"ブラシ"の"Background"を選択
  3. “エディター"で"⇆"に"#4C03A9F4″を入力
  4. 「プロパティ」で"ブラシ"の"BorderBrush"を選択
  5. “エディター"で"⇆"に"#FF03A9F4″を入力
  6. 「プロパティ」で"外観"の"∨"以下を表示
  7. “BorderThickness"の項目全てに"1″を入力

修正後ののデザイン

タイトルバーとCopyボタン、Closeボタン、リストボックス修正後ののデザインは以下のようになります。

変更後の新しいデザイン

ダイアログボックスの変更

メインウィンドウのデザインはかなりマシになったかと思いますが、ダイアログボックスのデザインは元のままです。

古いデザインのダイアログボックス

これでは中途半端です。

なので、MahApps.MetroMaterialDesignThemesを使用したダイアログボックスの表示を行いたいと思います。
ただ、MaterialDesignThemesを使用したダイアログボックスの表示についてはコードの量が多くなりますので現時点ではMahApps.Metroを使用したダイアログボックスの表示を行いたいと思います。

MahApps.Metroのダイアログボックス

MahApps.Metroでのダイアログボックスの表示は簡単です。

現状、MessageBox.Show("コピーしました。");としてダイアログボックスを表示していた部分をawait this.ShowMessageAsync("コピー", "コピーしました。");等のように変更するだけです。

なお、非同期関数コールですので、このメソッドをコールしているメソッドも非同期にする必要があります。

例えば、"MainWindow.xaml.cs"ファイル内、MainWindowクラスのメソッド"CopyButton_Click"は以下のようになります。

private async void CopyButton_Click(object sender, RoutedEventArgs e)
{
    CopyButton.IsEnabled = false;
    var rc = fileList.CopyFiles();
    if (rc < 0)
    {
        await this.ShowMessageAsync("エラー", fileList.Message);
    }
    else
    {
        await this.ShowMessageAsync("コピー", "コピーしました。");
    }
    CopyButton.IsEnabled = true;
}

メソッドの宣言部分にasyncを追加している事に注意して下さい。

表示されるダイアログボックスは以下のようになります。

MahApps.Metroを使用したダイアログ表示

メインウィンドウの幅一杯に表示され、少々イメージと変わっています。
所謂、Modern UIと呼ばれるデザインですね。

ついでにMainWindowクラスの"FromButton_Click"メソッド中のMessageBox.Showメソッドも変えておきます。

private async void FromButton_Click(object sender, RoutedEventArgs e)
{
    using (var openFolderDialog = new CommonOpenFileDialog()
    {
        Title = "コピー元フォルダを選択してください",
        IsFolderPicker = true,
    })
    {
        if (openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
        {
            var sourceDir = openFolderDialog.FileName;
            int rc = fileList.SetSourceDir(sourceDir);
            if (rc < 0)
            {
                await this.ShowMessageAsync("エラー", fileList.Message);
            }
            else
            {
                FromTextBox.Text = sourceDir;
                CopyButton.IsEnabled = (rc > 0);
            }
        }
    }
}

まとめ

これで今回のWPFアプリケーションのデザインの修正については終わりにしたいと思います。

一応、修正前のデザインと比べ、それなりの見栄えになったかと思います。

とは云えMahApps.MetroにもMaterialDesignThemesにも上記で使用した機能以外にも様々な機能がてんこ盛りです。
使い熟せるようになればアプリケーションもグッと見栄えが良くなる事でしょう。

最終的なコードはこちらを参照してください。

実践!Windowsアプリを作る

実践!Windowsアプリを作る

最近、色々なところでファイル名を一括して変えたいと云う要望を見かけます。
細かい仕様が違っているため全ての要望に応えられるアプリケーションと云うのは難しいですが、とりあえず基本的な機能を供えたアプリケーションと云う事で、指定フォルダ内のファイルを指定のファイル名でコピーするアプリケーションを作っていきたいと思います。

ユーザーインターフェースはWPFを使用します。
ユーザーインターフェースとしてはWindows FormsUWP等もありますが、今後のサポートや普及状況を鑑みてWPFを選択します。

WPFは言語としてC#VBを使用する事を前提にしていますので、使用する言語はC#とします。
C++の方が好きなんですけどね…。

なお、MicrosoftWPFを用いたアプリケーションのアーキテクチャとしてMVVMを用いる事を推奨していますが、今回は意識してMVVMに合わせる事はしません。
MVVMを厳密に適用する程の大きさのアプリケーションでは無いですし、MVVMに合わせなくてもWPFでも簡単にアプリケーションを作る事ができます。

Visual Studio

IDEは当然Visual Studioとなります。
今回はVisual Studio Community 2022を使用します。

Visual Studio 2022こちらからダウンロードできます。
なお、同じページにVisual Studio Codeと云うIDEがありますが、Visual Studio 2022とは異なりますので注意して下さい。

インストールについてはVisual Studio のインストールに詳細がありますので参照して下さい。
なお、ワークロードとしては、最低限".NET デスクトップ開発"は選択して下さい。

プロジェクト作成

早速、新しいプロジェクトを作成して行きましょう。

Visual Studioを起動すると、最初にプロジェクトを選択するためのウィンドウが表示されます。
そのウインドウで、

  1. 「新しいプロジェクトの作成」を選択
  2. 「新しいプロジェクトの作成」ウィンドウで「WPFアプリケーション」を選択し「次へ」をクリック
    なお、「C#」「Windows」「デスクトップ」を選択すると選択項目が減って選択し易くなる
  3. 「新しいプロジェクトを構成します」ウィンドウで「プロジェクト名」を指定して「次へ」をクリック
    • 「プロジェクト名」は今回"CopyFilesWithSpecifiedName"とする
    • 「場所」は適当に…
    • 今回は小さいプログラムなので「ソリューションとプロジェクトを同じディレクトリに配置する」をチェック
  4. 「追加情報」ウィンドウで「フレームワーク」に".NET 6.0″を選択し「作成」をクリック

アプリケーションのデザイン

今回作成するアプリケーションは、指定したフォルダ内にあるファイルを新しい名前を付けて指定のフォルダにコピーします。
新しいファイル名は、指定の名前に連番を付加した形式です。
なお、ファイルの拡張子については、最初の段階では変更しません。

アプリケーションのウィンドウのデザインは、とりあえず以下のようにします。

  1. Fromボタン: コピー元ファイルのフォルダを指定するためのボタン
  2. Fromテキストボックス: コピー元ファイルのフォルダを表示
  3. Toボタン: コピー先フォルダを指定するためのボタン
  4. Toテキストボックス: コピー先フォルダを表示
  5. Fromリストボックス: コピー元ファイルをリスト表示
  6. Toリストボックス: コピー先ファイル名をリスト表示
  7. Copyボタン: 実際にコピーを行うためのボタン
  8. ファイル名テキストボックス: コピー後のファイル名を指定するためのテキストボックス
  9. Closeボタン: アプリケーションを終了するためのボタン

グリッドの設定

何も配置していないウィンドウは以下のようになっています。

ここにボタン等のコントロールを配置して行きます。

その前に、コントロールの配置を制御するためグリッドコントロールを配置、設定します。

メインのグリッドの分割

先ず、メインウィンドウに最初からセットされているグリッドコントロールを、コントロールの配置を行いやすくするために分割します。

以下のように、「ドキュメントアウトライン」の[Window]下にある[Grid]をクリックして選択します。

先ず列を3分割します。

  1. 「プロパティ」の"Column Definition…"ボタン(図の赤丸)をクリック
  2. 「コレクション エディター:ColumnDefinitions」で行を3つ追加
  3. “[1]ColumnDefinition"の"Width"を"Auto"に設定

次に行を4分割します。

  1. 「プロパティ」の"Row Definition…"ボタン(図の青丸)をクリック
  2. 「コレクション エディター:RowDefinitions」で行を4つ追加
  3. “[1]RowDefinition"以外・・の"Width"を"Auto"に設定

グリッドの追加

メインのグリッドの中に更にグリッドコントロールを追加する事でコントロールの配置を簡単に行えるようにします。

追加するグリッドは以下の3つです。

  1. メインのグリッドの0行0列の位置に①Fromボタンと②Fromテキストボックスを配置するグリッド
  2. メインのグリッドの0行2列の位置に③Toボタンと④Toテキストボックスを配置するグリッド
  3. メインのグリッドの2行2列の位置に⑧ファイル名テキストボックスとそのラベル"共通ファイル名 : “を配置するグリッド

コンポーネントを横方向に2つ並べるだけなので、Gridでは無くStackPanelを使用しても問題ないように思えますが、StackPanelではコントロールの幅に合わせた配置が難しいため、今回はGridを使用します。

新しいグリッドをメインのグリッドに追加するには、「ツールボックス」の"コモンWPFコントロール"もしくは"全てのWPFコントロール"に含まれるGridコントロールを「ドキュメントアウトライン」の[Grid]の上にドラッグアンドドロップします。
今回は3つのグリッドを追加しますので、ドラッグアンドドロップを3回繰り返します。

次に各グリッドを選択し、「プロパティ」の"レイアウト"中の"Row"と"Column"について、其々、メインのグリッドの配置したい行と列の番号を設置します。
なお、"RowSpan"や"ColumnSpan"については、"1″以外が設定されている場合には"1″を設定します。

Row Column
①Fromボタンと②FromテキストボックスのGrid 0 0
③Toボタンと④ToテキストボックスのGrid 0 2
⑧ファイル名テキストボックスとそのラベル"共通ファイル名 : “のGrid 2 2

更に各グリッドのColumnDefinitionsでは2つの列を作成し、"[0]ColumnDefinition"の"Width"を"Auto"にしておきます。

リストボックスとボタンの追加

グリッドの配置の次はメインのグリッドに以下のようにリストボックスとボタンを追加します。

  1. メインのグリッドの1行0列の位置に⑤Fromリストボックス
  2. メインのグリッドの1行2列の位置に⑥Toリストボックス
  3. メインのグリッドの1行1列の位置に⑦Copyボタン
  4. メインのグリッドの3行2列の位置に⑨Closeボタン

リストボックスやボタン等のコントロールをメインのグリッドに追加するには、「ツールボックス」の"コモンWPFコントロール"もしくは"全てのWPFコントロール"に含まれるListBoxButtonコントロールを「ドキュメントアウトライン」の[Window]直下の[Grid]の上にドラッグアンドドロップします。

Fromリストボックスの設定

⑤Fromリストボックスのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で[ListBox]を選択
  2. 「プロパティ」の"名前"に"FromListBox"と記入
  3. 「プロパティ」の"Row"を"1″に、"Column"を"0″に設定
  4. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)

Toリストボックスの設定

⑥Toリストボックスのプロパティを設定していきます。
⑤Fromリストボックスと同様です。

  1. 「ドキュメントアウトライン」で[ListBox]を選択
  2. 「プロパティ」の"名前"に"ToListBox"と記入
  3. 「プロパティ」の"Row"を"1″に、"Column"を"2″に設定
  4. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)

Copyボタンの設定

⑦Copyボタンのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で[Button]を選択
  2. 「プロパティ」の"名前"に"CopyButton"と記入
  3. 「プロパティ」の"Row"を"1″に、"Column"を"1″に設定
  4. 「プロパティ」の"HorizontalAlignment"と"VerticalAlignment"で"中央揃え"を選択
  5. 「プロパティ」の"Margin"の"←"と"→"を"5″に、それ以外を"0″に設定(適当でOK)
  6. 「プロパティ」の"Content"を" ⇒ “に変更
  7. 「プロパティ」の"IsEnabled"のチェックを外す

リストボックスにファイル名が表示されていない状態では、Copyボタンは意味が無いため、最初は"IsEnabled"を"false"とし、クリックできない様にしています。

Closeボタンの設定

⑨Closeボタンのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で[Button]を選択
  2. 「プロパティ」の"名前"に"CloseButton"と記入
  3. 「プロパティ」の"Row"を"3″に、"Column"を"2″に設定
  4. 「プロパティ」の"HorizontalAlignment"で"左揃え"を、"VerticalAlignment"で"中央揃え"を選択
  5. 「プロパティ」の"Margin"の"→"と"↑"、"↓"を"5″に、"←"は"0″のままに設定(適当でOK)
  6. 「プロパティ」の"Content"を"Close"に変更

ボタンとラベル、テキストボックスの追加

メインのグリッドの0行0列に追加したグリッド内に①Fromボタンと②Fromテキストボックスを追加します。

  1. 0行0列のグリッドの0行0列の位置に①Fromボタン
  2. 0行0列のグリッドの0行1列の位置に②Fromテキストボックス

またメインのグリッドの0行2列に追加したグリッド内に③Toボタンと④Toテキストボックスを追加します。

  1. 0行1列のグリッドの0行0列の位置に③Toボタン
  2. 0行1列のグリッドの0行1列の位置に④Toテキストボックス

メインのグリッドの2行2列に追加したグリッド内に⑧ファイル名テキストボックスとそのラベル"共通ファイル名 : “を追加します。

  1. 2行2列のグリッドの0行0列の位置に"共通ファイル名 : “ラベル
  2. 2行2列のグリッドの0行1列の位置に⑧ファイル名テキストボックス

ボタンやテキストボックス等のコントロールをグリッドに追加するには、「ツールボックス」の"コモンWPFコントロール"もしくは"全てのWPFコントロール"に含まれるButtonTextBoxコントロールを「ドキュメントアウトライン」の[Window]直下の[Grid]の更に下の[Grid]上にドラッグアンドドロップします。

Fromボタンの設定

①Fromボタンのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で0行0列に配置した[Grid]内の[Button]を選択
  2. 「プロパティ」の"名前"に"FromButton"と記入
  3. 「プロパティ」の"Row"が"0″、"Column"が"0″であることを確認
  4. 「プロパティ」の"HorizontalAlignment"で"左揃え"を、"VerticalAlignment"で"中央揃え"を選択
  5. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  6. 「プロパティ」の"Content"を"From : “に変更
Fromテキストボックスの設定

②Fromテキストボックスのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で0行0列に配置した[Grid]内の[TextBox]を選択
  2. 「プロパティ」の"名前"に"FromTextBox"と記入
  3. 「プロパティ」の"Column"を"1″に変更
  4. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  5. 「プロパティ」の"IsReadOnly"をチェック
  6. 「プロパティ」の"Text"は"×"をクリックしてクリア
  7. 「プロパティ」の"Width"と"ColumnSpan"は"×"をクリックしてリセット

なおFromテキストボックスで"IsReadOnly"を"false"(編集可)とすると、入力された文字列が正しいディレクトリ名かどうかのチェック等をする必要があり、複雑になるため、今回はFromボタンでの選択のみとし、Fromテキストボックスには選択されたフォルダを表示するだけとします。

Toボタンの設定

③Toボタンのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で0行2列に配置した[Grid]内の[Button]を選択
  2. 「プロパティ」の"名前"に"ToButton"と記入
  3. 「プロパティ」の"Row"が"0″、"Column"が"0″であることを確認
  4. 「プロパティ」の"HorizontalAlignment"で"左揃え"を、"VerticalAlignment"で"中央揃え"を選択
  5. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  6. 「プロパティ」の"Content"を" To : “に変更
Toテキストボックスの設定

④Toテキストボックスのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で0行2列に配置した[Grid]内の[TextBox]を選択
  2. 「プロパティ」の"名前"に"ToTextBox"と記入
  3. 「プロパティ」の"Column"を"1″に変更
  4. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  5. 「プロパティ」の"IsReadOnly"をチェック
  6. 「プロパティ」の"Text"は"×"をクリックしてクリア
  7. 「プロパティ」の"Width"と"ColumnSpan"は"×"をクリックしてリセット

なおToテキストボックスで"IsReadOnly"を"false"(編集可)とすると、入力された文字列が正しいディレクトリ名かどうかのチェック等をする必要があり、複雑になるため、今回はToボタンでの選択のみとし、Toテキストボックスには選択されたフォルダを表示するだけとします。

ラベルの設定

“共通ファイル名 :"ラベルのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で2行2列に配置した[Grid]内の[Label]を選択
  2. 「プロパティ」の"HorizontalAlignment"で"左揃え"を、"VerticalAlignment"で"中央揃え"を選択
  3. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  4. 「プロパティ」の"Content"を"共通ファイル名 : “に変更
ファイル名テキストボックスの設定

⑧ファイル名テキストボックスのプロパティを設定していきます。

  1. 「ドキュメントアウトライン」で2行2列に配置した[Grid]内の[TextBox]を選択
  2. 「プロパティ」の"名前"に"FileNameTextBox"と記入
  3. 「プロパティ」の"Column"を"1″に変更
  4. 「プロパティ」の"Margin"を全て"5″に設定(適当でOK)
  5. 「プロパティ」の"Text"は"×"をクリックしてクリア
  6. 「プロパティ」の"Width"と"ColumnSpan"は"×"をクリックしてリセット

最初のMainWindow.xaml

最初のメインウィンドウのデザインのXAMLファイルは以下のようになります。
基本的には今までの「ドキュメントアウトライン」や「プロパティ」等での設定が反映されているはずですので、特に修正する部分は無いはずです。

<Window x:Class="CopyFilesWithSpecifiedName.MainWindow"
        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"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button x:Name="FromButton" Content="From : " HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5,5,5,5"/>
            <TextBox x:Name="FromTextBox" TextWrapping="Wrap" Grid.Column="1" Margin="5,5,5,5" IsReadOnly="True"/>
        </Grid>
        <Grid Grid.Column="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button x:Name="ToButton" Content=" To :  " HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5,5,5,5"/>
            <TextBox x:Name="ToTextBox" TextWrapping="Wrap" Grid.Column="1" Margin="5,5,5,5" IsReadOnly="True"/>
        </Grid>
        <Grid Grid.Row="2" Grid.Column="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Label Content="共通ファイル名 : " Margin="5,5,5,5" HorizontalAlignment="Left" VerticalAlignment="Center"/>
            <TextBox x:Name="FileNameTextBox" TextWrapping="Wrap" Grid.Column="1" Margin="5,5,5,5"/>
        </Grid>
        <ListBox x:Name="FromListBox" d:ItemsSource="{d:SampleData ItemCount=5}" Grid.Row="1" Margin="5,5,5,5"/>
        <ListBox x:Name="ToListBox" Grid.Row="1" Grid.Column="2" Margin="5,5,5,5" d:ItemsSource="{d:SampleData ItemCount=5}"/>
        <Button x:Name="CopyButton" Content=" ⇒ " Margin="5,0,5,0" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" Grid.Column="1" IsEnabled="False"/>
        <Button x:Name="CloseButton" Content="Close" Margin="0,5,5,5" Grid.Row="3" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center"/>
    </Grid>
</Window>

因みに<ListBox.../>タグ内のd:ItemsSource="{d:SampleData ItemCount=5}"は、デザイナーを使用してウィンドウのデザインを確認する際にダミーのデータを表示するための指定です。
実際のプログラムでは意味を持ちませんので、とりあえずはスルーします。

ファイル操作用クラス

アプリケーションの目的は、指定したファイル名でファイルをコピーする事なので、その機能を実装するクラスを新たに作成します。
今回のように小規模なアプリケーションでは、MainWindowクラスに直接実装しても然程問題では無いのですが、MVVM程では無くとも、Viewの操作とModel(Data)の操作をできる限り分離するのは良い習慣かと思います。

ファイル操作用クラスの作成

先ずファイル操作用クラスを作成します。

  1. 「ソリューションエクスプローラー」で"CopyFilesWithSpecifiedName"を右クリック
  2. ポップアップメニューで"追加"→"クラス…"を選択
  3. 「新しい項目の追加」で"名前"に"FileList"と記入し"追加"ボタンをクリック

“FileList.cs"ファイルが追加され、FileListクラスが作成されます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CopyFilesWithSpecifiedName
{
    internal class FileList
    {
    }
}

FileListクラスのフィールド/プロパティとメソッド

FileListクラスにフィールドやプロパティ、メソッドを追加していきます。

ObservableCollection<T>の追加

ListBoxクラスの"ItemsSource"プロパティにObservableCollection<T>クラスのインスタンスをセットする事で、ObservableCollection<T>のインスタンスに対する項目の追加、削除等の操作を直接リストボックスのリスト表示に反映する事ができます。

型引数Tstringを指定した場合には、リストボックスにはObservableCollection<string>のインスタンスの要素がそのまま表示されます。

型引数Tにクラスや構造体、タプル等を指定した場合にはリストボックスには、例えばObservableCollection<Tuple<string, string>>のインスタンスの要素のタプルの2つの文字列が並べて表示されます。
型引数に指定したクラスや構造体、タプルの特定のフィールドやプロパティのみを表示したい場合には、XAMLの<ListBox...>タグの子要素として<ListBox.ItemTemplate>タグ内で表示したいフィールドやプロパティを指定します。
例えばタプルの場合には、

<ListBox ...>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Item1}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

のようにします。
上記タプルのようにメンバー名が無い場合には、"Item1″, “Item2″…と云う名前を使用して指定します。

なお、<StackPanel><Grid>内に<TextBlock>を複数指定し、リストボックスに複数の項目をリストアップする事も可能です。
更に、<TextBlock>以外にも<Button><Image>等を配置する事も可能です。
詳細については検索などして調べて下さい。

今回はObservableCollection<T>の型引数にコピー元のファイル名とコピー先のファイル名をフィールドに持つクラスを指定します。
FromリストボックスとToリストボックスには、其々コピー元のファイル名のプロパティとコピー先のファイル名のプロパティを表示する事とします。

新しいクラスFileNamesObservableCollection<FileNames>のフィールド/プロパティは以下のようになります。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CopyFilesWithSpecifiedName
{
    internal class FileList
    {
        public class FileNames
        {
            public string FromFile { get; set; } = "";
            public string ToFile { get; set; } = "";
        }

        public ObservableCollection<FileNames> FileNameList { get; } = new ObservableCollection<FileNames>();
    }
}

FromリストボックスにFileNamesクラスの"FromFile"プロパティを、ToリストボックスにFileNamesクラスの"ToFile"プロパティを表示するためには、"MainWindow.xaml"ファイルの<ListBox...>タグの部分を以下のように変更します。
なお、GUIでの操作での追加は面倒なので、XAMLファイルを直接編集します。

<ListBox x:Name="FromListBox" d:ItemsSource="{d:SampleData ItemCount=5}" Grid.Row="1" Margin="5,5,5,5">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding FromFile}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
<ListBox x:Name="ToListBox" Grid.Row="1" Grid.Column="2" Margin="5,5,5,5" d:ItemsSource="{d:SampleData ItemCount=5}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding ToFile}"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

最後にMainWindowクラスのフィールドとしてFileListクラスを追加し、コンストラクタ"MainWindw"内でFromリストボックスとToリストボックスの"ItemsSource"にFileListクラスの"FileName"プロパティをセットします。

public partial class MainWindow : Window
{
    private FileList fileList= new FileList();

    public MainWindow()
    {
        InitializeComponent();

        FromListBox.ItemsSource = fileList.FileNameList;
        ToListBox.ItemsSource = fileList.FileNameList;
    }
}
コピー先フォルダのフィールド

コピー先フォルダを保持するプロパティ"TargetDir"を設定します。

internal class FileList
{
        :
        :
    public string TargetDir { get; set; } = "";
}
コピー元フォルダのフィールド

コピー元フォルダを保持するプロパティ"SourceDir"を設定します。
なお、例外発生時のエラーメッセージを保持するプロパティ"Message"も設定します。

今回、このフィールドに対するSetterはprivateとして外部からアクセスできないようにしています。
代わりに、コピー元フォルダをセットするためのメソッド"SetSourceDir"を作成します。

“SetSourceDir"メソッド内では、渡されたコピー元フォルダ内の全ファイル名を"FileNameList"にセットします。
因みに、コピー元フォルダ内のファイル名を取得する際に例外が発生する可能性がありますので、処理が成功したかどうかを戻り値として渡します。

internal class FileList
{
    public enum Code
    {
        OK,
        NG,
    }
        :
        :
    public string Message { get; private set; } = "";
    public string SourceDir { get; private set; } = "";
        :
        :
    public Code SetSourceDir(string dir)
    {
        Code result = Code.OK;

        try
        {
            var files = Directory.GetFiles(dir);
            SourceDir = dir;
            FileNameList.Clear();

            foreach (var file in files)
            {
                FileNameList.Add(new FileNames() { FromFile = file });
            }

            this.MakeToFilesList();
        }
        catch (Exception e)
        {
            Message = e.Message;
            result = Code.NG;
        }

        return result;
    }
}
  1. Directory.GetFiles(dir): 指定フォルダ内のファイル名を取得、例外が発生する可能性あり
  2. FileNameList.Clear(): 前にセットしていたファイル名はリセットする
  3. this.MakeToFilesList(): 後に追加するメソッド、コピー先ファイル名を作成
  4. Message = e.Message: 例外発生時のエラーメッセージを"Message"プロパティに追加
  5. result = Code.NG: 例外発生時にはNGを返す
共通ファイル名のフィールド

共通ファイル名を保持するフィールド"baseFileName"を設定します。
プロパティも追加しておきます。

なお、セッターについては、"baseFileName"に値をセットした後"MakeToFileList"メソッドをコールしています。

internal class FileList
{
        :
        :
    private string baseFileName = "new-file-name";
    public string BaseFileName {
        get { return baseFileName; }
        set
        {
            baseFileName = value;
            this.MakeToFilesList();
        }
    }
}

因みにコピー先ファイル名を作成するメソッドthis.MakeToFilesList()は後で追加します。

初期値は"new-file-name"としていますが他の値でも問題ありません。

なお、MainWindowクラスのコンストラクタ中で"FileNameTextBox"中の"Text"に初期値を設定しておきます。

public partial class MainWindow : Window
{
    private FileList fileList= new FileList();

    public MainWindow()
    {
        InitializeComponent();

        FromListBox.ItemsSource = fileList.FileNameList;
        ToListBox.ItemsSource = fileList.FileNameList;
        FileNameTextBox.Text = fileList.BaseFileName;    // 追加
    }
}
MakeToFilesListメソッド

“FileNameList"内にセットしたコピー元ファイル名と"baseFileName"を元に、連番を付加してコピー先ファイル名を作成します。
作成したファイル名は、FileNamesクラスの"ToFile"プロパティにセットします。
コピー先フォルダについてはファイル名に含めません。

protected void MakeToFilesList()
{
    int num = 0;
    foreach (var file in FileNameList)
    {
        var ext = Path.GetExtension(file.FromFile);
        var newFileName = $"{baseFileName}{num++:d3}{ext}";
        file.ToFile = newFileName;
    }
}
CopyFilesメソッド

本アプリケーションの肝となるメソッドです。

“FileNameList"にリストアップされたコピー元ファイル名とコピー先ファイル名、コピー先フォルダ名を使い、ファイルをコピーします。

public Code CopyFiles()
{
    Code result = Code.OK;

    var dir = (TargetDir == "") ? SourceDir : TargetDir;

    foreach (var file in FileNameList)
    {
        var targetFileName = Path.Join(dir, file.ToFile);

        try
        {
            File.Copy(file.FromFile, targetFileName);
        }
        catch (IOException)
        {
            continue;
        }
        catch (Exception e)
        {
            Message = e.Message;
            result = Code.NG;
            break;
        }
    }

    return result;
}
  1. dir = (TargetDir == "") ? SourceDir : TargetDir: コピー先フォルダが指定されていない場合にはコピー元フォルダにコピーする
  2. targetFileName = Path.Join(dir, file.ToFile): コピー先ファイル名の絶対パスを作成
  3. File.Copy(file.FromFile, targetFileName): 実際のファイルコピー、例外が発生する可能性あり

コピー先ファイルが既に存在する等の例外の場合は、そのファイルのコピーはスキップし、次のファイルのコピーを継続します。
それ以外の予期しない例外が発生した場合には、コピーを中断し、負数を返します。

イベントハンドラ

いよいよイベントハンドラを追加していきます。

リストボックスに対しては、ObservableCollection<FileNames>クラスのインスタンスに対する操作が基本的には・・・・・自動的に反映されますので、メインは各ボタンをクリックした際の処理です。
後は、共通ファイル名を変更した場合の処理が必要です。

Fromボタンのクリック

Fromボタンのクリックに対するイベントハンドラを追加します。

  1. 「ドキュメントアウトライン」や「デザイナー」で"FromButton"を選択
  2. 「プロパティ」で稲妻ボタンをクリック
  3. “Click"の入力欄をダブルクリック

“MainWindow.xaml.cs"ファイル内、MainWindowクラスに"FromButton_Click"メソッドが追加されます。

“FromButton_Click"メソッド内は以下のようにします。

private void FromButton_Click(object sender, RoutedEventArgs e)
{
    using (var openFolderDialog = new CommonOpenFileDialog()
    {
        Title = "コピー元フォルダを選択してください",
        IsFolderPicker = true,
    })
    {
        if (openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
        {
            var sourceDir = openFolderDialog.FileName;
            var rc = fileList.SetSourceDir(sourceDir);
            if (rc == FileList.Code.NG)
            {
                MessageBox.Show(fileList.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            else
            {
                FromTextBox.Text = sourceDir;
                CopyButton.IsEnabled = ((rc == FileList.Code.OK) && (fileList.FileNameList.Count > 0));
            }
        }
    }
}
  1. var openFolderDialog = new CommonOpenFileDialog(): フォルダ選択のダイアログ、IsFolderPicker = trueを指定
  2. openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok: ダイアログを表示してOKがクリックされたか確認
  3. var rc = fileList.SetSourceDir(sourceDir): コピー元フォルダ名をセットし、処理の成否を確認
  4. MessageBox.Show(fileList.Message, ...): エラーメッセージをメッセージボックスに表示
  5. FromTextBox.Text = sourceDir: ②Fromテキストボックスにコピー元フォルダ名を表示
  6. CopyButton.IsEnabled = ((rc == FileList.Code.OK) && (fileList.FileNameList.Count > 0)): ファイルが1個以上追加されていれば、⑦Copyボタンをクリックできるようにする

CommonOpenFileDialogについて

フォルダ選択ダイアログについては、従来はSystem.Windows.Forms.FolderBrowserDialogと云うクラスを用いて簡単に使うことができていました。
ただ、WPFでは基本的にデフォルトではSystem.Windows.Formsを使用できないようになっていて、別途、"COMの参照"等によりSystem.Windows.Formsを導入するように変更されています。
更に、最近では、WPFアプリケーションでSystem.Windows.Formsを使用できない場合もあるようです。

また、System.Windows.Forms.FolderBrowserDialogクラスで表示されるフォルダ選択ダイアログのデザインは評判が良くないようで、WPFで無理をして使用する必要性がありません。

他に、Microsoft.Win32.OpenFileDialogクラスによるファイル選択ダイアログを利用したフォルダ選択もできない事は無いのですが、結構、トリッキーな使い方をするため、あまり好ましくありません。

なので、今回はNuGetから"WindowsAPICodePack-Shell"と云うパッケージを使用する事にしました。

WindowsAPICodePack-Shellのインストール
  1. 「ソリューションエクスプローラー」の"CopyFilesWithSpecifiedName"を右クリック
  2. “NuGetパッケージの管理…"を選択
  3. 「NuGetパッケージマネージャー」で「参照」を選択
  4. “WindowsAPICodePack-Shell"を選択し、"インストール"をクリック

なお、インストール後やプロジェクトをコンパイルする際に"warning NU1701″の警告が出るかもしれませんが問題ありません。
“WindowsAPICodePack"の作成時の.NETのバージョンが古い事に対する警告です。

CommonOpenFileDialogでフォルダ選択

CommonOpenFileDialogクラスはダイアログを表示してファイル選択を行うためのクラスです。
ただ、"IsFolderPicker"プロパティを"true"にする事で、ファイルの代わりにフォルダを選択するダイアログとする事ができます。

つまり、Microsoft.Win32.OpenFileDialogクラスより高機能なクラスと言えます。

先ず、CommonOpenFileDialogクラスを使用する場合、usingディレクティブで"Microsoft.WindowsAPICodePack.Dialogs"を指定しておきます。

更に、他のファイル操作用APIにありがちではありますが、CommonOpenFileDialogクラスのインスタンスとして確保したメモリ等のリソースは自動的に解放してくれません。
なので、CommonOpenFileDialogクラスのインスタンスの利用を終えた後に明示的に"Dispose"メソッドを呼び出すか、usingを使用して使用範囲を抜けた際に暗黙的にリソースを解放するようにします。

因みに、今回はCommonOpenFileDialogクラスのインスタンス作成と同時にプロパティをセットするようにしています。

ダイアログの表示は"ShowDialog"メソッドで行います。
戻り値は列挙子CommonFileDialogResultの"OK"か"Cancel"です。
其々、ダイアログでクリックされたボタンに対応しています。

なお、選択されたフォルダ名は"FileName"プロパティに絶対パスとしてセットされています。

Toボタンのクリック

Toボタンのクリックに対するイベントハンドラを追加します。
Fromボタンと概ね同様の操作となります。

  1. 「ドキュメントアウトライン」や「デザイナー」で"ToButton"を選択
  2. 「プロパティ」で稲妻ボタンをクリック
  3. “Click"の入力欄をダブルクリック

“MainWindow.xaml.cs"ファイル内、MainWindowクラスに"ToButton_Click"メソッドが追加されます。

“ToButton_Click"メソッド内は以下のようにします。

private void ToButton_Click(object sender, RoutedEventArgs e)
{
    using (var openFolderDialog = new CommonOpenFileDialog()
    {
        Title = "コピー先のフォルダを選択してください",
        IsFolderPicker = true,
    })
    {
        if (openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok)
        {
            var targetDir = openFolderDialog.FileName;
            ToTextBox.Text = targetDir;
            fileList.TargetDir = targetDir;
        }
    }
}
  1. var openFolderDialog = new CommonOpenFileDialog(): フォルダ選択のダイアログ、IsFolderPicker = trueを指定
  2. openFolderDialog.ShowDialog() == CommonFileDialogResult.Ok: ダイアログを表示してOKがクリックされたか確認
  3. ToTextBox.Text = targetDir: ④Toテキストボックスにコピー先フォルダ名を表示
  4. fileList.TargetDir = targetDir: コピー先フォルダ名をセット

CommonOpenFileDialogクラスについては、CommonOpenFileDialogについてを参照して下さい。

Copyボタンのクリック

Copyボタンのクリックに対するイベントハンドラを追加します。
Fromボタンと概ね同様の操作となります。

  1. 「ドキュメントアウトライン」や「デザイナー」で"CopyButton"を選択
  2. 「プロパティ」で稲妻ボタンをクリック
  3. “Click"の入力欄をダブルクリック

“MainWindow.xaml.cs"ファイル内、MainWindowクラスに"CopyButton_Click"メソッドが追加されます。

“CopyButton_Click"メソッド内は以下のようにします。

private void CopyButton_Click(object sender, RoutedEventArgs e)
{
    CopyButton.IsEnabled = false;
    var rc = fileList.CopyFiles();
    if (rc < 0)
    {
        MessageBox.Show(fileList.Message);
    }
    else
    {
        MessageBox.Show("コピーしました。");
    }
    CopyButton.IsEnabled = true;
}
  1. CopyButton.IsEnabled = false: ファイルコピー中に再度クリックされないよう無効化
  2. var rc = fileList.CopyFiles(): 実際にファイルをコピーする

Closeボタンのクリック

Closeボタンのクリックに対するイベントハンドラを追加します。
Fromボタンと概ね同様の操作となります。

  1. 「ドキュメントアウトライン」や「デザイナー」で"CloseButton"を選択
  2. 「プロパティ」で稲妻ボタンをクリック
  3. “Click"の入力欄をダブルクリック

“MainWindow.xaml.cs"ファイル内、MainWindowクラスに"CloseButton_Click"メソッドが追加されます。

“CloseButton_Click"メソッド内は以下のようにします。

private void CloseButton_Click(object sender, RoutedEventArgs e)
{
    Application.Current.Shutdown();
}
  1. Application.Current.Shutdown(): アプリケーションを終了する

ファイル名テキストボックスへの入力

ファイル名テキストボックスへの入力に対するイベントハンドラを追加します。
これもボタン類と概ね同様の操作となります。

  1. 「ドキュメントアウトライン」や「デザイナー」で"FileNameTextBox"を選択
  2. 「プロパティ」で稲妻ボタンをクリック
  3. “TextChanged"の入力欄をダブルクリック

“MainWindow.xaml.cs"ファイル内、MainWindowクラスに"FileNameTextBox_TextChanged"メソッドが追加されます。

“FileNameTextBox_TextChanged"メソッド内は以下のようにします。

private void FileNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    fileList.BaseFileName = FileNameTextBox.Text;
    ToListBox.Items.Refresh();
}
  1. fileList.BaseFileName = FileNameTextBox.Text: テキストボックス内の文字列を"BaseFileName"プロパティにセット
  2. ToListBox.Items.Refresh(): リストボックスの内容が変更になっているのでアップデート

まとめ

以上で指定フォルダ内のファイルを指定のファイル名でコピーするアプリケーションの基本的な機能を持つプログラムが完成しました。

未だ足りない機能や使いづらい点等がありますが、後に追加、修正して行きます。

この時点でのプログラムはこちらからダウンロードできます。

実践!Windowsアプリを作る