DWM を使用したカスタム ウィンドウ フレーム
このトピックでは、デスクトップ ウィンドウ マネージャー (DWM) API を使用して、アプリケーションのカスタム ウィンドウ フレームを作成する方法について説明します。
- はじめに
- クライアント フレームの拡張
- 標準フレームの削除
- 拡張フレーム ウィンドウでの描画
- カスタム フレームのヒット テストを有効にする
- 付録 A: サンプル ウィンドウ プロシージャ
- 付録 B: キャプション タイトルの塗りつぶし
- 付録 C: HitTestNCA 関数
- 関連トピック
Windows Vista 以降では、アプリケーション ウィンドウのクライアント以外の領域 (タイトル バー、アイコン、ウィンドウの境界線、キャプション ボタン) の外観は DWM によって制御されます。 DWM API を使用すると、DWM がウィンドウのフレームをレンダリングする方法を変更できます。
DWM API の 1 つの機能は、アプリケーション フレームをクライアント領域に拡張できることです。 これにより、ツール バーなどのクライアント UI 要素をフレームに統合して、UI コントロールをアプリケーション UI のより目立つ場所にすることができます。 たとえば、Windows Vista の Windows Internet エクスプローラー 7 では、次のスクリーン ショットに示すように、フレームの上部を拡張してナビゲーション バーをウィンドウ フレームに統合します。
ウィンドウ フレームを拡張する機能を使用すると、ウィンドウの外観を維持しながらカスタム フレームを作成することもできます。 たとえば、Microsoft Office Word 2007 では、次のスクリーン ショットに示すように、標準の [最小化]、[最大化]、[閉じる] キャプション ボタンを提供しながら、カスタム フレーム内に Office ボタンとクイック アクセス ツール バーを描画します。
フレームをクライアント領域に拡張する機能は、 DwmExtendFrameIntoClientArea 関数によって公開されます。 フレームを拡張するには、ターゲット ウィンドウのハンドルと余白の inset 値を DwmExtendFrameIntoClientArea に渡します。 余白の差し込み値は、ウィンドウの 4 辺でフレームをどの程度延長するかを決定します。
次のコードは、 DwmExtendFrameIntoClientArea を使用してフレームを拡張する方法を示しています。
// Handle the window activation.
if (message == WM_ACTIVATE)
{
// Extend the frame into the client area.
MARGINS margins;
margins.cxLeftWidth = LEFTEXTENDWIDTH; // 8
margins.cxRightWidth = RIGHTEXTENDWIDTH; // 8
margins.cyBottomHeight = BOTTOMEXTENDWIDTH; // 20
margins.cyTopHeight = TOPEXTENDWIDTH; // 27
hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
if (!SUCCEEDED(hr))
{
// Handle the error.
}
fCallDWP = true;
lRet = 0;
}
フレーム拡張機能は、WM_CREATE メッセージではなく 、WM_ACTIVATEメッセージ内 で行われることに注意してください。 これにより、ウィンドウの既定のサイズと最大化時に、フレーム拡張機能が適切に処理されるようになります。
次の図は、標準のウィンドウ フレーム (左側) と同じウィンドウ フレームが拡張されている (右側) を示しています。 フレームは、前のコード例と既定の Microsoft Visual Studio WNDCLASS WNDCLASSEX/ 背景 (COLOR_WINDOW +1) を使用して拡張されます。
この 2 つのウィンドウの視覚的な違いは非常に微妙です。 2 つの唯一の違いは、左側のウィンドウのクライアント領域の細い黒い線の境界線が右側のウィンドウに表示されていないことです。 この不足している境界の理由は、拡張フレームに組み込まれているが、クライアント領域の残りの部分は組み込まれていないためです。 拡張フレームを表示するには、拡張フレームの各辺の基になる領域に、アルファ値が 0 のピクセル データが必要です。 クライアント領域の周囲の黒い境界線には、すべての色の値 (赤、緑、青、アルファ) が 0 に設定されているピクセル データがあります。 背景の残りの部分にはアルファ値が 0 に設定されていないため、拡張フレームの残りの部分は表示されません。
拡張フレームを確実に表示する最も簡単な方法は、クライアント領域全体を黒に塗りつぶす方法です。 これを実現するには、WNDCLASS または WNDCLASSEX 構造体の hbrBackground メンバーをストック BLACK_BRUSHのハンドルに初期化します。 次の図は、前に示したのと同じ標準フレーム (左) と拡張フレーム (右) を示しています。 ただし、今回は 、hbrBackground が GetStockObject 関数から取得したBLACK_BRUSH ハンドルに設定されます。
アプリケーションのフレームを拡張して表示したら、標準フレームを削除できます。 標準フレームを削除すると、単に標準フレームを拡張するのではなく、フレームの両側の幅を制御できます。
標準ウィンドウ フレームを削除するには、 WM_NCCALCSIZE メッセージを処理する必要があります。具体的には、 wParam 値が TRUE で、戻り値が 0 の場合です。 これにより、アプリケーションはウィンドウ領域全体をクライアント領域として使用し、標準フレームを削除します。
WM_NCCALCSIZE メッセージを処理した結果は、クライアント領域のサイズを変更する必要があるまで表示されません。 それまでは、ウィンドウの初期ビューが標準フレームと拡張境界線と共に表示されます。 これを解決するには、ウィンドウのサイズを変更するか、ウィンドウの作成時に WM_NCCALCSIZE メッセージを開始するアクションを実行する必要があります。 これは、 SetWindowPos 関数を使用してウィンドウを移動し、サイズを変更することで実現できます。 次のコードは、現在のウィンドウ四角形属性と SWP_FRAMECHANGED フラグを使用して、WM_NCCALCSIZE メッセージを強制的に送信する SetWindowPos の呼び出しを示しています。
// Handle window creation.
if (message == WM_CREATE)
{
RECT rcClient;
GetWindowRect(hWnd, &rcClient);
// Inform the application of the frame change.
SetWindowPos(hWnd,
NULL,
rcClient.left, rcClient.top,
RECTWIDTH(rcClient), RECTHEIGHT(rcClient),
SWP_FRAMECHANGED);
fCallDWP = true;
lRet = 0;
}
次の図は、標準フレーム (左) と、標準フレーム (右) のない新しく拡張されたフレームを示しています。
標準フレームを削除すると、アプリケーション アイコンとタイトルの自動描画が失われます。 これらをアプリケーションに追加し直すには、自分で描画する必要があります。 これを行うには、まず、クライアント領域に発生した変更を確認します。
標準フレームを削除すると、クライアント領域は拡張フレームを含むウィンドウ全体で構成されるようになりました。 これには、キャプション ボタンが描画される領域が含まれます。 次のサイド バイ サイド比較では、標準フレームとカスタム拡張フレームの両方のクライアント領域が赤で強調表示されています。 標準フレーム ウィンドウ (左) のクライアント領域は黒の領域です。 拡張フレーム ウィンドウ (右) では、クライアント領域はウィンドウ全体です。
ウィンドウ全体がクライアント領域であるため、拡張フレームに必要なものを描画できます。 アプリケーションにタイトルを追加するには、適切な領域にテキストを描画するだけです。 次の図は、カスタム キャプション フレームに描画されたテーマ付きのテキストを示しています。 タイトルは DrawThemeTextEx 関数を使用して描画されます。 タイトルを描画するコードを表示するには、「 付録 B: キャプション タイトルの描画」を参照してください。
注意
カスタム フレームで描画する場合は、UI コントロールを配置するときに注意してください。 ウィンドウ全体がクライアント領域であるため、拡張フレーム上または拡張フレームに表示しない場合は、フレーム幅ごとに UI コントロールの配置を調整する必要があります。
標準フレームを削除した場合の副作用は、既定のサイズ変更と移動動作の損失です。 アプリケーションで標準ウィンドウの動作を適切にエミュレートするには、ボタン ヒット テストとフレームのサイズ変更/移動キャプション処理するロジックを実装する必要があります。
ボタン ヒット テストキャプション、DWM には DwmDefWindowProc 関数が用意されています。 カスタム フレーム シナリオでキャプション ボタンを適切にヒット テストするには、最初に処理のためにメッセージを DwmDefWindowProc に渡す必要があります。 DwmDefWindowProc は、メッセージが処理される場合は TRUE を 返し、処理されない場合は FALSE を 返します。 メッセージが DwmDefWindowProc によって処理されない場合は、アプリケーションでメッセージ自体を処理するか、DefWindowProc にメッセージを渡す必要があります。
フレームのサイズ変更と移動を行うには、アプリケーションでヒット テスト ロジックを提供し、フレーム ヒット テスト メッセージを処理する必要があります。 フレーム ヒット テスト メッセージは、アプリケーションが標準フレームなしでカスタム フレームを作成した場合でも、 WM_NCHITTEST メッセージを介して送信されます。 次のコードは、DwmDefWindowProc が処理しない場合のWM_NCHITTEST メッセージの処理を示しています。 呼び出された HitTestNCA
関数のコードについては、「 付録 C: HitTestNCA 関数」を参照してください。
// Handle hit testing in the NCA if not handled by DwmDefWindowProc.
if ((message == WM_NCHITTEST) && (lRet == 0))
{
lRet = HitTestNCA(hWnd, wParam, lParam);
if (lRet != HTNOWHERE)
{
fCallDWP = false;
}
}
次のコード サンプルは、カスタム フレーム アプリケーションの作成に使用されるウィンドウ プロシージャとそのサポート ワーカー関数を示しています。
//
// Main WinProc.
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
bool fCallDWP = true;
BOOL fDwmEnabled = FALSE;
LRESULT lRet = 0;
HRESULT hr = S_OK;
// Winproc worker for custom frame issues.
hr = DwmIsCompositionEnabled(&fDwmEnabled);
if (SUCCEEDED(hr))
{
lRet = CustomCaptionProc(hWnd, message, wParam, lParam, &fCallDWP);
}
// Winproc worker for the rest of the application.
if (fCallDWP)
{
lRet = AppWinProc(hWnd, message, wParam, lParam);
}
return lRet;
}
//
// Message handler for handling the custom caption messages.
//
LRESULT CustomCaptionProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, bool* pfCallDWP)
{
LRESULT lRet = 0;
HRESULT hr = S_OK;
bool fCallDWP = true; // Pass on to DefWindowProc?
fCallDWP = !DwmDefWindowProc(hWnd, message, wParam, lParam, &lRet);
// Handle window creation.
if (message == WM_CREATE)
{
RECT rcClient;
GetWindowRect(hWnd, &rcClient);
// Inform application of the frame change.
SetWindowPos(hWnd,
NULL,
rcClient.left, rcClient.top,
RECTWIDTH(rcClient), RECTHEIGHT(rcClient),
SWP_FRAMECHANGED);
fCallDWP = true;
lRet = 0;
}
// Handle window activation.
if (message == WM_ACTIVATE)
{
// Extend the frame into the client area.
MARGINS margins;
margins.cxLeftWidth = LEFTEXTENDWIDTH; // 8
margins.cxRightWidth = RIGHTEXTENDWIDTH; // 8
margins.cyBottomHeight = BOTTOMEXTENDWIDTH; // 20
margins.cyTopHeight = TOPEXTENDWIDTH; // 27
hr = DwmExtendFrameIntoClientArea(hWnd, &margins);
if (!SUCCEEDED(hr))
{
// Handle error.
}
fCallDWP = true;
lRet = 0;
}
if (message == WM_PAINT)
{
HDC hdc;
{
PAINTSTRUCT ps;
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
EndPaint(hWnd, &ps);
}
fCallDWP = true;
lRet = 0;
}
// Handle the non-client size message.
if ((message == WM_NCCALCSIZE) && (wParam == TRUE))
{
// Calculate new NCCALCSIZE_PARAMS based on custom NCA inset.
NCCALCSIZE_PARAMS *pncsp = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0;
pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0;
pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0;
lRet = 0;
// No need to pass the message on to the DefWindowProc.
fCallDWP = false;
}
// Handle hit testing in the NCA if not handled by DwmDefWindowProc.
if ((message == WM_NCHITTEST) && (lRet == 0))
{
lRet = HitTestNCA(hWnd, wParam, lParam);
if (lRet != HTNOWHERE)
{
fCallDWP = false;
}
}
*pfCallDWP = fCallDWP;
return lRet;
}
//
// Message handler for the application.
//
LRESULT AppWinProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
HRESULT hr;
LRESULT result = 0;
switch (message)
{
case WM_CREATE:
{}
break;
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
PaintCustomCaption(hWnd, hdc);
// Add any drawing code here...
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
次のコードは、拡張フレームにキャプションタイトルを描画する方法を示しています。 この関数は、 BeginPaint 呼び出しと EndPaint 呼び出し内から呼び出す必要があります。
// Paint the title on the custom frame.
void PaintCustomCaption(HWND hWnd, HDC hdc)
{
RECT rcClient;
GetClientRect(hWnd, &rcClient);
HTHEME hTheme = OpenThemeData(NULL, L"CompositedWindow::Window");
if (hTheme)
{
HDC hdcPaint = CreateCompatibleDC(hdc);
if (hdcPaint)
{
int cx = RECTWIDTH(rcClient);
int cy = RECTHEIGHT(rcClient);
// Define the BITMAPINFO structure used to draw text.
// Note that biHeight is negative. This is done because
// DrawThemeTextEx() needs the bitmap to be in top-to-bottom
// order.
BITMAPINFO dib = { 0 };
dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
dib.bmiHeader.biWidth = cx;
dib.bmiHeader.biHeight = -cy;
dib.bmiHeader.biPlanes = 1;
dib.bmiHeader.biBitCount = BIT_COUNT;
dib.bmiHeader.biCompression = BI_RGB;
HBITMAP hbm = CreateDIBSection(hdc, &dib, DIB_RGB_COLORS, NULL, NULL, 0);
if (hbm)
{
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcPaint, hbm);
// Setup the theme drawing options.
DTTOPTS DttOpts = {sizeof(DTTOPTS)};
DttOpts.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE;
DttOpts.iGlowSize = 15;
// Select a font.
LOGFONT lgFont;
HFONT hFontOld = NULL;
if (SUCCEEDED(GetThemeSysFont(hTheme, TMT_CAPTIONFONT, &lgFont)))
{
HFONT hFont = CreateFontIndirect(&lgFont);
hFontOld = (HFONT) SelectObject(hdcPaint, hFont);
}
// Draw the title.
RECT rcPaint = rcClient;
rcPaint.top += 8;
rcPaint.right -= 125;
rcPaint.left += 8;
rcPaint.bottom = 50;
DrawThemeTextEx(hTheme,
hdcPaint,
0, 0,
szTitle,
-1,
DT_LEFT | DT_WORD_ELLIPSIS,
&rcPaint,
&DttOpts);
// Blit text to the frame.
BitBlt(hdc, 0, 0, cx, cy, hdcPaint, 0, 0, SRCCOPY);
SelectObject(hdcPaint, hbmOld);
if (hFontOld)
{
SelectObject(hdcPaint, hFontOld);
}
DeleteObject(hbm);
}
DeleteDC(hdcPaint);
}
CloseThemeData(hTheme);
}
}
次のコードは、カスタム フレームのHitTestNCA
ヒット テストの有効化で使用される関数を示しています。 この関数は、DwmDefWindowProc がメッセージを処理しない場合に、WM_NCHITTESTのヒット テスト ロジックを処理します。
// Hit test the frame for resizing and moving.
LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
// Get the point coordinates for the hit test.
POINT ptMouse = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
// Get the window rectangle.
RECT rcWindow;
GetWindowRect(hWnd, &rcWindow);
// Get the frame rectangle, adjusted for the style without a caption.
RECT rcFrame = { 0 };
AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
// Determine if the hit test is for resizing. Default middle (1,1).
USHORT uRow = 1;
USHORT uCol = 1;
bool fOnResizeBorder = false;
// Determine if the point is at the top or bottom of the window.
if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + TOPEXTENDWIDTH)
{
fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
uRow = 0;
}
else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - BOTTOMEXTENDWIDTH)
{
uRow = 2;
}
// Determine if the point is at the left or right of the window.
if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + LEFTEXTENDWIDTH)
{
uCol = 0; // left side
}
else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - RIGHTEXTENDWIDTH)
{
uCol = 2; // right side
}
// Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT)
LRESULT hitTests[3][3] =
{
{ HTTOPLEFT, fOnResizeBorder ? HTTOP : HTCAPTION, HTTOPRIGHT },
{ HTLEFT, HTNOWHERE, HTRIGHT },
{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
};
return hitTests[uRow][uCol];
}