■はじめに
Visual Studio Code
(VSCode
)と.NET 5.0
のWPF
でGUIアプリ(アプリケーションランチャー)を作ります。
■環境
- Windows 10(Version 20H2)
- .NET 5.0~
- Visual Studio Code
■準備
◇.NET SDKのインストール
.NET 5.x
のリンクを選択します。
SDK 5.x
- Windows
- Installers
のx64
のリンクを選択します。
インストールします。
◇VSCodeのインストール、日本語化
VSCodeをインストールします。
「エクスプローラーのディレクトリコンテキストメニューに[Codeで開く]アクションを追加する」はチェックを入れておいてください。
他のチェックは任意です。
VSCodeが起動したら、左のアクティビティ バーの「拡張機能」を選択し、検索ボックスにjapanese
と入力、
「Japanese Language Pack for VS Code」をインストールします。
◇プロジェクトフォルダの作成、VSCodeの起動
エクスプローラーでプロジェクトを格納するフォルダ、WrapLauncher
を作成します。
WrapLauncherフォルダ内で右クリックし、「Codeで開く」を選択してVSCodeを起動します。
■プロジェクトの作成
ターミナルが表示されていない場合はメニューの「表示」-「ターミナル」でターミナルを表示します。
ターミナルに以下のコマンドを入力し、バージョンが5.xであることを確認します。
dotnet --version
ターミナルに以下のコマンドを入力し、プロジェクトを作成します。
dotnet new wpf
■拡張機能インストール
左のファイル一覧から「MainWindow.xaml.cs」をクリックします。
右下に「C#にお勧めの拡張機能」通知が表示された場合は「インストール」してください。
「Required assets to ....」(ビルドとデバッグに必要なアセットがありません。追加しますか?)の通知が表示されたら「Yes」を選択してください。
アクティビティ バーで「拡張機能」を選択し、検索ボックスにxaml
と入力し、「Pretty XML」をインストールします。
これはXAMLの整形用に使います。
■とりあえず実行
◇デバッグ設定修正
デバッグの設定を修正します。
アクティビティ バーから「実行」を選択し、上の歯車マークを選択します。
「launch.json」が開くので、「program」のパスの「Debug」フォルダの後ろを
net5.0-windows/WrapLauncher.exe
に修正し、保存します。
◇実行
▷の「デバッグの開始」ボタンを押すか、F5キーを押してデバッグ実行します。
空の画面が表示されました。
起動した画面を閉じてください。
■プロジェクト設定
アクティビティ バーで「エクスプローラー」を選択し、「WrapLauncher.csproj」を選択します。
「ProjectGroup」の中に<Nullable>enable</Nullable>
を入力して保存します。
■画面の作成
「MainWindow.xaml」を開き、画面を作成します。
作成し終わったらF1でコマンドパレットを表示し、xml
と入力して「Prettify XML」を実行します。
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>
■ロジックの作成
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;
}
}
}
新しいファイルを追加します。
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);
}
}
}
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
を実行します。
■設定ファイルの作成
「bin\Debug\net5.0-windows」にWrapLauncher.path
という名前でファイルを作成します。
設定ファイルに起動したいフォルダパスやドキュメントファイルパス、プログラムパスなどを記述します。
ファイルの文字コードはBOM無しUTF-8。
色名、ボタン名、パスの間はTAB区切りです。
色名にはGreen
やBlue
などを書きます。
※使える色名の一覧は、プログラム完成後、ヘルプページで見ることができます。
//グループ見出し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」のチェックを外してください。
ついでにwhite
で検索して「Render Whitespace」をall
にしておくとタブ文字が見えるようになります。
■実行
設定ファイルを保存したら実行します。
ウィンドウの幅を狭めるとボタンが折り返して表示されます。
右クリックからヘルプ表示。
ランチャー画面に戻るには右クリックから設定再読み込み。
リリースビルドするときはターミナルでdotnet build -c Release
を実行します。
「Debug」フォルダの「WrapLauncher.path」を「Release」フォルダにもコピーしておきます。
「WrapLauncher.exe」を実行すればランチャーが起動します。