Controls and glass

Looking on Stack Overflow, I’ve just noticed a rather detailed question (really, set of questions) about Aero glass. As it currently stands, the question bears some misconceptions – for example, the questioner makes the false assertion that the VCL DoubleBuffered property ‘sets a window style bit used by the common controls library’. Nonetheless, it’s tweaked me into finishing a couple of posts I intended to follow on from my earlier ones about custom DWM title bars. So, without further ado, here’s the first…

Standard controls and glass

A basic issue with glass frames is that native Windows controls don’t paint very well to them. The reason for this is that these controls still use the drawing primitives of the ‘classic’ GDI (Graphics Device Interface), and these primitives (in the main) ignore alpha channels, despite the fact that respecting alpha channels is essential to playing nicely with glass. Since most standard VCL controls are simply wrappers round their native equivalent (a TEdit is really a single-line native EDIT control, for instance), they inherit this problem. Moreover, the fact that TCanvas and related classes (TBrush etc) wrap the GDI API means that regular custom drawing in Delphi will be glass-hostile by default as well.

Nonetheless, Delphi 2007 slightly repurposed an old feature – optional double-buffering, first introduced in something like Delphi 4 – to provide a general (if by no means foolproof) solution to this. In short, simply turn on the DoubleBuffered property of a control (in Delphi 2009 or later, this will percolate down child controls so long as their ParentDoubleBuffered property is kept set to True), and the VCL will get the control to paint to a bitmap first, a bitmap that will have its alpha channel set to all opaque just before it is blitted to the screen.

In effect, this makes something visibly rectangular like an edit box paint very nicely on glass. Why ‘visibly rectangular’ though? Because, as said, the buffer is made completely opaque before being drawn. Why is that necessary though? Because, as said, the main drawing is more than likely to be done using classic GDI primitives (be it explicitly or implicitly), and thus, using functions that will mess up the alpha channel when called. By making the buffer all opaque at the end, then, this mess is cleared up.

To be clear, it is not double-buffering as such that is the solution here. Indeed, to my eyes, it seems many standard controls at the API level implement their own double-buffering when theming is turned on, yet don’t paint correctly on glass even so – to get things to work, then, you still need to turn on the VCL-level double buffering. Basically, glass-friendly double-buffering need to be performed using a 32 bit bitmap, preferably with special API functions that MS provided for the task in Vista – and while the VCL will internally use these functions as appropriate, the standard controls themselves will not.

Unfortunately, simply asserting the need for VCL-level double buffering is not the end of the matter though. One thing you may find, for example, is that simply enabling any sort of extended glass frame is liable to cause flicker where there was none before, even when a control is not actually on glass and the DoubleBuffered property of both the form and controls is correctly set. Quite frankly, solving this just takes trial and error. Here’s a few notes about certain standard controls though:

TBitBtn

Works fine in Delphi XE, assuming DoubleBuffered is True. In D2007 (I don’t know about versions in between) it shows a thin solid border on glass.

TButton

Setting DoubleBuffered to True causes the control to lose its fade in/out effects, so if a TButton isn’t actually on glass, I’d suggest making sure its DoubleBuffered stays False. Since when put on glass it shows a thin solid border (the curse of built-in yet not glass-aware double buffering strikes again…), its probably better to keep it off of the stuff anyhow — use TBitBtn instead.

TComoboBox, TEdit, TMemo

Basically works fine, though text selection can be a bit flickery.

TFrame

Set its ParentBackground property to False to avoid flicker.

TGroupBox

Set its ParentBackground property to False to avoid its caption flickering.

TListBox

Basically fine, though its border practically disappears due to it being in the control’s non-client area, and therefore, outside of the double-buffered area.

TImage

Whether the picture displays correctly is entirely down to the graphic type. Use a PNG image (in Delphi 2007, you’ll have to find a copy of the code that was integrated into the next version – it was open source), and you should be be fine. Similarly, in recent Delphi versions, a 32 bit TBitmap with its alpha channel properly set should be fine too.

TProgressBar

Its own internal double buffering at the API level paints things fine, and the VCL level one only messes things up, so make sure its DoubleBuffered property stays False even when an instance is placed on glass.

TRadioGroup

Set its ParentBackground property to False to avoid its caption flickering; add csAcceptControls to its ControlStyle property before its handle gets created to prevent its radio buttons from flickering.

TRichEdit

Doesn’t work with VCL double buffering at all, so make sure its DoubleBuffered property (stays) False.

TSpeedButton

Text-only speed buttons paint fine, both in the push and tool button styles, since the VCL delegates to the (alpha channel-aware) theming API.  Set the Glyph property though, and things immediately go downhill; thanks to convoluted internals added in D3 or D4, it’s also very difficult to fix. If you want a speed button with an image to go on glass, then I suggest you write your own.

TToolBar

I’ve given up trying to make a transparent TToolBar work on glass – the control’s own double buffering at the API level just gets terribly confused. To get it to work at all, you have to get its background explicitly filled. This can be done either by enabling the gradient background style (said style being implemented at the VCL level), or by handling the OnCustomDraw event as thus:

Sender.Canvas.FillRect(Sender.ClientRect);

General tips

As implied above, for a flickering parent control on glass, two things to always try is to (a) set its ParentBackground property to False and (b) ensure that csAcceptsControls is included in its ControlStyle property. The latter causes the WS_CLIPCHILDREN window style to be set, which makes a big difference (in my view, it’s arguably a bug that this just isn’t always set, notwithstanding the fact that back in 1995, there may have been efficiencies involved not to). Also, in the main, you should always turn on the VCL’s double buffering, notwithstanding the odd exception mentioned.

Separately, for controls generally, the Stack Overflow question linked to above alludes to a SetLayeredWindowAttributes hack. Unfortunately, this only worked with certain Vista betas – by the time the RTM build came round, MS deliberately prevented the hack from working.

Lastly, an alternative to using the VCL’s double-buffering is to set the WS_EX_COMPOSITED extended window style for all top-level windowed controls. Don’t just set this for the form itself, however, since the style isn’t glass aware as such – in other words, whenever GlassFrame.Enabled is True, the form’s DoubleBuffered property should be True too even if no sub-control’s DoubleBuffered property is likewise.

Note that the big point against using the extended window style is that every child control (and every grandchild etc) will be forced into participating, unlike in the case of the DoubleBuffered property. While I haven’t come any across straghtforward incompatibilities a la the VCL’s double-buffering implementation and TToolBar, you will kill off a lot of the fancy fading effects that many standard controls now implement even so.

[Edit (29/7/10): fixed typo (thanks Will in the comments) and added a bit more on WS_EX_COMPOSITED.]

[Edit (9/4/10): added about TBitBtn, which now works in XE at least.]

7 thoughts on “Controls and glass

  1. I’m the StackOverflow question guy. Can you comment on when we should, and should not call SetLayeredWindowAttributes to change the transparency-on-glass color from rgb 0,0,0 (black) to something else, and why the VCL didn’t just change that color to fix the standard-control-label-black glitch?

    W

  2. Oh darn. I missed it. You answered that too. Boy I’m out of the loop. This is really a case of a horrible Windows API then?

    W

    • This is really a case of a horrible Windows API then?

      Well… an underlying cause is there being two fundamentally different graphics subsystems involved. Indeed, if you don’t extend the glass area of a form, its frame will run in a separate process to the form itself, keeping the DWM and GDI surfaces perfectly separate. As soon as glass is extended, however, the two must take account of one another — and unfortunately, the standard controls weren’t re-engineered to do this.

      That said, on balance, not allowing for the SetLayeredWindowAttributes hack was probably the right thing to do – it was still a hack after all. The disappointing thing is that nothing ‘proper’ was added to replace it.

      (FWIW, and I only mention this because you don’t appear to have realised it – the SetLayeredWindowAttributes API is surfaced in TForm as the AlphaBlend, AlphaBlendValue, TransparentColor and TransparentColorValue properties.)

  3. Pingback: Custom drawing on glass (1) « Delphi Haven

Leave a comment