Windowsプログラミング入門
ウィンドウズのAPIを使いこなそう


目次

 1ページ目
 第1話 Windowsプログラム版Hello World!
 第2話 ウィンドウの作成【UPDATE】(2010/03/13)
 第3話 アプリ間データの送受信
 第4話 アプリ間データの送受信part2
 第5話 画像の表示1
 第6話 画像の表示2

 関連ページ
 C言語の話(目次)
 C言語入門
 C++入門



ちょっとリニューアルしました。記事を書いたのが6年も7年も前なので、 随時加筆・修正していきたいと思います。(2010/03/13)


Windowsプログラム版Hello World!


Windowsプログラミングに入門しましょう!

記念すべき第一歩は、Hello World!プログラムのWindows版です。

Hello World!プログラムというのは、画面上にHello World!という文字列を表示するだけのシンプルなプログラムで、 初めてのプログラムの題材としてよく使われます。

Windowsプログラムの入門本の中には、ウインドウ内にHello Worldという文字列を出力させるプログラムを作っといて、 「WindowsプログラミングではHello Worldと表示するだけでも大変なんです」などとボケをかましている本が存在しますが、 はっきり言ってそれは邪道です。

文字列を表示するだけならばメッセージボックスを使えば済むものを、 わざわざウインドウを作っといて「大変だ大変だ」ってアンタそりゃないでしょうが!

近いうちに紹介しますがウインドウの作成は確かに面倒です。 ただ今回は本質を取り違えないようにWindowsプログラムで文字列を表示するということのみに焦点をあててみましょう。


まずは次のプログラムをご覧ください。

#include <windows.h>

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
		   LPSTR lpCmdLine, int nCmdShow)
{
	MessageBox(NULL, "Hello World!", "メッセージ", MB_OK);

	return 0;
}

比較するために、コンソールプログラム版Hello Worldアプリケーションも見てみましょう。 ちなみにコンソール版ってのは、コマンドプロンプト上で実行するプログラムのことね!

#include <stdio.h>

int main(int, char**)
{
	printf("hello world\n");

	return 0;
}

順々に見ていきましょう。まずインクルードされているヘッダファイルが、コンソールプログラムではstdio.hですが、 Windowsプログラムではwindows.hになっています。

windows.hの中にはWindowsプログラムで通常必要となるヘッダファイルなどが記述されています。 そのためWindowsプログラムを作成する際はまずwindows.hをインクルードする必要があります。


次にエントリーポイントの違いをみていきましょう。

コンソールプログラムではmainとなっていますがWindowsプログラムではWinMainとなっています。

WinMainに関しては”ウインドウの作成”のところで詳しく話しますので、 今はWindowsプログラミングでの”お約束”とでも思っておいてください。


最後にMessageBox関数について見ていきましょう。

Windowsプログラムではprintfやscanfといった関数は使うことが出来ません。 その代わりWindowsプログラムではMessageBox関数を用いることによって、文字列を手っ取り早く表示することが出来ます。

MSDNではMessageBoxは次のように記述されています。

int MessageBox(
  	HWND hWnd,           // handle of owner window
  	LPCTSTR lpText,      // address of text in message box
  	LPCTSTR lpCaption,   // address of title of message box
  	UINT uType           // style of message box
);

これに対し今回のプログラムでは次のようにパラメータを設定しています。

MessageBox(NULL, "Hello World!", "メッセージ", MB_OK);

それでは詳しく見ていきましょう。

hWndには親ウインドウのハンドルを指定します。しかし今回は親ウインドウがありませんのでNULLを指定しています。

lpTextには「メッセージボックスで表示する文字列」を指定します。
lpCaptionには「キャプションバーに表示する文字列」を指定します。
uTypeには「メッセージボックスに表示するボタンの種類」を指定します。

MB_OKと指定するとメッセージボックスにOKボタンが表示されます。
OKボタンが押された場合MessageBox関数はIDOKを戻り値として返します。


MB_OK以外にもMB_YESNOなど様々な定数がありますが、その他は気が向いたときにでも書いておきます。


ウィンドウの作成


今回はウィンドウの作成の話です。前回はメッセージボックスを用いて文字列を表示しましたが、 画像を表示したり、ゲームを作ったりといった用途にはメッセージボックスだけでは役不足です。

メッセージボックスだけで作ったゲームなんて聞いただけで泣けてきます(ToT)

ウィンドウを表示するためのコードを本格的に理解しようとすると結構骨が折れますが、 そこそこの理解でもある程度は使いこなせるでしょう。

以下にWindowsプログラムの雛形を用意しました。 これから様々なプログラムを作成する際に、使い回せば手間が省けるってもんです。

#include <windows.h>

//----------------------------------------------
// グローバル変数の宣言
//----------------------------------------------
HINSTANCE hInst;
char szTitle[] = "WindowSample";
char szWindowClass[] = "SampleClass";

//----------------------------------------------
// 関数のプロトタイプ宣言
//----------------------------------------------
ATOM MyRegisterClass( HINSTANCE hInstance );
BOOL InitInstance( HINSTANCE, int );
LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );

//----------------------------------------------
// Windowsのメイン関数(プログラムのエントリポイント)
//----------------------------------------------
int APIENTRY WinMain( HINSTANCE hInstance, 
		    HINSTANCE hPrevInstance,
		    LPSTR lpCmdLine,
		    int nCmdShow )
{
	MSG msg;

	MyRegisterClass( hInstance );

	if( !InitInstance( hInstance, nCmdShow ) ) 
	{
		return FALSE;
	}

	// メインメッセージループ:
	while( GetMessage(&msg, NULL, 0, 0) ) 
	{
		TranslateMessage( &msg );
		DispatchMessage( &msg );
	}

	return msg.wParam;
}

//----------------------------------------------
// ウィンドウクラスの登録
//----------------------------------------------
ATOM MyRegisterClass( HINSTANCE hInstance )
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX); 

	wcex.style		= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc		= (WNDPROC)WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon		= NULL;
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= NULL;
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= NULL;

	return RegisterClassEx( &wcex );
}

//----------------------------------------------
// ウィンドウの作成と表示
//----------------------------------------------
BOOL InitInstance( HINSTANCE hInstance, int nCmdShow )
{
	HWND hWnd;

	hInst = hInstance;  // グローバル変数にインスタンス ハンドルを保存します

	hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 
			  CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

	if( !hWnd ) 
	{
		return FALSE;
	}

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

	return TRUE;
}

//----------------------------------------------
// ウィンドウプロシージャ
//----------------------------------------------
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch( message ) 
	{
		case WM_COMMAND:
			break;
		case WM_PAINT:
			break;
		case WM_DESTROY:
			PostQuitMessage( 0 );
			break;
		default:
			break;
	}

	return DefWindowProc( hWnd, message, wParam, lParam );
}

詳しくは次回以降解説していきますが、ここでも大雑把にみていきましょう。

大まかな処理の流れは以下のようになります。

ウィンドウクラスの登録 → ウィンドウの作成と表示 → メッセージの取得と処理

メッセージの処理には、それ専用の関数を用意します。 それがウィンドウプロシージャです。 エントリーポイントで取得されたメッセージは、ウィンドウプロシージャに送ることによってはじめてメッセージに対応する 処理を行うことができるのです。

つまりWindowsプログラミングとは、《"メッセージに対応する処理"を記述することによってアプリケーションを組上げるもの》 と考えて良いでしょう。


アプリ間データの送受信


今回はアプリケーション間でデータをやりとりするにはどうすればいいかについてお話します。 まず結論から言ってしまいますと、WM_COPYDATAメッセージを使えばいいのです。 ではWM_COPYDATAとは一体どのようなメッセージなのでしょうか?MSDNライブラリには次のように書かれています。


The WM_COPYDATA message is sent when an application passes data to another application.
(意訳:あるアプリケーションから他のアプリケーションにデータを渡すときに送られるメッセージ)


送られるメッセージと訳しましたが、実際には「送るメッセージ」と訳した方が正しいかもしれません。 といいますのも、WM_COPYDATAはアプリ間でデータのやりとりを行う際、自動的に送信先のアプリケーションに メッセージが送られる訳ではなく、SendMessage関数を使って明示的に送ってやる必要があるのです。

では早速プログラムを見ていきましょう。まずはデータを送る側から。

void SendApp::SendData()
{
	COPYDATASTRUCT cd;
	HWND hwnd;
	char buffer[500];
	
	strcpy(buffer, "Hello World!");

	cd.dwData = 0;
	cd.cbData = strlen(buffer)+1;
	cd.lpData = buffer;

	hWnd = FindWindow("ReceiveApp", NULL);//NULLのとこはウィンドウ名を入力

	SendMessage(hWnd, WM_COPYDATA, (WPARAM)NULL, (LPARAM)&cd);
}

次にデータを受ける側のプログラムです。

bool ReceiveApp::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCD)
{
    MessageBox((char *)pCD->lpData);
    return true;
}

それではプログラムの説明をしていきましょう。

アプリケーション間でやりとりするデータはCOPYDATASTRUCT構造体を介して処理します。 これはファイルを扱うときにFILE構造体を使うのと同じようなもんです。一種の約束事と思っても問題無いでしょう。 で、そのCOPYDATASTRUCT構造体は次のようになっています。

typedef struct tagCOPYDATASTRUCT {
    DWORD dwData;//何コレ?・・・とりあえず0でよし!
    DWORD cbData;//データのサイズ
    PVOID lpData;//データへのポインタ
} COPYDATASTRUCT;

どーも dwData はデータのフォーマットをいじくるためのパラメータらしいのですが、よく分かりません。 少なくとも文字列を扱う分には0と設定しておけば問題ナシのはずです。(誰か詳しく教えてくれ〜〜)

残りの2つは難しいことはありませんね。コメントの通りです。


次にFindWindow関数について説明します。 FindWindow関数はデータの送り先のウィンドウハンドルを取得するための関数です。

第1引数に”(データ送り先の)ウィンドウのクラス名”を、

第2引数に”(データ送り先の)ウィンドウ名”を設定します。

これらの引数にNULLを設定した場合、それぞれ全てのクラス名、全てのウィンドウ名を指すことになります。

では次にSendMessage関数についてですが・・・各自で調べてね(←書くの疲れた、続きはまた気が向いたときってことで・・・)

今回の話を書くにあたって参考にしたページ。
Codesoup dotcom サイトが潰れてた・・・(2010/03/13)


アプリ間データの送受信part2


タイトルは"2"となっていますが、正直この話を先にすべきでした。

今回の話は前回に比べると全然簡単です。今回の話こそがアプリ間データの送受信の基礎であり、前回の話はその次の段階にあたります。 前回のあの中途半端な説明の終わり方といい、どうやら書き直した方が・・・ま、いっか。


今回は「単純なアプリケーション間のデータ送受信方法」と「SendMessage関数」の説明をします。

まずはメッセージ送信側のプログラムからですが、C++が分らない人のためにC言語風に前回のプログラムを書き直して みましょう。大して違いはありませんが・・・

void SendData()//←変わったのココだけ!
{
	COPYDATASTRUCT cd;
	HWND hWnd;
	char buffer[500];
	
	strcpy(buffer, "Hello World!");

	cd.dwData = 0;
	cd.cbData = strlen(buffer)+1;
	cd.lpData = buffer;

	hWnd = FindWindow("ReceiveApp", NULL);

	SendMessage(hWnd, WM_COPYDATA, (WPARAM)NULL, (LPARAM)&cd);
}

SendMessage関数ですが、これはその名の通りメッセージを送るための関数です。 パラメータは4つあり、左から(受信側のウィンドウハンドル, メッセージ, wParamパラメータ, lParamパラメータ)となっています。

wParamパラメータとlParamパラメータはメッセージが何であるかによって意味が変わります。

WM_COPYDATAメッセージの場合は次のような意味になります。


wParam = (WPARAM) (HWND) hwnd;            // handle of sending window 
lParam = (LPARAM) (PCOPYDATASTRUCT) pcds; // pointer to structure with data 

上のプログラムでは送信側のウィンドウハンドルはNULLになっています。 必要でしたらwParamに送信側ウィンドウハンドルを指定するとよいでしょう。 今回程度のプログラムでは特に必要ありません。lParamにはCOPYDATASTRUCT構造体へのポインタを指定してあります。


次にデータを受ける側のプログラムですが、単純な方法としてはWM_COPYDATAメッセージを ウィンドウプロシージャでつかまえてやればいいのです。

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
        case WM_COPYDATA://↓カッコ内が処理内容です。
            {
		COPYDATASTRUCT *pCD = (COPYDATASTRUCT*)lParam;
		MessageBox(hWnd,(LPCTSTR)pCD->lpData,"",MB_OK);
            }
            break;
        case WM_DESTROY://ウィンドウ破棄メッセージ
            PostQuitMessage(0);
            break;
        default:
            return(DefWindowProc(hWnd, msg, wParam, lParam));
    }
    return (0);
}

処理自体は非常に簡単ですね。送信側のプログラムがSendMessage関数を実行すればWM_COPYDATAメッセージが送られてきますので、 そのメッセージを受信側のウィンドウプロシージャでつかまえて処理すればよいのです。

その処理内容はCOPYDATASTRUCT構造体に格納されている文字列をメッセージボックスで表示するというシンプルなものです。 COPYDATASTRUCT構造体のアドレスはlParamパラメータに格納されていますから、 後はそれを利用して上のソースのように記述すればOKです。

説明は以上で終わりですが、どのようにプログラムが実行されるのか手っ取り早く試したい方は以下のファイルをダウンロードしてください。


Programダウンロード(←ソースと実行ファイルの詰め合わせ)


Send.exeが送信側で、Receive.exeが受信側です。

手順はReceive.exeを最初に実行しておき、その後でSend.exeを実行してください。
Send.exeの実行と同時にReceive.exeからメッセージボックスが表示されるはずです。


画像の表示1


今回はビットマップの表示方法について説明します。

画像の読み込みには、ファイルから読み込む方法とリソースから読み込む方法の2通りがありますが、 前者の方法での説明をすることにしましょう。


画像ファイルとは、簡単に言ってしまうと拡張子が.bmpとか.jpgとか.pngとかなってるヤツのことです。 その中で今回解説するのは拡張子が.bmpとなっているファイル限定のお話です。

何故.bmpファイル限定かと申しますと、JPEG形式やPNG形式などのファイルは圧縮が掛かっているため、 表示するためにデコード(解凍)処理が必要になり話しが少々ややこしくなってしまうためです。

第1回目からしてややこしい話をするなど、話しをする方も聞く方も迷惑な事この上ありません。 という訳で、当講座は当分の間はお手軽お気楽な話しか致しませんがどうぞよろしく(^^)


では早速本題に入りましょう。第一に知るべき関数(API)は以下の3つです。


 □画像ファイルの読み込み
 LoadImage関数

 □画像の表示
 BitBlt関数

 □メモリの解放
 DeleteObject関数


LoadImage( )で読み込んでBitBlt( )で表示する。

そして、画像を読み込みっぱなしではメモリの無駄使いなので、 使い終わったらDeleteObject( )でメモリの解放をしてやる。・・・これが基本です。


しかし、グラフィックスはこの3つの関数を理解するだけで表示できてしまうほど簡単な話ではありません。

グラフィックス(画像・文字を含む)を操るためには、デバイスコンテキストの理解が必用です。 ですが、ここでデバイスコンテキストについて詳しく説明してしまうと話が長くなってしまいますので、 簡単な例え話でとりあえず納得してください。


「なんちゃってデバイスコンテキスト講座」

例えば絵を書くとき、筆があったとしても画用紙がなければ絵は描けません。同様に写真を撮るとき、 カメラがあってもフィルムがなければ写真を撮ることはできませんね。

これと同様の話で、画像ファイルもデバイスコンテキスト(DC)がなければディスプレイ上に画像を 表示する事はできないのです。つまり、デバイスコンテキストとは画用紙やフィルムに相当する ものなのです。

では、そのデバイスコンテキストってどこにあるの?話になりますが、これは専用の関数を使って 自分で取得してこなければなりなせん。これは画用紙やフィルムを購入する行為に相当します。

また、デバイスコンテキストは取得して"そのままほったらかし"状態であってはいけません。 普通、使い終わったフィルムをいつまでもカメラの中に入れっぱなしにしたりはしないでしょ? それと同様に取得したデバイスコンテキストは使用後に必ず解放しなければなりません。



とりあえずデバイスコンテキストについて概要は理解してもらえたものとして具体的な話に入りましょう。
デバイスコンテキストの取得と解放は以下の関数で行います。


 □デバイスコンテキストの取得
 GetDC関数

 □デバイスコンテキストの解放
 ReleaseDC関数


GetDC( )で取得したデバイスコンテキストを元に画像の表示処理を行うことになります。 そして使い終わったらReleaseDC( )で開放することになります。ゲームプログラミングの場合、 DCの開放はゲーム終了時になりますね。

以上を考慮した上でこれまでの関数の流れをまとめると


 1.GetDC関数
   ↓
 2.LoadImage関数
   ↓
 3.BitBlt関数
   ↓
 4.DeleteObject関数
   ↓
 5.ReleaseDC関数

 (まずはデバイスコンテキストを取得し)
 
 (画像ファイルを読み込み)
 
 (画像を表示し)
 
 (LoadImageで使用したメモリを解放し)
 
 (GetDCで取得したデバイスコンテキストを解放する)



これでようやく話は終了か?というと全然終了じゃありません。まだ画像表示に必要な関数が 出揃ってくれていないのです。実はあと3つほど必要な関数があるのです。(~o~)

ですが、とりあえずここで一旦ブレイクタイム。

今回登場した関数を以下にまとめておきましたので確認してみてください。(次回解説する関数も入ってます)

(お手軽画像表示のための関数群)



画像の表示2


前回につづき画像表示の方法をみていきましょう。

実は今回登場する2つの関数を理解するカギはBitBlt関数にあります。

前回の説明ではBitBlt関数を「画像を表示するための関数」としていますが、 本当は「画像を転送するための関数」と表現する方が正確なのです。

ただし、「手抜きなBMP表示方法」においては、画像転送処理自体が実質的に表示処理を意味しますので、 前回そのように説明した訳です。

そこで今回はBitBlt関数を使ってどのように画像を表示するのかを理解することにしましょう。


まずはココ↓よりBitBlt関数の部分を見てください。
(お手軽画像表示のための関数群)


引数部分にコピー先コピー元の2つのデバイスコンテキストハンドル(以下DCハンドルと略します)を指定するようになっていることが 確認できると思います。結論から言いますと、コピー先にはGetDC関数で取得したDCハンドルを、コピー元には LoadImage関数で取得したDCハンドルをそれぞれ指定する事になります。



GetDC()で取得されるDCハンドルというのは "ウィンドウのクライアント領域に対するDCハンドル" ですので、 このDCに転送された画像データは即時にクライアント領域に反映されることになります。つまり、モニタ上に 画像が表示されるって訳です。

LoadImage関数で取得されるDCハンドルというのは、ファイル上からデータを読みこむために一時的に用意されたメモリに対するDCハンドルです。 このDCのことを一般的にメモリデバイスコンテキスト(メモリDC)と呼びます。

つまり話をまとめると、「メモリDC上の画像データ」をBitBlt関数で「クライアント領域に対するDC」 に転送することにより画像が画面上に表示されるって訳です。



さて、ここまで説明した段階でいまだに残り3つの関数が登場してないのはどういうことでしょう?

一見すると以上の知識だけで画像表示は十分可能なように思われる方も多々おられるでしょう。 現に上記の説明が本当であるならば、画像表示は可能です。 つまり、上記の説明にはウソが含まれているということです。

※別に引っかけ問題を作ろうってんじゃないよ。理解を容易にするための工夫ってやつです。


既にお気づきの方もおられると思いますが、LoadImage関数の戻り値はDCハンドルではありません。

「ロードされたイメージのハンドル」・・・即ちBMPデータに対するハンドルなのです。 ここで困った問題が生じます。BitBlt関数にはDCハンドルを指定しなければならないはずが、 LoadImage関数で用意されるのは「イメージのハンドル」のみ。困りました(-_-;)


ここで始めて、メモリDCを作成するための関数メモリDCとBMPを結びつけるための関数が必要になる訳です。 さらに「メモリDCを削除するための関数」も必要になります。以下がそのための関数です。


 □メモリデバイスコンテキストの作成
 CreateCompatibleDC関数

 □DCに対するオブジェクトの関連付け
 SelectObject関数

 □メモリDCの削除
 DeleteDC関数


CreateCompatibleDC( )は指定されたデバイスと互換性のあるDCを作成します。 この場合、互換性を持つべき対象はGetDC()で取得されたDCということになりますので、引数にはそれを指定しましょう。

次に、CreateCompatibleDC( )で作成したメモリDCとLoadImage( )で取得したBMP(オブジェクト)を結び付けねばなりません。 この作業をSelectObject( )で行います。 その後、BMPに関連付けられたメモリDCをBitBlt( )のコピー元に指定してやれば画像表示の完了です。

画像データを転送してしまえばメモリDCは不要になりますから、DeleteDC( )で削除してください。


では以下に全体の画像表示の流れをまとめてみましょう!


 1.GetDC関数
   ↓
 2.LoadImage関数
   ↓
 3.CreateCompatibleDC関数
   ↓
 4.SelectObject関数
   ↓
 5.BitBlt関数
   ↓
 6.DeleteDC関数
   ↓
 7.DeleteObject関数
   ↓
 8.ReleaseDC関数

 (まずはクライアント領域に対するDCを取得し)
 
 (画像ファイルを読み込み)
 
 (メモリDCを作成し)
 
 (メモリDCとBMPを関連づけ)
 
 (メモリDCからクライアント領域に対するDCに画像データを転送し:画像表示)
 
 (CreateCompatibleDCで作成したメモリDCを削除し)
 
 (LoadImageで使用したメモリを解放し)
 
 (GetDCで取得したDCを解放する)



以上でようやく画像表示の説明終了です。お疲れ様でした。

プログラム例を用意しましたので参考にしてください。

(簡易画像表示プログラムのソース)






Windowsプログラミング入門Topへ