在创建/恢复表单时,重叠的TCustomControl对象不按顺序绘制

c0p*_*t0p 8 delphi delphi-2007

我在使用Delust 2007中获得TCustomControl处理透明度时遇到了问题.我目前已将问题减少到下面的代码中.问题是,在最初创建表单时,控件的绘制顺序与它们添加到表单的顺序相反.调整表单大小后,它们会以正确的顺序绘制.我究竟做错了什么?排除第三方解决方案是否有更合适的路径?

调整窗口大小后,示例程序的屏幕截图

这是我的示例项目,展示了Delphi 2007中的问题.

unit Main;

interface

uses
  Forms, Classes, Controls, StdCtrls, Messages,
  ExtCtrls;

type
  // Example of a TWinControl derived control
  TMyCustomControl = class(TCustomControl)
  protected
    procedure CreateParams(var params: TCreateParams); override;
    procedure WMEraseBkGnd(var msg: TWMEraseBkGnd);
      message WM_ERASEBKGND;
    procedure Paint; override;
  end;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormPaint(Sender: TObject);
  private
    YellowBox: TMyCustomControl;
    GreenBox: TMyCustomControl;
  end;

var
  Form1: TForm1;

implementation

uses
  Windows, Graphics;

{$R *.dfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  self.OnPaint := FormPaint;

  GreenBox := TMyCustomControl.Create(self);
  GreenBox.Parent := self;
  GreenBox.SetBounds(10,10,200,200);
  GreenBox.color := clGreen;

  YellowBox := TMyCustomControl.Create(self);
  YellowBox.Parent := self;
  YellowBox.SetBounds(100,100,200,200);
  YellowBox.color := clYellow;

end;

// Paint bars on form background
procedure TForm1.FormPaint(Sender: TObject);
var
  Idx: Integer;
begin
  for Idx := 0 to ClientHeight div 8 do
  begin
    if Odd(Idx) then
      Canvas.Brush.Color := clWhite
    else
      Canvas.Brush.Color := clSilver;  // pale yellow
    Canvas.FillRect(Rect(0, Idx * 8, ClientWidth, Idx * 8 + 8));
  end;
end;

{ TMyCustomControl }

procedure TMyCustomControl.CreateParams(var params: TCreateParams);
begin
  inherited;
  params.ExStyle := params.ExStyle or WS_EX_TRANSPARENT;
end;

procedure TMyCustomControl.WMEraseBkGnd(var msg: TWMEraseBkGnd);
begin
  SetBkMode (msg.DC, TRANSPARENT);
  msg.result := 1;
end;

procedure TMyCustomControl.Paint;
begin
  Canvas.Brush.Color := color;
  Canvas.RoundRect(0,0,width,height,50,50);
end;



end.
Run Code Online (Sandbox Code Playgroud)

Ser*_*yuz 5

您对控件绘画顺序的期望是错误的。WM_PAINT记录接收消息的控件的顺序实际上是完全相反的顺序,最上面的控件首先接收消息。以后会有更多关于文档的信息,因为对WS_EX_TRANSPARENT同级元素进行了样式设置使我们处于未记录的领域。正如您已经提到的,在某些情况下,控件接收WM_PAINT消息的顺序是不确定的-在调整窗口大小时,顺序会更改。

我对您的复制案例做了一些修改,以查看发生了什么。修改包括两个面板以及它们接收时的调试输出WM_PAINT

unit Unit1;

interface

uses
  Forms, Classes, Controls, StdCtrls, Messages, ExtCtrls;

type
  TMyCustomControl = class(TCustomControl)
  protected
    procedure CreateParams(var params: TCreateParams); override;
    procedure WMEraseBkGnd(var msg: TWMEraseBkGnd);
      message WM_ERASEBKGND;
    procedure Paint; override;
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
  end;

  TPanel = class(extctrls.TPanel)
  protected
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
  end;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure FormPaint(Sender: TObject);
  private
    YellowBox: TMyCustomControl;
    GreenBox: TMyCustomControl;
    Panel1, Panel2: TPanel;
  end;

var
  Form1: TForm1;

implementation

uses
  sysutils, windows, graphics;

{$R *.dfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  Width := 590;
  Height := 270;
  OnPaint := FormPaint;

  GreenBox := TMyCustomControl.Create(self);
  GreenBox.Parent := self;
  GreenBox.SetBounds(20, 20, 140, 140);
  GreenBox.color := clGreen;
  GreenBox.Name := 'GreenBox';
//{
  Panel1 := TPanel.Create(Self);
  Panel1.Parent := Self;
  Panel1.SetBounds(240, 40, 140, 140);
  Panel1.ParentBackground := False;
  Panel1.Color := clMoneyGreen;
  Panel1.Name := 'Panel1';

  Panel2 := TPanel.Create(Self);
  Panel2.Parent := Self;
  Panel2.SetBounds(260, 60, 140, 140);
  Panel2.ParentBackground := False;
  Panel2.Color := clCream;
  Panel2.Name := 'Panel2';
//}
  YellowBox := TMyCustomControl.Create(self);
  YellowBox.Parent := self;
  YellowBox.SetBounds(80, 80, 140, 140);
  YellowBox.color := clYellow;
  YellowBox.Name := 'YellowBox';
  YellowBox.BringToFront;
end;

// Paint bars on form background
procedure TForm1.FormPaint(Sender: TObject);
var
  Idx: Integer;
begin
  for Idx := 0 to ClientHeight div 8 do
  begin
    if Odd(Idx) then
      Canvas.Brush.Color := clWhite
    else
      Canvas.Brush.Color := clSilver;  // pale yellow
    Canvas.FillRect(Rect(0, Idx * 8, ClientWidth, Idx * 8 + 8));
  end;
end;

{ TPanel }

procedure TPanel.WMPaint(var Message: TWMPaint);
begin
  OutputDebugString(PChar(Format(' %s painting..', [Name])));
  inherited;
end;

{ TMyCustomControl }

procedure TMyCustomControl.CreateParams(var params: TCreateParams);
begin
  inherited;
  params.ExStyle := params.ExStyle or WS_EX_TRANSPARENT;
end;

procedure TMyCustomControl.WMEraseBkGnd(var msg: TWMEraseBkGnd);
begin
  msg.Result := 1;
end;

procedure TMyCustomControl.WMPaint(var Message: TWMPaint);
begin
  OutputDebugString(PChar(Format(' %s painting..', [Name])));
  inherited;
end;

procedure TMyCustomControl.Paint;
begin
  Canvas.Brush.Color := Color;
  Canvas.RoundRect(0, 0, Width, Height, 50, 50);
end;

end.
Run Code Online (Sandbox Code Playgroud)


产生这种形式:

在此处输入图片说明

根据创建顺序确定,z顺序是从下到上,

  1. GreenBox,
  2. 面板1
  3. 面板2
  4. YellowBox。

WM_PAINT消息的调试输出是这样的:

Debug Output:  Panel2 painting.. Process Project1.exe (12548)
Debug Output:  Panel1 painting.. Process Project1.exe (12548)
Debug Output:  YellowBox painting.. Process Project1.exe (12548)
Debug Output:  GreenBox painting.. Process Project1.exe (12548)
Run Code Online (Sandbox Code Playgroud)

按此顺序有两件事需要注意。

首先,尽管Panel2的z顺序较高,但Panel2会在Panel1之前收到绘制消息。

那么,虽然我们将Panel2视为一个整体,但是即使稍后绘制它,也只能看到Panel1的一部分,那又如何呢?这是更新区域起作用的地方。WS_CLIPSIBLINGS控件中的样式标志告诉OS,z顺序较高的同级兄弟占据的控件部分不会被绘制。

相对于彼此剪辑子窗口;也就是说,当特定的子窗口接收到WM_PAINT消息时,WS_CLIPSIBLINGS 样式会将所有其他重叠的子窗口剪切到要更新的子窗口区域之外。

让我们在WM_PAINTPanel1 的处理程序中进行更深入的研究,看看操作系统的更新区域如何。

{ TPanel }

// not declared in D2007
function GetRandomRgn(hdc: HDC; hrgn: HRGN; iNum: Integer): Integer; stdcall;
    external gdi32;
const
  SYSRGN = 4;

procedure TPanel.WMPaint(var Message: TWMPaint);
var
  PS: TPaintStruct;
  Rgn: HRGN;

  TestDC: HDC;
begin
  OutputDebugString(PChar(Format(' %s painting..', [Name])));

  Message.DC := BeginPaint(Handle, PS);
  Rgn := CreateRectRgn(0, 0, 0, 0);
  if (Name = 'Panel1') and (GetRandomRgn(Message.DC, Rgn, SYSRGN) = 1) then begin
    OffsetRgn(Rgn, - Form1.ClientOrigin.X + Width + 40, - Form1.ClientOrigin.Y);
    TestDC := GetDC(Form1.Handle);
    SelectObject(TestDC, GetStockObject(BLACK_BRUSH));
    PaintRgn(TestDC, Rgn);
    ReleaseDC(Form1.Handle, TestDC);
    DeleteObject(Rgn);
  end;
  inherited;
  EndPaint(Handle, PS);
end;
Run Code Online (Sandbox Code Playgroud)


BeginPaint将剪辑更新区域与系统更新区域,你可以再与检索GetRandomRgn。我已将裁剪后的更新区域转储到表单的右侧。不用介意Form1引用或缺少错误检查,我们只是调试。无论如何,这将产生以下形式:

在此处输入图片说明

因此,无论您在Panel1的工作区中绘制什么,它都会被裁剪为黑色形状,因此无法从视觉上看到它进入Panel2的前面。

其次,请记住首先创建了绿色框,然后创建了面板,最后创建了黄色。那么为什么在两个面板之后绘制两个透明控件呢?

首先,请记住控件是从上到下绘制的。现在,透明控件怎么可能在它之后绘制的东西上绘制?显然这是不可能的。因此,整个绘画算法必须更改。没有关于此的文档,我找到的最好的解释是来自Raymond Chen 的博客条目

... WS_EX_TRANSPARENT扩展的窗口样式按以下方式更改绘制算法:如果WS_EX_TRANSPARENT窗口需要绘制,并且具有任何非WS_EX_TRANSPARENT窗口同级(属于同一进程),也需要对其进行绘制,则窗口管理器将首先喷涂非WS_EX_TRANSPARENT窗户。

自上而下的绘画顺序使您在使用透明控件时变得困难。然后是透明控件重叠的情况-哪个比其他控件更透明?只需接受重叠透明控件会产生不确定行为的事实即可。

如果您在上述测试案例中调查透明框的系统更新区域,则会发现两者都是精确的正方形。


让我们将面板移动到盒子之间。

procedure TForm1.FormCreate(Sender: TObject);
begin
  Width := 590;
  Height := 270;
  OnPaint := FormPaint;

  GreenBox := TMyCustomControl.Create(self);
  GreenBox.Parent := self;
  GreenBox.SetBounds(20, 20, 140, 140);
  GreenBox.color := clGreen;
  GreenBox.Name := 'GreenBox';
//{
  Panel1 := TPanel.Create(Self);
  Panel1.Parent := Self;
  Panel1.SetBounds(40, 40, 140, 140);
  Panel1.ParentBackground := False;
  Panel1.Color := clMoneyGreen;
  Panel1.Name := 'Panel1';

  Panel2 := TPanel.Create(Self);
  Panel2.Parent := Self;
  Panel2.SetBounds(60, 60, 140, 140);
  Panel2.ParentBackground := False;
  Panel2.Color := clCream;
  Panel2.Name := 'Panel2';
//}
  YellowBox := TMyCustomControl.Create(self);
  YellowBox.Parent := self;
  YellowBox.SetBounds(80, 80, 140, 140);
  YellowBox.color := clYellow;
  YellowBox.Name := 'YellowBox';
  YellowBox.BringToFront;
end;

 ...

procedure TMyCustomControl.WMPaint(var Message: TWMPaint);
var
  PS: TPaintStruct;
  Rgn: HRGN;

  TestDC: HDC;
begin
  OutputDebugString(PChar(Format(' %s painting..', [Name])));

  Message.DC := BeginPaint(Handle, PS);
  Rgn := CreateRectRgn(0, 0, 0, 0);
  if (Name = 'GreenBox') and (GetRandomRgn(Message.DC, Rgn, SYSRGN) = 1) then begin
    OffsetRgn(Rgn, - Form1.ClientOrigin.X + Width + 260, - Form1.ClientOrigin.Y);
    TestDC := GetDC(Form1.Handle);
    SelectObject(TestDC, GetStockObject(BLACK_BRUSH));
    PaintRgn(TestDC, Rgn);
    ReleaseDC(Form1.Handle, TestDC);
    DeleteObject(Rgn);
  end;
  inherited;
  EndPaint(Handle, PS);
end;
Run Code Online (Sandbox Code Playgroud)


在此处输入图片说明

最右边的黑色形状是GreenBox的系统更新区域。毕竟,系统可以将剪辑应用于透明控件。我认为可以得出结论,当您拥有大量透明控件时,绘画算法并不完美。


如承诺的那样,该订单的文档报价WM_PAINT。我将其保留下来的一个原因是它包括一个可能的解决方案(当然,我们已经找到了一个解决方案,将一些非透明控件分散在透明控件之间):

...如果父链中的某个窗口是复合窗口(带有WX_EX_COMPOSITED的窗口),则同级窗口将以其Z顺序的相反顺序接收WM_PAINT消息。这样,Z顺序最高的窗口(在顶部)将最后收到其WM_PAINT消息,反之亦然。如果父链中的窗口未复合,则兄弟窗口将以Z顺序接收WM_PAINT消息。

经过我的测试,WS_EX_COMPOSITED在父表单上进行设置似乎可行。但我不知道它是否适用于您的情况。