LoginSignup
7
7

More than 3 years have passed since last update.

.NET Core 3.0でサポートされるWindowsデスクトップアプリケーションを追う

Posted at

はじめに

.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コマンドで実行できます。
実行すると何もないウィンドウが表示されます。

image.png

Windowsデスクトップアプリケーションと従来のアプリケーションの違い

Windowsデスクトップアプリケーションがサポートされる前でもWin32APIを使用すればウィンドウを持ったアプリケーションを作成することはできました。
ではWindowsデスクトップアプリケーションとはwinformswpfことを指すのでしょうか。
従来のアプリケーションの決定的な違いは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&#xD;&#xA;                     '$(AppHostSourcePath)' != '' and&#xD;&#xA;                     Exists('@(IntermediateAssembly)') and&#xD;&#xA;                     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();
        }
    }
}

これでようやくOutputTypeWinExeにするとWindowsアプリケーションとして作成されることがわかりました。

最後に

今回の記事はほぼ何の役にもたたないことばかり書いてますが.NET Coreでデスクトップアプリケーションが作成できるようになったというのは感動的です。
他にもC#8.0が使えたり.NET Standard 2.1がサポートされていたりするので.NET Core3.0をぜひ試してみましょう😊

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