在图形用户界面(GUI)编程中,实现一个半透明的选择矩形是一个常见的需求。本文将介绍如何在C++中创建一个自定义的半透明选择矩形窗口,以及如何使用它。
在传统的API函数中,并没有一个直接的函数可以创建一个半透明的选择矩形。因此,需要自己实现这样的功能。一个分层窗口(layered window)可以很好地扮演这个角色,因为它可以以alpha混合的方式显示。这样,就不需要控制父视图的重绘区域,这在动态改变尺寸时是必要的。
首先,需要为分层窗口设置一个特殊的扩展窗口样式WS_EX_LAYERED。此外,还需要记住,分层窗口不能是子窗口。由于选择矩形没有标题栏,可以指定普通窗口样式为WS_POPUP:
bool CCoverWnd::Create(CWnd* pParentView) {
    bool bResult = false;
    if (pParentView) {
        CreateEx(WS_EX_LAYERED, _T("STATIC"), NULL, WS_POPUP | WS_VISIBLE, -1, -1, 0, 0, pParentView->GetSafeHwnd(), NULL);
        if (GetSafeHwnd()) {
            pParentView->GetClientRect(m_cParentCliRect);
            pParentView->ClientToScreen(m_cParentCliRect);
            pParentView->SetFocus();
            bResult = true;
        }
    }
    return bResult;
}
    
现在可以创建覆盖窗口,并且需要一种方法来显示它在某个位置、某个大小以及带有一些绘制的表面。
有一个API函数可以更新分层窗口的位置和内容:
BOOL WINAPI UpdateLayeredWindow(HWND hWnd, HDC hdcDst, POINT *pptDst, SIZE *psize, HDC hdcSrc, POINT *pptSrc, COLORREF crKey, BLENDFUNCTION *pblend, DWORD dwFlags);
    
参数pblend是一个类型为BLENDFUNCTION的结构体指针,它定义了混合参数:
typedef struct _BLENDFUNCTION {
    BYTE BlendOp;
    BYTE BlendFlags;
    BYTE SourceConstantAlpha;
    BYTE AlphaFormat;
} BLENDFUNCTION, *PBLENDFUNCTION;
    
现在可以提供自己的函数来显示它:
void CCoverWnd::ShowAt(const CRect& crPos) {
    if (GetSafeHwnd()) {
        CRect cMoveRect(crPos);
        cMoveRect.NormalizeRect();
        CRect cIntersectRect(cMoveRect);
        cIntersectRect.IntersectRect(m_cDrawRect, cMoveRect);
        int iWidth(cMoveRect.Width());
        int iHeight(cMoveRect.Height());
        HDC hdcScreen = ::GetDC(NULL);
        HDC hDC = ::CreateCompatibleDC(hdcScreen);
        BITMAPINFO sBI = {0};
        sBI.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
        sBI.bmiHeader.biWidth = iWidth; 
        sBI.bmiHeader.biHeight = iHeight; 
        sBI.bmiHeader.biPlanes = 1; 
        sBI.bmiHeader.biBitCount = 32; 
        sBI.bmiHeader.biCompression = BI_RGB;
        HBITMAP hBmp = ::CreateDIBSection(hDC, &sBI, DIB_RGB_COLORS, NULL, NULL, 0);
        HBITMAP hBmpOld = (HBITMAP) ::SelectObject(hDC, hBmp);
        bool bFillAlphaOK = FillAlpha(hBmp);
        if (!bFillAlphaOK) {
            FillRGB(hDC, iWidth, iHeight);
        }
        BLENDFUNCTION blend = {0};
        blend.BlendOp = AC_SRC_OVER;
        blend.SourceConstantAlpha = bFillAlphaOK ? 160 : 64;
        blend.AlphaFormat = bFillAlphaOK ? AC_SRC_ALPHA : 0;
        POINT ptPos = {cIntersectRect.left, cIntersectRect.top};
        SIZE sizeWnd = {cIntersectRect.Width(), cIntersectRect.Height()};
        POINT ptSrc = {cIntersectRect.left - cMoveRect.left, cIntersectRect.top - cMoveRect.top};
        ::UpdateLayeredWindow(m_hWnd, hdcScreen, &ptPos, &sizeWnd, hDC, &ptSrc, 0, &blend, ULW_ALPHA);
        ::SelectObject(hDC, hBmpOld);
        ::DeleteObject(hBmp);
        ::DeleteDC(hDC);
        ::ReleaseDC(NULL, hdcScreen);
    }
}
    
填充过程被封装在CCoverWnd中,可以在源文件中查看。
只需要在CWnd派生对象中实例化并创建一个CCoverWnd类的对象,它应该在其表面上提供选择。
class CCoverTestDlg : public CDialog {
    bool m_bCaptured;
    CPoint m_cpStart, m_cpEnd;
    CCoverWnd m_cCoverWnd;
    ...
};
    
现在可以显示覆盖层,例如:
void CCoverTestDlg::ShowCover() {
    if (!m_cCoverWnd.GetSafeHwnd()) {
        m_cCoverWnd.Create(this);
    }
    if (m_cCoverWnd.GetSafeHwnd()) {
        CRect cShowRect(m_cpStart, m_cpEnd);
        ClientToScreen(cShowRect);
        m_cCoverWnd.ShowAt(cShowRect);
    }
}
    
...以及“隐藏”它:
void CCoverTestDlg::DestroyCover() {
    if (m_cCoverWnd.GetSafeHwnd()) {
        m_cCoverWnd.DestroyWindow();
    }
}
    
感谢SledgeHammer01的建议,直接写入颜色数据,而不是使用函数CBitmap::SetBitmapBits(..)。
bool CCoverWnd::FillAlpha(HBITMAP hBmp) {
    bool bResult = false;
    if (hBmp) {
        BITMAP bmp;
        GetObject(hBmp, sizeof(BITMAP), &bmp);
        DWORD dwCount = bmp.bmWidthBytes * bmp.bmHeight;
        if (dwCount >= sizeof(DWORD)) {
            DWORD* pcBitsWords = (DWORD*) bmp.bmBits;
            if (pcBitsWords) {
                DWORD dwIndex(dwCount / sizeof(DWORD));
                DWORD dwUp = bmp.bmWidth;
                DWORD dwDn = dwIndex - dwUp;
                DWORD dwR = bmp.bmWidth - 1;
                while (dwIndex--) {
                    DWORD dwSides = dwIndex % bmp.bmWidth;
                    if (dwIndex < dwUp || dwIndex > dwDn || 0 == dwSides || dwR == dwSides) {
                        pcBitsWords[dwIndex] = sm_clrPenA;
                    } else {
                        pcBitsWords[dwIndex] = sm_clrBrushA;
                    }
                }
                bResult = true;
            }
        }
    }
    return bResult;
}