Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Read-Only Combo with Selectable Text

0.00/5 (No votes)
30 Nov 2005 6  
A combo box which behaves as a fully disabled combo box except that it allows you to select the text.

ScreenShot.png

Introduction

Rarely in the field of coding has so much energy been expended to produce so little. This problem is one of those itches that much be scratched; though trivial, it cannot be left alone until solved - something only true nit-pickers can appreciate. The problem is to create a "disabled" combo box which allows you to select the text in the edit box, or read-only combo box if you prefer.

Background

The fundamental problem is that, once you make a combo read-only by conventional means (disabling), you can't do anything with the text. This is frustrating because sometimes selection-and-copying is useful. There's an article by Tim McColl here which changes the edit color of a disabled combo to make it more readable, but doesn't allow selection. In one of my own projects, I did get as far as making the text in the edit box selectable without actually disabling the combo box; I simply blocked mouse clicks on the arrow and key presses in the edit in "read-only" mode. But then the problem morphed into an aesthetic one: the arrow remained frustratingly "enabled".

The (supposedly definitive) solution which I originally posted to this problem, which lets us face it, was a little bit ugly, and consisted in recreating the combo box as a "simple" combo and indulging in various forms of evil voodoo to make it look like a drop-down one. The idea came from Rich Frank, who had used Paul S. Vickery's RecreateComboBox function to change the combo type between simple and drop-down, and thus hide the arrow when he didn't want any editing - not quite what I wanted, but it didn't take much to imagine the rest - just paint the arrow in and voila.

All very devious, but its lack of elegance suggested that there had to be a better way. There was, and it was blindingly obvious. When the combo paints itself, just mask out the arrow part to prevent the enabled arrow being painted, and then paint in the disabled one instead.

Actually, this solution does create another minor problem, namely that the drop-list-type combos are no longer amenable to the read-only treatment, since they don't have edit controls, or indeed any other sort of child control that might allow selecting. This is pretty easy to solve, but involves a slightly dirty trick. Both drop-down and drop-list combos need to be created with the drop-down style, so that there's always an edit for us to play with. A flag, set in the handler, then determines the combo type. The handling is almost the same: the read-only mode is identical, and for editable drop-lists, the only thing we do is block more keys to stop the text from changing.

If you're wondering what I mean by "handler", it refers to the fact that I decided to avoid implementing this functionality in a derived class and use subclassing instead. See the section No Inheritance Required below.

How it works

First, what we need to do is make the normal "enabled" arrow disappear when we're in read-only mode. It's frighteningly simple once you know how. First, you need to know the exact position and dimensions of the combo arrow, which is not quite as trivial as it sounds. I hacked around for ages with borders and edges, trying to get the damn thing to work with all the Windows XP themes I could throw at it, as well as in the Windows classic mode, but no. It was never quite right. I tried the theme metrics functions, but I could never get them to work at all, for some reason. Then I discovered GetComboBoxInfo(), and proceeded to kick myself quite hard. Where had that function been all my life? It gives you all the dimensions in three lines.

Armed with this, the masking function is a doddle.

 void CReadOnlyComboHandler::MaskComboArrow (CDC& dc)
 {
  CRect rectArrow;
  
  if (m_bReadOnly)
  {
   rectArrow = GetComboArrowRect();
  
   // Exclude the arrow area from the paint update region

   dc.ExcludeClipRect(&rectArrow);
  }
 }

You just call this before calling the standard paint function, and bob's your uncle. OK, so having made the arrow disappear, all that remains is to paint a new, disabled one. The only complicating factor is the handling of XP themes. To do this, I've made use of a modified version of David Yuheng Zhao's Visual XP Styles class, which safely wraps the calls to the XP themes DLL. I've made some changes, in particular, in the routine which checks whether themes are in use. I've found that you need to check the version of the ComCtl32 DLL loaded by the app in addition to other checks, to be sure that themes are activated. Here, by the way, in GetComboArrowRect(), is where we make use of that nice function GetComboBoxInfo().

void CReadOnlyComboHandler::DrawDisabledDropArrow(CDC* pDC)
{
 ASSERT(m_pCombo != NULL);
 
 // If read-only, draw a disabled down-arrow

 if (m_bReadOnly && m_pCombo && pDC)
 {
  CRect rectArrow = GetComboArrowRect();
 
  // Now make the arrow rect the clipping region

  // and paint the arrow

  CRgn rgnArrow;
  rgnArrow.CreateRectRgnIndirect(&rectArrow);
  pDC->SelectClipRgn(&rgnArrow);
 
  // Draw control at RHS of control

 
  // If app is themed...

  if (CVisualStylesXP::GetInstance()->IsAppUsingThemes())  
  {
   HTHEME hTheme = 
    CVisualStylesXP::GetInstance()->OpenThemeData(m_pCombo->GetSafeHwnd(), 
    L"COMBOBOX");

   CVisualStylesXP::GetInstance()->DrawThemeBackground(hTheme, 
                         pDC->GetSafeHdc(), CP_DROPDOWNBUTTON, 
                         CBXS_DISABLED, &rectArrow, NULL);
   CVisualStylesXP::GetInstance()->CloseThemeData(hTheme);
  }
  else
  {
   // Do the draw 

   pDC->DrawFrameControl(rectArrow, DFC_SCROLL, 
                         DFCS_SCROLLDOWN | DFCS_INACTIVE);
  }
 }

Another point worth commenting on is that, to prevent the arrow keys being used to change the combo selection, we also need to install a handler for the edit box and use it to block these key presses when the edit is read-only. Previously, I used a derived edit class, but this is much cleaner. The handling of key presses is a little more refined now too; you can always use the arrow keys for selection, and you can use the up/down keys to change the selection in drop-list mode, even though you're prevented from editing. CTRL-C (or whatever the local combination is) should also work for copying, and I think I managed to stop all backspaces and deletes. As a final touch, the edit handler also blocks the cut and paste commands when in read-only mode.

One caveat: the code is expecting a drop-down or drop-list combo only. Using it with a "simple" combo makes little sense; I'm not sure if the results would be benign or not. I advise against it. In fact, using it with any type other than drop-down is a little pointless, since the drop-list style doesn't give you an edit and thus prevents you from selecting text, which is the whole point of this exercise.

In this update, I've changed the CComboBox pointer to an HWND to avoid problems with window maps when this class is used in a DLL.

No Inheritance Required

From version v1.1, all this is implemented in a "plug-in" handler class, rather than as a derived combo box class. This allows you to add the read-only functionality to any combo class, including your own derived MuchBetterSuperWidgetCombo classes. Well, that's the nice theory anyway. To do this, the handler replaces the combo's WindowProc with its own custom procedure to make the required modifications, doing the same for the combo's edit control. I've essentially copied this idea from GipsySoft's BuddyButton, but implemented it in a class rather than as a global function. I've also included a facility to subclass the combo edit control, because it was something I needed to do. SubclassComboEdit() and UnsubclassComboEdit() will do your subclassing and hook and unhook my handlers accordingly to make everything work. Adding the same functions for the combo would be simple, but I haven't done it.

Using the code

Pretty easy really. You just create an instance of the CReadOnlyComboHandler class and initialize it with your combo by calling Init(pMyCombo). You call SetReadOnly(true) on the handler to make it read-only. If you want a "drop-list"-style combo, create the combo with the drop-down style, and call SetDropListMode(true). You can, of course, include the handler as a member of your own derived combo class if you wish. I suppose I should include a derived combo class with the handler as a member for those who don't have their own derived classes and don't want to have to keep track of combo handlers. Maybe next year :-).

I'd love to hear of people's experiences with it, as well as reports of bugs, problems, improvements, etc.

Debts to pay

I have consulted or used a frightening amount of other people's code to accomplish such a modest objective. In particular:

History

  • v1.0 - 8 November 2004 - Posted.
  • v1.1 - 21 November 2004 - Re-implemented as an external handler class. Made compatible with drop-list combos.
  • v1.3 - 22 November 2005 - Massively delayed update. Combo no longer recreated, arrow fixed by masking. Now gets arrow dimensions correctly for all XP themes. Made combo into HWND.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here