実践!Windowsアプリを作る
第4章 おまけ

第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アプリを作る