LoginSignup
32
42

More than 1 year has passed since last update.

.NET 5とVSCodeでデスクトップアプリ(ランチャー)

Last updated at Posted at 2020-12-31

■はじめに

⚠️新しい記事があります。

Visual Studio CodeVSCode)と.NET 5.0WPFでGUIアプリ(アプリケーションランチャー)を作ります。
000_4.png

■環境

  • Windows 10(Version 20H2)
  • .NET 5.0~
  • Visual Studio Code

■準備

◇.NET SDKのインストール

.NET 5.xのリンクを選択します。
100.png
SDK 5.x - Windows - Installersx64のリンクを選択します。
105.png
インストールします。
110.png

◇VSCodeのインストール、日本語化

VSCodeをインストールします。
エクスプローラーのディレクトリコンテキストメニューに[Codeで開く]アクションを追加する」はチェックを入れておいてください。
他のチェックは任意です。
125.png

VSCodeが起動したら、左のアクティビティ バーの「拡張機能」を選択し、検索ボックスにjapaneseと入力、
「Japanese Language Pack for VS Code」をインストールします。
135.png

「Restart Now」でVSCodeを再起動します。
140.png

日本語化されました。
VSCodeをいったん終了します。
145.png

◇プロジェクトフォルダの作成、VSCodeの起動

エクスプローラーでプロジェクトを格納するフォルダ、WrapLauncherを作成します。
WrapLauncherフォルダ内で右クリックし、「Codeで開く」を選択してVSCodeを起動します。
130.png

■プロジェクトの作成

ターミナルが表示されていない場合はメニューの「表示」-「ターミナル」でターミナルを表示します。

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

dotnet --version

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

dotnet new wpf

150.png

■拡張機能インストール

左のファイル一覧から「MainWindow.xaml.cs」をクリックします。
右下に「C#にお勧めの拡張機能」通知が表示された場合は「インストール」してください。
155.png

「Required assets to ....」(ビルドとデバッグに必要なアセットがありません。追加しますか?)の通知が表示されたら「Yes」を選択してください。
160.png

アクティビティ バーで「拡張機能」を選択し、検索ボックスにxamlと入力し、「Pretty XML」をインストールします。
これはXAMLの整形用に使います。
165.png

■とりあえず実行

◇デバッグ設定修正

デバッグの設定を修正します。
アクティビティ バーから「実行」を選択し、上の歯車マークを選択します。
170.png
「launch.json」が開くので、「program」のパスの「Debug」フォルダの後ろを
net5.0-windows/WrapLauncher.exeに修正し、保存します。
175.png

◇実行

▷の「デバッグの開始」ボタンを押すか、F5キーを押してデバッグ実行します。
空の画面が表示されました。
180.png
起動した画面を閉じてください。

■プロジェクト設定

アクティビティ バーで「エクスプローラー」を選択し、「WrapLauncher.csproj」を選択します。
185.png
「ProjectGroup」の中に<Nullable>enable</Nullable>を入力して保存します。
190_2.png

■画面の作成

「MainWindow.xaml」を開き、画面を作成します。
作成し終わったらF1でコマンドパレットを表示し、xmlと入力して「Prettify XML」を実行します。
195.png
XAMLが整形されました。
200.png

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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WrapLauncher"
        mc:Ignorable="d"
        Title="ランチャー"
        Height="450"
        Width="800"
        ResizeMode="CanResizeWithGrip"
        Loaded="Window_Loaded">
  <Window.Resources>
    <!-- ボタンのスタイル -->
    <Style TargetType="Button">
      <Setter Property="Margin"
              Value="5" />
      <Setter Property="Padding"
              Value="3" />
    </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>
  <Window.ContextMenu>
    <!-- コンテキストメニュー -->
    <ContextMenu>
      <MenuItem Header="設定再読み込み(_R)"
                Click="MenuReload_Click" />
      <MenuItem Header="ランチャーの場所を開く(_O)"
                Click="MenuFolderOpen_Click" />
      <Separator />
      <MenuItem x:Name="MinimizedMenuItem"
                Header="アプリを起動したらランチャー最小化(_M)"
                IsCheckable="True"
                IsChecked="False"
                ToolTip="※Ctrlキーを押しながらアプリを起動した場合は最小化しません" />
      <Separator />
      <MenuItem Header="ヘルプ(_H)"
                Click="MenuHelp_Click" />
    </ContextMenu>
  </Window.ContextMenu>
  <ScrollViewer Margin="0,0,0,16">
    <!-- この中にボタン等を追加していく -->
    <StackPanel x:Name="MainContainer" />
  </ScrollViewer>
</Window>

■ロジックの作成

App.xaml.cs
using System.IO;
using System.Windows;

namespace WrapLauncher
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        public static string GetAppPath()
        {
            string? appPath = System.IO.Path.GetDirectoryName(
                System.Reflection.Assembly.GetExecutingAssembly().Location);
            if (appPath is null)
            {
                throw new DirectoryNotFoundException("実行ファイルのパス取得失敗");
            }

            return appPath;
        }
    }
}

新しいファイルを追加します。
205.png
ConfigFile.csというファイル名にします。
210.png

ConfigFile.cs
using System.Collections.Generic;

namespace WrapLauncher
{
    public class ConfigFile
    {
        /// <summary>
        /// 設定ファイル名
        /// </summary>
        public const string ConfigFileName = "WrapLauncher.path";

        /// <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>
        /// <returns>設定ファイルのフルパス。存在しない場合は空文字列を返す。</returns>
        public string GetPath()
        {
            // EXEのパス取得
            string appPath = App.GetAppPath();

            // 設定ファイルのフルパス組み立て
            string cfgFilePath = System.IO.Path.Combine(appPath, ConfigFileName);

            // 設定ファイル存在チェック
            if (System.IO.File.Exists(cfgFilePath))
            {
                return cfgFilePath;
            }
            else
            {
                // 設定ファイルなし
                return string.Empty;
            }
        }

        /// <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);
        }

    }
}
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;

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

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

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

[色名一覧]
";

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

        /// <summary>
        /// 起動時
        /// </summary>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                // 設定読み込み、画面に反映
                LoadConfig();
            }
            catch (Exception ex)
            {
                ShowException(ex);
            }
        }

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

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

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

        /// <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();
            txt.TextWrapping = TextWrapping.Wrap;
            txt.Margin = new Thickness(5);
            txt.IsReadOnly = true;
            txt.Text = ex.ToString();

            MainContainer.Children.Add(txt);
        }

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

            var helpText = new RichTextBox();
            helpText.IsReadOnly = true;
            helpText.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("■ ");
                r.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 LoadConfig()
        {
            // 設定ファイルのフルパス取得
            string cfgFilePath = _cfg.GetPath();

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

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

            while (!reader.EndOfStream)
            {
                // TABで分解
                var item = reader.ReadLine()?.Split(ConfigFile.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);
                }

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

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

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

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

        /// <summary>
        /// グループ見出し生成
        /// </summary>
        /// <param name="title"></param>
        /// <returns></returns>
        private TextBlock CreateGroupTitle(string title)
        {
            var txt = new TextBlock();
            txt.Style = (Style)(this.Resources["GroupTitleStyle"]);
            txt.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();
            txtContainer.Orientation = Orientation.Horizontal;
            // ■テキスト作成
            var txtMark = new TextBlock();
            txtMark.Text = "■";
            txtMark.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();
            txt.Text = text;
            txtContainer.Children.Add(txt);

            // ボタンテキスト設定
            btn.Content = txtContainer;
            // ボタンクリック時の処理
            btn.Click += (_, _) => 
            {
                // 起動成功 かつ 最小化メニューにチェックが入っている かつ Ctrlが押されていない場合
                if (ExecuteCmd(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 ExecuteCmd(string cmd)
        {
                var p = new System.Diagnostics.Process();
                p.StartInfo.FileName = cmd;
                p.StartInfo.UseShellExecute = true;
                try
                {
                    p.Start();
                    return true;
                }
                catch
                {
                    MessageBox.Show(
                        $"起動に失敗しました。\n{cmd}", "エラー", MessageBoxButton.OK, MessageBoxImage.Error);
                    return false;
                }
        }

    }
}

ここまでで一度ビルドしてエラーがないか確認しておきます。
ターミナルでdotnet buildを実行します。
215.png

■設定ファイルの作成

「bin\Debug\net5.0-windows」にWrapLauncher.pathという名前でファイルを作成します。
220.png

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

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

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

設定ファイル入力例)

色名 ボタン名 パス
Silver C: C:\
Silver D: D:\
Gold Program Files C:\Program Files
Gold Program Files (x86) C:\Program Files (x86)
Gold Work C:\Work
Gold drivers - etc C:\Windows\System32\drivers\etc
Gray 電卓 calc.exe
LightSkyBlue メモ帳 notepad.exe
Black コマンドプロンプト cmd.exe
DodgerBlue Win PowerShell PowerShell.exe
Navy PowerShell pwsh.exe
DimGray Win Terminal wt.exe
ForestGreen Excel excel.exe
RoyalBlue Word winword.exe
OrangeRed PowerPoint powerpnt.exe
DodgerBlue Outlook outlook.exe
Purple OneNote Onenote.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」のチェックを外してください。
225.png
ついでにwhiteで検索して「Render Whitespace」をallにしておくとタブ文字が見えるようになります。
230.png

■実行

設定ファイルを保存したら実行します。
235.png
ウィンドウの幅を狭めるとボタンが折り返して表示されます。
240.png

右クリックからヘルプ表示。
330.png
ランチャー画面に戻るには右クリックから設定再読み込み。

リリースビルドするときはターミナルでdotnet build -c Releaseを実行します。
「Debug」フォルダの「WrapLauncher.path」を「Release」フォルダにもコピーしておきます。
「WrapLauncher.exe」を実行すればランチャーが起動します。
245.png

32
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
42