Don’t you just hate it when computers try to be clever but get it wrong? OK, it’s not really the computer itself but the programmer who tried to be clever. The problem usually is that he overlooked a corner case that you then hit where his sophisticated strategy fails and leaves you with something – lets say less desirable.
Today I wasted several hours trying to find out what was wrong with my hack to create a TSpeedButton which can take the input focus (which standard TSpeedButtons can’t), by using a TBitBtn instead. I blogged about this before.
There was a certain button width which caused the button to display only a square part of the caption. At closer inspection it turned out to not be a fixed width but kind of a width to height ratio that caused the problem. Here are several buttons with different widths and heights that show this behaviour:
Buttons with a width to height ratio of (w-1) : (h-1) = 4 : 1 looked like the screenshot above, e.g. 97×25, 101×26, 105×27, 93×24 or 89×23. Other sizes were fine.
The code I use in this hack is very simple: I generate two bitmaps, one for the Up state of the button, the other for the Down state and assign them to the TBitBtn’s Glyph property.
procedure TdzSpeedBitBtn.UpdateGlyph; begin if FBtn.Tag <> 0 then FBtn.Glyph := FDownBmp else FBtn.Glyph := FUpBmp; end;
That bitmap is always one pixel smaller than the button itself, so the problem occurred with bitmaps of 96×24, 100×25, 104×26, 92×23 and 88×22 pixels respectively.
So I tried to reproduce that behaviour with a simple TBitBtn with default settings by assigning a bitmap with 96×24 pixels. It looked like this …
… even at design time. Closer inspection showed that the NumGlyps property also had a different value than expected: 4. Setting it to 1 made the button display the whole bitmap again:
So there seemed to be some code behind assigning a bitmap to the glyph that tried to guess how many images there actually are stored in the bitmap. If the width to height ratio is 4, it assumes that there are 4 images in the bitmap.
With that knowledge I looked into the documentation of TBitBtn.Glyph and guess what: It’s documented behaviour, kind of:
You can provide up to four images within a single bitmap. All images must be the same size and next to each other in a row. Bit buttons display one of these images depending on their state.
[…]
If you have multiple images in a bitmap, you must specify the number of images that are in the bitmap with the NumGlyphs property
I actually knew that but since I rarely use TBitBtn controls I hadn’t thought of it. It doesn’t say that the code tries to be clever and guess how many images there are though.
Tests showed that the code calculates the width to height ratio and for ratios of 2, 3 or 4 assumes that to be the number of images and sets NumGlyphs accordingly. And of course it’s easy to find once you know what you’re looking for:
procedure TButtonGlyph.SetGlyph(Value: TBitmap); var Glyphs: Integer; begin Invalidate; FOriginal.Assign(Value); if (Value <> nil) and (Value.Height > 0) then begin FTransparentColor := Value.TransparentColor; if Value.Width mod Value.Height = 0 then begin Glyphs := Value.Width div Value.Height; if Glyphs > 4 then Glyphs := 1; SetNumGlyphs(Glyphs); end; end; end;
(from unit Vcl.Buttons)
Why did it take me several hours to find some simple problem like this, you ask? (I asked myself that question too.) Of course this didn’t happen in a simple test program like the one in the screen shot. It happened in a dialog in GExperts. GExperts is a plugin for the Delphi IDE and as such must be built with runtime packages. And if you do that, you can’t compile with debug dcus and simply step into the RTL/VCL code in the debugger, all you can see is some assembler code. So I first tried to find the problem in my code. Did I maybe overlook a corner case? Then I tried to reproduce the problem with a test program but couldn’t, because the width to height ratio of my test buttons wasn’t the one which caused the problem. I puzzled quite a while over this, looked into the DFM files to find anything suspicious, but there was nothing. Only after I copied the buttons from the GExperts form to the form of the test program I could reproduce it.
Once I knew the cause, the fix was easy: After assigning the Glyph change NumGlyphs back to 1.
procedure TdzSpeedBitBtn.UpdateGlyph; begin if FBtn.Tag <> 0 then FBtn.Glyph := FDownBmp else FBtn.Glyph := FUpBmp; FBtn.NumGlyphs := 1; end;
This is the way it should have looked all along:
The fix is now in my dzlib repository on OSDN.
To be fair: The programmer meant this as a convenience function and it probably works most of the time. But if it doesn’t it becomes a pain in the lower back to debug.
If you want to discuss this article, you can do so in the corresponding post in the international Delphi Praxis forum.