パソコン活用研究シリコンバレー(C、C++、の活用研究)

文字列の描画1



=============================================================================
今回は画面に文字を表示させてみます。
=============================================================================

CUIへの文字列の表示がC言語なら
printf("Hello");
の1行で出来たのに比べるとWindowsのGUIでの文字列の描画はかなり面倒な手数がかかります。

Windowsの描画のためにGDI関数が用意されています。  
GDI(Graphics devaice interface)

文字列の描画にはTextOutというGDI関数を使います。
BOOL TextOut(HDC hdc, int x, int y, LPCSTR lpstring, int cbstring);

TextOut関数は第1引数に デバイスコンテキストのハンドル、
第2引数と第3引数に文字列を描画するx,y座標
第4引数に描画する文字列
第5引数に描画する文字列のバイト数
を設定します。

デバイスコンテキストについては後で説明しますが、描画を行うのに必要な情報を格納したデータベースであり、
描画を行うにあたりまずこのデバイスコンテキストへのハンドルを取得する必要があります。

デバイスコンテキストの取得に使う関数はいくつかありますが、まずBeginPaint, GetDCなどの関数があります。
描画が終了した場合は、デバイスコンテキストのハンドルを解放してやる必要があります。
解放にはEndPaint、ReleaseDCなどの関数を使います。

今回はマウスを左クリックしたら文字を表示するコードを、WM_LBUTTONDOWN ハンドラー(マウスの左クリック時
の処理)に記述していますが、Windowsのお作法的には描画のコードはWM_PAINTハンドラーに記述することと
なっています。
WM_PAINTハンドラーへの記述は文字列の描画2をご参照下さい。

1.文字を表示するプログラム


画面をクリックしたら文字を表示するプログラムを作ってみます。

マウスをクリックした位置に"X"を表示します。
※マウスのクリックについてはマウス入力1をご参照下さい。

(画像1)


デバイスコンテキストを取得するのにGetDC()関数を使います。
HDC GetDC(HWND hWnd);

引数hWndにはデバイスコンテキストを取得するウィンドウのハンドルを指定します。
成功すると、ウィンドウのクライアントエリアのデバイスコンテキストが返り、失敗するとNULLが返ります。

デバイスコンテキストのハンドルを取得した場合、描画の作業が完了したらこれを解放する必要があります。
GetDC()で取得したウキンドウのデバイスコンテキストはReleaseDC()で解放します。

int ReleaseDC(HWND hWnd, HDC hDC);

第1引数hWndにはデバイスコンテキストに対応するウィンドウハンドルを、
第2引数hDCには」解放するデバイスコンテキストのハンドルを指定します。
戻り値には、解放が成功したときは1、失敗したときは0が返ります。


コードは以下の通り
ウインドウプロシージャの中で以下の変数を宣言します。
HDC hdc;
char*txt="X";
static int x=0;
static int y=0;

hdcはデバイスコンテキストのハンドルを格納します。
x,yはマウスのクリックされた位置(座標)を保存します。

WM_LBUTTONDOWN ハンドラーの中で左ボタンが押下されたときの処理を書きます。
マウスのクリックされた位置(座標)をx,yに入れます。
x=LOWORD(lParam);
y=HIWORD(lParam);

"X"を表示します。
hdc=GetDC(hWnd);
TextOut(hdc, x,y,txt,strlen(txt));
ReleaseDC(hWnd,hdc);

<Cursor2e.cpp>

#include "windows.h"

// 関数のプロトタイプ宣言
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

// エントリポイント
int APIENTRY WinMain(HINSTANCE hInstance,
                        HINSTANCE hPrevInstance,
                        LPSTR lpCmdLine,
                        int nCmdShow )
{
        WNDCLASSEX wcex;        // ウインドウクラス構造体
        HWND hWnd;              // ウインドウハンドル
        MSG msg;                // メッセージ構造体

        // ウィンドウクラス構造体を設定します。
        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 = LoadIcon(NULL, IDI_APPLICATION);
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = "ModelApp";
        wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
        
        // ウインドウクラスを登録します。
        RegisterClassEx(&wcex);

        // ウインドウを作成します。
        hWnd = CreateWindow(wcex.lpszClassName, // ウインドウクラス名
                "Cursor2e",             // キャプション文字列
                WS_OVERLAPPEDWINDOW,            // ウインドウのスタイル
                CW_USEDEFAULT,                  // 水平位置
                CW_USEDEFAULT,                  // 垂直位置
                CW_USEDEFAULT,                  // 幅
                CW_USEDEFAULT,                  // 高さ
                NULL,                                   // 親ウインドウ
                NULL,                                   // ウインドウメニュー
                hInstance,                              // インスタンスハンドル
                NULL);                                  // WM_CREATE情報

                // ウインドウを表示します。
                ShowWindow(hWnd, nCmdShow);
                UpdateWindow(hWnd);

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

        // 戻り値を返します。
        return msg.wParam;
}

// ウインドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd,
                               UINT message,
                               WPARAM wParam,
                               LPARAM lParam)
{
        HDC hdc;
        char*txt="X";
        static int x=0;
        static int y=0;
        
        // メッセージの種類に応じて処理を分岐します。
        switch (message) 
        {
      HCURSOR hcur;
        case WM_LBUTTONDOWN:
        //        ShowCursor(false);

                x=LOWORD(lParam);
                y=HIWORD(lParam);
       hdc=GetDC(hWnd);
// クリックすると描画、画面サイズ変更など無効領域が発生すると画面が消去され、新たにクリックした位置に表示
                TextOut(hdc, x,y,txt,strlen(txt));
                ReleaseDC(hWnd,hdc);
       return 0;
                
                
                case WM_DESTROY:
                        // ウインドウが破棄されたときの処理
                        PostQuitMessage(0);
                        return 0;
                default:
                        // デフォルトの処理
                        return DefWindowProc(hWnd, message, wParam, lParam);
        }
}



2.無効領域と再描画


ひとつ留意すべき点は、上記のプログラム<Cursor2e.cpp>は文字の描画をマウスの左ボタンをクリックした時に
行っているため、このプログラムで描画した"X"は画面の最小化、最大化、画面サイズの変更があるとすべて
消えてしまう点です。(試した環境はWindows11)
その他、Windowsのバージョンやウィンドウスタイルの設定の違いによる挙動の違いなどがあり、なかなか厄介です。

ここでは、無効領域と再描画について、いろいろなコードを試しながら、この厄介な課題と対応策を探ってみます。
これらのテストを通して、画面に文字を表示するだけでもWindowsプログラムの奥の深さ(厄介さ)やお作法が
見えてくると思います。

(1)無効領域と再描画


画面サイズを変更した時 画面表示はクリアされてしまいます。
(画像2)



画面サイズの変更があると無効領域(更新領域、無効リージョン、更新リージョンと書かれている場合もあります)が
発生します。無効領域という用語が理解しにくいのですが、要は再描画する必要のある領域があるということです。
無効領域が発生するとシステム(Windows)からWM_PAINTメッセージが送信されます。

(画像3)


なお、他の画面に隠れた時の挙動はWindows11とWindowsXPとで異なります。
Windowsのバージョンによって、無効領域が発生したときの挙動に差があるようです。

WindowsXPでは、他の画面に隠れた時にも、隠れた部分は消えてしまいます。
(Windows95,98,MEでも同様と思われます)
※Windows11では他の画面に隠れていた部分が消えることはありません。

●WindowsXPでの挙動

最初の状態
(画像4)

メモ帳で画面の一部が隠された
(画像5)


メモ帳をどかすと、隠れていた部分は消えている
(画像6)



(2)  WM_PAINTハンドラにreturn 0;だけを記述

再描画はウインドウプロシージャにWM_PAINTに対する処理を記述していれば、その記述に従い、
また特にWM_PAINTハンドラーを記述していなければデフォルトのDefWindowProc()によって処理されます。
最初ののプログラム<Cursor2e.cpp>にはWM_PAINTハンドラーを記述していないので、再描画のメッセージは
DefWindowProc()によって処理されていました。

そこで、WM_PAINTハンドラに
return 0;
だけを記述した何もしないWM_PAINTハンドラを作ってみたプログラムの挙動を試してみます。

ウィンドウの最小化、最大化、サイズ変更があると画面はクリアされるのは上記のCursor2e.cppの時と同じですが、
画面が他の画面に隠れた時の挙動に差がでます。(試した環境はWindows11)


画面の一部に別のプログラムの画面が重なった
(画像7)


重なった画面を動かした後にマウスを左クリックすると前の画面はクリアされて
今マウスをクリックした場所に"X"が表示される。
※Cursor2fの画面がフォーカスを失った後に画面(クライアント領域だけではなく、システム領域も)
をクリックすると同様の挙動になる。
Cursor2e.cppのプログラムでは、画面が重なった後に重なった画面を移動した場合、前の画面はそのままで、
前の画面の描画に加えて新たに左クリックした場所に"X"が追加で描画されました。

(画像8)


推測ですが、TaskmanagerでCPU使用率を見るとアプリ起動後から10%程度を占拠しっぱなしなので
このプログラムではWM_PAINTメッセージが無限に呼ばれていっる状態になっていると思われます。
WM_PAINTメッセージハンドラでは何もしないでreturnしているので、無効領域が存在し続けてWM_PAINTが
呼ばれ続けている状態と思われます。
cursor2e.cppや後述するcursor2g.cppのプログラムではこのような高いCPU使用率は見られないので、
DefWindowProc()に処理を任せた場合や、cursor2g.cppのようなWM_PAINTメッセージハンドラの記述の仕方
をすれば正常描画な処理が行われているということになると思います。
つまり、WM_PAINTメッセージハンドラに return 0; だけを記述するというのはあまりよろしくないコード
(処理の仕方)で、ちゃんと無効領域を認識して処理する(無効領域をoffにする)コードを書かないといけない
ということです。(そのコードは後述のCursor2g.cppを見て下さい)

ウィンドウプロシージャの部分のコードのみ記載します。

<Cursor2f.cpp>

// ウインドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd,
                               UINT message,
                               WPARAM wParam,
                               LPARAM lParam)
{
        HDC hdc;
        char*txt="X";
        static int x=0;
        static int y=0;
        
        // メッセージの種類に応じて処理を分岐します。
        switch (message) 
        {
      HCURSOR hcur;
        case WM_LBUTTONDOWN:
        //        ShowCursor(false);

                x=LOWORD(lParam);
                y=HIWORD(lParam);
       hdc=GetDC(hWnd);
// クリックすると描画、画面サイズ変更など無効領域が発生すると画面が消去され、新たにクリックした位置に表示
// 別の画面に隠れたあと、クリックするといったん前の表示は消える。
                TextOut(hdc, x,y,txt,strlen(txt));
                ReleaseDC(hWnd,hdc);
       return 0;

      case WM_PAINT:
        return 0;
                
                
                case WM_DESTROY:
                        // ウインドウが破棄されたときの処理
                        PostQuitMessage(0);
                        return 0;
                default:
                        // デフォルトの処理
                        return DefWindowProc(hWnd, message, wParam, lParam);
        }
}

(3) WM_PAINTメッセージハンドラの記述

更にWM_PAINTメッセージに対するハンドラの処理を以下のようにしてみると
基本的な挙動はCursor2e.cppと同じようになりました。

case WM_PAINT:
BeginPaint(hWnd,&ps);
EndPaint(hWnd,&ps);
return 0;

<Cursor2g.cpp>

// ウインドウプロシージャ
LRESULT CALLBACK WndProc(HWND hWnd,
                               UINT message,
                               WPARAM wParam,
                               LPARAM lParam)
{
        PAINTSTRUCT ps;
        HDC hdc;
        char*txt="X";
        static int x=0;
        static int y=0;
        
        // メッセージの種類に応じて処理を分岐します。
        switch (message) 
        {
      HCURSOR hcur;
        case WM_LBUTTONDOWN:
        //        ShowCursor(false);

                x=LOWORD(lParam);
                y=HIWORD(lParam);
       hdc=GetDC(hWnd);
// クリックすると描画、画面サイズ変更など無効領域が発生すると画面が消去され、新たにクリックした位置に表示
// 別の画面に隠れたあと、クリックするといったん前の表示は消える。
                TextOut(hdc, x,y,txt,strlen(txt));
                ReleaseDC(hWnd,hdc);
       return 0;

      case WM_PAINT:
                BeginPaint(hWnd,&ps);
                EndPaint(hWnd,&ps);
        return 0;
                
                
                case WM_DESTROY:
                        // ウインドウが破棄されたときの処理
                        PostQuitMessage(0);
                        return 0;
                default:
                        // デフォルトの処理
                        return DefWindowProc(hWnd, message, wParam, lParam);
        }
}

上記のプログラムは全て、ウィンドウクラス構造体の設定でウィンドウのsタイルとして
ウィンドウのサイズが変更された時は再描画されるように設定していました。
wcex.style = CS_HREDRAW | CS_VREDRAW;

次はこの設定を
wcex.style = 0;
とし、ウィンドウサイズの変更で再描画しないようにしたプログラムにしてみます。
ウィンドウプロシージャの中はCuursor2e.cppと同じです。

ウインドウスタイルがこの設定の場合、画面サイズの変更で画面の表示がクリアされることは
ありませんが、画面を小さくしたときに消えてしまった部分は画面を再び大きくしても消えたままです。


最初の状態
(画像9)

画面を小さく変更
(画像10)

再び画面を大きくしてみる。消えた部分は消えたまま
(画像11)


コードはウィンドウクラス構造体の設定の個所だけ掲載しておきます。
他は基本的にcursor2e.cppと同じです。

<cursor2h.cpp>

// ウィンドウクラス構造体を設定します。
        wcex.cbSize = sizeof(WNDCLASSEX); 
        wcex.style = 0;
        wcex.lpfnWndProc = (WNDPROC)WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = 0;
        wcex.hInstance = hInstance;
        wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
        wcex.lpszMenuName = NULL;
        wcex.lpszClassName = "ModelApp";
        wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
        


(4)BeginPaint()でデバイスコンテキストへのハンドルの取得
 
Cursor2f.cppのプログラムではデバイスコンテキストへのハンドルの取得はGetDC()で行っていましたが、
これをBeginPaint()に変えたプログラムCursor2d.cppを試してみます。WM_PAINTメッセージハンドラは記述しません。

Cursor2f.cppでは、マウスを左クリックするたびに"X"が重ねて描画されましたが、Cursor2d.cppでは
"X"は最初の1つしか描画されず、それ以降はマウスを左クリックしても何も描画されません。
ここに、デバイスコンテキストへのハンドルを取得するのにGetDC()を使う場合とBeginPaint()を使う場合の
差がでます。



BeginPaint()はWM_Paintメセージの処理の中で使う関数で、無効領域がある場合、無効領域を有効化して
デバイスコンテキストへのハンドルを返すという動作をする関数です。無効領域がない場合は有効なデバイスコンテキストへの
ハンドルを返しません。

従って、1つめの"X"を描画した後に、無効領域が発生しない限りBeginPaint()では有効なデバイスコンテキストへのハンドルが
取得できず、2つ目以降の"X"は描画されない、という挙動になっていると推測されます。
画面サイズが変更されて無効領域が発生すると、再び1つだけ新たなクリック位置に"X"の描画がされます。

Cursor2d.cppのCPU使用率をTaskmanagerで見ると、アプリの起動後、Windowサイズの変更後他界はCPU使用率になっていますが
左クリックで描画後はCPU使用率はほぼ0%になります。
アプリの起動後、Windowサイズの変更後は無効領域が存在しWM_PAINTメッセージが呼ばれ続けているが、描画時にBeginPaint()が
呼ばれて無効領域が有効化されてWM_PAINTメセッージが発生しなくなったものと考えます。

Cursor2d.cppのウィンドウプロシージャの部分のみ掲載します。

LRESULT CALLBACK WndProc(HWND hWnd,
                               UINT message,
                               WPARAM wParam,
                               LPARAM lParam)
{
        PAINTSTRUCT ps;
        char*txt="X";
        static int x=0;
        static int y=0;
        
        // メッセージの種類に応じて処理を分岐します。
        switch (message) 
        {
      HCURSOR hcur;
        case WM_LBUTTONDOWN:
        //        ShowCursor(false);

                x=LOWORD(lParam);
                y=HIWORD(lParam);

                BeginPaint(hWnd,&ps); 
                TextOut(ps.hdc, x,y,txt,strlen(txt));
                EndPaint(hWnd,&ps);
       return 0;

     case WM_PAINT:
        return 0;
                
                
                case WM_DESTROY:
                        // ウインドウが破棄されたときの処理
                        PostQuitMessage(0);
                        return 0;
                default:
                        // デフォルトの処理
                        return DefWindowProc(hWnd, message, wParam, lParam);
        }
}



今回見たようにマウスのクリックで描画するような場合は、画面サイズの変更で画面が消えてしまうというようなこともあるので、
そのようなことを防ぐには、WM_PAINTメッセージの処理の中で描画するというのが基本的な方法になります。
次回(文字列の表示2)では、WM_PAINTメッセージでの描画処理について説明します。

Visual Basicに関してはFORMのイベントその1にVBの描画時のイベントと隠れた画面の再描画などの説明がありますので
ご参照下さい。


 


TopPage