使用 DWM 的自訂視窗框架
本主題示範如何使用桌面視窗管理員 (DWM) API 來建立應用程式的自訂視窗框架。
在 Windows Vista 和更新版本中,應用程式視窗的非用戶端應用程式區域外觀 (標題列、圖示、視窗框線和標題按鈕) 是由 DWM 控制。 使用 DWM API,您可以變更 DWM 轉譯視窗框架的方式。
DWM API 的其中一項功能是能夠將應用程式框架延伸至工作區。 這可讓您將用戶端 UI 元素,例如工具列整合到框架中,讓 UI 控制項在應用程式 UI 中更具顯著性的位置。 例如,Windows Vista 上的 Windows Internet Explorer 7 會將導覽列整合到視窗框架中,方法是擴充畫面頂端,如下列螢幕擷取畫面所示。
擴充視窗框架的能力也可讓您建立自訂框架,同時維護視窗的外觀和風格。 例如,Microsoft Office Word 2007 會在自訂框架內繪製 Office 按鈕和快速存取工具列,同時提供標準最小化、最大化和關閉標題按鈕,如下列螢幕擷取畫面所示。
將框架延伸至工作區的功能會由 DwmExtendFrameIntoClientArea 函式公開。 若要擴充框架,請將目標視窗的控制碼與邊界內嵌值一起傳遞至 DwmExtendFrameIntoClientArea。 邊界內嵌值會決定在視窗四邊延伸框架的程度。
下列程式碼示範 如何使用 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_ACTI加值稅E 訊息內完成,而不是 WM_CREATE 訊息。 這可確保視窗的預設大小和最大化時,會正確處理框架延伸模組。
下圖顯示左側) 的標準視窗框架 (,以及右側) 上延伸的相同視窗框架 (。 此框架是使用先前的程式碼範例和預設的 Microsoft Visual Studio WNDCLASS WNDCLASSEX/ 背景 (COLOR_WINDOW +1) 來擴充。
這兩個視窗之間的視覺差異非常細微。 這兩者的唯一差異在於左側視窗中的用戶端區域的細黑色線條框線從右側的視窗遺失。 此遺漏框線的原因是它已併入延伸框架中,但其餘的工作區則不是。 若要讓延伸框架可見,每個延伸框架側邊的基礎區域必須有 Alpha 值為 0 的圖元資料。 用戶端區域周圍的黑色框線具有圖元資料,其中所有色彩值 (紅色、綠色、藍色和 Alpha) 設為 0。 背景的其餘部分沒有 Alpha 值設定為 0,因此延伸框架的其餘部分不會顯示。
確保延伸框架可見的最簡單方式是繪製整個用戶端區域黑色。 若要達成此目的,請將WNDCLASS 或 WNDCLASSEX結構的hbrBackground成員初始化為股票BLACK_BRUSH的控制碼。 下圖顯示先前所示的相同標準框架 (左側) 和延伸框架) (右方。 不過,這次 hbrBackground 會設定為從 GetStockObject 函式取得的BLACK_BRUSH控制碼。
擴充應用程式的框架並使其可見之後,您可以移除標準框架。 移除標準框架可讓您控制框架每一側的寬度,而不只是擴充標準框架。
若要移除標準視窗框架,您必須處理 WM_NCCALCSIZE 訊息,特別是當 其 wParam 值為 TRUE 且傳回值為 0 時。 如此一來,您的應用程式會使用整個視窗區域作為工作區,並移除標準框架。
在用戶端區域需要調整大小之前,才會顯示處理 WM_NCCALCSIZE 訊息的結果。 在該時間之前,視窗的初始檢視會以標準框架和延伸框線顯示。 若要克服這個問題,您必須調整視窗的大小,或執行在建立視窗時起始 WM_NCCALCSIZE 訊息的動作。 這可以使用 SetWindowPos 函式來移動視窗並調整其大小來完成。 下列程式碼示範對 SetWindowPos 的呼叫,此呼叫會強制使用目前的視窗矩形屬性和SWP_FRAMECHANGED旗標傳送 WM_NCCALCSIZE 訊息。
// 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];
}