一行代码设置TForm颜色的前世今生(属性赋值引起函数调用,然后发消息实现改变显示效果),TForm的初始颜色在dfm中设置了clBtnFace色

来自万一的帖子:
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_消息进一步帮助处理(比如这个例子改变窗体颜色)。