I'm having issues getting a TCustomControl to work with transparency in Delphi 2007. I've currently reduced the problem to the code below. The issue is that when the form is initially created the controls are drawing in the reverse order they are added to the form. When the form is resized, they paint in the correct order. What am I doing wrong? Excluding 3rd party solutions is there a more appropriate path to follow?
Here's my sample project demonstrating the issue in 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.
What is wrong is your expectancy of the order of painting of your controls. The order of controls receiving
WM_PAINT
messages is documented to be actually in the exact opposite order, the top-most control receives the message first. More on the documentation later, since havingWS_EX_TRANSPARENT
styled siblings leaves us in undocumented territory. As you have already noted, you have a case where the order of the controls receivingWM_PAINT
messages is not deterministic - when resizing the window the order changes.I've modified a bit of your reproduction case to see what is happening. The modifications are the inclusion of two panels and a debug output when they receive
WM_PAINT
.Which produces this form:
As determined by order of creation, the z-order is, from bottom to top,
The debug output for the
WM_PAINT
messages is this:There are two things worth to note in this order.
First, Panel2 receives the paint message before Panel1, although Panel2 is higher in the z-order.
So how is it that while we see Panel2 as a whole, but we see only part of Panel1 even though it is painted later? This is where update regions come into play. The
WS_CLIPSIBLINGS
style flags in controls tell the OS that part of a control occupied by a sibling higher in the z-order is not going to be painted.Let's dig into a bit more in the
WM_PAINT
handler of Panel1 and see how the OS' update region looks like.The
BeginPaint
will clip the update region with the system update region which you can then retrieve withGetRandomRgn
. I've dumped the clipped update region to the right of the form. Don't mind theForm1
references or missing error checks, we are only debugging. Anyway, this produces the below form:So, whatever you draw in the client area of Panel1, it will get clipped into the black shape, hence it cannot be visually come into front of Panel2.
Second, remember that the green box is created first, then the panels and then the yellow last. So why is it that the two transparent controls are painted after the two panels?
First, remember that controls are painted from top to bottom. Now, how can it be possible for a transparent control to draw onto something which is drawn after it? Obviously it is not possible. So the entire painting algorithm have to change. There is no documentation on this and the best explanation I've found is from a blog entry of Raymond Chen:
The top to bottom painting order makes it a difficult one when you have transparent controls. Then there is the case of overlapping transparent controls - which is more transparent than the other? Just accept the fact that overlapping transparent controls produce undetermined behavior.
If you investigate the system update regions of the transparent boxes in the above test case, you'll find both to be exact squares.
Let's shift the panels to in-between the boxes.
The right-most black shape is the system update region for the GreenBox. After all the system can apply clipping to a transparent control. I think it would suffice to conclude that the painting algorithm is not perfect when you've got a bunch of transparent controls.
As promised, the documentation quote for the
WM_PAINT
order. One reason I've left this to last is that it includes a possible solution (of course we already found one solution, scatter some non-transparent controls in-between your transparent controls):For as little as I tested, setting
WS_EX_COMPOSITED
on the parent form seems to work. But I don't know if it is applicable in your case.