
This weekend I attempted to fix a cosmetic bug in eChef that had been bothering me for a long time. It didn’t go so well.
Below is a cropped screenshot from eChef 1.1. This is a new recipe window to which I have just added 1 cup of flour as an ingredient.
Now I’m going to drop down the Unit combobox.
Do you see it? The combobox grew in height by about 3 pixels, causing the bottom of the arrow to be cut off. This is very minor and superficial bug; it doesn’t affect functionality at all. But I am a perfectionist, so while I was working on some related code I decided to take a crack at it.
Warning: most of this post is very technical. Feel free to skip ahead to the conclusion if you’re not feeling nerdy.
The reason the combobox grows has to do with the way these grids work. We use .NET 2.0’s DataGridView class, which conserves resources by only instantiating one editing control for each column. When a cell is not being edited, it draws itself to resemble its control in an unfocused state. That’s what you see in the first screen shot: the cell has drawn a fake combobox at the correct height for this row (18 pixels).
When editing begins in the cell, the grid gives the cell the editing control, it’s initialized to hold the right value, and then it takes over drawing itself. That’s what you see in the second screen shot: a real combobox drawing itself. Unfortunately, the real combobox ignores the height of its containing cell, resulting in the cropping effect.
Naturally the first thing I tried was just setting the combobox’s height during its initialization, but it turns out comboboxes (at least in .NET) cannot be resized vertically. For some reason you can draw one at an arbitrary size à la unfocused cells, but you can’t make a real one be any height except 21 pixels. It was at this point that I started more serious experiments.
Since I knew I could draw a fake one at any height, I thought maybe I roll my own combobox control, and re-implement all the necessary functionality from scratch. It would mean reinventing the wheel, but it would give me complete freedom in rendering.
I was somewhat relieved to discover that this wouldn’t work. An editing control apparently can’t draw outside the bounds of its cell, so rendering the dropdown list proved impossible. I’m not sure how the built-in combobox actually accomplishes this; I suspect the DataGridView treats it as something of a special case, and passes the drawing duty on to some native part of Windows that’s not bound by this restriction.
So if I couldn’t draw my own combobox, I thought maybe I could override an OnPaint event somewhere and draw over the system one. Again, no dice. No matter where I hooked in, I couldn’t draw on top. Even if I overrode the OnPaint method of the containing form itself to ensure that my drawing happened last, combobox controls were unaffected. This provided further evidence for my theory that they are drawn by some low-level system routine; that is, by a cheater.
After a little more research, I found an API call outside of .NET that might work. I also decided to hide my combobox from the DataGridView by wrapping it in a custom control—the idea being that if the grid was the one responsible for passing off combobox drawing to the system, perhaps that I could sneak the combobox past it in disguise.
As it turned, out I could adjust the height of a combobox this way. The custom control I made worked like a charm: I finally had a combobox with an adjustable height. But, not when embedded in a cell. For whatever reason, as soon as I put it in a DataGridView, the combobox went back to its default height.
End of technical stuff.
After all this effort, I finally gave up. I tweaked our grids so that rows with combobox cells would draw 3 pixels taller to accommodate them. They now look like this when unfocused.
And like this when they’re dropped.
Which certainly looks fine, but is 3 pixels per row more than I wanted to sacrifice.
UI problems like this aren’t new for us. While working on eChef I’ve come across plenty of cases where the built-in UI components of .NET (and Windows in general) are simply not sufficient for what we wish to accomplish. Usually this is because we want something to work The Way It Works in iTunes, which is obviously not something Microsoft goes out of its way to support or sanction.
The solutions vary in complexity: sometimes it’s a simple tweak, and sometimes we have to rebuild basic functionality from scratch. But this weekend I experienced the first case where I absolutely could not attain the desired effect; success was impossible. I had to cut my losses and compromise a solution. This was both frustrating, since I’d dumped too many hours into a lost cause, and depressing, since, well, that is the nature of concession.