Compartilhar via


Quadro de janela personalizado usando DWM

Este tópico demonstra como usar as APIs do DWM (Gerenciador de Janelas da Área de Trabalho) para criar quadros de janela personalizados para seu aplicativo.

Introdução

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

Um recurso das APIs de DWM é a capacidade de estender o quadro do aplicativo para a área do cliente. Isso permite integrar um elemento de interface do usuário do cliente, como uma barra de ferramentas, ao quadro, dando aos controles de 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 ao quadro da janela estendendo a parte superior do quadro, conforme mostrado na captura de tela a seguir.

barra de navegação integrada ao quadro da janela.

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

botão 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 junto com os valores de inset de margem para DwmExtendFrameIntoClientArea. Os valores de inset de margem determinam até que ponto 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 na mensagem WM_ACTIVATE em vez da mensagem de WM_CREATE . Isso garante que a extensão de quadro seja tratada corretamente quando a janela estiver em seu tamanho padrão e quando for maximizada.

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

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

A diferença visual entre essas duas janelas é muito sutil. A única diferença entre os dois é que a borda fina da linha preta da região do cliente na janela à esquerda está ausente da janela à direita. O motivo dessa borda ausente é que ela é incorporada ao quadro estendido, mas o restante da área do cliente não é. Para que os quadros estendidos fiquem 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 da tela de fundo não tem o valor alfa definido como 0, portanto, o restante do quadro estendido não está visível.

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

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

Removendo o quadro padrão

Depois de estender o quadro do aplicativo e visível, você pode remover o quadro padrão. Remover o 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 for TRUE e o valor retornado for 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 de WM_NCCALCSIZE não ficam visíveis até que a região do cliente precise ser redimensionada. Até esse momento, a exibição inicial da janela aparece com o quadro padrão e bordas estendidas. Para superar isso, você deve redimensionar sua janela ou executar uma ação que inicie um WM_NCCALCSIZE mensagem 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 a seguir demonstra uma chamada para SetWindowPos que força uma mensagem WM_NCCALCSIZE a ser enviada usando os atributos de retângulo da janela atual e o sinalizador 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 um quadro personalizado (à direita)

Desenhando na janela quadro estendido

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ê deve desenhá-los por conta própria. Para fazer isso, primeiro examine a alteração que ocorreu na área do cliente.

Com a remoção do quadro padrão, sua área de cliente agora consiste em toda a janela, incluindo o quadro estendido. Isso inclui a região em que 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 estendido (à direita), a área do cliente é toda a janela.

captura de tela de uma área de cliente realçada em vermelho no quadro padrão e personalizado

Como toda a janela é sua área de cliente, você pode simplesmente desenhar o que quiser no quadro estendido. Para adicionar um título ao seu aplicativo, basta desenhar texto na região apropriada. A imagem a seguir mostra o texto com tema 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 de interface do usuário. Como a janela inteira é a região do cliente, você deve ajustar o posicionamento do controle de interface do usuário para cada largura de quadro se não quiser que elas apareçam no quadro estendido ou no quadro estendido.

 

Habilitando o teste de clique para o quadro personalizado

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

Para legenda teste de clique de botão, o DWM fornece a função DwmDefWindowProc. Para pressionar corretamente os botões de legenda em cenários de quadro personalizados, as mensagens devem primeiro ser passadas para DwmDefWindowProc para tratamento. DwmDefWindowProc retornará TRUE se uma mensagem for tratada e FALSE se não for. Se a mensagem não for tratada por DwmDefWindowProc, seu aplicativo deverá manipular a mensagem em si ou passar a mensagem para DefWindowProc.

Para redimensionamento e movimentação de quadros, seu aplicativo deve fornecer a lógica de teste de clique e manipular mensagens de teste de clique de quadro. As mensagens de teste de clique 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 lidar com a mensagem de WM_NCHITTEST quando DwmDefWindowProc não a manipula. Para ver o código da função chamada HitTestNCA , consulte 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 de janela de exemplo

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 legenda no quadro estendido. Essa função deve ser chamada de dentro das chamadas 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 HitTestNCA função usada em Habilitando o Teste de Clique para o Quadro Personalizado. Essa função manipula a lógica de teste de clique 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 Gerenciador de Janelas da Área de Trabalho