使用 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 按钮和快速访问工具栏,同时提供标准的最小化、最大化和关闭描述文字按钮,如以下屏幕截图所示。

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_ACTIVATE 消息而不是 WM_CREATE 消息中完成的。 这可确保在窗口处于其默认大小且最大化时正确处理帧扩展。

下图显示了左侧) 的标准窗口框架 (,右侧) 上扩展了同一窗口框架 (。 框架使用前面的代码示例和默认的 Microsoft Visual Studio WNDCLASS/WNDCLASS WNDCLASSEX 背景 (COLOR_WINDOW +1) 进行扩展。

带有白色背景的标准 (左) 和扩展框架 (右) 的屏幕截图

这两个窗口之间的视觉差异非常微妙。 两者的唯一区别在于,左侧窗口中客户端区域的黑色细线边框在右侧的窗口中缺失。 缺少此边框的原因是它已合并到扩展框架中,但工作区的其余部分则不是。 要使扩展帧可见,每个扩展帧侧的底层区域必须具有 alpha 值为 0 的像素数据。 客户端区域周围的黑色边框具有像素数据,其中 (红色、绿色、蓝色和 alpha) 的所有颜色值都设置为 0。 背景的其余部分没有将 alpha 值设置为 0,因此扩展帧的其余部分不可见。

确保扩展帧可见的最简单方法是将整个客户端区域绘制为黑色。 为此,请将 WNDCLASSWNDCLASSEX 结构的 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;
    }
}

附录 A:示例窗口过程

下面的代码示例演示了一个窗口过程及其用于创建自定义框架应用程序的支持工作器函数。

//
//  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;
}

附录 B:绘制标题

以下代码演示如何在扩展框架上绘制描述文字标题。 必须在 BeginPaintEndPaint 调用中调用此函数。

// 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);
    }
}

附录 C:HitTestNCA 函数

以下代码演示为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];
}

桌面窗口管理器概述