写真ファイルの自動振り分けソフトについて、登録用のアプリケーションを作成します。

ソフトの概要と仕様についてはサービスプロジェクト(仕組みと実装)をご確認ください。
振り分け(サービスプログラム)については写真振り分けソフト Step ② 【振り分けの実装】をご確認ください。

概要
  • ソースコードを参照してログと設定を実装
  • 画面実装
  • 登録チェックと書き込み・読み込み

Step②で作成したログや設定のソースコードを使用するため、画面以外はほぼ完成しています。

プロジェクトの作成

Visual Studio 2022を起動し、プロジェクトの作成を行います。
ここで、新しいプロジェクトを作成するのではなくStep②のサービスプロジェクトを開いてください。

ソリューションエクスプローラーよりソリューションを右クリックし、「追加」-「新しいプロジェクト」の順に選択します。

Windowsフォームアプリケーション(.NET Framework)を選択します。

プロジェクト名は任意ですが、「setting_app」にしました。
.NET Frameworkのバージョンは4.7.2を選択します。

これでプロジェクトが追加されます。
サービスプロジェクトと登録用アプリケーションのプロジェクトが同じソリューション上に作成されます。
こうすることでサービスプロジェクトのソースコードを参照できるようになります。

ログおよび設定の実装

ログおよび設定を実装します。

ライブラリの追加とソースコードの参照

写真振り分けソフト Step ② 【振り分けの実装】を参考に、次のライブラリを追加します。

  • NugetパッケージよりNLogのインストール
  • System.Text.Jsonの追加
  • System.Memoryの追加

次にソースコードを参照追加します。
setting_appプロジェクトを右クリックし、「追加」-「既存の項目」を選択します。

photo_sortingプロジェクト内のLog.csファイルを選択します。
ここで「追加」ボタン横の展開矢印をクリックし、「リンクとして追加」を選択します。

リンク追加することで、ソースコードをコピーするのではなく参照追加します。
実態(ファイル)はphoto_sortingに存在し、1ファイルを2つのプロジェクトで共有する形になります。

Setting.csファイルも同様にリンクとして追加します。

2つのソースファイルを追加後、Program.csに静的変数を追加します。

        /// <summary>
        ///ログ変数
        /// </summary>
        public static Log _log = new Log();

        /// <summary>
        ///設定
        /// </summary>
        public static Setting _setting = new Setting();


この状態で一度ビルドすると、以下のエラーが出力されて失敗します。

これはsetting_appプロジェクト上のProgram.csの名前空間が参照したソースコードの名前空間と異なるためです。
複数プロジェクトでソースコードを共有する場合、設定やログで作成した静的変数はProgram.csでなく、それぞれのクラスで定義するほうが役割を整理できます。

Program.csで宣言した静的変数(_setting)および(_Log)を削除して、それぞれのクラス内で再度宣言します。
また、共有して使用するファイル(Setting.cs、Log.cs)の名前空間は「photo_data」にしました。

namespace photo_data
{
    public class Setting
    {
        /// <summary>
        ///設定
        /// </summary>
        public static Setting _setting;
namespace photo_data
{
    internal class Log
    {
        /// <summary>
        ///ログ変数
        /// </summary>
        public static Log _log;

Program.csに戻り初期化します。

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            // ログと設定の初期化
            Log._log = new Log();
            Setting._setting = new Setting();

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

静的変数を使用した関数の呼び出しも修正してください。

namespaceは次の分類に分けました。

namespace分類
photo_sortingサービスプロジェクト(振り分け)
photo_setting登録用アプリケーション
photo_dataログ、設定などの共通データ

ログの区分け

出力ログを共有すると、サービスプロジェクトと登録用アプリケーションのログの区別が行えなくなります。
このため、初期化する時点で出力先を区分けします。

        /// <summary>
        /// 初期化
        /// </summary>
        /// <param name="setting_app">登録用アプリケーション判定</param>
        public Log(bool setting_app = false)
        {
            LoggingConfiguration config = new LoggingConfiguration();

            FileTarget fileTarget;

            if (setting_app == true)
            {
                // 登録用アプリケーション
                fileTarget = new FileTarget()
                {
                    Name = "log",
                    FileName = "${basedir}/log_app/${shortdate}.log",
                    Layout = "${longdate} - ${level} - ${message}",
                    Encoding = Encoding.UTF8,
                    ArchiveEvery = FileArchivePeriod.Day,
                    ArchiveFileName = "${basedir}/log_app/${shortdate}.{###}",
                    MaxArchiveDays = 7,
                };
            }
            else
            {
                // サービスプロジェクト
                fileTarget = new FileTarget()
                {
                    Name = "log",
                    FileName = "${basedir}/log/${shortdate}.log",
                    Layout = "${longdate} - ${level} - ${message}",
                    Encoding = Encoding.UTF8,
                    ArchiveEvery = FileArchivePeriod.Day,
                    ArchiveFileName = "${basedir}/log/${shortdate}.{###}",
                    MaxArchiveDays = 7,
                };
            }

            config.AddRule(LogLevel.Debug, LogLevel.Error, fileTarget);

            LogManager.Configuration = config;
        }
  1. Logクラスのコンストラクタに引数を追加し、登録用アプリケーションを判定します。
  2. 登録用アプリケーションの場合はログフォルダを「log_app」に変更します。

Program.csに戻り、変数(_log)の引数にtrueを追加します。

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            // ログと設定の初期化
            Log._log = new Log(true);
            Setting._setting = new Setting();

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

サービスプロジェクトの(Program.cs)も同様に変更します。
Logの初期化では、引数は省略またはfalseを指定します。

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        static void Main()
        {
            // ログと設定の初期化
            Log._log = new Log();
            Setting._setting = new Setting();

            _servicesToRun = new Service1();
            ServiceBase.Run(_servicesToRun);
        }

ログ出力の確認

登録用アプリケーションでは固定のログを用意していないため、デバッグログを出力します。

Form1クラスのコンストラクタにて、デバッグログを追加します。

        public Form1()
        {
            InitializeComponent();

            Log._log.WriteLoggerDebug("登録用アプリケーション起動");
        }

app_logフォルダが作成され、ログが出力されました。

登録画面の作成

登録画面の作成を行います。
フォームはデフォルトで作成されるクラス(Form1)をそのまま使います。

最初にプロパティ値を決めます。
Forrm1を開き、画面を右クリックして「プロパティ」を選択します。
プロパティウインドウが表示したらプロパティ変更します。内容は自由ですが、よく変更する内容を記載します。

  • Icon:アイコン表示です。アイコンがある場合は変更することができます。
  • MaximizeBox:サイズ変更に対応していないため、最大化ボタンは無効化(false)にします。
  • StartPosition:中央表示するため「CenterScreen」にします。
  • FormBorderStyle:境界線の表示と動作です。「FixedSingle」を選択してサイズ変更をできなくします。
  • Text:タイトル表示。ソフト名を入れます。

インターネットからアイコンを取得する場合は著作権にご注意ください。
使用しても問題ないアイコンを選択するか、自作してください。

Form1にコントロールを貼り付けします。

Label、Groupコントロールを除いたコントロールには識別できる名前(Name)プロパティを付けます。
レイアウトや名前はお好みで変更してください。

(Name)クラス設定機能
checkBox_usedCheckBoxused使用設定
textBox_sourceDirectoryTextBoxsourceDirectory振り分け元フォルダ
button_sourceDirectoryButtonsourceDirectory振り分け元フォルダのフォルダ参照ダイアログを表示
textBox_destinationDirectoryTextBoxdestinationDirectory振り分け先フォルダ
button_destinationDirectoryTextBoxdestinationDirectory振り分け先フォルダのフォルダ参照ダイアログを表示
checkBox_jpegCheckBoxextensionよく使用する拡張子(.jpeg)
checkBox_jpgCheckBoxextensionよく使用する拡張子(.jpg)
checkBox_pngCheckBoxextensionよく使用する拡張子(.png)
checkBox_gifCheckBoxextensionよく使用する拡張子(.gif)
checkBox_tifCheckBoxextensionよく使用する拡張子(.tif)
checkBox_tiffCheckBoxextensionよく使用する拡張子(.tiff)
checkBox_bmpCheckBoxextensionよく使用する拡張子(.bmp)
textBox_extensionCheckBoxextensionその他の拡張子をリストを指定するテキストエディタ
checkBox_backupCheckBoxbackup振り分け元のファイルバックアップ
radioButton_performance_highRadioButtonperformanceパフォーマンス(高)
radioButton_performance_middleRadioButtonperformanceパフォーマンス(中)
radioButton_performance_lowRadioButtonperformanceパフォーマンス(低)
numericUpDown_capacityNumericUpDowncapacity保存先の容量
1〜999を範囲にします
button_setting_updateButton登録を更新するボタン

フォルダ参照

振り分け元と振り分け先を選択するフォルダ参照ダイアログを実装します。
フォルダ参照ダイアログには「CommonOpenFileDialog」を使用します。

CommonOpenFileDialogはNuGetパッケージよりダウンロードします。

標準クラス「FolderBrowserDialog」を使用しても問題ありませんが、ユーザインタフェースを考慮するとCommonOpenFileDialogの方が優れています。

NuGetパッケージより「WindowsAPICodePack-Shell」と検索してインストールします。

「WindowsAPICodePack-Shell」と「WindowsAPICodePack-Core」の2つがリストアップします。
「適用」をクリックします。

フォルダ参照のボタンをダブルクリックすると、clickイベントの関数を自動生成します。
次の内容を入力します。

        /// <summary>
        /// 振り分け元フォルダ参照
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button_sourceDirectory_Click(object sender, EventArgs e)
        {
            using (CommonOpenFileDialog commonDlg = new CommonOpenFileDialog())
            {
                commonDlg.IsFolderPicker = true;
                commonDlg.Title = "振り込み元フォルダを選択します";

                if (commonDlg.ShowDialog() == CommonFileDialogResult.Ok)
                {
                    textBox_sourceDirectory.Text = commonDlg.FileName;
                }
            }
        }
  1. CommonOpenFileDialogクラスのインスタンスを作成します。
  2. フォルダ参照ダイアログのため、IsFolderPickerプロパティにfalseをセットします。
  3. タイトルを入れます。
  4. ShowDIalog関数の戻り値より「OK」がクリックされた場合は、テキストエディタに選択先フォルダをセットします。

振り分け先フォルダに対しても同様に作成します。

登録更新

登録更新の動作を行います。「更新」ボタンのclickイベントで処理します。

更新前に拡張子の登録データを作成します。

            CheckBox[] extListCtrl = { checkBox_jpeg, checkBox_jpg, checkBox_png, checkBox_gif, checkBox_tif, checkBox_tiff, checkBox_bmp};
            string[] extListName = { ".jpeg", ".jpg", ".png", ".gif", ".tif", ".tiff", ".bmp" };
            List<string> extList = new List<string>();

            // チェックボックスの拡張子を取得
            for(int i = 0; i < extListCtrl.Length; i++)
            {
                if (extListCtrl[i].Checked == true)
                {
                    // チェックあり
                    extList.Add(extListName[i]);
                }
            }

            // テキストボックスの拡張子を取得
            string[] extText = textBox_extension.Text.Split(';');
            for(int i = 0; i < extText.Length; i++)
            {
                if (extText[i].Length > 0 && extList.Contains(extText[i].ToLower()) != true)
                {
                    // 拡張子あり
                    extList.Add(extText[i].ToLower());
                }
            }
  1. よく使用する拡張子はチェックボックスが多いため配列化します。
    extListCtrlにチェックボックスを配列化、extListNameに拡張子を配列化します。
    2つの配列のインデックス順番を揃える必要があります。
    また、登録する拡張子を保持する変数(extList)を宣言します。
  2. extListCtrlをループさせ、チェック判定を行います。
    チェックが有効であればextListに対応する拡張子を追加します。
  3. テキストエディタ(textBox_extension)を「;」で区切った後でextLIstに追加します。
    同じ拡張子が繰り返し入らないよう、Contains関数で判定しています。
    また、すべて小文字として扱います。

次に登録内容をチェックします。
機能を使用する場合は「振り分け元」、「振り分け先」、「拡張子」が正しく登録されているか確認します。

先にフォルダを作成する関数(CreateDirectory)を作成します。

        /// <summary>
        /// フォルダ作成
        /// </summary>
        /// <param name="directory">作成するフォルダ</param>
        /// <returns>作成判定</returns>
        private bool CreateDirectory(string directory)
        {
            if(Directory.Exists(directory) == true)
            {
                // 作成されています
                return true;
            }

            if(string.IsNullOrWhiteSpace(directory) == true)
            {
                // フォルダが未指定
                return false;
            }

            try
            {
                Directory.CreateDirectory(directory);
            }
            catch(IOException ex)
            {
                Log._log.WriteLoggerDebug(ex);
            }
            catch (UnauthorizedAccessException ex)
            {
                Log._log.WriteLoggerDebug(ex);
            }
            catch (ArgumentException ex)
            {
                Log._log.WriteLoggerDebug(ex);
            }
            catch (Exception ex)
            {
                Log._log.WriteLoggerDebug(ex);
            }
            return true;
        }
  1. 既にフォルダが作成されていればtrueを返します。
  2. 作成するフォルダ名がブランクの場合はfalseを返します。
  3. DirectoryクラスのCreateDirectory関数でフォルダを作成します。
    この関数は例外発生するため、主要な例外を処理してデバッグログに記載します。

「更新」ボタンのclickイベントに戻ります。

            // 登録チェック
            if(checkBox_used.Checked == true)
            {
                // 振り分け元フォルダ作成
                if(this.CreateDirectory(textBox_sourceDirectory.Text) != true)
                {
                    MessageBox.Show("振り分け元フォルダが未設定またはフォルダ作成できません。");
                    textBox_sourceDirectory.Focus();
                    return;
                }

                // 振り分け先フォルダ作成
                if (this.CreateDirectory(textBox_destinationDirectory.Text) != true)
                {
                    MessageBox.Show("振り分け先フォルダが未設定またはフォルダ作成できません。");
                    textBox_destinationDirectory.Focus();
                    return;
                }

                if(extList.Count == 0)
                {
                    MessageBox.Show("拡張子は最低1つ以上指定してください。");
                    checkBox_jpeg.Focus();
                    return;
                }
            }
  1. 作成したCreateDirectory関数でフォルダが作成できることを確認し、成功すれば次の処理を行います。
    失敗した場合はメッセージを表示後、returnで設定をキャンセルします。
    振り分け元と振り分け先でチェックします。
  2. 拡張子のリスト(extList)が 0個 の場合はメッセージを表示して設定をキャンセルします。

numericUpDown_capacityコントロールは自動的に範囲判定されますが、念の為にチェックします。

            if(numericUpDown_capacity.Value < 1 || numericUpDown_capacity.Value > 999)
            {
                MessageBox.Show("指定容量が正しくありません。");
                numericUpDown_capacity.Focus();
                return;
            }
  1. 値が1〜999の範囲内かチェックします。
  2. 範囲外の場合はメッセージを表示後、設定をキャンセルします。

ここまで問題なければsetting.jsonファイルに書き込みします。

            Setting.SettingInfo info = new Setting.SettingInfo();

            // 登録内容セット
            info.Used = checkBox_used.Checked;
            info.SourceDirectory = textBox_sourceDirectory.Text;
            info.DestinationDirectory = textBox_destinationDirectory.Text;
            info.Extension = string.Join(";", extList);
            info.Backup = checkBox_backup.Checked;
            if(radioButton_performance_high.Checked == true)
            {
                info.Performance = 1;
            }
            else if(radioButton_performance_middle.Checked == true)
            {
                info.Performance = 2;
            }
            else if(radioButton_performance_low.Checked == true)
            {
                info.Performance = 3;
            }
            info.Capacity = (long)numericUpDown_capacity.Value * (1000 * 1000 * 1000);

            // 登録ファイル書き込み
            if (Setting._setting.WriteSetting(info) == true)
            {
                MessageBox.Show("登録しました。");
            }
            else
            {
                MessageBox.Show("登録失敗しました。");
            }
  1. 新しい登録データ(info)変数を作成します。
  2. 登録内容をセットします。
    • Extensionはリスト(extList)から「;」区切って連結した文字列をセットします。
    • Performanceはラジオボタンを判定します。
    • Capacityは単位がGBのため、1000 * 1000 * 1000で積算しています。long型へのキャストも必要です。
  3. WriteSetting関数で書き込みします。戻り値を取得して書き込み結果をメッセージ表示します。

登録の取得

登録用アプリケーションを起動時にsetting.jsonから内容を読み取って表示します。

        /// <summary>
        /// フォームのロード
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Form1_Load(object sender, EventArgs e)
        {
            // 登録ファイル読み込み
            Setting._setting.ReadSetting();

            if(Setting._setting.SettingData == null)
            {
                // ファイルなし
                return;
            }

            // 拡張子
            CheckBox[] extListCtrl = { checkBox_jpeg, checkBox_jpg, checkBox_png, checkBox_gif, checkBox_tif, checkBox_tiff, checkBox_bmp };
            List<string> extListName = new List<string> { ".jpeg", ".jpg", ".png", ".gif", ".tif", ".tiff", ".bmp" };
            List<string> extList = Setting._setting.SettingData.Extension.ToLower().Split(';').ToList();

            int index;
            foreach (string ext in new List<string>(extList))
            {
                if ((index = extListName.IndexOf(ext)) >= 0)
                {
                    // チェックボックス(よく使う拡張子)
                    extListCtrl[index].Checked = true;
                    extList.Remove(ext);
                }
            }

            textBox_extension.Text = string.Join(";", extList);
                       
            checkBox_used.Checked = Setting._setting.SettingData.Used;
            textBox_sourceDirectory.Text = Setting._setting.SettingData.SourceDirectory;
            textBox_destinationDirectory.Text = Setting._setting.SettingData.DestinationDirectory;
            checkBox_backup.Checked = Setting._setting.SettingData.Backup;
            if(Setting._setting.SettingData.Performance == 1)
            {
                radioButton_performance_high.Checked = true;
            }
            else if (Setting._setting.SettingData.Performance == 2)
            {
                radioButton_performance_middle.Checked = true;
            }
            else if (Setting._setting.SettingData.Performance == 3)
            {
                radioButton_performance_low.Checked = true;
            }
            numericUpDown_capacity.Value = Setting._setting.SettingData.Capacity / (1000 * 1000 * 1000);
        }

ここでも拡張子だけは少々手間なため、先に処理します。

  1. ReadSetting関数でsetting.jsonファイルを読み込みします。
    読み込みしたデータはプロパティ(SettingData)から取得します。
  2. チェックボックスのコントロール配列(extListCtrl)と拡張子のリスト(extListName)を作成します。
    用途は更新と同じですがextListNameは検索に使用するためListクラスを使用しています。
  3. extListを作成し、「;」で区切った拡張子のリストをセットします。
  4. extListをループさせ、チェックボックス対象の拡張子の場合はチェックを有効にします。
    同時にリスト(extList)から削除します。
    *削除するとインデックスがズレるため、ループはDeepCopyしたデータに対して行ってください
  5. リスト(extList)で残った情報をtextBox_extensionにセットします。
  6. 拡張子以外の登録情報をコントロールにセットします。

動作確認

完成したら動作確認します。
ここでsetting.jsonファイルは振り分けサービスの実態と同じフォルダに作成したいため、実行ファイルの作成先を変更します。

プロジェクトを右クリックし、プロパティを選択します。

項目より「ビルド」を選択します。
構成から「Debug」を選択します。
出力パスをサービスプロジェクトのデバッグ出力先に変更します。

サービスプロジェクトと登録用アプリケーションで同じ設定ファイル(setting.json)を扱えるようになりました。実行して登録を変更できることを確認します。

まとめ

これで振り分けと登録の2つのソフトが作成できました。
次の投稿では配布について取り組んでみます。