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>
なお、このようにすると、実行時にコンソール ウィンドウが表示されるようになります。プログラムがデスクトップ アプリケーションであり、実行時にコンソール ウィンドウが表示されるのが不格好だと感じるならば、するべきではありません。