在图形用户界面(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;
}