実践!Windowsアプリを作る
第1章 WPFアプリケーション

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

ユーザーインターフェースは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アプリを作る