実践!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アプリを作る

YUV⇒RGB変換

前回、AndroidでOpenGL ESを使用してYUVデータの表示を行いました。

今回はブラウザ上にYUV⇒RGB変換した画像を表示したいと思います。

現在、主要なブラウザでは、JavaScriptのAPIとしてOpenGL ESの派生規格であるWebGLをサポートしています。

今回、HTML5で追加された<canvas>要素に対し、ローカルファイルから読み込んだYUVデータをRGB変換して表示していきます。
最初は、YUV⇒RGB変換後の画像を2Dのレンダラーを使用して描画してみます。
その後、WebGLを使用した描画を行ってみます。
最後にWebGLとGLSL ESを使用し、GPUでYUV⇒RGB変換を行って画像を表示します。

なお、言語に関してはJavaScriptでも問題ないのですが、私としては型指定ができる方がしっくりと馴染むので、TypeScriptを用いたいと思います。

TypeScriptの開発環境

先ず、TypeScriptの開発環境を準備します。
詳細は他のホームページに任せますが、今回は以下の開発環境を使用します。

Visual Studio Code

IDE(開発環境)としてはVisual Studio Code(以下VSCode)を使用します。
こちらのページからダウンロードできます。

インストールについては

VSCode インストール

で検索すれば色々と出てきますのでそちらを参考にして下さい。

Live Server

現在のVSCodeでは、特に他のツール等を導入しなくても、単体でHTMLやJavaScriptの確認やデバッグを行うことができます。

一方、簡易なサーバーを立ち上げてHTMLやJavaScriptの確認やデバッグができれば、更に便利かと思います。
そんな場合に便利なのがVSCodeの拡張機能のLive Serverです。

使用方法等については、

Live Server

で検索すれば色々と出てきますのでそちらを参考にして下さい。

Node.js

TypeScriptのコンパイラー等の開発環境をVSCodeで使用する場合、Node.jsが必要となります。

WindowsではNode.jsのホームページから最新のインストーラーをダウンロードし、実行するだけでインストールできます。

その他のプラットホームでのインストールはNode.jsのダウンロードページにリンクがありますので、参照して下さい。

プロジェクト作成

開発環境が整いましたら、プロジェクトを作成して行きます。
今回はNode.jsを使用してプロジェクトを構築します。

先ず、プロジェクトを作成したいフォルダーを適当に作成して下さい。
例えば”ShowYUVwithWebGL”と云うフォルダーを作成し、プロジェクトのフォルダーとします。

VSCodeをオープンします。
“ファイル”メニューから”フォルダーを開く…“を選択し、作成したプロジェクトのフォルダーを選択します。
VSCodeの新しいウィンドウがオープンします。

TypeScriptのコンパイラーのインストール

TypeScriptのコンパイラーのインストールの方法は色々とあるようですが、今回はNode.jsの”npm”コマンドでインストールします。

VSCodeで”Ctrl-@“等でターミナルをオープンします。
ターミナルで以下のコマンドによりTypeScriptのコンパイラーをインストールします。

npm install -g typescript@latest

なお、“-g”オプションは、グローバルにインストールすることを示し、他のプロジェクトでもTypeScriptのコンパイラーを使用できるようにします。
“-g”オプションを使用しない場合は今回のプロジェクトでのみTypeScriptのコンパイラーを使用できるようになります。

TypeScriptのバージョン確認

以下のコマンドでTypeScriptのバージョンを確認できます。

tsc -v
    

もしバージョンが想定しているより低い、例えば”1.0.0”等のような場合には、デフォルトで古いバージョンのTypeScriptがインストールされている可能性があります。
環境変数の”Path”に”C:Files (x86)SDKs\1.0.x.x”等が記載されていないか確認して下さい。
もし記載がある場合には”Path”から削除してみて下さい。

tsconfig.jsonの作成と修正

以下のコマンドによりTypeScriptのコンパイルのデフォルトの設定ファイル”tsconfig.json”が作成されます。

tsc --init

デフォルトの設定ファイルに対しては、以下の修正、追加を行います。

targetを変更

“target”の項目を”ES6”に変更します。

"target": "ES6",
      
moduleを変更

“module”の項目を”commonjs”から”ES2015”に変更します。

"module": "ES2015",
ベースURL指定

インポート文で絶対パス指定をできるようにベースディレクトリを指定します。

"baseUrl": "./src",
        
入力及び出力ディレクトリの追加

ソース元のディレクトリとコンパイル後のファイルを出力するディレクトリを追加します。
以下の項目の行のコメントアウトを外し、値を修正します。
なお、ディレクトリ名は適宜変更してもOKです。

"rootDir": "./src",
"outDir": "./dst/js",
        

更に、此処で指定したディレクトリに対応したフォルダをプロジェクトフォルダ”ShowYUVwithWebGL”下に作成しておきます。

階層化したコンパイル対象の指定

階層化したファルダをコンパイルの対象とするため、“include”の項目を以下のように追加します。

"compilerOptions": {
    :
    :
  },
"include": ["src/**/*"]
        
JavaScriptを対象

念のため、JavaScriptファイルを対象とするように以下の項目のコメントアウトを外しておきます。

"allowJs": true,
sourceMapの追加

デバッグ用のソースマップを作成するため以下の項目のコメントアウトを外しておきます。
リリースする際には再度コメントアウトしてソースマップを作成しない様にします。

"sourceMap": true,
        

プロジェクトの初期化

“Ctrl-@”等でオープンしたターミナルで以下のコマンドを実行し、プロジェクトを初期化します。

npm init -y

作成された”package.json”については、以下のように修正を行います。

スクリプト修正

“package.json”の”scripts”の値を以下のように修正し、次に導入するwebpackのコマンドをNPMスクリプトとして登録します。

"scripts": {
  "build": "webpack",
  "build:watch": "webpack -w",
  "node_modules": "npm ci"
},
    

なお”package-lock.json”に従ってnode_modules内のパッケージを再インストールするためのコマンドも追加しています。

webpackの導入

今回、TypeScriptのコードを書くのですが、クラス毎にファイルを分割したいと思います。
ただ、コンパイル後に作成される、複数のJavaScriptのファイルをHTMLで読み込み、動作させるには、色々と面倒な手続きがあり、容易ではありません。

webpackは、TypeScriptのファイルから作成された、分割されているJavaScriptのファイルを1つに纏め、HTMLから読み込めるようにしてくれるツールです。
他にも機能はあるのですが、詳細は他のホームページに任せます。

インストールは”Ctrl-@“等でオープンしたターミナル上で以下のコマンドを使用します。

npm install --save-dev webpack webpack-cli ts-loader

webpack.config.jsの作成

以下の内容でwebpackの設定ファイル”webpack.config.js”を作成します。

const path = require('path');
module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.ts',
    },
    output: {
      path: path.join(__dirname, 'dst/js'),
      filename: 'index.js',
    },
    resolve: {
      extensions: ['.ts', '.js'],
    },
    devServer: {
        static: {
            directory: path.join(__dirname, 'dst'),
        },
        open: true,
    },
    module: {
        rules: [
            {
            test: /\.ts$/,
            loader: 'ts-loader',
            },
        ],
    },
    target: 'electron-main',
};
    

YUV⇒RGB変換

前回、JavaにOpenGLのラッパーであるJOGLを使用してYUVデータを表示しました。

今回はAndroid端末にYUVデータを表示使用と思います。

従来のAndroid OSでは元々OpenGL ES 1.0をサポートしていました。
画像の描画については他の方法よりも高速な方法でした。

ただ現状ではOpenGL ESを使用しなくても、描画はハードウェアアクセラレーションの恩恵を受けていますので、OpenGL ESの複雑なAPIで下手なプログラムを作成するよりも、通常の描画方法を採用した方が高速になる場合もあります。

現在はAndroid OSもOpenGL ES 2.0や3.0等をサポートするようになり、デフォルトでGLSLを使用したシェーダープログラムを使用するようになりました。
その結果、従来、CPU側で行っていた画像計算をGPU側で行うことができるようになり、その分、高速化できる可能性が出てきました。

以下、Android端末を使用して単にYUVファイルを読み込んで画像を表示する事から、OpenGL ESとGLSLを使用してGPUでYUV⇒RGB変換を行って画像を表示するまでをステップバイステップで説明していきます。

Androidの開発環境

先ず、Androidの開発環境を準備します。
詳細は他のホームページに任せますが、今回は以下の開発環境を使用します。

Android Studioのダウンロード / インストール

Androidの開発を行うためのIDEは、EclipseやVisual Studio等、様々な物があります。
今回は、Googleが提供するAndroid Studioを使用します。

Android Studioはこちらのダウンロードページからダウンロードして下さい。

インストールについてはこちらのインストールガイドを参照して下さい。

基本的にはダウンロードしたファイル”android-studio-XXXX.Y.Z.WW-windows.exe”をダブルクリックでインストールが開始します。

Android Studioの設定

デフォルトの設定でOKです。
但し、途中android-sdk-licenseintel-android-extra-licenseに対して"Accept"をチェックする必要があります。

エラー対応

設定後にインストールが継続しますが、最後に

Intel HAXM installation failed!

と云うエラーが発生する場合があります。

この場合、以下のようにWHPXが有効であれば問題ないのでスルーします。

WHPX

Hyper-Vによる仮想化技術です。
有効化する場合は以下のようにします

  1. “Windowsの機能の有効化または無効化”ダイアログボックスを開く
  2. “Hyper-V”をチェック
  3. “Windows ハイパーバイザープラットフォーム”をチェック

詳細についてはAndroid Emulator のハードウェア アクセラレーションを設定するを参照して下さい。

使用言語

前回Javaを使用しましたので、今回もJavaを使用します。

Googleの推奨はKotlinですし、最近のAndroidプログラミングの解説ページはKotlinでの記述が増えてきています。
ただ、KotlinはAndroidプログラミング以外で利用される割合が低いですし、JavaからKotlinへの乗り換えは意外と簡単だと云われています。

なので、Javaを採用します。

とりあえずアプリケーションを作成

先ず最初は、Android Studioでプロジェクトを作成し、表示領域とボタン類を配置していきます。

プロジェクト作成

Android Studioででのプロジェクト作成は以下のようにします。

  1. Android StudioのiconをダブルクリックWelcome to Android Studio
  2. “New Project”ボタンをクリック
  3. エディター画面が既に表示されている場合にはメニューから”File”→“New”->“New Project”を選択
  4. “New Project”ウィンドウが表示される
    New Project / Empty Activity
  5. “Empty Activity”が選択されている事を確認後”Next”ボタンをクリック
  6. プロジェクト名等を設定するウィンドウが表示される
    New Project / Setting
  7. 今回は”Name”を”ShowYUVonAndroid”とする(他の名称でもOK)
  8. “Language”は”Java”を選択
  9. “Package name”や”Minimum SDK”はデフォルトでOK
  10. “Finish”ボタンをクリック

“MainActivity.java”ファイルには以下のように空のActivityが作成されています。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
    

描画領域とボタンの作成

画像サイズの指定

今回もYUVファイルとしてCIFサイズの”akiyo_cif.yuv”を使用しますので、最初に画像サイズを指定しておきます。
以下のようにしてリソースに追加します。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / dimens.xml
  4. “Values File Name”に”dimens”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“dimens.xml”ファイルが作成される

なお、画像サイズ等の寸法を指定するリソースは”dimens.xml”に記載する事が推奨されています。
詳細はアプリのリソースの概要を参照してください。

CIFサイズの幅と高さを指定するため、“dimens.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="width">352dp</dimen>
    <dimen name="height">288dp</dimen>
</resources>
      

なお、“dimens.xml”内のリソースは必ず単位を付加します。
付加できる単位は、現状、以下の6つがサポートされています。

単位 意味
dp 密度非依存ピクセル
160dpiの画面を基準にしたピクセル
実際のピクセル数は画面密度により変化
sp スケール非依存ピクセル
dpに近いがフォントサイズ指定の際に使用
pt 72dpiの画面での1/72インチ
px ピクセル
実際の画面の画素数
mm ミリメートル
in インチ

詳細はその他のリソース→ディメンションを参照してください。

ボタン用のテキスト指定

AndroidではUI等に使用するテキストもリソースとして定義します。

左のProjectツリーペインの”app”→“res”→“values”→“strings.xml”ファイルを選択、オープンします。
以下のように”File…“ボタンと”Play”ボタンに表示するテキストを指定します。

<resources>
    <string name="app_name">ShowYUVonAndroid</string>
    <string name="play_button_text">Play</string>
    <string name="file_button_text">File...</string>
</resources>
      

レイアウトの修正

レイアウトを修正する場合には、先ず、左のProjectツリーペインから”app”→“res”→“layout”→“activity_main.xml”ファイルを選択、オープンします。

最初は"Layout Editor"が表示されるはずです。
“Layout Editor"の使い方についてはLayout Editor を使用して UI を作成する等を参照してください。

今回は右上の”View Mode”から”Code”モードを選択し、xmlコードを直接編集します。

最初はConstraintLayout内にTextViewとして”Hello World!“が表示されるようになっています。
今回は先ず、LinearLayout内にViewButtonを配置します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <View
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/play_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:enabled="false"
            android:text="@string/play_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/file_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/file_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>
      

因みにViewは仮置きです。
後で色々と変更します。

View.OnClickListenerインターフェースの追加

ボタンを配置しましたので、ボタンクリックに対する処理を実装するため、MainActivityクラスにView.OnClickListenerインターフェースを追加します。
それに伴い、onClickメソッドを追加します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.play_button).setOnClickListener(this);
        findViewById(R.id.file_button).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {

    }
}
      

なおonCreateメソッド内でボタンに対してsetOnClickListenerメソッドでリスナーを設定しておきます。

この状態で”Shift+F10”でアプリケーションを実行すると、デフォルトで作成されているデバイスエミュレーターが起動し以下のように表示されます。

最初の画面

AVD(Android Virtual Device)の作成

基本的にプログラムの実行/デバッグで使用するデバイスは、Android Studioと共にデフォルトで提供されているAVDで問題ないはずですが、以下のように新しく作成する事もできます。

  1. 右端上のタブ”DeviceManager”を選択
  2. “Device Manager”ペインの左上の”CreateDevice”ボタンをクリック
    Device Manager
  3. “Virtual Device Configuration”の”Select Hardware”ウィンドウが表示される
    Virtual Device Configulation / Select Hardweare
  4. 適当なデバイスを選択後、“Next”ボタンをクリック
    例として”Pixel 2”を選択
  5. “Virtual Device Configuration”の”System Image”ウィンドウが表示される Virtual Device Configulation / System Image
  6. 適当な”System Image”を選択して”Next”ボタンをクリック
    基本的にはデフォルトで選択されているImageでOK
  7. “Virtual Device Configuration”の”Android Virtual Device (AVD)“ウィンドウが表示される Virtual Device Configulation / Android Virtual Device
  8. 基本的にデフォルトで問題ないので”Finish”ボタンをクリック
    なお”AVD Name”は適当に変更可

以上で新しいAVDが作成されます。

なお実行/デバッグに使用するデバイスはエディター画面の右上部分で選択できます。AVDの選択

YUVファイル選択

“File…”ボタンをクリックした時にファイル選択のActivityを表示し、ファイルの選択を行えるようにします。

先ずActivityを呼び出す前にファイル選択の結果を受け取るコードを作成しておきます。
Activityから結果を受け取る為にはActivityResultLauncher<Intent>クラスのインスタンスをregisterForActivityResultで作成します。
ファイル選択の結果を受け取るコールバックメソッドはregisterForActivityResultの第2引数に渡すActivityResultCallback<ActivityResult>クラスインスタンスのonActivityResultメソッドに実装します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
    ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        Uri uri = intent.getData();
                        Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    }
                }
            });
                :
                :
}
    

とりあえず現状はToastを使用してファイルのURIを表示するようにしています。

このインスタンスを使用して、onClickメソッド中でファイル選択のActivityを呼び出すには以下のようにします。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
   @Override
    public void onClick(View view) {
        if (view.getId() == R.id.file_button) {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("application/octet-stream");
            filePickerStartForResult.launch(intent);
        }
    }
}
    
  1. ACTION_OPEN_DOCUMENTを引数にしてIntentのインスタンスを作成
  2. addCategoryメソッドにCATEGORY_OPENABLEを指定して”ファイルオープン”とする
  3. setTypeメソッドにMIMEタイプ”application/octet-stream”を指定してバイナリファイル全般を対象とする
    MIMEタイプとして”video/*“等としたいが、”yuv”はビデオのタイプとして登録されていないのでバイナリファイルで代替
  4. ActivityResultLauncher<Intent>launchインスタンスメソッドでファイル選択のActivityを開始

なおMIMEタイプについてはMIME タイプ (IANA メディアタイプ)が詳しいので、そちらを参照してください。

実際のファイル選択

この時点で実行し”File…“ボタンをクリックすると画面が”ファイル選択のActivity”に切り替わります。

“akiyo_cif.yuv”ファイルはこの時点で未だAVDに送られていません。
ファイルをAVDに送る一番簡単な方法は、AVDの上にファイルをドラッグアンドドロップする事です。

ファイル自体は”ダウンロード”以下に配置されますので、左上”≡“をクリックして”ダウンロード”を選択します。
“akiyo_cif.yuv”ファイルがあるはずですのでクリックします。

Selecty download folder ⇒ Select akiyo_cif.yuv file

ファイル選択後は元のActivityに戻り、下部ToastにURIが表示されます。

ちなみに、もう少し柔軟にADV上のファイルを出し入れしたい場合には、メニューから”View”→“Tool Windows”→“Device File Explorer”を選択します。
“Device File Explorer”ペインが表示されますので、ADV上のファイルを操作する事ができます。

YUV⇒RGB変換用クラスの作成

上記でファイルの選択を行ったので、続けてYUVファイルをオープンし、データを読み込んだ後、YUV⇒RGB変換を行うクラスを作成しておきます。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVImage
  4. クラス名として”ShowYUVImage”を入力、“Class”を選択しリターン

YUV⇒RGB変換用クラスのコンストラクタ作成

ShowYUVImageクラスが作成されますので、Contextクラスを引数とするコンストラクタを追加します。
インスタンス変数としては、画像サイズやファイルストリーム等を記載します。
なお、YUV⇒RGB変換も行いますので、“Y”, “U”, “V”, “ARGB”其々のデータを保持するインスタンス変数も追加しておきます。

public class ShowYUVImage {

    private int width;
    private int height;
    private int ySize;
    private int uvSize;

    private byte[] y;
    private byte[] u;
    private byte[] v;
    private int[] argb;

    private InputStream yuvFile;

    public ShowYUVImage(Context context) {
        int scale = context.getResources().getDisplayMetrics().densityDpi;
        int w = context.getResources().getDimensionPixelSize(R.dimen.width);
        int h = context.getResources().getDimensionPixelSize(R.dimen.height);
        width = w * 160 / scale;
        height = h * 160 / scale;
        ySize = width * height;
        uvSize = (width / 2) * (height / 2);
        y = new byte[ySize];
        u = new byte[uvSize];
        v = new byte[uvSize];
        argb = new int[ySize];
    }
}
      

なお、コンストラクタの最初の5行目、“height”の値を計算するまでは、“dp”で指定した画面サイズを”pixel”単位に変換する為の操作です。
これにより、“width”と”height”はCIFサイズ352 x 288にセットされます。

YUV⇒RGB変換用クラスでのファイルオープンとデータの読み込み

YUV⇒RGB変換用クラスShowYUVImageではファイルからYUVデータを読み込みます。
そのため、ファイルのURIを受け取ってオープンし、YUVデータを読み込むメソッドが必要となります。

まず、ファイルのURIを受け取ってオープンするメソッドとして以下を追加します。

private boolean available = false;

public boolean isAvailable() {
    return available;
}

public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
    try {
        yuvFile = contentResolver.openInputStream(uri);
        available = true;
        readYUV();    // YUVデータを読み込むメソッド
    }
    catch (Exception e) {
        Log.e("SetFile", e.toString());
        available = false;
    }
}
      

URIを指定してファイルをオープンするためには、ContentResolverクラスのopenInputStreamメソッドを使用します。
状況により例外が発生した場合にはLogCatに内容を出力してリターンします。

なお、インスタンス変数”available”はYUVデータが正常に読み込まれたことを示すフラグです。
“available”にはゲッターを作成しておきます。

YUVファイルが正常にオープンできましたら、YUVデータを読み込んでおきます。

protected void readYUV() {
    if (available) {
        try {
            yuvFile.read(y);
            yuvFile.read(u);
            int size = yuvFile.read(v);
            if (size < uvSize) {
                available = false;
            }
        } catch (IOException e) {
            Log.e("ReadFile", e.toString());
            available = false;
        }
    }
}
      

なお、“V”データを読み込んだ際にデータが足りない、もしくは読み込めなかった場合には読み込み失敗としてフラグavailableを"false"にセットします。

YUV⇒RGB変換メソッドの追加

YUVファイルからデータを読み込むことができましたら、次はYUV⇒RGB変換です。
基本的に使用する計算式は今までと同じですので、YUV⇒RGB変換(JOGL編)と概ね同じコードを使用できます。

protected int clip(int n) {
    return (n <= 0) ? 0 : ((n >= 255) ? 255 : n);
}

protected void transYUV2RGB() {
    for (int h = 0; h < height; h++) {
        int y_pos = h * width;
        int uv_pos = (h / 2) * (width / 2);

        for (int w = 0; w < width; w++) {
            int yi = y[y_pos + w] & 0x0FF;
            int ui = u[uv_pos + (w / 2)] & 0x0FF;
            int vi = v[uv_pos + (w / 2)] & 0x0FF;

            double y16 = yi - 16.0;
            double u128 = ui - 128.0;
            double v128 = vi - 128.0;

            int r = (int) ((1.164 * y16) + (0.0 * u128) + (1.596 * v128));
            int g = (int) ((1.164 * y16) + (-0.392 * u128) + (-0.813 * v128));
            int b = (int) ((1.164 * y16) + (2.017 * u128) + (0.0 * v128));
            r = clip(r);
            g = clip(g);
            b = clip(b);
            argb[y_pos + w] = (0xFF << 24) | (r << 16) | (g << 8) | b;
        }
    }
}
      

YUV⇒RGB変換クラスでのBitmapの作成

画像データをViewクラスに描画するためにはBitmap形式の画像としてViewクラスに渡す必要があります。
なので、作成したARGB形式のデータをBitmap形式に変換して返すメソッドを追加しておきます。

public Bitmap getNextBitmap() {
    if (available) {
        transYUV2RGB();
        Bitmap bitmap = Bitmap.createBitmap(argb, width, height, Bitmap.Config.ARGB_8888);
        readYUV();
        return bitmap;
    }
    return null;
}
      

ARGB形式のデータからBitmap形式の画像を作成するためにはcreateBitmapクラスメソッドを使用します。
Bitmap形式の画像作成後は次のYUVデータを読み込んでおきます。

Viewの修正

YUVファイルからデータを読み込んでBitmap形式の画像を作成できるようになりましたので、次はViewクラスを使って画像の表示を行う事にします。

Viewクラスは基本のクラスですし、描画のコードもonDrawメソッドに集約すれば良いので非常に手軽に使用できます。

Viewのサブクラスの作成

先ず、Viewのサブクラスを作成します。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVView
  4. クラス名として”ShowYUVView”を入力、“Class”を選択しリターン

新たに”ShowYUVView.java”ファイルが作成されますので、ShowYUVViewクラスがViewクラスを継承するように、クラス名の後にextends Viewを追加しておきます。

ShowYUVViewクラスへのコンストラクタの追加

ShowYUVViewクラスは”activity_main.xml”ファイル内で”View”タグを置き換えます。
そのため、Viewクラスが実装しているコンストラクタは全て実装する事が推奨されます。

なお、ShowYUVViewクラスは描画のためにShowYUVImageクラスのインスタンスを必要としますので、インスタンス変数としてコンストラクタ内で作成しておきます。

public class ShowYUVView extends View {

    private ShowYUVImage showYUVImage;

    public ShowYUVView(Context context) {
        super(context);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        showYUVImage = new ShowYUVImage(context);
    }
}
      

ShowYUVViewクラスのonDrawメソッドのオーバーライド

UIスレッドでShowYUVViewクラスのinvalidateメソッドがコールされると、onDrawメソッドがコールされます。
Viewクラスのサブクラスで描画を行うためには、ViewクラスのonDrawメソッドをオーバーライドします。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Bitmap bmp = showYUVImage.getNextBitmap();
    if (bmp != null) {
        Rect dst = new Rect(0, 0, canvas.getWidth() - 1, canvas.getHeight() - 1);
        canvas.drawBitmap(bmp, null, dst, null);
    }
    else {
        canvas.drawARGB(0xFF, 0x00, 0x00, 0xFF);
    }
}
      

最初にViewクラスのonDrawメソッドをコールしておきます。
次にBitmap形式の画像を取得し、CanvasクラスのdrawBitmapメソッドで描画を行います。
なお、Bitmap形式の画像の取得に失敗した場合には、drawARGBメソッドで画面全体を赤色に塗り潰すようにしています。

因みに描画領域を示す”dst”を作成する際にRectクラスのコンストラクタの第3引数と第4引数は幅と高さではありません
右と下の画素の座標を指定する事になっていますので、描画領域全体を指定するには、幅および高さから1を引く必要があります。

ShowYUVViewクラスのその他のメソッド

ShowYUVImageクラスのインスタンスはShowYUVViewで保持するようにしましたので、ファイルのURIのセットやYUVデータが準備できているかの問い合わせはShowYUVViewクラスを通して行う必要があります。
そのためのメソッドを追加しておきます。

    public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
        showYUVImage.setYUVFileURL(contentResolver, uri);
    }

    public boolean isAvailable() {
        return showYUVImage.isAvailable();
    }
      

ViewのサブクラスへのYUVデータの表示

以上で準備ができましたので、以下、ViewのサブクラスをUIにセットし、描画を行うようにします。

actibity_main.xmlへのShowYUVViewの追加

“activity_main.xml”をオープンし、コードを表示します。
以下のように”View”タグ部分を”com.example.showyuvonandroid.ShowYUVView”に変更します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.showyuvonandroid.ShowYUVView
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        :
        :
    </LinearLayout>

</LinearLayout>
      

因みにレイアウト用のXML中で独自に作成したクラスをタグの要素名として指定する場合には、完全修飾クラス名を指定する必要があります。

ShowYUVViewでのファイル名設定と表示

ファイル名のセットとShowYUVViewへの描画を行うため、“MainActivity.java”を修正します。
MainActivityのインスタンス変数”filePickerStartForResult”中のコールバックメソッドonActivityで以前にToastでURIを表示している部分にコードを追加します。

ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    Uri uri = intent.getData();
                    Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    ShowYUVView view = findViewById(R.id.view);    // 以下追加
                    view.setYUVFileURL(getContentResolver(), uri);
                    view.invalidate();
                    findViewById(R.id.play_button).setEnabled(true);
                }
            }
        });
      

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”ファイルを選択すると、以下のように表示されるはずです。Viewクラスによる最初の画面

なお次で動画再生をするため、最後の部分で”Play”ボタンを有効化しておきます。

動画対応

定期的に画像を更新してYUVデータを動画として表示するため、タイマーを使用します。

タイマータスクの作成

タイマーにより定期的に実行されるタスクを記述するため、先ず、TimerTaskクラスを継承するクラスを作成します。
TimerTaskのサブクラスでは、定期的に実行されるコードはrunメソッド内に記述します。
とりあえずサブクラスの名前をShowYUVTimerTaskとし、MainActivityクラスの内部クラスとして実装しておきます。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Handler handler = new Handler();
    Timer timer;
    TimerTask timerTask;
        :
        :
    protected class ShowYUVTimerTask extends TimerTask {

        @Override
        public void run() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    ShowYUVView view = findViewById(R.id.view);
                    if (view.isAvailable()) {
                        findViewById(R.id.view).invalidate();
                    }
                    else {
                        timer.cancel();
                        findViewById(R.id.file_button).setEnabled(true);
                    }
                }
            });
        }
    }
}
      

TimerTaskrunメソッドは基本的にUIスレッドとは別のスレッドでコールされます。
ですので、そのままViewでの描画等を行ってしまいますと色々と不具合を生じます。
それを避けるため、Handlerでメッセージキューにコードをポストし、UIスレッドで実行させるようにします。

Playボタンでタイマー開始

“Play”ボタンをクリックした時にタイマーを開始するコードを追加します。
MainActivityクラスのonClickメソッドを以下のように修正します。

public void onClick(View view) {
    if (view.getId() == R.id.file_button) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/octet-stream");
        filePickerStartForResult.launch(intent);
    }
    // 以下を追加
    else if (view.getId() == R.id.play_button) {
        timerTask = new ShowYUVTimerTask();
        timer = new Timer(true);
        long period = getResources().getInteger(R.integer.period);
        timer.schedule(timerTask, period, period);
        view.setEnabled(false);
        findViewById(R.id.file_button).setEnabled(false);
    }
}
      

因みに”R.integer.period”はリソースとして定義します。

タイマー間隔の指定

タイマーの間隔は66msecですが、リソースとして指定しておきます。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / constants.xml
  4. “Values File Name”に”constants”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“constants.xml”ファイルが作成される

なお、整数の定数等を指定するリソースは”constants.xml”に記載する事が決まっている訳ではないようで、“integers.xml”等でもOKです。

タイマー間隔指定するため、“constants.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="period">66</integer>
</resources>
      

タグの要素名は”integer”です。

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”を選択、“Play”ボタンをクリックすると動画が表示されます。

ここまでのコードはここからダウンロードできます。

YUV⇒RGB変換

前回、OpenGLを使用してYUVデータを表示しました。
その際、プログラム言語としてOpenGLと相性の良いC/C++C#を使用しました。

今回はOpenGLを使用してYUVデータを表示するためにプログラム言語としてJavaを使ってみようかと思います。

ただ、Javaから直接OpenGLのAPIをコールする事はできませんので、実際にはJNIを通してC/C++等のネイティブコードからOpenGLのAPIをコールする事になります。
ですのでJavaでOpenGLを使用するのは、かなりのオーバーヘッドを伴います。

また現在のJavaのGUIフレームワークではDirectXやOpenGLを使用して描画しているものが殆どですので、単純に描画を行うだけであれば態々OpenGLを使わなくても、GUIフレームワークをそのまま使用した方が良いでしょう。

JavaでOpenGLを使うメリットはGLSLを使用してGPU側でYUV⇒RGB変換等の重い計算を行わせる際に発揮されます。

以下、普通にJavaを使用してYUVファイルを読み込んで画像を表示する事からOpenGLとGLSLを使用してGPUでYUV⇒RGB変換を行って画像を表示するまでをステップバイステップで説明していきます。

Javaの開発環境

先ず、Javaの開発環境を準備します。
詳細は他の方の説明に任せますが、必要な物は以下の通りです。

JDK

Javaのプログラムを作成するためにはJDKが必要です。
最新版はOracleのページからダウンロードできます。
因みにJREとは異なりますので注意して下さい。

インストール方法についてはOracleのページに"Installation Instructions"へのリンクがありますので、そちらを参考にして下さい。

更に

JDK インストール

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

Visual Studio Code

IDE開発環境としてはVisual Studio Code(以下VSCode)を使用します。
こちらのページからダウンロードできます。

インストールについては

VSCode インストール

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

なお、JavaのIDEとしてはEclipseNetBeans等が有名ですが、今回はVSCodeを使用します。

Java向け拡張機能の追加

VSCodeでJavaの開発を行うためには、Java向けの拡張機能Java Extension Packを導入すると便利です。
と云うか必須です。

追加方法は

Java VScode 拡張機能

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

Apache Maven

今回、OpenGLのJava向けラッパーのライブラリを使用しますが、その管理をJava用プロジェクト管理ツールであるApache Mavenを使用します。

ライブラリの管理等はJava Extension Packを導入した際に同時に導入される拡張機能Maven for Javaでできるようなのですが、Apache Maven本体をインストールすると全ての機能を使用できるようになります。

ダウンロードやインストールについてはApache Mavenのホームページが詳しいので、そちらを参考にして下さい。

とりあえずウィンドウを表示してみる

プロジェクト作成

VSCodeで”コマンドパレット”(Ctrl+Shift+P)をオープンします。
コマンドパレットでは以下のように選択、入力します。

  1. “Java: Create Java Project…”を選択Javaプロジェクトの作成
  2. “Maven create from archetype”を選択Mavenを選択
  3. “maven-archetype-quickstart”を選択Maven Archetype quickstartを選択
  4. “Select version of maven-archetype-quickstart”で1.4を選択Maven Versionを選択
  5. “Input group Id of your project.”で”com.example”を入力Group IDを入力
  6. “Input artifact of your project.で”showyuv-jogl”を入力Artifact IDを入力

なお”group Id”び”artifact Id”については適当でOKです。
ターミナル上でMavenの設定が始まり、以下の問い合わせがありますのでそのままリターンして下さい。Version1 Snapshot

  1. “Define value for property ‘version’ 1.0-SNAPSHOT:”でリターン
  2. “Y: :”でリターン

描画領域とボタンを作成

メインのクラス名変更とインターフェース追加

クラス名およびファイル名がAppとなっているのでShowYUVに変更します(別の名前でもOKです)。
なおウィンドウを表示するためjavax.swing.JFrameを継承します。
因みにJavaのGUIフレームワークにはAWTSwingJavaFX等がありますが使い易さと標準で組み込まれている事を考慮してGUIフレームワークはSwingを中心に使用して行きます。

更にボタンを使用するためjava.awt.event.ActionListenerインターフェースをインプリメントしておきます。
なおActionListenerインターフェースはvoid actionPerformed(ActionEvente)メソッドが必要なのでとりあえず空のメソッドを追加しておきます。

package com.example;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
/**
 * YUVファイルを表示
 *
 */
public class ShowYUV extends JFrame implements ActionListener {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
    }
}

ShowYUVクラスのコンストラクタ作成

ShowYUVクラスのコンストラクタを作成しておきます。
コンストラクタ内ではファイルオープンボタンとプレイボタン、描画領域を追加します。
なおボタンと描画領域は他のメソッドからアクセスできるようにインスタンス変数としておきます。

public class ShowYUV extends JFrame implements ActionListener {
   static final int CIF_WIDTH = 352;
   static final int CIF_HEIGHT = 288;
   static final String TITLE = "CheckJOGL";
   static final String FILE_NAME = "File: ";
   static final String FILE_OPEN = "File...";
   static final String PLAY = "Play";
   private JPanel panel;
   private JLabel fileNameLabel;
   private JButton fileOpenButton;
   private JButton playButton;
   public ShowYUV(String title) throws HeadlessException {
       super(title);
       /* 描画領域 */
       JPanel borderPanel = new JPanel();
       borderPanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
       panel = new JPanel();
       panel.setPreferredSize(new Dimension(CIF_WIDTH, CIF_HEIGHT));
       borderPanel.add(panel);
       add(borderPanel);
       /* ボタン領域 */
       JPanel buttonPanel = new JPanel();
       buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
       buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
       fileNameLabel = new JLabel(FILE_NAME);
       fileOpenButton = new JButton(FILE_OPEN);
       playButton = new JButton(PLAY);
       playButton.setEnabled(false);
       fileOpenButton.addActionListener(this);
       playButton.addActionListener(this);
       buttonPanel.add(fileNameLabel);
       buttonPanel.add(Box.createGlue());
       buttonPanel.add(fileOpenButton);
       buttonPanel.add(Box.createHorizontalStrut(5));
       buttonPanel.add(playButton);
       add(buttonPanel, BorderLayout.SOUTH);
       pack();
       setResizable(false);
       setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       setLocationRelativeTo(null);
   }
       :
       :
}
とりあえずの描画領域の準備

描画領域はとりあえずJPanelクラスとしておきます(後で独自のクラスに置き換えます)。
なお描画領域”panel”をJPanelクラスの”borderPanel”の中に置いているのは単純に枠線を付けたいためなので枠線が必要ない場合にはJPanelを二重にする必要はありません。

描画領域のサイズは最終的にCIFサイズのYUVデータを描画するため(352, 288)にセットします。
因みにJPanelクラスのsetPreferredSizeメソッドで使用されているDimensionクラスはjava.awt.Dimensionなので注意が必要です。
他のフレームワークに同じ名前のクラスが存在するためIDEの補完機能で間違って選択されてしまって”???“となる場合があります。

ボタン類の配置

ファイルオープンボタン”fileOpenButton”とプレイボタン”playButton”の他にファイル名を表示するテキストラベル”fileNameLabel”を下部に配置します。
先ずこれらのボタン類を入れるコンテナJPanelクラスの”buttonPanel”をBoxレイアウトで準備します。
なおボタン類の周囲に余白を持たせるために”buttonPanel”に枠線を設定しておきます。
その中に左から”fileNameLabel”、“fileOpenButton”、“playButton”の順に配置して行きます。
各コンポーネントの間は適当にパディングを追加します。

コンポーネント配置後は”buttonPanel”をウィンドウの下部に配置しておきます。

メインウィンドウの諸設定

コンストラクタの最後ではウインドウの諸設定を行います。
先ずpack()メソッドでウィンドウのサイズを各コンポーネントの配置等に合わせて調整します。
次に描画するYUV画像のサイズが決まっているので”setResizable”メソッドでリサイズ出来ないようにしておきます。
更にウィンドウをクローズした時にアプリケーションを終了するように”setDefaultCloseOperation”メソッドで設定しておきます。

main関数でメインウィンドウの作成、表示設定

main関数ではShowYUVクラスのインスタンスを作成後setVisibleメソッドでウィンドウを表示します。

static final String TITLE = "ShowYUV";
public static void main(String[] args) {
    ShowYUV frame = new ShowYUV(TITLE);
    frame.setVisible(true);
}

この時点で実行すると、とりあえずのウィンドウを表示する事ができます。初期画面

ここまでのコードについてはGitHubからダウンロードできます。

YUV⇒RGB変換

前回、Direct3Dを使用してYUVデータを表示出来ましたので、次はDirectXと双璧をなすOpenGLを使用して表示してみようと思います。

OpenGLは、Khronos GroupがAPIを策定している3Dグラフィックスライブラリです。
Direct3Dが9から10、11、12と急速に進化したのと同様、OpenGLはVulkanと云う後継のライブラリが存在します。
ただ、四角形にテクスチャを貼り付ける程度の処理しか行いませんので、前回Direct3D9を使用したのと同様、今回はOpenGLを使用します。

なお、WPFやWindows Formsのコンポーネント上にOpenGLで描画するのは少々面倒なため、今回はGLFWを使用してウィンドウを表示し、そこに描画を行うようにします。

OpenGLのフレームワークとしては、他にGLUTが有名で、数多くの情報が得られますが、最近はアップデートが停止していて推奨されていません。
API互換のオープンソースであるFreeGLUTもありますので、そちらを使う手もありますが、今回はGLFWを使います。

ただ、Qt等の例外を除き、OpenGLのフレームワークは、WPFやWindows Formsのようにボタンやメニュー、テキストボックス等の便利なコンポーネントは準備されていません。
GLFWも同様で、マウスクリックやウィンドウのリサイズ等のイベントについてはイベントハンドラ等によって取り扱う事ができるようになっていますが、GUIを使用した便利なアプリをGLFWのみで作成するのは結構ハードルが高くなります。

ですので、今回はボタン等のGUIの部分についてはWPFを使い、YUVデータ表示については、GLFWで別ウィンドウを開いて、そちらに描画するようにします。

なお、OpenGL、GLFWおよびGLSL等につきましては、

等を参考にさせていただきました。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、WPFやVC++を使用しますので、インストール時に「C++ によるデスクトップ開発」や「.NETデスクトップ開発」を選択しているものとします。

もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++ & .NET

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

サンプルプログラムの作成

早速、サンプルプログラムを作成して行きます。

前回までと違い、ベースとなるプログラムとして妥当なコードが見つかりませんでしたので、ホームページ等を参考にしつつ、コードを作成して行こうと思います。

ソリューションとプロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「WPF アプリケーション」を選択します。
なお、上部でC#, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select WPF App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「OpenGLYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。
また、フレームワークは.NET 6.0 (長期的なサポート)を選択しておきます。

更に、C++のDLLのプロジェクトを追加します。

ソリューションエクスプローラ」ウィンドウでソリューションOpenGLYUVを右クリックし、「追加」から「新しいプロジェクト…」を選択します。

新しいプロジェクトの追加」画面で、「ダイナミックライブラリ (DLL)」を選択します。
なお、上部でC++, Windows, ライブラリを選択すると、選択肢が減って選びやすくなります。

Select C++ DLL

プロジェクト名を聞かれますが、適当に設定して構いません。
場所については、デフォルトでソリューションディレクトリが設定されているはずですので、そのままでOKです。

以下、サンプルコードでは、プロジェクト名を「ShowYUV」とします。

パッケージの追加

Windowsでは、OpenGLのDLLは標準でインストールされていますが、OpenGL 1.1で、非常に古いバージョンとなります。

また、GLFWはWindowsに標準でインストールされていませんので、追加でインストールする必要があります。

更に、GLEWGLMも使用しますので、それらも追加でインストールします。

何れも個別にサイトからダウンロードしてインストールする事もできますが、今回はVisual Studio 2022のNuGetパッケージからインストールします。

なお、GLFWとGLEWは、nupengl.coreパッケージをインストールすると、fleeglutと共に一緒にインストールされます。
ただ、現状、非推奨となっていますし、パスの設定を行う必要がある等、使い勝手が悪いので今回は使いません。

ですので、個別にglfwglew-2.2.0, glmをNuGetからインストールします。

なお、NuGetからのインストールは

  1. ソリューションエクスプローラー」のShowYUVプロジェクトを右クリック
  2. NuGetパッケージの管理…」を選択
  3. 参照」からパッケージを選択しインストール

する事で行います。

プロジェクトのプロパティ等

プロジェクトのコンパイルやデバッグのため、プロパティや依存関係を設定します。

OpenGLYUV

OpenGLYUVプロジェクトのプロパティについては、出力先やデバッグ設定を行います。

  1. Setting Output Dirソリューションエクスプローラー」でOpenGLYUVプロジェクトを右クリック
  2. プロパティ」を選択
  3. プロパティ画面で「ビルド」→「出力」を選択
  4. 基本出力パス」に$(SolutionDir)x64\を記入
  5. Setting Debug Nativeプロパティ画面で「デバッグ」を選択し、「デバッグ起動プロファイルUIを開く」をクリック
  6. ネイティブ コードのデバッグを有効にする」を☑チェック
  7. Projects Dipendences再度、「ソリューションエクスプローラー」でOpenGLYUVプロジェクトを右クリック
  8. ビルドの依存関係」→「プロジェクトの依存関係…」を選択
  9. プロジェクトの依存関係」ウィンドウで、「依存先」のShowYUVを☑チェックし、OKをクリック

ShowYUV

ShowYUVプロジェクトのプロパティでは、ライブラリの設定を行います。

  1. Add Opengl libソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. プロパティ」を選択
  3. ShowYUVプロパティ」ウィンドウの「構成」で全ての構成を、「プラットフォーム」でx64を選択
  4. 左ペインの「リンカー」→「入力」を選択
  5. 右ペインの「追加の依存ファイル」をクリックした後、右端の[∨]をクリックし「編集…」を選択
  6. 追加の依存ファイル」ウィンドウで、最上部の記入欄にopengl32.libを追加し、OKをクリック

GUIの作成

WPFを使用してGUIを作成します。

今回のGUIについては画像を表示する必要がありませんので、YUVファイルをオープンするためのボタンと再生ボタン、ファイル名を表示するための領域のみを配置したものとなります。

  1. OpenGLYUVプロジェクトのMainWindow.xamlのコードを開く
  2. <Grid>を2行2列に分割
  3. <Grid>の0行0列にFileOpen<Button>を配置
  4. <Grid>の0行1列にPlay<Button>を配置し、「IsEnable」をfalseに設定
  5. <Grid>の1行0列にFileName<TextBlock>を列幅2で配置し、「IsEnable」をfalseに設定
  6. OpenGLYUVプロジェクトのMainWindow.xamlのデザイナーを開く
  7. FileOpen<Button>を選択し、プロパティを表示
  8. 右上のイベントハンドラーボタンをクリックし、「Click」の項目をダブルクリック→FileOpen_Clickが挿入される
  9. Play<Button>を選択し、プロパティを表示
  10. 右上のイベントハンドラーボタンをクリックし、「Click」の項目をダブルクリック→Play_Clickが挿入される

なお、サンプルでは、ボタンのサイズを80(w)×40(h)に、テキストブロックのサイズを160(w)×20(h)にしておきます。
それに伴い、ウィンドウのサイズを200(w)×160(h)に変更しておきます。

コード

XAMLのコードは以下の通り

<Window x:Class="OpenGLYUV.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:OpenGLYUV"
        mc:Ignorable="d"
        Title="MainWindow" Height="160" Width="200" ResizeMode="NoResize">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button x:Name="FileOpen" Content="File…" Width="80" Height="40" Click="FileOpen_Click" Margin="5"/>
        <Button x:Name="Play" Content="Play" IsEnabled="False" Grid.Column="1" Width="80" Height="40" Margin="5" Click="Play_Click"/>
        <TextBlock x:Name="FileName" Text="File Name" Grid.Row="1" Grid.ColumnSpan="2" Width="160" Height="20" IsEnabled="False" TextTrimming="CharacterEllipsis"/>

    </Grid>
</Window>
      

DLLの作成

ShowYUVプロジェクトでは、OpenGLを使用してYUVデータを表示するためのDLLを作成します。

APIは2つ、

  1. YUVファイル名を指定し、ウィンドウをオープンしてファイルの最初のフレームを表示
  2. 次のフレームを表示

のみです。

クラスは4つ

  1. GLFWを使用してウィンドウを表示すると共にOpenGLの設定を行うCWindowクラス
  2. レクタングルの頂点やテクスチャ座標を設定、描画を行うCRendererクラス
  3. 指定ファイルからYUVデータを読み込み、テクスチャを作成、設定するCTextureクラス
  4. シェーダープログラムを読み込み、コンパイル、設定するCShaderクラス

です。

更に、バーテックスシェーダプログラムを記述したファイルとフラグメントシェーダプログラムを記述したファイルを作成します。

ヘッダー追加: framework.h

framework.hにDLLで使用するヘッダーを追加します。

先ず、C++の標準ライブラリ関連で、<string>, <fstream>, <stdexcept>, <mutex>, <vector>を追加しておきます。

次に、OpenGL関連で、<GL/glew.h>, <GLFW/glfw3.h>, <glm/glm.hpp>, <glm/ext.hpp>を追加しておきます。

最終的に、framework.hは以下のようになります。

#pragma once

#define WIN32_LEAN_AND_MEAN             // Windows ヘッダーからほとんど使用されていない部分を除外する
// Windows ヘッダー ファイル
#include <windows.h>

#include <string>
#include <fstream>
#include <stdexcept>
#include <mutex>
#include <vector>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/ext.hpp>
      

CWindowクラス

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCWindowを「.hファイル」にWindow.hを「.cppファイル」にWindow.cppを記入し、OKをクリック
ヘッダーファイル: Window.h

ヘッダーファイルは以下のようにします。

#pragma once

#include "Renderer.h"

/**
* CWindow
* OpenGLのGLFWでウィンドウを表示
*/
class CWindow
{
private:
    static const std::string DEF_TITLE;
    static std::mutex init;
    static int init_count;

private:
    // ウィンドウのハンドル
    GLFWwindow* m_pWindow;
    CRenderer* m_pRenderer;

public:
    /**
    * コンストラクタ
    */
    CWindow(
        UINT width = CTexture::CIF_WIDTH,
        UINT height = CTexture::CIF_HEIGHT,
        const char* pFileName = nullptr,
        const char* pTitle = DEF_TITLE.c_str());

    /**
    * デストラクタ
    */
    virtual ~CWindow();

    /**
    * 描画
    */
    virtual HRESULT Render();

protected:
    /**
    * OpenGLを初期化
    */
    virtual void InitOpenGL();

    /**
    * OpenGLを初期化
    */
    virtual void TerminateOpenGL();

    /**
    * ウィンドウサイズをセット
    */
    void SetSize(GLuint width, GLuint height);

    /**
    * ウィンドウのサイズ変更時のコールバック関数
    */
    static void Resize(GLFWwindow* const window, int width, int height);

};
        
静的変数/定数

先ず、クラス全体で使用する定数や変数をWindows.hファイル内のCWindowクラス宣言の内部で定義します。

定数としては、ウィンドウのデフォルトのタイトルとしてDEF_TITLEを定義します。
静的変数としては、OpenGLの初期化と終了処理の回数を管理する、initとinit_countを定義しておきます。

なお、静的変数や定数の初期化は、Windows.cppファイルの先頭付近で以下のようにして行います。

const std::string CWindow::DEF_TITLE = "Show YUV";
std::mutex CWindow::init;
int CWindow::init_count = 0;
          
メンバ変数

インスタンスのメンバ変数については、GLFWのウィンドウのハンドルm_pWindowとCRendererのオブジェクトm_pRendererの2つを宣言しておきます。

なお、CRendererを使用しますので、Window.hファイルの先頭付近でRenderer.hをインクルードしておきます。

コンストラクタ

コンストラクタは、引数としてウィンドウのサイズ、ファイル名、ウィンドウのタイトルを取るようにします。
其々、以下のようにデフォルト値を設定しておきます。

CWindow(
    UINT width = CTexture::CIF_WIDTH,
    UINT height = CTexture::CIF_HEIGHT,
    const char* pFileName = nullptr,
    const char* pTitle = DEF_TITLE.c_str());
        

なお、ウィンドウのサイズに使用しているCTexture::CIF_WIDTHとCTexture::CIF_HEIGHTですが、CTextureクラス内でテクスチャのサイズとして定義している定数です。
定義しているヘッダーファイルTexture.hはRenderer.hでインクルードするので、そのまま使用できています。

Window.cppでの実装は以下の通りです。

CWindow::CWindow(UINT width, UINT height, const char* pFileName, const char* pTitle)
    : m_pWindow(NULL)
    , m_pRenderer(nullptr)
{
    InitOpenGL();

    m_pWindow = glfwCreateWindow(width, height, pTitle, NULL, NULL);
    if (NULL == m_pWindow)
    {
        throw std::runtime_error("Failed to create opengl window using GLFW.");
    }
    // 現在のウィンドウを処理対象にする
    glfwMakeContextCurrent(m_pWindow);

    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);

    // GLEWを初期化する
    glewExperimental = GL_TRUE;
    if (GLEW_OK != glewInit())
    {
        TerminateOpenGL();
        throw std::runtime_error("Failed to initialize GLEW.");
    }

    // 垂直同期のタイミングを待つ
    glfwSwapInterval(1);

    // このインスタンスの this ポインタを記録しておく
    glfwSetWindowUserPointer(m_pWindow, this);

    // ウィンドウのサイズ変更時に呼び出す処理の登録
    glfwSetWindowSizeCallback(m_pWindow, Resize);

    m_pRenderer = new CRenderer(pFileName);

    // 開いたウィンドウの初期設定
    Resize(m_pWindow, width, height);
}
        

なお、InitOpenGL()メソッドとTerminateOpenGL()メソッド、Resize()メソッドは、後に追加します。

InitOpenGL()メソッドでOpenGLの初期化後、glfwCreateWindow()関数でウィンドウを作成します。

次に、glfwMakeContextCurrent()関数で作成したウィンドウを操作対象に指定します。

glClearColor()関数では、glClear()関数で描画領域をクリアする際の色を指定しています。
サンプルでは赤にしていますが、好きな色を指定してください。

その後、GLEWを初期化するため、glewInit()関数をコールします。
なお、

glewExperimental = GL_TRUE;

については、GLEWをフル活用するためのおまじないです。

glfwSwapInterval()関数は、描画を指示した後に実際の描画を行うタイミングの指定です。
何回目の垂直同期のタイミングで描画を行うかを指定するのですが、基本は1、つまり次の垂直同期のタイミングで描画を行うようにします。

glfwSetWindowUserPointer()関数については、後にクラスメソッドからオブジェクトにアクセスする際に使用するポインタを退避するためにコールしています。

glfwSetWindowSizeCallback()関数については、ウィンドウがリサイズされた際にコールされる関数を設定しています。

最後に、CRendererのオブジェクトを作成した後、Resize()メソッドで最初の描画を行っています。

デストラクタ

デストラクタでは、CRendererのオブジェクトを解放した後、glfwDestroyWindow()関数でウィンドウを破棄し、TerminateOpenGL()メソッドでOpenGLの終了処理を行っています。

CWindow::~CWindow()
{
    if (nullptr != m_pRenderer)
    {
        delete m_pRenderer;
    }
    glfwDestroyWindow(m_pWindow);
    TerminateOpenGL();
}
        
InitOpenGLメソッド

InitOpenGL()メソッドでは、glfwInit()関数でGLFWOpenGLを初期化後、glfwWindowHint()関数で使用するOpenGLのバージョンとプロファイルを設定しています。
今回は、Version 3.3, Core Profileとします。

なお、glfwInit()関数は

メインスレッドからのみコールされなければならない

ようなので、複数回のコールを適切に処理できるようにはなっていない感じです。

なので、今回はOpenGLの初期化の回数と終了処理の回数をカウントして、最初の初期化時のみglfwInit()関数をコールするようにします。
因みにmutexによるロックは、複数のスレッドから同時にアクセスされて初期化用のカウンタが壊されるのを防ぐためのものです。

void CWindow::InitOpenGL()
{
    init.lock();

    if (init_count == 0)
    {
        // GLFWを初期化する
        if (GL_FALSE == glfwInit())
        {
            init.unlock();
            throw std::runtime_error("Failed to initialize opengl using GLFW.");
        }

        // OpenGL Version 3.3 Core Profile を選択する
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
    init_count++;

    init.unlock();
}
        
TerminateOpenGLメソッド

TerminateOpenGL()メソッドでは、初期化用のカウンタinit_countをデクリメントし、0になったらglfwTerminate()関数をコールしてOpenGLの終了処理を行います。
初期化用のカウンタを使用していますので、mutexで制御しています。

void CWindow::TerminateOpenGL()
{
    init.lock();
    if (--init_count <= 0)
    {
        glfwTerminate();
    }
    init.unlock();
}
        
Renderメソッド

Render()メソッドは、YUVファイルから次のフレームデータを読み込み、テクスチャにセットした後、描画します。

先ず、CRendererのオブジェクトが存在する事を確認後、CRenderer::SetNextTexture()メソッドでテクスチャに次のフレームのデータをセットします。

その後、glClear()関数でウィンドウの描画領域をクリアした後、CRenderer::Render()メソッドで描画を行います。

描画完了後、glfwSwapBuffers()関数でバッファの入れ替えを行います。

なお、今回は4角形の板の描画しかしませんので、glClear()関数では、GL_COLOR_BUFFER_BITのみを指定しています。

一般的に複数のオブジェクトを立体的に配置するような場合にはGL_DEPTH_BUFFER_BITを、画素のマスク等で特殊効果を使用する場合には、GL_STENCIL_BUFFER_BITも追加で指定してください。

HRESULT CWindow::Render()
{
    HRESULT hr = S_FALSE;
    if (nullptr != m_pRenderer)
    {
        hr = m_pRenderer->SetNextTexture();
        if (S_OK == hr)
        {
            glClear(GL_COLOR_BUFFER_BIT);
            m_pRenderer->Render();
            glfwSwapBuffers(m_pWindow);
        }
    }
    return hr;
}
        
SetSizeメソッド

SetSize()メソッドでは、ウィンドウのフレームバッファの横幅と高さを引数にして、CRendererのオブジェクトにサイズをセット後、再描画を行います。

先ず、CRendererのオブジェクトが存在する事を確認後、CRenderer::SetSize()メソッドでフレームバッファの新しい横幅と高さをセットします。

その後は、Render()メソッドと同様、描画を行います。
ただ、テクスチャについては、前に設定したデータをそのまま使用します。

void CWindow::SetSize(GLuint width, GLuint height)
{
    if (nullptr != m_pRenderer)
    {
        m_pRenderer->SetSize(width, height);
        glClear(GL_COLOR_BUFFER_BIT);
        m_pRenderer->Render();
        glfwSwapBuffers(m_pWindow);
    }
}
        
Resizeメソッド

Resize()メソッドは、ウィンドウがリサイズされた際にコールバックされる関数のため、クラスメソッドとして宣言しています。

引数としては、コールしたGLFWwindowクラスのオブジェクトと新しいウィンドウのサイズがセットされます。
ただ、ウィンドウのサイズと実際の描画対象となるフレームバッファのサイズは異なることがあるようなので、先ずは、glfwGetFramebufferSize()関数でフレームバッファのサイズを取得します。

その後、glViewport()関数でビューポートをフレームバッファに合わせて、全体が表示されるよう変更します。

次に、CRendererのオブジェクトのサイズをセットし、再描画を行うのですが、Resize()メソッドはクラスメソッドであるため、直接、CRendererのオブジェクトを使用する事ができません。
そこで、glfwGetWindowUserPointer()関数で、コンストラクタで登録しておいたCWindowのオブジェクトを取り出し、そのオブジェクトを使用してCWindow::SetSize()メソッドをコールします。

void CWindow::Resize(GLFWwindow* const window, int width, int height)
{
    // フレームバッファのサイズを調べてフレームバッファ全体をビューポートに設定する
    int fbWidth, fbHeight;
    glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
    glViewport(0, 0, fbWidth, fbHeight);

    // windowに関連付けられているインスタンスを取得し、サイズをセット
    CWindow* const pinstance = static_cast<CWindow*>(glfwGetWindowUserPointer(window));
    if (NULL != pinstance)
    {
        pinstance->SetSize(fbWidth, fbHeight);
    }
}
        

CRendererクラス

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCRendererを「.hファイル」にRenderer.hを「.cppファイル」にRenderer.cppを記入し、OKをクリック
ヘッダーファイル: Renderer.h

ヘッダーファイルは以下のようにします。

#pragma once

#include "Shader.h"
#include "Texture.h"

/**
* CRenderer
* 実際にレンダリングを行う
*/
class CRenderer
{
private:
    static const GLfloat VERTEX[];
    static const GLfloat TEXTURE[];

private:
    GLuint m_vertexArrayObjID;
    GLuint m_vbuffer;
    GLuint m_tbuffer;
    CShader* m_pShader;
    CTexture* m_pTexture;

public:
    /**
    * コンストラクタ
    */
    CRenderer(const char* pFileName = nullptr);

    /**
    * デストラクタ
    */
    virtual ~CRenderer();

    /**
    * 描画
    */
    virtual void Render();

    /**
    * サイズセット
    */
    virtual void SetSize(GLuint width, GLuint height);

    /**
    * 次のテクスチャセット
    */
    virtual HRESULT SetNextTexture();
};
        
定数

先ず、クラス全体で使用する定数をRenderer.hファイル内のCRendererクラス宣言の内部で定義します。

定数としては、頂点データのVERTEX[]とテクスチャ座標のTEXTURE[]を定義します。
テクスチャ座標については、テクスチャ全体を4角形全体に貼り付けるように設定しています。
また、頂点データは、中心を原点にして、CIFサイズの4角形になるように設定しています。

なお、定数の初期化は、Renderer.cppファイルの先頭付近で以下のようにして行います。

const GLfloat CRenderer::TEXTURE[] =
{
    0.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
    1.0f, 1.0f,
};

const GLfloat CRenderer::VERTEX[] =
{
    -(CTexture::CIF_WIDTH / 2.0f), -(CTexture::CIF_HEIGHT / 2.0f), 0.0f,
    -(CTexture::CIF_WIDTH / 2.0f),   CTexture::CIF_HEIGHT / 2.0f,  0.0f,
      CTexture::CIF_WIDTH / 2.0f,    CTexture::CIF_HEIGHT / 2.0f,  0.0f,
      CTexture::CIF_WIDTH / 2.0f,  -(CTexture::CIF_HEIGHT / 2.0f), 0.0f,
};
          
メンバ変数

インスタンスのメンバ変数については、Vertex Array Object名をセットするm_vertexArrayObjID、頂点バッファ名をセットするm_vbuffer、テクスチャバッファ名をセットするm_tbuffer、CShaderオブジェクトm_pShader、CTextureオブジェクトm_pTextureを宣言しておきます。

なお、CShaderおよびCTextureを使用しますので、Renderer.hファイルの先頭付近でShader.hとTexture.hをインクルードしておきます。

コンストラクタ

コンストラクタは、引数としてファイル名を取るようにします。
なお、引数のファイル名のデフォルト値にはnullptrを設定しておきます。

コンストラクタ内では、Vertex Array Objectを作成、バインドした後、頂点データ用のバッファとテクスチャ座標用のバッファを作成、バインドします。
今回は、単純な4角形の頂点とテクスチャだけですので、Vertex Array Objectの作成、バインドは、ただ面倒なだけなのですが、最近のOpenGLでは頂点データ等の受け渡しにはVertex Array Objectを使用する事がデフォルトになっているようですので、とりあえずglGenVertexArrays()関数で作成し、glBindVertexArray()関数でバインドしておきます。

次に、glGenBuffers()関数でバッファを作成し、glBindBuffer()関数でバインドしておきます。

最後に、glBufferData()関数で、実際の頂点データ(VERTEX[])やテクスチャ座標(TEXTURE[])をセットしておきます。なお、今回、頂点データやテクスチャ座標は、一旦セットした後は変更しませんので、第4引数の"usage"には、GL_STATIC_DRAWをセットしています。

バッファ作成後は、CShaderとCTextureオブジェクトを作成しています。

CRenderer::CRenderer(const char* pFileName)
    : m_vertexArrayObjID(0)
    , m_vbuffer(0)
    , m_tbuffer(0)
    , m_pShader(nullptr)
    , m_pTexture(nullptr)
{
    glGenVertexArrays(1, &m_vertexArrayObjID);
    glBindVertexArray(m_vertexArrayObjID);
    glGenBuffers(1, &m_vbuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX), VERTEX, GL_STATIC_DRAW);
    glGenBuffers(1, &m_tbuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_tbuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE), TEXTURE, GL_STATIC_DRAW);
    m_pShader = new CShader();
    m_pTexture = new CTexture(pFileName);
}
        
デストラクタ

デストラクタでは、CShaderオブジェクトとCTextureオブジェクトを解放した後、コンストラクタで作成した頂点データ用のバッファとテクスチャ座標用のバッファ、Vertex Array Objectを削除しておきます。

CRenderer::~CRenderer()
{
    if (nullptr != m_pTexture)
    {
        delete m_pTexture;
    }
    if (nullptr != m_pShader)
    {
        delete m_pShader;
    }
    glDeleteBuffers(1, &m_tbuffer);
    glDeleteBuffers(1, &m_vbuffer);
    glDeleteVertexArrays(1, &m_vertexArrayObjID);
}
        
Renderメソッド

Render()メソッドでは、実際の描画を行います。

先ずは、頂点データとテクスチャ座標データをバッファを通してバーテックスシェーダープログラムの入力にセットします。

glEnableVertexAttribArray()関数で、バーテックスシェーダープログラムの入力の番号を指定してデータの割り当てができるようにします。

その後、glBindBuffer()関数でセットするバッファを指定後、glVertexAttribPointer()関数でデータのサイズや型等の指定を行います。

頂点データとテクスチャ座標データのバッファ其々に設定を行った後、glDrawArrays()関数で4角形をレンダリングします。

最後に、glDisableVertexAttribArray()関数でデータ割り当ての終了を指示します。

void CRenderer::Render()
{
    glEnableVertexAttribArray(CShader::POSITION_ID);
    glEnableVertexAttribArray(CShader::TEXTURE_ID);

    glBindBuffer(GL_ARRAY_BUFFER, m_vbuffer);
    glVertexAttribPointer(
        CShader::POSITION_ID,   // 属性0:0に特に理由はありません。しかし、シェーダ内のlayoutとあわせないといけません。
        3,                      // サイズ
        GL_FLOAT,               // タイプ
        GL_FALSE,               // 正規化?
        0,                      // ストライド
        (void*)0                // 配列バッファオフセット
    );

    glBindBuffer(GL_ARRAY_BUFFER, m_tbuffer);
    glVertexAttribPointer(
        CShader::TEXTURE_ID,      // 属性0:0に特に理由はありません。しかし、シェーダ内のlayoutとあわせないといけません。
        2,                      // サイズ
        GL_FLOAT,               // タイプ
        GL_FALSE,               // 正規化?
        0,                      // ストライド
        (void*)0                // 配列バッファオフセット
    );

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // 頂点0から始まります。合計4つの頂点です。

    glDisableVertexAttribArray(CShader::TEXTURE_ID);
    glDisableVertexAttribArray(CShader::POSITION_ID);
}
        
SetSizeメソッド

SetSize()メソッドでは、フレームバッファのサイズを受け取り、アスペクト比から視野空間(View Volume)の設定を行います。

CIFサイズの4角形をCIFのアスペクト比を維持しながら、フレーム一杯に表示するように、高さもしくは幅を拡縮し、CShader::SetTransformationMatrix()メソッドを使ってシェーダーの変換行列に設定します。

void CRenderer::SetSize(GLuint width, GLuint height)
{
    if (nullptr != m_pShader)
    {
        double tempH = static_cast<double>(CTexture::CIF_WIDTH) * height / width;
        double tempW;
        if (tempH > CTexture::CIF_HEIGHT)
        {
            tempW = static_cast<double>(CTexture::CIF_WIDTH);
        }
        else
        {
            tempH = static_cast<double>(CTexture::CIF_HEIGHT);
            tempW = static_cast<double>(CTexture::CIF_HEIGHT) * width / height;
        }
        m_pShader->SetTransformationMatrix(-tempW / 2, tempW / 2, -tempH / 2, tempH / 2);
    }
}
        
SetNextTextureメソッド

SetNexttexture()メソッドでは、CTextureオブジェクトのチェックを行った後、CTexture::SetNextFrame()メソッドをコールします。

HRESULT CRenderer::SetNextTexture()
{
    HRESULT hr = S_FALSE;
    if (nullptr != m_pTexture)
    {
        hr = m_pTexture->SetNextFrame();
    }
    return hr;
}
        

シェーダープログラム

CShaderクラスの説明の前に、バーテックス及びフラグメントシェーダープログラムについて見て行きます。

シェーダープログラムファイルは、Direct3DにおけるHLSLで記述するエフェクトファイルと同様、OpenGLにおけるGLSLで記述するファイルです。

前回のDirect3Dの場合には、テクスチャにセットされたY, U, VデータをRGB変換して、張り付けるプログラムをHSLSで記述しました。
今回は、OpenGLのGLSLで同様のプログラムをグラフィックスパイプライン中のバーテックスシェーダーとフラグメントシェーダーに対して行います。

なお、通常、この様な簡単な処理だけであれば、この二つのシェーダーのみをプログラムすれば事足ります。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「新しい項目…」を選択
  3. 新しい項目の追加」ウィンドウで、「名前」にShader.vertと記入し、OKをクリック
  4. 同様にして、Shader.fragファイルも作成しておきます。

基本的にファイル名は何でもOKですが、今回はバーテックスシェーダープログラムをShader.vert、フラグメントシェーダプログラムをShader.fragとします。

出力先変更

なお、Shader.vertおよびShader.fragはC++のプログラム中で実行時にコンパイルするため、Visual Studioではコンパイルしないように設定しなければなりません。

また、実行時にはDLLと同じフォルダ、もしくはC++プログラム内で指定するフォルダにコピーしておく必要があるため、以下のように、コンパイルの代わりにファイルをコピーするようにします。

  1. Setting File Copyソリューションエクスプローラー」からShader.vertを右クリックし、「プロパティ」を選択
  2. Shader.vert プロパティページ」ウィンドウの「構成」で全ての構成を選択
  3. ビルドから除外」は空欄とする
  4. 項目の種類」でファイルをコピーするを選択し、「適用」ボタンをクリック
  5. Destination左ペインの「構成プロパティ」に「ファイルをコピーする」が追加されますので「全般」をクリック
  6. 移動先のディレクトリ」の右側をクリックし、<編集…>を選択
  7. 移動先のディレクトリ」ウィンドウで$(OutDir)net6.0-windows\を記入
  8. 親またはプロジェクトの規定値から継承」の☑チェックを外し、OKボタンをクリック
  9. 「Shader.vert プロパティページ」ウィンドウでOKボタンをクリック

同様にShader.fragファイルについてもファイルをコピーする設定を行います。

Shader.vert

バーテックスシェーダープログラムは、頂点データ等の処理を行います。

#version 330 core

in vec3 position;
in vec2 vertexUV;
out vec2 uv;
uniform mat4 modelViewProj;

void main()
{
    vec4 v = vec4(position, 1);
    gl_Position = modelViewProj * v;
    uv = vertexUV;
}
        
バージョン

先ず、先頭で

#version バージョン

の形式でバージョンを指定します。

バージョン指定は、必ず最初の行に記述する必要があります。

なお、使用するOpenGLのバージョンとGLSLのバージョンは合わせるようにします。
例えば、OpenGL3.3 Core Profileであれば、GLSLは"#version 330 core"とします。

外部変数

外部変数は、データの入出力を行うために使用します。

形式は以下の通りです。

修飾子 型 変数名

修飾子は、const, uniform, in, out, inoutで変数の属性を示します。
型については、float, vec4, mat4等の変数の型を示します。
変数名は、適当な名前を指定します。

今回は、OpenGLから頂点データを渡す為に3次元ベクトルとしてpositionを、テクスチャ座標データをを渡すために2次元ベクトルとしてvertexUVを指定します。
なお、バーテックスシェーダープログラムでOpenGLから渡される頂点データやテクスチャ座標データの変数はin修飾子を指定します。

次に、テクスチャ座標データを次のステージ、今回はフラグメントシェーダ―に渡すための変数uvを指定します。
次のステージに渡す変数には、out修飾子を指定します。

最後に、OpenGLから渡される変換行列としてmodelViewProj変数を指定します。
頂点データやテクスチャ座標データ以外でOpenGLから渡すデータの変数については、uniform修飾子を指定します。

メイン関数

メイン関数内では、頂点データにw要素として1を付加して4次元ベクトルとした後、変換行列を掛けます。

結果は、gl_Positionに代入します。
なお、gl_Positionは予め設定された変数で、バーテックスシェーダープログラムの頂点データの出力をセットする事になっています。

また、テクスチャ座標データについては、何もせずにそのまま出力に渡しています。

Shader.frag

フラグメントシェーダープログラムは、頂点で囲まれた領域の各画素の処理を行います。
基本的にテクスチャの貼り付け等もフラグメントシェーダで行います。

今回は、YUVデータをテクスチャとして渡して、RGBデータに変換後、色データとしてセットします。

#version 330 core

const mat4 TORGB = mat4(
    1.164f,  1.164f, 1.164f, 0.0f,
    0.0f,   -0.392f, 2.017f, 0.0f,
    1.596f, -0.813f, 0.0f,   0.0f,
    0.0f,    0.0f,   0.0f,   1.0f);
const vec4 DIFF = vec4(16.0f / 255, 128.0f / 255, 128.0f / 255, 0.0f);

in vec2 uv;
out vec4 color;

uniform sampler2D textureSamplerY;
uniform sampler2D textureSamplerU;
uniform sampler2D textureSamplerV;

void main()
{
    vec4 fy = texture(textureSamplerY, uv);
    vec4 fu = texture(textureSamplerU, uv);
    vec4 fv = texture(textureSamplerV, uv);
    vec4 yuv = vec4(fy.r, fu.r, fv.r, 1.0f);

    yuv -= DIFF;
    vec4 rgb = TORGB * yuv;
    color = clamp(rgb, 0.0f, 1.0f);
}
        
バージョン

バーテックスシェーダープログラムと同じです。

外部変数

基本はバーテックスシェーダープログラムと同じです。

先ず、YUV⇒RGB変換のための4×4の行列TORGBを定数としてセットします。
DirectXのHLSLでも同様ですが、行列の並びは、特別な設定を行わない限り、

{{1列目}, {2列目}, {3列目}, {4列目}}

と列方向の値を並べるのがデフォルトとなっています。
ですので、サンプルのプログラムのように4×4の形で記載すると、実際の行列を転置したような形となります。

次に、YUVデータを変換行列と掛ける前に、YUVデータをシフトする必要がありますので、そのシフト量DIFFを定数としてセットします。

TORGBDIFFも定数ですので、修飾子としてconstを指定します。

更に、バーテックスシェーダーから渡されるテクスチャ座標データuvを2次元のベクトルとして指定します。
バーテックスシェーダーから渡されるデータの変数については、in修飾子を指定します。

また、次のステージに渡す、画素の色データcolorを4次元ベクトルとして指定します。
次のステージに渡す変数なので、out修飾子を指定します。

最後に、テクスチャにアクセスするための変数、textureSamplerY, textureSamplerU, textureSamplerVをsampler2Dとして指定します。
sampler2Dについては、2次元のテクスチャにアクセスするための変数の型です。
今回は、テクスチャとしてY, U, Vの3つのテクスチャをセットしますので、3つ用意する必要があります。
sampler2Dの値もOpenGLから設定思案すので、uniform修飾子を指定しておきます。

メイン関数

メイン関数内では、texture()関数を使用して、textureSamplerY, textureSamplerU, textureSamplerVのsampler2Dにバインドされたテクスチャとテクスチャ座標から、Y, U, Vのデータを取得します。
取得したY, U, Vデータ、fy, fu, fvは、4次元ベクトルで、其々の要素は0.0~1.0に規格化されています。

次にY, U, Vデータのr要素とα値1.0から4次元ベクトルを作成し、シフトを行います。

得られた4次元ベクトルとYUV⇒RGB変換行列を掛け合わせて、RGBAの4次元ベクトルを取得します。
なお、4×4の行列と掛け合わせる4次元ベクトルが行ベクトルか列ベクトルかは、自動で切り替わるようなので、掛け合わせる順番は

TORGB * yuv

でも

yuv * TORGB

でも問題ないようです。

結果は、0.0~1.0にクランプし、colorに代入して出力とします。

以前のバージョンのGLSLでは、フラグメントシェーダの出力は予め設定された変数、gl_FragColorに代入する必要がありましたが、新しいバージョンのGLSLでは、gl_FragColorは使用できなくなり、out修飾子の変数に出力するようになっています。

CShaderクラス

シェーダーファイルを作成できましたので、それらを読み込み、コンパイルしてOpenGLにセットするためのCShaderクラスを作成して行きます。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCShaderを「.hファイル」にShader.hを「.cppファイル」にShader.cppを記入し、OKをクリック
ヘッダーファイル: Shader.h

ヘッダーファイルは以下のようにします。

#pragma once

/**
* CShader
* シェーダープログラムを読み込んで設定
*/
class CShader
{
public:
    static const GLuint POSITION_ID = 0;
    static const GLuint TEXTURE_ID  = 1;

private:
    static const std::string VERTEX_FILE;
    static const std::string FRAGMENT_FILE;

private:
    GLuint m_programID;
    GLuint m_vertexID;      // バーテックスシェーダの番号
    GLuint m_fragmentID;    // フラグメントシェーダの番号

public:
    CShader();
    virtual ~CShader();
    void SetTransformationMatrix(double left, double right, double bottom, double top);

protected:
    void DeleteAllObjects();
    void ReadProgram(GLuint shaderID, const std::string& file_name);
    void SetTextureSampler();
};
        
定数

先ず、クラス全体で使用する定数をShader.hファイル内のCShaderクラス宣言の内部で定義します。

定数としては、バーテックスシェーダープログラム内のグローバル変数、positionvertexUVに割り当てる番号として、POSITION_IDTEXTURE_IDを定義します。

更に、シェーダープログラムのファイル名をVERTEX_FILEFRAGMENT_FILEとして定義しておきます。
なお、定数の初期化は、Shader.cppファイルの先頭付近で以下のようにして行います。

const std::string CShader::VERTEX_FILE = "Shader.vert";
const std::string CShader::FRAGMENT_FILE = "Shader.frag";
          
メンバ変数

インスタンスのメンバ変数については、プログラム全体に割り当てるm_programID、バーテックスシェーダープログラムに割り当てるm_vertexID、フラグメントシェーダプログラムに割り当てるm_fragmentIDを宣言しておきます。

コンストラクタ

コンストラクタでは、glCreateProgram()関数とglCreateShader()関数でプログラム全体およびバーテックスシェーダープログラム、フラグメントシェーダープログラムの作成を行います。

次に、CShader::ReadProgram()メソッドでファイルからバーテックスシェーダープログラムとフラグメントシェーダープログラムを読み込み、コンパイル、アタッチを行います。

プログラムの読み込み等に失敗した場合には、CShader::DeleteAllObjects()メソッドで作成済みのオブジェクトを解放した後、例外をスローします。

プログラムの読み込みが成功すれば、glBindAttribLocation()関数によりバーテックスシェーダープログラム内のグローバル変数positionvertexUVにアクセス用のIDをバインドします。
同時に、CShader::SetTextureSampler()メソッドでフラグメントシェーダープログラム内のtextureSampler型のグローバル変数のアクセス用IDを取得しておきます。

最後にglLinkProgram()関数とglUseProgram()関数でプログラムのリンクおよび使用開始の宣言を行っておきます。

CShader::CShader()
    : m_programID(0)
    , m_vertexID(0)
    , m_fragmentID(0)
{
    m_programID = glCreateProgram();
    if (0 == m_programID)
    {
        throw std::runtime_error("Failed to create new program.");
    }

    m_vertexID = glCreateShader(GL_VERTEX_SHADER);
    if (0 == m_vertexID)
    {
        DeleteAllObjects();
        throw std::runtime_error("Failed to create new vertex shader.");
    }
    try
    {
        ReadProgram(m_vertexID, VERTEX_FILE);
    }
    catch (std::runtime_error e)
    {
        DeleteAllObjects();
        throw;
    }

    m_fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
    if (0 == m_fragmentID)
    {
        DeleteAllObjects();
        throw std::runtime_error("Failed to create new fragment shader.");
    }
    try
    {
        ReadProgram(m_fragmentID, FRAGMENT_FILE);
    }
    catch (std::runtime_error e)
    {
        DeleteAllObjects();
        throw;
    }

    glBindAttribLocation(m_programID, POSITION_ID, "position");
    glBindAttribLocation(m_programID, TEXTURE_ID, "vertexUV");

    glLinkProgram(m_programID);

    glUseProgram(m_programID);

    SetTextureSampler();
}
        
デストラクタ

デストラクタでは、コンストラクタで作成したオブジェクトをCShader::DeleteAllObjects()メソッドで削除します。

CShader::~CShader()
{
    DeleteAllObjects();
}
        
SetTransformationMatrixメソッド

SetTransformationMatrix()メソッドでは、引数として渡された視野空間(View Volume)の左右上下の座標を元に、ModelViewProjection変換用の行列を作成して、バーテックスシェーダープログラム内の変換行列のグローバル変数に渡します。

void CShader::SetTransformationMatrix(double left, double right, double bottom, double top)
{
    glm::mat4 Projection = glm::ortho(left, right, bottom, top);
    glm::mat4 View = glm::lookAt(
        glm::vec3(0, 0, 1), // ワールド空間でカメラは(4,3,3)にあります。
        glm::vec3(0, 0, 0), // 原点を見ています。
        glm::vec3(0, 1, 0)  // 頭が上方向(0,-1,0にセットすると上下逆転します。)
    );
    glm::mat4 Model = glm::mat4(1.0f);
    glm::mat4 MVP = Projection * View * Model;

    GLuint MatrixID = glGetUniformLocation(m_programID, "modelViewProj");
    glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
}
        

Direct3Dを使用したYUVデータの表示でも説明した通り、視野角があって遠くにある物ほど小さく見えるのでは都合が悪いため、投射行列には平行投影用の行列を使用します。

GLMでは、平行投影用の行列を作成する関数、glm::ortho()がありますので、それを使用します。

次に、視点であるViewですが、GLMにはカメラ位置や向き、方向を指定して行列を作成する関数、glm::lookAt()が用意されています。

最後に、物体の移動や変形等を指定するModelの行列ですが、今回は描画する四角形は移動も変形もせずにそのまま使用しますので、単位行列を使います。
GLMでは、4×4の単位行列はglm::mat4(1.0f)で作成できます。

最後に、Model, View, Projectionの行列を掛け合わせて、バーテックスシェーダープログラム内の変換行列とします。

バーテックスシェーダープログラム内の変換行列のグローバル変数modelViewProjは、uniformで宣言されていますので、glGetUniformLocation()関数により、アクセス用のIDを取得します。

バーテックスシェーダープログラム内の4×4の行列のuniform変数については、glUniformMatrix4fv()関数により、アクセス用のIDを使って値をセットします。

DeleteAllObjectsメソッド

DeleteAllObjects()メソッドについては、コンストラクタ内でプログラムにアタッチしたバーテックスシェーダープログラムとフラグメントシェーダープログラムをでタッチした後、削除します。

最後に、プログラム自体を削除します。

void CShader::DeleteAllObjects()
{
    glDetachShader(m_programID, m_fragmentID);
    glDetachShader(m_programID, m_vertexID);
    glDeleteShader(m_fragmentID);
    glDeleteShader(m_vertexID);
    glDeleteProgram(m_programID);
}
        
ReadProgramメソッド

ReadProgram()メソッドについては、ファイルからシェーダープログラムを読み込み、コンパイルした後、プログラムにアタッチします。

void CShader::ReadProgram(GLuint shaderID, const std::string& file_name)
{
    // ファイルからソースコードを読み込み
    std::ifstream sourceFile;
    sourceFile.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
    if (!sourceFile.is_open())
    {
        throw std::runtime_error("Failed to open shader file " + file_name);
    }

    size_t size = sourceFile.tellg();
    sourceFile.seekg(0, sourceFile.beg);
    if (size <= 0)
    {
        sourceFile.close();
        throw std::runtime_error("Failed to read shader file " + file_name);
    }

    std::vector<char> source(size + 1);
    char* psource = source.data();
    sourceFile.read(psource, size);
    sourceFile.close();
    *(psource + size) = '\0';

    // ソースコードをコンパイルしてプログラムにアタッチ
    glShaderSource(shaderID, 1, &psource, NULL);
    glCompileShader(shaderID);
    GLint result;
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result);
    if (GL_FALSE == result)
    {
        // コンパイル失敗ならエラーログ出力
        std::vector<char> info(256);
        GLsizei len;
        glGetShaderInfoLog(shaderID, 256, &len, info.data());
        _RPT0(_CRT_ERROR, info.data());
        throw std::runtime_error("Failed to compile shader file" + file_name);
    }

    glAttachShader(m_programID, shaderID);
}
        

先ず、ファイル名はShader.vertShader.fragを指定されていますので、全体をバイナリデータとして読み込んでしまいます。
この辺りの読み込み方法については、幾つかのやり方がありますので、基本的には、どの方法を使用してもOKです。
なお、今回、ファイル全体をバイナリデータとして読み込みましたので、終端にNUL文字’\0'を付加して文字列の終わりを明示します。
これを忘れると意味不明なコンパイルエラーで悩む事になります。

シェーダープログラムを読み込んだら、glShaderSource()関数でプログラムの文字列を指定し、glCompileShader()関数でコンパイルします。

コンパイル結果については、glGetShaderiv()関数の第2引数にGL_COMPILE_STATUSを指定して問い合わせます。
コンパイルに失敗した場合には、glGetShaderInfoLog()関数でエラーログを得る事ができます。
もし、コンパイルエラーが出た場合には、エラーログを確認してシェーダープログラムの修正を行います。

最後に、コンパイルしたシェーダープログラムをglAttachShader()関数でアタッチします。

SetTextureSamplerメソッド

SetTextureSampler()メソッドでは、先ず、フラグメントシェーダープログラム内のtextureSamplerのアクセス用IDをglGetUniformLocation()を使って取得します。

次に、glUniform1i()関数を使用し、textureSamplerのアクセス用IDを用いて其々のtextureSamplerにテクスチャのユニット番号を割り当てます。

なお、テクスチャのユニット番号とは、GL_TEXTURE0, GL_TEXTURE1, …等として定義されたテクスチャの識別番号で、GL_TEXTUREに続く番号をglUniform1i()関数でtextureSamplerのグローバル変数にセットする事で、テクスチャをフラグメントシェーダープログラムに紐付ける事ができます。
今回は、Y, U, V其々のデータ毎にテクスチャを作成しますので、3つのテクスチャの割り当てが必要となります。

void CShader::SetTextureSampler()
{
    // GLSL内のTextureSamplerにIDを割り振り
    GLint samplerIDY = glGetUniformLocation(m_programID, "textureSamplerY");
    glUniform1i(samplerIDY, 0); // GL_TEXTURE0
    GLint samplerIDU = glGetUniformLocation(m_programID, "textureSamplerU");
    glUniform1i(samplerIDU, 1); // GL_TEXTURE1
    GLint samplerIDV = glGetUniformLocation(m_programID, "textureSamplerV");
    glUniform1i(samplerIDV, 2); // GL_TEXTURE2
}
        

CTextureクラス

指定のYUVファイルからYUVデータを1フレーム毎に読み込み、テクスチャを作成します。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCTextureを「.hファイル」にTexture.hを「.cppファイル」にTexture.cppを記入し、OKをクリック
ヘッダーファイル: Texture.h

ヘッダーファイルは以下のようにします。

#pragma once

class CTexture
{
public:
    static const UINT CIF_WIDTH = 352;
    static const UINT CIF_HEIGHT = 288;
    static const std::string YUV_FILE_NAME;

private:
    std::ifstream* m_pFile;
    GLuint m_textureIDY;
    GLuint m_textureIDU;
    GLuint m_textureIDV;
    GLubyte* m_pY;
    GLubyte* m_pU;
    GLubyte* m_pV;
    GLuint m_width;
    GLuint m_height;
    GLuint m_size;

public:
    CTexture(const char* pFileName = YUV_FILE_NAME.c_str(), GLuint width = CIF_WIDTH, GLuint height = CIF_HEIGHT);
    virtual ~CTexture();
    virtual HRESULT SetNextFrame();

protected:
    void SetTextureAttribute();
    HRESULT ReadYUV();
};
        
定数

先ず、クラス全体で使用する定数をTexture.hファイル内のCTextureクラス宣言の内部で定義します。

定数としては、サンプルのYUVファイルakiyo_cif.yuvがCIFサイズですので、デフォルトの幅と高さとしてCIF_WIDTH352, CIF_HEIGHT288を定義しておきます。

また、デフォルトのYUVファイルとしてYUV_FILE_NAMEを定義しておきます。
なお、定数の初期化は、Texture.cppファイルの先頭付近で以下のようにして行います。

const std::string CTexture::YUV_FILE_NAME = "akiyo_cif.yuv";
          
メンバ変数

インスタンスのメンバ変数については、テクスチャアクセス用のIDをY, U, V其々にm_textureIDY, m_textureIDU, m_textureIDVとして宣言しておきます。

更に、ファイルから読み込んだ1フレーム分のY, U, Vのデータをストアしておくための領域用にm_pY, m_pU, m_pVを宣言しておきます。

後は、テクスチャのサイズを保持するためのm_width, m_height, m_sizeを宣言します。

コンストラクタ

コンストラクタでは、YUVファイル名とテクスチャの幅と高さを引数に取ります。

先ず、指定された幅と高さから、m_width, m_height, m_sizeをセットします。

次に、指定されたファイルをオープンしておきます。

更に、Y, U, Vデータを1フレーム分ストアできる領域を確保します。
なお、YUVデータはYUV420を想定していますので、UおよびVのデータサイズは、Yと比べて幅および高さが其々半分なので、配列のサイズとしては1/4となります。

最後に、CTexture::SetTextureAttribute()メソッドでY, U, V其々のテクスチャの作成を行い、CTexture::SetNextFrame()メソッドでテクスチャに実際のデータをセットします。

CTexture::CTexture(const char* pFileName, GLuint width, GLuint height)
    : m_pFile(nullptr)
    , m_textureIDY(0)
    , m_textureIDU(0)
    , m_textureIDV(0)
    , m_pY(nullptr)
    , m_pU(nullptr)
    , m_pV(nullptr)
    , m_width(width)
    , m_height(height)
    , m_size(width * height)
{
    m_pFile = new std::ifstream(pFileName, std::ifstream::in | std::ifstream::binary);
    if ((nullptr == m_pFile) || !m_pFile->good())
    {
        delete m_pFile;
        throw std::runtime_error("Failed to open YUV file " + YUV_FILE_NAME);
    }

    m_pY = new GLubyte[m_size];
    m_pU = new GLubyte[m_size / 4];
    m_pV = new GLubyte[m_size / 4];

    SetTextureAttribute();

    SetNextFrame();
}
        
デストラクタ

デストラクタでは、オープンしたファイルを閉じ、コンストラクタで作成したY, U, Vデータを1フレーム分ストアできる領域を解放します。

更に、glDeleteTextures()関数でテクスチャを削除します。

CTexture::~CTexture()
{
    if (nullptr != m_pFile)
    {
        m_pFile->close();
        delete m_pFile;
    }
    if (nullptr != m_pY)
    {
        delete[] m_pY;
    }
    if (nullptr != m_pU)
    {
        delete[] m_pU;
    }
    if (nullptr != m_pV)
    {
        delete[] m_pV;
    }
    if (0 != m_textureIDY)
    {
        glDeleteTextures(1, &m_textureIDY);
    }
    if (0 != m_textureIDU)
    {
        glDeleteTextures(1, &m_textureIDU);
    }
    if (0 != m_textureIDV)
    {
        glDeleteTextures(1, &m_textureIDV);
    }
}
        
SetNextFrameメソッド

SetNextFrame()メソッドでは、先ず、CTexture::ReadYUV()メソッドで1フレーム分のYUVデータを読み込みます。

データの読み込みが成功したら、テクスチャのユニット番号を指定してglActiveTexture()関数を呼び出す事でフラグメントシェーダープログラム中のtextureSampler型のグローバル変数をアクティベートします。

glBindTexture()関数で指定のIDのテクスチャをバインドし、glTexImage2D()で実際のデータ、m_pY, m_pU, m_pVをセットします。

HRESULT CTexture::SetNextFrame()
{
    HRESULT hr = ReadYUV();
    if (S_OK == hr)
    {
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, m_textureIDY);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width, m_height, 0, GL_RED, GL_UNSIGNED_BYTE, m_pY);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, m_textureIDU);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width / 2, m_height / 2, 0, GL_RED, GL_UNSIGNED_BYTE, m_pU);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, m_textureIDV);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width / 2, m_height / 2, 0, GL_RED, GL_UNSIGNED_BYTE, m_pV);
    }
    return hr;
}
        
SetTextureAttributeメソッド

SetTextureAttribute()メソッドでは、各テクスチャを作成し、属性を設定しています。

先ず、glGenTextures()関数でテクスチャを作成し、IDを取得します。

次に、glBindTexture()関数で作成したテクスチャをバインドし、それに続く関数が適用されるテクスチャを指定します。

glPixelStorei()関数では、m_pY, m_pU, m_pVがバイト単位のデータですので、メモリアライメントを1に指定しています。

また、glTexParameteri()関数では、テクスチャを拡大、縮小する際に用いるフィルターを線形フィルターに指定しています。

void CTexture::SetTextureAttribute()
{
    glGenTextures(1, &m_textureIDY);
    glBindTexture(GL_TEXTURE_2D, m_textureIDY);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glGenTextures(1, &m_textureIDU);
    glBindTexture(GL_TEXTURE_2D, m_textureIDU);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glGenTextures(1, &m_textureIDV);
    glBindTexture(GL_TEXTURE_2D, m_textureIDV);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
        
ReadYUVメソッド

ReadYUV()メソッドでは、単純に、1フレーム分のYUVデータをファイルから読み込んでいます。

HRESULT CTexture::ReadYUV()
{
    HRESULT hr = S_FALSE;

    if ((nullptr != m_pFile) && (m_pFile->good()))
    {
        if (!m_pFile->eof())
        {
            m_pFile->read(reinterpret_cast<char*>(m_pY), m_size);
            m_pFile->read(reinterpret_cast<char*>(m_pU), m_size / 4);
            m_pFile->read(reinterpret_cast<char*>(m_pV), m_size / 4);
            hr = S_OK;
        }
    }

    return hr;
}
        

dllmain.cpp

DLLのAPIを作成していきます。
既にdllmain.cppは作成されていますので、DLLMain()関数以外のコードを追加していきます。

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include "Window.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

static CWindow* pwin = NULL;

extern "C" HRESULT WINAPI CreateOpenGLWindow(const char* pFileName)
{
    HRESULT hr = S_OK;

    if (pwin != NULL)
    {
        delete pwin;
    }

    try
    {
        UINT sideLen = CTexture::CIF_WIDTH;
        pwin = new CWindow(sideLen, sideLen, pFileName);
    }
    catch (std::exception e)
    {
        _RPT0(_CRT_ERROR, e.what());
        hr = S_FALSE;
    }

    return hr;
}

extern "C" HRESULT WINAPI Render()
{
    HRESULT hr = S_FALSE;
    if (pwin != NULL)
    {
        hr = pwin->Render();
    }
    return hr;
}
      

基本的に、DLLのAPIからのアクセスは、CWindowクラス使用しますので、Window.hをインクルードしておきます。

次に、CWindowクラスのオブジェクトを保持しておくグローバル変数として、pwinを宣言しておきます。

APIとしての関数は2つです。
1つ目は、YUVファイル名を指定してYUVデータを表示するウィンドウを作成するための関数、CreateOpenGLWindow()です。
2つ目は、次のフレームを表示するための関数、Render()です。

CreateOpenGLWindow関数

引数は、YUVファイル名です。

既にウィンドウがオープンされている場合には、pwinはNULLではありませんので、解放してウィンドウを閉じておきます。

次に、ウィンドウサイズとYUVファイル名を指定して、CWindowのオブジェクトを作成します。
なお、ウィンドウサイズは、CIFサイズではなく、幅、高さ共にCIFの幅に合わせた、正方形としています。
こうする事で、画像が表示された際に上下に帯ができて、画像のクリアに使用した色の確認等ができます。
ウィンドウのサイズは、適当に設定しても問題ないはずです。

Render関数

CWindow::Render()メソッドをコールします。
YUVファイルが終わりに達した場合には、S_FAIL(1L)が返ります。

モジュール定義ファイル

DLLのAPIを定義します。

  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「新しい項目…」を選択
  3. 新しい項目の追加」ウィンドウの左ペインの「Visual C++」→「コード」を選択
  4. 中央ペインの、モジュール定義ファイル(.def)を選択
  5. 名前」にShowYUVと記入し、OKをクリック

作成されたファイルには、以下のように記入します。

LIBRARY ShowYUV.dll

EXPORTS
    CreateOpenGLWindow
    Render
      

GUIのコード作成

DLL作成後は、WPFで作成したGUIからAPIを呼び出します。

DLLの設定

MainWindow.xaml.csのMainWindowクラス内で、DLLのAPIの宣言をしておきます。

[System.Runtime.InteropServices.DllImport("../ShowYUV.dll")]
private static extern int CreateOpenGLWindow(string fileName);

[System.Runtime.InteropServices.DllImport("../ShowYUV.dll")]
private static extern int Render();
      

なお、ShowYUV.dllが作成される場所により、指定のフォルダが異なる場合がありますので、DLLの読み込みに失敗する場合には確認してください。

FileOpen<Button>への対応

FileOpen<Button>がクリックされたら、FileOpen_Click()メソッドがコールされます。

FileOpen_Click()メソッド内では、ファイルオープンダイアログを表示し、YUVファイル名を取得します。

YUVファイル名が取得できたら、DLLのAPIである、CreateOpenGLWindow()関数をコールして、ウィンドウを作成し、最初のフレームを表示します。

private void FileOpen_Click(object sender, RoutedEventArgs e)
{
    var dialog = new Microsoft.Win32.OpenFileDialog();
    dialog.Title = "YUVファイルの選択";
    dialog.Filter = "YUVァイル (*.yuv)|*.yuv";
    dialog.CheckFileExists = true;

    // ダイアログを表示する
    if (dialog.ShowDialog() == true)
    {
        FileName.Text = dialog.FileName;
        Play.IsEnabled = true;
        CreateOpenGLWindow(dialog.FileName);
    }

}
      

Play<Button>への対応

Play<Button>がクリックされたら、Play_Click()メソッドがコールされます。

Play_Click()メソッド内では、定期的にDLLのAPIであるRender()関数がコールされるように、タイマーをスタートさせます。

タイマーの設定は、予めMainWindowクラスのコンストラクタ内で行っておきます。
タイマーのイベントハンドラとしてCompositionTarget_Rendering()メソッドを設定し、間隔を33msecとしておきます。

public MainWindow()
{
    InitializeComponent();
    _playTimer = new DispatcherTimer();
    _playTimer.Tick += new EventHandler(CompositionTarget_Rendering);
    _playTimer.Interval = new TimeSpan(0, 0, 0, 0, 33);
}
      
DispatcherTimer _playTimer;
private void Play_Click(object sender, RoutedEventArgs e)
{
    Play.IsEnabled = false;
    _playTimer.Start();
}
      

イベントハンドラ

タイマーのイベントハンドラcompositionTarget_Rendering()メソッドでは、DLLのAPIであるRender()関数をコールします。
Render()関数の戻り値がS_OK(0)以外の場合には、YUVファイルが終わりに達した等のエラーが発生していますので、タイマーを止めます。

void CompositionTarget_Rendering(object? sender, EventArgs e)
{
    int rc = Render();
    if (rc != 0)
    {
        _playTimer.Stop();
    }
}
      

実行

Debug Runコンパイル、実行を行いますと、先ず、ボタンのみが配置されたウィンドウが表示されます。

Play WindowFile…ボタンをクリックすると、ファイルオープンダイアログが表示されますので、YUVデータakiyo_cif.yuvを選択します。
新たにウィンドウが表示され、画像が表示されます。

サンプルコードにつきましては、こちらからダウンロードできます。

まとめ

以上でOpenGLによってYUVファイルを表示する事ができるようになりました。

なお、Windows向けのアプリケーションを作成する場合には、OpenGLを使うよりも、DirectXを使用した方が、相性も良いですし、使い勝手も良いです。
ただ、AndroidやLinux向けのアプリケーションをターゲットにする場合には、DirectXよりもOpenGLの方が汎用性も高く、情報も得やすいと思います。
何れにしても、YUVデータを表示する際に、YUV⇒RGB変換をCPUで行うのは負担が非常に大きく、お勧めできません。

最近のCPUはSIMD命令も充実してきていますので、数枚程度の画像であれば問題ないのですが、動画のように短時間で数十フレーム以上も処理するのであれば、浮動小数点の行列/ベクトル計算に特化したGPUに任せた方が遥かに効率が良いです。

YUV⇒RGB変換

YUV⇒RGB変換

前回、YUV⇒RGB変換を行った後、Direct2Dを用いてYUVデータを動画として表示できました。
今回は、Direct3Dを使用してYUVデータを動画として表示しようかと思います。

本来、YUVデータを動画として表示する程度であれば、Direct2DとDirectXMathの組み合わせで十分と云う気がします。
とは云え、最近のグラフィックチップは3Dに特化しているものが多く、グラフィック関連の処理は3Dで行った方が自由度が高く、且つ高速に行えます。

と云う事で、Direct3Dを用いて、YUVデータを表示する事にします。

なお、今回は素のウィンドウアプリケーションではなく、WPFを使ってみようと思います。
WPFのようなフレームワークを使用できれば、ボタン等のGUIを手軽に利用できますので、アプリの開発が非常に楽になります。

ただ、WPFはC#等のマネージドコードを使用する事が前提となっているのに対し、Direct3DはC++を前提としているため、WPFで直接Direct3Dを利用する事ができません。

そのため、Managed DirectXやSlimDX、SharpDX等、マネージドコードから利用できるラッパーも提供されてはいますが、どれも開発が止まっているようで、使い勝手が良くありません。

Windows API Code Pack for Microsoft .NET Frameworkと云うマイクロソフトによる公式のマネージコード用ライブラリもあるみたいですが、扱いがNuGetに移っているようです。

ですので、今回は、基本的にはDirect3Dに係わる部分はC++で記述してDLLとして纏め、UI部分をWPFとC#で作成するようにします。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、WPFやVC++を使用しますので、インストール時に「C++ によるデスクトップ開発」や「.NETデスクトップ開発」を選択しているものとします。

もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++ & .NET

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

プログラムの作成

プロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「WPF アプリケーション」を選択します。
なお、上部でC#, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select WPF App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「WPFViewYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。
また、フレームワークは.NET 6.0 (長期的なサポート)を選択しておきます。

更に、C++のDLLのプロジェクトを追加します。

ソリューションエクスプローラ」ウィンドウでソリューションWPFViewYUVを右クリックし、「追加」から「新しいプロジェクト…」を選択します。

新しいプロジェクトの追加」画面で、「ダイナミックライブラリ (DLL)」を選択します。
なお、上部でC++, Windows, ライブラリを選択すると、選択肢が減って選びやすくなります。

Select C++ DLL

プロジェクト名を聞かれますが、適当に設定して構いません。
場所については、デフォルトでソリューションディレクトリが設定されているはずですので、そのままでOKです。

以下、サンプルコードでは、プロジェクト名を「Direct3DYUV」とします。

Direct3D9の実装

早速、Direct3Dを実装していきます。

現在、よく使われているDirect3Dは、Direct3D9~12がありますが、今回はDirect3D9を使用します。
Direct3D9については資料が豊富ですし、YUVデータを表示する程度でしたら能力的にも十分です。

DLLの作成

先ずは、MSDNで説明されている「チュートリアル: WPF でホストするための Direct3D9 コンテンツの作成」に沿って、C++のDLLを作成して行きます。

サンプルコード内では、プロジェクト名が「D3DContent」となっていますが、「Direct3DYUV」に読み替えて下さい。

前準備

チュートリアルの「Direct3D9 プロジェクトを作成するには」の1~4については、既にプロジェクトを作成していますのでスキップします。

6~9については、Windows SDK 10を使用するVisual Studio 2022では必要ありません。

11については、d3d9.libは必要ですが、d3dx9.libは必要ありません。

12については、後で説明しますので、D3DContent.defの追加は保留しておきます。

クラスの作成

とりあえずチュートリアルの「Direct3D9 コンテンツを作成するには」の1で指示されているクラス、CRenderer, CRendererManager, CTriangleRendererを作成します。

クラスの作成については、「ソリューションエクスプローラ」ウィンドウでプロジェクトDirect3DYUVを右クリックし、「追加」から「クラス…」を選択します。

作成するファイルは、Renderer.h, Renderer.cpp, RendererManager.h, RendererManager.cpp, TriangleRenderer.h, TriangleRenderer.cppとします。
なお、CTriangleRendererについては、「基底クラス」をCRendererとしておきます。

クラスを追加後、2~7のコードを其々のファイルにコピーして行きます。

なお、今回作成した「Direct3DYUV」プロジェクトではStdAfx.hを使用しません。
代わりにpch.hframework.hを使用します。

なので、.cppファイルにコードをコピーする場合、元のインクルードファイルの記述は残しつつ、

#include "StdAfx.h"

の行は削除してください。

stdafx.h

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

8. コード エディターで stdafx.h を開き、自動生成されたコードを次のコードに置き換えます。

については、今回、stdafx.hファイルを使用しないので、代わりに内容をframework.hにコピーしてください。

dllmain.cpp

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

9.コード エディターで dllmain.cpp を開き、自動生成されたコードを次のコードに置き換えます。

については、置き換えた後に

#include "StdAfx.h"

#include "pch.h"

に直しておいて下さい。

Direct3DYUV.def

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

10.コード エディターで D3DContent.def を開きます。

については、現状、D3DContent.defの作成を保留している状態ですので存在しません。
ですので、新規に作成します。

ただ、今回はプロジェクト名を「Direct3DYUV」にしていますので、ファイル名をD3DContent.defの代わりにDirect3DYUV.defを作成します。

  1. ソリューションエクスプローラ」ウィンドウでプロジェクトDirect3DYUVを右クリックし、「追加」から「新しい項目…」を選択
  2. 新しい項目の追加」ウィンドウで、左ペインのVisualC++下のコードをクリックし、中ペインのモジュール定義ファイルを選択
  3. 名前」をDirect3DYUV.defとし、「追加」ボタンをクリック
  4. 作成されたDirect3DYUV.defにサンプルコードをコピー
  5. ライブラリ名「LIBRARY “D3DContent"」を「LIBRARY “Direct3DYUV"」に変更
ライブラリの追加

ここで、チュートリアルではコンパイルを指示されていますが、そのままコンパイルするとエラーが出ます。

Visual Studio 2022で採用されているWindows SDK 10では、DirectXのライブラリの多くが取り込まれていますが、D3DXMath等、以前のDirectX9と共に提供されていたライブラリが分離され、別のライブラリとして提供されるようになっています。

今回は、NuGetから必要なライブラリ等をダウンロードするようにします。

  1. プロジェクト」→「NuGetパッケージの管理…」を選択
  2. NuGetウィンドウで「参照」をクリック
  3. Microsoft.DXSDK.D3DXを選択、インストール(検索欄にDXSDKと記入すると選びやすい)
エラーの修正

チュートリアルのコードでは、ライブラリを追加後も幾つかのエラーが報告されますので修正します。

CTriangleRenderer::Initメソッド内、

IFC(CRenderer::Init(pD3D, pD3DEx, hwnd, uAdapter));

は、初期化後に記述する必要がありますので、

CUSTOMVERTEX vertices[] =
{
    { -1.0f, -1.0f, 0.0f, 0xffff0000, }, // x, y, z, color
    {  1.0f, -1.0f, 0.0f, 0xff00ff00, },
    {  0.0f,  1.0f, 0.0f, 0xff00ffff, },
};
          

の後に移動します。

また、CTriangleRenderer::Renderメソッド内、

IFC(m_pd3dDevice->BeginScene());
IFC(m_pd3dDevice->Clear(
    0,
    NULL,
    D3DCLEAR_TARGET,
    D3DCOLOR_ARGB(128, 0, 0, 128),  // NOTE: Premultiplied alpha!
    1.0f,
    0
));
          

も初期化後に記述する必要がありますので、

UINT  iTime = GetTickCount() % 1000;
FLOAT fAngle = iTime * (2.0f * D3DX_PI) / 1000.0f;
          

の後に移動します。

UIの作成

次に、MSDNで説明されている「チュートリアル: WPF での Direct3D9 コンテンツのホスト」に沿って、WPFでUIを作成して行きます。

コード作成

チュートリアルの「Direct3D9 コンテンツをインポートするには」の手順に従い、MainWindow.xaml.csにコードを追加します。

なお、今回のサンプルコードでは、DLLの名前をDirect3DYUVとしましたので、コード中、

[DllImport("D3DCode.dll")]

となっている部分は、

[DllImport("Direct3DYUV.dll")]

に変更します。

次に、チュートリアルの「Direct3D9 コンテンツをホストするには」に従い、MainWindow.xamlにUIの記述を追加します。

最後にコンパイルして間違いが無いか確認します。

DLLのコピー

チュートリアルの「Direct3D9 コンテンツをホストするには」では、

3.Direct3D9 コンテンツを含む DLL を bin/Debug フォルダーにコピーします。

となっていますが、今回の場合、DLLはbin\Debug\net6.0-windowsにコピーします。

なお、他にも必要となるDLLがありますので、以下のようにコピーは自動化しておきます。

  1. ソリューションエクスプローラ」ウィンドウでソリューションWPFViewYUVを右クリックし、「ビルドの依存関係」から「プロジェクトの依存関係…」を選択
  2. 依存先」でDirect3DYUVをチェックしOKボタンをクリック
  3. ソリューションエクスプローラ」ウィンドウでソリューションDirect3DYUVを右クリックし、「プロパティ」を選択
  4. 構成」で「全ての構成」を選択
  5. 構成プロパティ」の「全般」を選択
  6. 全般プロパティ」ページの「出力ディレクトリ」を$(SolutionDir)WPFViewYUV\bin\$(Configuration)\net6.0-windowsに変更し、OKボタンをクリック

設定が終われば、「ビルド」→「ソリューションのリビルド」メニューを選択し、ソリューション全体をリビルドします。

実行

WPFViewYUVを実行すると、三角形がくるくると回るデモが表示されます。

ここまでのサンプルコードは、こちらからダウンロードできます。

YUV⇒RGB変換

前回、YUV⇒RGB変換を行ってWindowsFormで表示できたので、次はDirect2Dで表示してみます。

昔はDirectDrawを使って、かなり簡単に表示できたのですが、現在のDirectXはDirectDrawが無くなり、代わりにDirect2Dが使えるようになりました。
ただ、Direct2DもDirectXの一部なので、C#等のマネージドコードからは使い難く、どうしてもVC++を使う必要があります。

今回は、Win32APIを使って実装してみます。
以前はフレームワークとしてMFCを使い、かなり簡単に実装できたのですが、現在は流行っていないので使いませんでした。
MFCは歴史が古く実績が多い上に、Visual Studio 2022では無償版でも使えるため、もう少し使われても良いと思うのですが…。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、VC++を使用しますので、インストール時に「C++ によるデスクトップ開発」を選択しているものとします。
もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

プログラムの作成

プロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「Windows デスクトップアプリケーション」を選択します。
なお、上部でC++, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select Windows Desktop App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「WinDeskTopViewYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。

コンパイル

とりあえず、何もせずにコンパイル⇒実行してみましょう。
以下のような画面が表示されます。

Win32 Form1 Initial

Direct2Dの実装

早速、Direct2Dを実装して行きます。

Direct2Dについては、MSDNで詳しく説明されています。
先ずは、その中の「単純なDirect2Dアプリケーションの作成」に記載されたサンプルコードを略そのまま実装します。

ヘッダーファイル

パート 1: DemoApp ヘッダーを作成する」の1および2のヘッダーおよびインターフェース、マクロ類は、framework.hに追加します。

なお、追加するヘッダー類は元々framework.hに記載されているものもあるため、重複しない様にします。

また、<tchar.h>と<wchar.h>は機能が被りますので今回は<tchar.h>を使います。
結果としてframework.hは以下のようになります。

// header.h : 標準のシステム インクルード ファイルのインクルード ファイル、
// またはプロジェクト専用のインクルード ファイル
//

#pragma once

#include "targetver.h"
#define WIN32_LEAN_AND_MEAN             // Windows ヘッダーからほとんど使用されていない部分を除外する
// Windows ヘッダー ファイル
#include <windows.h>
// C ランタイム ヘッダー ファイル
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <math.h>

#include <d2d1.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <wincodec.h>

template<class Interface>
inline void SafeRelease(
    Interface** ppInterfaceToRelease
)
{
    if (*ppInterfaceToRelease != NULL)
    {
        (*ppInterfaceToRelease)->Release();

        (*ppInterfaceToRelease) = NULL;
    }
}

#ifndef Assert
#if defined( DEBUG ) || defined( _DEBUG )
#define Assert(b) do {if (!(b)) {OutputDebugStringA("Assert: " #b "\n");}} while(0)
#else
#define Assert(b)
#endif //DEBUG || _DEBUG
#endif

#ifndef HINST_THISCOMPONENT
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
#endif
      

クラス追加

次に、実際のDirect2Dの表示を行う為のクラス、DemoAppを追加します。

プロジェクト」メニューから、「クラスの追加」を選択します。
クラス名」にDemoAppと記入すれば、ファイル名が自動的に入力されるので、確認後にOKボタンをクリックします。

DemoAppクラスのヘッダーファイル

作成されたDemoApp.hに「パート 1: DemoApp ヘッダーを作成する」の3および4のコードを転記します。
なお、DemoApp.hの先頭に、先程変更したframework.hをインクルードする事を忘れないでください。

また、後程、「情報表示」ダイアログを表示するためのコールバックをWinDeskTopViewYUV.cppからコピーするので、About(…)関数をクラスメソッドとして宣言をしておきます。
結果としてDemoApp.hは以下のようになります。

#pragma once

#include "framework.h"   // 忘れないように!!

class DemoApp
{
private:
    HWND m_hwnd;
    ID2D1Factory* m_pDirect2dFactory;
    ID2D1HwndRenderTarget* m_pRenderTarget;
    ID2D1SolidColorBrush* m_pLightSlateGrayBrush;
    ID2D1SolidColorBrush* m_pCornflowerBlueBrush;

public:
    DemoApp();
    ~DemoApp();

    // Register the window class and call methods for instantiating drawing resources
    HRESULT Initialize();

    // Process and dispatch messages
    void RunMessageLoop();

private:
    // Initialize device-independent resources.
    HRESULT CreateDeviceIndependentResources();

    // Initialize device-dependent resources.
    HRESULT CreateDeviceResources();

    // Release device-dependent resource.
    void DiscardDeviceResources();

    // Draw content.
    HRESULT OnRender();

    // Resize the render target.
    void OnResize(
        UINT width,
        UINT height
    );

    // The windows procedure.
    static LRESULT CALLBACK WndProc(
        HWND hWnd,
        UINT message,
        WPARAM wParam,
        LPARAM lParam
    );

    // 情報表示ダイアログ用コールバック
    static INT_PTR CALLBACK About(
        HWND hDlg,
        UINT message,
        WPARAM wParam,
        LPARAM lParam
    );
};
        
DemoAppクラスのコード

作成されたDemoApp.cppに「パート 2: クラスインフラストラクチャを実装する」の1~3のコードを転記します。
4のコードについては、後程、使用します。

更に、「パート 3: Direct2D リソースの作成」の1~5のコードを転記します。

なお、2と3のコードはHRESULT DemoApp::CreateDeviceResources()メソッドの内容のみで、HRESULT DemoApp::CreateDeviceResources()メソッドの全体は4にまとまっていますので、2と3は飛ばして4のみを転記しておきます。

次に「パート 4: Direct2D コンテンツをレンダリングする」の1~13までのコードを転記します。
なお、2~12は1つのメソッドを分割していますので、HRESULT DemoApp::OnRender()メソッドに纏めます。

最後に、「情報表示」ダイアログのメッセージハンドラーをWinDeskTopViewYUV.cppのAbout(…)メッセージハンドラーからコピーしておきます。
追加として、Direct2Dのライブラリをコードの先頭でpragmaによって指定しておきます。

また、ID2D1Factory::GetDesktopDpiが非推奨とするエラーが出るので、pragmaで抑制しておきます。

更に、後程、Resource.hに定義された値を使用しますので、DemoAPP.cppの先頭でResource.hもインクルードしておきます。

結果としてDemoApp.cppは以下のようになります。

#pragma comment(lib, "d2d1.lib")
#pragma warning(disable:4996)

#include "DemoApp.h"
#include "Resource.h"

DemoApp::DemoApp() :
    m_hwnd(NULL),
    m_pDirect2dFactory(NULL),
    m_pRenderTarget(NULL),
    m_pLightSlateGrayBrush(NULL),
    m_pCornflowerBlueBrush(NULL)
{
}

DemoApp::~DemoApp()
{
    SafeRelease(&m_pDirect2dFactory);
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&m_pLightSlateGrayBrush);
    SafeRelease(&m_pCornflowerBlueBrush);

}
        
void DemoApp::RunMessageLoop()
{
    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}
        
HRESULT DemoApp::Initialize()
{
    HRESULT hr;

    // Initialize device-indpendent resources, such
    // as the Direct2D factory.
    hr = CreateDeviceIndependentResources();

    if (SUCCEEDED(hr))
    {
        // Register the window class.
        WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = DemoApp::WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = sizeof(LONG_PTR);
        wcex.hInstance = HINST_THISCOMPONENT;
        wcex.hbrBackground = NULL;
        wcex.lpszMenuName = NULL;
        wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION);
        wcex.lpszClassName = L"D2DDemoApp";

        RegisterClassEx(&wcex);


        // Because the CreateWindow function takes its size in pixels,
        // obtain the system DPI and use it to scale the window size.
        FLOAT dpiX, dpiY;

        // The factory returns the current system DPI. This is also the value it will use
        // to create its own windows.
        m_pDirect2dFactory->GetDesktopDpi(&dpiX, &dpiY);


        // Create the window.
        m_hwnd = CreateWindow(
            L"D2DDemoApp",
            L"Direct2D Demo App",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            static_cast<UINT>(ceil(640.f * dpiX / 96.f)),
            static_cast<UINT>(ceil(480.f * dpiY / 96.f)),
            NULL,
            NULL,
            HINST_THISCOMPONENT,
            this
        );
        hr = m_hwnd ? S_OK : E_FAIL;
        if (SUCCEEDED(hr))
        {
            ShowWindow(m_hwnd, SW_SHOWNORMAL);
            UpdateWindow(m_hwnd);
        }
    }

    return hr;
}
        
HRESULT DemoApp::CreateDeviceIndependentResources()
{
    HRESULT hr = S_OK;

    // Create a Direct2D factory.
    hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);

    return hr;
}
        
HRESULT DemoApp::CreateDeviceResources()
{
    HRESULT hr = S_OK;

    if (!m_pRenderTarget)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(
            rc.right - rc.left,
            rc.bottom - rc.top
        );

        // Create a Direct2D render target.
        hr = m_pDirect2dFactory->CreateHwndRenderTarget(
            D2D1::RenderTargetProperties(),
            D2D1::HwndRenderTargetProperties(m_hwnd, size),
            &m_pRenderTarget
        );


        if (SUCCEEDED(hr))
        {
            // Create a gray brush.
            hr = m_pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::LightSlateGray),
                &m_pLightSlateGrayBrush
            );
        }
        if (SUCCEEDED(hr))
        {
            // Create a blue brush.
            hr = m_pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::CornflowerBlue),
                &m_pCornflowerBlueBrush
            );
        }
    }

    return hr;
}
        
void DemoApp::DiscardDeviceResources()
{
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&m_pLightSlateGrayBrush);
    SafeRelease(&m_pCornflowerBlueBrush);
}
        
LRESULT CALLBACK DemoApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT result = 0;

    if (message == WM_CREATE)
    {
        LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
        DemoApp* pDemoApp = (DemoApp*)pcs->lpCreateParams;

        ::SetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA,
            reinterpret_cast<LONG_PTR>(pDemoApp)
        );

        result = 1;
    }
    else
    {
        DemoApp* pDemoApp = reinterpret_cast<DemoApp*>(static_cast<LONG_PTR>(
            ::GetWindowLongPtrW(
                hwnd,
                GWLP_USERDATA
            )));

        bool wasHandled = false;

        if (pDemoApp)
        {
            switch (message)
            {
            case WM_SIZE:
            {
                UINT width = LOWORD(lParam);
                UINT height = HIWORD(lParam);
                pDemoApp->OnResize(width, height);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DISPLAYCHANGE:
            {
                InvalidateRect(hwnd, NULL, FALSE);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_PAINT:
            {
                pDemoApp->OnRender();
                ValidateRect(hwnd, NULL);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DESTROY:
            {
                PostQuitMessage(0);
            }
            result = 1;
            wasHandled = true;
            break;
            }
        }

        if (!wasHandled)
        {
            result = DefWindowProc(hwnd, message, wParam, lParam);
        }
    }

    return result;
}
        
HRESULT DemoApp::OnRender()
{
    HRESULT hr = S_OK;

    hr = CreateDeviceResources();
    if (SUCCEEDED(hr))
    {
        m_pRenderTarget->BeginDraw();

        m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());

        m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));

        D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();

        // Draw a grid background.
        int width = static_cast<int>(rtSize.width);
        int height = static_cast<int>(rtSize.height);

        for (int x = 0; x < width; x += 10)
        {
            m_pRenderTarget->DrawLine(
                D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
                D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
                m_pLightSlateGrayBrush,
                0.5f
            );
        }

        for (int y = 0; y < height; y += 10)
        {
            m_pRenderTarget->DrawLine(
                D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
                D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
                m_pLightSlateGrayBrush,
                0.5f
            );
        }

        // Draw two rectangles.
        D2D1_RECT_F rectangle1 = D2D1::RectF(
            rtSize.width / 2 - 50.0f,
            rtSize.height / 2 - 50.0f,
            rtSize.width / 2 + 50.0f,
            rtSize.height / 2 + 50.0f
        );

        D2D1_RECT_F rectangle2 = D2D1::RectF(
            rtSize.width / 2 - 100.0f,
            rtSize.height / 2 - 100.0f,
            rtSize.width / 2 + 100.0f,
            rtSize.height / 2 + 100.0f
        );

        // Draw a filled rectangle.
        m_pRenderTarget->FillRectangle(&rectangle1, m_pLightSlateGrayBrush);

        // Draw the outline of a rectangle.
        m_pRenderTarget->DrawRectangle(&rectangle2, m_pCornflowerBlueBrush);

        hr = m_pRenderTarget->EndDraw();
    }

    if (hr == D2DERR_RECREATE_TARGET)
    {
        hr = S_OK;
        DiscardDeviceResources();
    }

    return hr;
}
        
void DemoApp::OnResize(UINT width, UINT height)
{
    if (m_pRenderTarget)
    {
        // Note: This method can fail, but it's okay to ignore the
        // error here, because the error will be returned again
        // the next time EndDraw is called.
        m_pRenderTarget->Resize(D2D1::SizeU(width, height));
    }
}
        
// バージョン情報ボックスのメッセージ ハンドラーです。
INT_PTR CALLBACK DemoApp::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}
        
メニューの追加

MSDNのDirect2Dのサンプルコードではメニューが無いのですが、せっかく、Windowsデスクトップアプリケーションのテンプレートではメニューが付いていて、「バージョン情報」のダイアログも表示できるようになっているので、メニューを追加してみます。

ダイアログのメッセージハンドラーは既にDemoAppクラスに追加しているので、ウィンドウ作成時にメニューのリソースを追加すると共に、メインのメッセージハンドラーでメニュー選択時の動作を記述していきます。

先ず、DemoApp::Initialize()メソッド中のWindow Classを設定するための構造体、WNDCLASSEX wcexのlpszMenuNameメンバの値をNULLからMAKEINTRESOURCEW(IDC_WINDESKTOPVIEWYUV)に変更します。
結果、DemoApp::Initialize()メソッドは、以下の通りとなります。

HRESULT DemoApp::Initialize()
{
    HRESULT hr;

    // Initialize device-indpendent resources, such
    // as the Direct2D factory.
    hr = CreateDeviceIndependentResources();

    if (SUCCEEDED(hr))
    {
        // Register the window class.
        WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = DemoApp::WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = sizeof(LONG_PTR);
        wcex.hInstance = HINST_THISCOMPONENT;
        wcex.hbrBackground = NULL;
        wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDESKTOPVIEWYUV);
        wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION);
        wcex.lpszClassName = L"D2DDemoApp";

        RegisterClassEx(&wcex);


        // Because the CreateWindow function takes its size in pixels,
        // obtain the system DPI and use it to scale the window size.
        FLOAT dpiX, dpiY;

        // The factory returns the current system DPI. This is also the value it will use
        // to create its own windows.
        m_pDirect2dFactory->GetDesktopDpi(&dpiX, &dpiY);


        // Create the window.
        m_hwnd = CreateWindow(
            L"D2DDemoApp",
            L"Direct2D Demo App",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            static_cast<UINT>(ceil(640.f * dpiX / 96.f)),
            static_cast<UINT>(ceil(480.f * dpiY / 96.f)),
            NULL,
            NULL,
            HINST_THISCOMPONENT,
            this
        );
        hr = m_hwnd ? S_OK : E_FAIL;
        if (SUCCEEDED(hr))
        {
            ShowWindow(m_hwnd, SW_SHOWNORMAL);
            UpdateWindow(m_hwnd);
        }
    }

    return hr;
}
          

次に、メインのメッセージハンドラーであるDemoAPP::WndProc(…)にメニューのメッセージハンドラーを追加します。

元々、WinDeskTopViewYUV.cpp内のメッセージハンドラーとして登録されていたWndProc(…)内の、

case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        // 選択されたメニューの解析:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
            break;
        case IDM_EXIT:
            DestroyWindow(hWnd);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;
          

部分でメニューの処理をしていたので、これをそのままDemoAPP::WndProc(…)内にコピーします。
但し、hWndはhwndに、hInstはGetModuleHandle(NULL)に変える必要があります。

結果、DemoAPP::WndProc(…)は以下のようになります。

LRESULT CALLBACK DemoApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT result = 0;

    if (message == WM_CREATE)
    {
        LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
        DemoApp* pDemoApp = (DemoApp*)pcs->lpCreateParams;

        ::SetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA,
            reinterpret_cast<LONG_PTR>(pDemoApp)
        );

        result = 1;
    }
    else
    {
        DemoApp* pDemoApp = reinterpret_cast<DemoApp*>(static_cast<LONG_PTR>(
            ::GetWindowLongPtrW(
                hwnd,
                GWLP_USERDATA
            )));

        bool wasHandled = false;

        if (pDemoApp)
        {
            switch (message)
            {
            case WM_COMMAND:
            {
                int wmId = LOWORD(wParam);
                // 選択されたメニューの解析:
                switch (wmId)
                {
                case IDM_ABOUT:
                    DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_ABOUTBOX), hwnd, About);
                    break;
                case IDM_EXIT:
                    DestroyWindow(hwnd);
                    break;
                default:
                    return DefWindowProc(hwnd, message, wParam, lParam);
                }
            }
            result = 0;
            wasHandled = true;
            break;
            case WM_SIZE:
            {
                UINT width = LOWORD(lParam);
                UINT height = HIWORD(lParam);
                pDemoApp->OnResize(width, height);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DISPLAYCHANGE:
            {
                InvalidateRect(hwnd, NULL, FALSE);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_PAINT:
            {
                pDemoApp->OnRender();
                ValidateRect(hwnd, NULL);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DESTROY:
            {
                PostQuitMessage(0);
            }
            result = 1;
            wasHandled = true;
            break;
            }
        }

        if (!wasHandled)
        {
            result = DefWindowProc(hwnd, message, wParam, lParam);
        }
    }

    return result;
}
          
メイン関数の変更

元々のプロジェクトのコードであるWinDeskTopViewYUV.cppについては、とりあえずWin32APIでWindowを表示するコードが全て入っています。

ただ、DemoAPPクラスにDirect2Dを使用してWindowを表示するコードを記述しましたので、メイン関数以外は必要ありません。

ですので、wWinMain(…)関数とAbout(…)コールバック関数以外は、キッパリと削除します。

また、マクロ定義やグローバル変数、関数の宣言等も使用しないため削除します。

最後に、wWinMain(…)関数の内容は、「パート 2: クラスインフラストラクチャを実装する」の4のWinMain(…)関数の中身と入れ替えます。
wWinMain(…)関数内で、DemoAppを使っていますので、DemoApp.hをインクルードする事を忘れないでください。

なお、wWinmain(…)関数内の最初にCoInitialize(NULL)を、最後にCoUninitialize()をコールしているのは、DirectXを使用する際の、と云うか、COMを使う際の作法です。
通常、各スレッドの先頭でCoInitialize(NULL)を、最後にCoUninitialize()をコールしておきます。

結果として、WinDeskTopViewYUV.cppは以下のようになります。

// WinDeskTopViewYUV.cpp : アプリケーションのエントリ ポイントを定義します。
//

#include "framework.h"
#include "WinDeskTopViewYUV.h"
#include "DemoApp.h"

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hInstance);
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);
    UNREFERENCED_PARAMETER(nCmdShow);

    // TODO: ここにコードを挿入してください。

    // Use HeapSetInformation to specify that the process should
    // terminate if the heap manager detects an error in any heap used
    // by the process.
    // The return value is ignored, because we want to continue running in the
    // unlikely event that HeapSetInformation fails.
    HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

    if (SUCCEEDED(CoInitialize(NULL)))
    {
        {
            DemoApp app;

            if (SUCCEEDED(app.Initialize()))
            {
                app.RunMessageLoop();
            }
        }
        CoUninitialize();
    }

    return 0;
}
        

動作確認

コンパイルして実行すれば、格子模様の真ん中に2重の四角が描画されたWindowが表示されます。
メニューもちゃんと動作するはずです。

Direct2D Demo App

なお、ここまでのコードについては、こちらからダウンロードできます。