Partilhar via


Moldura de janela personalizada com o uso de DWM

Este tópico demonstra como usar as APIs do Desktop Window Manager (DWM) para criar quadros de janela personalizados para seu aplicativo.

Introdução

No Windows Vista e versões posteriores, a aparência das áreas não clientes das janelas do aplicativo (a barra de título, o ícone, a borda da janela e os botões de legenda) é controlada pelo DWM. Usando as APIs DWM, você pode alterar a maneira como o DWM processa o quadro de uma janela.

Um recurso das APIs DWM é a capacidade de estender o quadro do aplicativo para a área do cliente. Isso permite que você integre um elemento da interface do usuário do cliente, como uma barra de ferramentas, no quadro, dando aos controles da interface do usuário um lugar mais proeminente na interface do usuário do aplicativo. Por exemplo, o Windows Internet Explorer 7 no Windows Vista integra a barra de navegação no quadro da janela estendendo a parte superior do quadro, conforme mostrado na captura de tela a seguir.

barra de navegação integrada na moldura da janela.

A capacidade de estender a moldura da janela também permite que você crie molduras personalizadas, mantendo a aparência da janela. Por exemplo, o Microsoft Office Word 2007 desenha o botão Office e a barra de ferramentas Acesso Rápido dentro do quadro personalizado enquanto fornece os botões padrão Minimizar, Maximizar e Fechar legenda, conforme mostrado na captura de tela a seguir.

botão do Office e barra de ferramentas de acesso rápido no Word 2007

Estendendo o quadro do cliente

A funcionalidade para estender o quadro para a área do cliente é exposta pela função DwmExtendFrameIntoClientArea. Para estender o quadro, passe o identificador da janela de destino juntamente com os valores de inset de margem para DwmExtendFrameIntoClientArea. Os valores de inserção de margem determinam até onde estender o quadro nos quatro lados da janela.

O código a seguir demonstra o uso de DwmExtendFrameIntoClientArea para estender o quadro.

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

Observe que a extensão de quadro é feita dentro da mensagem WM_ACTIVATE em vez da mensagem WM_CREATE. Isso garante que a extensão do quadro seja tratada corretamente quando a janela estiver em seu tamanho padrão e quando for maximizada.

A imagem a seguir mostra uma moldura de janela padrão (à esquerda) e a mesma moldura de janela estendida (à direita). O quadro é estendido usando o exemplo de código anterior e o padrão Microsoft Visual Studio WNDCLASS/WNDCLASSEX plano de fundo (COLOR_WINDOW +1).

captura de tela de um quadro padrão (à esquerda) e estendido (à direita) com fundo branco

A diferença visual entre estas duas janelas é muito subtil. A única diferença entre os dois é que a borda fina preta da região do cliente na janela à esquerda não está presente na janela à direita. A razão para essa borda ausente é que ela é incorporada ao quadro estendido, mas o resto da área do cliente não. Para que os quadros estendidos sejam visíveis, as regiões subjacentes a cada um dos lados do quadro estendido devem ter dados de pixel com um valor alfa de 0. A borda preta ao redor da região do cliente tem dados de pixel nos quais todos os valores de cor (vermelho, verde, azul e alfa) são definidos como 0. O restante do plano de fundo não tem o valor alfa definido como 0, portanto, o restante do quadro estendido não é visível.

A maneira mais fácil de garantir que os quadros estendidos sejam visíveis é pintar toda a região do cliente de preto. Para fazer isso, inicialize o hbrBackground membro do seuWNDCLASSou estrutura de WNDCLASSEX para o identificador do BLACK_BRUSH de estoque. A imagem a seguir mostra o mesmo quadro padrão (esquerda) e quadro estendido (direita) mostrados anteriormente. Desta vez, no entanto, hbrBackground é definido como o manipulador BLACK_BRUSH obtido da função GetStockObject.

captura de tela de um quadro padrão (à esquerda) e estendido (à direita) com fundo preto

Removendo o quadro padrão

Depois de estender o quadro do seu aplicativo e torná-lo visível, você pode remover o quadro padrão. A remoção do quadro padrão permite controlar a largura de cada lado do quadro em vez de simplesmente estender o quadro padrão.

Para remover o quadro de janela padrão, você deve manipular a mensagem WM_NCCALCSIZE, especificamente quando seu valor wParam é TRUE e o valor de retorno é 0. Ao fazer isso, seu aplicativo usa toda a região da janela como a área do cliente, removendo o quadro padrão.

Os resultados do tratamento da mensagem WM_NCCALCSIZE não são visíveis até que a região do cliente precise ser redimensionada. Até esse momento, a visão inicial da janela aparece com a moldura padrão e bordas estendidas. Para superar isso, você deve redimensionar sua janela ou executar uma ação que inicia uma mensagem WM_NCCALCSIZE no momento da criação da janela. Isso pode ser feito usando a função SetWindowPos para mover sua janela e redimensioná-la. O código seguinte demonstra uma chamada para SetWindowPos que força o envio de uma mensagem WM_NCCALCSIZE usando os atributos do retângulo da janela atual e o flag 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;
}

A imagem a seguir mostra o quadro padrão (à esquerda) e o quadro recém-estendido sem o quadro padrão (à direita).

captura de tela de um quadro padrão (à esquerda) e de um quadro personalizado (à direita)

Desenho na janela de quadro estendida

Ao remover o quadro padrão, você perde o desenho automático do ícone e do título do aplicativo. Para adicioná-los de volta ao seu aplicativo, você mesmo deve desenhá-los. Para fazer isso, primeiro olhe para a mudança que ocorreu na sua área de cliente.

Com a remoção do quadro padrão, a área do cliente agora consiste em toda a janela, incluindo o quadro estendido. Isso inclui a região onde os botões de legenda são desenhados. Na comparação lado a lado a seguir, a área do cliente para o quadro padrão e o quadro estendido personalizado é realçada em vermelho. A área do cliente para a janela de quadro padrão (à esquerda) é a região preta. Na janela de quadro estendida (à direita), a área de trabalho do cliente ocupa toda a janela.

captura de tela de áreas de cliente realçadas em vermelho em quadros padrão e personalizados

Como toda a janela é a sua área de cliente, você pode simplesmente desenhar o que quiser no quadro estendido. Para adicionar um título ao seu aplicativo, basta desenhar o texto na região apropriada. A imagem a seguir mostra o texto temático desenhado no quadro de legenda personalizado. O título é desenhado usando a função DrawThemeTextEx. Para exibir o código que pinta o título, consulte Apêndice B: Pintando o título da legenda.

captura de tela de um quadro personalizado com título

Observação

Ao desenhar em seu quadro personalizado, tenha cuidado ao colocar controles da interface do usuário. Como toda a janela é a região do cliente, deve-se ajustar a colocação de controlos da interface de utilizador para cada largura de quadro, se não quiser que apareçam no quadro estendido.

 

Habilitando o teste de impacto para o quadro personalizado

Um efeito colateral de remover o quadro padrão é a perda do comportamento padrão de redimensionamento e movimentação. Para que seu aplicativo emule corretamente o comportamento padrão da janela, você precisará implementar lógica para lidar com o teste de toque do botão de legenda e o redimensionamento/movimentação do quadro.

Para o teste de clique no botão de legenda, o DWM fornece a função de DwmDefWindowProc. Para testar adequadamente os botões de legenda em cenários de quadro personalizado, as mensagens devem ser primeiramente passadas para DwmDefWindowProc para tratamento. DwmDefWindowProc retorna TRUE se uma mensagem for manipulada e FALSE se não for. Se a mensagem não for manipulada por DwmDefWindowProc , seu aplicativo deverá manipular a mensagem em si ou passá-la para DefWindowProc.

Para redimensionamento e movimentação de janelas, o seu aplicativo deve fornecer a lógica de teste de colisão e manipular mensagens de teste de colisão de janela. As mensagens de teste de acerto de quadro são enviadas para você por meio da mensagem WM_NCHITTEST, mesmo que seu aplicativo crie um quadro personalizado sem o quadro padrão. O código a seguir demonstra como manipular a mensagem WM_NCHITTEST quando DwmDefWindowProc não a manipula. Para ver o código da função chamada HitTestNCA, consulte o Apêndice C: Função 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;
    }
}

Apêndice A: Procedimento da janela de amostra

O exemplo de código a seguir demonstra um procedimento de janela e suas funções de trabalho de suporte usadas para criar um aplicativo de quadro personalizado.

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

Apêndice B: Pintando o título da legenda

O código a seguir demonstra como pintar um título de legenda no quadro estendido. Essa função deve ser chamada de dentro das chamadas de BeginPaint e 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);
    }
}

Apêndice C: Função HitTestNCA

O código a seguir mostra a função HitTestNCA usada em Habilitando o teste de acerto para ode quadro personalizado. Esta função lida com a lógica de teste de ocorrências para o WM_NCHITTEST quando DwmDefWindowProc não manipula a mensagem.

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

Visão geral do Desktop Window Manager