Delphi 11(64位Windows)下使用Direct2D在TPaintBox绘图时出现闪烁问题
Delphi 11(64位Windows)下使用Direct2D在TPaintBox绘图时出现闪烁问题
兄弟,我之前在Delphi 11 64位环境下用Direct2D操作TPaintBox实时绘图时,也碰到过一模一样的闪烁问题!折腾了好一阵子才摸清楚根源,主要是系统默认的背景擦除和未正确使用双缓冲导致的,给你分享几个亲测有效的解决办法:
1. 禁用TPaintBox的自动背景擦除
默认情况下,TPaintBox每次重绘前都会调用系统的EraseBackground方法擦除原有内容,这一步就是闪烁的元凶之一——擦除和新绘制之间的间隙会让屏幕闪一下。我们可以通过拦截WM_ERASEBKGND消息来跳过这一步:
在你的Form类里添加消息处理函数:
type TFormMain = class(TForm) PaintBox: TPaintBox; procedure PaintBoxPaint(Sender: TObject); procedure PaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); private // 拦截背景擦除消息 procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND; public { Public declarations } end; // 实现消息处理 procedure TFormMain.WMEraseBkgnd(var Message: TWMEraseBkgnd); begin // 返回1告诉系统:我们自己搞定背景,不用你擦! Message.Result := 1; end;
2. 用Direct2D离屏渲染目标实现双缓冲
Direct2D本身支持离屏渲染——先把所有要画的内容绘制到一个“虚拟画布”(离屏渲染目标)上,然后一次性把这个画布复制到TPaintBox的真实渲染目标上,这样就避免了实时绘制时的逐帧闪烁。
步骤1:声明相关变量
在Form类的私有部分添加Direct2D对象:
private FRenderTarget: ID2D1RenderTarget; // 关联PaintBox的渲染目标 FOffscreenRT: ID2D1BitmapRenderTarget; // 离屏渲染目标(双缓冲画布) FBitmap: ID2D1Bitmap; // 离屏画布的位图对象 procedure InitDirect2D; // 初始化Direct2D procedure UpdateOffscreenContent; // 更新离屏画布内容
步骤2:初始化Direct2D
procedure TFormMain.InitDirect2D; var D2DFactory: ID2D1Factory; HwndRTProps: D2D1_HWND_RENDER_TARGET_PROPERTIES; BitmapProps: D2D1_BITMAP_RENDER_TARGET_PROPERTIES; begin // 创建Direct2D工厂 D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, D2DFactory); // 配置关联PaintBox的渲染目标 ZeroMemory(@HwndRTProps, SizeOf(HwndRTProps)); HwndRTProps.hwnd := PaintBox.Handle; HwndRTProps.pixelSize.width := PaintBox.ClientWidth; HwndRTProps.pixelSize.height := PaintBox.ClientHeight; HwndRTProps.presentOptions := D2D1_PRESENT_OPTIONS_IMMEDIATE; D2DFactory.CreateHwndRenderTarget(D2D1_RENDER_TARGET_PROPERTIES_DEFAULT, HwndRTProps, FRenderTarget); // 创建离屏渲染目标(双缓冲画布) BitmapProps.pixelFormat := D2D1_PIXEL_FORMAT(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED); BitmapProps.dpiX := 96; BitmapProps.dpiY := 96; BitmapProps.size := D2D1_SIZE_U(PaintBox.ClientWidth, PaintBox.ClientHeight); FRenderTarget.CreateBitmapRenderTarget(BitmapProps, FOffscreenRT); end;
步骤3:更新离屏画布内容
所有绘图逻辑都放到这里,先画到离屏画布:
procedure TFormMain.UpdateOffscreenContent; var Brush: ID2D1SolidColorBrush; begin if not Assigned(FOffscreenRT) then Exit; FOffscreenRT.BeginDraw; try // 先清空离屏画布(用你需要的背景色) FOffscreenRT.Clear(D2D1_COLOR_F(0.9, 0.9, 0.9, 1.0)); // --- 这里写你的绘图逻辑,比如画图形、文本 --- // 示例:画一个红色矩形 FOffscreenRT.CreateSolidColorBrush(D2D1_COLOR_F(1.0, 0, 0, 1.0), Brush); FOffscreenRT.FillRectangle(D2D1_RECT_F(100, 100, 200, 200), Brush); finally FOffscreenRT.EndDraw; end; // 触发PaintBox重绘 PaintBox.Invalidate; end;
步骤4:在Paint事件中绘制到屏幕
把离屏画布的内容一次性复制到TPaintBox:
procedure TFormMain.PaintBoxPaint(Sender: TObject); begin // 初始化Direct2D(第一次绘图时执行) if not Assigned(FRenderTarget) then InitDirect2D; if Assigned(FOffscreenRT) then begin FOffscreenRT.GetBitmap(FBitmap); FRenderTarget.BeginDraw; try // 把离屏位图绘制到当前渲染目标 FRenderTarget.DrawBitmap(FBitmap, D2D1_RECT_F(0, 0, PaintBox.ClientWidth, PaintBox.ClientHeight)); finally FRenderTarget.EndDraw; end; end; end;
额外处理:PaintBox大小变化
当PaintBox尺寸改变时,需要重新创建离屏渲染目标,所以给PaintBox添加OnResize事件:
procedure TFormMain.PaintBoxResize(Sender: TObject); begin // 释放旧的离屏资源 FOffscreenRT := nil; FBitmap := nil; // 重新调整渲染目标大小并初始化 if Assigned(FRenderTarget) then begin var Size := D2D1_SIZE_U(PaintBox.ClientWidth, PaintBox.ClientHeight); FRenderTarget.Resize(Size); InitDirect2D; end; // 更新内容 UpdateOffscreenContent; end;
3. 最小化重绘区域
不要每次都重绘整个PaintBox!比如在鼠标移动更新画面时,只标记需要变化的小区域为无效,而不是调用Invalidate()(整个区域重绘)。
示例:在MouseMove事件中只重绘鼠标周围的区域:
procedure TFormMain.PaintBoxMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); var UpdateRect: TRect; begin // 只标记鼠标附近20x20的区域需要重绘 UpdateRect := Rect(X-10, Y-10, X+10, Y+10); PaintBox.InvalidateRect(UpdateRect); // 更新离屏内容后,同样只重绘该区域 UpdateOffscreenContent; PaintBox.InvalidateRect(UpdateRect); end;
把这三个方法结合起来,基本就能彻底解决闪烁问题啦!核心思路就是:减少不必要的背景擦除,用双缓冲一次性完成绘制,以及尽量缩小重绘范围。
备注:内容来源于stack exchange,提问作者Achille PACE




