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。