终于懂了:FWinControls子控件的显示是由Windows来管理,而不是由Delphi来管理(显示透明会导致计算无效区域的方式有所不同——透明的话应减少剪裁区域,所以要进行仔细计算)

在研究TCustomControl的显示过程中,怎么样都找不到刷新FWinControls并重新显示的代码:

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
    if FControls = nil then PaintWindow(DC) else
    begin
      SaveIndex := SaveDC(DC);
      Clip := SimpleRegion;
      for I := 0 to FControls.Count - 1 do
        with TControl(FControls[I]) do
          if (Visible or (csDesigning in ComponentState) 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);
      RestoreDC(DC, SaveIndex);
    end;
    PaintControls(DC, nil);
  finally
    if Message.DC = 0 then EndPaint(Handle, PS);
  end;
end;

procedure TWinControl.PaintControls(DC: HDC; First: TControl);
var
  I, Count, SaveIndex: Integer;
  FrameBrush: HBRUSH;
begin
  if DockSite and UseDockManager and (DockManager <> nil) then
    DockManager.PaintSite(DC);
  if FControls <> nil then
  begin
    I := 0;
    if First <> nil then
    begin
      I := FControls.IndexOf(First);
      if I < 0 then I := 0;
    end;
    Count := FControls.Count;
    while I < Count do
    begin
      with TControl(FControls[I]) do
        if (Visible or (csDesigning in ComponentState) and
          not (csNoDesignVisible in ControlStyle)) and
          RectVisible(DC, Rect(Left, Top, Left + Width, Top + Height)) then
        begin
          if csPaintCopy in Self.ControlState then
            Include(FControlState, csPaintCopy);
          SaveIndex := SaveDC(DC);
          MoveWindowOrg(DC, Left, Top);
          IntersectClipRect(DC, 0, 0, Width, Height);
          Perform(WM_PAINT, DC, 0);
          RestoreDC(DC, SaveIndex);
          Exclude(FControlState, csPaintCopy);
        end;
      Inc(I);
    end;
  end;
  if FWinControls <> nil then
    for I := 0 to FWinControls.Count - 1 do
      with TWinControl(FWinControls[I]) do
        if FCtl3D and (csFramed in ControlStyle) and
          (Visible or (csDesigning in ComponentState) and
          not (csNoDesignVisible in ControlStyle)) then
        begin // 只是绘制边框而已
          FrameBrush := CreateSolidBrush(ColorToRGB(clBtnShadow));
          FrameRect(DC, Rect(Left - 1, Top - 1, Left + Width, Top + Height),
            FrameBrush);
          DeleteObject(FrameBrush);
          FrameBrush := CreateSolidBrush(ColorToRGB(clBtnHighlight));
          FrameRect(DC, Rect(Left, Top, Left + Width + 1, Top + Height + 1),
            FrameBrush);
          DeleteObject(FrameBrush);
        end;
end;

就连在TWinControl.UpdateShowing里也找不到相关代码:

procedure TWinControl.UpdateShowing;
var
  ShowControl: Boolean;
  I: Integer;
begin
  ShowControl := (FVisible or (csDesigning in ComponentState) and
    not (csNoDesignVisible in ControlStyle)) and
    not (csReadingState in ControlState);
  if ShowControl then
  begin
    if FHandle = 0 then CreateHandle;
    if FWinControls <> nil then
      for I := 0 to FWinControls.Count - 1 do
        TWinControl(FWinControls[I]).UpdateShowing;
  end;
  if FHandle <> 0 then
    if FShowing <> ShowControl then
    begin
      FShowing := ShowControl;
      try
        Perform(CM_SHOWINGCHANGED, 0, 0);
      except
        FShowing := not ShowControl;
        raise;
      end;
    end;
end;

procedure TWinControl.CMShowingChanged(var Message: TMessage);
const
  ShowFlags: array[Boolean] of Word = (
    SWP_NOSIZE + SWP_NOMOVE + SWP_NOZORDER + SWP_NOACTIVATE + SWP_HIDEWINDOW,
    SWP_NOSIZE + SWP_NOMOVE + SWP_NOZORDER + SWP_NOACTIVATE + SWP_SHOWWINDOW);
begin
  SetWindowPos(FHandle, 0, 0, 0, 0, 0, ShowFlags[FShowing]);
end;

后来在火车上想啊想,忽然灵机一动,明白了这些FWinControls是由Windows来管理,而不是Delphi管理。

一个具有Handle的窗口,不仅仅是Delphi的一部分,并且也是在整个Windows中挂了号的。除去首次显示之外(即上面的SetWindowPos,这个得另外研究),这个Windows窗口什么时候需要刷新显示,是由Windows说了算。而Windows只有发现这个Windows窗口具有无效区域的时候,才会对它进行刷新显示。即Windows系统直接对这个Windows窗口发送WM_PAINT消息,而不需要Delphi在VCL体系内部写代码发送WM_PAINT消息。这就是我始终找不到for I := 0 to FWinControls.Count - 1 do Perform(WM_PAINT, 0, 0);或者UpdateWindow()的原因。

话说是Windows自动判断无效区域才会决定是否刷新这个Windows控件,而造成无效区域的原因有2类:1.程序员调用Invalidate 这类API 2.用户实际操作,造成窗口移动/遮挡/显示等不同的情况。

---------------------------------------------------------------------------------------------------

补充:当一个TWinControl内部包含的图形控件的属性有变化而需要重新显示的时候,Windows就没法知道这些事情了,所以聪明的Delphi在属性变化的时候,就会手动执行:

procedure TControl.Repaint;
var
  DC: HDC;
begin
  if (Visible) and (Parent <> nil) and
    Parent.HandleAllocated then
    if csOpaque in ControlStyle then // 不透明(一般情况下)
    begin
      DC := GetDC(Parent.Handle);
      try // 不透明的话,比较简单,使用一个API直接就可以获得新的无效剪裁区域
        IntersectClipRect(DC, Left, Top, Left + Width, Top + Height); // API 从当前剪裁区域和指定矩形的交叉区域中,创建一个新的剪裁区域
        Parent.PaintControls(DC, Self); // 不管是否具有无效区域,直接发送WM_PAINT要求重绘。我觉得换成调用Self.Update也可以,但是效率会比较低
      finally
        ReleaseDC(Parent.Handle, DC);
      end;
    end else // 透明会导致计算无效区域的方式不同
    begin 
      Invalidate; // 透明的话,应减少剪裁区域,所以要进行仔细计算
      Update;
    end;
end;

这样就强迫父窗口刷新这个图形控件的显示。

如果是Invalidate和Update,其本质不变:

procedure TControl.Invalidate;
begin
  InvalidateControl(Visible, csOpaque in ControlStyle);
end;

procedure TControl.InvalidateControl(IsVisible, IsOpaque: Boolean);
var
  Rect: TRect;

  function BackgroundClipped: Boolean;
  var
    R: TRect;
    List: TList;
    I: Integer;
    C: TControl;
  begin
    Result := True;
    List := FParent.FControls;
    I := List.IndexOf(Self);
    while I > 0 do
    begin
      Dec(I);
      C := List[I];
      with C do
        if C.Visible and (csOpaque in ControlStyle) then // 不透明需要计算,透明就不用计算了(我懂了,透明就是不用管这个控件所占用的整体区域,而是直接使用API绘制,这样不需要Delphi帮忙管其它东西了)
        begin
          IntersectRect(R, Rect, BoundsRect); // API 计算交叉区域,R是其返回值
          if EqualRect(R, Rect) then Exit;
        end;
    end;
    Result := False;
  end;

begin
  if (IsVisible or (csDesigning in ComponentState) and
    not (csNoDesignVisible in ControlStyle)) and (Parent <> nil) and
    Parent.HandleAllocated then
  begin
    Rect := BoundsRect;
    InvalidateRect(Parent.Handle, @Rect, not (IsOpaque or
      (csOpaque in Parent.ControlStyle) or BackgroundClipped)); // API
  end;
end;

procedure TControl.Update;
begin
  if Parent <> nil then Parent.Update;
end;

procedure TWinControl.Update;
begin
  if HandleAllocated then UpdateWindow(FHandle); // API
end;
原文地址:https://www.cnblogs.com/findumars/p/5185050.html