Frame finestra personalizzato con DWM

Questo argomento illustra come usare le API DWM (Desktop Window Manager) per creare frame di finestre personalizzati per l'applicazione.

Introduzione

In Windows Vista e versioni successive, l'aspetto delle aree non client delle finestre dell'applicazione (la barra del titolo, l'icona, il bordo della finestra e i pulsanti didascalia) è controllata dal DWM. Usando le API DWM, è possibile modificare il modo in cui il DWM esegue il rendering del frame di una finestra.

Una funzionalità delle API DWM è la possibilità di estendere il frame dell'applicazione nell'area client. Ciò consente di integrare un elemento dell'interfaccia utente client, ad esempio una barra degli strumenti, nella cornice, offrendo ai controlli dell'interfaccia utente un posto più importante nell'interfaccia utente dell'applicazione. Ad esempio, Windows Internet Explorer 7 in Windows Vista integra la barra di spostamento nella cornice della finestra estendendo la parte superiore della cornice, come illustrato nella schermata seguente.

barra di spostamento integrata nella cornice della finestra.

La possibilità di estendere la cornice della finestra consente anche di creare fotogrammi personalizzati mantenendo l'aspetto e l'aspetto della finestra. Ad esempio, Microsoft Office Word 2007 disegna il pulsante Office e la barra degli strumenti Di accesso rapido all'interno della cornice personalizzata, fornendo i pulsanti Riduci, Ingrandisci e Chiudi didascalia, come illustrato nella schermata seguente.

pulsante office e barra degli strumenti di accesso rapido in word 2007

Estensione del frame client

La funzionalità per estendere il frame nell'area client viene esposta dalla funzione DwmExtendFrameIntoClientArea . Per estendere il frame, passare l'handle della finestra di destinazione insieme ai valori del margine inset a DwmExtendFrameIntoClientArea. I valori del margine inset determinano la distanza per estendere la cornice sui quattro lati della finestra.

Il codice seguente illustra l'uso di DwmExtendFrameIntoClientArea per estendere il frame.

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

Si noti che l'estensione del frame viene eseguita all'interno del messaggio WM_ACTIVATE anziché il messaggio WM_CREATE . Ciò garantisce che l'estensione del frame venga gestita correttamente quando la finestra è alle dimensioni predefinite e quando viene ingrandita.

L'immagine seguente mostra una cornice di finestra standard (a sinistra) e la stessa cornice di finestra estesa (a destra). Il frame viene esteso usando l'esempio di codice precedente e lo sfondo WNDCLASS WNDCLASSEX/ predefinito di Microsoft Visual Studio (COLOR_WINDOW +1).

screenshot di una cornice standard (sinistra) e cornice estesa (destra) con sfondo bianco

La differenza visiva tra queste due finestre è molto sottile. L'unica differenza tra i due è che il bordo sottile della linea nera dell'area client nella finestra a sinistra manca dalla finestra a destra. Il motivo per questo bordo mancante è che è incorporato nella cornice estesa, ma il resto dell'area client non è. Affinché i fotogrammi estesi siano visibili, le aree sottostanti ognuno dei lati della cornice estesa deve avere dati pixel con un valore alfa pari a 0. Il bordo nero intorno all'area client include dati pixel in cui tutti i valori di colore (rosso, verde, blu e alfa) sono impostati su 0. Il resto dello sfondo non ha il valore alfa impostato su 0, quindi il resto della cornice estesa non è visibile.

Il modo più semplice per garantire che i fotogrammi estesi siano visibili consiste nel disegnare l'intera area client nera. A tale scopo, inizializzare il membro hbrBackground della struttura WNDCLASS o WNDCLASSEX nell'handle della BLACK_BRUSH di magazzino. L'immagine seguente mostra la stessa cornice standard (a sinistra) e la cornice estesa (destra) illustrata in precedenza. Questa volta, tuttavia, hbrBackground è impostato sull'handle BLACK_BRUSH ottenuto dalla funzione GetStockObject .

screenshot di una cornice standard (sinistra) e cornice estesa (destra) con sfondo nero

Rimozione del frame standard

Dopo aver esteso il frame dell'applicazione e reso visibile, è possibile rimuovere il frame standard. La rimozione del frame standard consente di controllare la larghezza di ogni lato del frame anziché semplicemente estendere il frame standard.

Per rimuovere la cornice di finestra standard, è necessario gestire il messaggio WM_NCCALCSIZE , in particolare quando il relativo valore wParam è TRUE e il valore restituito è 0. A tale scopo, l'applicazione usa l'intera area finestra come area client, rimuovendo il frame standard.

I risultati della gestione del messaggio di WM_NCCALCSIZE non sono visibili finché l'area client non deve essere ridimensionata. Fino a quel momento, la visualizzazione iniziale della finestra viene visualizzata con il frame standard e i bordi estesi. Per superare questo problema, è necessario ridimensionare la finestra o eseguire un'azione che avvia un messaggio di WM_NCCALCSIZE al momento della creazione della finestra. Questa operazione può essere eseguita usando la funzione SetWindowPos per spostare la finestra e ridimensionarla. Il codice seguente illustra una chiamata a SetWindowPos che impone l'invio di un messaggio di WM_NCCALCSIZE usando gli attributi del rettangolo della finestra corrente e il flag di 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;
}

L'immagine seguente mostra la cornice standard (sinistra) e la nuova cornice estesa senza la cornice standard (destra).

schermata di una cornice standard (sinistra) e cornice personalizzata (destra)

Disegno nella finestra cornice estesa

Rimuovendo la cornice standard, si perde il disegno automatico dell'icona e del titolo dell'applicazione. Per aggiungerli nuovamente all'applicazione, è necessario disegnarli autonomamente. A tale scopo, esaminare prima di tutto la modifica che si è verificata nell'area client.

Con la rimozione del frame standard, l'area client è ora costituita dall'intera finestra, inclusa la cornice estesa. Include l'area in cui vengono disegnati i pulsanti didascalia. Nel confronto side-by-side seguente, l'area client sia per la cornice standard che per la cornice estesa personalizzata è evidenziata in rosso. L'area client per la finestra cornice standard (sinistra) è l'area nera. Nella finestra della cornice estesa (a destra), l'area client è l'intera finestra.

schermata di un'area client evidenziata in rosso sulla cornice standard e personalizzata

Poiché l'intera finestra è l'area client, puoi semplicemente disegnare ciò che vuoi nel frame esteso. Per aggiungere un titolo all'applicazione, è sufficiente disegnare testo nell'area appropriata. L'immagine seguente mostra il testo disegnato nella cornice personalizzata didascalia. Il titolo viene disegnato usando la funzione DrawThemeTextEx . Per visualizzare il codice che disegna il titolo, vedere Appendice B: Disegnare il titolo della didascalia.

screenshot di una cornice personalizzata con titolo

Nota

Quando si disegna nella cornice personalizzata, prestare attenzione quando si inseriscono i controlli dell'interfaccia utente. Poiché l'intera finestra è l'area client, è necessario modificare il posizionamento del controllo dell'interfaccia utente per ogni larghezza di fotogramma se non si vuole che vengano visualizzati nel frame esteso.

 

Abilitazione di Hit Testing per il frame personalizzato

Un effetto collaterale della rimozione del frame standard è la perdita del comportamento di ridimensionamento e spostamento predefinito. Per emulare correttamente il comportamento della finestra standard, è necessario implementare la logica per gestire didascalia pulsante hit testing e ridimensionamento dei fotogrammi.

Per didascalia pulsante hit testing, DWM fornisce la funzione DwmDefWindowProc. Per eseguire correttamente il test dei pulsanti didascalia negli scenari frame personalizzati, i messaggi devono essere passati prima a DwmDefWindowProc per la gestione. DwmDefWindowProc restituisce TRUE se un messaggio viene gestito e FALSE se non è. Se il messaggio non viene gestito da DwmDefWindowProc, l'applicazione deve gestire il messaggio stesso o passare il messaggio a DefWindowProc.

Per il ridimensionamento e lo spostamento dei frame, l'applicazione deve fornire la logica di hit testing e gestire i messaggi di hit test del frame. I messaggi di hit test frame vengono inviati tramite il messaggio di WM_NCHITTEST , anche se l'applicazione crea un frame personalizzato senza la cornice standard. Il codice seguente illustra la gestione del messaggio WM_NCHITTEST quando DwmDefWindowProc non lo gestisce. Per visualizzare il codice della funzione chiamata HitTestNCA , vedere Appendice C: Funzione 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;
    }
}

Appendice A: Procedura finestra di esempio

L'esempio di codice seguente illustra una procedura di finestra e le relative funzioni di lavoro di supporto usate per creare un'applicazione frame personalizzata.

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

Appendice B: Disegnare il titolo della didascalia

Il codice seguente illustra come disegnare un titolo didascalia sulla cornice estesa. Questa funzione deve essere chiamata dall'interno delle chiamate 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);
    }
}

Appendice C: Funzione HitTestNCA

Il codice seguente mostra la HitTestNCA funzione usata in Abilitazione dei test di hit per il frame personalizzato. Questa funzione gestisce la logica di hit testing per il WM_NCHITTEST quando DwmDefWindowProc non gestisce il messaggio.

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

Cenni preliminari di Gestione finestre desktop