Поделиться через


Настраиваемый кадр окна с помощью DWM

В этом разделе показано, как использовать API диспетчера окон рабочего стола (DWM) для создания пользовательских кадров окон для приложения.

Знакомство

В Windows Vista и более поздних версиях внешний вид не клиентских областей окон приложений (строка заголовка, значок, граница окна и кнопки заголовка) управляется DWM. Используя API DWM, вы можете изменить способ отрисовки кадра окна в DWM.

Одной из функций API DWM является возможность расширения кадра приложения в клиентской области. Это позволяет интегрировать компонент пользовательского интерфейса клиента (например, панель инструментов) в структуру, обеспечивая элементам управления пользовательского интерфейса более заметное место в интерфейсе приложения. Например, Windows Internet Explorer 7 в Windows Vista интегрирует панель навигации в рамку окна, расширив верхнюю часть кадра, как показано на следующем снимке экрана.

панель навигации, встроенная в рамку окна.

Возможность увеличения размеров рамки окна также позволяет создавать пользовательские рамки, сохраняя внешний вид окна. Например, Microsoft Office Word 2007 отображает кнопку Office и панель быстрого доступа внутри настраиваемой рамки, предоставляя стандартные кнопки "Свернуть", "Развернуть" и "Закрыть", как показано на следующем снимке экрана.

кнопка Office и панель быстрого доступа в Word 2007

Расширение клиентской рамки

Функциональные возможности расширения кадра в клиентской области предоставляются функцией 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. Это гарантирует, что расширение фрейма обрабатывается правильно, когда окно находится в своем обычном размере и когда оно развернуто на весь экран.

На следующем рисунке показан стандартный фрейм окна (слева) и та же рамка окна, расширенная (справа). Кадр расширяется с помощью предыдущего примера кода и фона WNDCLASS /WNDCLASSEX по умолчанию (COLOR_WINDOW +1).

снимок экрана стандартного (слева) и расширенного кадра (справа) с белым фоном

Визуальное различие между этими двумя окнами очень тонкое. Единственное различие между двумя окнами заключается в том, что в окне справа отсутствует тонкая черная граница клиентской области, которая есть в окне слева. Причина отсутствия границы заключается в том, что она включена в расширенный кадр, но остальная часть клиентской области не включена. Чтобы расширенные кадры были видимыми, регионы, лежащие в каждой из сторон расширенного кадра, должны иметь пиксельные данные с альфа-значением 0. Черная граница вокруг клиентского региона содержит пиксельные данные, в которых для всех значений цвета (красный, зеленый, синий и альфа-цвет) задано значение 0. Остальная часть фона не имеет альфа-значения, равного 0, поэтому остальная часть расширенного кадра не отображается.

Самый простой способ убедиться, что расширенные кадры видимы, заключается в том, чтобы покрасить весь клиентский регион черным. Для этого инициализируйте член hbrBackground в вашей структуре WNDCLASS или WNDCLASSEX до дескриптора штатной кисти BLACK_BRUSH. На следующем рисунке показан тот же стандартный кадр (слева) и расширенный кадр (справа). Однако на этот раз hbrBackground задается для дескриптора BLACK_BRUSH, полученного из функции GetStockObject.

снимок экрана стандартного (слева) и расширенного кадра (справа) с черным фоном

Удаление стандартного кадра

После расширения кадра приложения и его видимости можно удалить стандартный кадр. Удаление стандартного кадра позволяет контролировать ширину каждой стороны кадра, а не просто расширить стандартный кадр.

Чтобы удалить стандартный кадр окна, необходимо обработать сообщение WM_NCCALCSIZE, в частности, если значение wParam равно TRUE, а возвращаемое значение равно 0. При этом приложение использует весь регион окна в качестве клиентской области, удаляя стандартный кадр.

Результаты обработки сообщения WM_NCCALCSIZE не отображаются, пока не потребуется изменить размер клиентского региона. До этого времени начальное представление окна отображается со стандартными рамками и расширенными границами. Чтобы преодолеть это, необходимо изменить размер окна или выполнить действие, которое инициирует WM_NCCALCSIZE сообщение во время создания окна. Это можно сделать с помощью функции SetWindowPos, чтобы переместить окно и изменить его размер. Следующий код демонстрирует вызов функции SetWindowPos, который заставляет отправить сообщение WM_NCCALCSIZE, используя текущие атрибуты прямоугольника окна и флаг SWP_FRAMECHANGED.

// 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: Рисование заголовка.

снимок экрана пользовательского кадра с заголовком

Заметка

При рисовании в собственной рамке будьте осторожны при размещении элементов управления пользовательского интерфейса. Поскольку всё окно — это область клиента, необходимо отрегулировать расположение элементов управления интерфейса в зависимости от ширины кадра, если вы не хотите, чтобы они отображались на или в расширенном кадре.

 

Включение тестирования на попадание для пользовательской рамки

Побочным эффектом удаления стандартного кадра является потеря размера и перемещения по умолчанию. Чтобы приложение правильно эмулировало стандартное поведение окна, необходимо реализовать логику для обработки нажатия кнопки заголовка и изменения размера кадра.

Для проверки нажатия кнопки заголовка DWM предоставляет функцию DwmDefWindowProc. Чтобы правильно проверить кнопки заголовка в пользовательских сценариях кадров, сообщения должны сначала передаваться в DwmDefWindowProc для обработки. DwmDefWindowProc возвращает TRUE, если сообщение обрабатывается, и FALSE, если не обрабатывается. Если сообщение не обрабатывается через DwmDefWindowProc, приложение должно обработать его самостоятельно или передать в DefWindowProc.

Для изменения размера и перемещения кадра ваше приложение должно предоставить логику определения попадания и обрабатывать сообщения касания кадра. Сообщения определения попадания в кадр отправляются вам через сообщение WM_NCHITTEST, даже если ваше приложение создает настраиваемую рамку без стандартной рамки. Следующий код демонстрирует обработку сообщения WM_NCHITTEST, если DwmDefWindowProc не обрабатывает его. Чтобы увидеть код вызываемой функции 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. Рисование заголовка текста

В следующем коде показано, как зарисовать заголовок на расширенном кадре. Эта функция должна вызываться в вызовах 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);
    }
}

Приложение C. Функция HitTestNCA

В следующем коде показана функция HitTestNCA, используемая в включение тестирования попаданий для пользовательского кадра. Эта функция обрабатывает логику тестирования попаданий для WM_NCHITTEST, если DwmDefWindowProc не обрабатывает сообщение.

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

Обзор диспетчера окон рабочего стола