Sdílet prostřednictvím


Vlastní rámec okna pomocí DWM

Toto téma ukazuje, jak pomocí rozhraní API Desktop Window Manageru (DWM) vytvořit vlastní rámce oken pro vaši aplikaci.

Úvod

Ve Windows Vista a novějších verzích se vzhled neklientních oblastí oken aplikací (záhlaví, ikona, ohraničení okna a tlačítka titulků) řídí DWM. Pomocí rozhraní API DWM můžete změnit způsob, jakým DWM vykresluje rámec okna.

Jednou z funkcí rozhraní API DWM je schopnost rozšířit rámec aplikace do klientské oblasti. Díky tomu můžete do rámce integrovat prvek klientského uživatelského rozhraní, například panel nástrojů, a tím získáte ovládací prvky uživatelského rozhraní na výraznějším místě v uživatelském rozhraní aplikace. Například Windows Internet Explorer 7 v systému Windows Vista integruje navigační panel do rámečku okna rozšířením horní části rámce, jak je znázorněno na následujícím snímku obrazovky.

navigační panel integrovaný do rámečku okna.

Možnost rozšířit rámec okna také umožňuje vytvářet vlastní rámy při zachování vzhledu a dojmu okna. Například Microsoft Office Word 2007 nakreslí tlačítko Office a panel nástrojů Rychlý přístup uvnitř vlastního rámečku a současně poskytuje standardní tlačítka Minimalizovat, Maximalizovat a Zavřít titulky, jak je znázorněno na následujícím snímku obrazovky.

tlačítko Office a panel nástrojů pro rychlý přístup ve Wordu 2007

Rozšíření rámce klienta

Funkce pro rozšíření rámce do klientské oblasti je poskytována funkcí DwmExtendFrameIntoClientArea. Chcete-li rozšířit rámec, předejte popisovač cílového okna spolu s hodnotami okrajových insetů do DwmExtendFrameIntoClientArea. Hodnoty odstupů okrajů určují, jak daleko rozšířit výchozí rám na všech čtyřech stranách okna.

Následující kód ukazuje použití DwmExtendFrameIntoClientArea k rozšíření rámce.

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

Všimněte si, že rozšíření rámce se provádí v WM_ACTIVATE zprávě, nikoli ve zprávě WM_CREATE. Tím zajistíte, že se rozšíření rámce zpracuje správně, když je okno ve své výchozí velikosti a kdy je maximalizované.

Následující obrázek znázorňuje standardní rámeček okna (vlevo) a stejný rámeček okna rozšířený (vpravo). Rámec se rozšiřuje předchozím příkladem kódu a výchozím prostředím Microsoft Visual Studio WNDCLASS /WNDCLASSEX pozadí (COLOR_WINDOW +1).

snímek obrazovky se standardním (levým) a rozšířeným rámečkem (vpravo) s bílým pozadím

Vizuální rozdíl mezi těmito dvěma okny je velmi jemný. Jediným rozdílem mezi těmito dvěma je, že v okně na levé straně chybí v okně vpravo tenké černé ohraničení oblasti klienta. Důvodem chybějícího ohraničení je, že je začleněn do rozšířeného rámce, ale zbytek klientské oblasti není. Aby byly rozšířené snímky viditelné, musí oblasti pod každou stranou rozšířeného rámce obsahovat pixelová data s hodnotou alfa 0. Černé ohraničení kolem oblasti klienta obsahuje pixelová data, ve kterých jsou všechny barevné hodnoty (červené, zelené, modré a alfa) nastavené na 0. Zbytek pozadí nemá alfa hodnotu nastavenou na 0, takže zbytek rozšířeného rámce není viditelný.

Nejjednodušší způsob, jak zajistit, aby byly rozšířené rámce viditelné, je natřít celou klientskou oblast černě. Chcete-li toho dosáhnout, inicializujte hbrBackground člen vaší WNDCLASS nebo WNDCLASSEX struktury pro zpracování BLACK_BRUSH zásob. Následující obrázek ukazuje stejný standardní rámec (vlevo) a rozšířený rámec (vpravo) zobrazený dříve. Tentokrát je však hbrBackground nastaven na popisovač BLACK_BRUSH získaný z funkce GetStockObject.

snímek obrazovky standardního (levého) a rozšířeného rámu (vpravo) s černým pozadím

Odebrání standardního rámce

Jakmile prodloužíte rámec aplikace a zviditelníte ho, můžete standardní rámec odebrat. Odebrání standardního rámečku umožňuje řídit šířku každé strany rámce, a ne jednoduše rozšířit standardní rámec.

Chcete-li odebrat standardní rámec okna, musíte zpracovat zprávu WM_NCCALCSIZE, konkrétně pokud je její hodnota wParamTRUE a návratová hodnota je 0. Aplikace tak jako klientskou oblast používá celou oblast okna a odebere standardní rámec.

Výsledky zpracování WM_NCCALCSIZE zprávy nejsou viditelné, dokud nebude potřeba změnit velikost oblasti klienta. Do té doby se počáteční zobrazení okna zobrazí se standardním rámečkem a rozšířenými ohraničeními. Chcete-li to překonat, musíte změnit velikost okna nebo provést akci, která zahájí WM_NCCALCSIZE zprávu při vytváření okna. Toho lze dosáhnout pomocí funkce SetWindowPos k přesunutí okna a změně jeho velikosti. Následující kód ukazuje volání SetWindowPos, které vynutí odeslání zprávy WM_NCCALCSIZE s použitím aktuálních atributů obdélníku okna a příznaku 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;
}

Následující obrázek znázorňuje standardní rámec (vlevo) a nově rozšířený rámec bez standardního rámce (vpravo).

snímek obrazovky se standardním rámečkem (vlevo) a vlastním rámečkem (vpravo)

Kreslení v okně s rozšířeným rámem

Odebráním standardního rámečku ztratíte automatický výkres ikony a názvu aplikace. Pokud je chcete přidat zpátky do aplikace, musíte je nakreslit sami. Nejprve se podívejte na změnu, ke které došlo ve vaší klientské oblasti.

S odstraněním standardního rámce nyní zahrnuje vaše klientská oblast celé okno, včetně rozšířeného rámce. To zahrnuje oblast, ve které jsou tlačítka titulků nakreslena. V následujícím porovnání vedle sebe je oblast klienta pro standardní rámec i vlastní rozšířený rámec zvýrazněná červeně. Klientská oblast standardního okna s rámem (vlevo) je černou oblastí. V rozšířeném okně rámce (vpravo) je klientskou oblastí celé okno.

snímek obrazovky s červeně zvýrazněnými oblastmi klienta na standardním a vlastním rámečku

Vzhledem k tomu, že celé okno je oblast klienta, můžete jednoduše nakreslit, co chcete v rozšířeném rámečku. Pokud chcete do aplikace přidat název, stačí nakreslit text v příslušné oblasti. Následující obrázek ukazuje motivovaný text nakreslený na vlastním rámečku titulků. Nadpis se nakreslí pomocí funkce DrawThemeTextEx. Chcete-li zobrazit kód, který vykresluje titulek, viz Příloha B: Vykreslení titulku.

snímek obrazovky vlastního rámu s nadpisem

Poznámka

Při kreslení do vlastního rámečku buďte opatrní při umísťování ovládacích prvků uživatelského rozhraní. Vzhledem k tomu, že celé okno je oblast klienta, musíte upravit umístění ovládacího prvku uživatelského rozhraní pro každou šířku rámce, pokud nechcete, aby se zobrazovaly na rozšířeném rámečku nebo v rozšířeném rámečku.

 

Povolení hit-testování pro vlastní rámec

Vedlejším účinkem odebrání standardního rámu je ztráta výchozího chování pro změnu velikosti a přesun. Aby vaše aplikace správně emulovala standardní chování okna, budete muset implementovat logiku pro zpracování testování zásahu do tlačítka titulku a změnu velikosti a přesouvání rámečku.

Pro testování stisknutí tlačítka titulků poskytuje DWM funkci DwmDefWindowProc. Aby bylo možné správně otestovat tlačítka titulků ve vlastních scénářích snímků, měly by se zprávy nejprve předat DwmDefWindowProc pro zpracování. DwmDefWindowProc vrátí TRUE, pokud je zpráva zpracována a FALSE, pokud ne. Pokud zprávu nezpracovává DwmDefWindowProc, měla by aplikace zprávu zpracovat přímo nebo ji předat dále k DefWindowProc.

Pro změnu velikosti a přesunutí rámce musí vaše aplikace poskytnout logiku testování průseku a zpracovat zprávy testu průseku rámce. Zprávy testu souřadnic rámce jsou vám posílány prostřednictvím zprávy WM_NCHITTEST, i když vaše aplikace vytvoří vlastní rám bez použití standardního rámce. Následující kód ukazuje zpracování WM_NCHITTEST zprávy, když DwmDefWindowProc ji nezpracuje. Pokud chcete zobrazit kód volané funkce HitTestNCA, přečtěte si Dodatek C: Funkce 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;
    }
}

Příloha A: Ukázková procedura okna

Následující ukázka kódu ukazuje proceduru okna a její podpůrné pracovní funkce používané k vytvoření vlastní aplikace rámce.

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

Příloha B: Malování názvu titulku

Následující kód ukazuje, jak nakreslit název titulku na rozšířeném rámečku. Tato funkce musí být volána zevnitř volání k BeginPaint a 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);
    }
}

Příloha C: Funkce HitTestNCA

Následující kód ukazuje funkci HitTestNCA používanou k povolení testování hitů v pro vlastního rámce. Tato funkce zpracovává logiku testování hitů pro WM_NCHITTEST, když DwmDefWindowProc zprávu nezpracuje.

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

Přehled Správce desktopových oken