はじめに
以前投稿した記事 Windows 11 のコンソール AP で GDI 描画ができない制限を回避する、軽量 Win32 コールバック・フレームワーク では、 GDI_CB.inc という C 形式コールバック型の軽量描画枠組みを紹介しました。
しかし、コールバック方式では WM_DROPFILES(ファイルドロップ)や WM_TIMER といった新しい Win32 メッセージに対応するたびに、基底フレームワーク側の WndProc や登録関数を書き換える必要がある という拡張上の課題がありました。
そこで今回は、基底ヘッダー( GDI_CRTP.inc )には一切手を加えず、CRTP(Curiously Recurring Template Pattern:静的多態)と Mixin パターンを組み合わせることで、必要な機能(ドラッグ&ドロップ等)を自由に着脱できる進化版フレームワークの構築と、その応用例としての EMF(拡張メタファイル)ビューアの実装を紹介します。
CRTP 版フレームワークの特長
-
virtual( vtable ) のオーバーヘッドゼロ : コンパイル時に静的バインドされるため、仮想関数のコストがありません - 基底クラス非破壊の拡張性 : 派生クラス側でのメッセージフックや Mixin クラスの多重継承により、基底コードを汚さずに機能を追加できます
-
コンソールアプリ(
main関数)対応: デバッグログ出力と GDI ウィンドウ表示を両立できます
フレームワークの実装( GDI_CRTP.inc )
GDI_CRTP.inc はインクルード 1 枚で完結する最小限の GDI ウィンドウ枠組みです。
GDI_CRTP.inc(展開して表示)
// **************************************************************************
// @file GDI_CRTP.INC
// @brief CRTP BASED GDI FRAMEWORK INTERFACE AND IMPLEMENTATION
//
// @author Iwao ( https://mish.work/ )
// @date 2026-08-20
//
// @modify
// 2026-08-20 新規作成
//
// @disclaimer
// 本コードの使用により生じたいかなる損害についても著作者は責任を負いません
// 引用時は上記 URL を明記してください
//
// (C) 2026 Iwao. All Rights Reserved.
// **************************************************************************
#pragma once
#include <windows.h>
#include <tchar.h>
#include <iostream>
// **************************************************************************
// CRTP GDI ウィンドウ基底クラス
// 作成日 : 2026-08-20
// **************************************************************************
template <class T>
class CGdiWindowCRTP
{
public:
HWND m_hWnd;
CGdiWindowCRTP()
{
m_hWnd = NULL;
}
// デフォルトイベントハンドラ
void OnDrawGdi(HWND hWnd, HDC hDC)
{
RECT rect;
if (::GetClientRect(hWnd, &rect)) {
rect.left += 20;
rect.top += 20;
rect.right -= 20;
rect.bottom -= 20;
if (rect.left < rect.right && rect.top < rect.bottom) {
// ペン:明るい緑 (0, 255, 0)
HPEN hPen = ::CreatePen(PS_SOLID, 2, RGB(0, 255, 0));
HPEN hOldPen = (HPEN)::SelectObject(hDC, hPen);
// ブラシ:暗い緑 (0, 128, 0)
HBRUSH hBrush = ::CreateSolidBrush(RGB(0, 128, 0));
HBRUSH hOldBrush = (HBRUSH)::SelectObject(hDC, hBrush);
::Rectangle(hDC, rect.left, rect.top, rect.right, rect.bottom);
::SelectObject(hDC, hOldBrush);
::SelectObject(hDC, hOldPen);
::DeleteObject(hBrush);
::DeleteObject(hPen);
}
}
}
void OnMouse(int button, int state, int x, int y)
{
// 左ボタン(0)が押された(0)場合に座標を出力する
if (button == 0 && state == 0) {
std::cout << "Mouse L-Down at: " << x << ", " << y << std::endl;
}
}
void OnKey(unsigned char key, int x, int y)
{
// ESCキー('\x1b')が押された場合にウィンドウを閉じるメッセージを送る
if (key == '\x1b') {
HWND hWnd = ::GetActiveWindow();
if (hWnd != NULL) {
::PostMessage(hWnd, WM_CLOSE, 0, 0);
}
}
}
// コンソールなどの外部 HDC へ直接描画を行う関数
void RenderToDC(HWND hWnd, HDC hDC)
{
T* pT = static_cast<T*>(this);
pT->OnDrawGdi(hWnd, hDC);
}
// GUI ウィンドウの作成と表示
BOOL CreateGdiWindow(const TCHAR* title, int x = 100, int y = 100, int w = 600, int h = 400)
{
HINSTANCE hInst = ::GetModuleHandle(NULL);
const TCHAR* szClassName = _T("GdiCrtpWindowClass");
WNDCLASS wc;
if (!::GetClassInfo(hInst, szClassName, &wc)) {
::ZeroMemory(&wc, sizeof(wc));
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = StaticWndProc;
wc.hInstance = hInst;
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = szClassName;
::RegisterClass(&wc);
}
RECT rect = { 0, 0, w, h };
::AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
m_hWnd = ::CreateWindow(
szClassName,
title,
WS_OVERLAPPEDWINDOW,
x, y,
rect.right - rect.left,
rect.bottom - rect.top,
NULL,
NULL,
hInst,
this
);
if (m_hWnd != NULL) {
::ShowWindow(m_hWnd, SW_SHOW);
::UpdateWindow(m_hWnd);
return TRUE;
}
return FALSE;
}
// メッセージループの実行
void MainLoop()
{
MSG msg;
while (::GetMessage(&msg, NULL, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
}
// 静的ウィンドウプロシージャから呼び出すメッセージハンドラ
LRESULT HandleMessage(UINT msg, WPARAM wp, LPARAM lp)
{
T* pT = static_cast<T*>(this);
switch (msg) {
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hDC = ::BeginPaint(m_hWnd, &ps);
pT->OnDrawGdi(m_hWnd, hDC);
::EndPaint(m_hWnd, &ps);
}
return 0;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
{
int button = (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP) ? 0 : 1;
int state = (msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN) ? 0 : 1;
pT->OnMouse(button, state, LOWORD(lp), HIWORD(lp));
}
return 0;
case WM_CHAR:
{
POINT pt;
::GetCursorPos(&pt);
::ScreenToClient(m_hWnd, &pt);
pT->OnKey((unsigned char)wp, pt.x, pt.y);
}
return 0;
case WM_DESTROY:
m_hWnd = NULL;
::PostQuitMessage(0);
return 0;
}
return ::DefWindowProc(m_hWnd, msg, wp, lp);
}
private:
static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp)
{
T* pSelf = NULL;
if (msg == WM_NCCREATE) {
LPCREATESTRUCT lpcs = (LPCREATESTRUCT)lp;
pSelf = (T*)lpcs->lpCreateParams;
pSelf->m_hWnd = hWnd;
::SetWindowLong(hWnd, GWL_USERDATA, (LONG)pSelf);
}
else {
pSelf = (T*)::GetWindowLong(hWnd, GWL_USERDATA);
}
if (pSelf != NULL) {
return pSelf->HandleMessage(msg, wp, lp);
}
return ::DefWindowProc(hWnd, msg, wp, lp);
}
};
// **************************************************************************
// デフォルト動作
// 作成日 : 2026-08-20
// **************************************************************************
class CGdiDefaultWindow : public CGdiWindowCRTP<CGdiDefaultWindow>
{
};
応用例 : ドロップ機能 Mixin と EMF ビューアの実装
GDI_CRTP.inc は変更せず、ファイルドロップ解析を担当する Mixin クラス CDropTargetMixin<T> を用意し、派生クラスで多重継承します。
#include "GDI_CRTP.inc"
/////////////////////////////////////////////////////////////////////////////
template <class T>
class CDropTargetMixin
{
public:
// ドロップされたファイルを解析して派生クラスの通知関数を呼ぶ
LRESULT ProcessDropFiles(HDROP hDrop)
{
T* pT = static_cast<T*>(this);
TCHAR szPath[MAX_PATH];
// ドロップされた最初のファイルのパスを取得
UINT count = ::DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
if (count > 0) {
::DragQueryFile(hDrop, 0, szPath, MAX_PATH);
// 派生クラスのファイル受け取り処理を呼び出す
pT->OnFileDropped(szPath);
}
::DragFinish(hDrop);
return 0;
}
} ;
/////////////////////////////////////////////////////////////////////////////
class CEmfViewerWindow
: public CGdiWindowCRTP <CEmfViewerWindow>,
public CDropTargetMixin<CEmfViewerWindow>
{
private:
HENHMETAFILE m_hEmf;
public:
CEmfViewerWindow() : m_hEmf(NULL) {}
~CEmfViewerWindow()
{
if (m_hEmf != NULL) {
::DeleteEnhMetaFile(m_hEmf);
m_hEmf = NULL;
}
}
// Mixin (CDropTargetMixin) からファイルパスが渡される
void OnFileDropped(const TCHAR* szPath)
{
// 既存の EMF を解放
if (m_hEmf != NULL) {
::DeleteEnhMetaFile(m_hEmf);
m_hEmf = NULL;
}
// Qiita 記事の手法:GetEnhMetaFile で読み込み
m_hEmf = ::GetEnhMetaFile(szPath);
if (m_hEmf != NULL) {
std::cout << "EMF Loaded: " << szPath << std::endl;
// クライアント領域の再描画を要求
::InvalidateRect(m_hWnd, NULL, TRUE);
}
else {
std::cout << "Failed to load EMF: " << szPath << std::endl;
}
}
// CGdiWindowCRTP から呼び出される描画イベント
void OnDrawGdi(HWND hWnd, HDC hDC)
{
if (m_hEmf == NULL) {
return;
}
RECT rc;
::GetClientRect(hWnd, &rc);
// Qiita 記事の手法:PlayEnhMetaFile で描画領域(RECT)に合わせて描画
::PlayEnhMetaFile(hDC, m_hEmf, &rc);
}
// メッセージルーティング(WM_CREATE と WM_DROPFILES をフック)
LRESULT HandleMessage(UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_CREATE:
::DragAcceptFiles(m_hWnd, TRUE);
return 0;
case WM_DROPFILES:
return ProcessDropFiles((HDROP)wp);
}
return CGdiWindowCRTP<CEmfViewerWindow>::HandleMessage(msg, wp, lp);
}
} ;
/////////////////////////////////////////////////////////////////////////////
int main(void)
{
std::cout << "--- EMF Viewer (CRTP/GDI) ---" << std::endl;
std::cout << "Drag & Drop an EMF file onto the window." << std::endl;
CEmfViewerWindow wnd;
if (wnd.CreateGdiWindow(_T("EMF Viewer - Drag & Drop"))) {
wnd.MainLoop();
}
return 0;
}
詳細解説と設計のポイント
全コードの設計の背景および GDI_CRTP.inc の詳細な解説については、自ブログ記事をご参照ください:
GDI CRTP :インクルード 1 枚で完結する CRTP 版 GDI フレームワーク - IwaoDev
※ 本記事の構成および文章の作成には、生成 AI を利用しています。
