Unity(C#)でWINAPIを使う

# はじめに
Windowsのデスクトップアプリで枠（BORDER）とかタイトルバーを消したいというのはよくあることですが、WINAPIのドキュメントを引用しながら方法を教えてくれるページがそんなに無いようなので作ることにしました。つまりは下記あたりを見るんですね。
https://docs.microsoft.com/en-us/windows/win32/api/winuser/

そもそもQiitaでWINAPI（WindowsAPI）のタグがない？　みんな使っていると思うんですが興味ないんですかね……。
だらだら説明を書くので、方法だけを知りたい人は別のページをググると良いです。

# 背景
「NICT インターネット時刻供給サービス」を利用するUnity(2018.4.24f1)製のデスクトップ時計を作成しました。
https://github.com/hakua-doublemoon/NetClock
なんでこんなものを作ったかといえば、私の富士通製タブレット型PCの時計が半年に一回ぐらい狂いまくる時期が来るからです。（RTCとかがおかしいのかな……）
ついでについなちゃんの声で時報してもらってます。

**環境**

| 項目名 | 値 |
|---|---|
| OS    | Windows10   |
| Unity | 2018.4.24f1 |

# 内容
## 基本的な考え方
Windowsのアプリケーションですので、WindowsのAPIを使ってWindow（＝アプリの表示）を変えることができるようです。
このWINAPIは基本的にC/C++のコードですが、DLLImportすることでC#でも使えます。DLLImportについては他の記事が詳しいのではと思います。（これに関してはもう調べてない。）

## 事前: Windowの取得
BorderやTitleの表示はWindowの属性として決まっています。なのでWindowを取得し、現在の属性を取得し、必要な部分だけ変更して設定しなおします。

#### Window（ハンドラー）の取得: FindWindow
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-findwindowa
とにかく第二引数に指定した名前のWindow（アプリ）を見つけたい場合は第一引数をNULLにするようです。

```WindowController.cs
    [DllImport("user32.dll", EntryPoint = "FindWindow")]
    public static extern IntPtr FindWindow(System.String className, System.String windowName);

    string windowName = "net_clock"; // Unityで設定するアプリ名とそろえる。
    var window = FindWindow(null, windowName);
```

#### Window属性の取得: GetWindowLong
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlonga
第一引数で指定したハンドラーのWindowの属性（情報）を取得します。ただしWindowの属性は何種類もあり、内部仕様があるのか知りませんが32bitで収まっていません。そこで第二引数で取得/変更したい属性のインデックスを指定します。
（すなわち、特定の属性を変更したい場合はそれに対応したIndexで取得、設定する必要があるということです）

```WindowController.cs
    [DllImport("user32.dll")]
    public static extern int GetWindowLong(IntPtr hWnd, int nIndex);
```

ほかのAPIについてはそれぞれのユースケースに沿って述べていくことにします。

## 枠（BORDER）とタイトルバーを消す
#### BORDERの属性: WS_CAPTION
BORDERの属性があるのでそれを変更します。すなわち下記です:

https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles

> WS_CAPTION 0x00C00000L
> The window has a title bar (includes the WS_BORDER style).

WS_BORDER と WS_DLGFRAME を合わせてこれにしているコードが散見されますが、あまり意味がないようです。
上のGetWindowLongで書きましたが、Windowの属性は多数あり、取得・設定する際はそれに対応したIndexを使用する必要がありますが、どれなのかはGetWindowLongなどの説明に書いてあります。

> GWL_STYLE  -16
>	Retrieves the window styles.

したがって下記のようなコードで取得ができます。

```WindowsController.cs
            const int GWL_STYLE = -16;
            int style = GetWindowLong(window, GWL_STYLE);
```

#### 属性の変更: SetWindowLong
属性の変更は、現在の属性をGetWindowLongで取得し、SetWindowLongで一部変更したものを設定しなおします。

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlonga

説明のとおり、引数はGetWindowLongと同様で、第三引数に設定する値を渡します。

```WindowsController.cs
    [DllImport("user32.dll")]
    public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

            style &= ~WS_CAPTION;
            SetWindowLong(window, GWL_STYLE, style);
```

#### Window位置の変更: SetWindowPos
BORDERを消すと容易にWindowの位置を変えられなくなります。そこでSetWindowPosで適当な位置に移動させてやります。

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos

第二引数はWindowの順序の操作のようです。こだわりがなければHWND_TOPでよいでしょう。前面に出てきます。
第七引数は（ざっくり言うと）細かい動きを制御できるようです。特にこだわりがなければ0でよさそうです。

```WindowsController.cs
    [DllImport("user32.dll", EntryPoint = "SetWindowPos")]
    private static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);

            const int HWND_TOP = 0;
            SetWindowPos(window, HWND_TOP, x, y, width, height, 0);
```

ここまででこういう感じなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227756/84b686ee-c4bb-379a-1ad7-c63533a5c986.png)

## 背景を透明にする
せっかく枠を消したら背景も消したくなるでしょう。背景の削除は下記の手順で行います。

0. Layered Window属性の設定
0. 背景色へのAlpha値の適用

#### Layered Window属性: WS_EX_LAYERED
https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles

> WS_EX_LAYERED   0x00080000
> The window is a layered window. This style cannot be used if the window has a class style of either CS_OWNDC or CS_CLASSDC.

Layered Windowについて:
https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#layered-windows

> visual effects for a window ... wishes to use alpha blending effects. The system automatically composes and repaints layered windows and the windows of underlying applications
> Alpha Blending 効果を使用したいウィンドウの視覚効果。システムは自動的にLayered Windowと下にあるアプリケーションのウィンドウを構成し再描画します。

これだって感じですね。WS_EX_LAYERED属性もSetWindowLongで設定できますが、Indexは-20になります。

> GWL_EXSTYLE  -20
> Sets a new extended window style. 

```WindowsController.cs
            const int GWL_EXSTYLE = -20;
            const int WS_EX_LAYERED = 0x80000;

            int style = GetWindowLong(window, GWL_EXSTYLE);
            SetWindowLong(window, GWL_EXSTYLE, style | WS_EX_LAYERED);
```

#### Layered Windowの属性の設定: SetLayeredWindowAttributes
https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setlayeredwindowattributes
第三引数が難しくて正直に言って理解できていません。
単純に背景色を透明にするだけなら、第四引数をLWA_COLORKEYにすれば、第二引数（crKey）を透明な色としてくれます。つまりUnityの方で背景色を適当な色にして、そのカラーコードを第二引数に書けばいいです。

```WindowsController.cs
    [DllImport("user32.dll", EntryPoint = "SetLayeredWindowAttributes")]
    private static extern Boolean SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags);

            const int LWA_COLORKEY = 1;
            SetLayeredWindowAttributes(window, 0x00000000, 0, LWA_COLORKEY);
```

これでこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227756/ba63ee78-3ff5-1118-6269-907e95aef679.png)

-----

いじょ。
この記事を書きながら改めて各APIを調査し、コードを整理できました。やはり正しい知識が妥当なコードを書く鍵だと思います。