Windows Forms - NavigationView のようなサイドバー形式

## はじめに

UWP - NavigationView は、左端 もしくは 上端にナビゲーションメニューを配置し、選択された項目に応じてメインコンテンツを切り替えることができます。

https://learn.microsoft.com/ja-jp/windows/apps/design/controls/navigationview

WPF では、[Nuget - ModernWpfUI](https://www.nuget.org/packages/ModernWpfUI/) を導入することで、NavigationView を利用できます。

https://qiita.com/Kosen-amai/items/f103143c69f7abbdee52

本記事では、Windows Forms で、NavigationView のようなサイドバー形式とする手法について記載します。

### テスト環境

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

::: note
記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 `?` を削除してください。
:::

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

### 素材

アイコンとして下記を利用させて頂きました。

 - [無料アイコン素材｜ICON BOX](https://iconbox.fun/)

## デザイン

Windows Forms で NavigationView のようなレイアウト実現は、いくつかの手法が存在します。
本記事では、RadioButton と TabControl を用いて、NavigationView のような左端サイドバー形式レイアウトを実現しようと思います。

![sample-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/a83000f8-42b3-41b8-a31b-7df7d729b1bb.png)

::: note
本記事では、TabControl+Panel という実装としていますが、Qiita コメントで、個々の Panel を UserControl にする手法を提示されました。
確かに UserControl 化が正攻法で、TabControl+Panel は力業なので、実際に利用する場合は、正攻法をご検討ください。

TabControl+Panel を選択したのは、手軽な（標準コントールのみで）実装というアプローチでした。
:::

## 手順
### ベースとなるパネル配置

コンパクトなサンプルとしたいので、フォームサイズを 500, 300 とします。
サイドバー用 Panel - pnlNavigation を追加、Dock = Left として、幅を 150 とします。
次に、コンテンツ用 Panel - pnlContents を追加、Dock = Fill とします。

![Design-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/386f582c-4e0a-42ab-9f32-12b32c2dde2f.png)


### コンテンツ配置

デザイナ画面で、各コンテンツを編集することを目的に TabControl - tabControl1 を、pnlNavigation に Dock = Fill で配置します。
今回のサンプルにあわせて、タブをひとつ追加した後、それぞのタブに対して Panel を Dock = Fill で追加します。


![Design-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/8c7d3e21-fff5-4e44-a152-2f680f9d6874.png)

| 対象タブ  | 配置する Panel  |
| -------- | --------------- |
| tabPage1 | pnlUpload       |
| tabPage2 | pnlDownload     |
| tabPage3 | pnlSettings     |

::: note
デザイナ画面でタブを切り替えて、それぞれの Panel にコンテンツを構築（コントールを配置）します。
後述ソースコードで、上記 Panel を pnlContents の子コントールに再配置（Dock = Fill）して、tabControl1 は Visuble = False として無効化します。
ナビゲーションボタン選択で、ひとつの Panel のみ表示することで、コンテンツ切り替えを実現します。
このように、tabControl1 はデザイナ画面での編集だけのために利用します。
:::

それぞれの Panel に対して、パネル切り替えを識別するために Label を追加します。

![Design-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/ce73f0ad-25fc-4bd8-bbb7-d49e751ccfcb.png)

| 対象タブ  | 対象 Panel    | 配置する Label の Text   |
| -------- | ------------- | ----------------------- |
| tabPage1 | pnlUpload     | UPLOAD コンテンツ        |
| tabPage2 | pnlDownload   | DOWNLOAD コンテンツ      |
| tabPage3 | pnlSettings   | SETTINGS コンテンツ      |


### ナビゲーション配置

RadioButton は単一選択のコントールです。
この単一選択という特性を利用して、ナビゲーションボタンとして下記プロパティで配置します。

- Appearance = Appearance.Button
- AutoSize = false
- FlatStyle = FlatStyle.Flat
- FlatAppearance.BorderSize = 0
- Image = `<UPLOADアイコン>`
- ImageAlign = ContentAlignment.MiddleLeft
- Text = `UPLOAD`
- TextImageRelation = TextImageRelation.ImageBeforeText
- Locaion = `0, 0`
- Height = 32
- Width = 150（pnlNavigation.Width）

![Design-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/5546b15e-9f68-432c-a7e6-ad3d0438fca5.png)


コンテンツに対応する RadioButton を配置します。

| RadioButton  | Image  | Text     | Location |
| ------------ | ------ | -------- | -------- |
| rbUpload     | ![upload-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/d7613cf6-5055-4025-83ba-c5a194c48290.png)  | UPLOAD   | 0, 0     |
| rbDownload   | ![download-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/4155268e-0829-4169-8875-3af21e3ff8f0.png)  | DOWNLOAD | 0, 32    |
| rbSettings   | ![Setting-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/dc9b4612-a122-4089-954a-333ca7351f24.png)   | SETTINGS | 0, 64    |


![Design-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/50318dd0-b8c9-40be-bbfd-c4b508cb98d6.png)


### ソースコード

```CSharp:Form1.cs
public partial class Form1 : Form
{
  private List<Panel> lstPanels = new List<Panel>();
  private List<RadioButton> lstNavigate = new List<RadioButton>();

  public Form1()
  {
    InitializeComponent();

    // Panel と RadioButton の関連づけ
    // Navigate2Panel(string keyword)用
    pnlUpload.Tag = rbUpload.Text;
    pnlDownload.Tag = rbDownload.Text;
    pnlSettings.Tag = rbSettings.Text;

    // Panel
    lstPanels.Add(pnlUpload); 
    lstPanels.Add(pnlDownload);
    lstPanels.Add(pnlSettings);
    foreach(var panel  in lstPanels)
    {
      panel.Parent = pnlContents; // pnlContents の子コントールに設定
      // panel.Dock = DockStyle.Fill;   // サンプルではデザイナで設定
    }
    tabControl1.Visible = false;  // TabControl は不要

    // RadioButton
    lstNavigate.Add(rbUpload);
    lstNavigate.Add(rbDownload);
    lstNavigate.Add(rbSettings);
    int height = 32;
    int py = 0;
    foreach(var button in lstNavigate)
    {
      button.CheckedChanged += rbNavigate_CheckChanged;
      button.Location = new Point( 0, py );   // レイアウト再調整
      button.Width = pnlNavigation.Width;     // レイアウト再調整
      button.Height = height;                 // レイアウト再調整
      py += height;
    }

    // 初期選択
    lstNavigate[0].Checked = true;
  }
  // .NET Framework 時 object? の ? 不要
  private void rbNavigate_CheckChanged(object? sender, EventArgs e)
  {
    if (sender is RadioButton button)
    {
      Navigate2Panel(button.Text);
    }
  }
  // Navigate に連動して、対象 Panel を表示
  private void Navigate2Panel(string keyword)
  {
    bool bMatch = false;
    foreach (var panel in lstPanels)
    {
      // keyword に一致するか？
      if (panel.Tag is string tag
       && string.Compare(keyword, tag) == 0)
      {
        panel.Visible = true;
        bMatch = true;
      }
      else
      {
        panel.Visible = false;
      }
    }
    // 一致するモノがない場合は先頭を選択（あり得ない...）
    if (!bMatch)
    {
      lstPanels[0].Visible = true;
    }
  }
}
```