Visual C# 2010 Express ~ Visual Studio 2019 で、WPF アプリケーションを開発した際の覚え書きです。未だ WPF には詳しくないので、ここに書いてあることは大間違いかもしれません。
Startup
イベントを使う)Visual C# 2010 Express で WPF アプリケーション プロジェクトを作成すると、App.xaml、App.xaml.cs、MainWindow.xaml、MainWindow.xaml.cs の4つのファイルが生成されます。このうちの App.xaml の Application
要素から StartupUri
属性を削除し、Startup
属性にイベントハンドラーとなるメソッドの名前を指定します。
<Application x:Class="WpfApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup">
<Application.Resources>
</Application.Resources>
</Application>
App.xaml.csは次のようにします。イベントハンドラーに渡されるStartupEventArgs
オブジェクトのArgs
プロパティーがコマンドライン引数です。
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
foreach (string arg in e.Args)
MessageBox.Show(arg);
// ウィンドウを表示。
MainWindow mainWindow = new MainWindow(); // 13行目
mainWindow.Show(); // 14行目
}
}
}
App.xamlからStartupUri
属性を削除したので、13、14行目を書かないとウィンドウは表示されません。
Mutex
を使う)System.Threading.Mutex
を使います。Mutex
のコンストラクターで、UUIDなど、アプリケーションを一意に識別するための文字列を渡します。
using System.Threading;
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
private static Mutex mutex = new Mutex(false, "WpfApplication1"); // 実際はUUIDなどがよい。
private void Application_Startup(object sender, StartupEventArgs e)
{
if (mutex.WaitOne(0, false))
{
MessageBox.Show("先に起動しているインスタンスはありません。");
// ウィンドウを表示
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
}
else
{
MessageBox.Show("先に起動しているインスタンスがあります。");
Shutdown(); // 終了する。
}
}
}
}
注意点として、生成したMutex
オブジェクトがガーベッジコレクションによって消滅すると、2つ目以降のインスタンスが起動できるようになってしまいます。そこで上記の例では、Mutex
オブジェクトをstaticフィールドとすることで、消滅しないようにしています。
ListBox
やListView
で表示する次のように、ListBox
やListView
のItemsSource
プロパティーにコレクションオブジェクトを指定するだけです。コレクションオブジェクトは、System.Collections.IEnumerable
を実装している必要があります。
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class App : Application
{
List<string> list = new List<string>();
ICollectionView collectionView;
private void Application_Startup(object sender, StartupEventArgs e)
{
list.Add("リートルード");
list.Add("Hello, World!");
collectionView = CollectionViewSource.GetDefaultView(list);
MainWindow mainWindow = new MainWindow();
mainWindow.listBox1.ItemsSource = list;
mainWindow.Show();
list.Add("Coming Era...");
collectionView.Refresh(); // 26行目
}
}
}
ただし、コレクションオブジェクトの更新をListBox
やListView
の表示に反映させるには、26行目のようにICollectionView.Refresh
メソッドを呼び出す必要があります。あるいは、ObservableCollectionクラスを使えば、Refresh()は不要です。
ListView
の各列に指定したプロパティを表示する次のように、GridViewColumn
要素のDisplayMemberBinding
属性で、プロパティ名を指定します。
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="400" Height="300">
<Grid>
<ListView Name="listView1">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Name}"/>
<GridViewColumn DisplayMemberBinding="{Binding Level}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
この例では、App.xaml.csは次のようにしています。User
というクラスがあり、ListView
の各行に表示されるのは、List
に格納されたUser
のインスタンスです。
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace WpfApplication1
{
public partial class App : Application
{
List<User> list = new List<User>();
ICollectionView collectionView;
private void Application_Startup(object sender, StartupEventArgs e)
{
list.Add(new User("Abc", 5));
list.Add(new User("Defg", 4));
collectionView = CollectionViewSource.GetDefaultView(list);
MainWindow mainWindow = new MainWindow();
mainWindow.listView1.ItemsSource = list;
mainWindow.Show();
list.Add(new User("Hijkl", 7));
collectionView.Refresh();
}
}
class User
{
public string Name { get; set; }
public int Level { get; set; }
public User(string name, int level)
{
Name = name;
Level = level;
}
}
}
ListView
の各列の幅を自動的に最適にするforeach (GridViewColumn column in (listView1.View as GridView).Columns)
{
column.Width = 0;
column.Width = double.NaN;
}
このようにすれば、列ヘッダーの境界をダブルクリックしたときのように、列幅が最適な状態になります。何度も行うなら、そのGridViewColumn
をフィールドに保持しておいてもいいでしょう。
次のようなXAMLがあるとします。
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Width="400" Height="300">
<StackPanel>
<Rectangle Name="rectangle1" Width="0" Height="20" Fill="Blue"/>
<Button Content="Start" Click="StartButton_Click"/>
<Button Content="Pause/Resume" Click="PauseResumeButton_Click" IsEnabled="False" Name="pauseResumeButton"/>
<Button Content="Stop" Click="StopButton_Click" IsEnabled="False" Name="stopButton"/>
<!-- 起動直後はPause/ResumeボタンとStopボタンは無効状態としている。-->
</StackPanel>
</Window>
Startボタンを押すとアニメーション開始、Pause/Resumeボタンを押すと一時停止、同じボタンをもう一度押すと再開、Stopボタンを押すと停止、という挙動をC#コードで実現するには、MainWindow.xaml.csを次のようにします。
using System;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
Storyboard storyboard = new Storyboard();
public MainWindow()
{
InitializeComponent();
DoubleAnimation animation = new DoubleAnimation();
Storyboard.SetTarget(animation, rectangle1);
Storyboard.SetTargetProperty(animation, new PropertyPath(Rectangle.WidthProperty));
animation.To = 400; // アニメーション後の値を指定。
animation.Duration = TimeSpan.FromSeconds(10); // アニメーションする時間を指定。
storyboard.Children.Add(animation);
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
storyboard.Begin();
if (!pauseResumeButton.IsEnabled)
{
// 起動直後は無効状態としていたPause/ResumeボタンとStopボタンを有効にする。
pauseResumeButton.IsEnabled = true;
stopButton.IsEnabled = true;
}
}
private void PauseResumeButton_Click(object sender, RoutedEventArgs e)
{
if (storyboard.GetIsPaused())
storyboard.Resume();
else
storyboard.Pause();
}
private void StopButton_Click(object sender, RoutedEventArgs e)
{
storyboard.Stop();
}
}
}
ListBox
の項目のドラッグによる並べ替えMainWindow.xamlは次のようにします。
<Window x:Class="ListBoxExTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox Name="listBox1">
<ListBoxItem AllowDrop="True" MouseMove="ListBoxItem_MouseMove" DragEnter="ListBoxItem_DragEnter">色は匂えど</ListBoxItem>
<ListBoxItem AllowDrop="True" MouseMove="ListBoxItem_MouseMove" DragEnter="ListBoxItem_DragEnter">散りぬるを</ListBoxItem>
<ListBoxItem AllowDrop="True" MouseMove="ListBoxItem_MouseMove" DragEnter="ListBoxItem_DragEnter">我が世誰ぞ</ListBoxItem>
<ListBoxItem AllowDrop="True" MouseMove="ListBoxItem_MouseMove" DragEnter="ListBoxItem_DragEnter">常ならん</ListBoxItem>
</ListBox>
</Grid>
</Window>
まずListBoxItem
のAllowDrop
プロパティをOnにします。そして、MouseMove
とDragEnter
という2つのイベントにイベントハンドラーを指定します。
MainWindow.xaml.csは次のようにします。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace ListBoxExTest
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ListBoxItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
DragDrop.DoDragDrop(sender as ListBoxItem, sender, DragDropEffects.Move);
}
private void ListBoxItem_DragEnter(object sender, DragEventArgs e)
{
ListBoxItem source = e.Data.GetData(typeof(ListBoxItem)) as ListBoxItem;
ItemCollection items = listBox1.Items;
int index = items.IndexOf(sender as ListBoxItem);
items.Remove(source);
items.Insert(index, source);
}
}
}
ドラッグの開始は、MouseMove
のイベントハンドラーの中で、DragDrop
クラスの静的メソッドであるDoDragDrop()
によって行います。このとき、並べ替えるListBoxItem
を引数に指定しておきます。
並べ替えは、DragEnter
のイベントハンドラーの中で行います。第2引数のDragEventArgs
オブジェクトのData
プロパティーのGetData
メソッドで、先に指定したListBoxItem
が取得できます。
Main
メソッドを使う)Visual Studio Community 2015でWPF アプリケーション プロジェクトを作成すると、App.xaml、App.xaml.cs、MainWindow.xaml、MainWindow.xaml.csの4つのファイルが生成されます。このうち、App.xamlのプロパティで、ビルド アクションをApplicationDefinitionからPageに変更します。
ビルド アクションをPageにすると、Main
メソッドが自動で実装されなくなり、以下のようにApp.xaml.csに明示的に定義する必要があります。このとき、Main
メソッドの仮引数からコマンドライン引数を取得できます。
using System;
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
public static string[] Args { get; set; }
[STAThread]
public static void Main(string[] args)
{
Args = args;
var app = new App(); // 15行目
app.InitializeComponent(); // 16行目
app.Run(); // 17行目
}
}
}
このコードでは、コマンドライン引数をほかの場所からも参照できるようにするため、App
にArgs
という静的プロパティを定義し、そこに代入しています。また、Main
メソッドを明示的に実装しているため、15~17行目を記述しなければアプリケーションは開始しません。
Semaphore
を使う)System.Threading.Semaphore
を使う方法です。Semaphore
のコンストラクターで、UUIDなど、アプリケーションを一意に識別するための文字列を渡します。
using System;
using System.Threading;
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
const string ApplicationId = "00000000-0000-0000-0000-000000000000"; // 実際はUUIDなどが良い。
[STAThread]
public static void Main(string[] args)
{
bool createdNew;
using (var semaphore = new Semaphore(1, 1, ApplicationId, out createdNew))
{
if (createdNew)
{ var app = new App();
app.InitializeComponent();
app.Run();
}
} }
}
}
まず、参照設定にSystem.Runtime.Remotingを追加する必要があります。
次に、System.MarshalByRefObject
を継承したクラスを作成します。
using System;
namespace WpfApplication1
{
class Handler : MarshalByRefObject
{
public void Handle()
{
// 通信が行われたときの処理を記述する。
}
// 通信可能な状態を保ち続けるためのオーバーライド。
public override object InitializeLifetimeService() => null;
}
}
MarshalByRefObject
を継承したHandler
クラスでは、InitializeLifetimeService
というメソッドをオーバーライドしています。これは、時間経過によって、通信できなくなる状態に自動的になってしまうのを防ぐためです。
通信のサーバー側は、次のように記述します。IpcServerChannel
のコンストラクターでは、UUIDなど、アプリケーションを一意に識別するための文字列を渡します。RemotingServices.Marshal
メソッドの第2引数では、MarshalByRefObject
オブジェクトを識別するための文字列を指定します。
const string ApplicationId = "00000000-0000-0000-0000-000000000000"; // 実際はUUIDなどが良い。
const string HandlerName = "handler";
ChannelServices.RegisterChannel(newIpcServerChannel(ApplicationId), true);
RemotingServices.Marshal(new Handler(), HandlerName, typeof(Handler));
通信のクライアント側は、次のように記述します。
ChannelServices.RegisterChannel(new IpcClientChannel(), true);
((Handler)Activator.GetObject(typeof(Handler), "ipc://" + ApplicationId + "/" + HandlerName)).Handle();
ただし、ユーザーインターフェースに変更を加えるなど、別のスレッドに関係する処理を行う場合は、Dispatcher.Invoke
メソッドを使う必要があります。次のセクションの例では、Dispatcher.Invoke
メソッドを使っています。
Semaphore
を使って二重起動を防止し、System.Runtime.Remoting.Channels.Ipc
名前空間のクラスを使って起動済みのウィンドウと通信します。
System.Runtime.Remoting.Channels.Ipc
名前空間のクラスを使うので、参照設定にSystem.Runtime.Remotingを追加する必要があります。
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
const string ApplicationId = "00000000-0000-0000-0000-000000000000"; // 実際はUUIDなどが良い。
const string HandlerName = "handler";
[STAThread]
public static void Main(string[] args)
{
bool createdNew;
using (var semaphore = new Semaphore(1, 1, ApplicationId, out createdNew))
{
if (createdNew)
{
ChannelServices.RegisterChannel(new IpcServerChannel(ApplicationId), true);
RemotingServices.Marshal(new Handler(), HandlerName, typeof(Handler));
var app = new App();
app.InitializeComponent();
app.Run();
}
else
{
ChannelServices.RegisterChannel(new IpcClientChannel(), true);
((Handler)Activator.GetObject(typeof(Handler), "ipc://" + ApplicationId + "/" + HandlerName)).Handle();
} }
}
class Handler : MarshalByRefObject
{
public void Handle()
{
Current.Dispatcher.Invoke(() => Current.MainWindow.Activate());
}
public override object InitializeLifetimeService() => null;
} }
}
App.xaml.csは以下のようにします。ArgsReceived
という静的イベントを宣言し、二重起動を防止したとき、イベントが発生するようにしています。このとき、引数にコマンドライン引数を指定します。
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;
using System.Threading;
using System.Windows;
namespace WpfApplication1
{
public partial class App : Application
{
const string ApplicationId = "00000000-0000-0000-0000-000000000000"; // 実際はUUIDなどが良い。
const string HandlerName = "handler";
public delegate void ArgsReceivedEventHandler(string[] args);
public static event ArgsReceivedEventHandler ArgsReceived;
[STAThread]
public static void Main(string[] args)
{
bool createdNew;
using (var semaphore = new Semaphore(1, 1, ApplicationId, out createdNew))
{
if (createdNew)
{
ChannelServices.RegisterChannel(new IpcServerChannel(ApplicationId), true);
RemotingServices.Marshal(new Handler(), HandlerName, typeof(Handler));
var app = new App();
app.InitializeComponent();
app.Run();
}
else
{
ChannelServices.RegisterChannel(new IpcClientChannel(), true);
((Handler)Activator.GetObject(typeof(Handler), "ipc://" + ApplicationId + "/" + HandlerName)).Handle(args);
}
}
}
class Handler : MarshalByRefObject
{
public void Handle(string[] args)
{
if (ArgsReceived != null)
Current.Dispatcher.Invoke(ArgsReceived, (object)args); }
public override object InitializeLifetimeService() => null;
}
}
}
Handler
クラスのHandle
メソッドで、args
をobject
にキャストしている所でキャストが冗長です。
という警告が出ます。しかし、このキャストは必要です。キャストしないと、string[]
型のargs
がstring
型の可変長引数と判断され、例外が発生します。もしくは、new object[] { args }
のように記述すれば警告も例外も出ません。
MainWindow.xaml.csは以下のようにします。App
のArgsReceived
静的イベントに、渡されたコマンドライン引数を処理するイベントハンドラーを追加しています。イベントハンドラーの中でウィンドウをアクティブにし、またコマンドライン引数を処理しています。
using System.Windows;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
App.ArgsReceived += args =>
{
Activate();
foreach (string arg in args)
MessageBox.Show(arg);
}; }
}
}
まずは Main
メソッドを明示的に定義するようにします。そのために、App.xaml
のビルド アクションを Page
に変更します。
また、App.xaml
から StartupUri
属性を削除します。これで、表示処理を明示的に実行するまで、メインウィンドウは表示されません。
<Application x:Class="WpfApp1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1">
<Application.Resources>
</Application.Resources>
</Application>
次に、MainWindow
に、ウィンドウハンドルを公開するプロパティを定義します。このプロパティには、SourceInitialized
イベントで値を設定します。SourceInitialized
イベントは、ウィンドウハンドルを取得できるようになる、最も早いタイミングのイベントです。
using System;
using System.Windows;
using System.Windows.Interop;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public IntPtr Handle { get; private set; }
public MainWindow()
{
InitializeComponent();
}
protected override void OnSourceInitialized(EventArgs e)
{
Handle = new WindowInteropHelper(this).Handle;
base.OnSourceInitialized(e);
}
}
}
そして、App.xaml.cs
の Main
メソッドを記述します。Semaphore
で二重起動かどうかを判定し、二重起動であれば、メモリマップトファイルから、起動済みウィンドウのウィンドウハンドルを読み込みます。
起動済みウィンドウをアクティブにするには、Windows API の SetForegroundWindow
関数を使います。この関数にウィンドウハンドルを渡します。また、ウィンドウが最小化していた場合は元のサイズに戻すため、IsIconic
関数と ShowWindowAsync
関数も使っています。
なお、ごく短い時間ですが、「Semaphore
によって二重起動は防止できているが、まだウィンドウハンドルは書き込まれていない」というタイミングが存在します。そのため、TryActivateExistingWindow
メソッド(起動済みウィンドウのアクティブ化を試みるメソッド)では、メモリマップトファイルから読み込んだ値が 0 だった場合、10回まで再試行するようにしています。
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
namespace WpfApp1
{
public partial class App : Application
{
const string applicationId = "00000000-0000-0000-0000-000000000000"; // 実際はUUIDなどが良い。
const string memoryMappedFileName = applicationId + ".dat";
const int SW_RESTORE = 9;
[DllImport("user32.dll")]
static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
static extern bool SetForegroundWindow(IntPtr hWnd);
[STAThread]
public static void Main(string[] args)
{
using (var semaphore = new Semaphore(1, 1, applicationId, out var createdNew))
{
if (!createdNew)
{
TryActivateExistingWindow();
return;
}
var app = new App();
app.InitializeComponent();
var mainWindow = new MainWindow();
using (var mmf = MemoryMappedFile.CreateNew(memoryMappedFileName, 8))
{
mainWindow.SourceInitialized += (sender, e) =>
{
var windowHandle = mainWindow.Handle.ToInt64();
using (var stream = mmf.CreateViewStream())
{
var binaryWriter = new BinaryWriter(stream);
binaryWriter.Write(windowHandle);
}
};
app.Run(mainWindow);
}
}
}
private static void TryActivateExistingWindow()
{
var count = 0;
do
{
try
{
using (var mmf = MemoryMappedFile.OpenExisting(memoryMappedFileName))
using (var stream = mmf.CreateViewStream(0, 8, MemoryMappedFileAccess.Read))
{
var binaryReader = new BinaryReader(stream);
var windowHandle = binaryReader.ReadInt64();
if (windowHandle > 0)
{
ActivateExistingWindow(windowHandle);
return;
}
}
}
catch (FileNotFoundException) { }
Thread.Sleep(1000);
} while (++count < 10);
}
private static void ActivateExistingWindow(long windowHandle)
{
var hWnd = new IntPtr(windowHandle);
if (IsIconic(hWnd))
{
ShowWindowAsync(hWnd, SW_RESTORE);
}
SetForegroundWindow(hWnd);
}
}
}
今後の .NET 開発では、.NET Framework ではなく .NET 5.0 以降を検討するべきです。ここでも .NET 5.0 を使っています。
C# 8.0 で null 許容参照型が追加されました。Visual Studio 2019 で作成したプロジェクトで null 許容参照型を使うには、.csproj
を直接編集します。下記のように、<Nullable>enable</Nullable>
を記述すると、null 許容参照型が有効になります。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable> </PropertyGroup>
</Project>
Main
メソッドを明示的に実装する.NET 5.0 の WPF アプリケーションでは、App.xaml
というファイルは、ビルド アクションが自動的に ApplicationDefinition になります。そのため、Visual Studio のプロパティ ウィンドウで、App.xaml
のビルド アクションを変更するとエラーが発生します。
Main
メソッドを明示的に実装するには、まずこの挙動を変更します。それには、.csproj
を直接編集して、<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
を記述します。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition> </PropertyGroup>
</Project>
これで、Main
メソッドが自動生成されなくなるので、App.xaml.cs
に、Main
メソッドを明示的に実装します。コマンドライン引数を受け取ることもできます。
public partial class App : Application
{
[STAThread]
public static void Main(string[] args)
{
var app = new App();
app.InitializeComponent();
app.Run();
}}
Console.WriteLine()
の値を表示するVisual Studio 2019 で、新たに WPF アプリケーションを作成し、Console.WriteLine()
を記述しても、値はコンソールに表示されません。.NET Framework の場合は、プロジェクトのプロパティで「出力の種類」を「コンソール アプリケーション」にすると表示されるようになりますが、.NET 5.0 の場合、単にこの操作を行っても、自動的に「WPF アプリケーション」に戻ります。
あえて「コンソール アプリケーション」にするならば、.csproj
に <DisableWinExeOutputInference>true</DisableWinExeOutputInference>
を記述します。これで、自動的に「WPF アプリケーション」に戻ることはなくなります。あとは、OutputType
を Exe
に変更するか、プロジェクトのプロパティの「出力の種類」を「コンソール アプリケーション」にします。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType> <TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<DisableWinExeOutputInference>true</DisableWinExeOutputInference> </PropertyGroup>
</Project>
なお、このようにすると、実行時にコンソール ウィンドウが表示されるようになります。プログラムがデスクトップ アプリケーションであり、実行時にコンソール ウィンドウが表示されるのが不格好だと感じるならば、するべきではありません。