2022/09/06(火) [n年前の日記]
#1 [prog][windows] GDIでダブルバッファ描画
Windows10 x64 21H2上で、C/C++ (MinGW) と Windows API を使ってウインドウ描画ができるか実験中。
_昨日、GDI+を使ってpng画像を描画できるのか、 実験していたわけだけど。GDI+ は描画が遅いという話を見かけたので、GDI+ ではなく GDI だけを使って描画ができるならそのほうがいいのかなと…。そんなわけで、GDI でダブルバッファ描画をする方法を試してみた。まあ、png画像の使用を諦めて、bmp画像(bitmap画像)だけを利用することになってしまうけど…。
環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。MSYS2 + MinGW-w64 ではなくて、MinGW (+ MSYS) で実験してる。
以下を参考にさせてもらいました。
_ビットマップのマルチバッファリング【Windowsプログラミング研究所】
今回書いたサンプルの実行結果は以下。ちょっと解像度が荒いけど、雰囲気は伝わるかと…。ダブルバッファで描画しているので、ちらつきは感じられない。
_昨日、GDI+を使ってpng画像を描画できるのか、 実験していたわけだけど。GDI+ は描画が遅いという話を見かけたので、GDI+ ではなく GDI だけを使って描画ができるならそのほうがいいのかなと…。そんなわけで、GDI でダブルバッファ描画をする方法を試してみた。まあ、png画像の使用を諦めて、bmp画像(bitmap画像)だけを利用することになってしまうけど…。
環境は、Windows10 x64 21H2 + MinGW (gcc 9.2.0)。MSYS2 + MinGW-w64 ではなくて、MinGW (+ MSYS) で実験してる。
以下を参考にさせてもらいました。
_ビットマップのマルチバッファリング【Windowsプログラミング研究所】
今回書いたサンプルの実行結果は以下。ちょっと解像度が荒いけど、雰囲気は伝わるかと…。ダブルバッファで描画しているので、ちらつきは感じられない。
◎ リソースファイル。 :
リソースファイルは以下。このファイルは、windres を使って .o に変換する。
_res.rc
_res.rc
BALL_IMG BITMAP "ball.bmp" BALL_MSK BITMAP "ball_mask.bmp" BG_IMG BITMAP "bg2.bmp"
- 3つのbmp画像ファイルを含めるように指定している。
- 先頭に書かれている「BALL_IMG」「BALL_MSK」「BG_IMG」が、C言語のソース内で指定する識別名。
◎ 使用画像。 :
使用画像は以下。
_images.zip
一応、png画像も載せておきます。
_images.zip
- ball.bmp : 赤いボール画像。透明にしたい部分は黒(#000000)にしておく。
- ball_mask.bmp : ボール画像のマスク画像。不透明部分は黒(#000000)、透明部分は白(#ffffff)。アルファチャンネルを反転した感じの画像。
- bg2.bmp : 背景用画像。タイル状に描画することを前提にしている。
一応、png画像も載せておきます。
◎ C言語のソースファイル。 :
C言語のソースファイルは以下。今回は GDI+ を使ってないので、C++ ではなく C言語で書くことができる。
_01_dblbuf.c
_01_dblbuf.c
/* GDI Double buffer sample */ #include <windows.h> #define SCRW 1280 #define SCRH 720 #define FPS 60 #define TM_COUNT1 101 #define LPCLASSNAME TEXT("DoubleBufferSample") #define WINDOW_TITLE TEXT("Double Buffer Sample") LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { static HBITMAP hBitmap; static HBITMAP hBitmap_mask; static HBITMAP hBitmap_bg; static HBITMAP offScrnBitmap; static int wdw_w, wdw_h; static int imgw, imgh; static int bgw, bgh; static int bgx, bgy; static float x, y; static float dx, dy; switch (msg) { case WM_CREATE: // get window size { RECT rc; GetClientRect(hwnd, &rc); wdw_w = rc.right - rc.left; wdw_h = rc.bottom - rc.top; } // create off screen bitmap { HDC hdc; hdc = GetDC(hwnd); offScrnBitmap = CreateCompatibleBitmap(hdc, wdw_w, wdw_h); ReleaseDC(hwnd, hdc); // GetDC() -> ReleaseDC() } // load bitmaps from resource hBitmap = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_IMG")); hBitmap_mask = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BALL_MSK")); hBitmap_bg = LoadBitmap(((LPCREATESTRUCT)lp)->hInstance, TEXT("BG_IMG")); // get images size { BITMAP bp; // get ball image size GetObject(hBitmap, sizeof(BITMAP), &bp); imgw = bp.bmWidth; imgh = bp.bmHeight; // get bg image size GetObject(hBitmap_bg, sizeof(BITMAP), &bp); bgw = bp.bmWidth; bgh = bp.bmHeight; } // init work x = wdw_w / 2; y = wdw_h / 2; dx = (float)wdw_w / (float)FPS; dy = dx * 0.2; bgx = 0; bgy = 0; SetTimer(hwnd, TM_COUNT1, (int)(1000 / FPS), NULL); return 0; case WM_TIMER: // main loop x += dx; y += dy; if (x <= 0 || (x + imgw) >= wdw_w) dx *= -1; if (y <= 0 || (y + imgh) >= wdw_h) dy *= -1; bgx -= 1; bgy -= 1; if (bgx <= -(bgw)) bgx += bgw; if (bgy <= -(bgh)) bgy += bgh; if (0) { // clear background InvalidateRect(hwnd, NULL, TRUE); } else { // not clear background InvalidateRect(hwnd, NULL, FALSE); } break; case WM_PAINT: // draw window { HDC hdc, hBmpDC, hMemDC; PAINTSTRUCT ps; hdc = BeginPaint(hwnd, &ps); // hdc = GetDC(hwnd); // set off-screen bitmap hMemDC = CreateCompatibleDC(hdc); SelectObject(hMemDC, offScrnBitmap); // draw bg to off-screen hBmpDC = CreateCompatibleDC(hdc); SelectObject(hBmpDC, hBitmap_bg); { int by = bgy; while (by <= wdw_h) { int bx = bgx; while (bx <= wdw_w) { BitBlt(hMemDC, bx, by, bgw, bgh, hBmpDC, 0, 0, SRCCOPY); bx += bgw; } by += bgh; } } DeleteDC(hBmpDC); // draw mask to off-screen hBmpDC = CreateCompatibleDC(hdc); SelectObject(hBmpDC, hBitmap_mask); BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCAND); DeleteDC(hBmpDC); // draw source image to off-screen hBmpDC = CreateCompatibleDC(hdc); SelectObject(hBmpDC, hBitmap); BitBlt(hMemDC, (int)x, (int)y, imgw, imgh, hBmpDC, 0, 0, SRCPAINT); DeleteDC(hBmpDC); // drawing to off-screen is finished // draw off-screen bitmap to window BitBlt(hdc, 0, 0, wdw_w, wdw_h, hMemDC, 0, 0, SRCCOPY); DeleteDC(hMemDC); // Create*() -> DeleteDC() EndPaint(hwnd, &ps); } return 0; case WM_DESTROY: KillTimer(hwnd, TM_COUNT1); // kill timer DeleteObject(offScrnBitmap); DeleteObject(hBitmap); DeleteObject(hBitmap_mask); DeleteObject(hBitmap_bg); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wp, lp); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nCmdShow) { HWND hwnd; MSG msg; WNDCLASS winc; winc.style = CS_HREDRAW | CS_VREDRAW; winc.lpfnWndProc = WndProc; winc.cbClsExtra = winc.cbWndExtra = 0; winc.hInstance = hInstance; winc.hIcon = LoadIcon(NULL, IDI_APPLICATION); winc.hCursor = LoadCursor(NULL, IDC_ARROW); winc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); winc.lpszMenuName = NULL; winc.lpszClassName = LPCLASSNAME; if (!RegisterClass(&winc)) return -1; hwnd = CreateWindow( LPCLASSNAME, WINDOW_TITLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, SCRW, SCRH, NULL, NULL, hInstance, NULL ); if (hwnd == NULL) return -1; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; }
- SetTimer() で指定したミリ秒が経過すると、その都度、WM_TIMER というメッセージが Windows から送られてくる。
- WM_TIMER が来た時に、ボールやBG画像の表示位置を更新することで、アニメーションをさせている。
- InvalidateRect() を呼べば、ウインドウの再描画を要求できる。(WM_PAINTメッセージが送られてくる。)
- InvalidateRect(hwnd, NULL, TRUE); なら、背景を消去しつつ、ウインドウを再描画(するように要求)。
- InvalidateRect(hwnd, NULL, FALSE); なら、背景を消去せずに、ウインドウを再描画(するように要求)。ダブルバッファ描画をする時はこちらを指定する。
- WM_PAINT が来た時に、描画処理を行う。
- WM_DESTROY が来たら、終了処理を行う。タイマーを殺して、bitmap を全部削除しておく。
◎ Makefile。 :
Makefile は以下。.c や .rc から .o を生成後、.o群をリンクして .exe を生成する。
_Makefile
_Makefile
01_dblbuf.exe: 01_dblbuf.o res.o gcc 01_dblbuf.o res.o -o 01_dblbuf.exe -mwindows 01_dblbuf.o: 01_dblbuf.c gcc -c 01_dblbuf.c res.o: res.rc ball.bmp ball_mask.bmp bg2.bmp windres res.rc res.o .PHONY: clean clean: rm -f *.exe rm -f *.o
◎ コンパイル/ビルド。 :
全部で以下のファイルになる。
ちなみに、make clean と打てば、*.o や *.exe を削除してくれる。
- 01_dblbuf.c
- ball.bmp
- ball_mask.bmp
- bg2.bmp
- Makefile
- res.rc
ちなみに、make clean と打てば、*.o や *.exe を削除してくれる。
◎ 余談。 :
DeleteDC() と ReleaseDC() の使い分けが分からなくて悩んだけれど、以下のページを眺めたら分かってきた。
_API 関数解説
_API 関数解説
- GetDC() を使って取得したデバイスコンテキストは、ReleaseDC() を使って解放する。
- Create*() を使って取得したデバイスコンテキストは、DeleteDC() を使って解放する。
◎ 余談その2。 :
今回のソースは、中括弧(?)を利用して、変数のスコープを極力狭めつつ書いてみた。
_変数のスコープ(C言語) - 超初心者向けプログラミング入門
_C/C++ 中括弧でローカルスコープを作る - 【はてな】ガットポンポコ
_変数のスコープ(C言語) - 超初心者向けプログラミング入門
_C/C++ 中括弧でローカルスコープを作る - 【はてな】ガットポンポコ
[ ツッコむ ]
以上です。