mieki256's diary



2022/09/05(月) [n年前の日記]

#1 [prog][windows] GDI+でpng画像を読み込んで描画

Windows10 x64 21H2上で、C/C++ と Windows APIを使って画像を描画する実験をしているところ。Windows API (GDI?)ではbitmap画像しか扱えないけれど、Windows XP から使えるようになった GDI+ なるものを使えばpng画像も扱えるという話を見かけたので試してみた。

環境は、Windows10 x64 21H2 + MinGW (g++ 9.2.0)。MSYS2 + MinGW-w64 ではなく、MinGW (+ MSYS) を使ってコンパイルしている。

ちなみに、GDI+ を使う場合は C++ を使うことになるらしい。

pngファイルを読み込む版。 :

まずは、実行ファイルと同じディレクトリに置いてある png ファイルを読み込んで、描画する処理を試してみた。以下のサンプルや解説を参考にさせてもらった。

_GDI+入門
_GDI+でBMP/PNG/JPEGの読み込みを簡単に | 株式会社ヘキサドライブ


_01_gdiplus01.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS    60

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static Image *img;
  static Image *bg;
  static Graphics *g;
  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
      case WM_CREATE:
        // get window size
        GetClientRect(hWnd, &rect);
        wdw_w = rect.right - rect.left;
        wdw_h = rect.bottom - rect.top;

        img = new Image(L"test.png");
        bg = new Image(L"bg.png");

        // int work
        x = (float)(wdw_w / 2);
        y = (float)(wdw_h / 2);
        dx = (float) wdw_w / (float)FPS;
        dy = dx * 0.7;

        SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        break;

      case WM_TIMER:
        // main loop
        x += dx;
        y += dy;

        if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

        if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

        InvalidateRect(hWnd, NULL, TRUE);
        // InvalidateRect(hWnd, NULL, FALSE);
        break;

      case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        g = new Graphics(hdc);

        g->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());
        g->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

        delete (g);
        EndPaint(hWnd, &ps);
        break;

      case WM_DESTROY:
        delete (img);
        delete (bg);

        PostQuitMessage(0);
        break;

      default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

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

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}

せっかくだから、SetTimer() を使って、一定時間毎にタイマーで処理が走るようにして、簡単なアニメーションをするようにしてみた。SetTimer() を使ってタイマーをセットすれば、そこで指定したミリ秒(1/1000秒)が経過すると、WM_TIMER というメッセージがWindowsから送られてくるようになるので、WM_TIMER が送られてきた時の処理を書けばアニメーションができる。また、InvalidateRect() を呼べば、再描画を要求できる(WM_PAINT メッセージが送られてくる)。

ただ、SetTimer() の精度(?)は、Windows NT系OSの場合、10ms らしい。60 FPS を期待して16ms前後を指定しても、おそらく 20ms ぐらいの間隔で処理することになるのだろう…。


使用画像は以下。

_test.png
_bg.png


コンパイルは以下。01_gdiplus01.exe が生成される。
g++ 01_gdiplus01.cpp -o 01_gdiplus01.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32
  • GDI+ を使うので、-lgdiplus -lgdi32 をつけないといかんらしい。


実行結果は以下。




たしかに、GDI+ を使うことで、png画像を読み込んで描画することができた。赤いボールのpng画像はアルファチャンネルを持っているので、周辺がちゃんと抜けた状態で描画されている。

ただ、少々問題が…。ウインドウ内がとにかくちらつく。描画処理として、ウインドウ内消去、背景画像相当の描画、ボールの描画を行っているので、その描画過程が見えちゃってちらつくのだな…。まあ、解決策については後で考えることにする。

リソースファイル内のpng画像を利用する。 :

上記のサンプルはpngファイルを読み込んで利用しているけれど、せっかくだからリソースファイル(.exeファイルに含めるデータ群)にpng画像を含めておいて、それを利用するようにしたい。

以下を参考にして作業。

_プログラミングTips : Vistaの透け透けウィンドウにアルファ付き画像を描画する
_winapi - C++ GDI+ how to get and load image from resource? - Stack Overflow
_gdi+ - "undefined reference to `CreateStreamOnHGlobal@12'" error faced on executing gdiplus c++ code - Stack Overflow

リソース内からpng画像を一発で読み込む関数は用意されてないらしくて、いくつかの手順を踏んで処理しないといけないらしい…。


さておき。リソースファイルは以下。

_res.rc
#include "resource.h"

IDI_BALL    RCDATA     "test.png"
IDI_BG      RCDATA     "bg.png"

「RCDATA」を記述することで、何らかのバイナリファイルであることを指定している。「PNG」や「IMAGE」を記述している事例も見かけたのだけど、自分の環境では「RCDATA」にしないと上手く行かなかった。MinGW を使って作業している関係だろうか…?


リソースファイル関連ヘッダファイルは以下。ボール画像とBG画像のID番号を設定している。

_resource.h
#ifndef RESOURCE_H_
#define RESOURCE_H_

#define IDI_BALL    101
#define IDI_BG      102

#endif


C++のソースは以下。

_02_gdiplus_res.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
#include "resource.h"

using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS  60

HINSTANCE hInst;

// Load image from resource
Bitmap *LoadImageFromResource(
  HINSTANCE hinst,  // handle instance
  LPCTSTR pName,  // resource ID
  LPCTSTR pType   // resource type
  )
{
  // Search resource
  HRSRC hRes = FindResource(hinst, pName, pType);

  if (hRes == NULL)
    {
      MessageBox(NULL, "Not found resource", "Error", MB_OK);
      return NULL;
    }

  // get resource size
  DWORD Size = SizeofResource(hinst, hRes);

  if (Size == 0)
    {
      MessageBox(NULL, "Resource size = 0", "Error", MB_OK);
      return NULL;
    }


  HGLOBAL hData = LoadResource(hinst, hRes);

  if (hData == NULL)
    {
      MessageBox(NULL, "Failure load resource", "Error", MB_OK);
      return NULL;
    }

  const void *pData = LockResource(hData);

  if (pData == NULL)
    {
      MessageBox(NULL, "Failure lock resource", "Error", MB_OK);
      return NULL;
    }

  // Copy resource data
  HGLOBAL hBuffer = GlobalAlloc(GMEM_MOVEABLE, Size);

  if (hBuffer == NULL)
    {
      MessageBox(NULL, "Failure alloc", "Error", MB_OK);
      return NULL;
    }

  void *pBuffer = GlobalLock(hBuffer);

  if (pBuffer == NULL)
    {
      MessageBox(NULL, "Failure lock", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  CopyMemory(pBuffer, pData, Size);
  GlobalUnlock(hBuffer);

  // read image
  IStream *pStream;

  if (CreateStreamOnHGlobal(hBuffer, TRUE, &pStream) != S_OK)
    {
      MessageBox(NULL, "Failure create stream", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  Bitmap *pBitmap = Bitmap::FromStream(pStream);
  pStream->Release();

  return pBitmap;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  static Gdiplus::Bitmap* img;
  static Gdiplus::Bitmap* bg;

  static Graphics *g;
  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
    case WM_CREATE:
      // get window size
      GetClientRect(hWnd, &rect);
      wdw_w = rect.right - rect.left;
      wdw_h = rect.bottom - rect.top;

      img = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BALL), RT_RCDATA);
      bg = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BG), RT_RCDATA);

      // not work
      // img = LoadImageFromResource(hInst, TEXT("IDI_BALL"), TEXT("RCDATA"));
      // bg = LoadImageFromResource(hInst, TEXT("IDI_BG"), TEXT("RCDATA"));

      if (img != NULL && bg != NULL)
        {
          // init work
          x = (float)(wdw_w / 2);
          y = (float)(wdw_h / 2);
          dx = (float) wdw_w / (float)FPS;
          dy = dx * 0.5;

          // Set timer
          SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        }

      break;

    case WM_TIMER:
      // main loop
      x += dx;
      y += dy;

      if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

      if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

      InvalidateRect(hWnd, NULL, TRUE);
      // InvalidateRect(hWnd, NULL, FALSE);
      break;

    case WM_PAINT:
      hdc = BeginPaint(hWnd, &ps);
      g = new Graphics(hdc);

      if (bg != NULL)
        g->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());

      if (img != NULL)
        g->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

      delete (g);
      EndPaint(hWnd, &ps);
      break;

    case WM_DESTROY:
      delete (img);
      delete (bg);

      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  hInst = hInstance;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

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

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}


使用画像は以下。

_test.png
_bg.png


コンパイル/ビルドは以下。
windres res.rc res.o
g++ -c 02_gdiplus_res.cpp
g++ 02_gdiplus_res.o res.o -o 02_gdiplus_res.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32 -lole32
  • 上から順に、「リソースをコンパイルして .o を生成」「C++ソースをコンパイルして .o を生成」「.o群をリンクして.exe を生成」を行っている。
  • CreateStreamOnHGlobal() がリンク時に見つからなかったけど、ole32 が必要になるらしい。リンクの指定として、-lole32 を追加した。


実行結果は以下。




png画像を描画することができている。リソースからpng画像を読み込むこともできると分かった。

まあ、これもウインドウ内のちらつきが酷いけど…。

ダブルバッファを利用。 :

前述のサンプルは、ウインドウ内がとにかくちらついてしまうので、そのあたりを改善したい。ダブルバッファを導入すれば改善されるらしいので試してみる。

以下のページが参考になった。

_VC++でGDI+ そにょ8 ?ダブルバッファリング? - yuyarinの日記
_ビットマップのマルチバッファリング【Windowsプログラミング研究所】

仕組みとしては…。
  • ウインドウと同サイズの、空のbitmapを用意しておく。
  • 一旦、そのbitmapにアレコレを描画。
  • そのbitmapをウインドウ内に描画する。
これで、細かいアレコレを描画している過程を見せないようにする。また、毎回ウインドウ内をクリアしてから描画せずに、前回の画面を残したまま上書き描画することでもちらつきが改善されるらしい。

resource.h、res.rc、使用するpng画像は、前述のものをそのまま利用する。

_res.rc
_resource.h
_test.png
_bg.png

C++のソースは以下。

_03_gdiplus_dbuf.cpp
#define WINVER          0x0501      // 0x0501 = WindowsXP
#define _WIN32_WINNT    0x0501

#include <windows.h>
#include <gdiplus.h>
#include "resource.h"

using namespace Gdiplus;

#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdiplus.lib")

#define SCRW 512
#define SCRH 512

#define TM_COUNT1 1
#define FPS  60

HINSTANCE hInst;

// Load image from resource
Bitmap *LoadImageFromResource(
  HINSTANCE hinst,  // handle instance
  LPCTSTR pName,  // resource ID
  LPCTSTR pType   // resource type
  )
{
  // Search resource
  HRSRC hRes = FindResource(hinst, pName, pType);

  if (hRes == NULL)
    {
      MessageBox(NULL, "Not found resource", "Error", MB_OK);
      return NULL;
    }

  // get resource size
  DWORD Size = SizeofResource(hinst, hRes);

  if (Size == 0)
    {
      MessageBox(NULL, "Resource size = 0", "Error", MB_OK);
      return NULL;
    }


  HGLOBAL hData = LoadResource(hinst, hRes);

  if (hData == NULL)
    {
      MessageBox(NULL, "Failure load resource", "Error", MB_OK);
      return NULL;
    }

  const void *pData = LockResource(hData);

  if (pData == NULL)
    {
      MessageBox(NULL, "Failure lock resource", "Error", MB_OK);
      return NULL;
    }

  // Copy resource data
  HGLOBAL hBuffer = GlobalAlloc(GMEM_MOVEABLE, Size);

  if (hBuffer == NULL)
    {
      MessageBox(NULL, "Failure alloc", "Error", MB_OK);
      return NULL;
    }

  void *pBuffer = GlobalLock(hBuffer);

  if (pBuffer == NULL)
    {
      MessageBox(NULL, "Failure lock", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  CopyMemory(pBuffer, pData, Size);
  GlobalUnlock(hBuffer);

  // read image
  IStream *pStream;

  if (CreateStreamOnHGlobal(hBuffer, TRUE, &pStream) != S_OK)
    {
      MessageBox(NULL, "Failure create stream", "Error", MB_OK);
      GlobalFree(hBuffer);
      return NULL;
    }

  Bitmap *pBitmap = Bitmap::FromStream(pStream);
  pStream->Release();

  return pBitmap;
}


LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  HDC hdc;
  PAINTSTRUCT ps;
  
  static Bitmap* img;
  static Bitmap* bg;
  static Bitmap* offScrnBmp;

  static Graphics* onScrn;
  static Graphics* offScrn;

  RECT rect;

  static float x, y, dx, dy;
  static int wdw_w, wdw_h;

  switch (uMsg)
    {
    case WM_CREATE:
      // get window size
      GetClientRect(hWnd, &rect);
      wdw_w = rect.right - rect.left;
      wdw_h = rect.bottom - rect.top;

      // create off screen bitmap
      offScrnBmp = new Bitmap(wdw_w, wdw_h);

      // Load image from resource
      img = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BALL), RT_RCDATA);
      bg = LoadImageFromResource(hInst, MAKEINTRESOURCE(IDI_BG), RT_RCDATA);

      // not work
      // img = LoadImageFromResource(hInst, TEXT("IDI_BALL"), TEXT("RCDATA"));
      // bg = LoadImageFromResource(hInst, TEXT("IDI_BG"), TEXT("RCDATA"));

      if (img != NULL && bg != NULL)
        {
          x = (float)(wdw_w / 2);
          y = (float)(wdw_h / 2);
          dx = (float) wdw_w / (float)FPS;
          dy = dx * 0.5;

          SetTimer(hWnd, TM_COUNT1, (int)(1000 / FPS), NULL);
        }

      break;

    case WM_TIMER:
      // main loop
      x += dx;
      y += dy;

      if (x <= 0 || x + img->GetWidth() >= wdw_w) dx *= -1;

      if (y <= 0 || y + img->GetHeight() >= wdw_h) dy *= -1;

      // InvalidateRect(hWnd, NULL, TRUE);
      InvalidateRect(hWnd, NULL, FALSE);  // not clear background
      break;

    case WM_PAINT:
      // draw image to off screen
      offScrn = new Graphics(offScrnBmp);
      
      if (bg != NULL)
        offScrn->DrawImage(bg, 0, 0, bg->GetWidth(), bg->GetHeight());

      if (img != NULL)
        offScrn->DrawImage(img, (int)x, (int)y, img->GetWidth(), img->GetHeight());

      delete (offScrn);

      // draw off screen to window
      hdc = BeginPaint(hWnd, &ps);
      
      onScrn = new Graphics(hdc);
      onScrn->DrawImage(offScrnBmp, 0, 0, offScrnBmp->GetWidth(), offScrnBmp->GetHeight());
      delete(onScrn);
      
      EndPaint(hWnd, &ps);
      
      break;

    case WM_DESTROY:
      delete (img);
      delete (bg);
      delete (offScrnBmp);

      PostQuitMessage(0);
      break;

    default:
      return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }

  return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
  MSG msg;
  GdiplusStartupInput gpSI;
  ULONG_PTR lpToken;

  hInst = hInstance;

  /* GDI+初期化 */
  GdiplusStartup(&lpToken, &gpSI, NULL);

  HCURSOR hCursor = LoadCursor(NULL, IDC_ARROW);
  HBRUSH hBrush = (HBRUSH)(COLOR_WINDOW + 1);
  WNDCLASS wcl = { 0, WndProc, 0, 0, hInst, NULL, hCursor, hBrush, NULL, "mh" };
  DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;

  if (!RegisterClass(&wcl) ||
      !CreateWindowEx(0, "mh", "gdiplus test",
                      style,
                      CW_USEDEFAULT, CW_USEDEFAULT,
                      SCRW, SCRH,
                      NULL, NULL, hInst, NULL))
    return FALSE;

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

  /* GDI+終了 */
  GdiplusShutdown(lpToken);

  return msg.wParam;
}


コンパイル/ビルドは以下。03_gdiplus_dbuf.exe が生成される。
windres res.rc res.o
g++ -c 03_gdiplus_dbuf.cpp
g++ 03_gdiplus_dbuf.o res.o -o 03_gdiplus_dbuf.exe -mwindows -static -lstdc++ -lgcc -lgdiplus -lgdi32 -lole32

一応、前述の2つのファイルについても記述した Makefile も置いておく。Makefile があれば、make と打つだけでコンパイル/ビルドができる。

_Makefile


実行結果は以下。




ちらつきがかなり改善された。

まあ、bitmapをウインドウ内に転送している過程が見えちゃってるというか、いわゆるティアリングのような見た目が発生している気もするけれど…。

_ティアリング、スタッタリングについて | ドスパラ サポートFAQ よくあるご質問|お客様の「困った」や「知りたい」にお応えします。

それでも、ダブルバッファを導入してないサンプルと比べたら雲泥の差かなと…。

GDI+は遅いらしい。 :

関連情報をググっていたら、GDI+ は描画が遅いという話を見かけた。GDI+ を使わずに、GDIで済ませられるなら、GDIだけを使ったほうが速いのだとか。

_Windows 10 で描画処理が遅くなる問題について(WebArchive)
_kenjinote/DrawSpeedTest: DWM が有効か無効かで GDI+ の描画速度が異なるのでその検証を行うためのプロジェクトです。
_Pasture | GDI+のビットマップ転送速度

png画像を使えるようになったのはありがたいけど、bitmapだけを使うようにして、GDIで処理したほうがいいのかもしれない…。

以上です。

過去ログ表示

Prev - 2022/09 - Next
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30

カテゴリで表示

検索機能は Namazu for hns で提供されています。(詳細指定/ヘルプ


注意: 現在使用の日記自動生成システムは Version 2.19.6 です。
公開されている日記自動生成システムは Version 2.19.5 です。

Powered by hns-2.19.6, HyperNikkiSystem Project