.NET 6とVSCodeでデスクトップアプリ（ランチャー）

# ■はじめに

`Visual Studio Code`（`VSCode`）と`.NET 6.0`の`WPF`でGUIアプリ（アプリケーションランチャー）を作ります。
![00_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/02ec1b34-602c-d01f-068e-bcb5ce6f05ca.png)

# ■環境

* Windows 11（Version 21H2）
* .NET 6.0～
* Visual Studio Code

# ■準備

## ◇.NET SDKのインストール

* [Download .NET](https://dotnet.microsoft.com/download)

`Windows` - `.NET 6.x`の`Download .NET SDK x64`を選択します。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/4e8fb0e9-083c-cee8-2445-3088b9bf962a.png)

インストールします。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/131bdd0e-18f5-ebb2-728e-3f5e1374b066.png)

## ◇VSCodeのインストール、起動

* [Visual Studio Code - Code Editing. Redefined](https://code.visualstudio.com)

VSCodeをインストールして起動してください。

# ■プロジェクトの作成

左のアクティビティ バーの「エクスプローラー」を選択し、「フォルダーを開く」ボタンを押します。
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/650ae69d-a795-cd76-9a57-ab53407384c7.png)

任意の作業フォルダを開き、新しいフォルダー `WrapLauncher`を作成し、そのフォルダを選択します。
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/4c138195-198d-6392-5d1c-b4057a0948a4.png)
![05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/0d4f3e13-76cd-5255-c95f-f13c62e643c5.png)

![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/4a571093-7428-a527-c0e4-d20c9e9433fc.png)

ターミナルが表示されていない場合はメニューの「表示」-「ターミナル」でターミナルを表示します。
![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/14fe155a-885b-bdac-533c-22fde59c0343.png)

ターミナルに以下のコマンドを入力し、バージョンが6.xであることを確認します。

```powershell
dotnet --version
```

![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/65536d71-02d0-d3fb-26ee-82808cd36d36.png)

ターミナルに以下のコマンドを入力し、プロジェクトを作成します。

```powershell
dotnet new wpf
```

![09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/d94a4671-664b-a582-5d1a-c908b29141c4.png)

# ■とりあえず実行

ターミナルで以下のコマンドを実行します。

```powershell
dotnet run
```

無地のウィンドウが起動します。
![10_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/67f7015b-ce37-a195-ab5f-f5963488595a.png)

最大化ボタンのスナップレイアウトメニューが表示される、標準的なウィンドウです。
一通り動作確認したら閉じてください。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/f1aef908-5bb2-8de1-1169-52b182bfdfa2.png)

# ■画面の作成

「MainWindow.xaml」を開き、画面を作成します。

```xml:MainWindow.xaml
<Window
    x:Class="WrapLauncher.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:local="clr-namespace:WrapLauncher"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="スタート"
    Width="800"
    Height="450"
    Background="WhiteSmoke"
    Closing="Window_Closing"
    Loaded="Window_Loaded"
    ResizeMode="CanResizeWithGrip"
    mc:Ignorable="d">

    【リソース】

    【コンテキストメニュー】

    <ScrollViewer Margin="0,0,0,16">
        <!--  この中にボタン等を追加していく  -->
        <StackPanel x:Name="MainContainer" />
    </ScrollViewer>
</Window>
```

【リソース】部分には以下を入力します。

```xml:MainWindow.xaml【リソース】部分
    <Window.Resources>
        <!--  ボタンのスタイル  -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="5" />

            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <!--  角丸ボタン  -->
                        <Border
                            Padding="5,3"
                            Background="{TemplateBinding Background}"
                            BorderBrush="Black"
                            BorderThickness="1"
                            CornerRadius="3">
                            <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>

            <Style.Triggers>
                <!--  マウスオーバー時のスタイル  -->
                <Trigger Property="IsMouseOver" Value="true">
                    <!--  背景色  -->
                    <Setter Property="Background" Value="#bee6fd" />
                </Trigger>
                <!--  押されたときのスタイル  -->
                <Trigger Property="IsPressed" Value="true">
                    <!--  背景色  -->
                    <Setter Property="Background" Value="#88d3fd" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  グループ見出しのスタイル  -->
        <Style x:Key="GroupTitleStyle" TargetType="TextBlock">
            <Setter Property="Margin" Value="5,10,5,0" />
            <Setter Property="FontWeight" Value="Bold" />
            <Setter Property="Foreground" Value="DarkBlue" />
        </Style>
    </Window.Resources>
```

【コンテキストメニュー】部分には以下を入力します。

```xml:MainWindow.xaml【コンテキストメニュー】部分
    <Window.ContextMenu>
        <!--  コンテキストメニュー  -->
        <ContextMenu x:Name="WindowContextMenu">
            <MenuItem Click="MenuReload_Click" Header="設定再読み込み(_R)" />
            <MenuItem Click="MenuFolderOpen_Click" Header="ランチャーの場所を開く(_O)" />

            <Separator />

            <MenuItem
                x:Name="MinimizedMenuItem"
                Header="アプリを起動したらランチャー最小化(_M)"
                IsCheckable="True"
                IsChecked="False"
                ToolTip="※Ctrlキーを押しながらアプリを起動した場合は最小化しません" />

            <StackPanel x:Name="ScaleMenu" Orientation="Horizontal">
                <TextBlock Margin="0,0,10,0" Text="表示倍率" />
                <RadioButton
                    x:Name="DefaultScaleItem"
                    Margin="5,0"
                    Click="ScaleItem_Click"
                    Content="1.0"
                    IsChecked="True" />
                <RadioButton
                    Margin="5,0"
                    Click="ScaleItem_Click"
                    Content="1.5" />
                <RadioButton
                    Margin="5,0"
                    Click="ScaleItem_Click"
                    Content="2.0" />
                <RadioButton
                    Margin="5,0"
                    Click="ScaleItem_Click"
                    Content="3.0" />
            </StackPanel>

            <Separator />

            <MenuItem
                x:Name="InfoMenuItem"
                Click="MenuInfo_Click"
                Header="情報(_I)" />

            <Separator />

            <MenuItem Click="MenuHelp_Click" Header="ヘルプ(_H)" />
        </ContextMenu>
    </Window.ContextMenu>
```

# ■ロジックの作成

Appクラスに設定ファイル関連の処理を書きます。
設定ファイルはアプリケーション設定と、ランチャー定義（起動ボタン等）の2種類です。

```csharp:App.xaml.cs
using System;
using System.IO;
using System.Windows;

namespace WrapLauncher
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        /// <summary>
        /// アプリケーション設定ファイル名
        /// </summary>
        public const string AppSettingsFileName = "appsettings.json";

        /// <summary>
        /// ランチャー定義ファイル名
        /// </summary>
        public const string LaunchDefFileName = "WrapLauncher.path";

        /// <summary>
        /// アプリケーションパス取得
        /// </summary>
        /// <returns></returns>
        public static string GetAppPath()
        {
            string? appPath = AppContext.BaseDirectory;
            if (appPath is null)
            {
                throw new DirectoryNotFoundException("実行ファイルのパス取得失敗");
            }

            return appPath;
        }

        /// <summary>
        /// アプリケーション設定ファイルパス取得
        /// </summary>
        /// <returns></returns>
        public static string GetAppSettingsFilePath()
        {
            return Path.Combine(GetAppPath(), AppSettingsFileName);
        }

        /// <summary>
        /// ランチャー定義ファイルパス取得
        /// </summary>
        /// <returns></returns>
        public static string GetLaunchDefFilePath()
        {
            return Path.Combine(GetAppPath(), LaunchDefFileName);
        }
    }
}
```

設定ファイル関連のコードを入れる`Settings`フォルダを作成します。
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/82b8fa3c-cd35-4597-0b67-d05c17b8dd50.png)
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/e7f5484c-708d-e645-0811-06d4714eb649.png)

Settingsフォルダの中に`AppSettings.cs`を作成します。
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/29caca6d-cfa7-c84f-f463-706ea1102f05.png)
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/345a5e13-52e9-5d9c-602c-253071cc0971.png)

```csharp:AppSettings.cs
namespace WrapLauncher.Settings
{
    /// <summary>
    /// アプリケーション設定
    /// </summary>
    public class AppSettings
    {
        /// <summary>
        /// アプリ起動後にランチャー最小化
        /// </summary>
        public bool MinimizedAfterLaunch { get; set; } = false;

        /// <summary>
        /// 表示倍率
        /// </summary>
        /// <value></value>
        public double Scale { get; set; } = 1.0;
    }
}
```

同様にSettingsフォルダの中に以下を作成します。

* `AppSettingsReader.cs`
* `AppSettingsWriter.cs`
* `LauncherDefinition.cs`

![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/3e22cb83-b109-106a-6d65-4d8ecfb9a5f7.png)


```csharp:AppSettingsReader.cs
using System.IO;
using System.Text.Json;

namespace WrapLauncher.Settings
{
    /// <summary>
    /// アプリケーション設定読み込み
    /// </summary>
    public class AppSettingsReader
    {
        /// <summary>
        /// ファイル存在確認
        /// </summary>
        /// <returns></returns>
        public bool ExistsFile()
        {
            return File.Exists(App.GetAppSettingsFilePath());
        }

        /// <summary>
        /// ファイル読み込み
        /// </summary>
        /// <returns>設定情報</returns>
        public AppSettings ReadFromFile()
        {
            if (!ExistsFile())
            {
                // ファイルがなければ初期値を返す
                return new AppSettings();
            }

            // 読み込み
            string jsonStr = File.ReadAllText(App.GetAppSettingsFilePath());
            var stg = JsonSerializer.Deserialize<AppSettings>(jsonStr);

            return stg ?? new AppSettings();
        }
    }
}
```

```csharp:AppSettingsWriter.cs
using System.IO;
using System.Text.Json;
using JE = System.Text.Encodings.Web;
using UN = System.Text.Unicode;

namespace WrapLauncher.Settings
{
    /// <summary>
    /// アプリケーション設定書き込み
    /// </summary>
    public class AppSettingsWriter
    {
        /// <summary>
        /// ファイル書き込み
        /// </summary>
        /// <param name="stg"></param>
        public void WriteToFile(AppSettings stg)
        {
            var opt = new JsonSerializerOptions
            {
                // シリアライズするUnicodeの範囲
                Encoder = JE.JavaScriptEncoder.Create(UN.UnicodeRanges.All),
                // インデントする
                WriteIndented = true,
            };

            // JSONオブジェクトを文字列化
            string jsonStr = JsonSerializer.Serialize(stg, opt);
            // ファイル書き込み
            File.WriteAllText(App.GetAppSettingsFilePath(), jsonStr);
        }
    }
}
```

```csharp:LauncherDefinition.cs
using System.Collections.Generic;

namespace WrapLauncher.Settings
{
    /// <summary>
    /// ランチャー定義情報
    /// </summary>
    public class LauncherDefinition
    {
        /// <summary>
        /// 区切り文字
        /// </summary>
        public const char Delimiter = '\t';

        /// <summary>
        /// グループ見出しの先頭記号
        /// </summary>
        public const string GroupTitleHeader = "//";

        /// <summary>
        /// グループ見出しのカラム位置
        /// </summary>
        public const int GroupTitleColumnIndex = 0;

        /// <summary>
        /// カラム位置
        /// </summary>
        public static IReadOnlyDictionary<string, int> Columns = new Dictionary<string, int>
        {
            {"Color", 0},
            {"ButtonTitle", 1},
            {"Path", 2},
        };

        /// <summary>
        /// グループ見出し判定
        /// </summary>
        /// <param name="values"></param>
        /// <returns>グループ見出しならtrue</returns>
        public bool IsGroupTitle(string[] values)
        {
            return values[GroupTitleColumnIndex].StartsWith(GroupTitleHeader);
        }

        /// <summary>
        /// グループ見出し取得
        /// </summary>
        /// <param name="values"></param>
        /// <returns></returns>
        public string GetGroupTitle(string[] values)
        {
            return values[GroupTitleColumnIndex].Substring(GroupTitleHeader.Length);
        }

    }
}
```

プロジェクトの直下に`ShellExecution.cs`を作成します。
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/699b225c-61fa-b8f4-5411-4d8a222dd239.png)

```csharp:ShellExecution.cs
using System.Diagnostics;

namespace WrapLauncher
{

    public static class ShellExecution
    {
        /// <summary>
        /// プログラム実行
        /// </summary>
        /// <param name="cmd">実行するコマンド</param>
        public static void Run(string cmd)
        {
            var p = new Process();
            p.StartInfo.FileName = cmd;
            p.StartInfo.UseShellExecute = true;
            // 実行
            p.Start();
        }
    }

}
```

メインの処理を書きます。

```csharp:MainWindow.xaml.cs
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using WrapLauncher.Settings;

namespace WrapLauncher
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly LauncherDefinition _btnDef = new();

        // ヘルプテキスト
        private const string HelpText = @"設定ファイルはEXEと同じ場所に「WrapLauncher.path」のファイル名で配置する。
文字コードはBOM無しのUTF8。

データ構造
------------------------------
//グループ見出し
色名  ボタンテキスト   起動プログラム/フォルダのフルパス
色名  ボタンテキスト   起動プログラム/フォルダのフルパス
：
//グループ見出し
色名  ボタンテキスト   起動プログラム/フォルダのフルパス
：
------------------------------
グループ見出しは先頭「//」で始める。
色名、ボタンテキスト、起動するパスはTABで区切る。

[色名一覧]
";

        private int _buttonCount = 0;
        private int _groupCount = 0;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindow()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 起動時
        /// </summary>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                // アプリケーション設定読み込み、画面に反映
                LoadAppSettings();
                // ボタン設定読み込み、画面に反映
                LoadLauncherDef();
            }
            catch (Exception ex)
            {
                ShowException(ex);
            }
        }

        /// <summary>
        /// 終了時
        /// </summary>
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            try
            {
                // アプリケーション設定保存
                SaveAppSettings();
            }
            catch (Exception ex)
            {
                MessageBox.Show(
                    $"アプリケーション設定の保存に失敗しました。\n{ex}",
                    "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        /// <summary>
        /// コンテキストメニュー「設定再読み込み」
        /// </summary>
        private void MenuReload_Click(object sender, RoutedEventArgs e)
        {
            // 画面クリア
            MainContainer.Children.Clear();

            try
            {
                // 設定読み込み、画面に反映
                LoadLauncherDef();
            }
            catch (Exception ex)
            {
                ShowException(ex);
            }
        }

        /// <summary>
        /// コンテキストメニュー「ランチャーの場所を開く」
        /// </summary>
        private void MenuFolderOpen_Click(object sender, RoutedEventArgs e)
        {
            string appPath = App.GetAppPath();
            Execute(appPath);
        }

        /// <summary>
        /// コンテキストメニュー「表示倍率」
        /// </summary>
        private void ScaleItem_Click(object sender, RoutedEventArgs e)
        {
            string? scaleStr = (sender as RadioButton)?.Content?.ToString();
            if (!double.TryParse(scaleStr, out double scale))
            {
                scale = 1.0;
                DefaultScaleItem.IsChecked = true;
            }
            SetContainerScale(scale);
            WindowContextMenu.IsOpen = false;
        }

        /// <summary>
        /// コンテナ拡大縮小処理
        /// </summary>
        /// <param name="scaleXY"></param>
        private void SetContainerScale(double scaleXY)
        {
            MainContainer.LayoutTransform = new ScaleTransform(scaleXY, scaleXY);
        }

        /// <summary>
        /// コンテキストメニュー「情報」
        /// </summary>
        private void MenuInfo_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show($"グループ数：{_groupCount}\nボタン数：{_buttonCount}", "情報");
        }

        /// <summary>
        /// コンテキストメニュー「ヘルプ」
        /// </summary>
        private void MenuHelp_Click(object sender, RoutedEventArgs e)
        {
            ShowHelp();
        }

        /// <summary>
        /// エラー情報表示
        /// </summary>
        private void ShowException(Exception ex)
        {
            MainContainer.Children.Clear();

            var txt = new TextBox
            {
                TextWrapping = TextWrapping.Wrap,
                Margin = new Thickness(5),
                IsReadOnly = true,
                Text = ex.ToString()
            };

            MainContainer.Children.Add(txt);
        }

        /// <summary>
        /// ヘルプ内容表示
        /// </summary>
        private void ShowHelp()
        {
            MainContainer.Children.Clear();

            var helpText = new RichTextBox
            {
                IsReadOnly = true,
                FontSize = 16
            };

            // テキストの説明文追加
            helpText.Document.Blocks.Add(new Paragraph(new Run(HelpText)));

            var bc = new BrushConverter();
            var docList = new System.Windows.Documents.List();

            // 色名一覧作成
            var bList = typeof(Brushes).GetProperties()
                .Where(x => x.Name != "Transparent")
                .OrderBy(x => x.Name);
            foreach (var b in bList)
            {
                var li = new ListItem();
                var p = new Paragraph();
                var r = new Run("■ ")
                {
                    Foreground = (Brush)bc.ConvertFromString(b.Name)!
                };
                p.Inlines.Add(r);
                p.Inlines.Add(new Run(b.Name));

                li.Blocks.Add(p);
                docList.ListItems.Add(li);
            }

            helpText.Document.Blocks.Add(docList);
            MainContainer.Children.Add(helpText);
        }

        /// <summary>
        /// アプリケーション設定読み込み、画面に反映
        /// </summary>
        private void LoadAppSettings()
        {
            var asr = new AppSettingsReader();
            SetScreen(asr.ReadFromFile());
        }

        /// <summary>
        /// アプリケーション設定を画面に反映
        /// </summary>
        /// <param name="appStg"></param>
        private void SetScreen(AppSettings appStg)
        {
            // アプリを起動したらランチャー最小化
            MinimizedMenuItem.IsChecked = appStg.MinimizedAfterLaunch;
            // 表示倍率
            DefaultScaleItem.IsChecked = true;
            foreach (var child in LogicalTreeHelper.GetChildren(ScaleMenu))
            {
                if (child is RadioButton radio)
                {
                    if (double.TryParse(radio.Content.ToString(), out double scale))
                    {
                        if (scale == appStg.Scale)
                        {
                            radio.IsChecked = true;
                            SetContainerScale(scale);
                            break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// アプリケーション設定保存
        /// </summary>
        private void SaveAppSettings()
        {
            // 画面から設定取得
            var appStg = GetScreen();

            var asw = new AppSettingsWriter();

            // 書き込み
            asw.WriteToFile(appStg);
        }

        /// <summary>
        /// 画面からアプリケーション設定取得
        /// </summary>
        /// <returns></returns>
        private AppSettings GetScreen()
        {
            var appStg = new AppSettings
            {
                // アプリを起動したらランチャー最小化
                MinimizedAfterLaunch = MinimizedMenuItem.IsChecked
            };
            // 表示倍率
            double scale = 1.0;
            foreach (var child in LogicalTreeHelper.GetChildren(ScaleMenu))
            {
                if (child is RadioButton radio)
                {
                    if (radio.IsChecked == true)
                    {
                        if (double.TryParse(radio.Content.ToString(), out scale))
                        {
                            break;
                        }
                    }
                }
            }
            appStg.Scale = scale;

            return appStg;
        }

        /// <summary>
        /// ランチャー定義読み込み、画面に反映
        /// </summary>
        private void LoadLauncherDef()
        {
            // 設定ファイルのフルパス取得
            string filePath = App.GetLaunchDefFilePath();

            // 設定ファイルが見つからなければ終了
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException("設定ファイルなし");
            }

            // 設定ファイル読み込み
            using var reader = new StreamReader(filePath);
            WrapPanel? btnContainer = null;

            _buttonCount = 0;
            _groupCount = 0;

            while (!reader.EndOfStream)
            {
                // TABで分解
                var item = reader.ReadLine()?.Split(LauncherDefinition.Delimiter);
                if (item is null ||
                    item.Length < 1)
                {
                    // データなしの行
                    continue;
                }

                /*
                MainContainer           ..... StackPanel
                    grpContainer        ..... StackPanel
                        見出し          ..... TextBlock
                        btnContainer    ..... WrapPanel
                            ボタン
                            ボタン
                            ：
                    grpContainer        ..... StackPanel
                        見出し          ..... TextBlock
                        btnContainer    ..... WrapPanel
                            ボタン
                            ボタン
                            ：
                */

                // グループ作成するローカル関数
                void MakeGroup(ref WrapPanel? btnContainer, string grpTitle = "")
                {
                    // グループコンテナ作成
                    var grpContainer = new StackPanel();
                    // グループコンテナをメインコンテナに追加
                    MainContainer.Children.Add(grpContainer);

                    if (string.IsNullOrEmpty(grpTitle) == false)
                    {
                        // グループ見出しを生成し、グループコンテナに追加
                        grpContainer.Children.Add(CreateGroupTitle(grpTitle));
                    }

                    // ボタンコンテナを作成
                    btnContainer = new WrapPanel();
                    // ボタンコンテナをグループコンテナに追加
                    grpContainer.Children.Add(btnContainer);
                    _groupCount++;
                }

                if (_btnDef.IsGroupTitle(item))
                {
                    // 見出し

                    // グループ作成
                    MakeGroup(ref btnContainer, _btnDef.GetGroupTitle(item));
                }
                else if (item.Length == LauncherDefinition.Columns.Count)
                {
                    // ボタン

                    // まだボタンコンテナが作成されていない？
                    if (btnContainer is null)
                    {
                        // グループ作成（見出し無し）
                        MakeGroup(ref btnContainer);
                    }

                    // ボタン作成
                    Button btn = CreateLaunchButton(
                        item[LauncherDefinition.Columns["Color"]],
                        item[LauncherDefinition.Columns["ButtonTitle"]],
                        item[LauncherDefinition.Columns["Path"]]);
                    // ボタンコンテナにボタンを追加
                    btnContainer?.Children.Add(btn);
                    _buttonCount++;
                }
                else
                {
                    throw new Exception($"カラム数不正\n{string.Join(LauncherDefinition.Delimiter, item)}");
                }
            }

            if (_buttonCount == 0 &&
                _groupCount == 0)
            {
                throw new Exception("定義内容なし");
            }
        }

        /// <summary>
        /// グループ見出し生成
        /// </summary>
        /// <param name="title"></param>
        /// <returns></returns>
        private TextBlock CreateGroupTitle(string title)
        {
            var txt = new TextBlock
            {
                Style = (Style)(this.Resources["GroupTitleStyle"]),
                Text = title
            };

            return txt;
        }

        /// <summary>
        /// ボタン作成
        /// </summary>
        /// <param name="colorName">色名</param>
        /// <param name="text">ボタンテキスト</param>
        /// <param name="execute">起動プログラムのファイルパス</param>
        private Button CreateLaunchButton(string colorName, string text, string execute)
        {
            var btn = new Button();
            var txtContainer = new StackPanel
            {
                Orientation = Orientation.Horizontal
            };
            // ■テキスト作成
            var txtMark = new TextBlock
            {
                Text = "■",
                Margin = new Thickness(0, 0, 2, 0)
            };

            try
            {
                // 色指定が有効なら■の色を変える。無効な色名なら例外発生で終了させる。
                var bcnv = new BrushConverter();
                txtMark.Foreground = (Brush)bcnv.ConvertFromString(colorName)!;
            }
            catch
            {
                throw new Exception($"無効な色名[{colorName}]");
            }

            txtContainer.Children.Add(txtMark);

            // ボタン名テキスト作成
            var txt = new TextBlock
            {
                Text = text
            };
            txtContainer.Children.Add(txt);

            // ボタンテキスト設定
            btn.Content = txtContainer;
            // ボタンクリック時の処理
            btn.Click += (_, _) =>
            {
                if (Execute(execute) &&
                    MinimizedMenuItem.IsChecked &&
                    !Keyboard.IsKeyDown(Key.LeftCtrl) &&
                    !Keyboard.IsKeyDown(Key.RightCtrl))
                {
                    // ウィンドウ最小化（Ctrl+クリックの場合は最小化しない）
                    WindowState = WindowState.Minimized;
                }
            };

            return btn;
        }

        /// <summary>
        /// プログラム実行
        /// </summary>
        /// <param name="cmd">実行するコマンド</param>
        /// <returns>成否</returns>
        private bool Execute(string cmd)
        {
            try
            {
                ShellExecution.Run(cmd);
                return true;
            }
            catch
            {
                MessageBox.Show(
                    $"起動に失敗しました。\n{cmd}", "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
                return false;
            }
        }

    }
}
```

ここまでで一度ビルドしてエラーがないか確認しておきます。
ソースファイルを保存し、ターミナルで以下のコマンドを実行します。

```powershell
dotnet build
```

![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/bb11555e-7be1-2eaf-010a-a7725a6d1c27.png)


# ■ランチャー定義ファイルの作成
「bin\Debug\net6.0-windows」に`WrapLauncher.path`という名前でファイルを作成します。
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/b9e45ad0-e99b-c44c-237f-3d3bbdc58778.png)

定義ファイルに起動したいフォルダパスやドキュメントファイルパス、プログラムパスなどを記述します。
ファイルの文字コードはBOM無しUTF-8。
色名、ボタン名、パスの間はTAB区切りです。
色名には`Green`や`Blue`などを書きます。

:::note info
使える色名の一覧は、プログラム完成後、ヘルプページで見ることができます。
:::

```text:定義ファイルのフォーマット
//グループ見出しA
色名	ボタン名1	パス1
色名	ボタン名2	パス2
:
//グループ見出しB
色名	ボタン名3	パス3
:
```

定義ファイル入力例）

| 色名         | ボタン名                | パス                               |
| ------------ | ----------------------- | ---------------------------------- |
| CadetBlue   | ダウンロード↓ | shell:Downloads   |
| DeepSkyBlue | デスクトップ | shell:Desktop     |
| Silver      | ドキュメント | shell:Personal    |
| DodgerBlue  | ピクチャ     | shell:My Pictures |
| BlueViolet  | ビデオ       | shell:My Video    |
| Chocolate   | ミュージック♪ | shell:My Music    |
| Silver | Cドライブ                 | C:\                           |
| Silver | Dドライブ                 | D:\                           |
| Gold   | Local                     | shell:Local AppData           |
| Gold   | Apps/2.0                  | shell:Local AppData\Apps\2.0  |
| Gold   | Roaming                   | shell:AppData                 |
| Gold   | Program Files (x86)       | shell:ProgramFilesX86         |
| Gold   | Program Files             | shell:ProgramFiles            |
| Gold   | プロファイル👤            | shell:Profile                 |
| Gold   | スタート/プログラム       | shell:Programs                |
| Gold   | プログラム/スタートアップ | shell:Startup                 |
| Gold   | 送る                      | shell:SendTo                  |
| Gold   | スクリーンショット📷      | shell:My Pictures\Screenshots |
| Gold   | drivers/etc               | shell:System\drivers\etc      |
| Gray         | 電卓               | shell:AppsFolder\Microsoft.WindowsCalculator_8wekyb3d8bbwe!App     |
| LightSkyBlue | メモ帳             | notepad.exe                                                        |
| SkyBlue      | ペイント🎨         | mspaint.exe                                                        |
| ForestGreen  | Excel              | excel.exe                                                          |
| RoyalBlue    | Word               | Winword.exe                                                        |
| Coral        | PowerPoint         | Powerpnt.exe                                                       |
| DodgerBlue   | Outlook            | Outlook.exe                                                        |
| Purple       | OneNote            | Onenote.exe                                                        |
| Black        | コマンドプロンプト | cmd.exe                                                            |
| DodgerBlue   | Win PowerShell     | PowerShell.exe                                                     |
| Navy         | PowerShell         | pwsh.exe                                                           |
| DimGray      | Win Terminal       | wt.exe                                                             |
| White        | MSストア           | shell:AppsFolder\Microsoft.WindowsStore_8wekyb3d8bbwe!App          |
| SkyBlue | コンパネ                            | shell:ControlPanelFolder           |
| Gray    | システムの詳細設定               | SystemPropertiesAdvanced.exe                |
| Gray    | システム/ディスプレイ               | ms-settings:display                |
| Gray    | システム/バージョン情報             | ms-settings:about                  |
| Gray    | 個人用設定/色                       | ms-settings:personalization-colors |
| Gray    | 個人用設定/スタート                 | ms-settings:personalization-start  |
| Gray    | アプリ/アプリと機能                 | ms-settings:appsfeatures           |
| Gray    | 時刻と言語/言語と地域/Microsoft IME | ms-settings:regionlanguage-jpnime  |

TAB入力でSPACEになってしまう場合は、「ファイル」-「ユーザー設定」-「設定」で検索ボックスに
`insert`を入力し、「Editor:Insert Spaces」のチェックを外してください。

ついでに`white`で検索して「Editor:Render Whitespace」を`all`にしておくとタブ文字が見えるようになります。

![21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/1687a62a-b993-9bfc-3af0-3ffb611dd994.png)

# ■実行

定義ファイルを保存したら実行します。
ターミナルで以下のコマンドを実行してください。

```powershell
dotnet run
```

![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/20bee901-0ece-c223-1b7f-6dff7e4f9a38.png)

ウィンドウの幅を狭めるとボタンが折り返して表示されます。
![23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/985d5556-ed10-b862-fd19-a6212834fd61.png)

右クリックからヘルプ表示。
元の画面に戻るには右クリックから「設定再読み込み」。
![24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/57fd0892-3e6d-3a95-6200-a8b16bb6d292.png)

ボタンを大きく表示するには右クリックから「表示倍率」を変更します。
![25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/9e6a17ff-4fbc-086c-014b-6d9eda59db8e.png)

リリースビルドするときはターミナルで以下のコマンドを実行します。

```powershell
dotnet build -c Release
```

「Debug」フォルダの「WrapLauncher.path」を「Release」フォルダにもコピーしておきます。
「WrapLauncher.exe」を実行すればランチャーが起動します。
![26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/d36ec800-8d10-a52a-2a10-a90363c79ff5.png)

# ■おまけ

## ◇アイコンの設定

実行ファイルとウィンドウ左上のアイコンを設定します。
やり方は、アイコンを用意（[参考](https://qiita.com/Kosen-amai/items/4700100342c76f9fda78)）し、プロジェクトのフォルダに配置、プロジェクトファイルに記述を追加します。

![27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/f4869609-32b0-e065-abfc-76321fe537d1.png)

アイコンファイル名は任意の名前を付けられます。

```xml:WrapLauncher.csproj（追加分）
  <PropertyGroup>
    <ApplicationIcon>appIcon.ico</ApplicationIcon>
  </PropertyGroup>
```

## ◇お勧めの使い方

WrapLauncher.exeを右クリックして「タスクバーにピン留め」します。
![30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/35785226-369f-c039-3b02-f8bdb858791d.png)

ピン留めされたランチャーを一番左端に移動します。
![31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/51ee946f-5bd1-4b33-a000-27934a229b83.png)

ランチャーを<kbd>Shift</kbd>を押しながら右クリックし、プロパティを表示します。
![32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/8fd081f0-7ad6-a5d6-644d-ad2429414949.png)

「実行時の大きさ」を「最大化」に設定します。
![33_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/570f933d-c9fe-7a55-e297-f57b4bf254fa.png)

これで<kbd><kbd>Win</kbd> + <kbd>1</kbd></kbd>でランチャーが起動して
スタートメニューと同じような使用感で使えます。

![40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142879/ba81f05e-d053-75e7-e31a-2dfa4932ebaf.png)

# ■参考URL

## ◇発展形

https://3xkesgjqsmeqlafumv9qikf8i9y7bf1d6njguxg.github.io/spacious-start-menu/index-ja.html