4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Windows上で動く、見た目だけMacっぽいアプリを作った

Last updated at Posted at 2022-12-12

はじめに

 WindowsとMacでは色々な違いがありますが、普段遣いする上で気がつく最も大きな差の一つが「アプリの見た目」でしょう。例えばWindowsでは右上に最小化ボタン、最大化ボタン、終了ボタンがありますが、Macでは逆に左上についています。またその見た目も全く異なりますね。
 他にも、メニューバーの位置も異なります。Windowsでは各アプリケーションの上部にありますが、Macでは全アプリケーション共通のスペースが画面の最上部に位置します。

 さて、僕は「こっちが使いやすい」のような終わりのない議論をするつもりはありません。今回僕が行うのはあくまで一種の実験です。

 その実験とは「Windowsは自由にアプリケーション設計が出来るのだから、見た目だけMacっぽいアプリも作れるのではないか?」というものです。
 では早速作っていきましょう!

完成目標

 以下の写真のような、Macっぽい見た目のアプリを完成させようと思います。
source3.png

※この先を読む方へ
 この記事はWindowsAPIをある程度知っている方向けに書いています。もしWindowsAPIに詳しくないなら、先にこちらの記事 を読むか、あるいはプログラムの解説をすっ飛ばして読むのが良いと思います。
 プログラムの全文はGithubに載せています。良かったら見てみてください。なお、Visual Studioでコンパイルしました。完成版のプログラムはこちら

タイトルバーが無いウィンドウ

CreateWindow関数

 前提として、Windows APIでウィンドウを作るにはCreateWindow関数を用います。なおCreateWindowAとCreateWindowWがありますが、この違いは「アプリケーションのID」と「Windowのタイトル」に使う文字列がchar(1バイト)かwchar_t(2バイト)かという違いがあります。基本的にはどちらを使っても大丈夫ですが、僕はCreateWindowAを使いました。

/*
void CreateWindowA(
  アプリケーションのID(RegisterClassExで指定した文字列),
  Windowのタイトル,
  Windowスタイル,
  Windowの左上のX座標, Y座標,
  Windowの幅, 高さ,
  NULL, NULL, hCurInst, NULL);
*/
HWND hWnd = CreateWindowA(
		"MyFirstApp",
		"Title",
		WS_OVERLAPPEDWINDOW,
		0, 0, 500, 300,
		NULL, NULL, hCurInst, NULL);

ウィンドウスタイル

 さて、このままでは普通にタイトルバーやシステムボタン(最小化・最大化・閉じる)があるウィンドウが出来てしまいます。今回はそれらが無いウィンドウを作りたいので、第三引数の「Windowのタイトル」を変更しないといけません。
 そんな訳で使うのが「WS_POPUP」というスタイル。これを使う事でタイトルバーもシステムボタンもない、真っ白なウィンドウを作る事が出来ます。
 以下のスクリーンショットをご覧ください。「本当に真っ白なウィンドウ」が出来ています。※ペイントで白い四角形を書き足した訳じゃないですよ!!
source1

    HWND hWnd = CreateWindowA(
		"WinAppLikeMac",
		"",
		WS_POPUP,
		100, 100, 500, 300,
		NULL, NULL, hCurInst, NULL);

DestroyWindow関数

 このままでは、このウィンドウを閉じる事が出来ません。そこで、エスケープキーを押したら消えるようにプログラムしようと思います。
 ウィンドウに何かが起きた時(クリックされた、最小化された、文字が入力された、など)、ウィンドウプロシージャと呼ばれる関数が呼び出されます。この関数は四つの引数があり、ウィンドウのハンドル、メッセージ、Wパラメータ、Lパラメータがあります。

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
	if(msg==クリックされた){
        何かする
        return 0;
    }
    if(msg==キーが押された){
        何かする
        return 0;
    }
    ……その他多数の処理……
    //特にすることが無い場合、デフォルトの行動を行う
	return DefWindowProcA(hWnd, msg, wp, lp);
}

 ここで、何かのキーが押されると、そのウィンドウには「WM_KEYDOWN」というメッセージが送られてきます。つまり、msg==WM_KEYDOWNとなる訳ですね。その時、Wパラメータにキーコードが入っていますので、それを基に「押されたキーはエスケープキーか?」を判断します。
 ウィンドウを削除するのに使う関数は「DestroyWindow関数」です。これを使えば、明示的にウィンドウを消す事が出来ます。
 具体的には、ウィンドウプロシージャに以下の様に書き足しました。

    case WM_KEYDOWN:
		if (wp == VK_ESCAPE) {
			DestroyWindow(hWnd);
		}
		return 0;

プログラム全体をGitHubで公開中

偽のタイトルバーを作る

 このままでは、ただの真っ白な四角形です。ここから、アプリっぽくしていきますよ~!

WM_PAINTメッセージ

 先ほど、ウィンドウプロシージャの話をしました。ここでは新しいメッセージ「WM_PAINT」について話します。
 WM_PAINTは「再描画が必要な時」に呼び出されます。例えば、ウィンドウが出来た直後、何かを描かないといけませんよね。また、画面のサイズが変わった時もウィンドウを「再描画」しないといけませんよね。

四角形を描く

 まずはタイトルバー代わりになる「灰色の四角形」をウィンドウの上部に書こうと思います。
 WindowsAPIで四角形を描画する関数はRectangle関数です。引数は四つ、ウィンドウのメモリデバイスコンテキストと左上の座標と右下の座標です。

「ちょっと待て、メモリデバイスコンテキストって何ぞや?」

 当然の疑問ですよね。これは一言で言うと「メモリ上にある画面」です。例えば今あなたはQiitaを開いておられるかと思いますが、あなたが見ているこの画面は一種の「画像」ですよね。ディスプレイは随時、この「画像」を表示している訳です。
 先ほどから「画像」と言っている物こそが「メモリ上にある画面」であり、メモリデバイスコンテキストなのです。お分かりいただけたでしょうか?

HDC hdc;
PAINTSTRUCT ps;
……中略……
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		Rectangle(hdc, 左上のX, 左上のY, 右下のX, 右下のY);
		EndPaint(hWnd, &ps);

 こんな感じです。なお、hdcがメモリデバイスコンテキストです。

四角形に色を塗る

 上記のプログラムを動かしても、黒い枠線の付いた四角形しか描画されません。このままでは少し「タイトルバー」っぽくないです。
 そこで、四角形に色を塗ろうと思います。塗りつぶすためには「ブラシ」と言う物を定義しなくてはいけません。

「だいたいわかるぞ。SetBrush(色);みたいに書けばいいって事だろ?」

 半分正解で半分不正解です。詳しく話すと物凄くややこしい話になるので、ざっくり話すと「WindowsAPIではブラシやペンは裏でmalloc的な物が動いているから、作ったら破棄しないといけません」。つまり、「赤色の四角形を描きたい!」と思ったら......

「ブラシの作成」
・red_brush=CreateSolidBrush(RGB(255, 0, 0));

「描画 ※描画が終わったら、ブラシの設定を元に戻す」
・original_brush=元のブラシを記憶;
・SelectObject(hdc, red_brush);
・Rectangle(hdc, ......);
・SelectObject(hdc, original_brush);

「ブラシの削除」
・DeleteObject(red_brush);

 という行程が必要になります。

 ここで、「ブラシの作成」はWM_CREATEが飛んできた時に行います。「ブラシの削除」はWM_DESTROYが呼び出された時に行います。
 という訳で、ウィンドウプロシージャをどん!

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
    static PAINTSTRUCT ps;
	static HDC hdc;
	static HGDIOBJ defpen, defbrush;
	static HBRUSH titlebrush;
	switch (msg) {
	case WM_CREATE:
		titlebrush = CreateSolidBrush(RGB(235, 235, 235));
		return 0;
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		defbrush = SelectObject(hdc, titlebrush);
		defpen = SelectObject(hdc, GetStockObject(NULL_PEN));
		Rectangle(hdc, 0, 0, 500, 32);
		SelectObject(hdc, defbrush);
		SelectObject(hdc, defpen);
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		DeleteObject(titlebrush);
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProcA(hWnd, msg, wp, lp);
}

 なんで四角形を描くだけで、こんな面倒なことをしないといけないんだよ……。

ウィンドウの大きさ

 先ほどのプログラムで、タイトルバーを描いているのは「Rectangle(hdc, 0, 0, 500, 32);」ですが、このままではウィンドウの大きさが変わってもタイトルバーの大きさが変わらないです。それを避けるため、ウィンドウの大きさが変わったら、それを受け取れるようにしたいと思います。
 またまたウィンドウプロシージャに加筆します。今回使うのはWM_SIZEWM_SIZINGWM_MOVEWM_MOVINGという四つのメッセージです。これらは全て、ウィンドウが動いたりしたときに呼び出されます。これらのメッセージが飛んで来たら、ウィンドウの大きさを取得するようにしました。そのプログラムはこんな感じ。

RECT windowrect;
……中略……
	case WM_SIZE:
	case WM_SIZING:
	case WM_MOVE:
	case WM_MOVING:
		GetWindowRect(hWnd, &windowrect);
		return 0;

 GetWindowRectはウィンドウの位置と大きさを取得する関数です。その第二引数はRECT構造体のポインタで、この構造体は左上と右下の座標を保持します。

WM_NCHITTEST

僕「よし、良い感じに出来上がったぜ! ちゃんとウィンドウが表示されて、タイトルバーっぽい物も表示されたぞ!」

10秒後

僕「……ちょっと待った。このウィンドウ、動かせないし、大きさも変えれないんだが?」

 そうです。このままでは、ウィンドウの大きさを変えることは出来ないのです。また、今表示されているのは「タイトルバーっぽく見える四角形」であり、タイトルバーじゃないです。ですので、そこをドラッグしてもウィンドウは動かないですね。

僕「何とかならないものか……? そうだ!」

 という訳で、このままではウィンドウとしての動きが出来ないので、もう少し加筆していきますね!

 ウィンドウプロシージャに「WM_NCHITTEST」というメッセージが送られてくる事があります。これは「今、カーソルが乗ってる場所ってどこ?」と言うのをシステムが聞く訳ですね。
 つまり、こういう事です。


~~
~~~
「ふわああ、良く寝た。あれ?」

 昨日、徹夜でゲームしていた為、少し寝不足のシステム。窓から入ってきた日の光に目を覚ました彼は、目を覚まそうと思いっきり伸びをするが、何か柔らかい物に触れた。寝ぼけ眼でそちらを見ると、妹のウィンドウが立っているのが見える。

「あ、ウィンドウ。おはよー。ところで、これって何だ?」もにゅ

「そ、そこは私の……よ!」

「え、なんだって?」

「そこは私のタイトルバーよ! いつまで触ってんのよ、バカーー!」
~~~
~~

 って感じですね。

 そのコードがこちら。

	case WM_NCHITTEST:
		mousex = GET_X_LPARAM(lp);
		mousey = GET_Y_LPARAM(lp);
		if (mousey <= windowrect.top + 5) {
			if (mousex <= windowrect.left + 5)return HTTOPLEFT;
			if (mousex >= windowrect.right - 5)return HTTOPRIGHT;
			return HTTOP;
		}
		else if (mousey <= windowrect.top + 32)return HTCAPTION;
		else if (mousey >= windowrect.bottom - 5) {
			if (mousex <= windowrect.left + 5)return HTBOTTOMLEFT;
			if (mousex >= windowrect.right - 5)return HTBOTTOMRIGHT;
			return HTBOTTOM;
		}
		if (mousex <= windowrect.left + 5)return HTLEFT;
		if (mousex >= windowrect.right - 5)return HTRIGHT;
		return HTCLIENT;

 Lパラメータにマウスの座標が入っているのですが、それを取得するには「GET_X_LPARAMマクロ、そしてGET_Y_LPARAMマクロ」を利用します。これを利用するためには「#include <windowsx.h>」というように、windowsx.hをインクルードしてやる必要があります。

 HTTOPLEFTHTTOPHTTOPRIGHTはカーソルがウィンドウの上部にあり、ドラッグされたらリサイズすべきという事を意味します。
 HTCAPTIONはタイトルバーに触れていることを意味し、ドラッグされたらウィンドウが動きます。
 HTCLIENTはその他の領域と言う意味で、特に何もしません。

 ここまでで出来上がった物。
偽のタイトルバー

 ここまでのプログラム全体をGitHubで公開中

システムボタンを作る

 このままではMacっぽくないです。ここから、Macみたいな赤、黄、緑のボタンを作ります。

閉じるボタン

 赤色のブラシを作成し、円を描きます。円を描く関数はEllipse関数です。メモリデバイスコンテキスト、左上の座標(x, y)、右下の座標(x, y)の順番です。「中心の座標と幅・高さ」ではないので注意しましょう。

static HBRUSH red;
…中略…
    case WM_CREATE:
        //略
        red = CreateSolidBrush(RGB(250, 100, 100));
        //略
    case WM_PAINT:
        //略
        Ellipse(hdc, 4, 4, 28, 28);
        //略

 これで円を描くことができました。

 さて、この円がクリックされた時は、NCHITTESTで「そこはタイトルバーではないよ」と言う必要があります。その為、コードは次のようにします。
 ついでにホバーされたらボタンが大きくなるようにしました。

    case WM_PAINT:
        //略
        SelectObject(hdc, red);
		if (hoversysbtn == 1) {
			Ellipse(hdc, 4, 4, 28, 28);
		}
		else {
			Ellipse(hdc, 6, 6, 26, 26);
		}
        //略
	case WM_NCHITTEST:
		mousex = GET_X_LPARAM(lp);
		mousey = GET_Y_LPARAM(lp);
		if (windowrect.left + 4 < mousex && mousex < windowrect.left + 28
			&& windowrect.top + 4 < mousey && mousey < windowrect.top + 28) {
			if (hoversysbtn != 1) {
				hoversysbtn = 1;
				InvalidateRect(hWnd, NULL, FALSE);
			}
		}
        else{
            //先ほどのプログラム
        }

 InvalidateRectは「無理やり再描画させる関数」です。今回は、「ホバーされたらボタンが大きくなる」という物を実装すべく再描画をリクエストしている訳ですね。なお、SendMessage(hWnd, WM_PAINT, 0, 0)としてはいけません。必ずInvalidateRectを使います。

 次に、このボタンがクリックされた時の動作を書きます。クリックの検出にはWM_LBUTTONUPが利用されます。なお、WM_NCHITTESTはクリックされていなくても呼び出されますが、WM_LBUTTONUPはクリック後に呼び出されるという違いがあります。
 また、クリックされた場所を調べるためにGET_X_LPARAMGET_Y_LPARAMを利用する点は先ほどと同様ですが、こちらは「ウィンドウの左上を原点とした座標」です。ややこしい……。

	case WM_LBUTTONUP:
		mousex = GET_X_LPARAM(lp);
		mousey = GET_Y_LPARAM(lp);
		if (4 < mousex && mousex < 28 && 4 < mousey && mousey < 28) {
			DestroyWindow(hWnd);
		}
		break;

最小化・最大化(元に戻す)ボタン

 残りもほとんど同じことをするだけです。
 なお、最小化する際はSendMessageA(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);と書きます。最大化(元に戻す)ボタンが押された時の処理は以下の通りです。

if (IsZoomed(hWnd)) SendMessageA(hWnd, WM_SYSCOMMAND, SC_RESTORE, 0);
else SendMessageA(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);

 IsZoomed関数は、ウィンドウが現在最大化されているかどうかを取得します。もし最大化されているなら元に戻し、最大化されていないなら最大化します。

完成!!!

source3.png

結論

 WindowsAPIをしっかり学べば、色々と面白いことができるという事が分かった。

プログラム全体をGitHubで公開中

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?