C#定石 - フォルダ選択 - Drag & Drop とフォルダ選択ダイアログ

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

### 変更履歴
- 2025/02/03　OpenFolderDialog - InitialDirectory プロパティを見落としていたことを、Qiitaコメントで指摘頂いたので修正しました

### 記載内容

本記事では、下記２つのセクションにわけて、情報を記載することとします。
- フォルダ選択ダイアログの選択肢
- フォルダ選択イベントハンドラ 

::: note
「フォルダ選択イベントハンドラ」が本記事の主題です。
「フォルダ選択イベントハンドラ」サンプルコードを記載する上で、「フォルダ選択ダイアログの選択肢」も併載しています。

同一フォーマットでファイル選択をまとめた [C#定石 - ファイル選択 - Drag & Drop とファイル選択ダイアログ](https://qiita.com/chai0917/items/d285f40531de5ae0f856) も存在しています。
:::


### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::



## フォルダ選択ダイアログの選択肢

フォルダ選択ダイアログの選択肢として下記が存在します。

- System.Windows.Forms.FolderBrowserDialog
  - Windows Forms - .NET Framework / .NET で利用可能
  - .NET 5 以降で UI 改善（.NET Framework は対象外）
    - AutoUpgradeEnabledプロパティ false で、以前の UI 利用  
  - WPF - .NETFramewok 4.8 からの利用
    - Visual Studio のソリューション エクスプローラーでプロジェクトを選択し、右クリックして「参照の追加」を選択
    - 「アセンブリ」タブに移動、「フレームワーク」を選択、「System.Windows.Forms」をチェック
  - WPF - .NET 8　からの利用
    - プロジェクトファイル（.csproj）を開き、`UseWindowsForms` を追加<br>![WPF-Windows.Forms-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/06f7ce65-67c9-798c-90cc-e29df000ccfc.png)
- Microsoft.Win32.OpenFolderDialog
  - WPF - .NET 8 以降で利用可能（WPF - .NET Framework は利用不可）
  - Windows Forms から、わざわざ理由するメリットはない
- Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog
  - [NuGet Gallery | Microsoft-WindowsAPICodePack-Shell](https://www.nuget.org/packages/Microsoft-WindowsAPICodePack-Shell) 導入が必要
  - Microsoft-WindowsAPICodePack-Shell は、Windows API CodePack の一部で、Windows シェル機能を、.NET Framework / .NET で利用するためのライブラリ
    - Dialogs - ダイアログ表示機能
  - プロパティで、ファイル選択とフォルダ選択の切り替え可能
  - .NET Framework 4.5.2 以降、.NET 6 以降で利用可能

::: note
初期値設定可能で、ファイル選択と同等の UI を提供したい場合、下記となります。
- .NET 5 以降の場合
  - FolderBrowserDialog、もしくは、CommonOpenFileDialog
  - 双方でフォルダ選択 UI が異なります
    - FolderBrowserDialog では、上部パス表示 - 対象パスの親フォルダ
    - CommonOpenFileDialog では、上部パス表示 - 対象パス
- .NET Framework の場合
  - CommonOpenFileDialog
- WPF - .NET 8 以降の場合
  - OpenFolderDialog、CommonOpenFileDialog、もしくは、FolderBrowserDialog
:::


それぞれのサンプルコードと、ダイアログ表示形態を以降に記載します。

### System.Windows.Forms.FolderBrowserDialog

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.folderbrowserdialog

```CSharp
// 初期選択値
var path = @"C:\Program Files (x86)\Microsoft Visual Studio";

// フォルダ選択ダイアログ
using (var dlg = new FolderBrowserDialog())
{
  dlg.Description = "フォルダを選択してください";
//dlg.RootFolder = Environment.SpecialFolder.MyComputer;
  dlg.ShowNewFolderButton = true;

  // 初期選択値があれば設定
  if (!string.IsNullOrEmpty(path))
  {
    if (Directory.Exists(path))
    {
      dlg.SelectedPath = path;
    }
  }
  // フォルダ選択ダイアログ表示
  if (dlg.ShowDialog() == DialogResult.OK)
  {
    // 選択値で更新
    path = dlg.SelectedPath;
  }
}
```

Windows Forms - .NET Framework 4.8 の場合

![FolderSelect-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/fc02d3d6-b789-c3f5-3918-230e768d53a2.png)

Windows Forms - .NET 8（AutoUpgradeEnabled = true）の場合

![FolderSelect-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/f45d4ad2-3eb3-4955-341a-ba9fc6cbc8e4.png)

### Microsoft.Win32.OpenFolderDialog

https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.win32.openfolderdialog

```CSharp
// 初期選択値
var path = @"C:\Program Files (x86)\Microsoft Visual Studio";

// フォルダ選択ダイアログ
var dlg = new Microsoft.Win32.OpenFolderDialog
{
  Multiselect = false,
  Title = "フォルダを選択してください"
};
// 初期選択値があれば設定
if (!string.IsNullOrEmpty(path))
{
  if (Directory.Exists(path))
  {
    dlg.InitialDirectory = path;
    dlg.FolderName = path;
  }
}
// フォルダ選択ダイアログ表示
if (dlg.ShowDialog() == true)
{
  // 選択値で更新
  target.Text = dlg.FolderName;
}
```

![FolderSelect-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/341e2e49-9fb2-492a-7a38-ff6c7ae385a1.png)


### Microsoft.WindowsAPICodePack.Dialogs.CommonOpenFileDialog

NuGet で、[NuGet Gallery | Microsoft-WindowsAPICodePack-Shell](https://www.nuget.org/packages/Microsoft-WindowsAPICodePack-Shell) を導入します。

```
PM> NuGet\Install-Package Microsoft-WindowsAPICodePack-Shell
```

CommonOpenFileDialog プロパティについては、下記、公開ソースで確認できます。

https://github.com/contre/Windows-API-Code-Pack-1.1/tree/main/source/WindowsAPICodePack/Shell/CommonFileDialogs

```CSharp
using Microsoft.WindowsAPICodePack.Dialogs;
```

```CSharp
// 初期選択値
var path = @"C:\Program Files (x86)\Microsoft Visual Studio";

// フォルダ選択ダイアログ
using (var dlg = new CommonOpenFileDialog())
{
  dlg.IsFolderPicker = true;  // true:フォルダ選択  false:ファイル選択

  dlg.Multiselect = false;
  dlg.Title = "フォルダを選択してください";

  // 初期選択値があれば設定
  if (!string.IsNullOrEmpty(path))
  {
    if (Directory.Exists(path))
    {
      dlg.InitialDirectory = path;
      dlg.DefaultFileName = path;
    }
  }
  // フォルダ選択ダイアログ表示
  if (dlg.ShowDialog() == CommonFileDialogResult.Ok)
  {
    // 選択値で更新
    path = dlg.FileName;
  }
}
```

![FolderSelect-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/161063c0-ed97-cbd6-430f-81d07ccb70bd.png)

## フォルダ選択イベントハンドラ

Drag & Drop 可能なテキストボックス、フォルダ選択ダイアログ表示ボタンのペアで、フォルダ選択する UI を良く利用していました。
このようなペアが複数存在するケースでは、イベントハンドラを共有させると効率的です。

![FolderSelect.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/9159fb54-9b99-d0b2-25ad-e988b20746fa.png)

テキストボックスとフォルダ選択ダイアログ表示ボタンとして、下記をデザイナーで配置した場合のサンプルコードを、いくつかのパターンで以降に記載します。

```
TextBox txtFolder1 - Drag & Drop 可能なテキストボックス
Button  btnFolder1 - txtFolder1 のフォルダ選択ダイアログ表示ボタン
TextBox txtFolder2 - Drag & Drop 可能なテキストボックス
Button  btnFolder2 - txtFolder2 のフォルダ選択ダイアログ表示ボタン
```

::: note
Drag & Drop 操作に関して、Windows Forms と WPF では、イベント / ソースコード記載内容に相違があるので、注意してくだい。
:::

::: note
フォルダパスが格納されているテキストボックスと、フォルダ選択ダイアログ表示ボタンの連動は、カスタムコントロール化が正規の手順ですが、、、
フォルダ選択ダイアログ表示ボタン - Click イベントハンドラ「OneFolder_Selection」引数に、対象となるテキストボックスを追加して、連動させる手法を選択しています。
:::

::: note
前述「フォルダ選択ダイアログ選択肢」で記載されているフォルダ選択ダイアログと、フレームワークの組み合わせとなりますが、全てのパターンを網羅した記載はしません。
:::

### Windows Forms - .NET 8 + FolderBrowserDialog

```CSharp
txtFolder1.AllowDrop = true;
txtFolder1.DragEnter += OneFolder_DragEnter;
txtFolder1.DragDrop += OneFolder_DragDrop;
btnFolder1.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder1);

txtFolder2.AllowDrop = true;
txtFolder2.DragEnter += OneFolder_DragEnter;
txtFolder2.DragDrop += OneFolder_DragDrop;
btnFolder2.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder2);
```

```CSharp
// Drag & Drop - 対象：フォルダ単一
private void OneFolder_DragEnter(object? sender, DragEventArgs e)
{
  // １つのフォルダがドラッグされている場合のみ対象
  if (e != null)
  {
    if (e.Data?.GetDataPresent(DataFormats.FileDrop) == true)
    {
      var paths = e.Data?.GetData(DataFormats.FileDrop) as string[];
      if (paths?.Length == 1)
      {
        if (Directory.Exists(paths[0]))
        {
          e.Effect = DragDropEffects.Copy;
        }
      }
    }
  }
}
private void OneFolder_DragDrop(object? sender, DragEventArgs e)
{
  var paths = e?.Data?.GetData(DataFormats.FileDrop) as string[];
  if (paths?.Length == 1)
  {
    var target = sender as TextBox;
    if (target != null)
    {
      target.Text = paths[0];
    }
  }
}
// フォルダ選択ダイアログ表示 - テキストボックス反映
private void OneFolder_Selection(object? sender, EventArgs e, TextBox target)
{
  // 初期選択値
  var path = target.Text.Trim();

  // フォルダ選択ダイアログ
  using (var dlg = new FolderBrowserDialog())
  {
    dlg.Description = "フォルダを選択してください";
    dlg.ShowNewFolderButton = true;

    // 初期選択値があれば設定
    if (!string.IsNullOrEmpty(path))
    {
      if (Directory.Exists(path))
      {
        dlg.SelectedPath = path;
      }
    }
    // フォルダ選択ダイアログ表示
    if (dlg.ShowDialog() == DialogResult.OK)
    {
      // 選択値で更新
      target.Text = dlg.SelectedPath;
    }
  }
}
```

### WPF - .NET 8 + OpenFolderDialog

```CSharp
txtFolder1.AllowDrop = true;
txtFolder1.PreviewDragOver += OneFolder_PreviewDragOver;
txtFolder1.Drop += OneFolder_Drop;
btnFolder1.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder1);

txtFolder2.AllowDrop = true;
txtFolder2.PreviewDragOver += OneFolder_PreviewDragOver;
txtFolder2.Drop += OneFolder_Drop;
btnFolder2.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder2);
```

```CSharp
// Drag & Drop - 対象：フォルダ単一
private void OneFolder_PreviewDragOver(object sender, DragEventArgs e)
{
  // １つのフォルダがドラッグされている場合のみ対象
  if (e != null)
  {
    e.Effects = DragDropEffects.None;
    
    if (e.Data?.GetDataPresent(DataFormats.FileDrop) == true)
    {
      var paths = e.Data?.GetData(DataFormats.FileDrop) as string[];
      if (paths?.Length == 1)
      {
        if (Directory.Exists(paths[0]))
        {
          e.Effects = DragDropEffects.Copy;
        }
      }
    }
    e.Handled = true;
  }
}
private void OneFolder_Drop(object sender, DragEventArgs e)
{
  var paths = e?.Data?.GetData(DataFormats.FileDrop) as string[];
  if (paths?.Length == 1)
  {
    var target = sender as TextBox;
    if (target != null)
    {
      target.Text = paths[0];
    }
  }
}
// フォルダ選択ダイアログ表示 - テキストボックス反映
private void OneFolder_Selection(object? sender, EventArgs e, TextBox target)
{
  // 初期選択値
  var path = target.Text.Trim();

  // フォルダ選択ダイアログ
  var dlg = new Microsoft.Win32.OpenFolderDialog
  {
    Multiselect = false,
    Title = "フォルダを選択してください"
  };
  // 初期選択値があれば設定
  if (!string.IsNullOrEmpty(path))
  {
    if (Directory.Exists(path))
    {
      dlg.InitialDirectory = path;
      dlg.FolderName = path;
    }
  }
  // フォルダ選択ダイアログ表示
  if (dlg.ShowDialog() == true)
  {
    // 選択値で更新
    target.Text = dlg.FolderName;
  }
}
```

### WPF - .NET Framework 4.8 + CommonOpenFileDialog

```CSharp
using Microsoft.WindowsAPICodePack.Dialogs;
```

```CSharp
txtFolder1.AllowDrop = true;
txtFolder1.PreviewDragOver += OneFolder_PreviewDragOver;
txtFolder1.Drop += OneFolder_Drop;
btnFolder1.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder1);

txtFolder2.AllowDrop = true;
txtFolder2.PreviewDragOver += OneFolder_PreviewDragOver;
txtFolder2.Drop += OneFolder_Drop;
btnFolder2.Click += (sender, e) => OneFolder_Selection(sender, e, txtFolder2);
```

```CSharp
// Drag & Drop - 対象：フォルダ単一
private void OneFolder_PreviewDragOver(object sender, DragEventArgs e)
{
  // １つのフォルダがドラッグされている場合のみ対象
  if (e != null)
  {
    e.Effects = DragDropEffects.None;
    
    if (e.Data?.GetDataPresent(DataFormats.FileDrop) == true)
    {
      var paths = e.Data?.GetData(DataFormats.FileDrop) as string[];
      if (paths?.Length == 1)
      {
        if (Directory.Exists(paths[0]))
        {
          e.Effects = DragDropEffects.Copy;
        }
      }
    }
    e.Handled = true;
  }
}
private void OneFolder_Drop(object sender, DragEventArgs e)
{
  var paths = e?.Data?.GetData(DataFormats.FileDrop) as string[];
  if (paths?.Length == 1)
  {
    var target = sender as TextBox;
    if (target != null)
    {
      target.Text = paths[0];
    }
  }
}
// フォルダ選択ダイアログ表示 - テキストボックス反映
private void OneFolder_Selection(object sender, EventArgs e, TextBox target)
{
  // 初期選択値
  var path = target.Text.Trim();

  // フォルダ選択ダイアログ
  using (var dlg = new CommonOpenFileDialog())
  {
    dlg.IsFolderPicker = true;  // true:フォルダ選択  false:ファイル選択

    dlg.Multiselect = false;
    dlg.Title = "フォルダを選択してください";

    // 初期選択値があれば設定
    if (!string.IsNullOrEmpty(path))
    {
      if (Directory.Exists(path))
      {
        var info = new FileInfo(path);
        dlg.InitialDirectory = path;
        dlg.DefaultFileName = info.Name;
      }
    }
    // フォルダ選択ダイアログ表示
    if (dlg.ShowDialog() == CommonFileDialogResult.Ok)
    {
      // 選択値で更新
      path = dlg.FileName;
    }
  }
}
```