来自万一的帖子:
http://www.cnblogs.com/del/archive/2008/04/27/1173658.html
的确做到了一行代码设置TForm控件的颜色(一点感想:Delphi程序员真幸福)。但真实的情况是,VCL框架在这个过程中做了大量的工作,经过多次消息的发送和响应,才达到了目的,大致顺序如下:
procedure TForm1.Button1Click(Sender: TObject);
begin
Self.Color := clRed;
end; procedure TControl.SetColor(Value: TColor);
begin
if FColor <> Value then
begin
FColor := Value;
FParentColor := False;
Perform(CM_COLORCHANGED, 0, 0); // 第一个消息,当前类和各祖先类简直是群起响应,但既然是虚函数嘛,入口函数还是当前类自己的消息函数
end;
end; procedure TCustomForm.CMColorChanged(var Message: TMessage);
begin
inherited;
if FCanvas <> nil then FCanvas.Brush.Color := Color; // 虽然这里把Canvas.Brush.Color的值给覆盖了,但它是用来专门绘图的,就当前效果来说,VCL框架使用的是FillRect API,而没有用到Canvas,所以不起作用
end; procedure TWinControl.CMColorChanged(var Message: TMessage);
begin
inherited; // 自己的颜色变化起效果,是通过这句话来实现的,它包含了一连串的执行过程。父控件变化完了再通知子控件,风格很强势。
FBrush.Color := FColor; // 这里,读取控件的颜色,然后给控件的Brush赋值
NotifyControls(CM_PARENTCOLORCHANGED); // 第二个消息,组建消息并传播,通知子控件,但没有任何子控件响应
end; procedure TControl.CMColorChanged(var Message: TMessage);
begin
Invalidate; // 虚函数,所以要看是谁调用的这个函数。这个例子中会调用TWinControl.Invalidate;
end; procedure TWinControl.Invalidate;
begin
// 注意,第二个参数即WParam是0,即要求API使自己失效,而不是仅仅做一个通知作用。
Perform(CM_INVALIDATE, 0, 0); // 第三个消息,还是要再次发消息,直到CM消息才能真正起作用,因为它统一了参数。在这里,虚函数的作用被弱化了,只起了一个包装的作用。
end; procedure TWinControl.CMInvalidate(var Message: TMessage);
var
I: Integer;
begin
if HandleAllocated then
begin
if Parent <> nil then
Parent.Perform(CM_INVALIDATE, 1, 0); // 第四个消息,递归,先通知父类(父类要提前通知)。Form1的Parent是Application,它没有响应。
if Message.WParam = 0 then
begin
// API,第二个参数为NULL的话,则重画整个客户区;第三个参数TRUE则背景重绘。
InvalidateRect(FHandle, nil, not (csOpaque in ControlStyle)); // 总算初步达到目的,使Form1失效,后面还要自绘
{ Invalidate child windows which use the parentbackground when themed }
if ThemeServices.ThemesEnabled then
for I := 0 to ControlCount - 1 do
if csParentBackground in Controls[I].ControlStyle then // important
Controls[I].Invalidate;
end;
end;
end;
以上过程使Form1的画板失效(说到底,还是通过Form1.FCanvas.Brush起作用,类属性Color只是表象),接下去还有一个绘制Form1的过程。TForm1继承自TForm,TForm继承自TWinControl,而不是TCustomControl,而且响应WM_PAINT消息并覆盖了WMPaint函数,所以Windows会把WM_PAINT直接发给Form1,调用顺序如下:
TCustomForm.WMEraseBkgnd(var Message: TWMEraseBkgnd); // 区分正常情况和图标情况
TWinControl.WMEraseBkgnd(var Message: TWMEraseBkgnd); // 绘制背景色(相当于擦除了旧的背景)
TCustomForm.WMPaint(var Message: TWMPaint); // 区分正常情况和图标情况
TWinControl.WMPaint(var Message: TWMPaint); // 判断双缓冲和自绘,除了极少数Windows自带控件的包装(比如TEdit,TButton),都要执行PaintHandler自绘
TWinControl.PaintHandler(var Message: TWMPaint); // 先绘制当前win控件,比如Form1(有可能是剪裁后的剩余部分),然后绘制其所有图形子控件
procedure TCustomForm.PaintWindow(DC: HDC); // 使用WM_PAINT消息自带的DC句柄绘制Form1窗体
procedure TCustomForm.Paint; // 调用程序员事件,或者显示设计时的网格
----------------------------------------------------------------------------
而Form1的初始颜色在哪里设置呢?回答是没有在代码里设置,而是在dfm中设置了clBtnFace色,如果手动把它改成clRed,就立即就可以看到效果。这是一个空白窗体的dfm内容,一共14项:
object Form1: TForm1
Left = 0
Top = 0
Height = 282
Width = 418
Caption = 'Form1'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
end
----------------------------------------------------------------------------
另外我终于明白了,为什么我在TCustomForm里直接改源代码,却始终无法得到相应的效果:
constructor TCustomForm.CreateNew(AOwner: TComponent; Dummy: Integer);
begin
Color := clRed; // 这三句语句为什么都不起作用?
Brush.Color := clRed;
FCanvas.Brush.Color := clRed;
end;
因为TForm的创建顺序如下:
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end. procedure TApplication.CreateForm(InstanceClass: TComponentClass; var Reference);
var
Instance: TComponent;
begin
Instance := TComponent(InstanceClass.NewInstance); // 经典,使用元类创建实例。分配内存
TComponent(Reference) := Instance;
try
Instance.Create(Self); // 这里相当于调用TForm.Create;
except
TComponent(Reference) := nil;
raise;
end;
if (FMainForm = nil) and (Instance is TForm) then
begin
TForm(Instance).HandleNeeded;
FMainForm := TForm(Instance);
end;
end;
而TForm.Create会先调用TForm.CreateNew;后调用InitInheritedComponent读取dfm文件,这样就相当于存储字dfm文件里的颜色覆盖了我手动指定的clRed颜色,这就是始终无法生效的原因。所以应该在TCustomForm.Create函数里的InitInheritedComponent语句之后写:
Color :=clBlue;
Brush.Color := clBlue;
就可以立刻生效。但是如果写:
Canvas.Brush.Color := clBlue;
则无法生效,原因是Canvas在这个过程中并没有被用到,而且这个赋值过早,它在TCustomForm.CMColorChanged函数里被类属性Color的值覆盖了,所以无法生效。
----------------------------------------------------------------------------
对于Form1.Color, Brush.Color, Canvas.Brush.Color三个颜色值之间的关系分析:
如果把这两句:
Color :=clRed;
Brush.Color := clBlue;
写在TCustomForm.Create里面,无论哪句写在后面,都会按照后面一句的颜色来设置。但是其过程有所不同:
1. 如果Color :=clRed;写在后面,那么相当于调用了类属性,以及后面一系列变化,当执行TWinControl.CMColorChanged的时候,就会执行FBrush.Color := FColor;,相当于把FBrush.Color的值给覆盖了,前面那句话的效果就失效了。
2. 如果Brush.Color := clBlue;写在后面,在执行了前一句的效果以后,再把Brush.Color的值给简单覆盖掉了,前面那句话的效果自然就没有效果了。
总结:这两句话虽然都有相同的效果,但是执行过程可大不一样。使用Brush.Color := clBlue;这样更省事一些,因为它只是Delphi语言层面改变一个值,然后在刷新背景的时候供FillRect直接使用。如果使用Color :=clRed;其实分为2步,第一步是使整个Form1客户区失效,第二步是指Delphi语言层面改变Brush的值。这上面两步,都是在WM_ERASEBKGND消息和WM_PAINT消息来之前做的准备工作,这样一旦刷新消息来了就会立刻产生刷新的效果。
通过以上分析,我忽然注意到一个问题:如果直接执行Brush.Color := clBlue;,只是改变了控件画刷的颜色,并没有使客户区失效,那还有有效果吗?我试了一下:
procedure TForm1.Button2Click(Sender: TObject);
begin
Brush.Color := clGreen;
end;
点击按钮,Form1果然没有变换颜色的效果!这说明,虽然画刷颜色被改变了,但毕竟少了一个步骤,客户区没有失效,所以还是没效果。要等到下一次客户区失效,才能起效果。于是把窗口最小化,再恢复最大化,这样Form1客户区就变绿色了。而且以后也一直保持绿色。更有意思的是,用另外一个窗口(比如记事本)挡住Form1的部分客户区,然后移开,那么这部分客户区的颜色是绿色,其它部分仍然是红色!
而在constructor TCustomForm.Create里写上Brush.Color := clBlue;也会立刻生效,原因是Form1从未被显示过,所以第一次显示的时候,会自动发送擦除背景消息,此时画刷的颜色正是刚才设置的颜色,被FillRect API直接使用,所以能够立刻起作用!所以这是特殊情况,在一般情况下这样是不行的。
所以可以改成这样:
procedure TForm1.Button3Click(Sender: TObject);
begin
Invalidate;
Brush.Color := clGreen;
end;
这样和VCL框架的执行过程是一个意思,当然有效果。
再改成这样:
procedure TForm1.Button3Click(Sender: TObject);
begin
Brush.Color := clGreen;
Invalidate;
end;
也同样有效果,但其实我觉得这样写更合理。万事俱备了,再发消息做相应的动作,当然万无一失。
在TCustomForm.CMColorChanged函数里,虽然有:
if FCanvas <> nil then FCanvas.Brush.Color := Color;
但是这是专门使用Canvas画图的时候才使用。而此时,VCL使用的是FillRect API画出的效果,所以即使把这句话屏蔽掉也没关系,它也没起到相应的作用。
最后用代码总结一下这三种颜色之间的关系:
procedure TForm1.Button2Click(Sender: TObject);
begin
Brush.Color := clGreen;
if (Color=clGreen) then
ShowMessage('yes'); // 不执行
if (Canvas.Brush.Color=clGreen) then
ShowMessage('yes'); // 不执行
end; procedure TForm1.Button3Click(Sender: TObject);
begin
Color := clGreen;
if (Brush.Color=clGreen) then
ShowMessage('yes'); // 执行
if (Canvas.Brush.Color=clGreen) then
ShowMessage('yes'); // 执行
end; procedure TForm1.Button4Click(Sender: TObject);
begin
Canvas.Brush.Color := clGreen;
if (Brush.Color=clGreen) then
ShowMessage('yes'); // 不执行
if (Color=clGreen) then
ShowMessage('yes'); // 不执行
end;
----------------------------------------------------------------------------
另外还有一个问题是,这个颜色到底使用哪个winapi起作用的,通过搜索FillRect得知,是它在起作用:
procedure TWinControl.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
with ThemeServices do
if ThemesEnabled and Assigned(Parent) and (csParentBackground in FControlStyle) then
begin
{ Get the parent to draw its background into the control's background. }
DrawParentBackground(Handle, Message.DC, nil, False);
end
else
begin
{ Only erase background if we're not doublebuffering or painting to memory. }
if not FDoubleBuffered or
(TMessage(Message).wParam = TMessage(Message).lParam) then
FillRect(Message.DC, ClientRect, FBrush.Handle); // 这里,重新填充背景色(相当于擦除旧的背景色),注意画刷都有句柄
end; Message.Result := 1;
end;
------------------------------------------------------------------------
总结:改变类属性后就会立刻生效,原因是会调用类属性对应的Set函数,然后发消息真正显示到窗口上。如果WM_消息能直接解决问题(比如设置窗体标题),就行了,但有时候还不够,还需要使用CM_消息进一步帮助处理(比如这个例子改变窗体颜色)。