ITAdvancedComboBox

From ISoft Wiki
Jump to: navigation, search

This page is designed to explain to developers how and why to utilize the ITAdvancedComboBox in our code.

Why

This is because the main alternative (CComboBox or CComboBox subclasses like the ITComboBox) have some inherent flaws, inconsistencies, and weirdnesses. We've spent years trying to get around these, but unlike most of the other win32 commmon controls, "COMBOBOX" type controls don't send enough messages/call enough virtual functions to allow a subclass to *truly* change its behavior.

A lot of the difficulties stem from the fact that the CComboBox takes on too much. Combos of type CBS_DROPDOWN (dropdown that can be edited) are very different in aim, behavior, and goal than CBS_DROPLIST combos. CBS_DROPDOWN are essentially auto-complete edits, and CBS_DROPLIST are essentially a 'select one only' list control. Obviously, taken this way, these are 2 very different controls. Lets not even talk about CBS_SIMPLE, which is essentially just a list box (WTF?) and is 100% different. The outcome of this is that the combo box often does things in one of those cases that doesn't work very well in others. In some cases, we weren't ever able to fully correct the strange behavior.

This is annoying in several significant cases:

  1. The CComboBox's auto-complete behavior is not intuitive and is inconsistent. This is especially true when comparing CBS_DROPDOWN and CBS_DROPLIST combos.
  2. It is nearly impossible to get/set the size of the droplist or resize it dynamically.
  3. Because of a weird multithreaded thing that happens when the dropdown first appears, we can't ever make the dropdown initially visible.
  4. Doing ownerdraw things like font/color/size modification is difficult because the internal listbox isn't exposed to subclasses of a CComboBox.
  5. CComboBox's ignore information passed into the MoveWindow call except for width, and top (IE height is ignored).

The ITACB solves all these problems to at least some extent. This is mostly done by not being a CComboBox subclass. It inherits directly from CWnd. For the most part, I used the CComboBox's behavior as the starting point for our control's behavior, modifying strangeness as I came across it. In addition, the ITACB has a resizable dropdown, which is cool.

How:

This is a bit more complicated, but, as you'll see, there are some tools in place to make this easy. First, an explanation of why this is complicated.

Why are custom controls hard in VS?

When you use the class wizard to add controls to your dialog template, the resulting compiled resource has the win32 registered classnames embedded in it ("COMBOBOX" for a win32 combo box). The win32 common controls are all loaded with the dependencies at program load time. So, initializing any dialog template with one in it succeeds. However, our custom control, which registers itself under ITAdvancedComboBox (or something) doesn't exist in the resource chain until RegisterClass is called. This is done in the constructor of the class. The constructors are called very early in initialization (before create is called), but not before MFC loads all dialog resources from an afx extension dll and puts them in the primary resource chain (which is done when the dll is LoadLibrary'ed). This means that normally, if you embed an ITAdvancedComboBox directly into a resource template, it will fail.

This is not true for dialog based applications, but who the fuck uses those? I believe we could side step this issue if we made master (hard-coded) register all our custom controls. I think that since our whole app uses one shared resource chain, this would be sufficient in all contexts, but I don't know.

That said, I didn't go in that direction because using "Custom control"s in the VS dialog editor is really crappy and hard. Since VS doesn't 'know' about your class, it just gives you a really general config screen. I believe you can set the window text, size, control ID, and you can set the window style and EX window style. However, the window style has to be entered as a hex value (rather than a bunch of individual check boxes like with a normal CComboBox). I think people have made VS plugins that make custom controls work in here, but I didn't want to look into it.

The Solution

The best solution to this problem that I've found so far (actually proposed in an article of Flounder's that I read) is the following. Create normal 'combo box' controls in your dialog template. Set its styles/etc like normal. Create an ITAdvancedComboBox member in your dialog. In OnInitDialog, call m_cmbMyCombo.CreateFromPlaceholder(this, IDC_COMBO_MYCOMBID). What happens is that the ITACB will read the information about the COMBOBOX win32 control you configured from the dialog and configure itself identically. It will then destroy the win32 control and put itself in its place.

Things that are copied over include:

  • Window style, including combo specific styles like CBS_DROPDOWNLIST/etc
  • EX window style
  • 'statically' entered combo options (entered in the class wizard)
  • Location
  • Size
  • Location in the tab order

What isn't copied over is the initial size of the dropdown list. This is because (as mentioned above) it is very difficult to retrieve the desired size of the dropdown. This is partly because the list box whose size we're interested in doesn't exist until the combo displays it for the first time.

How I normally add a new ACB

Considering the solution above, the way I like to add a *new* ACB to a dialog is by adding a normal combo box to the dialog (as per usual). Add a member variable for that resource (the classwizard will create a CComboBox). Change CComboBox to ITAdvancedComboBox, and delete the DDX_Control macro entry (this attempts to bind your class to the window handle associated with a control ID). We can't use DDX_Control because that would tie the ITACB control to a win32 COMBOBOX control. This actually works (doesn't crash) since many of the same messages are used, but you'll see it has 2 dropdowns and it looks crazy and doesn't work quite right. Then, in OnInitDialog, add m_cmbMyCombo.CreateFromPlaceholder(this, IDC_COMBO_MYCOMBO). So the steps are:

1. Add normal win32 combo box to dialog 2. Add member variable through class wizard and change it to an ITACB (or manually create an ITACB in the .h file) 3. Delete the DDX_Control macro entry (if any) 4. Call CreateFromPlaceholder(this, IDC_COMBO_YOURCONTROL) on each of your ITACB's on OnInitDialog.

Conversion From CComboBox

I don't really know of any situations in which the CComboBox is superior. It is undeniably sufficient for CBS_DROPLIST combos (with sort turned on so that typing in the box works right), but leaves something to be desired in all other cases. For this reason, I'd promote use of the ITACB in all combo related situations. I have optimized it to be at least as fast as a CComboBox, though the process of having to create/destroy a win32 combo (placeholder) and replace it with the new combo probably adds time to initialization, it hasn't seemed significant (plus its constant time overhead).

In order to mass-convert a project, the following would need to be done: 1. Add an include in the appropriate headers so that the ITACB symbols are available. 2. Change all CComboBox members into ITAdvancedComboBox members. 3. Using a regular expression, turn the swath of DDX_Control messages that configure these combos into a similar swath of CreateFromPlaceholder calls. I leave the creation of this regex as an exercise to the reader, however it could be added here if someone was so inclined.

That is really it. The ITAdvancedComboBox should throw all the same messages that people were used to intercepting from a CComboBox, and it has all the same member functions. It also subclasses the ITCommonControl the same as the ITComboBox, so it should be a drop-in replacement. The ITACB has more functionality, but doing this type of replacement should work as expected.

If anyone runs into other considerations, important configurations, or landmines, they should be added here.