利用双缓冲技术将位图像素数据写到DIB位图中并在指定的窗口显示(主要使用CreateDIBSection和BitBlt方法)

本文介绍了一种通过双缓冲技术解决屏幕刷新时出现的闪烁问题的方法。具体实现了从缓冲区到屏幕的位图绘制过程,包括创建兼容的内存设备上下文、创建设备独立位图并进行像素填充,最后通过BitBlt函数将内存中的图像复制到屏幕上。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

至于为什么要使用双缓冲技术,大家可以查阅相关的资料,这里我只提一点,就是解决屏幕刷新频繁导致闪烁的问题。

下面是相关的代码实现:(这里我保存的DIB位图的大小为320*240,深度为24位)

void SaveToBmpAndBlend(BYTE *buffer)
{
	HDC hdc;
	//获取屏幕显示DC        
	hdc = GetDC(m_hWnd);
	//创建内存DC
	HDC hdcMem = CreateCompatibleDC(hdc);
	//创建一个bmp内存空间
	HBITMAP hBmp;
	
	BITMAPINFO bmpinfo;
	bmpinfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
	bmpinfo.bmiHeader.biWidth=320;
	bmpinfo.bmiHeader.biHeight=240;
	bmpinfo.bmiHeader.biPlanes=1;
	bmpinfo.bmiHeader.biBitCount=24;
	bmpinfo.bmiHeader.biCompression=BI_RGB;
	bmpinfo.bmiHeader.biSizeImage=320*240*3;
	bmpinfo.bmiHeader.biXPelsPerMeter=0;
	bmpinfo.bmiHeader.biClrImportant=0;
	bmpinfo.bmiHeader.biClrUsed=0;
	BYTE* pDibs = NULL;
        //需要注意,当第5个参数为NULL时,PDibs指向的是一块新申请的内存,需要在外部将缓冲数据复制到这块新内存中。
	hBmp=CreateDIBSection(hdcMem,&bmpinfo,DIB_RGB_COLORS,(void**) &pDibs,NULL,0);
	for (int i=0;i<320*240*3;i++)
	{
		pDibs[i]=buffer[i];
		
	}
	//将bmp内存空间分配给内存DC
	HGDIOBJ hOldSel = SelectObject(hdcMem,hBmp);
	//将内存DC的内容复制到屏幕显示DC中,完成显示
	BitBlt(hdc,0,0,320,240,hdcMem,0,0,SRCCOPY);//SRCCOPY 完全覆盖
	//清除资源
	SelectObject(hdcMem,hOldSel);    
	DeleteDC(hdcMem); 
	ReleaseDC(m_hWnd, hdc);
}


<think>我们之前实现的水印类CWatermarkWnd,在OnPaint中使用双缓冲技术,但出现背景色从透明变为黑色的问题。这是因为在双缓冲绘图过程中,内存DC的位图初始状态是黑色(0,0,0)的,而我们需要的是透明背景。因此,我们需要将背景初始化为透明。解决方案:1.创建兼容的内存DC位图后,将背景初始化为透明(而不是黑色或白色)。2.使用GDI+绘制时,需要确保绘制的背景是透明的。可以通过设置graphics.Clear为透明色来实现。具体修改步骤如下:在CWatermarkWnd::OnPaint()函数中:-创建内存DC位图后,我们需要将位图的背景设置为透明。但是,我们不能简单地使用FillSolidRect填充白色,因为那样会变成不透明的背景。-正确的方法使用GDI+的Clear函数,传入一个完全透明的颜色(Alpha=0)。但是,我们注意到在原有的代码中,我们已经使用了:graphics.Clear(Gdiplus::Color(0,0,0,0));//透明背景这行代码应该已经将背景设置为透明了。然而,为什么还会出现黑色背景呢?可能是因为在将内存位图复制到屏幕DC时,黑色背景(未初始化的位图)被复制了过去。因为我们在创建位图后没有初始化,而紧接着我们使用GDI+绘制,但是GDI+的Clear操作实际上只是将位图设置为透明背景(在内存中是ARGB=0,0,0,0),但当我们使用BitBlt进行复制时,SRCCOPY复制方式会直接将这个内存位图(包含透明信息)复制到屏幕。但是,透明背景在屏幕上显示为黑色?这不对,因为BitBlt不支持透明混合。因此,问题在于:我们创建了一个分层窗口(WS_EX_LAYERED),但是我们在OnPaint中直接使用BitBlt绘制到屏幕上,这种方式无法利用分层窗口的透明特性。我们应该使用UpdateLayeredWindow来实现透明效果。所以,我们需要改变绘制策略:不使用OnPaintCPaintDC,而是使用UpdateLayeredWindow来更新整个窗口。这样我们就可以直接使用一个带有Alpha通道的位图来更新窗口。因此,我们需要重新设计OnPaint函数,改为使用UpdateLayeredWindow的方式。步骤如下:1.在创建窗口时,我们使用了WS_EX_LAYERED样式。2.在绘制时,我们不使用OnPaint,而是自己创建一个绘制函数,在需要更新时调用。3.绘制函数中,创建一个兼容的DC,创建一个32位色的位图(支持Alpha通道)。4.在这个位图上,使用GDI+绘制透明背景水印文字。5.然后使用UpdateLayeredWindow函数更新窗口。但是,由于我们使用了MFC,窗口是CWnd,我们可以重写OnPaint,但是不要用CPaintDC,而是自己创建DC。或者,我们可以直接使用UpdateLayeredWindow来更新。修改方案:我们将不再使用OnPaint,而是自己实现一个Redraw函数,在需要重绘的时候调用(如初始创建、文字改变、窗口大小改变时)。具体修改如下:第一步:从消息映射中移除ON_WM_PAINT()且移除OnPaint函数。第二步:添加一个Redraw函数,用于重新绘制分层窗口。第三步:在Redraw函数中,创建一个32位色的位图使用GDI+绘制,然后调用UpdateLayeredWindow。第四步:在适当的地方调用Redraw(如在Create函数最后、UpdateWatermark、OnSize等)。由于改动较大,我们将重新写一个CWatermarkWnd类的绘制部分。具体代码:在CWatermarkWnd类中:1.移除消息映射中的ON_WM_PAINT2.添加成员函数voidRedraw();3.修改OnSize,不再调用基类的OnSize,而是调用Redraw4.修改UpdateWatermark,调用Redraw而不是InvalidateRedraw函数的实现:```cppvoidCWatermarkWnd::Redraw(){//如果没有创建窗口,则返回if(!GetSafeHwnd())return;CRectrect;GetWindowRect(&rect);CPointptWinScreen(rect.TopLeft());CPointptOrigin(0,0);CSizesizeWindow(rect.Width(),rect.Height());//创建内存DCCClientDCdcScreen(NULL);CDCmemDC;memDC.CreateCompatibleDC(&dcScreen);//创建32位位图CBitmapbmp;BITMAPINFObmi;memset(&bmi,0,sizeof(BITMAPINFO));bmi.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);bmi.bmiHeader.biWidth=rect.Width();bmi.bmiHeader.biHeight=rect.Height();bmi.bmiHeader.biPlanes=1;bmi.bmiHeader.biBitCount=32;bmi.bmiHeader.biCompression=BI_RGB;bmi.bmiHeader.biSizeImage=0;LPVOIDpBits=NULL;HBITMAPhBmp=::CreateDIBSection(memDC.m_hDC,&bmi,DIB_RGB_COLORS,&pBits,NULL,0);if(hBmp==NULL)return;bmp.Attach(hBmp);CBitmap*pOldBmp=memDC.SelectObject(&bmp);//使用GDI+绘制内存DCGdiplus::Graphicsgraphics(memDC);graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);//覆盖模式graphics.SetCompositingQuality(Gdiplus::CompositingQualityHighSpeed);graphics.SetPixelOffsetMode(Gdiplus::PixelOffsetModeNone);graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit);//清除为透明graphics.Clear(Gdiplus::Color(0,0,0,0));//完全透明//设置水印字体、颜色等Gdiplus::Fontfont(L"微软雅黑",m_fontSize,Gdiplus::FontStyleBold,Gdiplus::UnitPixel);BYTEr=GetRValue(m_color);BYTEg=GetGValue(m_color);BYTEb=GetBValue(m_color);Gdiplus::SolidBrushbrush(Gdiplus::Color(m_alpha,r,g,b));//使用传入的透明度//计算位置,居中Gdiplus::RectFlayoutRect(0,0,rect.Width(),rect.Height());Gdiplus::StringFormatformat;format.SetAlignment(Gdiplus::StringAlignmentCenter);format.SetLineAlignment(Gdiplus::StringAlignmentCenter);format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap);//旋转45度Gdiplus::Matrixmatrix;matrix.RotateAt(-45.0f,Gdiplus::PointF(rect.Width()/2.0f,rect.Height()/2.0f));graphics.SetTransform(&matrix);//绘制水印graphics.DrawString(m_strWatermark.GetString(),m_strWatermark.GetLength(),&font,layoutRect,&format,&brush);//恢复变换graphics.ResetTransform();//更新分层窗口BLENDFUNCTIONblendFunc;blendFunc.BlendOp=AC_SRC_OVER;blendFunc.BlendFlags=0;blendFunc.SourceConstantAlpha=255;//整个窗口的透明度,255表示不透明,但具体像素的透明度由位图的alpha通道决定blendFunc.AlphaFormat=AC_SRC_ALPHA;//说明我们带有alpha通道//使用UpdateLayeredWindow更新窗口CPointptDst=ptWinScreen;::UpdateLayeredWindow(GetSafeHwnd(),dcScreen.GetSafeHdc(),&ptDst,&sizeWindow,memDC.GetSafeHdc(),&ptOrigin,0,&blendFunc,ULW_ALPHA);//清理memDC.SelectObject(pOldBmp);bmp.DeleteObject();}```同时,在Create函数中,创建窗口后,我们调用Redraw来绘制水印。注意:我们在OnSize中调整窗口位置后,也需要重绘,所以OnSize改为:```cppvoidCWatermarkWnd::OnSize(UINTnType,intcx,intcy){CWnd::OnSize(nType,cx,cy);if(GetSafeHwnd()){//当窗口大小变化时,我们重新设置整个窗口位置以覆盖父窗口,然后重绘if(CWnd*pParent=GetParent()){CRectparentRect;pParent->GetClientRect(&parentRect);MoveWindow(parentRect);}Redraw();//重绘}}```且,在Create函数的最后,在创建窗口后,我们不需要立即MoveWindow,因为初始创建时也会触发OnSize,所以我们在Create中只需调用一次Redraw:```cppBOOLCWatermarkWnd::Create(...){//...之前的窗口创建代码...if(CreateEx(...)){//初始位置设置if(pParentWnd){CRectrect;pParentWnd->GetClientRect(&rect);MoveWindow(rect);}Redraw();returnTRUE;}returnFALSE;}```这样,我们就使用了UpdateLayeredWindow来绘制水印,且支持透明背景,避免黑色背景问题。同时,因为使用了UpdateLayeredWindow,窗口不会再收到WM_PAINT消息,所以我们的绘制代码在Redraw中,由我们自己触发。另外,我们在GDI+绘制时已经将背景清除为透明,所以不会再出现黑色背景。注意:UpdateLayeredWindow要求窗口是分层窗口(我们创建时使用了WS_EX_LAYERED),且这个函数会改变窗口的属性,使用窗口将不会收到WM_PAINT消息,所以不再使用OnPaint。因此,我们移除了消息映射中的ON_WM_PAINT,删除了OnPaint函数。现在,背景透明问题应该就解决了。但是,由于我们使用了UpdateLayeredWindow,窗口绘制完全由我们自己控制,所以双缓冲也不再需要(因为UpdateLayeredWindow本身就会避免闪烁),所以Redraw函数中我们直接创建了一个32位的位图,然后在这个位图绘制,最后更新到窗口。此外,由于UpdateLayeredWindow需要整个窗口位图,我们每次重绘都是绘制整个窗口,所以对于频繁更新可能需要优化,但水印一般更新不频繁。总结:通过将绘制方式改为UpdateLayeredWindow,使用32位带Alpha通道的位图清除背景为透明,我们解决了背景变黑的问题。其他注意事项:-在初始化GDI+时,我们在构造函数中初始化,在析构函数中也要记得关闭GDI+。-如果水印需要动态变化(比如文字变化),调用UpdateWatermark,它会调用Redraw。关于性能:每次重绘都创建了一个新的位图,如果频繁改变水印,可能会影响性能。但水印通常不频繁更新,所以可以接受。现在,修改后的CWatermarkWnd类应该可以正确显示透明背景的水印。</think> 基于您描述的双缓冲后水印背景变黑的问题,我提供一个完整的解决方案。关键在于正确初始化位图的透明背景处理色彩合成方式: ### 双缓冲透明水印实现方案 ```cpp // WatermarkWnd.cpp void CWatermarkWnd::OnPaint() { CPaintDC dcScreen(this); CRect rect; GetClientRect(&rect); // 创建兼容DC32位位图(支持Alpha通道) CDC memDC; memDC.CreateCompatibleDC(&dcScreen); CBitmap memBitmap; // 关键修改:使用32位色深位图(支持Alpha通道) BITMAPINFO bmi; ZeroMemory(&bmi, sizeof(BITMAPINFO)); bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = rect.Width(); bmi.bmiHeader.biHeight = rect.Height(); bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; // 32位色深 bmi.bmiHeader.biCompression = BI_RGB; void* pBits = NULL; HBITMAP hBitmap = CreateDIBSection( dcScreen.GetSafeHdc(), &bmi, DIB_RGB_COLORS, &pBits, NULL, 0 ); memBitmap.Attach(hBitmap); CBitmap* pOldBitmap = memDC.SelectObject(&memBitmap); // 使用GDI+绘制内存DC Gdiplus::Graphics graphics(memDC.GetSafeHdc()); // 关键修改:清除为完全透明背景 graphics.Clear(Gdiplus::Color(0, 0, 0, 0)); // ARGB(0, 0, 0, 0) // 设置水印字体颜色 Gdiplus::Font font(L"Arial", m_fontSize, Gdiplus::FontStyleBold); BYTE r = GetRValue(m_color); BYTE g = GetGValue(m_color); BYTE b = GetBValue(m_color); Gdiplus::SolidBrush brush(Gdiplus::Color(m_alpha, r, g, b)); // 计算旋转中心点水印位置 Gdiplus::RectF layoutRect(0, 0, rect.Width(), rect.Height()); graphics.TranslateTransform( static_cast<Gdiplus::REAL>(rect.Width() / 2), static_cast<Gdiplus::REAL>(rect.Height() / 2) ); graphics.RotateTransform(-45.0f); // 旋转角度 // 绘制水印文字 graphics.DrawString( m_strWatermark.GetString(), m_strWatermark.GetLength(), &font, layoutRect, &Gdiplus::StringFormat(), &brush ); // 关键修改:使用Alpha混合模式复制到屏幕 BLENDFUNCTION blend; blend.BlendOp = AC_SRC_OVER; blend.BlendFlags = 0; blend.SourceConstantAlpha = 255; // 完全使用位图的Alpha通道 blend.AlphaFormat = AC_SRC_ALPHA; // 标识位图包含Alpha通道 // 使用Alpha混合复制到屏幕(代替BitBlt) dcScreen.AlphaBlend( 0, 0, rect.Width(), rect.Height(), &memDC, 0, 0, rect.Width(), rect.Height(), blend ); // 清理资源 memDC.SelectObject(pOldBitmap); memBitmap.DeleteObject(); } ``` ### 解决关键问题的技术说明 1. **32位色深位图创建** ```cpp bmi.bmiHeader.biBitCount = 32; // 启用Alpha通道 ``` 创建支持Alpha通道的位图(非默认的24位)[^2] 2. **透明背景初始化** ```cpp graphics.Clear(Gdiplus::Color(0, 0, 0, 0)); // ARGB(0,0,0,0) ``` 使用完全透明的背景替代默认黑色背景 3. **Alpha混合复制** ```cpp dcScreen.AlphaBlend(..., AC_SRC_ALPHA); ``` 替代BitBlt,保留源位图的Alpha信息: $$\text{目标颜色} = \text{源颜色} \times \alpha + \text{目标颜色} \times (1 - \alpha)$$ 4. **色彩混合函数设置** ```cpp blend.AlphaFormat = AC_SRC_ALPHA; // 启用源Alpha通道 ``` ### 支持方法修改 1. 更新`Create()`中的窗口样式: ```cpp return CreateEx( WS_EX_LAYERED | WS_EX_TRANSPARENT, className, _T("Watermark Window"), WS_POPUP | WS_VISIBLE, 0, 0, 0, 0, pParentWnd->GetSafeHwnd(), NULL ); ``` 2. 修改透明度应用位置: ```cpp void CWatermarkWnd::SetTransparency(BYTE alpha) { m_alpha = alpha; SetLayeredWindowAttributes(0, m_alpha, LWA_ALPHA); // 全局窗口透明度 } ``` ### 工作原理流程图 ```mermaid graph TD A[创建32位位图] --> B[初始化全透明背景] B --> C[绘制水印内容] C --> D[设置Alpha混合参数] D --> E[使用AlphaBlend输出] E --> F[保留透明度通道] ``` ### 常见问题解决 1. **位图残留问题**:每次`OnPaint()`后释放位图资源 ```cpp memBitmap.DeleteObject(); // 防止GDI资源泄漏 ``` 2. **文字锯齿问题**: ```cpp graphics.SetTextRenderingHint( Gdiplus::TextRenderingHintAntiAlias ); ``` 3. **多显示器支持**: ```cpp // 在OnSize中获取主屏尺寸 CRect screenRect; ::SystemParametersInfo(SPI_GETWORKAREA, 0, &screenRect, 0); ``` ### 性能优化建议 ```cpp // 在类声明中添加 class CWatermarkWnd : public CWnd { protected: Gdiplus::Bitmap* m_pCachedBitmap; // 缓存位图 bool m_bRedrawNeeded; // 重绘标志 }; ``` 通过缓存机制减少重绘次数,仅在文字或尺寸变化时重绘。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值