C++ Win32 API GDI:矩形抗锯齿在透明背景下无法正常工作

C++ Win32 API GDI : Rectangle AntiAliasing not working properly with transparent background

我使用扩展框架来呈现自定义标题和 window 边框。

hr = DwmExtendFrameIntoClientArea(hWnd, &margins);

我还使用分层 window 来渲染背景透明。我的 COLORKEYRGB(0,0,0)

SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 255, LWA_COLORKEY);

我使用分层 window 的原因是,我想使 window 的底部边框的角变圆。

问题是我想做一些漂亮的事情,我试图用 GDI 在客户区渲染抗锯齿 window 边框。但是,它在绘制背景(纯色)但不使用透明背景时作为抗锯齿工作。

图像:https://ibb.co/fD9GsFg

我应该怎么做才能解决这个问题?

如果你尝试它,请使用 VS 调试器,因为我没有放置功能性 window 按钮。

#include <windows.h>
#include <tchar.h>
#include <windowsx.h>
#include <objidl.h>
#include <gdiplus.h>

#include <dwmapi.h>
#pragma comment(lib,"Dwmapi")

using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")

HINSTANCE hInst;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int nWidth = 600, nHeight = 400;

#define RECTWIDTH(rc)            (rc.right - rc.left)
#define RECTHEIGHT(rc)            (rc.bottom - rc.top)

//CHANGE THEM TO 0 when thats maximized!
const int TOPEXTENDWIDTH = 48;
const int LEFTEXTENDWIDTH = 8;
const int RIGHTEXTENDWIDTH = 8;
const int BOTTOMEXTENDWIDTH = 8;

HBRUSH RED_BRUSH = CreateSolidBrush(RGB(237, 28, 36));
HBRUSH DARKBLUE_BRUSH = CreateSolidBrush(RGB(26, 31, 96));
HBRUSH PURPLE_BRUSH = CreateSolidBrush(RGB(163, 73, 164));
HBRUSH LIGHTPURPLE_BRUSH_1 = CreateSolidBrush(RGB(189, 106, 189));
HBRUSH LIGHTPURPLE_BRUSH_2 = CreateSolidBrush(RGB(255, 174, 201));
HBRUSH DARKEST_BRUSH = CreateSolidBrush(RGB(0, 0, 0));

LRESULT HitTestNCA(HWND hWnd, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain( _In_opt_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_opt_ LPTSTR lpCmdLine, _In_opt_ int nCmdShow)
{
    GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR           gdiplusToken;

    // Initialize GDI+.
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

    hInst = hInstance;
    WNDCLASSEX wcex =
    {
        sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInst, LoadIcon(NULL, IDI_APPLICATION),
        LoadCursor(NULL, IDC_ARROW), (HBRUSH)GetStockObject(BLACK_BRUSH), NULL, TEXT("WindowClass"), NULL,
    };
    if (!RegisterClassEx(&wcex))
        return MessageBox(NULL, L"Cannot register class !", L"Error", MB_ICONERROR | MB_OK);
    int nX = (GetSystemMetrics(SM_CXSCREEN) - nWidth) / 2, nY = (GetSystemMetrics(SM_CYSCREEN) - nHeight) / 2;
    HWND hWnd = CreateWindowEx(0, wcex.lpszClassName, TEXT("Test"), WS_OVERLAPPEDWINDOW, nX, nY, nWidth, nHeight, NULL, NULL, hInst, NULL);
    
    if (!hWnd) return MessageBox(NULL, L"Cannot create window !", L"Error", MB_ICONERROR | MB_OK);

    //NO SHADOW
    SystemParametersInfoA(SPI_SETDROPSHADOW,0,(const PVOID) false,SPIF_SENDWININICHANGE);

    ShowWindow(hWnd, SW_SHOWNORMAL);
    UpdateWindow(hWnd);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return (int)msg.wParam;
}
void FillRoundRectangle(Gdiplus::Graphics* g, Brush* p, Gdiplus::Rect& rect, UINT8 radius[4])
{
    if (g == NULL) return;
    GraphicsPath path;
    //TOP RIGHT
    path.AddLine(rect.X + radius[0], rect.Y, rect.X + rect.Width - (radius[0] * 2), rect.Y);
    path.AddArc(rect.X + rect.Width - (radius[0] * 2), rect.Y, radius[0] * 2, radius[0] * 2, 270, 90);

    //BOTTOM RIGHT
    path.AddLine(rect.X + rect.Width, rect.Y + radius[1], rect.X + rect.Width, rect.Y + rect.Height - (radius[1] * 2));
    path.AddArc(rect.X + rect.Width - (radius[1] * 2), rect.Y + rect.Height - (radius[1] * 2), radius[1] * 2, radius[1] * 2, 0, 90);

    //BOTTOM LEFT
    path.AddLine(rect.X + rect.Width - (radius[2] * 2), rect.Y + rect.Height, rect.X + radius[2], rect.Y + rect.Height);
    path.AddArc(rect.X, rect.Y + rect.Height - (radius[2] * 2), radius[2] * 2, radius[2] * 2, 90, 90);

    //TOP LEFT
    path.AddLine(rect.X, rect.Y + rect.Height - (radius[3] * 2), rect.X, rect.Y + radius[3]);
    path.AddArc(rect.X, rect.Y, radius[3] * 2, radius[3] * 2, 180, 90);
    path.CloseFigure();

    g->FillPath(p, &path);
}
VOID OnPaint(HDC hdc,int width, int height)
{
    Graphics graphics(hdc);

    graphics.SetSmoothingMode(SmoothingMode::SmoothingModeHighQuality);

    graphics.SetCompositingQuality(CompositingQuality::CompositingQualityInvalid);
    graphics.SetPixelOffsetMode(PixelOffsetMode::PixelOffsetModeHighQuality);
    SolidBrush mySolidBrush(Color(255, 255, 0, 0)); ;
    Gdiplus::Rect rect1;
    rect1.X = 0;
    rect1.Y = TOPEXTENDWIDTH;
    rect1.Width = width;
    rect1.Height = height- TOPEXTENDWIDTH-111;
    UINT8 rad[4]{ 0,12,12,0 };
    FillRoundRectangle(&graphics, &mySolidBrush, rect1, rad);
    SolidBrush DarkSolidBrush(Color(255, 0, 1, 0)); ;

    Gdiplus::Rect rectX = {0,455,55,55};
    graphics.FillEllipse(&DarkSolidBrush,rectX);
}


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?
    static HICON hIcon = NULL;

    fCallDWP = !DwmDefWindowProc(hWnd, message, wParam, lParam, &lRet);
    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);

        HMODULE hDLL = LoadLibrary(L"Setupapi.dll");
        if (hDLL)
        {
            hIcon = (HICON)LoadImage(hDLL, MAKEINTRESOURCE(2), IMAGE_ICON, 32, 32, LR_DEFAULTCOLOR | LR_SHARED);
        }
        SetWindowLong(hWnd, GWL_EXSTYLE,
            GetWindowLong(hWnd, GWL_EXSTYLE) | WS_EX_LAYERED);
        /*Use pointer to function*/
        SetLayeredWindowAttributes(hWnd, 0,
            (255 * 70) / 100, LWA_ALPHA);
        fCallDWP = true;
        lRet = 0;
    }

    // Handle window activation.
    if (message == WM_ACTIVATE)
    {
        // Extend the frame into the client area.
        MARGINS margins;
        margins.cxLeftWidth = 0;
        margins.cxRightWidth = 0;
        margins.cyBottomHeight = 0;
        margins.cyTopHeight = 0;
        hr = DwmExtendFrameIntoClientArea(hWnd, &margins);

        if (!SUCCEEDED(hr))
        {
            // Handle error.
        }

        fCallDWP = true;
        lRet = 0;
    }

    if (message == WM_PAINT)
    {
        PAINTSTRUCT ps;
        BITMAP bm;
        RECT rect, rectCaptionButtonBounds, rectText,myRect,ContentRect,ClientRect,CaptionBorderBottom,  rect_EXIT_BTN, rect_RESTORE_BTN, rect_MINIMIZE_BTN;

        HFONT windowTitleText;

        GetClientRect(hWnd,&ClientRect);
        BeginPaint(hWnd, &ps);

        SetGraphicsMode(ps.hdc, GM_ADVANCED);

        SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 255, LWA_COLORKEY);
        SetBkMode(ps.hdc, TRANSPARENT);
        if (SUCCEEDED(DwmGetWindowAttribute(hWnd, DWMWA_CAPTION_BUTTON_BOUNDS, &rectCaptionButtonBounds, sizeof(rectCaptionButtonBounds))))
        {
            GetClientRect(hWnd, &rect);
            
            //HRGN hrgn_cptBtmBrdrRND = CreateRoundRectRgn(0, 0, RECTWIDTH(ClientRect), RECTHEIGHT(ClientRect), 16, 16);

            //FillRgn(ps.hdc, hrgn_cptBtmBrdrRND, DARKBLUE_BRUSH);

            HRGN hrgn = CreateRectRgn(0, 0, RECTWIDTH(ClientRect), TOPEXTENDWIDTH);

            FillRgn(ps.hdc, hrgn, PURPLE_BRUSH);

            DrawIconEx(ps.hdc, rect.right - (rectCaptionButtonBounds.right - rectCaptionButtonBounds.left) - 32, 0, hIcon, 32, 32, 0, NULL, DI_NORMAL);
            SetRect(&myRect, LEFTEXTENDWIDTH, 10, RECTWIDTH(rect)-200, TOPEXTENDWIDTH);
            SetTextColor(ps.hdc, RGB(1, 0, 0));
            DrawText(ps.hdc,L"test",-1,&myRect, DT_SINGLELINE | DT_RIGHT);


            SetTextColor(ps.hdc, RGB(255, 255, 255));
            WCHAR wsText[255] = L"ARMNET";
            SetRect(&rectText, LEFTEXTENDWIDTH, 0, RECTWIDTH(rect), TOPEXTENDWIDTH);


                windowTitleText =
                CreateFontA
                (
                  32,
                  0,
                  GM_ADVANCED,
                  0,
                  FW_DONTCARE,
                  false,
                  false,
                  false,
                  DEFAULT_CHARSET,
                  OUT_OUTLINE_PRECIS,
                  CLIP_DEFAULT_PRECIS,
                  CLEARTYPE_QUALITY, //BETTER BLENDING THAN ANTIALIASED
                  VARIABLE_PITCH,
                  "RETRO COMPUTER");

                SelectObject(ps.hdc, windowTitleText);
            DrawText(ps.hdc, wsText, -1, &rectText, DT_SINGLELINE | DT_VCENTER);
            DeleteObject(windowTitleText);
            DeleteObject(hrgn);

        }

        //CONTENT AREA
        //SetRect(&ContentRect, 0, TOPEXTENDWIDTH, RECTWIDTH(ClientRect) - 0, RECTHEIGHT(ClientRect) - 0);
        //FillRect(ps.hdc, &ContentRect, DARKBLUE_BRUSH);

        HRGN hrgn_cptBtmBrdr = CreateRectRgn(0, TOPEXTENDWIDTH-1, RECTWIDTH(ClientRect), TOPEXTENDWIDTH);
        FillRgn(ps.hdc, hrgn_cptBtmBrdr, CreateSolidBrush(RGB(132, 68, 133)));

        hrgn_cptBtmBrdr = CreateRectRgn(0, TOPEXTENDWIDTH - 2, RECTWIDTH(ClientRect), TOPEXTENDWIDTH-1);
        FillRgn(ps.hdc, hrgn_cptBtmBrdr,CreateSolidBrush(RGB(185, 91, 186)));

        //BUTTONS
        hrgn_cptBtmBrdr = CreateRectRgn(RECTWIDTH(ClientRect)-32, 0, RECTWIDTH(ClientRect), 32);
        FillRgn(ps.hdc, hrgn_cptBtmBrdr, CreateSolidBrush(RGB(11, 11, 111)));
        hrgn_cptBtmBrdr = CreateRectRgn(RECTWIDTH(ClientRect) - 64, 0, RECTWIDTH(ClientRect)-32, 32);
        FillRgn(ps.hdc, hrgn_cptBtmBrdr, CreateSolidBrush(RGB(111, 11, 111)));
        hrgn_cptBtmBrdr = CreateRectRgn(RECTWIDTH(ClientRect) - 96, 0, RECTWIDTH(ClientRect)-64, 32);
        FillRgn(ps.hdc, hrgn_cptBtmBrdr, CreateSolidBrush(RGB(11, 111, 11)));
        
        OnPaint(ps.hdc, RECTWIDTH(ClientRect), RECTHEIGHT(ClientRect));

        DeleteObject(hrgn_cptBtmBrdr);
        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 + 1;
        pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0;
        pncsp->rgrc[0].right = pncsp->rgrc[0].right - 1;
        pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 1;

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

    if (message == WM_SIZE)
    {
        if (unsigned int(wParam) == SIZE_MAXIMIZED) {

        }
        else
        {

        }
    }

    if (message == WM_GETMINMAXINFO)
    {
        LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;
        lpMMI->ptMinTrackSize.x = 800;
        lpMMI->ptMinTrackSize.y = 600;
    }

    if (message == WM_DESTROY)
    PostQuitMessage(0);
    *pfCallDWP = fCallDWP;

    return lRet;
}

// 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)  )
    {
        if((ptMouse.x < rcWindow.right - 100) || (ptMouse.y > rcWindow.top + 32)){
        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] =
    {
        { fOnResizeBorder ? HTTOPLEFT : HTLEFT, fOnResizeBorder ? HTTOP : HTCAPTION, fOnResizeBorder ? HTTOPRIGHT : HTRIGHT },
        { HTLEFT,       HTNOWHERE,     HTRIGHT },
        { HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
    };

    return hitTests[uRow][uCol];
}

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);
        lRet = DefWindowProc(hWnd, message, wParam, lParam);
    }
    return lRet;
}

好吧,在寻找解决方案之后,最后我想我找到了答案。

解决方案: 抗锯齿问题的解决方案可能是捕获整个 window 的背景,并通过 排除 将其从捕获中捕获。此时您将不需要分层 window,因为您正在使用位图绘制背景。 HDC 将能够将其与背景混合。希望 Windows 10 2004 版本你有一个选项叫做:

WDA_EXCLUDEFROMCAPTURE

用法:

SetWindowDisplayAffinity(hWnd, WDA_EXCLUDEFROMCAPTURE); //At creation time

来源: https://blogs.windows.com/windowsdeveloper/2019/09/16/new-ways-to-do-screen-capture/

之后您可以用位图绘制背景,然后在其上绘制所有内容。但是,这产生了低性能,我没有受益。尽管如此,绘制时仍然有效并产生 ANTI-ALIASED 外观。对于性能问题,Direct2D 可用于从返回的位图中绘制。

示例:

int DsktpBkSS(HWND hWnd) {

    HDC hdcScreen;
    HDC hdcWindow;
    HDC hdcMemDC = NULL;
    HBITMAP hbmScreen = NULL;
    BITMAP bmpScreen;
    RECT windowPos;
    SetWindowDisplayAffinity(hWnd, WDA_EXCLUDEFROMCAPTURE);
    GetWindowRect(hWnd, &windowPos);

    // Retrieve the handle to a display device context for the client 
    // area of the window. 
    hdcScreen = GetDC(NULL);
    hdcWindow = GetDC(hWnd);

    // Create a compatible DC which is used in a BitBlt from the window DC
    hdcMemDC = CreateCompatibleDC(hdcWindow);

    if (!hdcMemDC)
    {
        MessageBox(hWnd, L"CreateCompatibleDC has failed", L"Failed", MB_OK);
    }

    // Get the client area for size calculation
    RECT rcClient;
    GetClientRect(hWnd, &rcClient);

    //This is the best stretch mode
    SetStretchBltMode(hdcWindow, HALFTONE);

    //The source DC is the entire screen and the destination DC is the current window (HWND)
    if (!StretchBlt(hdcWindow,
        0, 0,
        GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
        hdcScreen,
        windowPos.left+1,windowPos.top,
        GetSystemMetrics(SM_CXSCREEN),
        GetSystemMetrics(SM_CYSCREEN),
        SRCCOPY))
    {
        MessageBox(hWnd, L"StretchBlt has failed", L"Failed", MB_OK);
    }

    // Create a compatible bitmap from the Window DC
    hbmScreen = CreateCompatibleBitmap(hdcWindow, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top);

    if (!hbmScreen)
    {
        MessageBox(hWnd, L"CreateCompatibleBitmap Failed", L"Failed", MB_OK);
    }

    // Select the compatible bitmap into the compatible memory DC.
    if(hdcMemDC && hbmScreen){
    SelectObject(hdcMemDC, hbmScreen);
    }
    // Bit block transfer into our compatible memory DC.
    if(hdcMemDC)
    if (!BitBlt(hdcMemDC,
        0, 0,
        rcClient.right - rcClient.left, rcClient.bottom - rcClient.top,
        hdcWindow,
        0, 0,
        SRCCOPY))
    {
        MessageBox(hWnd, L"BitBlt has failed", L"Failed", MB_OK);
    }

    // Get the BITMAP from the HBITMAP
    if(hbmScreen)
    GetObjectW(hbmScreen, sizeof(BITMAP), &bmpScreen);

    if (hbmScreen)DeleteObject(hbmScreen);
    if (hdcMemDC)DeleteObject(hdcMemDC);
    ReleaseDC(NULL, hdcScreen);
    ReleaseDC(hWnd, hdcWindow);

    return 0;
}

稍后,

(...){
if(message==WM_PAINT)
{
PAINTSTRUCT ps;
BeginPaint(hWnd, &ps);
Graphics graphics(ps.hdc);
/*SET SMOOTHING (AA)*/
graphics.SetSmoothingMode(SmoothingMode::SmoothingModeHighQuality);
if(DsktpBkSS(hWnd))
{
/*DRAW ROUNDED RECTANGLE*/
}
}
//...
};

对于使用“Desktop Window Manager”绘制的 window 还有一件更重要的事情,当从左侧调整大小时太快且连续会产生“闪烁”一段时间后。在 Microsoft 文档中,建议使用 StretchBlt(...) 进行绘图,因为 GDI+ 会导致此问题。

DWM提到的闪烁:https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-stretchblt

"Use BitBlt or StretchBlt function instead of Windows GDI+ to present your drawing for rendering. GDI+ renders one scan line at a time with software rendering. This can cause flickering in your applications."