Joao Paulo Figueira (joao.fig@mail.telepac.pt), October 07, 2003.
I want to create a combo box with check marks on its items. How do I do it?
In Windows CE neither lists nor combo boxes are owner-drawable. This means that MFC classes like CCheckListBox cannot be implemented (MFC 3.0 has the source code, but it is excluded from compilation by the preprocessor). This does not mean, though, that implementing lists and combo boxes with check marks on the items is an impossible proposition.
During the research for this article, I found two possible answers for this question. The first fakes a combo box using a custom control and a list view control. The second, and most effective one, subclasses both the combo box and its list and uses an interesting technique for showing the check marks.
For the hurried reader, I recomend jumping right now to the sample application source code: there is a lot to read ahead.
I have modeled the solution's behaviour from this article: CheckComboBox Control
One of the most popular ways to create a list of text items with check marks is by using a list view control. There is a very popular article by Daniel Strigl on the CodeProject that shows you how to do this. Enabling check marks is a simple matter of setting one of the list view's extended style bits (LVS_EX_CHECKBOXES).
This led me to believe that I was in the right track for a solution. So, my next question was how to create the fake combo box. My first option was to do something like this. The approach used in this article was to use an owner-drawn button to simulte the color picker control. Also, I used the same approach for creating the drop list: a popup window. As you can see from the article´s image, this is not the right solution: the property sheet lost the ok button ang got a smart minimize button. Helping me drop the solution were the workarounds I had to use to make the owner-drawn button work like a combobox.
So I decided to go for a custom control. This control (CCheckCombo) is based on MFC's CWnd, so I had to add all the painting and clicking logic to it.
One of the first problems I had to overcome was inheriting the parent's font, especially on dialogs. Dialogs automatically create the child controls from the resource script using the control's window class. When these are created, the dialog uses its own font to set the children's font. This is done using the WM_SETFONT message, where the HFONT is sent in the WPARAM parameter. The control has to store this handle somewhere and use it for drawing text. This doesn't work if you register your class with DefWindowProc as its window procedure. The solution for this problem was to create a small window procedure that handles the WM_SETFONT message and stores the HFONT data in the HWND storage area using SetWindowLong(). This window procedure is only used until the control is subclassed by MFC. From then on, I will use MFC's message handling mechanisms.
Now we can go about the business of having this control showing and hiding a checked list view (CCheckList). As you can see from the source code (CreatePopup()) the CCheckList is created as a sibling of CCheckCombo using the WS_CLIPSIBLINGS window style and forcing it to the top with BringWindowToTop().
Combo box items are stored in CCheckCombo using the CCheckListItem helper class. When the list is created, these items are used to populate it and set the check statuses.
Almost everything worked like in the model. I say almost because I couldn't get the list to close exactly as a combo box. Combo boxes close their lists when you click the combo arrow or anywhere but the list. This behaviour is achieved by using the stylus capture - something the list view control just does not allow you to. As I found out, the list view uses the stylus capture for its own internal implementation, so you cannot rely on securing the stylus capture when using this control. This is a dead end. We need another solution.
The second solution was born out of two very important pieces of information about combo and list boxes.
First: combo boxes are actually two controls. The first is the permanent window that may contain an edit control, and the second is a regular list box control. Using the Spy utility you can see that the list box part is a child of the root window (HWND_DESKTOP).
Second: A list bo can manage child windows. This is an interesting behaviour: list boxes can have child windows! And even more interesting, they will be scrolled appropriately, so you can actually build a list box containing check buttons.
Bingo! Here is the (almost) perfect solution. But before I got into implementing it, I developed an MFC control to test the ideas and code opf the aforementioned article. You can find my implementation (CCheckBoxList) on the sample application. It is by no means a full implementation, but you can certainly build from there and it illustrates the basic principles.
Now, we only need to apply this knowledge to combo boxes. The first thing we need to do is to get the list box handle. This is documented on the CheckComboBox Control article. This is done by intercepting the WM_CTLCOLORLISTBOX message that has the list box handle on the LPARAM. I have to confess that my first attempt at subclassing the list was through MFC's SubclassWindow(). After all, I already had an MFC class to handle lists with check buttons... Well, I was wrong. After using the reset button of my PDA for several times, I was forced to believe that this was not a good solution. What else? Well, when all esle fails, use the API. But this was a tough choice because I had to rewrite all of CCheckBoxList's functionality into this class (CCheckBoxCombo). Worse: I have to manage two window procedures inside one class!
To sort this mess out, I used an STL map to store pairs of list box HWND and CCheckBoxCombo pointers. The subclassing window procedure uses this map to retrieve a CCheckBoxCombo* given the list box's HWND that is supplied as its first argument. From then on, we have a pointer to the combo box class that subclasses the list box, so we have our hands free to go about our business.
The subclassing window procedure (CCheckBoxCombo::ListWndProc()) handles a number of list box messages, some of them in a counter-intuitive way. If you look at the code, you will see brief explanations. Here are their full versions:
WM_LBUTTONDOWN: Somehow, the list box (or HWND_DESKTOP) has captured the stylus, so all stylus messages will be intercepted. This message will not be seen by the list box if the click happens over an item (button). If this happens, we get a handle to the button and simulate the clicking. Else, we let the original list window proc handle it.
WM_LBUTTONUP: This message cannot be seen by the list window proc, otherwise it will force the list to close. This is not a desired behaviour because we want the user to make several selections with the list down.
WM_MOUSEMOVE: This is the least intuitive of the three. If this message is seen by the list window proc, an item will be selected, and a blue strip will be painted. The big issue is that our buttons do not fill the row area completely, so the background will show through. This issue is solved in a different way in CCheckBoxList: we can specify the LBS_NOSEL style upon creation (see sample application).
The rest of the window procedure should be pretty obvious. Finally, let's discuss why I'm using CBS_DROPDOWN as the combo style. The control desing reference states that when a user selects or deselects an option, it must be reflected on the combo box main window. This would be very difficult to achieve with the CBS_DROPDOWNLIST style. Using the CBS_DROPDOWN style, setting the text is very simple.
Allowing users to write on the edit control is not an acceptable behaviour. After searching for several possible solutions, I decided to intercept the CBN_EDITCHANGE notification and force the window text and selection. This is a bit of overkill, but clean.
Neither solution is perfect.
The first solution has the advantage of using a list view control. This control can be made "virtual", meaning that you can have a very large number of items in it. The disadvantage is that it does not behave like the real thing: clicking outside the control will not close it.
The second solution it the real thing: a real combo box. It behaves like one and you can cleanly use it in the dialog editor. Its major problem is the exact reverse of the first solution's advantage: how many check buttons can you create inside the list before it becomes unusable? It is slower to create and destroy.
And that's it.
You can download a sample here - CheckComboDemo.zip (62K).