Dela via


Anpassad fönsterram med DWM

Det här avsnittet visar hur du använder API:erna för Desktop Window Manager (DWM) för att skapa anpassade fönsterramar för ditt program.

Introduktion

I Windows Vista och senare styrs utseendet på icke-klientområdena i programfönster (namnlisten, ikonen, fönsterkantlinjen och bildtextknapparna) av DWM. Med hjälp av DWM-API:erna kan du ändra hur DWM återger ett fönsters ram.

En funktion i DWM-API:erna är möjligheten att utöka programramen till klientområdet. På så sätt kan du integrera ett klientgränssnittselement , till exempel ett verktygsfält, i ramen, vilket ger användargränssnittskontrollerna en mer framträdande plats i programmets användargränssnitt. Till exempel integrerar Windows Internet Explorer 7 i Windows Vista navigeringsfältet i fönsterramen genom att utöka bildens överkant enligt följande skärmbild.

navigeringsfältet integrerat i fönsterramen.

Möjligheten att utöka fönsterramen gör det också möjligt att skapa anpassade ramar samtidigt som fönstrets utseende och känsla bibehålls. Microsoft Office Word 2007 ritar till exempel Office-knappen och verktygsfältet Snabbåtkomst i den anpassade ramen samtidigt som standardknapparna Minimera, Maximera och Stäng bildtext visas enligt följande skärmbild.

office-knapp och verktygsfältet för snabbåtkomst i word 2007

Utvidga klientens ramverk

Funktionerna för att utöka ramen till klientområdet exponeras av funktionen DwmExtendFrameIntoClientArea. Om du vill utöka ramen skickar du handtaget för målfönstret tillsammans med marginalens inset-värden till DwmExtendFrameIntoClientArea. Marginalens inuppsättningsvärden avgör hur långt ramen ska utökas på fönstrets fyra sidor.

Följande kod visar användningen av DwmExtendFrameIntoClientArea för att utöka ramen.

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

Observera att ramtillägget görs i det WM_ACTIVATE meddelandet i stället för det WM_CREATE meddelandet. Detta säkerställer att ramtillägget hanteras korrekt när fönstret har sin standardstorlek och när det maximeras.

Följande bild visar en standardfönsterram (till vänster) och samma fönsterram utökad (till höger). Ramen utökas med hjälp av det föregående kodexemplet och standardvärdet för Microsoft Visual Studio WNDCLASS/WNDCLASSEX bakgrundsfärg (COLOR_WINDOW +1).

skärmbild av en standard (vänster) och utökad ram (höger) med vit bakgrund

Den visuella skillnaden mellan dessa två fönster är mycket subtil. Den enda skillnaden mellan de två är att den tunna svarta linjens kantlinje i klientregionen i fönstret till vänster saknas i fönstret till höger. Orsaken till den här saknade kantlinjen är att den ingår i den utökade ramen, men resten av klientområdet är det inte. För att de utökade bildrutorna ska vara synliga måste de regioner som ligger bakom var och en av den utökade ramens sidor ha pixeldata med alfavärdet 0. Den svarta kantlinjen runt klientregionen har pixeldata där alla färgvärden (röd, grön, blå och alfa) är inställda på 0. Resten av bakgrunden har inte alfavärdet inställt på 0, så resten av den utökade ramen visas inte.

Det enklaste sättet att se till att de utökade ramarna är synliga är att måla hela klientregionen svart. För att åstadkomma detta initierar du hbrBackground medlem i din WNDCLASS- eller WNDCLASSEX- struktur till handtaget för BLACK_BRUSH. Följande bild visar samma standardram (vänster) och utökad ram (höger) som visades tidigare. Den här gången är dock hbrBackground- inställd på den BLACK_BRUSH-referens som hämtas från funktionen GetStockObject.

skärmbild av en standard (vänster) och utökad ram (höger) med svart bakgrund

Ta bort standardramen

När du har utökat ramen för ditt program och gjort den synlig kan du ta bort standardramen. Om du tar bort standardramen kan du styra bredden på varje sida av ramen i stället för att bara utöka standardramen.

Om du vill ta bort standardfönsterramen måste du hantera WM_NCCALCSIZE-meddelandet, särskilt när dess wParam- värde är TRUE- och returvärdet är 0. På så sätt använder programmet hela fönsterregionen som klientområde och tar bort standardramen.

Resultatet av hanteringen av WM_NCCALCSIZE-meddelandet visas inte förrän klientregionen behöver storleksändras. Fram till dess visas den första vyn av fönstret med standardramen och utökade kantlinjer. För att lösa detta måste du antingen ändra storlek på fönstret eller utföra en åtgärd som initierar ett WM_NCCALCSIZE meddelande när fönstret skapas. Detta kan du göra med hjälp av funktionen SetWindowPos för att flytta fönstret och ändra storlek på det. Följande kod visar ett anrop till SetWindowPos som tvingar ett WM_NCCALCSIZE-meddelande att skickas med de nuvarande attributen för fönsterrektangeln och flaggan 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;
}

Följande bild visar standardramen (vänster) och den nyligen utökade ramen utan standardramen (höger).

skärmbild av en standardram (vänster) och anpassad ram (höger)

Ritning i den utökade ramfönstret

Genom att ta bort standardramen förlorar du den automatiska ritningen av programikonen och rubriken. Om du vill lägga till dem i programmet igen måste du rita dem själv. Det gör du genom att först titta på den ändring som har skett i klientområdet.

När standardramen tas bort består klientområdet nu av hela fönstret, inklusive den utökade ramen. Detta inkluderar den region där bildtextknapparna ritas. I följande jämförelse sida vid sida är klientområdet för både standardramen och den anpassade utökade ramen markerat i rött. Klientområdet för standardramfönstret (till vänster) är den svarta regionen. I det utökade ramfönstret (till höger) är klientområdet hela fönstret.

skärmbild av röda markerade klientområden i standard- och anpassade ramar

Eftersom hela fönstret är ditt klientområde kan du helt enkelt rita det du vill ha i den utökade ramen. Om du vill lägga till en rubrik i ditt program ritar du bara text i rätt region. Följande bild visar tematext ritad på den anpassade bildtextramen. Rubriken ritas med hjälp av funktionen DrawThemeTextEx. Information om hur du visar koden som målar rubriken finns i bilaga B: Måla rubrikrubriken.

skärmbild av en anpassad ram med rubrik

Not

När du ritar i din anpassade ram bör du vara försiktig när du placerar användargränssnittskontroller. Eftersom hela fönstret är din klientregion måste du justera placeringen av användargränssnittskontrollen för varje bildrutebredd om du inte vill att de ska visas på eller i den utökade ramen.

 

Aktivera träfftestning för den anpassade ramen

En bieffekt av att ta bort standardramen är att standardbeteendet för ändring och flyttning förloras. För att din applikation ska kunna emulera standardfönsterbeteende korrekt måste du implementera logik för att hantera träfftestning av titelfältets knappar och ändra storlek på och flytta fönsterramen.

För att testa rubrikknappar tillhandahåller DWM funktionen DwmDefWindowProc. Om du vill testa bildtextknapparna korrekt i scenarier med anpassad ram skickas meddelanden först till DwmDefWindowProc- för hantering. DwmDefWindowProc returnerar TRUE om ett meddelande hanteras och FALSE om det inte är hanterat. Om meddelandet inte hanteras av DwmDefWindowProcska programmet hantera själva meddelandet eller skicka meddelandet till DefWindowProc.

För att ändra storlek och flytta ramen måste programmet tillhandahålla logik för träfftest och hantera meddelanden för ramträfftest. Testmeddelanden för ramträffar skickas till dig via WM_NCHITTEST-meddelandet, även om ditt program skapar en anpassad ram utan standardramen. Följande kod visar hur du hanterar WM_NCHITTEST meddelandet när DwmDefWindowProc inte hanterar det. Om du vill se koden för den anropade funktionen HitTestNCA läser du Bilaga C: HitTestNCA-funktion.

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

Bilaga A: Exempelfönsterprocedur

Följande kodexempel visar en fönsterprocedur och dess stödjande arbetsfunktioner som används för att skapa ett anpassat ramprogram.

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

Bilaga B: Måla rubriktiteln

Följande kod visar hur du målar en rubrik på den utökade ramen. Den här funktionen måste anropas från anropen BeginPaint och 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);
    }
}

Bilaga C: HitTestNCA-funktion

Följande kod visar funktionen HitTestNCA som används i för att aktivera träfftestning för den anpassade ramen. Den här funktionen hanterar testlogik för träff för WM_NCHITTEST när DwmDefWindowProc inte hanterar meddelandet.

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

Översikt över Desktop Window Manager