August 1998
Download Aug98CQA.exe (43KB)
Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and
writer-at-large. He can be reached at askpd@pobox.com
or http://pobox.com/~askpd.
|
One of the worst things about writing this column is that so many good programmers read it. Whatever mistakes I make, it's almost certain that someone will discover them. Just imagine what it would be like if you had to publish your code for thousands of top-flight programmersyikes! This month, it's time to confess some booboos.
In the April 1998 issue, I showed you how to write a class, CModuleVersion, that reads the module version number and other version information. I showed two methodsone using DllGetVersion and the other using GetFileVersionInfo. In particular, I showed how to get the language information: |
|
Normally, the return value from VerQueryValue is 4 (two words = four bytes). My implementation of CModuleVersion might lead you to suspect a DLL can support only one language when, in fact, DLLs can be multilingual. That's right, it's possible to have a polyglot DLL. In this case the return value from VerQueryValue might be 8 or 12or generally 4xn, where n is the number of languages supported. The returned data is an array of language ID/code page pairs. It's up to you to select the one you want when building the language ID string that you must pass to retrieve the version information. A more complete and international-friendly version of CModuleVersion would have an iterator to navigate the languages supported. Thanks to Steve Davis for finding this bug. |
Figure 1 Tab Error |
|
Figure 2 Fixed Tab |
This is one of those "Say what?" screen shots that could go on the Abort, Retry, Fail? page of PC Magazine. What am I talking about? The tab, of course. In Figure 1, Page 2 of the property sheet is selected. So why is there a white line below the tab? Oops. Figure 2 shows how it should look. Like the disclaimer says, if the code doesn't work, I don't know who wrote it. But I do know how to fix this problem: just erase the background before drawing the text. |
|
By the way, CDC::FillSolidRect is a handy function. It fills any rectangle with a solid color. The implementation is a bit weird, though: |
|
FillSolidRect calls TextOut with an empty string, using your color as the background color. That's strange because the "normal" way to fill a rectangle is to create a solid brush, select it, call PatBlt, then deselect the brush. I guess the Redmondtonians know some secret I don'tnamely, that SetBkColor/ExtTextOut is faster. It certainly requires fewer API calls.
There were a couple of more obscure bugs in CTabCtrlWithDisable. Whoever wrote it must have been asleep at the keyboard that day. Fortunately, I was able to fix the bugs and post a new version on the MSJ Web site. So I won't bother reprinting all the code here. Thanks to readers Roberto Grassi and Ken Thomas for finding these bugs and submitting their solutions. In last month's issue, in describing how to implement a cancel dialog using multithreading, I said that when sending a message from the worker thread to the main thread, it was better to use PostMessage instead of SendMessage. This was so the code that handles the message would "run in the main thread, not the worker thread." What I should have said was "so as not to block the worker thread while the main thread handles the message." In fact, whether you use PostMessage or SendMessage, the handler code will run in the thread that created the window. Thanks to John A. Tasler for being the first to point this out. The last bug is technically not a bug. It falls under the category of feature omission. Q In the May 1998 issue, you showed how to implement a toolbar with dropdown buttons (see Figure 3) using TBSTYLE_EX_DRAWDDARROWS and TBSTYLE_DROPDOWN. The sample program you gave, MyEdit, uses a non-docking toolbar. When I try to add docking to my own toolbar with dropdown buttons, the right end of the toolbar is cut off, presumably because of the wider button with the dropdown arrow. The same happens if I dock the toolbar vertically. How can I make my docking toolbar work with both dropdown buttons? Observant Readers
|
Figure 3 Non-docking Toolbar |
|
Figure 4 Docking Toolbar |
When you call LoadToolBar to load your toolbar, MFC reads the (x, y) button size from the toolbar resource data and stores these numbers in a private CSize data member, CToolBar::m_sizeButton. Whenever CToolBar needs to calculate the size of the toolbar window in CalcSize and WrapToolBar, it uses m_sizeButton as the button size. In particular, MFC's implementation assumes that each button in a toolbar has the same size and that the size never changes after the toolbar is loaded. These two assumptions fail if you make some buttons wider by adding TBSTYLE_DROPDOWN. It's a classic case of getting burned because you try to be efficient by caching a value, but then the real value changes after you cache it. A more correct implementation would not store the button size, but instead send a TB_GETITEMRECT message to the toolbar to retrieve the actual button size when needed.
|
Figure 5 |
The problem is fixed in Visual C++ 6.0, but for those of you who can't wait, I implemented a new class, CFixMFCToolBar, that corrects the problem. Conceptually, CalcSize goes like this: |
|
The actual function is significantly more complicated than I've shown42 lines in all. But the main point is, CalcSize uses m_sizeButton instead of the actual button size. What you want to do is change the function as follows: |
|
GetButtonSize is a new function I wrote to get the actual button size by sending TB_GETITEMTRECT to the button. But before I show you GetButtonSize, there's just one little problem here: how do I change CToolBar::CalcSize? It's not virtual. This is one of the most agonizing things that happens to C++ programmers; you struggle to track down some nasty bug and you locate the problem in a specific function, only to discover that the function isn't virtual (aarrgh)! So what do you do? Well, if you take a look at which MFC functions call CalcSize and which functions in turn call those functions, you'll end up with a tree like the one in Figure 6.
CalcFixedLayout and CalcDynamicLayout, which are virtual, both call CalcLayout, which calls SizeToolBar, which calls WrapToolBar and CalcSize. MFC uses all these functions to calculate the size of a toolbar based on its orientation, docking state, and so on. The code is long, ugly, and difficult to follow. Fortunately, all you have to understand is that WrapToolBar and CalcSize are the functions you want to change to use the actual button size instead of m_sizeButton. You have to copy all the functions verbatim into a new class, make the changes, and use the new class instead of CToolBar. It's pretty disgusting, but this is what you're stuck with when the gods-that-be decide not to make things virtual. |
Figure 6 CalcSize Callers |
CFixMFCToolBar does everything I have just described (it can be downloaded from the link at the top of this article). It reimplements all the CToolBar size-calculating functions, with the critical ones (CalcSize and WrapToolBar) modified to use a new function, GetButtonSize, to get the button size. Instead of replacing m_sizeButton everywhere with Get ButtonSize, I used a little C++ trick: |
|
At the top of the loop, I define a static variable, m_sizeButton, with the same name as the class member m_sizeButton. When two variables have the same name, C++ uses the one nearest in scope. In this case, it uses my local variable instead of the class member. This way, I don't have to edit any of the lines in CalcSize or WrapToolBar that refer to m_sizeButton; my local variable overrides the class member. This reduces the chance of introducing a bug when editing MFC source, and more importantly, ensures that I call GetButtonSize only once for each iteration of the loop.
Implementating the function GetButtonSize is almost trivial. It sends TB_GETITEMRECT to get the size of the button, then fudges the size in certain situations depending on the vagaries of comctl32.dll. For example, in comctl32 version 4.71, if the toolbar is vertically aligned, it returns the height of a separator in the width field. (Don't you just love Windows?) The final implementation can be found in the code file. Before you send me email, I'll confess right now that I may not have gotten the calculations straight for every possible version of comctl32.dllwhich is why I made GetButtonSize virtual. If my algorithm is flawed for some reason, you can easily change it. This is how you design a class. When I implemented CFixMFCToolBar, I discovered another problem. The MFC toolbar calls two other non- virtual functions, _GetButton and _SetButton, to get and set the TBBUTTON data. These seem to be normal non-virtual protected functions, but when I built my program, I got "undefined function" linker errors for them. Scratching my head, I looked at the MFC source more carefully, only to discover that _GetButton and _SetButton are linked into a special MFC-internal code segment AFX_CORE3_ SEG, which makes them invisible to your app. Man, I hate it when they do that! So I had to copy _GetButton and _SetButton as well. In the process, I renamed them GetButton and SetButton, and made them public since they're so useful. To employ CFixMFCToolBar, just use it to instantiate your toolbar instead of using CToolBar. For full source code see Aug98CQA.exe |
|
Once I edited this line in the original MyEdit program from the May 1998 issue, the toolbar showed all its buttons, whether docked, floating, or vertical. Unfortunately, if you dock the toolbar vertically, ToolbarWindow32 insists on displaying a separator after the Save button, even though it looks grotesquely tacky. This is a bug in comctl32.dll, and the only way to fix it is to get into custom draw, which is beyond the scope of this column.
In case you don't like the bogus separator and would prefer the original, equally bogus narrow vertical toolbar without the dropdown arrow (see Figure 5), I invented a flag, CFixMFCToolBar::m_bShowDropdownArrowWhenVertical, which you can set to FALSE. Incidentally, Visual C++® 5.0 has the same problem; if you dock the toolbar vertically, the dropdown arrows for the Undo/Redo buttons disappear. Of course, only wackos and people with their heads on sideways dock their toolbars vertically. The toolbar-sizing bug in CToolBar has been corrected in Visual C++ 6.0 (due out soon), but I've seen the fix and it still uses m_sizeButton, adding a fudge factor in CalcSize if the button has TBSTYLE_DROPDOWN set. Yuk! CalcSize and WrapToolBar still do not retrieve the actual size with TB_ GETITEMRECT. This means CToolBar may break yet again if the button sizes ever changeso CFixMFCToolBar may still prove useful. And incidentally, CToolBar is still littered with all the calls to DefWindowProc I pointed out in the May 1998 column. Sigh. There are some lessons here. It's certainly admirable to be performance-conscious, but it often breaks your program. In particular, you should never store any value that's subject to change. One of the basic rules of Good Programming 101 is that a value should exist in one place and one place only. Whenever you need the value, get it from there. If profiling later reveals a performance bottleneck, then you can worry about it. Of course, whoever first wrote CToolBar never expected the button width to change, which leads me to another lesson: a good class does what it's intended to do, but a great class does what it was never intended to do. I hate to beat a dead horse, but CToolBar fails in this regard; it breaks whenever something new comes along, like dropdown arrows, or you try to do something different. But doc/view architecture, on the other hand, is quite flexible. But how do you anticipate the unanticipatable? By using virtual functions! Whenever you find yourself implementing some algorithm or operation (for example, computing the layout of a window or getting a button size) you'll do well to expose it as a virtual function. If the algorithm has sub-algorithms, make them virtual too. That way, if your code is flawed or becomes outdated, someone else can correct it just by overriding a function. Don't take the paranoid (and patronizing) I-know-best attitude MFC sometimes seems to take. And don't be afraid to go virtual. |
|
From the August 1998 issue of Microsoft Systems Journal.