VCL中DoubleBuffered属性以及TWinControl的WMPaint的研究

http://www.yangxc.com/?tag=twincontrol

   前几天做的自绘控件,直接从TWinControl继承下来,并且自己处理WM_PAINT消息,发现有一个很严重的问题,在自绘控件上放标准控件的时候,随着自绘控件的刷新,标准控件没有被刷新。初步猜测是自绘控件刷新的时候,把整个区域按自己的意愿画了。想到的解决办法,就是在自绘的时候,把子控件的Rect从自己的DC里Clip掉。

    昨天晚上查看VCL源码,仔细研究了一下TWinControl处理WM_PAINT消息的方法,分析如下:

procedure TWinControl.WMPaint(var Message: TWMPaint);
var
  DC, MemDC: HDC;
  MemBitmap, OldBitmap: HBITMAP;
  PS: TPaintStruct;
begin
  if not FDoubleBuffered or (Message.DC <> 0) then
  begin
    if not (csCustomPaint in ControlState) and (ControlCount = 0) then
      inherited
    else
      PaintHandler(Message);
  end
  else
  begin
    DC := GetDC(0);
    MemBitmap := CreateCompatibleBitmap(DC, ClientRect.Right, ClientRect.Bottom);
    ReleaseDC(0, DC);
    MemDC := CreateCompatibleDC(0);
    OldBitmap := SelectObject(MemDC, MemBitmap);
    try
      DC := BeginPaint(Handle, PS);
      Perform(WM_ERASEBKGND, MemDC, MemDC);
      Message.DC := MemDC;
      WMPaint(Message);
      Message.DC := 0;
      BitBlt(DC, 0, 0, ClientRect.Right, ClientRect.Bottom, MemDC, 0, 0, SRCCOPY);
      EndPaint(Handle, PS);
    finally
      SelectObject(MemDC, OldBitmap);
      DeleteDC(MemDC);
      DeleteObject(MemBitmap);
    end;
  end;
end;

先看if语句:

{ 当未设DoubleBuffered或是控件有自身的DC的时候 }
  if not FDoubleBuffered or (Message.DC <> 0) then
  begin
{ 当非自画并且没有子控件的时候,调用父类的处理函数 }
    if not (csCustomPaint in ControlState) and (ControlCount = 0) then
      inherited
    else
      PaintHandler(Message); { 关键就在这里,一般情况下,直接调用PaintHandler }
  end

    再找到PaintHandler函数,如下:

procedure TWinControl.PaintHandler(var Message: TWMPaint);
var
  I, Clip, SaveIndex: Integer;
  DC: HDC;
  PS: TPaintStruct;
begin
  DC := Message.DC;
  if DC = 0 then DC := BeginPaint(Handle, PS);
  try
{ 当没有子控件的时候,调用PaintWindow }
    if FControls = nil then PaintWindow(DC) else
    begin
{ 可以看到,下面的代码是针对有子控件的情况处理 }
      SaveIndex := SaveDC(DC);
      try
        Clip := SimpleRegion;
{ 遍历子控件 }
        for I := 0 to FControls.Count – 1 do
          with TControl(FControls[I]) do
{ 当子控件Visible并且处于非设计期或是设计期不隐藏并且控件不透明,
              则调用ExlucdeClipRect这个API把子控件的Rect从当前控件中Clip掉 }
            if (Visible and (not (csDesigning in ComponentState) or not (csDesignerHide in ControlState)) or ((csDesigning in ComponentState) and not (csDesignerHide in ControlState)) and not (csNoDesignVisible in ControlStyle)) and (csOpaque in ControlStyle) then
            begin
              Clip := ExcludeClipRect(DC, Left, Top, Left + Width, Top + Height);
              if Clip = NullRegion then Break;
            end;
        if Clip <> NullRegion then PaintWindow(DC);
      finally
        RestoreDC(DC, SaveIndex);
      end;
    end;
    PaintControls(DC, nil);
  finally
    if Message.DC = 0 then EndPaint(Handle, PS);
  end;
end;

从上面的函数可知,TWinControl在自绘的时候,会自动把可视的子控件所占的区域从DC中去掉,再回到PaintHandler中,发现 csCustomPaint这个状态,是在什么时候设置的?在Controls.pas里找来找去,发现,这是在子类TCustomControl里用到的,查看TCustomControl的WMPaint函数,就可以看到

procedure TCustomControl.WMPaint(var Message: TWMPaint);
begin
  Include(FControlState, csCustomPaint);
  inherited;
  Exclude(FControlState, csCustomPaint);
end;

很简单的代码,但是很明确地告诉了一个信息,TCustomControl就是专门用来做自绘的,明白了这样一个原理,就毫不犹豫地把我的控件改成从TCustomControl继承了。

    再来看TWinControl.WMPaint的else部分,这里面处理了DoubleBuffered的情况,想看一下VCL究竟是怎么来做DoubleBuffer的:

    DC := GetDC(0);
    MemBitmap := CreateCompatibleBitmap(DC, ClientRect.Right, ClientRect.Bottom);
    ReleaseDC(0, DC);
    MemDC := CreateCompatibleDC(0);
    OldBitmap := SelectObject(MemDC, MemBitmap);
    try
      DC := BeginPaint(Handle, PS);
      Perform(WM_ERASEBKGND, MemDC, MemDC);
      Message.DC := MemDC;
      WMPaint(Message);
      Message.DC := 0;
      BitBlt(DC, 0, 0, ClientRect.Right, ClientRect.Bottom, MemDC, 0, 0, SRCCOPY);
      EndPaint(Handle, PS);
    finally
      SelectObject(MemDC, OldBitmap);
      DeleteDC(MemDC);
      DeleteObject(MemBitmap);
    end;

可以看到在这段代码里,使用了桌面DC作为双缓冲的MemBitmap,来作为WM_ERASEBKGND时MemDC的位图刷,然后再将 MemDC拷贝到当前DC上,所以当设DoubleBuffered为True并没有处理WM_PAINT的时候,自绘控件看起来就是全透明的。也就是说,其实这个DoubleBuffered属性并不是真正意义上的完全的双缓冲,而只是迈出了双缓冲的第一步——在重绘时不刷新背景。其实这个有很多方法可以实现,比如可以把窗体类的背景刷设成nil。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值