You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动