Benutzerdefinierter Fensterrahmen mit DWM

In diesem Thema wird veranschaulicht, wie Sie mithilfe der DWM-APIs (Desktop Window Manager) benutzerdefinierte Fensterrahmen für Ihre Anwendung erstellen.

Einführung

In Windows Vista und höher wird die Darstellung der nicht clientbezogenen Bereiche von Anwendungsfenstern (Titelleiste, Symbol, Fensterrahmen und Untertitel-Schaltflächen) vom DWM gesteuert. Mithilfe der DWM-APIs können Sie die Art und Weise ändern, wie der DWM den Frame eines Fensters rendert.

Ein Feature der DWM-APIs ist die Möglichkeit, den Anwendungsframe auf den Clientbereich zu erweitern. Auf diese Weise können Sie ein Client-UI-Element ( z. B. eine Symbolleiste ) in den Frame integrieren, sodass die Benutzeroberflächensteuerelemente in der Benutzeroberfläche der Anwendung einen prominenteren Platz erhalten. Beispielsweise integriert Windows Internet Explorer 7 unter Windows Vista die Navigationsleiste in den Fensterrahmen, indem der obere Rand des Frames erweitert wird, wie im folgenden Screenshot dargestellt.

In den Fensterrahmen integrierte Navigationsleiste.

Durch die Möglichkeit, den Fensterrahmen zu erweitern, können Sie auch benutzerdefinierte Frames erstellen und gleichzeitig das Aussehen und Verhalten des Fensters beibehalten. Microsoft Office Word 2007 zeichnet beispielsweise die Office-Schaltfläche und die Symbolleiste für den Schnellzugriff im benutzerdefinierten Rahmen, während die Standardschaltflächen Minimieren, Maximieren und Schließen Untertitel bereitgestellt werden, wie im folgenden Screenshot gezeigt.

Office-Schaltfläche und Symbolleiste für den Schnellzugriff in Word 2007

Erweitern des Clientframes

Die Funktionalität zum Erweitern des Frames auf den Clientbereich wird von der DwmExtendFrameIntoClientArea-Funktion verfügbar gemacht. Um den Frame zu erweitern, übergeben Sie das Handle des Zielfensters zusammen mit den Randeinsetwerten an DwmExtendFrameIntoClientArea. Die Randeinsetwerte bestimmen, wie weit der Rahmen auf den vier Seiten des Fensters erweitert werden soll.

Der folgende Code veranschaulicht die Verwendung von DwmExtendFrameIntoClientArea zum Erweitern des Frames.

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

Beachten Sie, dass die Frameerweiterung nicht in der WM_CREATE-Nachricht , sondern innerhalb der WM_ACTIVATE-Nachricht erfolgt. Dadurch wird sichergestellt, dass die Frameerweiterung ordnungsgemäß behandelt wird, wenn das Fenster seine Standardgröße aufweist und wenn es maximiert wird.

Die folgende Abbildung zeigt einen Standardfensterrahmen (links) und denselben erweiterten Fensterrahmen (rechts). Der Frame wird mithilfe des vorherigen Codebeispiels und des standardmäßigen Microsoft Visual Studio WNDCLASS/WNDCLASSEX-Hintergrunds (COLOR_WINDOW +1) erweitert.

Screenshot eines Standardrahmens (links) und eines erweiterten Frames (rechts) mit weißem Hintergrund

Der visuelle Unterschied zwischen diesen beiden Fenstern ist sehr subtil. Der einzige Unterschied zwischen den beiden besteht darin, dass der dünne schwarze Linienrahmen des Clientbereichs im Fenster auf der linken Seite im Fenster auf der rechten Seite fehlt. Der Grund für diesen fehlenden Rahmen ist, dass er in den erweiterten Frame integriert ist, der Rest des Clientbereichs jedoch nicht. Damit die erweiterten Frames sichtbar sind, müssen die Bereiche, die den Seiten des erweiterten Frames zugrunde liegen, Pixeldaten mit dem Alphawert 0 aufweisen. Der schwarze Rahmen um den Clientbereich enthält Pixeldaten, in denen alle Farbwerte (Rot, Grün, Blau und Alpha) auf 0 festgelegt sind. Für den rest des Hintergrunds ist der Alphawert nicht auf 0 festgelegt, sodass der Rest des erweiterten Frames nicht sichtbar ist.

Die einfachste Möglichkeit, sicherzustellen, dass die erweiterten Frames sichtbar sind, besteht darin, den gesamten Clientbereich schwarz zu zeichnen. Um dies zu erreichen, initialisieren Sie den hbrBackground-Member Ihrer WNDCLASS- oder WNDCLASSEX-Struktur im Handle der BLACK_BRUSH. Die folgende Abbildung zeigt den gleichen Standardrahmen (links) und den erweiterten Frame (rechts), wie zuvor gezeigt. Dieses Mal wird hbrBackground jedoch auf das BLACK_BRUSH-Handle festgelegt, das von der GetStockObject-Funktion abgerufen wird.

Screenshot eines Standardrahmens (links) und erweiterten Rahmens (rechts) mit schwarzem Hintergrund

Entfernen des Standardframes

Nachdem Sie den Rahmen Ihrer Anwendung erweitert und sichtbar gemacht haben, können Sie den Standardrahmen entfernen. Wenn Sie den Standardrahmen entfernen, können Sie die Breite jeder Seite des Rahmens steuern, anstatt einfach den Standardrahmen zu erweitern.

Um den Standardfensterrahmen zu entfernen, müssen Sie die WM_NCCALCSIZE Meldung behandeln, insbesondere wenn der wParam-WertTRUE und der Rückgabewert 0 ist. Auf diese Weise verwendet Ihre Anwendung den gesamten Fensterbereich als Clientbereich und entfernt den Standardrahmen.

Die Ergebnisse der Behandlung der WM_NCCALCSIZE Nachricht sind erst sichtbar, wenn die Größe der Clientregion geändert werden muss. Bis zu diesem Zeitpunkt wird die anfängliche Ansicht des Fensters mit dem Standardrahmen und erweiterten Rahmen angezeigt. Um dies zu umgehen, müssen Sie entweder die Größe Des Fensters ändern oder eine Aktion ausführen, die zum Zeitpunkt der Fenstererstellung eine WM_NCCALCSIZE Nachricht initiiert. Dies kann erreicht werden, indem Sie die SetWindowPos-Funktion verwenden, um Das Fenster zu verschieben und die Größe zu ändern. Der folgende Code veranschaulicht einen Aufruf von SetWindowPos , der erzwingt, dass eine WM_NCCALCSIZE Nachricht mit den aktuellen Fensterrechteckattributen und dem SWP_FRAMECHANGED-Flag gesendet wird.

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

Die folgende Abbildung zeigt den Standardrahmen (links) und den neu erweiterten Rahmen ohne den Standardrahmen (rechts).

Screenshot eines Standardrahmens (links) und eines benutzerdefinierten Frames (rechts)

Zeichnen im Fenster "Erweiterter Rahmen"

Wenn Sie den Standardrahmen entfernen, verlieren Sie die automatische Zeichnung des Anwendungssymbols und des Titels. Um diese wieder ihrer Anwendung hinzuzufügen, müssen Sie sie selbst zeichnen. Sehen Sie sich hierzu zunächst die Änderung an, die in Ihrem Clientbereich aufgetreten ist.

Mit dem Entfernen des Standardrahmens besteht Ihr Clientbereich jetzt aus dem gesamten Fenster, einschließlich des erweiterten Rahmens. Dies schließt den Bereich ein, in dem die Untertitel Schaltflächen gezeichnet werden. Im folgenden parallelen Vergleich wird der Clientbereich sowohl für den Standardrahmen als auch für den benutzerdefinierten erweiterten Frame rot hervorgehoben. Der Clientbereich für das Standardrahmenfenster (links) ist der schwarze Bereich. Im erweiterten Rahmenfenster (rechts) ist der Clientbereich das gesamte Fenster.

Screenshot eines rot hervorgehobenen Clientbereichs im Standard- und benutzerdefinierten Frame

Da das gesamte Fenster Ihr Clientbereich ist, können Sie einfach die gewünschten Elemente im erweiterten Frame zeichnen. Um Ihrer Anwendung einen Titel hinzuzufügen, zeichnen Sie einfach Text in der entsprechenden Region. Die folgende Abbildung zeigt designierten Text, der auf dem benutzerdefinierten Untertitel Frame gezeichnet wurde. Der Titel wird mit der DrawThemeTextEx-Funktion gezeichnet. Informationen zum Anzeigen des Codes, der den Titel zeichnet, finden Sie unter Anhang B: Zeichnen des Titels der Beschriftung.

Screenshot eines benutzerdefinierten Frames mit Titel

Hinweis

Wenn Sie in Ihrem benutzerdefinierten Frame zeichnen, sollten Sie beim Platzieren von UI-Steuerelementen vorsichtig sein. Da das gesamte Fenster Ihr Clientbereich ist, müssen Sie die Ui-Steuerelementplatzierung für jede Framebreite anpassen, wenn sie nicht im oder im erweiterten Frame angezeigt werden sollen.

 

Aktivieren von Treffertests für den benutzerdefinierten Frame

Ein Nebeneffekt des Entfernens des Standardrahmens ist der Verlust des Standardmäßigen Größenänderungs- und Bewegungsverhaltens. Damit Ihre Anwendung das Standardfensterverhalten ordnungsgemäß emulieren kann, müssen Sie Logik implementieren, um Untertitel Treffertests für Schaltflächen und Das Ändern der Größe/Verschiebung von Frames zu verarbeiten.

Für Untertitel Schaltflächentreffertests stellt DWM die DwmDefWindowProc-Funktion bereit. Um die Untertitel-Schaltflächen in benutzerdefinierten Frameszenarien ordnungsgemäß zu testen, sollten Nachrichten zunächst zur Behandlung an DwmDefWindowProc übergeben werden. DwmDefWindowProc gibt TRUE zurück, wenn eine Nachricht behandelt wird, und FALSE , wenn dies nicht der Fall ist. Wenn die Nachricht nicht von DwmDefWindowProc verarbeitet wird, sollte Ihre Anwendung die Nachricht selbst verarbeiten oder an DefWindowProc übergeben.

Zum Ändern der Framegröße und zum Verschieben muss Ihre Anwendung die Treffertestlogik bereitstellen und Frametreffertestmeldungen verarbeiten. Frametreffertestmeldungen werden über die WM_NCHITTEST-Nachricht an Sie gesendet, auch wenn Ihre Anwendung einen benutzerdefinierten Frame ohne den Standardframe erstellt. Der folgende Code veranschaulicht die Behandlung der WM_NCHITTEST Meldung, wenn DwmDefWindowProc sie nicht verarbeitet. Den Code der aufgerufenen HitTestNCA Funktion finden Sie im Anhang 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;
    }
}

Anhang A: Beispielfensterprozedur

Das folgende Codebeispiel veranschaulicht eine Fensterprozedur und ihre unterstützenden Workerfunktionen, die zum Erstellen einer benutzerdefinierten Frameanwendung verwendet werden.

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

Anhang B: Malen des Titels der Beschriftung

Der folgende Code veranschaulicht, wie sie einen Untertitel Titel auf den erweiterten Frame zeichnen. Diese Funktion muss innerhalb der Aufrufe BeginPaint und EndPaint aufgerufen werden.

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

Anhang C: HitTestNCA-Funktion

Der folgende Code zeigt die Funktion, die HitTestNCA unter Aktivieren von Treffertests für den benutzerdefinierten Frame verwendet wird. Diese Funktion verarbeitet die Treffertestlogik für die WM_NCHITTEST , wenn DwmDefWindowProc die Nachricht nicht verarbeitet.

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

Übersicht über Desktop Window Manager