Bingkai Jendela Kustom Menggunakan DWM

Topik ini menunjukkan cara menggunakan API Desktop Window Manager (DWM) untuk membuat bingkai jendela kustom untuk aplikasi Anda.

Pengantar

Di Windows Vista dan yang lebih baru, tampilan area non-klien jendela aplikasi (bilah judul, ikon, batas jendela, dan tombol caption) dikendalikan oleh DWM. Dengan menggunakan API DWM, Anda dapat mengubah cara DWM merender bingkai jendela.

Salah satu fitur API DWM adalah kemampuan untuk memperluas bingkai aplikasi ke area klien. Ini memungkinkan Anda mengintegrasikan elemen UI klien—seperti toolbar—ke dalam bingkai, memberikan kontrol UI tempat yang lebih menonjol di UI aplikasi. Misalnya, Windows Internet Explorer 7 di Windows Vista mengintegrasikan bilah navigasi ke bingkai jendela dengan memperluas bagian atas bingkai seperti yang ditunjukkan pada cuplikan layar berikut.

bilah navigasi terintegrasi ke dalam bingkai jendela.

Kemampuan untuk memperluas bingkai jendela juga memungkinkan Anda untuk membuat bingkai kustom sambil mempertahankan tampilan dan nuansa jendela. Misalnya, Microsoft Office Word 2007 menggambar tombol Office dan toolbar Akses Cepat di dalam bingkai kustom sambil menyediakan tombol Minimalkan, Maksimalkan, dan Tutup caption standar, seperti yang diperlihatkan dalam cuplikan layar berikut ini.

tombol office dan toolbar akses cepat di word 2007

Memperluas Bingkai Klien

Fungsionalitas untuk memperluas bingkai ke area klien diekspos oleh fungsi DwmExtendFrameIntoClientArea . Untuk memperluas bingkai, lewati handel jendela target bersama dengan nilai inset margin ke DwmExtendFrameIntoClientArea. Nilai margin inset menentukan seberapa jauh untuk memperluas bingkai di empat sisi jendela.

Kode berikut menunjukkan penggunaan DwmExtendFrameIntoClientArea untuk memperluas bingkai.

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

Perhatikan bahwa ekstensi bingkai dilakukan dalam pesan WM_ACTIVATE daripada pesan WM_CREATE . Ini memastikan bahwa ekstensi bingkai ditangani dengan benar ketika jendela berada pada ukuran defaultnya dan ketika dimaksimalkan.

Gambar berikut menunjukkan bingkai jendela standar (di sebelah kiri) dan bingkai jendela yang sama diperluas (di sebelah kanan). Bingkai diperluas menggunakan contoh kode sebelumnya dan latar belakang Microsoft Visual Studio WNDCLASS/WNDCLASSEX default (COLOR_WINDOW +1).

cuplikan layar standar (kiri) dan bingkai yang diperluas (kanan) dengan latar belakang putih

Perbedaan visual antara kedua jendela ini sangat halang. Satu-satunya perbedaan antara keduanya adalah batas garis hitam tipis dari wilayah klien di jendela di sebelah kiri hilang dari jendela di sebelah kanan. Alasan untuk batas yang hilang ini adalah bahwa ia dimasukkan ke dalam bingkai yang diperluas, tetapi sisa area klien tidak. Agar bingkai yang diperluas terlihat, wilayah yang mendasar setiap sisi bingkai yang diperluas harus memiliki data piksel dengan nilai alfa 0. Batas hitam di sekitar wilayah klien memiliki data piksel di mana semua nilai warna (merah, hijau, biru, dan alfa) diatur ke 0. Latar belakang lainnya tidak memiliki nilai alfa yang diatur ke 0, sehingga sisa bingkai yang diperluas tidak terlihat.

Cara term mudah untuk memastikan bahwa bingkai yang diperluas terlihat adalah dengan melukis seluruh wilayah klien menjadi hitam. Untuk mencapai hal ini, inisialisasi anggota hbrBackground dari struktur WNDCLASS atau WNDCLASSEX Anda ke handel stok BLACK_BRUSH. Gambar berikut menunjukkan bingkai standar (kiri) dan bingkai yang diperluas (kanan) yang sama yang ditunjukkan sebelumnya. Namun kali ini, hbrBackground diatur ke handel BLACK_BRUSH yang diperoleh dari fungsi GetStockObject .

cuplikan layar bingkai standar (kiri) dan diperluas (kanan) dengan latar belakang hitam

Menghapus Bingkai Standar

Setelah Memperluas bingkai aplikasi dan membuatnya terlihat, Anda dapat menghapus bingkai standar. Menghapus bingkai standar memungkinkan Anda mengontrol lebar setiap sisi bingkai daripada hanya memperluas bingkai standar.

Untuk menghapus bingkai jendela standar, Anda harus menangani pesan WM_NCCALCSIZE , khususnya ketika nilai wParam-nyaTRUE dan nilai yang dikembalikan adalah 0. Dengan demikian, aplikasi Anda menggunakan seluruh wilayah jendela sebagai area klien, menghapus bingkai standar.

Hasil penanganan pesan WM_NCCALCSIZE tidak terlihat sampai wilayah klien perlu diubah ukurannya. Hingga saat itu, tampilan awal jendela muncul dengan bingkai standar dan batas yang diperluas. Untuk mengatasinya, Anda harus mengubah ukuran jendela atau melakukan tindakan yang memulai pesan WM_NCCALCSIZE pada saat pembuatan jendela. Ini dapat dicapai dengan menggunakan fungsi SetWindowPos untuk memindahkan jendela Anda dan mengubah ukurannya. Kode berikut menunjukkan panggilan ke SetWindowPos yang memaksa pesan WM_NCCALCSIZE dikirim menggunakan atribut persegi panjang jendela saat ini dan bendera 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;
}

Gambar berikut menunjukkan bingkai standar (kiri) dan bingkai yang baru diperluas tanpa bingkai standar (kanan).

cuplikan layar bingkai standar (kiri) dan bingkai kustom (kanan)

Menggambar di Jendela Bingkai yang Diperluas

Dengan menghapus bingkai standar, Anda kehilangan gambar otomatis ikon dan judul aplikasi. Untuk menambahkannya kembali ke aplikasi, Anda harus menggambarnya sendiri. Untuk melakukan ini, pertama-tama lihat perubahan yang telah terjadi pada area klien Anda.

Dengan penghapusan bingkai standar, area klien Anda sekarang terdiri dari seluruh jendela, termasuk bingkai yang diperluas. Ini termasuk wilayah tempat tombol caption digambar. Dalam perbandingan berdampingan berikut, area klien untuk bingkai standar dan bingkai perluasan kustom disorot dengan warna merah. Area klien untuk jendela bingkai standar (kiri) adalah wilayah hitam. Pada jendela bingkai yang diperluas (kanan), area klien adalah seluruh jendela.

cuplikan layar area klien merah yang disorot pada bingkai standar dan kustom

Karena seluruh jendela adalah area klien Anda, Anda cukup menggambar apa yang Anda inginkan dalam bingkai yang diperluas. Untuk menambahkan judul ke aplikasi Anda, cukup gambar teks di wilayah yang sesuai. Gambar berikut menunjukkan teks bertema yang digambar pada bingkai caption kustom. Judul digambar menggunakan fungsi DrawThemeTextEx . Untuk melihat kode yang melukis judul, lihat Lampiran B: Melukis Judul Keterangan.

cuplikan layar bingkai kustom dengan judul

Catatan

Saat menggambar di bingkai kustom Anda, berhati-hatilah saat menempatkan kontrol UI. Karena seluruh jendela adalah wilayah klien Anda, Anda harus menyesuaikan penempatan kontrol UI untuk setiap lebar bingkai jika Anda tidak ingin jendela muncul pada atau dalam bingkai yang diperluas.

 

Mengaktifkan Pengujian Hit untuk Bingkai Kustom

Efek samping dari menghapus bingkai standar adalah hilangnya perubahan ukuran default dan perilaku bergerak. Agar aplikasi Anda dapat meniru perilaku jendela standar dengan benar, Anda harus menerapkan logika untuk menangani pengujian tekan tombol caption dan mengubah ukuran/pemindahan bingkai.

Untuk pengujian tekan tombol caption, DWM menyediakan fungsi DwmDefWindowProc. Untuk menekan tombol caption dengan benar dalam skenario bingkai kustom, pesan harus terlebih dahulu diteruskan ke DwmDefWindowProc untuk penanganan. DwmDefWindowProc mengembalikan TRUE jika pesan ditangani dan FALSE jika tidak. Jika pesan tidak ditangani oleh DwmDefWindowProc, aplikasi Anda harus menangani pesan itu sendiri atau meneruskan pesan ke DefWindowProc.

Untuk mengubah ukuran dan pemindahan bingkai, aplikasi Anda harus menyediakan logika pengujian hit dan menangani pesan pengujian hit bingkai. Pesan uji tekan bingkai dikirimkan kepada Anda melalui pesan WM_NCHITTEST , bahkan jika aplikasi Anda membuat bingkai kustom tanpa bingkai standar. Kode berikut menunjukkan penanganan pesan WM_NCHITTEST ketika DwmDefWindowProc tidak menanganinya. Untuk melihat kode fungsi yang disebut HitTestNCA , lihat Lampiran C: Fungsi 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;
    }
}

Lampiran A: Contoh Prosedur Jendela

Sampel kode berikut menunjukkan prosedur jendela dan fungsi pekerja pendukungnya yang digunakan untuk membuat aplikasi bingkai kustom.

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

Lampiran B: Melukis Judul Keterangan

Kode berikut menunjukkan cara melukis judul caption pada bingkai yang diperluas. Fungsi ini harus dipanggil dari dalam panggilan BeginPaint dan 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);
    }
}

Lampiran C: Fungsi HitTestNCA

Kode berikut menunjukkan fungsi yang HitTestNCA digunakan dalam Mengaktifkan Pengujian Hit untuk Bingkai Kustom. Fungsi ini menangani logika pengujian hit untuk WM_NCHITTEST ketika DwmDefWindowProc tidak menangani pesan.

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

Gambaran Umum Manajer Jendela Desktop