Udostępnij przez


Niestandardowa ramka okna przy użyciu usługi DWM

W tym temacie pokazano, jak używać interfejsów API programu Desktop Window Manager (DWM) do tworzenia niestandardowych ramek okien dla aplikacji.

Wprowadzenie

W systemie Windows Vista i nowszych wygląd obszarów aplikacji innych niż klienci (pasek tytułu, ikona, obramowanie okna i przyciski podpisu) jest kontrolowany przez DWM. Za pomocą API DWM można zmienić sposób, w jaki DWM renderuje ramkę okna.

Jedną z funkcji interfejsów API DWM jest możliwość rozszerzenia ramki aplikacji na obszar klienta. Dzięki temu można zintegrować element interfejsu użytkownika klienta — taki jak pasek narzędzi — do ramki, dzięki czemu kontrolki interfejsu użytkownika będą bardziej widoczne w interfejsie użytkownika aplikacji. Na przykład program Windows Internet Explorer 7 w systemie Windows Vista integruje pasek nawigacyjny z ramką okna, rozszerzając górną część ramki, jak pokazano na poniższym zrzucie ekranu.

pasek nawigacyjny zintegrowany z ramką okna.

Możliwość rozszerzenia ramki okna umożliwia również tworzenie niestandardowych ramek przy zachowaniu wyglądu i działania okna. Na przykład program Microsoft Office Word 2007 rysuje przycisk Office i pasek narzędzi Szybki dostęp wewnątrz ramki niestandardowej, zapewniając standardowe przyciski Minimalizuj, Maksymalizuj i Zamknij, jak pokazano na poniższym zrzucie ekranu.

przycisk pakietu Office i pasek narzędzi szybkiego dostępu w programie Word 2007

Rozszerzanie ramki klienta

Funkcja rozszerzania ramki na obszar klienta jest uwidaczniona przez funkcję DwmExtendFrameIntoClientArea. Aby rozszerzyć ramkę, przekaż uchwyt okna docelowego wraz z wartościami wcięcia marginesu do DwmExtendFrameIntoClientArea. Wartości marginesu wewnętrznego określają, jak daleko należy rozciągnąć ramkę po czterech stronach okna.

Poniższy kod demonstruje użycie DwmExtendFrameIntoClientArea w celu rozszerzenia ramki.

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

Należy pamiętać, że rozszerzenie ramki jest wykonywane w komunikacie WM_ACTIVATE, a nie w komunikacie WM_CREATE. Dzięki temu rozszerzenie ramki jest prawidłowo obsługiwane, gdy okno jest w domyślnym rozmiarze i gdy jest zmaksymalizowane.

Na poniższej ilustracji przedstawiono standardową ramkę okna (po lewej stronie) i tę samą ramę okna rozszerzoną (po prawej stronie). Ramka została rozszerzona przy użyciu poprzedniego przykładu kodu i domyślnego tła Microsoft Visual Studio WNDCLASS/WNDCLASSEX (COLOR_WINDOW +1).

zrzut ekranu standardowej (po lewej) i rozszerzonej ramki (po prawej) z białym tłem

Różnica wizualna między tymi dwoma oknami jest bardzo subtelna. Jedyną różnicą między nimi jest to, że cienka czarna linia obszaru klienta w oknie po lewej stronie jest nieobecna w oknie po prawej stronie. Przyczyną braku tego obramowania jest fakt, że stanowi ono część rozszerzonej ramki, podczas gdy pozostała część obszaru klienta już nie. Aby rozszerzone ramki mogły być widoczne, regiony leżące poniżej po bokach rozszerzonej ramki muszą zawierać dane pikseli o wartości alfa równej 0. Czarne obramowanie wokół regionu klienta zawiera dane pikseli, w których wszystkie wartości kolorów (czerwony, zielony, niebieski i alfa) są ustawione na 0. Pozostała część tła nie ma wartości alfa ustawionej na 0, więc pozostała część rozszerzonej ramki nie jest widoczna.

Najprostszym sposobem zapewnienia widoczności ramek rozszerzonych jest malowanie całego regionu klienta na czarno. W tym celu zainicjuj członka hbrBackground struktury WNDCLASS lub WNDCLASSEX do uchwytu zapasowego pędzla BLACK_BRUSH. Na poniższej ilustracji przedstawiono tę samą ramkę standardową (po lewej) i ramkę rozszerzoną (po prawej) pokazaną wcześniej. Tym razem jednak uchwyt hbrBackground jest ustawiony na BLACK_BRUSH uzyskany z funkcji GetStockObject.

zrzut ekranu standardowej (po lewej) i rozszerzonej ramki (po prawej) z czarnym tłem

Usuwanie standardowej ramki

Po wydłużeniu ramki aplikacji i jej uwidocznieniu można usunąć ramkę standardową. Usunięcie standardowej ramki umożliwia sterowanie szerokością każdej strony ramki, a nie po prostu rozszerzaniem standardowej ramki.

Aby usunąć standardową ramkę okna, należy obsłużyć komunikat WM_NCCALCSIZE, w szczególności gdy jego wartość wParam jest true, a zwracana wartość to 0. Dzięki temu aplikacja używa całego regionu okna jako obszaru klienta, usuwając ramkę standardową.

Wyniki obsługi komunikatu WM_NCCALCSIZE nie są widoczne aż do momentu, gdy konieczna jest zmiana rozmiaru regionu klienta. Do tego czasu zostanie wyświetlony początkowy widok okna ze standardową ramką i obramowaniem rozszerzonym. Aby rozwiązać ten problem, musisz zmienić rozmiar okna lub wykonać akcję, która inicjuje komunikat WM_NCCALCSIZE podczas tworzenia okna. Można to zrobić za pomocą funkcji SetWindowPos, aby przenieść okno i zmienić jego rozmiar. Poniższy kod demonstruje wywołanie SetWindowPos, które wymusza wysłanie komunikatu WM_NCCALCSIZE przy użyciu bieżących atrybutów prostokąta okna i flagi 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;
}

Na poniższej ilustracji przedstawiono standardową ramkę (po lewej) i nowo rozszerzoną ramkę bez standardowej ramki (po prawej).

zrzut ekranu przedstawiający standardową ramkę (po lewej) i ramkę niestandardową (po prawej)

Rysowanie w oknie rozszerzonej ramki

Usunięcie standardowej ramki pozwala utracić automatyczne rysowanie ikony i tytułu aplikacji. Aby dodać je z powrotem do aplikacji, musisz samodzielnie je narysować. Aby to zrobić, najpierw przyjrzyj się zmianie, która zaszła w strefie klienta.

Po usunięciu standardowej ramki obszar klienta składa się teraz z całego okna, w tym ramki rozszerzonej. Obejmuje to region, w którym są rysowane przyciski tytułowe. W poniższym porównaniu obok siebie obszar klienta zarówno dla standardowej ramki, jak i niestandardowej rozszerzonej ramki jest wyróżniony na czerwono. Obszar klienta w standardowym oknie ramowym (po lewej) to czarny region. W rozszerzonym oknie ramowym (po prawej) obszar klienta jest całym oknem.

zrzut ekranu przedstawiający wyróżnione na czerwono obszary klienta na standardowej i niestandardowej ramce

Ponieważ całe okno jest obszarem klienta, możesz po prostu narysować żądane elementy w ramce rozszerzonej. Aby dodać tytuł do aplikacji, wystarczy narysować tekst w odpowiednim regionie. Na poniższym obrazie pokazano tematyczny tekst narysowany na ramkę niestandardowego podpisu. Tytuł jest rysowany przy użyciu funkcji DrawThemeTextEx. Aby zapoznać się z kodem, który rysuje tytuł nagłówka, zobacz Dodatek B: Malowanie tytułu nagłówka .

zrzut ekranu przedstawiający ramkę niestandardową z tytułem

Notatka

Podczas rysowania w ramce niestandardowej należy zachować ostrożność podczas umieszczania kontrolek interfejsu użytkownika. Ponieważ całe okno jest regionem klienta, należy dostosować położenie kontrolki interfejsu użytkownika dla każdej szerokości ramki, jeśli nie chcesz, aby były wyświetlane w ramce rozszerzonej.

 

Włączanie sprawdzania uderzeń dla niestandardowej ramki

Efektem ubocznym usunięcia standardowej ramki jest utrata domyślnego zachowania dotyczącego zmiany rozmiaru i przesuwania. Aby aplikacja prawidłowo emulowała standardowe zachowanie okna, należy zaimplementować logikę do obsługi testowania trafień przycisków tytułu i zmiany rozmiaru/przenoszenia ramki.

W przypadku testowania trafień przycisku podpisu usługa DWM udostępnia funkcjęDwmDefWindowProc. Aby prawidłowo przetestować przyciski nagłówków w niestandardowych scenariuszach ramek, komunikaty powinny najpierw zostać przekazane do DwmDefWindowProc w celu obsługi. DwmDefWindowProc zwraca true, jeśli komunikat jest obsługiwany i false, jeśli tak nie jest. Jeśli komunikat nie jest obsługiwany przez DwmDefWindowProc, aplikacja powinna obsługiwać sam komunikat lub przekazywać komunikat do DefWindowProc.

Aby zmieniać rozmiar i przesuwać ramki, aplikacja musi zapewnić logikę testowania kolizji i obsługiwać komunikaty dotyczące testów ramek. Komunikaty testowe trafień ramek są wysyłane za pośrednictwem komunikatu WM_NCHITTEST, nawet jeśli aplikacja tworzy ramkę niestandardową bez standardowej ramki. Poniższy kod demonstruje obsługę komunikatu WM_NCHITTEST, gdy dwmDefWindowProc go nie obsługuje. Aby wyświetlić kod funkcji o nazwie HitTestNCA, zobacz Dodatek C: HitTestNCA Function.

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

Dodatek A: Przykładowa procedura okna

Poniższy przykładowy kod demonstruje procedurę okna i jej pomocnicze funkcje pracowników używane do tworzenia niestandardowej aplikacji ramowej.

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

Dodatek B: Malowanie tytułu nagłówka

Poniższy kod demonstruje, jak rysować tytuł nagłówka na rozszerzonej ramce. Ta funkcja musi być wywoływana z poziomu wywołań BeginPaint i 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);
    }
}

Dodatek C: HitTestNCA, funkcja

Poniższy kod przedstawia funkcję HitTestNCA używaną w Włączanie funkcji testowania trafień dla niestandardowej ramki. Ta funkcja obsługuje logikę testowania trafień dla WM_NCHITTEST, gdy DwmDefWindowProc nie obsługuje komunikatu.

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

Desktop Window Manager — omówienie