はじめに
.NET Core 3.0が9月23日にリリースされ、様々な新機能が追加されています。
この中でも個人的に気になったのがWindowsデスクトップアプリケーションのサポートです。
今回はこれについて詳しく調べてみました。
.NET Core 3.0の新機能については以下のページが参考になります。
.NET Core 3.0 (プレビュー 9) の新機能
Microsoft、「.NET Core 3.0」を正式リリース ~オープンソース化されたWPF/WinForms開発をサポート - 窓の杜
Windowsデスクトップアプリケーションの作成
Windowsデスクトップアプリケーションの作成は以下のコマンドで作成することができます。
# WinForms
$ dotnet new winforms
# Wpf
$ dotnet new wpf
作成できるプロジェクトはdotnet new
で確認することができます。
$ dotnet new
使用法: new [options]
## 省略
Templates Short Name Language Tags
----------------------------------------------------------------------------------------------------------------------------------
Console Application console [C#], F#, VB Common/Console
Class library classlib [C#], F#, VB Common/Library
WPF Application wpf [C#] Common/WPF
WPF Class library wpflib [C#] Common/WPF
WPF Custom Control Library wpfcustomcontrollib [C#] Common/WPF
WPF User Control Library wpfusercontrollib [C#] Common/WPF
Windows Forms (WinForms) Application winforms [C#] Common/WinForms
Windows Forms (WinForms) Class library winformslib [C#] Common/WinForms
Worker Service worker [C#] Common/Worker/Web
## 省略
試しにwpfプロジェクトを作成してみます。
## wpfプロジェクト作成
C:\Users\xxx\Documents\dotnet-wpf-test>dotnet new wpf
The template "WPF Application" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj...
C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj の復元が 49.81 ms で完了しました。
Restore succeeded.
# ファイル確認
C:\Users\xxx\Documents\dotnet-wpf-test>dir
ドライブ C のボリューム ラベルがありません。
ボリューム シリアル番号は 7036-8880 です
C:\Users\xxx\Documents\dotnet-wpf-test のディレクトリ
2019/09/24 21:21 <DIR> .
2019/09/24 21:21 <DIR> ..
2019/09/24 21:21 384 App.xaml
2019/09/24 21:21 348 App.xaml.cs
2019/09/24 21:21 276 dotnet-wpf-test.csproj
2019/09/24 21:21 509 MainWindow.xaml
2019/09/24 21:21 667 MainWindow.xaml.cs
2019/09/24 21:21 <DIR> obj
5 個のファイル 2,184 バイト
3 個のディレクトリ 22,242,934,784 バイトの空き領域
通常の.NET Frameworkでwpfプロジェクトを作成したときとほぼ同じ内容のファイルが作成されています。
このプロジェクトは通常の.NET Coreのプロジェクトと同様にdotnet run
コマンドで実行できます。
実行すると何もないウィンドウが表示されます。
Windowsデスクトップアプリケーションと従来のアプリケーションの違い
Windowsデスクトップアプリケーションがサポートされる前でもWin32APIを使用すればウィンドウを持ったアプリケーションを作成することはできました。
ではWindowsデスクトップアプリケーションとはwinformsやwpfことを指すのでしょうか。
従来のアプリケーションの決定的な違いはpublish
したときに現れます。
試しに先ほど作成したプロジェクトをpublish
してみます。
# publish
C:\Users\xxx\Documents\dotnet-wpf-test>dotnet publish
.NET Core 向け Microsoft (R) Build Engine バージョン 16.3.0+0f4c62fea
Copyright (C) Microsoft Corporation.All rights reserved.
C:\Users\xxx\Documents\dotnet-wpf-test\dotnet-wpf-test.csproj の復元が 13.77 ms で完了しました。
dotnet-wpf-test -> C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\dotnet-wpf-test.dll
dotnet-wpf-test -> C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\publish\
# ファイル確認
C:\Users\xxx\Documents\dotnet-wpf-test>dir bin\Debug\netcoreapp3.0\publish
ドライブ C のボリューム ラベルがありません。
ボリューム シリアル番号は 7036-8880 です
C:\Users\xxx\Documents\dotnet-wpf-test\bin\Debug\netcoreapp3.0\publish のディレクトリ
2019/09/24 21:30 <DIR> .
2019/09/24 21:30 <DIR> ..
2019/09/24 21:24 437 dotnet-wpf-test.deps.json
2019/09/24 21:30 7,168 dotnet-wpf-test.dll
2019/09/24 21:30 159,744 dotnet-wpf-test.exe
2019/09/24 21:30 1,644 dotnet-wpf-test.pdb
2019/09/24 21:24 161 dotnet-wpf-test.runtimeconfig.json
5 個のファイル 169,154 バイト
2 個のディレクトリ 22,245,412,864 バイトの空き領域
dotnet-wpf-test.exe
をエクスプローラからダブルクリックします。
するとMainWindow
のみが表示されます。
従来のプロジェクトでは必ずコンソールウィンドウが同時に表示されていましたがWindows Desktopアプリケーションでは表示されません。
これがWindows Desktopアプリケーションと従来のアプリケーションの違いです。
(補足)PEフォーマットとSubsystem
Windowsがサポートしている.exe
ファイルはPEフォーマットと呼ばれるフォーマットになっています。
PEフォーマットにはSubsystem
と呼ばれるフィールドがあり、Windowsはこのフィールドを見てWindowsアプリケーションかコンソールアプリケーションかを判断しています。
Windowsデスクトップアプリケーションをpublish
するとこのフィールドがWindowsアプリケーションの値になるためコンソールウィンドウが表示されなくなります。
参考:PE(Portable Executable)ファイルフォーマットの概要
csproj解読
作成したプロジェクトの.csproj
は以下のようになっています。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<RootNamespace>dotnet_wpf_test</RootNamespace>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
注目すべきはOutputTypeがWinExeになっていること、SdkがMicrosoft.NET.Sdk.WindowsDesktopになっていることでしょうか。
プロジェクトを展開してWinExeで調べてみます。
$ dotnet build -pp > project-all.txt
いくつか引っ掛かりますが興味深いのは以下の場所です。
<!--
============================================================
_CreateAppHost
If we found a restored apphost, create the modified destination apphost
with options from the project.
============================================================
-->
<UsingTask TaskName="CreateAppHost" AssemblyFile="$(MicrosoftNETBuildTasksAssembly)" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
<Target Name="_CreateAppHost" Inputs="@(IntermediateAssembly);$(AppHostSourcePath)" Outputs="$(AppHostIntermediatePath)" DependsOnTargets="_GetAppHostPaths;CoreCompile" Condition="'$(ComputeNETCoreBuildOutputFiles)' == 'true' and
 '$(AppHostSourcePath)' != '' and
 Exists('@(IntermediateAssembly)') and
 Exists('$(AppHostSourcePath)')" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- ★注目ポイント -->
<_UseWindowsGraphicalUserInterface Condition="($(RuntimeIdentifier.StartsWith('win')) or $(DefaultAppHostRuntimeIdentifier.StartsWith('win'))) and '$(OutputType)'=='WinExe'">true</_UseWindowsGraphicalUserInterface>
</PropertyGroup>
<CreateAppHost AppHostSourcePath="$(AppHostSourcePath)" AppHostDestinationPath="$(AppHostIntermediatePath)" AppBinaryName="$(AssemblyName)$(TargetExt)" IntermediateAssembly="@(IntermediateAssembly->'%(FullPath)')" WindowsGraphicalUserInterface="$(_UseWindowsGraphicalUserInterface)" Retries="$(CopyRetryCount)" RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" />
</Target>
OutputTypeがWinExeのとき、CreateAppHostタスクのWindowsGraphicalUserInterfaceがtrueになることがわかります。
CreateAppHostのソースは以下にあります。
// CreateAppHost.csより引用
HostWriter.CreateAppHost(appHostSourceFilePath: AppHostSourcePath,
appHostDestinationFilePath: AppHostDestinationPath,
appBinaryFilePath: AppBinaryName,
windowsGraphicalUserInterface: isGUI,
assemblyToCopyResorcesFrom: resourcesAssembly);
HostWriterのCreateAppHost関数にisGUI (WindowsGraphicalUserInterface)
を渡しています。
HostWriterのソースは以下にあります
// HostWriter.csより引用
// Re-write the destination apphost with the proper contents.
using (var memoryMappedFile = MemoryMappedFile.CreateFromFile(appHostDestinationFilePath))
{
using (MemoryMappedViewAccessor accessor = memoryMappedFile.CreateViewAccessor())
{
BinaryUtils.SearchAndReplace(accessor, AppBinaryPathPlaceholderSearchValue, bytesToWrite);
appHostIsPEImage = BinaryUtils.IsPEImage(accessor);
if (windowsGraphicalUserInterface)
{
if (!appHostIsPEImage)
{
throw new AppHostNotPEFileException();
}
BinaryUtils.SetWindowsGraphicalUserInterfaceBit(accessor);
}
}
}
BinaryUtils.SetWindowsGraphicalUserInterfaceBitがそれっぽい処理みたいです。
// BinaryUtils.csより引用 (★部分は追記部分)
/// <summary>
/// The value of the sybsystem field which indicates Windows GUI (Graphical UI)
/// </summary>
private const UInt16 WindowsGUISubsystem = 0x2;
/// <summary>
/// This method will attempt to set the subsystem to GUI. The apphost file should be a windows PE file.
/// </summary>
/// <param name="accessor">The memory accessor which has the apphost file opened.</param>
internal static unsafe void SetWindowsGraphicalUserInterfaceBit(MemoryMappedViewAccessor accessor)
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
byte* bytes = pointer + accessor.PointerOffset;
// https://en.wikipedia.org/wiki/Portable_Executable
UInt32 peHeaderOffset = ((UInt32*)(bytes + PEHeaderPointerOffset))[0];
if (accessor.Capacity < peHeaderOffset + SubsystemOffset + sizeof(UInt16))
{
throw new AppHostNotPEFileException();
}
UInt16* subsystem = ((UInt16*)(bytes + peHeaderOffset + SubsystemOffset));
// https://docs.microsoft.com/en-us/windows/desktop/Debug/pe-format#windows-subsystem
// The subsystem of the prebuilt apphost should be set to CUI
if (subsystem[0] != WindowsCUISubsystem)
{
throw new AppHostNotCUIException();
}
// ★WindowsGUISubsystem(2)の書き込み
// Set the subsystem to GUI
subsystem[0] = WindowsGUISubsystem;
}
finally
{
if (pointer != null)
{
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
これでようやくOutputTypeをWinExeにするとWindowsアプリケーションとして作成されることがわかりました。
最後に
今回の記事はほぼ何の役にもたたないことばかり書いてますが.NET Coreでデスクトップアプリケーションが作成できるようになったというのは感動的です。
他にもC#8.0が使えたり.NET Standard 2.1がサポートされていたりするので.NET Core3.0をぜひ試してみましょう😊