OpenSiv3D+Windows APIで好きな形のウィンドウを作ろう

# はじめに：自己紹介
OpenSiv3D でゲームやツールなどを作っています、yotio と申します。
Advent Calender 初参戦どころか Qiita 初投稿で幾分慣れない点がありますが、お手柔らかにお願い申し上げます。

# 目的
タイトルの通り、__OpenSiv3D で好きな形のウィンドウを作る！__ というのが今回の主な目的です。
ウィンドウに関する操作といえば、例えば OpenSiv3D ならば標準機能で

```c++:
Window::SetStyle(WindowStyle::Frameless);    // 枠なしウィンドウ
```

とすることで枠なしウィンドウが作れます。

しかし、[Siv3D リファレンス](https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-scene)によれば、ウィンドウスタイルに関して設定可能なオプションは枠無し（Frameless）とリサイズ可能にする（Sizable）のみです。

もっと自由に操りたいのです。例えば、ウィンドウの形そのものを変えるとか、最前面表示にするとか。
ではどうするか？
> すべての道はWindows APIに通ず
> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;─── ジャン・ド・ラ・フォンテーヌ（1621-1695、フランス）

ライブラリに実装されていないなら Windows API に頼ればいいのです。
今回は対象を Windows 版に限定し、Windows API を活用して OpenSiv3D のウィンドウを操ってみたいと思います。  
そして記事の最後に、実際に Windows API と OpenSiv3D を組み合わせて作った自作アプリを作例としてご紹介します。
  
**※注意※  
今回は Windows API を扱うので、Mac 版や Linux 版には対応できません。ご了承ください。**

# 大まかな手順
大まかな手順を示すと以下のようになります。
1. ウィンドウハンドルを取得する
1. Windows API の関数で目的に合わせて処理する

ウィンドウハンドルの取得用メソッドは OpenSiv3D 側に実装されています。
```c++
auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());
```

この``hWnd``を Windows API の各関数に設定値とともに渡してやれば、お好みのウィンドウスタイルが反映されます。

# ウィンドウの形を変える
ウィンドウの中身はそのまま、ウィンドウの形だけ変更してみます。
手順としては：
1. 枠なしウィンドウを作成
1. Windowハンドル取得
1. 図形のリージョンを作成
1. それをWindowハンドルを用いてウィンドウに適用
1. リージョンを解放

この5ステップです。
「枠なしウィンドウを作成」に関しては飛ばしても問題ないですが、ウィンドウの形を変えたときに枠やタイトルバーが見切れる不格好なウィンドウになってしまうので、枠なしウィンドウにしておくことをおすすめします。

## 枠なし角丸ウィンドウを作る
以下にサンプルを示します。
```C++:Main.cpp
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 標準機能で枠なしウィンドウを作成
	Window::SetStyle(WindowStyle::Frameless);

	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// ウィンドウを角丸にする（要Windows API）
	// まず、ウィンドウの形を示すためのリージョンを作成（大きさ:ウィンドウサイズと同じ, 角丸100x100）
	auto hRegion = CreateRoundRectRgn(0, 0, Scene::Width(), Scene::Height(), 100, 100);
	// 次にウィンドウに適用
	SetWindowRgn(hWnd, hRegion, 1);
    DeleteObject(hRegion);

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
これでビルドしてみると…
![SnapCrab_NoName_2022-12-4_1-14-28_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/10bf1c27-09d2-4415-9ecd-f94d16b9628e.png)
角丸なウィンドウが出現しました。  

しかし、このままではウィンドウを動かせません。枠なしウィンドウを生成した場合、ウィンドウの移動処理は別途用意する必要があります。
そこで、マウスカーソルでウィンドウをクリックしたときに、ウィンドウがカーソルに追従させるコードを while 文内に追加します。

```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 標準機能で枠なしウィンドウを作成
	Window::SetStyle(WindowStyle::Frameless);

	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// ウィンドウを角丸にする（要Windows API）
	// まず、ウィンドウの形を示すためのリージョンを作成（大きさ:ウィンドウサイズと同じ, 角丸100x100）
	auto hRegion = CreateRoundRectRgn(0, 0, Scene::Width(), Scene::Height(), 100, 100);
	// 次にウィンドウに適用
	SetWindowRgn(hWnd, hRegion, 1);
    DeleteObject(hRegion);

	// マウスクリックした地点の記録用
	Point mouse_clicked = Point{ 0, 0 };
	while (System::Update()) {
		// ウィンドウの移動用処理（←追加）
		if (MouseL.down()) {
			mouse_clicked = Cursor::Pos();
		}
		if (MouseL.pressed()) {
			Window::SetPos(Cursor::ScreenPos() - mouse_clicked);
		}
		Print << U"Hello!";
	}
}
```
これでマウスカーソルでウィンドウを動かせるようになりました。
![siv3d_1_AdobeExpress (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/e2a966d1-910f-de69-bed6-6cc43d523232.gif)

## いろんな図形のウィンドウを作ってみる
``hRegion``を別の図形に置き換えることで、自由自在な形のウィンドウを生成できます。いくつか例を示してみます。

### 楕円形
``CreateEllipticRgn()``で作成可能。左上と右下の x,y 座標を指定することで楕円形リージョンを作成し、それをウィンドウに適用します。
```c++
...
// 楕円形リージョンの生成
// 左上x=0, 左上y=0, 右下x=Scene::Width(), 右下y=Scene::Height()
auto hRegion = CreateEllipticRgn(0, 0, Scene::Width(), Scene::Height());
// ウィンドウに適用
SetWindowRgn(hWnd, hRegion, 1);
DeleteObject(hRegion);
...
```
![SnapCrab_NoName_2022-12-12_23-57-14_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/13df8e6f-12e3-9589-b84f-2b86fdecfac7.png)
### 多角形
``CreatePolygonRgn()``で多角形リージョンを作成してウィンドウに適用することで、より自由自在な形に変更できます。
``POINT``型の座標配列を用意しておき、各要素に座標値を代入、そして座標配列と n 角形の辺の数 n を指定することで、多角形のリージョンが生成されます。

例えば三角形。
```c++
...
// 三角形リージョンの生成
POINT pt[3];
pt[0].x = 0;				pt[0].y = 0;					// 頂点1: (0, 0)
pt[1].x = Scene::Width();	pt[1].y = 0;					// 頂点2: (Scene::Width(), 0)
pt[2].x = 30;				pt[2].y = Scene::Height();		// 頂点3: (30, Scene::Height())
auto hRegion = CreatePolygonRgn(pt, 3, ALTERNATE);			// 多角形リージョンより生成
// ウィンドウに適用
SetWindowRgn(hWnd, hRegion, 1);
DeleteObject(hRegion);
...
```
![SnapCrab_NoName_2022-12-13_0-5-52_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/1e7af767-2a99-d326-5fb5-e4a98dc9982b.png)

次に星型。
```c++
...
// 星型リージョンの生成
POINT pt[10];
int r = 300;
pt[0].x = Scene::Center().x + r * Math::Cos(-90_deg);
pt[0].y = Scene::Center().y + r * Math::Sin(-90_deg);
pt[1].x = Scene::Center().x + r / 2.5 * Math::Cos(-90_deg + 36_deg);
pt[1].y = Scene::Center().y + r / 2.5 * Math::Sin(-90_deg + 36_deg);
pt[2].x = Scene::Center().x + r * Math::Cos(-90_deg + 72_deg);
pt[2].y = Scene::Center().y + r * Math::Sin(-90_deg + 72_deg);
pt[3].x = Scene::Center().x + r / 2.5 * Math::Cos(-90_deg + 72_deg + 36_deg);
pt[3].y = Scene::Center().y + r / 2.5 * Math::Sin(-90_deg + 72_deg + 36_deg);
pt[4].x = Scene::Center().x + r * Math::Cos(-90_deg + 72_deg * 2);
pt[4].y = Scene::Center().y + r * Math::Sin(-90_deg + 72_deg * 2);
pt[5].x = Scene::Center().x + r / 2.5 * Math::Cos(-90_deg + 72_deg * 2 + 36_deg);
pt[5].y = Scene::Center().y + r / 2.5 * Math::Sin(-90_deg + 72_deg * 2 + 36_deg);
pt[6].x = Scene::Center().x + r * Math::Cos(-90_deg + 72_deg * 3);
pt[6].y = Scene::Center().y + r * Math::Sin(-90_deg + 72_deg * 3);
pt[7].x = Scene::Center().x + r / 2.5 * Math::Cos(-90_deg + 72_deg * 3 + 36_deg);
pt[7].y = Scene::Center().y + r / 2.5 * Math::Sin(-90_deg + 72_deg * 3 + 36_deg);
pt[8].x = Scene::Center().x + r * Math::Cos(-90_deg + 72_deg * 4);
pt[8].y = Scene::Center().y + r * Math::Sin(-90_deg + 72_deg * 4);
pt[9].x = Scene::Center().x + r / 2.5 * Math::Cos(-90_deg + 72_deg * 4 + 36_deg);
pt[9].y = Scene::Center().y + r / 2.5 * Math::Sin(-90_deg + 72_deg * 4 + 36_deg);
auto hRegion = CreatePolygonRgn(pt, 10, ALTERNATE);			// 多角形リージョンより生成
// ウィンドウに適用
SetWindowRgn(hWnd, hRegion, 1);
DeleteObject(hRegion);
...
```
![SnapCrab_NoName_2022-12-13_0-46-39_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/89bc2135-6760-97c6-c071-dac453f8f988.png)

### OpenSiv3D の図形クラスから生成する
もちろん、OpenSiv3D の図形クラスから作ることもできます。
星型を図形クラス``Shape2D::Star``から作ると、こう。
```c++
...
// 星型リージョンの生成
POINT pt[100];
int r = 300;
// Siv3D の図形からポリゴンを取得
s3d::Polygon polygon = Shape2D::Star(r, Scene::Center()).asPolygon();
// ポリゴンの座標をptにコピー
int i = 0;
for (const auto& point : polygon.outer()) {
	if (i > 100) {
		break;
	}
	pt[i].x = point.x;
	pt[i].y = point.y;
	i++;
}

auto hRegion = CreatePolygonRgn(pt, i, ALTERNATE);			// 多角形リージョンより生成
// ウィンドウに適用
SetWindowRgn(hWnd, hRegion, 1);
DeleteObject(hRegion);
...
```
![SnapCrab_NoName_2022-12-13_21-17-48_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/3efd4185-237a-f78c-bf79-7fa0febfaaac.png)
こちらのほうがシンプルかつ簡単に作れますね。

### 絵文字フォントから生成する
絵文字からもウィンドウを形成できます。
例えば、クリスマスツリーの絵文字からウィンドウを作成するとなると、
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>
void Main()
{
	// 標準機能で枠なしウィンドウを作成
	Window::SetStyle(WindowStyle::Frameless);

	// 背景色
	Scene::SetBackground(Color(Palette::Green));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// クリスマスツリー型リージョンの生成
	POINT pt[1000];
	const Font emojiFont{ 20, Typeface::ColorEmoji };
	const Font font(20);

	// Imageに🎄をoverwrite
	// ImageからMultiPolygonを取得
	Image image{ 150, 150 };
	auto tree = emojiFont(U"🎄");
	tree.overwrite(image, Vec2{ -15, 0 });	// フォントの余白分を左に寄せてoverwriteする
	MultiPolygon polygons = image.alphaToPolygons();

	// ポリゴンをもとにリージョンを作成
	int max_x = 0, max_y = 0;
	int i = 0;
	for (const auto& polygon : polygons)
	{
		for (const auto& point : polygon.outer())
		{
			if (i > 1000) {
				break;			// 最大1000角形とする
			}
			pt[i].x = point.x * 2;
			pt[i].y = point.y * 2;

			if (max_x < pt[i].x)
				max_x = pt[i].x;
			if (max_y < pt[i].y)
				max_y = pt[i].y;

			i++;
		}
	}
	auto hRegion = CreatePolygonRgn(pt, i, ALTERNATE);			// 多角形リージョンより生成

	// ウィンドウをリサイズ
	Window::Resize(max_x, max_y);

	// ウィンドウに適用
	SetWindowRgn(hWnd, hRegion, 1);
    DeleteObject(hRegion);

	// マウスクリックした地点の記録用
	Point mouse_clicked = Point{ 0, 0 };
	while (System::Update()) {
		// ウィンドウの移動用処理
		if (MouseL.down()) {
			mouse_clicked = Cursor::Pos();
		}
		if (MouseL.pressed()) {
			Window::SetPos(Cursor::ScreenPos() - mouse_clicked);
		}

		font(U"Merry Xmas!").drawAt(Scene::Width() / 2, Scene::Height() / 2 + 70);
		emojiFont(U"🎄").drawAt(Scene::Width() / 2, Scene::Height() / 2);
	}
}
```
![SnapCrab_NoName_2022-12-13_20-49-39_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/5acbe32c-703b-14c2-13e0-9ee9b51716ab.png)

このように、絵文字を一旦 Image に書き出し、そこからポリゴンを取得して、ポリゴンの各座標からリージョンを作成することで実現しています。

# ウィンドウの半透明化

後ろが透けるウィンドウも作れます。
ウィンドウをレイヤードウィンドウ（``WS_EX_LAYERED``）として設定しておき、``SetLayeredWindowAttributes()``に任意の不透明度を指定することで半透明化できます。
不透明度は0～255で指定。0に近づくほど透明になります。

例：不透明度 200 のウィンドウを生成
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{

	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// ウィンドウを半透明化
	auto nIndex = GetWindowLongA(hWnd, GWL_EXSTYLE);
	SetWindowLongA(hWnd, GWL_EXSTYLE, nIndex | WS_EX_LAYERED);
	SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA);    // 不透明度200とする

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
![SnapCrab_NoName_2022-12-13_21-56-9_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/f6e017b0-4c60-71cd-c3cb-236af794a460.png)


# ウィンドウスタイルを変える

``SetWindowLongA()``で``GWL_STYLE``または``GWL_EXSTYLE``を変更してやります。
注意点として、``GWL_STYLE``と``GWL_EXSTYLE``は論理和で表現されるので、予め
```c++
auto nIndex = GetWindowLongA(hWnd, GWL_STYLE);
```
で現在の値を取得しておき、現在の値に論理演算でオプションを追加 / 消去した値を``SetWindowLongA()``に渡してやることで実現できます。

## アイコン・最小化・最大化・閉じるボタン非表示

ウィンドウの左上のアイコンと、右上にある 最小化・最大化・閉じるボタンを非表示にできます。
``GWL_STYLE``から``WS_SYSMENU``を除去します。
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// 現在のウィンドウスタイルを取得
	auto nIndex = GetWindowLongA(hWnd, GWL_STYLE);

	// アイコン・最小化・最大化・閉じるボタン非表示
	SetWindowLongA(hWnd, GWL_STYLE, nIndex & ~WS_SYSMENU);

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
![SnapCrab_NoName_2022-12-12_2-12-0_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/32dd1b79-3eb5-efa3-d4c9-fd46f44264c8.png)
画像では分かりづらいですが、よく見るとタイトル表示はそのままで、左側のアイコンと右側の3つのボタンだけ消えてることが確認できます。

## 閉じるボタンだけ表示

今度は、最小化・最大化ボタンは非表示にしておき、閉じるボタンだけ右上に表示するウィンドウを作成します。
``GWL_STYLE``から``WS_MINIMIZEBOX``を除去した値をセットします。

```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// 現在のウィンドウスタイルを取得
	auto nIndex = GetWindowLongA(hWnd, GWL_STYLE);

	// 閉じるボタンだけ表示
	SetWindowLongA(hWnd, GWL_STYLE, nIndex & ~WS_MINIMIZEBOX);

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
![SnapCrab_NoName_2022-12-12_2-15-55_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/4197d591-ac37-309e-3d45-cb8ea686de96.png)

## タイトルバーを左右逆にする

拡張ウィンドウスタイルに``WS_EX_LAYOUTRTL``を追加します。元々はアラビア語のような右から左に表記する言語向けの機能ですが、日本語環境でも使えます。
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// 現在のウィンドウ拡張スタイルを取得
	auto nIndexEx = GetWindowLongA(hWnd, GWL_EXSTYLE);

	// タイトルバーを左右逆に
	SetWindowLongA(hWnd, GWL_EXSTYLE, nIndexEx | WS_EX_LAYOUTRTL);

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
![SnapCrab_Siv3D App (Debug Build)  D3D11  60 FPS  F 800x600  V 800x600  S 800x600_2022-12-13_21-37-29_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/9f73a071-6424-9881-bc2f-18e2d70d7b0c.png)

## 細めのタイトルバー
ちょっとだけ細いタイトルバーを持つウィンドウを生成します。拡張ウィンドウスタイルに``WS_EX_TOOLWINDOW``を追加します。
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

void Main()
{
	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// 現在のウィンドウ拡張スタイルを取得
	auto nIndexEx = GetWindowLongA(hWnd, GWL_EXSTYLE);

	// 細めのタイトルバー（フローティング ツールバー）
	SetWindowLongA(hWnd, GWL_EXSTYLE, nIndexEx | WS_EX_TOOLWINDOW);

	while (System::Update()) {
		Print << U"Hello!";
	}
}
```
![SnapCrab_Siv3D App (Debug Build)  D3D11  60 FPS  F 800x600  V 800x600  S 800x600_2022-12-13_21-40-42_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/05481b2f-0370-48fa-9a4a-8db3099de54d.png)

# その他

## ウィンドウを最前面表示にする

ウィンドウが常に最前面に表示されるようにもできます。
``SetWindowPos()``で``HWND_TOPMOST``を設定してやればOK。
```c++
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
```
![siv3d_2_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/60c47734-038b-766a-b0ee-2feef98bf665.gif)


最前面表示を解除するには``HWND_NOTOPMOST``を設定します。
```c++
SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
``` 


## dpi の変化に対応する

Windowsでは 96dpi が標準になっていますが、設定により画面の拡大率、すなわち dpi を変更できます。
![SnapCrab_NoName_2022-12-12_18-1-6_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/a7817112-85e0-e4e1-da66-9a76fe5a7d26.png)
推奨される拡大率は画面の解像度によって異なっており、例えば 4K モニターの場合は 150％、つまり 144dpi に設定することが推奨されています。

ここで問題となってくるのは、ウィンドウを形作るリージョンのサイズも画面の dpi に合わせて変化させなければならない点です。
``SetWindowRgn()``でウィンドウの形を変えたとき、Windows の設定で画面の dpi が変化すると、ウィンドウサイズが表示領域と合わなくなり、ウィンドウの大きさやレイアウトが崩れてしまいます。

OpenSiv3D 自体は dpi の変化に対応していると思われますが、Windows API を使用した部分に関しては自分で対応しなければなりません。
そこで、現在のモニタの dpi を取得し、96dpi に対して何倍の数値であるかを計算した上で、それを掛けたウィンドウサイズを``SetWindowRgn()``に渡すことで、dpi の変化に対応できます。

現在の dpi の値は``GetDpiForWindow(hWnd)``で取得できます。
例として、角丸ウィンドウのサンプルで dpi の変化に対応させてみます。
```c++
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <Siv3D/Windows/Windows.hpp>

#define DPI_STANDARD	96

// 現在の dpi 値を取得（←追加）
// 96dpi に対して何倍の値か？
double getDpiDist(HWND hWnd) {
	return (double)GetDpiForWindow(hWnd) / DPI_STANDARD;
}

void Main()
{
	// 標準機能で枠なしウィンドウを作成
	Window::SetStyle(WindowStyle::Frameless);

	// 背景色は白に
	Scene::SetBackground(Color(Palette::White));

	// Windowハンドル取得
	auto hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());

	// ウィンドウを角丸にする（要Windows API）
	// 現在の dpi を取得（←追加）
	double dpi_dist = getDpiDist(hWnd);
	// ウィンドウの形を示すためのリージョンを作成（大きさ:ウィンドウサイズ * dpiの倍数, 角丸100x100）
	auto hRegion = CreateRoundRectRgn(0, 0, Scene::Width() * dpi_dist, Scene::Height() * dpi_dist, 100, 100);
	// 次にウィンドウに適用
	SetWindowRgn(hWnd, hRegion, 1);
    DeleteObject(hRegion);

	// マウスクリックした地点の記録用
	Point mouse_clicked = Point{ 0, 0 };
	while (System::Update()) {
		// ウィンドウの移動用処理（←追加）
		if (MouseL.down()) {
			mouse_clicked = Cursor::Pos();
		}
		if (MouseL.pressed()) {
			Window::SetPos(Cursor::ScreenPos() - mouse_clicked);
		}
		Print << U"Hello!";
	}
}
```
これをまずは 96dpi のままで実行してみます。
![SnapCrab_NoName_2022-12-12_17-58-57_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/b8458587-76d3-0b98-3819-b7c9c2e62340.png)
96dpi に対して 96dpi は1倍なので、特に変化はありません。
次に 144dpi（150%）に設定変更した上で実行してみます。
![SnapCrab_NoName_2022-12-12_18-1-46_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/536db774-704f-a9ba-38b2-bca0c4d06da3.png)
画像では分かりづらいですが、拡大率の変化とともにリージョンのサイズが1.5倍になっており、画面の拡大率が変化してもレイアウトを崩すことなく実行できています。

# 作例：Albus Box
最後に、Windows API と OpenSiv3D を組み合わせていろいろやった例として、[Albus Box](https://github.com/YotioSoft/Albus-Box) という自作の音楽プレイヤーアプリをご紹介します。
![SnapCrab_NoName_2022-12-11_14-44-25_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236924/8abb76c7-eee9-0b0b-d3c0-3505e222f6f7.png)
一番最初にご紹介した角丸ウィンドウをウィンドウスタイルに応用しており、さらに最前面表示と dpi の変化にも対応。いずれも Windows API で実現しています。
数年前に一瞬だけ流行したニューモーフィズムデザインを採用しており、丸みを帯びたデザインなので角丸ウィンドウがピッタリです。
（本題からは外れますが、ニューモーフィズム UI 自体のソースコードは[こちら](https://github.com/YotioSoft/NeumorphismUI)で配布しています）

## ウィンドウを角丸ウィンドウにする ＆ dpi の変化への対応
角丸長方形のリージョンをウィンドウに適用し、dpi の変化にも対応します。
アプリ実行中の画面拡大率の変更にも対応できるよう、while 文内で一定の間隔で現在の dpi を確認し、dpi が変化していたらウィンドウのリージョンに適用して更新します。
```c++
#define DPI_STANDARD	96

double before_dpi_dist = 0.0;
HWND hWnd;
double dpi_dist;

double specific::getDpiDist() {
	return (double)GetDpiForWindow(hWnd) / DPI_STANDARD;
}

// ウィンドウスタイルの設定
bool specific::setWindowStyle(int x1, int y1, int x2, int y2, int w, int h, bool update_dpi) {
	// ウィンドウハンドルを取得（Siv3D）
	if (hWnd == 0) {
		hWnd = static_cast<HWND>(s3d::Platform::Windows::Window::GetHWND());
	}

	if (update_dpi)
		dpi_dist = getDpiDist();
	if (dpi_dist == before_dpi_dist) {
		return;    // リージョンの更新の必要がないならここでreturn
	}
	before_dpi_dist = dpi_dist;

	// 角丸長方形の生成
	auto hRegion = CreateRoundRectRgn(x1, y1, x2 * dpi_dist, y2 * dpi_dist, w, h);
	// 角丸長方形をウィンドウの形に適用
	SetWindowRgn(hWnd, hRegion, 1);
    DeleteObject(hRegion);
}
```
これを while ループ内で呼び出します。
```c++
#define WINDOW_WIDTH			    400
#define WINDOW_HEIGHT			    640
#define WINDOW_ROUNDRECT_RADIUS		40

void Main() {
    Scene::SetBackground(DEFAULT_BACKGROUND_COLOR);
	Window::Resize(WINDOW_WIDTH, WINDOW_HEIGHT);
    ...

    while (System::Update()) {
        // fft_updateは数周に1回trueになる
        setWindowStyle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 40, 40, fft_update);
        ...
    }
}
```
## 最前面表示への対応
ウィンドウ右上のピンボタンが押されたとき、最前面表示の ON/OFF を切り替えます。
```c++
bool before_pin_window = false;

// ウィンドウ最前面表示のON/OFF
void specific::pinWindow(const bool pin) {
    // 前回と比較し、ピンボタンの値が変化していたら最前面表示の切り替え
	if (pin != before_pin_window) {
		if (pin) {
			SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
		}
		else {
			SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
		}
		before_pin_window = pin;
	}
}
```

# 参考文献
- Microsoft Learn
    - CreateRoundRectRgn 関数 (wingdi.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createroundrectrgn](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createroundrectrgn)
    - SetWindowRgn 関数 (winuser.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowrgn](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowrgn)
    - DeleteObject 関数 (wingdi.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-deleteobject](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-deleteobject)
    - CreateEllipticRgn 関数 (wingdi.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createellipticrgn](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createellipticrgn)
    - CreatePolygonRgn 関数 (wingdi.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createpolygonrgn](https://learn.microsoft.com/ja-jp/windows/win32/api/wingdi/nf-wingdi-createpolygonrgn)
    - GetWindowLongA function (winuser.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getwindowlonga](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getwindowlonga)
    - SetWindowLongA 関数 (winuser.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowlonga](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowlonga)
    - SetWindowPos 関数 (winuser.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowpos](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-setwindowpos)
    - GetDpiForWindow 関数 (winuser.h)
      [https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdpiforwindow](https://learn.microsoft.com/ja-jp/windows/win32/api/winuser/nf-winuser-getdpiforwindow)
    - ウィンドウスタイルのオプション
      [https://learn.microsoft.com/ja-jp/windows/win32/winmsg/window-styles](https://learn.microsoft.com/ja-jp/windows/win32/winmsg/window-styles)
    - 拡張ウィンドウスタイルのオプション
      [https://learn.microsoft.com/ja-jp/windows/win32/winmsg/extended-window-styles](https://learn.microsoft.com/ja-jp/windows/win32/winmsg/extended-window-styles)
- OpenSiv3D でウィンドウハンドルを取得する方法について
    - OpenSiv3D(Windows)で自前のウィンドウプロシージャを使用する
      [https://qiita.com/tyanmahou/items/88b793e255f1e04f85d3](https://qiita.com/tyanmahou/items/88b793e255f1e04f85d3)
- Siv3D リファレンス
    - チュートリアル 15 | シーンとウィンドウ
      [https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-scene](https://zenn.dev/reputeless/books/siv3d-documentation/viewer/tutorial-scene)

- 以下、ご参考までに（自分のブログ記事です）
  - [デスクトップアプリ開発ではdpiの違いに要注意 | 為せばnull](https://blog.yotiosoft.com/2022/01/08/%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E3%81%A7%E3%81%AFdpi%E3%81%AE%E9%81%95%E3%81%84%E3%81%AB%E8%A6%81%E6%B3%A8%E6%84%8F.html)