• 一行代码设置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_消息进一步帮助处理(比如这个例子改变窗体颜色)。

  • 相关阅读:
    Java DbUtils简介
    Java JDBC 操作二进制数据、日期时间
    Java JDBC事务
    Java JDBC结果集的处理
    Java 使用properties配置文件加载配置
    Java JDBC的使用
    CSS3
    CSS 基础样式
    CSS 样式的使用方式、选择器
    HTML 表单
  • 原文地址:https://www.cnblogs.com/findumars/p/4117783.html
Copyright © 2020-2023  润新知