さて、このシリーズのトップはC++による独自塗潰し関数の紹介から始めましょう。

ご紹介するのは、20年以上前にツールバービットマップ作成、編集ツール(TBEditor.exe)を作った際に参考にさせていただいたFuzzy氏(注)の塗潰し関数のコードをアルゴリズムをそのままに書き直してみたものです。コメントを多く描いているので、詳細末尾にコードを載せますのではそちらを見てください。

注:webでFuzzy氏を探してみたのですが、もう何も引っ掛かりませんでした。残念です。

 

描画アルゴリズムは、

(1)初期設定として、描画始点(x, y)のy座標上、描画画面の左右を描画始点の領域色と同じ色のピクセル走査して、(lx, y)-(rx, y)という横線の情報をキュー(LINFO構造体配列Linfo)の先頭(Linfo_Top)に入れます。

(2)次にループに入り、キューの先頭からLINFO情報を取得して描画色(paint_color)で横線を描きます。

(3)その後、始点y座標の上(lx, y - 1)-(rx, y - 1)、下(lx, y + 1)-(rx, y + 1)についてScanLine関数で走査して、同じ領域色の横線情報をキューの末尾(Linfo_Bottom)に追加し、

(4)キューの先頭を進め(++Linfo_Top)て上記(2)に戻り、描画、走査を繰り返します。

(5)Linfo配列のポインターであるLinfo_TopとLinfo_Bottomが配列外に出てしまうと(== &Linfo[MAXSIZE])、また先頭に戻り、処理を続けます。

(6)描画処理はキューに描画情報が残る限り続きます。(Linfo_Top != Linfo_Bottom)

というものです。

 

オリジナルのコードはSetPixel関数(末尾のサンプルコードでは所要時間2.4秒)を使っていたのですが、LineTo関数にかえることで若干スピードアップ(同所要時間1.5秒)が図れましたが、矢張り赤ブラシでのExtFloddFillと比べると速さが全く違います。

 

ということで、このアルゴリズムは「ン~」と評価し、また他をあたってみます。

 

【Paint関数(含むScanLine関数)】

//////////////////////////////////////////////
//    同一領域色の横線情報による塗潰し
//描画画面の設定はg_Width - 1, g_Heightで行う
//////////////////////////////////////////////

#define MAXSIZE    1024    //バッファサイズは1024
//塗潰しを行う画面の外延(変更可能)
RECT PaintLimit = {0, 0, g_Width - 1, g_Height - 1};
//塗潰し横線情報構造体
typedef struct {
    int Left_x;        
//領域左端のX座標
    int Right_x;    //領域右端のX座標
    int Present_y;    //領域のY座標
    int Original_y;    //元ラインのY座標
} LINFO, *PTLINFO;
//塗潰し情報配列(X軸上の塗潰し横線情報をY軸に沿ってキュー化)
LINFO Linfo[MAXSIZE];
//塗潰し情報配列の先頭、末尾ポインター
PTLINFO Linfo_Top, Linfo_Bottom;
//////////////////////////////////////
// 新規LINFO構造体にy座標の塗潰し横線
// 情報(lx <-- 領域色 --> rx)を記録/
/////////////////////////////////////

void ScanLine(HDC hDC, int lx, int rx, int y, int oy, COLORREF col) {

    while(lx <= rx) {
        
//非領域色を飛ばす
        while(lx < rx && GetPixel(hDC, lx, y) != col)
            lx++;
        
//領域色が同一の横線(複数可)をLINFOに記録してゆく
        if(GetPixel(hDC, lx, y) == col) {
            Linfo_Bottom->Left_x = lx;        
//新規LINFOの左端を記録
            //領域色を飛ばす
            while(lx <= rx && GetPixel(hDC, lx, y) == col)
                lx++;
            Linfo_Bottom->Right_x = lx - 1;    
//新規LINFOの右端を記録
            Linfo_Bottom->Present_y = y;    //現在のy座標を記録
            Linfo_Bottom->Original_y = oy;    //現在のy座標を記録
            if(++Linfo_Bottom == &Linfo[MAXSIZE])    //末尾を追加し、配列を超えたならば
                Linfo_Bottom = Linfo;                //先頭に戻す
        }
        
//非領域色がない(lx == rx)場合終了
        else
            break;
    }
}

////////////////////////////////////
//塗潰し横線情報配列をキューとして
//ScanLine関数で末尾に追加して行き、
//Paint関数で横線情報配列をキュー
//から取り出し、描画色で横線を引く
//ことにより、塗潰しを行う
////////////////////////////////////

void Paint(HDC hDC, int x, int y, COLORREF paint_color) {

  
 //局所変数
    int lx, rx, uy, dy, oy;
    int Left_Rend, Right_Lend;
    COLORREF col = GetPixel(hDC, x, y);    
//(x, y)の画素色を領域色として取得
    //初期化
    Linfo_Top = Linfo;            //LINFO配列(横走査情報))先頭のポインター
    Linfo_Bottom = Linfo + 1;    //LINFO配列(横走査情報))末尾のポインター
    Linfo_Top->Left_x = Linfo_Top->Right_x = x;            //xに初期化
    Linfo_Top->Present_y = Linfo_Top->Original_y = y;    //yに初期化(始点判別条件)
    //描画準備処理
    HPEN hPen = CreatePen(PS_SOLID, 1, paint_color);
    HPEN hOldPen = (HPEN)SelectObject(hDC, hPen);
  
 //描画ループ
    do {
        lx = Linfo_Top->Left_x;                
//描画横線左端のX座標
        rx = Linfo_Top->Right_x;            //描画横線右端のX座標
        uy = Linfo_Top->Present_y;            //描画横線のy座標(上に走査する)
        dy = Linfo_Top->Present_y;            //描画横線のy座標(下に走査する)
        oy = Linfo_Top->Original_y;            //描画横線の元y座標
        Left_Rend = lx - 1;                    //始点の左(左側走査の右端)
        Right_Lend = rx + 1;                //始点の右(右側走査の左端)
        if(++Linfo_Top == &Linfo[MAXSIZE])    //Linfo_Topを一つ進め、配列を超えたならば
            Linfo_Top = Linfo;                //先頭に戻す
        //領域色が描画色(処理済を含む)なら次のLINFO(塗りつぶし横線情報)にスキップする
        if(GetPixel(hDC, lx, uy) == paint_color)
            continue;
        
//右方向の境界を探す
        while(rx < PaintLimit.right && GetPixel(hDC, rx + 1, uy) == col)
            rx++;
      
 //左方向の境界を探す
        while(lx > PaintLimit.left && GetPixel(hDC, lx - 1, uy) == col)
            lx--;
      
 //lx - rxの線分を描画
        MoveToEx(hDC, lx, uy, NULL);
        LineTo(hDC, rx + 1, uy);        
//終点は描画されない為
        //for(int i = lx; i <= rx; i++)    //オリジナルはSetPixelによる処理
        //    SetPixel(hDC, i, uy, paint_color);

        //現在の左右端(lx, rx)を使い、
        if(--uy >= PaintLimit.top) {    //一段上のラインを
            //始点の横線の場合(初期化でuy == oy)
            if(uy == oy) {
                
//始点の左(Left_Rend)、始点の右(Right_Lend)左右別々に走査する
                ScanLine(hDC, lx, Left_Rend, uy, uy + 1, col);
                ScanLine(hDC, Right_Lend, rx, uy, uy + 1, col);
            }
            else {
                ScanLine(hDC, lx, rx, uy, uy + 1, col);
            }
        }
        if(++dy < PaintLimit.bottom) {    
//一段下のラインを
            //始点の横線の場合(初期化でuy == oy)
            if(dy == oy) {
                
//始点の左(Left_Rend)、始点の右(Right_Lend)左右別々に走査する
                ScanLine(hDC, lx, Left_Rend, dy, dy - 1, col);
                ScanLine(hDC, Right_Lend, rx, dy, dy - 1, col);
            }
            else {
                ScanLine(hDC, lx, rx, dy, dy - 1, col);
            }
        }
    } while(Linfo_Top != Linfo_Bottom);    
//未処理の横線情報がある限りループ
    //描画終了処理
    SelectObject(hDC, hOldPen);
    DeleteObject(hPen);
}

 

【テストプログラム-WM_PAINT部分のみ赤-定番コード緑-描画関係青-コメント、橙-GDIオブジェクト関連

case WM_PAINT:
    PAINTSTRUCT paint;
    HDC hDC = BeginPaint(hWnd, &paint);

    //ユーザー処理
    HPEN hPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 255));    //青のソリッドペン
    HPEN hOldPen = (HPEN)SelectObject(hDC, hPen);
    Rectangle(hDC, 10, 10, g_Width - 30, g_Height - 70);
    Rectangle(hDC, 100, 100, g_Width - 120, g_Height - 160);
    Rectangle(hDC, g_Width - 220, 120, g_Width - 140, g_Height - 180);
    Ellipse(hDC, 120, 80, 200, g_Height - 180);

    HBRUSH hBrush = CreateSolidBrush(RGB(255, 0, 0));    //赤のソリッドブラシ
    HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);
    if(g_PaintFlag) {    //メニューまたはマウスクリックでg_PaintFlagが真となるように設定している
        ExtFloodFill(hDC, 50, 50, GetPixel(hDC, 50, 50), FLOODFILLSURFACE);    //この描画が実に速い
        DWORD start = GetTickCount();       // スタート時間
        Paint(hDC, 110, 110, RGB(0, 255, 0));
        Paint(hDC, 150, 250, RGB(64, 128, 64));
        Paint(hDC, g_Width - 200, 130, RGB(128, 64, 64));
        Paint(hDC, 0, 0, RGB(0, 64, 64));

        DWORD end = GetTickCount();    // 終了時間
        char buff[MAX_PATH];
        wsprintf(buff, "Time elapsed is %d miliseconds.", (end - start));
        MessageBox(hWnd, buff, "CHeck time", MB_OK | MB_ICONINFORMATION);
    }
    
SelectObject(hDC, hOldBrush);
    DeleteObject(hBrush);
    SelectObject(hDC, hOldPen);
    DeleteObject(hPen);   
//DC、ペンやブラシなどを作ったら、必ず開放すること
    EndPaint(hWnd, &paint);    //BeginPaintで取得したhDCはReleaseDCで開放する必要はない。
    break;

 

 

 

実際独自のアルゴリズムは一つ思いつき、コードに実装し、テストしてみました。

(1)アルゴリズム

Easel.Canvas(Bitmap)の一点(x, y)のピクセルの色を調べ(Canvas.GetPixel())、↑~→~↓~←(~には「斜め」入れて0~7の方向を設定)の順に隣接するピクセルの色を調べ、同じ色のピクセルがあれば(x, y)に描画色の点を打って、o-7の方向を定め」、次に同じ色のピクセルで同様の処理を繰り返し(再帰)ます。これにより、(x, y)と同じ色の隣接するピクセルを次々に描画色で点を打ち、0-7方向全く打つ場所がなくなると元に戻ってやり残した処理を継続することになります。結構悪くないな、と考えましたが、処理は遅く、膨大なスタック(再帰処理の場合、処理画素数だけPush動作を繰り返すので)が必要になるだろうな、とは覚悟していました。

(2)コード

省略。↑の緑色を実装しました。

(3)テスト

矢張りスタックオーバーフローで落ちますね(汗;)。

 

これにめげずに、現在次のような手を打っています。

 

(1)Webに載っていた塗りつぶしアルゴリズムの2つの例(他にもあったが、ドン臭そうなのでヤメ)を理解し、自分なりに書き直して移植しようと考えています

(2)もう一つの(C#らしい)塗りつぶしのアルゴリズムを考え付いたので、それをC#で実装してみたいと思います。

(3)最後に「何故ExtFloodFillが正常に動作しないのか?」を別途追究してみたいと考えています。

 

ps. もう余り家庭内のアプリ開発のアイデアがないので、このような「探求活動」で(初期の目的である)脳みそを鍛えてみようと考えています。

 

ウ”~、分かりません。苦”るじい~。前回触れましたが、テストしてみるとEaselのPaintメソッドの動作が異常です。仕様はCANVASと同じく、指定位置の色が連続して続く領域を指定色で塗りつぶすはずなのですが、

(EZImage.exe)

テストしてみるとEasel.Canvas全体が塗りつぶされてしまいます。

なんで???どして???

 

The story is that ... 最初、webで色々と調べ、どうもC#にはBasicの"Paint"命令のような、「任意の色で、任意の位置から塗りつぶす」メソッドがないことが分かりました。(注)

注:Win32 APIにはExFloodFill(HDC, int, int, COLORREF, uint)という関数があり、私のEZImageではこれを使っています。(21年前のTBEditorではこのExFloodFillはおろか、FloodFill関数も知らず、Fuzzy氏のアルゴリズムをもとに独自コーディングしています。)

 

従って道は二つ、独自にCでデコーディングするか、Win32を使うか、です。前者はトライされた方も多いのですが、コードが結構長いので安きに流されて後者を選択し、C#でExFloodFillを使う例を調べました。以下が当初のEaselのコードの関連部分です。

 

using System.Runtime.InteropServices;    //ExtFloodFill関数を使用する為

        [DllImport("gdi32.dll")]
        static extern bool ExtFloodFill(IntPtr hdc, int nXStart, int nYStart, uint crColor, uint fuFillType);

        //指定色で塗潰す
        public bool Paint(int x, int y, Color col)
        {
            bool result = false;    //戻り値変数
            IntPtr hDC = gHandle.GetHdc();    //gHandleはEasel.Canvas(Imageクラスオブジェクト)のGraphicsです。
            IntPtr hBrush = CreateSolidBrush((uint)ColorTranslator.ToWin32(col));    //colで塗りつぶすブラシを作成
            IntPtr hOldBrush = SelectObject(hDC, hBrush);    //塗りつぶしブラシを選択し、既存ブラシをhOldBrushに保存
            //hDC内の(x, y)位置のピクセル色をGetPixelで取得し、その色が連続する領域を塗りつぶす

            result = ExtFloodFill(hDC, x, y, GetPixel(hDC, x, y), 1);    //"#define  FLOODFILLSURFACE  1" in wingdi.h
            SelectObject(hDC, hOldBrush);    //ブラシを既存のものに戻す
            DeleteObject(hBrush);    //作成した塗りつぶしブラシを開放する
            gHandle.ReleaseHdc(hDC);    //取得したDCハンドルを開放する
            return result;
        }

 

一見よさげなコードなんですが、結果は↑の通り。(青で矩形を描き、その中を赤で塗りつぶそうとしたので、本来であれば少なくとも青の矩形は残るはずなのだが、完全に塗りつぶされている=与えられたデバイスコンテキストが無地になっている?)

 

チェックは以下の要領で行いました。

 

(1)指定位置のピクセル色取得値はが正しいか?

これは背景色を黒(ARGB = 0)にして、gdiplus.dllのGetPixel(Win32 API)とC#の(uint)ColorTranslator.ToWin32(Canvas.GetPixel())でチェックしてみました。すると、

Win32では789261(0x0C0B0D)、C#(0x000000)となり、C#の方が正しいように思えます。(描画色はColor.Blueなので0x0000FF)

 

(2)デバイスコンテキストは正しいか?

EaselクラスインスタンスのImageクラスインスタンスであるCanvasから取得したグラフィックス(gHandle)と、ピクチャーボックス(picBox)のImageプロパティから取得したグラフィックスを取得し、それぞれのDC値をとって対比すると、

これは全く同じですね。

 

しかし、いくらいじっても結果は総て塗りつぶされる(GetPixel(Win32 API))か、全く描画しない((uint)ColorTranslator.ToWin32(Canvas.GetPixel()))か、のいずれかで、もういじるところがなくなってしまいました。

 

矢張りExFloodFillを諦めて、独自コードで塗りつぶしを行うべきなのでしょうかね?(面倒だけど。)

 

前回の報告で、

 

2.問題点

BCCSkelton(含ECCSkelton)の場合は、Win32 APIを簡単に使う為に作りましたが、C#では既に高級言語化されているのでCANVASをそのまま持ってくること自体がミスマッチなのかもしれません。どういうことかというと、Easelではブラシがソリッドブラシしか使えないという難点がありますが、C#では簡単にハッチブラシやグラデーションブラシ等が作れるので、メンバーをBrushに戻し、引数にブラシオブジェクトを与える仕様の方がよりC#的です。しかし、元々の発想が「昔のBasic程度の描画機能を再現したい」だったので、これをすると二律背反に陥ってしまいます。ウ~ン、ナヤミ、ナヤミ、ですね。(このことはC#の描画機能の多彩、多様性を生かしていない、という点でも同じです。Easelは単純な描画しかできませんが、簡単に描ける、というのが売りとなるはずなので。)

 

と書きましたが、最終的に次のような仕様にしてピリオッドを打ちます。

(1)描画用ブラシは参照するだけのprivate変数にします。

        private Brush gBrush;                        //描画用代表ブラシ(以下のブラシの代入用)
(2)実際に描画するブラシは以下の通り代表的な4つ(指定番号0~3)を設けました。

        private SolidBrush _gsbrush;                //描画用ソリッドブラシ-0
        private HatchBrush _ghbrush;                //描画用ハッチブラシ-1
        private LinearGradientBrush _glgbrush;        //描画用リニアグラディエントブラシ-2
        private TextureBrush _gtbrush;                //描画用テクスチュァブラシ-3

これらのBrushクラスから派生したブラシは変更の際にDisposeするので、カスタムプロパティにし、ブラシの切り替えはbool SelectBrush(int)というメソッドを設けました。(注)

注:選択された場合のみtrueとなり、初期状態で生成されていないブラシやブラシ指定そのものの誤りはfalseが返ります。

(3)コンストラクターで実際に生成する初期ブラシはSolidBrushのみとし、その他は必要に応じてユーザーが生成、代入するようにします。(あまり使わないものにメモリーを喰わせない。)

【例】

            //ハッチブラシを登録して選択
            easel.ghBrush = new HatchBrush(HatchStyle.Cross, Color.Yellow, Color.FromArgb(255, 64, 64, 255));
            if(easel.SelectBrush(1))    //0 - Solid, 1 - Hatch, 2 - LinearGradient, 3 - Texture
                MessageBox.Show(easel.ghBrush.ToString(), "選択ブラシ");
            //リニアグラディエントブラシを登録して選択
            easel.glgBrush = new LinearGradientBrush(new Point(350, 180), new Point(430, 260), Color.FromArgb(255, 255, 0, 0), Color.FromArgb(255, 0, 0, 255));
            if(easel.SelectBrush(2))    //0 - Solid, 1 - Hatch, 2 - LinearGradient, 3 - Texture
                MessageBox.Show(easel.glgBrush.ToString(), "選択ブラシ");
            //テクスチュァブラシを登録して選択
            easel.gtBrush = new TextureBrush((Bitmap)Image.FromFile(".\\Cat.bmp", true));
            if(easel.SelectBrush(3))    //0 - Solid, 1 - Hatch, 2 - LinearGradient, 3 - Texture
                MessageBox.Show(easel.gtBrush.ToString(), "選択ブラシ");

(4)前景色(gFtColor)を変更した際にそれが適用されるのはSolidBrushだけにしました。理由はハッチブラシのForegroundColorがR/Oになっているのと、その他のブラシに「一つの前景色」という概念がないためです。

 

まぁ、これくらいで我慢しときましょうか?一応動作テストをして正常に動いています。

赤のソリッドブラシ、縦横青黄色縞のハッチブラシ、赤青のリニアグラディエントブラシで正方形を描き、最後に「猫ちゃん」テキスチャーブラシで円と「うるせーぞ!」を描いています。(しかし、リニアグラディエントブラシとテキスチャーブラシはすごいな、と思う反面、私には無用かな、という感じもあります...汗;)

 

ps. 一難去ってまた一難。Win32 API(ExtFloodFill関数)を使ったPaintメソッドのテストをしたのですが、描画されません。今度はその原因探しです。

 

さて、Windowsだ、GUIだ、描画だ、と御託を並べていましたが、やっと何がしたいかが見えてきて「C#で使うBCCSkeltonのCANVASクラスみたいなもの」を試作しようということになったこと、ご高承の通りです。また、この"Easel"(注)クラスオブジェクトは基本PictureBoxで使う、ということで(メモリーリークの危険なども考え)使い方も一応はチェックしました。

注:前回の御託です。「描画する背景ビットマップを"Canvas"(画版)プロパティとし、それ(及び描画用のペン、ブラシ、フォントおよび画像取り込み用のイメージ)を有する基底クラスを"Easel"(画架)しましたので、独立DLLとさせたときの名前空間とDLLのファイル名を"Atelier"(アトリエ)にしたのは必然でしょう!

 

現在のEaselクラスは、正直BCCSkelotonのCANVASよりも劣り、GDI+をカバーしているC#のGraphicsクラスメソッドが全然生かされておりません。現状は以下の通りです。

 

1.メンバー

(1)変数(C#ではDBみたいに「フィールド」というみたいですね)

アンマネージドリソースは、プロパティの定型の{set; get;}処理以外の処理をするので別途フィールドを設けて、カスタム化しています。
        private Pen _gpen;                            //描画用ペン
        private SolidBrush _gbrush;                    //描画用ブラシ
        private Font _gfont;                        //描画用フォント
        private Image _gimage;                        //画像入出力用イメージ

実は、元々はCANVASのようにブラシはより抽象化されたBrushクラスにしていたのですが、C#ではこの基底クラスがほとんど使い物にならず(注)、ソリッド、ハッチは別々のクラスになってしまっているので、取り敢えずの使用の為にSolidBrushクラスにしています。

注:Brushクラスにすると、色(Color)を変えようとしてもメンバーを持っていないので変えられません。
(2)プロパティ

{set; get;}の定型処理を行うプロパティは以下の通り。

        public Bitmap Canvas {set; get;}            //仮想画面ビットマップ
        public Graphics gHandle {set; get;}            //仮想画面のグラフィック
        public Color gFtColor {set; get;}            //仮想画面の前景色
        public Color gBkColor {set; get;}            //仮想画面の背景色

C#が高級言語になり、CANVASでC++とWin32 APIでできていたことが逆に結構難しくなってしまいました。例えば前景色(gFtColor)を変えただけではペンとブラシの色が変わらないので、カスタムプロパティにしてPen.Color、SolidBrush.Color(↑で述べた通り、Brush.Colorはない)も変えることを考えたのですが、ペンやブラシが初期化されていないとエラーになるし、それを回避しようとすると却ってコードが長くなることもあってやめ、UpdateColor()というprivate メソッドでお茶を濁しております。

(3)メソッド

現在実装したメソッドは次の通り。

        //Canvasのサイズ変更-コンストラクターが二つあり、一つはデスクトップサイズの固定キャンバス、もう一つは指定サイズのキャンバスにします。後者の場合、PictureBoxのサイズが変更された場合を想定し、再初期化処理で対応します。
        public bool Resize(int w, int h);
        //画面消去1(背景色(gBkColor)で消去)
        public void Clear();
        //画面消去2(指定色(gBkColor)で消去)
        public void Clear(Color c);
        //点を描画-これはGraphicsクラスではなく、BitmapクラスのSetPixelメソッドを使っています。
        public void Dot(int x, int y);
        //線を描画(前景色(gFtColor)で描画)
        public void Line(int x1, int y1, int x2, int y2);
        //矩形を描画(フラグf-塗りつぶし有無)
        public void Box(int x1, int y1, int x2, int y2, bool f = false);
        //円を描画(フラグf-塗りつぶし有無)
        public void Circle(int x, int y, int r, bool f = false);
        //矩形内接楕円を描画(フラグf-塗りつぶし有無)
        public void Ellipse(int x1, int y1, int x2, int y2, bool f = false);
        //円弧(線無)を描画
        public void Arced(int x1, int y1, int x2, int y2, int dg1, int dg2);
        //円弧(線有)を描画
        public void Chorded(int x1, int y1, int x2, int y2, int dg1, int dg2, bool f = false);
        //指定色で塗潰す-これはGraphicsクラスにメソッドが無く、やむなくWin32APIでCANVASから流用しました。
        public bool Paint(int x, int y, Color col);
        //ビットマップ画像取込みー内部画像Imageクラスを使います。
        public void GetBMP(PictureBox pb, int x1, int y1, int x2, int y2);
        //内部ビットマップ画像張付け
        public void PutBMP(int x, int y);
        //外部画像張付け
        public void PutBMP(Image img, int x, int y);
        //外部画像を指定サイズに変えて張付け
        public void PutBMP(Image img, int x1, int y1, int x2, int y2);
        //文字を描画
        public void PrintText(String str, Single x, Single y, StringFormat format);

 

2.問題点

BCCSkelton(含ECCSkelton)の場合は、Win32 APIを簡単に使う為に作りましたが、C#では既に高級言語化されているのでCANVASをそのまま持ってくること自体がミスマッチなのかもしれません。どういうことかというと、Easelではブラシがソリッドブラシしか使えないという難点がありますが、C#では簡単にハッチブラシやグラデーションブラシ等が作れるので、メンバーをBrushに戻し、引数にブラシオブジェクトを与える仕様の方がよりC#的です。しかし、元々の発想が「昔のBasic程度の描画機能を再現したい」だったので、これをすると二律背反に陥ってしまいます。ウ~ン、ナヤミ、ナヤミ、ですね。(このことはC#の描画機能の多彩、多様性を生かしていない、という点でも同じです。Easelは単純な描画しかできませんが、簡単に描ける、というのが売りとなるはずなので。)

 

ということで、もうしばらくは色々と悩んで、仕様変更もあるでしょうから、コードの公表は控えます。

 

3.プロトタイプイメージ

とはいえ、Easelの本来の目的である「昔のBasic程度の描画機能を再現したい」が出来ているという点をご紹介する意味で、QBasic64のサンプルで実行したときのイメージを紹介します。

 

では、また次回。

前回触れた、BCCSkeltonのCANVASクラス類似のクラスをC#で作って、一応"Version 1.0"的な感じになりました。また、アプリのコードの中にそのクラスを定義する他、単独でコンパイルしてライブラリーにして使うテストもしました。(

前に書いたように、描画する背景ビットマップを"Canvas"(画版)プロパティとし、それ(及び描画用のペン、ブラシ、フォントおよび画像取り込み用のイメージ)を有する基底クラスを"Easel"(画架)しましたので、独立DLLとさせたときの名前空間とDLLのファイル名を"Atelier"(アトリエ)にしたのは必然でしょう!従って"using Atelier;"として、"Easel easel;"とオブジェクトを作り、イメージは"easel.Canvas;"となります。

 

さて、動作テストは(SDIスケルトン、MDIスケルトンと一緒に作った)DialogスケルトンにPictureBoxコントロールを貼り付けてテストしましたが、このコントロールは"Image"プロパティを持ち、WM_PAINTメッセージの際にこのImageを描画するように設計されています。

 

で、

気に掛ったのは、このImageプロパティの性格です。

 

C#という言語は、C++で当たり前のように使う(「危険な」)ポインターの利用を制限(注1)しておりますが、変数やオブジェクトの宣言は将に「ポインター的」であり、その変数やオブジェクトに(new付き、またはstring等では無しで)「初期化(実体となる所要メモリーをスタックに確保)」をしなければエラーになりますし、等号('=')で他の初期化済変数やオブジェクトを代入するとそれらに対する単なる「(自分には実体のない)参照」をするだけとなります。(注2

注1:"unsafe"が無ければ使えない。

注2:C#で、

"   SomeClass sc;                //オブジェクトの宣言

    sc = new SomeClass();    //初期化(メモリーの確保)→通常ガーベージコレクション処理となる

"

と書くのは、C++の

"   SomeClass *sc;               //オブジェクトポインターの宣言

    sc = new SomeClass();    //初期化(メモリーの確保)→必ず"delete sc"が必要。

"

に酷似しています。

 

従って第1の疑問は、

Q1:PictureBox.Imageは単にポインター的変数なのか、実際にメモリーを確保して代入されたイメージをコピーするのか?

であり、

Q2:(仮に「実際にメモリーを確保」しているのなら)再代入(イメージの変更)の際、確保されたメモリー(元のイメージ)をDispose()でユーザーが開放すべきなのか

否か、ということです。(「開放すべき」でない場合はガーベージコレクションで開放されているはず。)

 

Q1の回答を得るべく、Form_Load(WM_CREATE)処理時に"picBox.Image = easel.Canvas;"とし、更に描画処理を行った後に"picBox.Image = easel.Canvas;"(画像の更新)としていたのを描画処理時のコードをコメントアウトしてみました。(「参照処理」であればそのまま更新されるはずであり、PictureBoxが独立したメモリーを持ってコピーしているのであれば、更新されない筈。)

結果は、...更新されませんでした。(PictureBox.Imageが独自の画像用メモリーを持っている。)

 

次にQ2についてwebをググったのですが、この問いに直接的に回答するものはなく(注)、Disposeをする派、しない派共に見受けられました。その為、自らいくつかのテストを行いました。

注:Visual Studio質問コーナーおよび「使用中のリソースをDisposeしてはいけません

 

テスト1:"(初期代入済の)picBox.Image = easel.Canvas;"(画像の更新)のコードの前に"picBox.Image.Dispose();"を入れてみました。(コンパイル、動作に問題なければ、悪い処理ではない。)結果、このプロパティの処理(Imageの"get; (Width)”処理)時、"有効な引数ではない"エラーに引っ掛かってしまいました。また、

        Image oldimg = picBox.Image;    //古いイメージデータの参照変数を用意しても(新たにnewしてメモリーを確保しても意味がない) easel.Canvas;    //新しい画像データに入れ替えた後に
        oldimg.Dispose
        picBox.Image =();    //開放処理しようとしても、結局は新しいpicBox.Imageを参照していることになり、同じエラーとなります。

 

この為、「そもそも本当にPictureBox.Imageの画像変更や更新を行う際にユーザーによるDisposeが必要なのか?」と考え、タスクマネージャーを起動して連続して"picBox.Image = easel.Canvas;"を実行してみました。その結果、テストプログラムの所要メモリー(リソース消費も含むのだろう)は増加してゆきますが、放っておくと自然に減少(more probably than not, by garbage collection)することを確認しました。

 

なので、面倒なリソース解放処理はシステムに任せて、私は何もしないことにしました!

 

いいんだろうか?(汗;)

 

5月に入って、WindowsというOSの特徴であるGUI、その基礎となる描画等、色々ととりとめもなく描いてきましたが、前回Windows(SDK)における「再描画しても消えないユーザーによる描画作法」(注1)、その面倒を軽減するためのBCCSkeltonのCANVASクラスの仕組み(注2ビットマップを使った仮想ウィンドウ)を語り、C#においてもFormクラスでは矢張りOnPaintメソッド(注3)で描画処理を行うことが基本であることを書きました。

注1:ウィンドウが無効領域を認知すると発するWM_PAINTメッセージを受けて、BiginPaint、EndPaint両関数の間に描画処理を行う。

注2:いつでも描画処理ができるようにウィンドウのクライアント領域に貼る「画版(CANVAS)」をビットマップで作り、そのCANVASに書き込み、(InvalidateRect関数等で)無効化処理を行ってWM_PAINTを呼び出し、WM_PAINTにおける描画処理を「CANVASビットマップを貼り付ける」だけにする。

注3:Formクラスから派生するControlクラスでも同じくOnPaintメソッドがあります。ユーザーがここで処理を行う為には"override"するか、イベントハンドラーで処理を追加する必要があります。

 

又、イメージを扱うPictureBoxというコントロールがあることと、GDIによる描画処理を扱うGraphicsというクラスがあるようなので、それを調べてみたところ、

(1)PictureBox

Formから派生したコントロールで、特徴はこのコントロールに表示させるImageクラスのプロパティを有していることです。しかし、このImageプロパティに直接描画するようなメソッドは見当たりません。

(2)Graphics

GDI(含GDI+)に基づく描画処理を担うクラスで、実に多彩なプロパティと多様な(実際、私如きでは使いきれない感じです-泣;)描画メソッドを持っています。

(3)PictureBoxへの描画

PictureBoxがImageプロパティを持っているので、これにビットマップ等イメージを貼り付けることで簡単に描画できます。(恐らく↑の注2で述べた仮想ウィンドウの表示と同じような処理をカプセル化して、このImageプロパティへのsetでやっているのでしょう。)従って、「線を引く」「楕円(含真円)を描く」等の描画処理はImageプロパティへ貼り付けるビットマップ等の画像で行うことで「再描画しても消えない描画」ができることになると思われます。実際、webでサンプルコードを見てみると、以下のような感じでした。

 

    //ビットマップを作成

    Bitmap bmp = new Bitmap(幅、高さ);    //幅と高さ指定だけの「無地」ビットマップを作成するコンストラクター

    //ビットマップのGraphicクラスインスタンスを作成

    Graphics g = Graphics.FromImage(bmp);

    //Graphicsオブジェクトgを使った描画例

    //位置(200, 300)に100x80の四角を赤色で描く
    g.DrawRectangle(Pens.Red, 200, 300, 100, 80);

    //Graphicsオブジェクトの開放(

    g.Dispose();

    //ピクチャーボックス(PictureBox picBox)へ表示

    picBox.Image = bmp;

    //Bitmapオブジェクトの開放は必要か?-Visual Studioを使っているサンプルでは書かれていない(

    bmp.Dispose();    //必要?

 

:GraphicsクラスオブジェクトはDC(Device Context)を持っているのでDisposeによる明示的メモリー開放が必要になるのはわかります。また、描画関係ではPenクラス、Brushクラスのオブジェクトもユーザーが明示的にDisposeしてメモリーを開放する必要があるようです。しかし、サンプルのコードにはBitmapクラスオブジェクトbmpはDisposeしていません。Bitmapクラスがガーベージコレクションの対象になるのかどうか、調べた人がいました。一応メモリーリーク的は起こりますが、やばい状況になるとガーベージコレクションが働くようですね。しかし、「使い終わったらDispose」を推奨しています。ここにもDispose派がいますね。この人もそうです。Bitmapではなく、その基底になるImageクラスですが、Microsoftもこう言っています。(「Dispose を使い終わったら Image を呼び出します。」は「Image を使い終わったら Dispose を呼び出します。」の翻訳誤りですね。)C++では「newしたら、delete」、「Createしたら、DeleteまたはDestroyあるいはRelease」が当たり前ですが、C#でも描画系オブジェクトはDisposeしておいた方が無難のようですね。(しかし、delete、destroy、releaseのC++とDispose、FinalizeのC#では用語を敢えて変えている印象がありますね。)

 

ここまで調べたことの「まとめ」として、(C#は多様、多彩なクラス、コンポーネントクラスがあるので、自分勝手にクラスを作るのはご法度なんでしょうが)C#でもBCCSkeltonのCANVASクラスのようなものを欲しくなってしまいました。

 

ということで、次からは現在進行形のPictureBox用描画ツールクラス"Easel"()について書いてゆきます。

:まぁ、大したことをするクラスじゃないので飽くまでC#学習の一環として「習作」であることをお断りしておきます。また名称については、CanvasクラスやScreenクラスなどが既に存在するので、現在のクラスにない名称として(「Canvas「画版」を置くならEasel「画架」だろう?」という発想で命名しました。"C# Easel"でググっても何も出てきませんので大丈夫かと。)

 

前回のおさらいとして、WindowsのGUIの為にOSはベースとして(ウィンドウそのものがそうであるような)矩形Rectangle-位置、幅・高さ)単位で、前後の重なり方を「Zオーダー」というもので管理して描画します。(矩形、楕円、その他多角形の「合成」としての領域Regionもありますが、取り敢えずスキップします。)従ってWindowsの画面は操作に従って何度も再描画されるわけです。また、再描画については、「WM_PAINTメッセージの際にBeginPaint関数とEndPaint関数の間に書く必要があります。

【WM_PAINT時に呼び出す関数の例】

bool OnPaint(HWND hWnd) {

    PAINTSTRUCT paint;
    HDC hDC = BeginPaint(hWnd, &paint);
    //ユーザー処理
    EndPaint(hWnd, &paint);
    return TRUE;         //WM_PAINTメッセージ処理をした場合はTRUEを返す
}

 

しかし、同時に「私もこのWindowsの描画処理のお約束(WM_PAINTメッセージ時にBeginPaintEndPaintの中に描画コードをいれる)というのが面倒且つ(プログラムのどこでも描画命令が描けないという)柔軟性の欠如でうんざりしていました」とも書きました。

 

ではどうしたかと言うと、或る雑誌に「描画を消えないようにするには」という記事が載っていたのでそれを参考としてCANVASクラスというものを作りました。使い方は以下の通り。

(1)ビットマップで「仮想ウィンドウ」(CANVASクラスインスタンス)を作り、WM_CREATEで初期設定を行う。

<Project.h>

///////////////////////
//仮想ウィンドウの作成
///////////////////////
CANVAS cvs;


<ProjectProc.hのOnCreate関数>

    //仮想ウィンドウの初期化
    cvs.SetCanvas(m_hWnd);

 

(2)アプリケーションプログラムでは、その「仮想ウィンドウ」に随時、どこからでも書き込める。(CANVASクラスのメンバー関数に、Line、Circle、Paint等の「描画関数」命令を設定)

<描画関数例>

Basic命令に模した関数
cvs.Dot(int x, int y);
cvs.Line(int x1, int y1, int x2, int y2);
cvs.Box(int x1, int y1, int x2, int y2, int br);
cvs.Circle(int x, int y, int r, int br);
Win32由来の関数
cvs.Ellipsed(int x1, int y1, int x2, int y2, int br) ;
cvs.Arced(int x1, int y1, int x2, int y2, int sx, int sy, int ex, int ey);
cvs.Chorded(int x1, int y1, int x2, int y2, int sx, int sy, int ex, int ey, int br); 
その他

cvs.GetBMP、cvs.PutBMP、cvs.FitBMP等

 

(3)書き込んだらウィンドウのクライアント領域全体を無効化(Invalidate)し、メッセージキューに置かれたWM_PAINTが処理される段階で「仮想ウィンドウ」をクライアント領域に張り付ける。(メンバー関数のOnPaint関数を使うが、これはSkeltonWizardが予め書いてくれる。)

<ProjectProc.hのOnPaint関数>

bool CMyWnd::OnPaint(WPARAM wParam, LPARAM lParam) {
    PAINTSTRUCT paint;
    cvs.OnPaint(BeginPaint(m_hWnd, &paint));
    EndPaint(m_hWnd, &paint);
    return TRUE;
}

 

21年前に作った、このCANVASクラスは結構重宝しており、私ごときが行う描画では大体これで済んでしまいました。

 

で、

 

いよいよ本題ですが、この間C#で古いBasicのプログラムを移植しようとして、「こんなに簡単になったC#だからきっと描画も簡単になって、PictureBoxコントロールのメソッドに描画系があるんだろうな」と思いながらWEBでググりましたが、結局は何も変わっていませんでした。(注)

注:大体、詳しく調べてはいませんが、GDI系は殆どがアンマネージドリソースで、使い終わったらDispose()メソッドを呼ぶ必要があるようです。

 

Formクラスでいえば、矢張りWM_PAINTに相当するOnPaintメッセージをオーバーライドしてデフォルトの処理の後に描画関係のメソッドを置きますが、そのメソッドはOnOPaintメソッドの引数であるPaintEventArgsのものなので、OnPaint以外では取得できません。

 

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            // FormのClientRectangleプロパティにより、Form領域の四角形を取得し、同じサイズで描画
            e.Graphics.DrawImage(bitmap, this.ClientRectangle);
        }
 

「おいおい、勘弁してくれよ。C#でもまたWM_PAINT縛りかよ。」と気落ちしたのですが、このeのプロパティの「Graphicsって何だ?」という疑問がわき、もう少し詳しくこのクラスらしきGraphicsPictureBoxを調べてみることにしました。

 

お後は次回ということで。

 

前回、次の質問を残しました。

Q:では、ウィンドウに対する描画命令はどこに描けば(「書けば」でした)よいのでしょうか?

A1:ウィンドウを登録、作成表示した後ならどこでもよい。

A2:ユーザー処理はこのコールバック関数内で行う。

A3:殊に描画処理は、WM_PAINTメッセージが来た際にこのコールバック関数内で行う。

またまた「やっちまったな」という感じです。「~すればよい」という言葉は多義的なので、質問の意図が曖昧、不明瞭(vague and ambiguous)ですよね?(英語でいえば、suggested or recommended to doとかmay doの意味でしょうか?)

「書いてもよい」という意味であれば、総て〇です。(実際、ChatGPTのプログラミングだとウィンドウ作成、表示後にコードを置いています。)

翻って「再描画されても画像が消えないようにする方が良いので、そうするならば」という意味であればA3のみが〇です。

 

私は実験好きなので、こういう時は必ず実際にやってみます。

 

以下(↓)のようなテスト用関数を作り、SKDスケルトンファイルのProcedure.hファイルの冒頭に置きます。また実行プログラム(Program.exe)のあるフォールダーに何かビットマップイメージを"Image.bmp"というファイル名で保存してください。

//テスト用関数
BOOL ShowImage(HWND hWnd) {

    // 画像の読み込み
    HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, "C:\\Users\\ysama\\Programing\\Windows Program\\WinTemplate\\Debug\\Image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    // ウィンドウの描画
    HDC hDC = GetDC(hWnd);    //ウィンドウのデバイスコンテキスト(DC)の取得
    HDC hMemDC = CreateCompatibleDC(hDC);    //DCと同じ構造の仮想DCをメモリー上に確保(コンパチDCの作成)
    SelectObject(hMemDC, hBitmap);     //コンパチDCにビットマップを取り込む
    BITMAP bitmap = {};    //ビットマップ情報を取得するためのITMAP構造体
    GetObject(hBitmap, sizeof(BITMAP), &bitmap);    //ビットマップ情報の取得

    //同じサイズのビットマップを単に表示するのであればBitBlt関数を使うが、ここでは縦横を2倍に引き伸ばしている
    bool result = StretchBlt(hDC, 0, 0, bitmap.bmWidth * 2, bitmap.bmHeight * 2, hMemDC, 0, 0, 32, 32, SRCCOPY);
    DeleteObject(hBitmap);    //使ったビットマップは必ずお片づけ(メモリーの開放)

    DeleteDC(hMemDC);    //使ったコンパチDCも必ずお片づけ(メモリーの開放)
    ReleaseDC(hWnd, hDC);    //最後にウィンドウのデバイスコンテキストを開放
    return result;    //StretchBlt関数の成功、失敗を返す
}

A1:次に、何処でもよいのですが(コールバック関数内でもよいので、A2、A3と論理的にオーバーラップ=共に真となるする余地があるということですよね?)まずSDI.hのInitInstance関数のreturn TRUE;文の前に

//テスト用-再描画されると消えてしまう
ShowImage(hWnd);

を入れてコンパイルしてください。ビットマップが表示されるのが分かります。この時、ウィンドウを最小化したり、サイズを変更したりすると「再描画」が行われ、ビットマップ画像が消えてしまうことを確かめてください。

 

A2:これはコールバック関数の何処でもよいのですが(WM_PAINT以外のとは書かれていないので、A2とA3は論理的にオーバーラップ=共に真となるする余地があるということですよね?)、例えばWM_CREATEに同様にテスト用関数を置くと全く同じ動作をします。

 

A3:最後にWM_PAINTメッセージの場合に呼び出されるOnPaint(HWND hWnd)関数内にテスト用関数を置くのですが、唯単に置くのではなく、以下のコメントにあるように

 

bool OnPaint(HWND hWnd) {
/*
    PAINTSTRUCT paint;
    HDC hDC = BeginPaint(hWnd, &paint);
    //ユーザー処理←ここにShowImage(hWnd);を置く
    EndPaint(hWnd, &paint);
    return TRUE;         //WM_PAINTメッセージ処理をした場合はTRUEを返す
*/

    return FALSE;        //WM_PAINTメッセージ処理をしない場合はFALSEを返す
}

PAINTSTRUCT構造体を使ったBeginPaint(hWnd, &paint);文とEndPaint(hWnd, &paint);文の間に描画処理を行う必要があります。

何故か?

これ(OSが行う描画処理)については、Microsoft Learnの良い記事を見つけましたので、特にこれを読んでいただけるとよいと思います。(前回分かっていればリンクを張ったのですが...)要すれば、前回書いたように「矩形(「位置」および幅と高さを含めた「領域」)を描画単位」としており、そこに変更が生じればOSが検知して「無効領域(Invalidated region)」として「再描画要求(WM_ERASEBKGRDWM_PAINT)」を出してくるということです。

では、このWM_PAINTメッセージ処理(OnPaint関数)をコメントを外し、"retuern FALSE;"をコメントアウトしてコンパイルするとどうなるでしょう?今度はウィンドウを最小化したり、サイズを変更しても、ビットマップ画像が消えなくなることにご注意ください。

どうでしょう?

あーっ、めんどーくせー!

と思いませんでしたか?実は私もこのWindowsの描画処理のお約束(WM_PAINTメッセージ時にBeginPaintEndPaintの中に描画コードをいれる)というのが面倒且つ(プログラムのどこでも描画命令が描けないという)柔軟性の欠如でうんざりしていました。

 

それがBCCSkeltonで

CANVASクラス

を書く動機になりました。(後は、次回にしましょう。)

 

前回、エラソーに「WindowsというOSの本質ってなんだろう」なんて書いて(内心)『失敗したなぁ』って思いました。現在のWINDOWS世界は広大で私が簡潔に書けるようなものではありません。ということで、おしまいに「まぁ、素人はPCをブラックボックスとして扱うのが無難である、ということはわかると思います。次はもっと絞ってウィンドウの描画についてみてみますか。」と書かせていただきました。

 

単純にWebで「Windows 描画」でググると、ユーザーによる描画方法が出てくるので、

Windows API-描画処理」(素人向けに簡潔にわかりやすく解説していると思います。)OSがどのように描画管理しているのか、という点ではあまり書かれたものがありません。

 

まぁ、「ユーザーが余りOSのやることについて知らなくてもよいOSが良いOS」と思うので、それはそれとしてよいか、と思いますが、ウィンドウシステムはウィンドウの重なりが変わる(Zオーダーの変更)等、常に画面を書き換える必要があり、目まぐるしく描画更新をしています。OSが管理するそこら辺のお話(Windowsの描画作法についての一般知識)は知っている方がよく、

Windows (設計の基本)-ウィンドウの管理」(Win 7時点の記事ですが役に立ちます。窓枠等その他の記事も面白いです。)

辺りが参考となります。

 

要すれば元々ウィンドウシステムは、出力先(デバイス)毎に異なるDC(デバイスコンテキスト-注1)と、点、線、楕円(円を含む)という描画機能をまとめたGDI(Graphic Device Interface-注2)に基づいた矩形(「位置」および幅と高さを含めた「領域」)を描画単位としており、その後透明、半透明を利用した非定型図形もカバーするようになりました。

注1:「デバイス コンテキストは、一連のグラフィック オブジェクトとその関連する属性、および出力に影響を与えるグラフィック モードを定義する構造」、「ディスプレイデバイスやプリンタデバイスなどの表示デバイスを仮想化するために導入されたWindowsのしくみ 」とか「デバイスコンテキスト(以下DC)とは、ディスプレイやプリンタなどの表示デバイスと、アプリケーションの仲介を行うWindowsの仕組み」と説明されていますが「はぁ?」という反応が予想されます。私的には「デバイスコンテキストとは、ウィンドウズシステムの中で、『異なるデバイスという外国人』に対して、伝えたい情報を異なる言葉で通訳してくれるもの(インターフェース)」という感じでしょうかね?

注2現在のGDIは、旧来のWindows GDIに加え、より複雑な描画機能を実現したGDI+やDIrect XのAPIを含みます。

 

またまた話がでかくなりましたが、まぁ、素人プログラマーにはこういう背景をもとにどう描画すればよいかが分かればよいのでしょうね。

で、

「どう描画すればよいか」が結構厄介なのです。従来のROM Basic等の逐次処理プログラミングでは「何時、どの段階でも、単に描画命令を入れれば出力先画面に描画してくれた」のですが、Windowsでは一寸作法が異なります。

 

前々回のブログで、C++によるWindows SKDベースのスケルトンプログラムを載せましたが、ウィンドウズプログラムの流れは以下の通りです。

 

(1)自分のウィンドウを登録する。

    WNDCLASSEX wc;
    ・

    ・

    ・

    return (RegisterClassEx(&wc));

 

(2)登録したウィンドウを作成する。

    HWND hWnd = CreateWindowEx(
            NULL,                    //dwExStyle
            szClassName,            //ウィンドウクラス名
            szTitleName,            //タイトルバーにこの名前が表示されます
            WS_OVERLAPPEDWINDOW,    //ウィンドウの種類
            CW_USEDEFAULT,            //X座標
            CW_USEDEFAULT,            //Y座標
            CW_USEDEFAULT,            //幅
            CW_USEDEFAULT,            //高さ
            NULL,                    //親ウィンドウのハンドル、親を作るときはNULL
            NULL,                    //メニューハンドル、クラスメニューを使うときはNULL
            hInstance,                //インスタンスハンドル
            NULL);

 

(3)作成したウィンドウを表示する。

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

注:表示状態を設定するShowWindow関数とクライアント領域の更新領域に対して描画メッセージを発信するUpdateWindow関数についてはリンクをお読みください。

 

(4)作成されたウィンドウのメッセージループ開始します。

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

 

(5)このループは毎回ウィンドウのコールバック関数を呼び、このコールバック関数内で必要なユーザー処理を行うか、OSによる既定処理(DefWindowProc(hWnd, msg, wParam, lParam))を行います。

 

Q:では、ウィンドウに対する描画命令はどこに描けばよいのでしょうか?

A1:ウィンドウを登録、作成表示した後ならどこでもよい。

A2:ユーザー処理はこのコールバック関数内で行う。

A3:殊に描画処理は、WM_PAINTメッセージが来た際にこのコールバック関数内で行う。

 

さぁ、どうでしょう?あとは次回にしましょうか。