WindowsAPI

サンプル

環境

Visual Studio 2017

リンク

サンプル

WindowsAPIの必要事前知識

ハンドル

Windowsはプログラムやウィンドウ、ファイルなどを 識別するためにユニーク(一意)な番号を設定しています。 この番号はWindowsが管理しており、その番号を指定して ウィンドウやファイルを操作しています。 このユニークな番号のことをハンドルと呼んでいます。 ハンドルは識別するデータの種類ごとに「~ハンドル」と呼ばれており、 ウィンドウに対して設定しているハンドルと「ウィンドウハンドル」、 ファイルに対して設定してるハンドルを「ファイルハンドル」と呼んでいます。 directx_0001 directx_0001

インスタンス

インスタンスとは「実体」という意味で、オブジェクト指向においてよく使われる用語です。 メモリ上に領域が確保されたデータをインスタンスと呼び、 クラスを作成する場合、クラスのデータがメモリ上に確保されることから インスタンス化と呼んでいます。

インスタンスハンドル> インスタンスハンドルとはOS(Windows)がアプリに設定したハンドルのことです。 この後に説明するWinMain関数でインスタンスハンドルが引数でありますが それがOS(Windows)が決めたそのアプリのハンドル(番号)になります。

WindowsAPI

WindowsAPIとはWindowsプログラミングを行うために Microsoftが提供しているAPIのことです。 APIとはApplication Programming Interfaces の略で プログラムを行うために使用できる関数や規約の集合の事です。 WindowsAPIを使用することでWindowsアプリを作成するために必要な機能を 自分で作成せずに済むので作業工数を短縮することができます。

ウィンドウを作りたい => CreateWindow関数で簡単に作れる

WinMain関数

WinMain関数はC言語のMain関数にあたる関数です。 Windowsアプリを作成する場合は必ずWinMainからプログラムを始める必要があります。

WinMain関数仕様

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) 内容: Windowsアプリを作る場合のエントリポイント C/C++言語のmain関数の役割を持つ 戻り値: int: WinMain関数の実行結果を返す メッセージループ開始前は0を メッセージループ開始後はメッセージ.wParamの値を返す 関数名: WinAPI WinMain WinMain関数の前に設定されている「WinAPI」は Windows関数を呼ぶために必要な規約 ほとんどのWINAPIの関数はWinMainのようにWinAPIがある 引数: 第一引数: HINSTANCE このプログラム(アプリ)に割り当てられた インスタンスハンドル 第二引数: HINSTANCE 古い仕様の残り 常にNULLが入っているので無視しても問題がない 第三引数: LPSTR コマンドラインで設定された文字列のポインタ C言語のmain関数の「char *argc[]」 第四引数: int ウィンドウの初期表示状態の指定 通常表示の他に最大化や最小化表示などがある

ウィンドウ作成の流れ

ウィンドウを作成する手順は以下の通りです。
	1.ウィンドウ情報の登録

		↓

	2.ウィンドウ作成

		↓

	3.ウィンドウのリサイズ

1.ウィンドウ情報の登録

WindowsAPIを使用してウィンドウを表示する場合、CreateWindow関数を
使用してウィンドウの作成を行います。
ですが、CreateWindowの前にウィンドウの性質を設定する必要があります。
性質とはウィンドウスタイル(ウィンドウサイズが変更された時に
再描画を行う、閉じるボタン無効化等)やタイトルのアイコン、
アプリのアイコン、カーソル、背景色の設定などがあります。

WNDCLASSEX構造体

ウィンドウ情報を設定するための構造体です。 構造体のメンバ変数は以下の内容になります。

UINT cbSize

WNDCLASSEX構造体のサイズ

UINT style

ウィンドウスタイル(CS_HREDRAWやCS_VREDRAWなど)

WNDPROC lpfWndProc

ウィンドウプロシージャのアドレス

int cbClsExtra

予備メモリ(構造体登録時に確保される) 基本的に0

int cbWndExtra

ウィンドウオブジェクト作成時に確保されるメモリサイズ 基本的に0

HINSTANCE hInstance

インスタンスハンドル

HICON hIcon

アプリのショートカットなどで使用されるアイコン デフォルトでいいならNULL

HCURSOR hCursor

ウィンドウのクライアント上のマウスカーソル デフォルトでいいならNULL

HBRUSH hbrBackground

ウィンドウのクライアント領域の背景色

LPCTSTR lpszMenuName

メニュー名 メニューがなければNULL

LPCTSTR lpszClassName

Windowクラスの名前

HICON hIconSm

タイトルバーで使用されるアイコン デフォルトでいいならNULL

RegisterClassEx

内容

WNDCLASSEXを登録するための関数 ここで登録した情報が後に行うCreateWindow関数に反映される

戻り値

成功:ATOM型の値 失敗:0

引数

WNDCLASSEXのポインタ ※戻り値のATOMは特に覚えなくていいので、0になったら登録が失敗  それ以外なら成功と覚えておけば問題はありません。

WNDCLASSEX window_class = { // 構造体のサイズ sizeof(WNDCLASSEX), // クラスのスタイル CS_HREDRAW | CS_VREDRAW, // ウィンドウプロシージャ WindowProc, // 補助メモリ 0, // 補助メモリ 0, // インスタンスハンドル instance, // アイコン画像 LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION)), // カーソル画像 LoadCursor(NULL, IDC_ARROW), // 背景ブラシ(背景色) NULL, // メニュー名 NULL, // クラス名 TEXT("WinAPITest"), // 小さいアイコン NULL }; // 構造体の登録 if (RegisterClassEx(&window_class) == 0) { return false; }

2.ウィンドウ作成

ウィンドウクラスの登録を行ったら次はウィンドウの作成を行います。
作成にはCreateWindow関数を使用します。

CreateWindow関数

内容

ウィンドウを作成する関数 無事成功すると作成されたウィンドウのハンドルが返される

戻り値

HWND: ウィンドウハンドル

引数

LPCSTR: 登録されているウィンドウクラスの文字列 LPCSTR: ウィンドウ名(タイトル部分に表示される文字列) DWORD: ウィンドウスタイル メニューボックスの有無 最大、最小ボタンの有無等 int: ウィンドウの表示位置(X軸) int: ウィンドウの表示位置(Y軸) int: ウィンドウの横幅 int: ウィンドウの縦幅 HWND: 親のウィンドウハンドル 親のウィンドウがなければNULL HMENU: メニューハンドル メニューがなければNULL HINSTANCE: インスタンスハンドル LPVOID: WM_CREATEメッセージのlpparamのCREATESTRUCT構造体のポインタ NULLで問題ない

// ウィンドウ作成 window_handle = CreateWindow( TEXT("WinAPITest"), TEXT("WinAPITest"), (WS_OVERLAPPEDWINDOW ^ WS_THICKFRAME) | WS_VISIBLE, CW_USEDEFAULT, 0, width, height, NULL, NULL, instance, NULL); if (window_handle == NULL) { return 0; }

3.ウィンドウのリサイズ

CreateWindowで作成したウィンドウのサイズはタイトル部分を含めたサイズになっています。
しかし、ゲームなどの場合はクライアント領域のサイズをCreateWindowで設定したサイズに
変更する必要があります。

directx_0002
directx_0002

リサイズの方法

リサイズの方法はクライアント領域以外の部分を算出して その値をクライアント領域に加算します。

1.全体のサイズ取得

リサイズのための最初の手順として全体のサイズを取得します。 ウィンドウサイズはGetWindowRect関数で取得できます。

2.クライアントサイズの取得

次はクライアント領域のサイズを取得します。 クライアント領域のサイズはGetClientRect関数で取得できます。

3.ウィンドウ部分のサイズの算出

ウィンドウ部分のサイズを出すには全体のウィンドウサイズの幅と高さから クライアント領域の幅と高さを引いて割り出します。 directx_0003 directx_0003

4.新規ウィンドウサイズの算出

ウィンドウ部分のサイズが割り出せたら新規のウィンドウサイズを算出します。 計算式は3で割り出したウィンドウ部分のサイズと 本来クライアント領域にしたかったサイズを足します。 directx_0004 directx_0004

5.新規サイズの登録

新規サイズが割り出せたらSetWindowPos関数を使用して 新規のサイズを登録します。

RECT window; RECT client; // ウィンドウ領域矩形取得 GetWindowRect(window_handle, &window); // クライアント領域矩形取得 GetClientRect(window_handle, &client); // ウィンドウ外枠のサイズ int window_size_x = (window.right - window.left); int window_size_y = (window.bottom - window.top); int client_size_x = (client.right - client.left); int client_size_y = (client.bottom - client.top) int frame_size_x = window_size_x - client_size_x; int frame_size_y = window_size_y - client_size_y; // ウィンドウサイズ再設定 SetWindowPos( // ウィンドウハンドル window_handle, // 配置順序のハンドル(NULLでよし) NULL, // 表示座標X CW_USEDEFAULT, // 表示座標Y CW_USEDEFAULT, // 新規クライアントサイズ(横) frame_size_x + width, // 新規クライアントサイズ(縦) frame_size_y + height, // SWP_NOMOVE => 位置変更なし SWP_NOMOVE); // ウィンドウ表示 ShowWindow(window_handle, SW_SHOW); // クライアント領域更新 UpdateWindow(window_handle);

メッセージ駆動型(イベント駆動型)

WindowsプログラムはOS(Windows)から様々なメッセージが送られてきます。
メッセージの内容は「画面内で右クリックされた」「ウィンドウのサイズを変更した」等です。
この送られてきたメッセージに対して処理を行うプログラムを
メッセージ駆動型プログラミングと呼びます。
「画面クリック」や「キーボードを押した」等のユーザーの行動(イベント)によって
メッセージが送信されることからイベント駆動型プログラミングとも呼ばれています。

メッセージ

OS(Windows)から送られてくる情報のことをメッセージと呼びます。
メッセージの種類は「画面をクリックした」「画面を再描画する必要がある」
「画面を最小、最大化する」等、数多く用意されています。

メッセージ取得

OS(Windows)からメッセージを受信するにはGetMessageかPeekMessage関数を使用します。 この二つの関数にはそれぞれ特徴があり、その特徴を利用して 後述するメッセーループ、メインループを作成します。

GetMessage

特性: OS(Windows)がメッセージを送信するまで処理が停止する ※scanf関数のようにそれ以降に処理が進まない 内容: OS(Window)からのメッセージを取得する 戻り値: WM_QUITメッセージ:0 WM_QUITメッセージ以外:非0 引数: LPMSG lpMsg 取得メッセージ HWND ウィンドウハンドル メッセージを取得するウィンドウハンドル UINT wMsgFilterMin 取得メッセージの最小値 UINT wMsgFilterMax 取得メッセージの最大値

PeekMessage

特性: OS(Windows)にメッセージの有無を確認しに行く GetMessageと違い、メッセージの有無に関係なく処理が進む 内容: OS(Window)からのメッセージを取得する 戻り値: WM_QUITメッセージ:0 WM_QUITメッセージ以外:非0 引数: LPMSG lpMsg 取得メッセージ HWND ウィンドウハンドル メッセージを取得するウィンドウハンドル UINT wMsgFilterMin 取得メッセージの最小値 UINT wMsgFilterMax 取得メッセージの最大値 UINT wRemoveMsg 取得メッセージの削除オプション PM_NOREMOVE: 取得したメッセージをメッセージキューから削除しない PM_REMOVE: 取得したメッセージをメッセージキューから削除する

メッセージの変換

メッセージの種類の中にキーボードのいずれかを押したというものがあります。 これでキーボードが押された内容はわかりますが、 大文字、小文字の判断はできません。 そこで、キーボード入力メッセージに対して変換をかけて 押されたキーボードの文字がわかるようにします。 このメッセージの変換にはTranslateMessage関数を使用します。

TranslateMessage

メッセージ変換関数はTranslateMessageを使用します。 この関数を使用することでメッセージの変換が行われます。

受信メッセージの送信

受信したメッセージはウィンドウプロシージャと呼ばれる関数で 処理しなければいけません。 ウィンドウプロシージャは通常の関数と異なるので 呼び出すためには専用の関数にメッセージを渡して ウィンドウプロシージャを呼び出します。 このメッセージ送信にはDispatchMessage関数を使用します。

DispatchMessage

メッセージをウィンドウプロシージャに送信する

ウィンドウプロシージャ

ウィンドウプロシージャとはOS(Windows)から送られてきたメッセージを
処理するためのコールバック関数です。
この関数はウィンドウを作成する際に登録します。
ウィンドウプロシージャは戻り値や引数の型や数は決まっており、
変更することができません。
変更可能な箇所は関数名と関数内部の処理です。

ウィンドウプロシージャの基本形

LRESULT CALLBACK 関数名(HWND, UINT, WPARAM, LPARAM)

戻り値

LRESULT: 基本的に自分でメッセージを処理した場合は0、 それ以外はDefWindowProcの戻り値を返す

CALLBACK

WINAPIと同様の呼び出し規約

引数

HWND: ウィンドウハンドル UINT: メッセージ WPARAM、LPARAM: メッセージに対する付加情報 メッセージの内容で情報は異なる

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { // キーを押した case WM_KEYDOWN: break; default: return DefWindowProc(hwnd, msg, wp, lp); } return 0; }

メッセージ取得からウィンドウプロシージャまでの流れ

directx_0009
directx_0009

メッセージループ

メッセージループはWinMain関数に存在するループ処理です。
このループを抜けるとWinMain関数が終了し、プログラムが終了します。
つまり、このメッセージループがWinMainの核となる部分です。

メッセージループの終了条件

メッセージループの終了条件は二つあります。 一つはOSから特定のメッセージを取得すること、 もう一つはOSからのメッセージ取得関数が失敗した場合です。 メッセージ取得関数は「GetMessage」です。 この関数の戻り値が「0」の場合終了メッセージ取得、 「-1」の場合が関数失敗になります。 bool ret = false; MSG msg; // GetMessageの戻り値が0になるまでループする while ((ret = GetMessage(&msg, hWnd, 0, 0) != 0) { // -1は関数エラーなのでループを抜ける if (ret == -1) { break; } TranslateMessage(&msg); DispatchMessage(&msg); }

メッセージループの欠点

メッセージループの欠点はメッセージが送られてきた時以外は 待機し続けることです。 GetMessageの仕様でOS(Windows)からメッセージが送られてくるまでは scanf関数のように処理が先に進まず待機し続けます。 なのでゲームのような常に画面が切り替わり続けるような アプリではメッセージループは有効ではありません。

メインループ

ゲームなどの一定の周期で画面を切り替えるアプリでは
メッセージループではなくメインループと呼ばれるループを作成し、
そのループが一定の周期で繰り返されるようにします。

bool game_loop = true; while (game_loop) { // 0.017秒でループする }

OSからのメッセージに対する対処方法

メインループを作成したら、その中でアプリのメイン処理を 実装していくことになりますが、ほとんどのアプリがOS(Windows)の メッセージを使用することになりますので、 メインループ内でメッセージを取得する必要があります。 取得のために使用する関数がPeekMessageです。 この関数はGetMessageと同様にOS(Windows)から メッセージを取得する関数ですが、GetMessageと違って メッセージがなくても待機状態にはならず、プログラムは進みます。

bool game_loop = true; while (game_loop) { MSG msg; // メッセージ取得(0:メッセージ有り、1:メッセージ無し) if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) != 0) { // メッセージ処理 } else { // アプリ本編の処理 } }

フレーム

フレームとはメインループでループする回数に使用する単位です。

メインループを60回ループ => 60フレーム 1フレームの単位時間はゲームによって異なりますが、 ほとんどのゲームが1秒間を60フレームまたは30フレームとして開発しており、 60フレームの場合は1フレームが17ms、30フレームの場合は33msとなります。

FPS固定処理

ゲームではメインループ1回にかかる時間は固定する必要があります。
でないと時間経過などで正確な時間がはかれなくなったり、
PCのスペックや環境によってゲームの結果が変わってしまいます。
	
※今回紹介している方法は複数あるFPS固定処理のうちのひとつです。
 この方法だけが正解というわけではありません。

時間取得

時間取得はtimeGetTime関数を使用します。 この関数は5msの精度で時間を計測することが可能です。 ただ、ゲームではより高精度の時間計測が必要となるので、 この後に説明するtimeBeginPeriod関数を使用して精度をあげます。

timeGetTime

内容: システムの時間(ミリ秒単位)を取得する 戻り値: システムの時間(ミリ秒単位)

取得時間の精度向上処理

timeGetTimeの通常の精度ではゲームの1フレームの時間を 正確に測ることはできません。 正確に測るには取得する時間の精度を上げる必要があり、 そのためにはtimeBeginPeriodとtimeEndPeriod関数を使用します。 関数の使用場所はtimeBeginPeriodはメインループ開始前、 timeEndPeriodはメインループ終了後です。

timeBeginPeriod

内容: タイマーの精度を変更する 戻り値: 成功:TIMERR_NOERROR 失敗:TIMERR_NOCANDO 引数: タイマーの新しい精度(ms) 例: timeBeginPeriod(1); // タイマーの精度を1msに変更する

timeEndPeriod

内容: timeBeginPeriodで設定した内容を解除する 引数: timeBeginPeriodで設定した値 戻り値: 成功:TIMERR_NOERROR 失敗:TIMERR_NOCANDO

精度が高くない理由

通常のタイマーの精度が高性能ではない理由は タイマーの精度を高めれば高めるほど処理コストが増加するからです。 なので、通常はタイマーの精度をゆるく設定してあり、 ゲームなどの高精度タイマーが必要であれば、精度を高められるようになっています。

FPS固定処理フロー

directx_0005 directx_0005 上のフローにあるようにメインループ内で必要なゲームの処理を行った後、 timeGetTimeで時間計測を行います。 取得した経過時間とひとつ前のループ時に取得した時間の差分が 1フレーム未満だった場合はSleep関数で処理を停止させます。