C#でウィンドウを表示したい


# はじめに

## 注意

**この記事はネタ**です。「C#はGUIアプリ開発が簡単らしいからちょっとやってみよう」と調べて運悪く当記事にたどり着いてしまった方は直ちにブラウザバックしてください。

## 全体の流れ

Windowsでウィンドウを表示するためには，まずウィンドウクラスを登録し，その後ウィンドウのインスタンスを作成して表示する必要があります。これをC#で再現してやろうというのが今回の企画です。

ウィンドウクラスは，実際のウィンドウ(インスタンス)の外観や動作を定義します。この構造はオブジェクト指向におけるクラスの概念と似ています。ウィンドウクラスに関しては [About Window Classes - Win32 apps](https://docs.microsoft.com/en-us/windows/win32/winmsg/about-window-classes) に詳しいです。

## 実行環境

OSはWin10(64bit[^1])です。

フレームワークは.NET 5.0とし，アンセーフコードを許可してコンパイルします。

[^1]: "The IntPtr type is designed to be an integer whose size is platform-specific. That is, an instance of this type is expected to be 32-bits on 32-bit hardware and operating systems, and 64-bits on 64-bit hardware and operating systems."らしいので，この辺りの環境がもしかしたら動作に効いてくるのかもしれません。Any CPUにして「32ビットを選ぶ」は外しています。

# やってみよう

## 準備

```C#
using System;
using System.Runtime.InteropServices;

// 見た目を不穏にするためのエイリアス
using ATOM      = System.UInt16;
using BOOL      = System.Int32;
using DWORD     = System.UInt32;
using HBRUSH    = System.IntPtr;
using HCURSOR   = System.IntPtr;
using HICON     = System.IntPtr;
using HINSTANCE = System.IntPtr;
using HMENU     = System.IntPtr;
using HWND      = System.IntPtr;
using LRESULT   = System.IntPtr;
using LPARAM    = System.IntPtr;
using WPARAM    = System.IntPtr;

const BOOL FALSE = 0;
const BOOL TRUE  = 1;

// Window Class Styles
const int CS_VREDRAW = 0x0001;
const int CS_HREDRAW = 0x0002;

// LoadImageのパラメータ
const int LR_DEFAULTSIZE = 0x00000040;
const int LR_SHARED      = 0x00008000;
const int IMAGE_ICON   = 1;
const int IMAGE_CURSOR = 2;
const string IDI_APPLICATION = "#32512";
const string IDC_ARROW       = "#32512";

// 拡張ウィンドウスタイル
const uint WS_EX_WINDOWEDGE = 0x00000100;
const uint WS_EX_CLIENTEDGE = 0x00000200;
const uint WS_EX_OVERLAPPEDWINDOW = WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE;

// 拡張じゃないウィンドウスタイル
const uint WS_OVERLAPPED  = 0x00000000;
const uint WS_MAXIMIZEBOX = 0x00010000;
const uint WS_MINIMIZEBOX = 0x00020000;
const uint WS_THICKFRAME  = 0x00040000;
const uint WS_SYSMENU     = 0x00080000;
const uint WS_CAPTION     = 0x00C00000;
const uint WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;

// その他諸々
const int WHITE_BRUSH = 0;
const int SW_SHOWNORMAL = 1;
const int CW_USEDEFAULT = unchecked((int)0x80000000);
const uint WM_DESTROY = 0x0002;

var NULL = IntPtr.Zero;
```

### 構造体

```C#
// ウィンドウクラス登録時に情報を突っ込む
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct WNDCLASSEX
{
    [MarshalAs(UnmanagedType.U4)]
    public int cbSize;
    [MarshalAs(UnmanagedType.U4)]
    public int style;
    public WNDPROC<WindowProcedure> lpfnWndProc;
    public int cbClsExtra;
    public int cbWndExtra;
    public HINSTANCE hInstance;
    public HICON hIcon;
    public HCURSOR hCursor;
    public HBRUSH hbrBackground;
    public string lpszMenuName;
    public string lpszClassName;
    public HICON hIconSm;
}

// 関数ポインタをそれっぽくするためのラッパー
public struct WNDPROC<TDelegate>
{
    private readonly IntPtr ptr;

    public WNDPROC(IntPtr ptr)
    {
        this.ptr = ptr;
    }

    public static explicit operator WNDPROC<TDelegate>(TDelegate @delegate)
        => new(Marshal.GetFunctionPointerForDelegate(@delegate));

    public static implicit operator IntPtr(WNDPROC<TDelegate> wndproc)
        => wndproc.ptr;
}

public delegate IntPtr WindowProcedure(HWND hWnd, uint msg, IntPtr wParam, IntPtr lParam);

[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
    public HWND hwnd;
    public uint message;
    public WPARAM wParam;
    public LPARAM lParam;
    public DWORD time;
    public POINT pt;
    public DWORD lPrivate;
}

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }

    public static implicit operator System.Drawing.Point(POINT p)
    {
        return new System.Drawing.Point(p.X, p.Y);
    }

    public static implicit operator POINT(System.Drawing.Point p)
    {
        return new POINT(p.X, p.Y);
    }
}
```

### 関数

```C#
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern HINSTANCE GetModuleHandle(
   [MarshalAs(UnmanagedType.LPWStr)] in string lpModuleName
);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.U2)]
static extern ATOM RegisterClassEx(
   [In] ref WNDCLASSEX lpwcx
);

[DllImport("gdi32.dll")]
static extern HBRUSH GetStockObject(
   int fnObject
);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern HICON LoadImage(
   HINSTANCE hinst,
   string lpszName,
   uint uType,
   int cxDesired,
   int cyDesired,
   uint fuLoad
);

[DllImport("user32.dll")]
static extern LRESULT DefWindowProc(
   HWND hWnd,
   uint uMsg,
   WPARAM wParam,
   LPARAM lParam
);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern HWND CreateWindowEx(
    uint dwExStyle,
    string lpClassName,
    [MarshalAs(UnmanagedType.LPStr)] string lpWindowName,
    uint dwStyle,
    int x,
    int y,
    int nWidth,
    int nHeight,
    HWND hWndParent,
    HMENU hMenu,
    HINSTANCE hInstance,
    IntPtr lpParam
);

[DllImport("user32.dll")]
static extern bool ShowWindow(
   HWND hWnd,
   int nCmdShow
);

[DllImport("user32.dll")]
static extern bool UpdateWindow(
   HWND hWnd
);

[DllImport("user32.dll")]
static extern int GetMessage(
   out MSG lpMsg,
   HWND hWnd,
   uint wMsgFilterMin,
   uint wMsgFilterMax
);

[DllImport("user32.dll")]
static extern bool TranslateMessage(
   [In] ref MSG lpMsg
);

[DllImport("user32.dll")]
static extern IntPtr DispatchMessage(
   [In] ref MSG lpmsg
);

[DllImport("user32.dll")]
static extern void PostQuitMessage(
   int nExitCode
);
```

## メイン処理

```C#
// C#のエントリーポイント(トップレベルステートメント)
var szAppName = "WinForm";
unsafe
{
    var hCurInst = GetModuleHandle(null);
    WinMain(hCurInst, NULL, (char*)(void*)NULL, SW_SHOWNORMAL);
}


// エントリーポイント(?)
unsafe int WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, char* lpsCmdLine, int nCmdShow)
{
    MSG msg;
    int bRet;

    if (InitApp(hCurInst) == 0)
        return FALSE;
    if (InitInstance(hCurInst, nCmdShow) == 0)
        return FALSE;

    while ((bRet = GetMessage(out msg, NULL, 0, 0)) != 0)
    {
        if (bRet == -1)
        {
            break;
        }
        else
        {
            TranslateMessage(ref msg);
            DispatchMessage(ref msg);
        }
    }

    return (int)msg.wParam;
}

// ウィンドウクラス登録
unsafe ATOM InitApp(HINSTANCE hInst)
{
    var wcx = new WNDCLASSEX()
    {
        cbSize = Marshal.SizeOf(typeof(WNDCLASSEX)),
        style = CS_VREDRAW | CS_HREDRAW,
        lpfnWndProc = (WNDPROC<WindowProcedure>)WndProc,
        cbClsExtra = 0,
        cbWndExtra = 0,
        hInstance = hInst,
        hIcon = LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED),
        hCursor = LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED),
        hbrBackground = GetStockObject(WHITE_BRUSH),
        lpszMenuName = null,
        lpszClassName = szAppName,
        hIconSm = LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED),
    };

    return RegisterClassEx(ref wcx);
}

// インスタンス作成
BOOL InitInstance(HINSTANCE hInst, int nCmdShow)
{
    var hWnd = CreateWindowEx(
        WS_EX_OVERLAPPEDWINDOW,
        szAppName,
        "C#は楽しい",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        hInst,
        NULL
    );

    if (hWnd == NULL) return FALSE;

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

// コールバック
LRESULT WndProc(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, msg, wParam, lParam);
    }

    return NULL;
}
```

# 実行結果
指定したウィンドウタイトルを持ったウィンドウが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211353/9b523562-435a-5c57-8596-6dda32ec802d.png)

# おまけ: `new Form().ShowDialog()`

`new System.Windows.Forms.Form().ShowDialog()`したときには，上で書いたようなめんどくさい処理が勝手に行われています。実際にどのようなことを行っているのか少し見てみましょう。
(長くなるので`System.Windows.Forms`は省略します。)

`DllImport`であったりそれに関連する構造体や定数であったりは`UnsafeNativeMethods`クラスと`NativeMethods`クラスにまとめられています。

`Form`の基底の基底の基底である`Control`クラスは`ControlNativeWindow`型のフィールドを持っていて，`ControlNativeWindow`は`NativeWindow`を継承します。`NativeWindow`では`WindowClass`を使ってハンドルを作成し(`WindowClass.RegisterClass`メソッドから`UnsafeNativeMethods.RegisterClass`が呼ばれる)，さらに`UnsafeNativeMethods.CreateWindowEx`を呼んでウィンドウを作成します。

継承などでごちゃごちゃしていますが，やっていることの大まかな流れはP/Invokeで実装したことと大差ないことがわかるかと思います。

なお，ここで挙げたクラスのほとんどは`internal`以下のアクセシビリティになっているためこれを利用することはできません(リフレクションで叩けば使えますが……)。

# 参考文献

- [PInvoke.net](https://www.pinvoke.net/index.aspx)
- [MSDN](https://docs.microsoft.com/ja-jp/windows/win32/learnwin32/creating-a-window) (←以外にもたくさん)