终于懂了:WM_PAINT 与 WM_ERASEBKGND,三种情况:用户操作,UpdateWindow,InvalidateRect产生的效果并不相同,并且用Delphi代码验证 good

一直对这两个消息的关系不是太了解,借重新深刻学习windows编程的机会研究一番。

1)当窗口从无效变为有效时,比方将部分覆盖的窗口恢复时会重绘窗口时:程序首先会通过发送其他消息调用DefWindowProc,它内部会发送WM_ERASEBKGND消息然后才会发送WM_PAINT消息,而且不经过消息队列(笔记:这结论从而何来?)。用Delphi的代码当场验证:

procedure TWinControl.WMSize(var Message: TWMSize);
begin
  UpdateBounds; // 类函数
  inherited;
  Realign; // 类函数
  if not (csLoading in ComponentState) then Resize; // 类函数,简单调用程序员事件
end;

procedure TWinControl.WMMove(var Message: TWMMove);
begin
  inherited;
  UpdateBounds;
end;

果然发现两个inherited,会把WM_SIZE和WM_MOVE消息发送到DefWindowProc,从而触发WM_ERASEBKGND。

2)诸如UpdateWindow也会先调用WM_ERASEBKGND消息的处理过程,然后才会调用WM_PAINT消息的处理过程

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

procedure TControl.Repaint;
var
  DC: HDC;
begin
  if (Visible or (csDesigning in ComponentState) and
    not (csNoDesignVisible in ControlStyle)) and (Parent <> nil) and
    Parent.HandleAllocated then
    if csOpaque in ControlStyle then
    begin
      DC := GetDC(Parent.Handle);
      try
        IntersectClipRect(DC, Left, Top, Left + Width, Top + Height);
        Parent.PaintControls(DC, Self);
      finally
        ReleaseDC(Parent.Handle, DC);
      end;
    end else
    begin
      Invalidate;
      Update;
    end;
end;

procedure TWinControl.Update;
begin
  if HandleAllocated then UpdateWindow(FHandle); // API,只有这一处使用这个API函数,但是有另外三处会来调用当前函数,因为这有这个API函数可以真正做到立刻刷新
end;

procedure TWinControl.Repaint;
begin
  Invalidate;
  Update;
end;

3)当调用InvalidateRect时(三个参数:hWnd,RECT,bErase),如果bErase参数为TRUE时会先调用WM_ERASEBKGND消息的处理过程。为False时,只会发送WM_PAINT消息到队列。所以InvalidateRect不是立刻调用WM_PAINT消息的处理过程,他只是给程序窗口增加一个更新区域(参阅MSDN)。

Delphi里一共有三处调用这个API函数:

procedure TControl.InvalidateControl(IsVisible, IsOpaque: Boolean);
var
  Rect: TRect;
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));
  end;
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);
    if Message.WParam = 0 then
    begin
      InvalidateRect(FHandle, nil, not (csOpaque in ControlStyle)); // 注意,处理的是自己的句柄,nil表示自己的整个客户区都重绘
      { 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
            Controls[I].Invalidate;
    end;
  end;
end;

procedure TWinControl.InvalidateFrame;
var
  R: TRect;
begin
  R := BoundsRect;
  InflateRect(R, 1, 1);
  InvalidateRect(Parent.FHandle, @R, True); // 也是重绘自己,但却是无条件重绘,而不需要考虑是否透明之类的问题,因为设计期也要绘制边框。其次,边框绘制出来以后,其内容可以被覆盖,因此不用管内容是否需要使用Theme
end;

另外关于WM_ERASEBKGND处理函数的返回值,如果处理WM_ERASEBKGND消息时返回FALSE,BeginPaint标记pt.fErase为TRUE, 如果处理WM_ERASEBKGND时返回TRUE,BeginPaint标记pt.fErase为FALSE。注意,Delphi就是这样做的,意思就是Delphi已经处理过了,不需要进一步处理。看这里:

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;

如果pt.fErase标记为TRUE,指示应用程序应该处理背景,但是应用程序不一定需要处理,pt.fErase只是作为一个标记(笔记:留给程序员的灵活性依然很强)。

http://www.aichengxu.com/view/2426114