§ ¶Tab control sadness
I ran into a strange problem trying to combine a tab control with a list view.
Tab controls are among the weirder of the controls in the Win32 common controls suite. A tab control lets the user select between a number of tabs, and the program then changes information displayed based on the selected tab. If you've used another UI system, you might expect the pages to be parented to the tab, but not in Win32 -- if you try to do that you quickly discover that all of your child controls break, because all of their notifications now go to the tab control. What you have to do instead is overlap the controls corresponding to the page on top of the tab control instead, with all of them parented to the dialog. This doesn't look any different to the user, but it has lots of implications for the way that the UI code is structured.
One of the annoying problems with Win32 UI is that overlapping controls in general are bad news. In theory it's not too hard of a problem to solve in a windowing system, as you just have to and dutifully paint all windows affected by any invalidation in the correct order. In practice, I've found that Windows doesn't quite get this right, and you get into many corner case redrawing problems. This is compounded by buttons, which (a) render transparent and require that the dialog itself supply the background erase, and (b) must overlap other controls when used in group box mode. Therefore, you can end up in a number of cases where a resizable dialog doesn't quite redraw correctly when resized.
In this case, the problem was a bit more severe:
Both images are of a dialog containing a tab control with a list view on top of it, created fresh in the VC++ dialog editor. The first image is how it initially displays; the second one is after changing a column width. Yuck. It was actually worse than I can show in a static image, as sometimes the entire list view would disappear.
My first thought was, oh great, the tab control is drawing over the list view. Indeed, that was the case, but according to Spy++ WS_CLIPSIBLINGS was set on the tab control, and no amount of style fiddling seemed to fix the problem. The WS_CLIPSIBLINGS flag is supposed to exclude other child windows from that window's paint, which in this case would prevent the tab control from drawing over the list view. Well, after subclassing the tab control and doing some experiments, it turns out that the documentation is both absolutely correct and still misleading. WS_CLIPSIBLINGS does indeed exclude other overlapping child windows during painting (WM_PAINT). What it does not do is exclude other windows during the background erase (WM_ERASEBKGND)! Tab controls defer to DefWindowProc() for the latter message, which dutifully does a FillRect() with the class background brush of COLOR_3DFACE... with no clip region. Owww.
I ended up "fixing" the problem by subclassing the tab control, intercepting WM_ERASEBKGND, and calling ExcludeClipRect() with the rectangle of the list view. I still can't believe that WS_CLIPSIBLINGS doesn't clip the erase operation, though, because that seems like a gaping hole in the implementation. Actually, it doesn't surprise me too much -- frankly, many parts of USER are broken as designed -- but it still bugs me because checked against the Wine source code, which appears to use the same clip region for erase and paint operations. If anyone knows of an additional condition for WM_CLIPSIBLINGS to react in this manner, I'm curious.
I put the controls on a child dialog/window, then parent that window to the tab control. In the child window/dialog procedure I pass on the notifications (usually by calling the main window's proc directly for WM_COMMAND/WM_NOTIFY).
That solves many of the problems with tab controls. There's still the issue with the tab control background not filing under XP's visual style. (I solve that by drawing the background texture myself, drawing alternating rows upside down so the gradient at least tiles. In Vista/7 it's a solid white background so that technique still looks fine but also isn't needed.)
There are also issues with the tab control background not being redrawn properly in the transparent bits of controls which are moved due to resizing. The group box is an obvious example but it also happens with buttons (which have a 1px transparent border around them). It's difficult to see in most cases but it can produce ugly results.
To solve that I ended up doing some fairly complex subclassing of all the controls so that they paint their backgrounds with properly-aligned brushes.
It's almost 2010 and making a resizeable window using native Windows APIs is still a giant pain in the arse. MS should be ashamed of themselves in this department. Maybe if they actually bothered to make more of their UIs resizeable they would realise how painful it is. Or maybe that's why they don't bother and we have garbage like the file permissions dialogs. :)
Leo Davidson (link) - 23 12 09 - 22:26
Arenít you supposed to use Property Sheets for this kind of dialogs? The MSDN KB article 300606 says you just have to tweak the window style when handling the PSCB_PRECREATE message in your PropSheetProc.
Yuri Khan - 24 12 09 - 01:23
You can use property sheets, but IMO they don't buy you anything besides extra hassle. If you have any sort of UI framework at all the child-dialog-as-page scheme that Leo mentioned isn't hard to implement, and I've done some dialogs that way. I haven't checked whether list views work in that model without extra tweaking, though. In the actual case where I ran into this problem I had a number of tabs that all used very similar list views and controls, so I was trying to avoid an extra layer of UI.
As for 300606, it describes how to hack the raw dialog template to add the thick frame style to make the dialog resizable, so that isn't really applicable here. (Having to do that in the first place in MFC is another brand of WTF, by the way.)
I place at least half of the blame for Win32 UI still being a PITA on the Visual Studio team, since they've done nothing to make more features accessible in the dialog editor since VC6, and if anything have made the editing experience worse. Working on this I ran into that &#*$(#$ select-bottom-control bug again. Since there's no way to add simple metadata to a dialog, though, everything has to go through a side channel and there's no simple way to do annotations for resizing and other features that are now mandatory for any UI library.
Phaeron - 24 12 09 - 07:00
If you want to attach metadata to the windows, SetProp/GetProp works pretty well.
Kayamon - 24 12 09 - 10:22
That attaches metadata to the runtime representation -- it doesn't give you a place to store it in loadable form in a template or provide an editor for that data.
Incidentally, I've never found a use for SetProp/GetProp(). If I'm doing simple subclassing I can use GWL_USERDATA, if I'm writing my own control I just allocate window data, and if I need to do more complex subclassing I use a dynamically generated thunk like ATL does.
Phaeron - 24 12 09 - 16:40
I've given up on the VS dialog editor in most of my new code.
That thing is like MFC: The small pro is that it makes doing very easy things slightly easier, and gets you sucked into using it as a result. The massive, massive con is that it makes doing everything else a huge PITA.
I mean, honestly, who makes a dialog editor that doesn't have the first clue about how controls are laid out relative to each other? I'm not even talking about resizing; it fails at just spacing controls out on a static dialog.
It doesn't help that Win32 provides no APIs or even static data to (re-)size or position controls according to the style guide, the content of the controls and the fonts they are using.
I ended up converting the details of the style guide PDF into code and now create and position all my controls at run-time. I do still have dialog templates (mainly to let translators adjust the font) but they're completely blank.
(The style guide says stuff like "a checkbox will be the width of its text label plus six dialog units, and will be positioned 3 dialog units below a related control and 6 below an unrelated control." Numbers off the top of my head and probably wrong, but that kind of thing. Why none of the common controls have methods to auto-size them is completely beyond my comprehension!)
And, of course, every 5 years or so Microsoft s**t-can their current UI framework and bring out yet-another-UI-framework which solves some nebulous issues but does very little to address these basic, underlying problems. I've not used the .Net GUI stuff much -- almost all my .Net coding has been server-side stuff -- but people don't sound overjoyed with WPF and given the history of MS UI frameworks I'll be gobsmacked if it isn't made obsolete soon, while still half-baked, of course. :)
It's ironic that MFC, probably the worst UI framework ever made and the one which most deserved to die, is the one that was recently given a new lease of life and is still the one Visual Studio pushes C++ coders towards the most.
(I used MFC for something by accident recently, haha. By the time I started plumbing in the UI and realised I'd created an MFC project without thinking it was easier to roll with it. That thing had a trivial UI, though, which MFC does an okay job of.)
Merry Christmas! :)
Leo Davidson (link) - 24 12 09 - 18:54
> Tab controls are among the weirder of the controls in the Win32 common controls suite.
You just use it in the way it evidently wasn't intended to be used in (groupbox-like). The tab was meant to be the parent of its pages, not a sibling. The header control is for same-level non-switching UI.
So no, the tab doesn't beat my favorite, the rebar control in the weirdness area. Because rebar, in general, has AFAIN 2 modes:
- included into intermediate child window that owns the rebar (rebar fills its whole client area)
- not working :)
Both Explorers always use the 1st mode, naturally. But I didn't see it documented.
And that's not some advanced usage that you obtain bugs from without an "insulating" window. It's just required for rebar to work in all the cases as a very regular rebar (like, simply a bunch of movable toolbars on top of your MDI window).
> I still can't believe that WS_CLIPSIBLINGS doesn't clip the erase operation
Well, without WS_CLIPSIBLINGS, background erasure of sibling child windows gets even worse, the backgrounds constantly leak into each others (and outdated unerased foregrounds, too, but not only foregrounds). So the flag does something useful, sometimes.
BTW, did _both_ windows in question (tab and list view) have the flag? It usually doesn't work if not set on BOTH of the overlapping children.
In general, all windows should have both of WS_CLIP... flags (one more API weirdness - the normal mode has to be explicitly turned on). If some are not set, it usually means that some trick is implemented in the child window to optimize painting. It usually doesn't work with most sorts of tricks ;) and surely doesn't work out of the box.
> Incidentally, I've never found a use for SetProp/GetProp().
I know of only one use of that feature - Delphi VCL (some versions) uses it instead of GWL_USERDATA when subclassing. Never have seen it anywhere else. Either it's too complex for the task, or people didn't know of it and have already implemented something else.
You just don't know of SetProp unless you dig through the whole MSDN section, it's not mentioned much in other places.
avek - 24 12 09 - 20:19
> The tab was meant to be the parent of its pages, not a sibling.
I'm not sure why you think this. If you look at a standard property page dialog in Windows, the tab control is a sibling of the page child dialog, not the parent. Making it the parent would break notification messages. The tab control sample code in MSDN also works this way.
> BTW, did _both_ windows in question (tab and list view) have the flag?
Both the tab control and the parent dialog had WS_CLIPSIBLINGS, but not the list view. If you set it on the list view, the list view goes away, because it clips the tab control out of its clip area, which effectively occludes the entire control. I tried it.
> It usually doesn't work if not set on BOTH of the overlapping children.
I'm not sure how this would work, since you're basically excluding both children from the overlapping area.
> In general, all windows should have both of WS_CLIP... flags (one more API weirdness - the normal mode has to be explicitly turned on). If some are not set, it usually means that some trick is implemented in the child window to optimize painting. It usually doesn't work with most sorts of tricks ;) and surely doesn't work out of the box.
This doesn't work if you have group boxes in a dialog, because they have a transparent background AND are designed to be overlapped by other controls. If you set WS_CLIPCHILDREN on the enclosing window, you will get garbage beneath the group box. I've found that in general you CANNOT set this flag on dialogs or in windows that use dialog controls for this reason. Instead, it's better to do the opposite and override WM_ERASEBKGND to exclude controls that you know have an opaque background, such as pushbuttons and listboxes.
Similarly, if you set WS_CLIPSIBLINGS on the controls positioned within the group box, those controls will go away, because the controls are fully overlapped by other controls and this flag ignores Z order.
Phaeron - 25 12 09 - 09:57