Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Hacking the Combo Box to give it horizontal scrolling

Rate me:
Please Sign up or sign in to vote.
4.82/5 (41 votes)
31 Jan 2005CPOL14 min read 261.4K   14.8K   71   38
How to hack the combo box to give it a horizontal scroll bar, thus giving a polished look with ease + simplicity.

Combo Box with Dropdown exceeding client window border

Figure 1

Combo Box with Dropdown consisting of Horizontal scrolling and looking very trim!

Figure 2

Introduction

Welcome to my very first article (Thanks to Marc Clifton & others for guidelines to writing articles on CP!). *Takes deep breath* I hope it conforms to the CP guidelines! :-) ;-) The article is about showing how to solve a dilemma that I experienced when I was developing an application, and it governs the usage of combo boxes! Ahhh! This pesky control by Microsoft seems a bit too.... contrived, to say the least, and caused me grief in trying to solve the problem with combo boxes. Basically, the problem with combo boxes is twofold:

  • If the combo box items consist of very large strings, the dropdown is not going to show all of it, in fact, it gets clipped within the region of the dropdown box.
  • Secondly, the all too familiar code in adjusting the dropdown width to match that of the longest string can cause a UI issue. The issue is that if a window is designed in such a way that a combo box appears near the border of the client window and it happens to have a very long string, the familiar code to deal with adjusting the dropdown width can make the overall look and feel a bit *cough cough* ahem, IMHO amateurish, since the dropdown box may or can indeed cover the edge of the client window, which can look a bit ugly.

Look at Figure 1 above to see what I mean. I know, I know, the screenshot looks simple, but what if you have a window that contains many controls, and a combo box happens to be near the edge of the client window and have a situation like the one as shown in the above screenshot! Hand on heart, it's not nice looking, is it?

Now, take a look at Figure 2 to see the horizontal scrolling in place. Now, the end user who would be looking at this, can safely scroll across to see if that pertinent selected item contains whatever is relevant, without fear of cluttering up the overall display. Sure, I can manually decrease the dropdown width and still it can be seen!

The first part of the solution involves having to figure out the length of the largest string in terms of pixels, which is easy enough. The last part is having to insert a horizontal scrollbar to the dropdown box, and this can make the overall look of the application more polished. (See Figure 2 above.)

Some of the code highlighted here can be found in the source archive.

I have included links to the relevant articles, at the bottom of the page, for reference.

Background

There're a few pre-requisites. First, knowledge of using the Win32 API is vital, P/Invoke a must have! Secondly, a knowledge of translating from VB.NET to C# (more about this in a second!). :-). Lastly, loads of patience, trial & error, plenty of coffee and cigarettes, and a good reading/looking up MSDN!! <g>

In order to determine the length of the largest string, it is not in string length we're talking about here, it is in terms of pixels. Have a look at this section of code which calculates the length in pixels for a range of list items within the Items collection of the combo box. (See Figure 3.)

C#
#region GetLargestTextExtent - Obtain largest string in pixels
private void GetLargestTextExtent(System.Windows.Forms.ComboBox cbo, 
                                              ref int largestWidth){
 int maxLen = -1;
 if (cbo.Items.Count >= 1){
  using (Graphics g = cbo.CreateGraphics()){
   int vertScrollBarWidth = 0;
   if (cbo.Items.Count > cbo.MaxDropDownItems){
    vertScrollBarWidth = SystemInformation.VerticalScrollBarWidth;
   }
   for (int nLoopCnt = 0; nLoopCnt < cbo.Items.Count; nLoopCnt++){
    int newWidth = (int) g.MeasureString(cbo.Items[nLoopCnt].ToString(), 
                                   cbo.Font).Width + vertScrollBarWidth; 
    if (newWidth > maxLen) {
     maxLen = newWidth;
    }
   }
  }
 }
 largestWidth = maxLen;
}
#endregion

Figure 3

Typical incantation of the above would be:

C#
#region cboBoxStandard_DropDown Event Handler
private void cboBoxStandard_DropDown(object sender, System.EventArgs e) {
 int pw = -1;
 this.GetLargestTextExtent(this.cboBoxStandard, ref pw);
 this.cboBoxStandard.DropDownWidth = pw;
}
#endregion

Figure 4

In Figure 4, the code consists of a combo box named cboBoxStandard and the DropDown event handler is wired up! Now, that's the first part of the problem solved, which will produce the result as shown in Figure 1.

The tricky part, is having to get the handle of the actual dropdown box; in C# speak, handle refers to SomeControl.Handle (it can also refer to a System.IntPtr type when using P/Invoke). In Win32 API, it is HWND which is a 32-bit double word otherwise known as a DWORD.

Great! I have the combo box's handle, but that is as far as it goes in the eyes of .NET. Looking at the following information found in the Microsoft's KB, INFO article: Q262954 titled 'The parts of a Windows Combo Box and How they Relate': There're actually three windows combined to form a combo box, well I never.... a Combo Box control whose Windows class is 'ComboBox', an Edit control whose Windows class is 'Edit' and finally, a list box whose Windows class is 'ComboLBox'. For the uninitiated, a Windows class from Win32 API point of view, is how each and every window is registered in the Windows system. That is to say, it is not an OO (Object Oriented) thing.

Righto! OK...ummm...hmmm...this 'ComboLBox' is what I'm interested in. In fact, it is the same as an ordinary standard list box, but contained in a window depending on the style of the combo box, i.e., Simple, DropDown or DropDownList. To recap:

  • Simple shows the text box, with the list box below it.
  • DropDown shows the text box which is editable with a dropdown arrow beside it, and pops up the list box.
  • DropDownList shows the text box, which is not editable, ditto the pop up listbox et al.

Right, next part is how to get at that list box's handle...so I dug deep within MSDN after a cuppa too many, with a few cigarettes included. There's a neat Win32 API function which does exactly what I needed to achieve this... GetComboBoxInfo which returns a reference to a structure called ComboBoxInfo. In Win32 API speak, it returns a pointer to a structure COMBOBOXINFO. See Figure 5 for the declaration of the function which is commonly used in C/C++ family of Win32 development.

// Win32 API Function as per MSDN docs
BOOL GetComboBoxInfo(HWND hwndCombo, PCOMBOBOXINFO pcbi);
//
C#
// C#'s equivalent Function.
[DllImport("user32")] public static extern bool 
        GetComboBoxInfo(IntPtr hwndCombo, ref ComboBoxInfo info);
//

#region RECT struct
[StructLayout(LayoutKind.Sequential)]
public struct RECT {
 public int Left;
 public int Top;
 public int Right;
 public int Bottom;
}
#endregion

#region ComboBoxInfo Struct
[StructLayout(LayoutKind.Sequential)]
public struct ComboBoxInfo {
 public int cbSize;
 public RECT rcItem;
 public RECT rcButton;
 public IntPtr stateButton;
 public IntPtr hwndCombo;
 public IntPtr hwndEdit;
 public IntPtr hwndList; // That's what I'm interested in....
}
#endregion

Figure 5

I included the StructLayout attribute to guarantee the values will go into the right offsets during the P/Invoke call, as using P/Invoke marshals the data from managed to unmanaged boundaries and back again. I wrapped up this function into a simple method as shown in Figure 6.

C#
private bool InitComboBoxInfo(System.Windows.Forms.ComboBox cbo){
 this.cbi = new ComboBoxInfo();
 this.cbi.cbSize = Marshal.SizeOf(this.cbi);
 if (!GetComboBoxInfo(cbo.Handle, ref this.cbi)){
  return false;
 }
 return true;
}

Figure 6

this.cbi is a global variable within the form's class. We call new on it to get a block of memory assigned to the variable, and we use Marshal.SizeOf() to pre-fill the cbiSize field of that structure prior to the call via P/Invoke. Some structures which are passed into Win32 API functions require this prior to P/Invoke. Check with the MSDN or pinvoke.net. Then pass it into the Win32 API function via P/Invoke, so that it is guaranteed that the block of memory gets filled up after the trip to the unmanaged world. If the call fails, we bail out, and the combo box will have standard default behavior after doing a simple check on the bool value returned in certain places! Great!

Now, that we have the list box's handle, next part is 'sticking in the horizontal scroll bar'. More coffee and cigarettes, more reading...until I came across an article written in MSDN's December 2000 edition 'ActiveX and Visual Basic: Enhance the Display of Long Text Strings in a Combobox or Listbox'. In the article, the author described how to achieve the above code in Figure 3 using VB 6. It provided the inspiration to do what I needed to do exactly, albeit it was in VB 6. Look at Figure 7 to see the classic VB 6 code.

VB
Private Const WS_HSCROLL = &H100000
Dim lWindowStyle As Long
lWindowStyle = GetWindowLong(List1.hwnd, GWL_STYLE)
lWindowStyle = lWindowStyle Or WS_HSCROLL
SetLastError 0
lWindowStyle = SetWindowLong(List1.hwnd, GWL_STYLE, lWindowStyle)

Figure 7.

It was a matter of translating the code directly to C#'s equivalent, as shown in Figure 8.

C#
[DllImport("user32")] public static extern int 
          GetWindowLong(IntPtr hwnd, int nIndex);
[DllImport("user32")] public static extern int 
          SetWindowLong(IntPtr hwnd, int nIndex, int dwNewLong);

public const int WS_HSCROLL = 0x100000;
public const int GWL_STYLE = (-16);

int listStyle = GetWindowLong(this.cbi.hwndList, GWL_STYLE);
listStyle |= WS_HSCROLL;
listStyle = SetWindowLong(this.cbi.hwndList, GWL_STYLE, listStyle);

Figure 8.

That section of code can be found in the cboBoxEnhanced_DropDown event handler. Basically, what the above code does is, it adjusts the style of the list box to include a Window Style Horizontal SCROLLbar. Every control has a default style, which is a combination of bits, that defines the behavior of the control and how Windows handles the default behavior or processing of events. In this instance, I extract the original bit-mask for the list box's handle using Win32 API Function GetWindowLong via P/Invoke. Then I perform a bit-wise OR on the mask itself to include the horizontal scrollbar, then call SetWindowLong via P/Invoke again.

The constants can be found in the SDK; if you have Visual Studio 2003, it can be found in the VC7\PlatformSDK\Include. A browse around the C/C++ header file winuser.h is where constants can be found; for common controls it is commctrl.h. If you don't have Visual Studio, why not try get the Borland C++ 5.5 Compiler (Command Line only - which includes the SDK stuff).

Note the use of this.cbi.hwndList in the above Figure 8 (this.cbi.hwndList was obtained in the above Figure 6)! That's how the horizontal scroll bar gets inserted into the list box. Next, we need to notify the list box's horizontal scrollbar so that the scrolling magic can take place. To achieve that, another Win32 API function call is required, our friend SendMessage.

C#
[DllImport("user32")] public static extern int 
      SendMessage(IntPtr hwnd, int wMsg, int wParam, IntPtr lParam);
public const int LB_SETHORIZONTALEXTENT = 0x194;

// Set the horizontal extent for the listbox!
SendMessage(this.cbi.hwndList, LB_SETHORIZONTALEXTENT, 
                           this.pixelWidth, IntPtr.Zero);

Figure 9.

So that's it...or so I thought....scrolling works just fine, the scrollbar's thumb-tracking doesn't work...damn... even more cups of coffee...OK...I realized that I need to subclass this list box and take care of the horizontal scrolling...more searching around until I came across a very fine article here on CP ' Subclassing in .NET -The pure .NET way' by Sameers (theAngrycodeR), which was written using VB.NET. It would be helpful if I could divert you to read the article and to understand how his code works. It is impressive! Thanks Sameers for publishing your article, without it, this wouldn't have been achieved!

Here's the translation of the VB.NET code into C#, as shown in Figure 10. I enhanced it slightly by changing the constructor and adding message crackers (a legacy from the Win 3.1 days when wParam and lParam were used to hold two 16 bit values within a long data type - which was C/C++'s datatype of 32 bit value at the time). Of course, this is an excellent example of how events/delegates comes into play here.

C#
#region SubClass Classing Handler Class
public class SubClass : System.Windows.Forms.NativeWindow{
 public delegate void 
   SubClassWndProcEventHandler(ref System.Windows.Forms.Message m);
 public event SubClassWndProcEventHandler SubClassedWndProc;
 private bool IsSubClassed = false;

 public SubClass(IntPtr Handle, bool _SubClass){
  base.AssignHandle(Handle);
  this.IsSubClassed = _SubClass;
 }

 public bool SubClassed{
  get{ return this.IsSubClassed; }
  set{ this.IsSubClassed = value; }
 }

 protected override void WndProc(ref Message m) {
  if (this.IsSubClassed){
   OnSubClassedWndProc(ref m);
  }
  base.WndProc (ref m);
 }

 #region HiWord Message Cracker
 public int HiWord(int Number) {
  return ((Number >> 16) & 0xffff);
 }
 #endregion

 #region LoWord Message Cracker
 public int LoWord(int Number) {
  return (Number & 0xffff);
 }
 #endregion

 #region MakeLong Message Cracker
 public int MakeLong(int LoWord, int HiWord) { 
  return (HiWord << 16) | (LoWord & 0xffff); 
 } 
 #endregion

 #region MakeLParam Message Cracker
 public IntPtr MakeLParam(int LoWord, int HiWord) { 
  return (IntPtr) ((HiWord << 16) | (LoWord & 0xffff)); 
 } 
 #endregion

 private void OnSubClassedWndProc(ref Message m){
  if (SubClassedWndProc != null){
   this.SubClassedWndProc(ref m);
  }
 }
}
#endregion

Figure 10.

Every control, no matter what, is inherited from NativeWindow which is the essence of how the .NET wrappers within the FCL work for all sorts of controls. There's one caveat emptor that I must mention regarding this class, it does not work for components such as ToolTips (BTW, its handle is not exposed at all! - Can somebody explain how to get at handle for controls such as Tooltips?). So now, it is a matter of deriving an instance of this class and passing in the this.cbi.hwndList into the class' constructor, create the event handler, and then we're in business..

C#
// Within the Constructor of the Form.
this.gotCBI = this.InitComboBoxInfo(this.cboBoxEnhanced); 
if (this.gotCBI){
 this.cboListRect = new RECT();
 this.si = new SCROLLINFO();
 this.scList = new SubClass(this.cbi.hwndList, false);
 this.scList.SubClassedWndProc += new 
    testform.SubClass.SubClassWndProcEventHandler(scList_SubClassedWndProc);
}

Figure 11.

RECT and SCROLLINFO are structures which hold the rectangle region and scrolling information (surprise, surprise) respectively. You'll see why I initialized/instantiated the variables...hint, hint, subclass...

C#
private void scList_SubClassedWndProc(ref Message m) {
 switch (m.Msg){
  case WM_SIZE:
   GetClientRect(this.cbi.hwndList, ref this.cboListRect);
   this.xNewSize = this.scList.LoWord(m.LParam.ToInt32());
   this.xMaxScroll = Math.Max(this.pixelWidth - this.xNewSize, 0);
   this.xCurrentScroll = Math.Min(this.xCurrentScroll, this.xMaxScroll);
   this.si.cbSize = Marshal.SizeOf(this.si);
   this.si.nMax = this.xMaxScroll;
   this.si.nMin = this.xMinScroll;
   this.si.nPos = this.xCurrentScroll;
   this.si.nPage = this.xNewSize;
   this.si.fMask = SIF_RANGE | SIF_PAGE | SIF_POS;
   SetScrollInfo(this.cbi.hwndList, SB_HORZ, ref this.si, false);
   break;
  case WM_HSCROLL:
   int xDelta = 0;
   int xNewPos = 0;
   int modulo = (this.xNewSize > this.pixelWidth) ? 
        (this.xNewSize % this.pixelWidth) : (this.pixelWidth % this.xNewSize);
   switch (this.scList.LoWord(m.WParam.ToInt32())){
    case SB_PAGEUP:
     xNewPos = this.xCurrentScroll - modulo;
     break;
    case SB_PAGEDOWN:
     xNewPos = this.xCurrentScroll + modulo;
     break;
    case SB_LINEUP:
     xNewPos = this.xCurrentScroll - 1;
     break;
    case SB_LINEDOWN:
     xNewPos = this.xCurrentScroll + 1;
     break;
    case SB_THUMBPOSITION:
     xNewPos = this.scList.HiWord(m.WParam.ToInt32());
     break;
    default:
     xNewPos = this.xCurrentScroll;
     break;
   }
   xNewPos = Math.Max(0, xNewPos);
   xNewPos = Math.Min(xMaxScroll, xNewPos);
   if (xNewPos == this.xCurrentScroll) break;
   xDelta = xNewPos - this.xCurrentScroll;
   this.xCurrentScroll = xNewPos;
   this.si.cbSize = Marshal.SizeOf(this.si);
   this.si.fMask = SIF_POS;
   this.si.nPos = this.xCurrentScroll;
   SetScrollInfo(this.cbi.hwndList, SB_HORZ, ref this.si, true);
   break;
  }
}

Figure 12.

Even more Win32 API function calls come into play here...well, that sounds like an overstatement, in truth that's two APIs here! :-) APIs used here are SetScrollInfo, GetClientRect, which can be seen in the above Figure 12...

  • GetClientRect - I needed to know how big is the list box when it gets dropped down and visible. During the trapping of WM_SIZE message, I can easily find out the width of the list box so that it can be used in setting up the thumb tracking in the scroll bar.
  • SetScrollInfo - This is where the thumb tracking in the scroll bar gets adjusted and repainted. The above code works...I admit, it's not exactly correct since I ripped the code from an example of scrolling a bitmap, from the MSDN article 'Using Scrollbars'.

The scrolling is not 100% accurate, download and take a look at the demo app and play with the thumb tracking and arrow buttons...that's it. From there on, the sky's the limit, and of course, you can put all of this into an extender control if you so desire. There, it wasn't too hard, was it?...a bit of ingenuity, persistence, and patience does indeed pay off!

Points of Interest

In the source archive, I have included two radio buttons and two labels, so it is slightly different to the screenshot in the above. It essentially changes the dropdown style at runtime to convince myself that the code works for both styles: DropDown and DropDownList.

No error checking is done. If you use this code, please put in error checking to make it production-ready! This hacking took me three days + nights.

While hacking this, initially, I tried to display a tooltip depending on the cursor position whilst the dropdown list box is visible, and the tooltip never showed up, it took me ages to figure out why - but I discovered through the MSDN archive, that apparently, the tooltip's window (in which the tooltip text is contained) has lower precedence than the dropdown list box, i.e., z-order of the window is such that the tooltip's window appears on the bottom of other windows. Hence the dropdown portion is on top of it and the tooltip will never show up! That I didn't know....but it is interesting because I initially made an attempt to simply bring the tooltip's window to the foreground via the Win32 API function call SetWindowPos. But then I was caught out as I realized that the handle of the tooltip wasn't exposed publicly...I don't know why...but that's for another day.....

The other thing, is that you might question - would subclassing the actual combo box work? To my amusement - with the above code in place, the combo box, get this...did not get any of the WM_HSCROLL messages.... funny this is, after investigating via Spy++, deciphering the hexadecimal messages flashing past my eyes, scrolling off the screen, and coming to a conclusion, the mouse capturing is taking place within the dropdown box and hence all mouse messages were sent to the dropdown box. That explains how the combo box got the focus on the dropdown box when the dropdown style is set to DropDownList or plain DropDown.

Yeah, I admit my code ain't reliable, i.e., the scrolling, but hey it works! :-)

I did put this into an extender control class, and learnt a very important lesson, if you intend to develop a custom combo control using the code like above, be sure that the combo box's parent handle is set to the form at design time, otherwise bizarre problems will appear, such as subclassing not firing, the horizontal scrollbar not appearing etc. In fact, GetWindowLong fails with an error code of 0, and a quick check to Marshal.GetLastWin32Error() informs me of error code 1400 which is 'Invalid Windows Handle', and SetWindowLong fails!! Bear in mind, that you would have to drop a plain Win combo box when in design view, and in the code, change it to match that of the user control/extender control and you should be OK. If you would like to see a working example of the extender control code, which you can add to the toolbox in VS 2003, drag and drop it on to the form etc., let me know!

Tip: It would be best to create an event handler for the DropDownStyleChanged and put the call to InitComboBoxInfo in there, as you would have to call it anyway in order to ensure that the dropdown box's handle is up-to-date and to instantiate a fresh instance of the subclass to match that of the up-to-date handle. Otherwise, you'll run into the similar situations and problems like I did regarding invalid handles and bizarre problems!

A side bonus that I discovered when I put the above code in place was I got automatic vertical scrolling when the high-light was at the bottom of the dropdown box, whether that was because I have a mouse-wheel-type of mouse - I don't know! :-) Another plus, if you have DrawMode set to OwnerDrawFixed with plain fonts and fancy colors for backgrounds etc., it works a treat. With images, you're on your own.

Final note: I was surprised at how much I have learnt from hacking this Combo Box. If I were ever to look at such a difficult control like this Combo Box again, I'd be feeling like 'Oh no! Not another pesky control *sigh*'......

References

  • MSDN - Windows Shell and Controls - GetComboBoxInfo.
  • MSDN - Windows Shell and Controls - 'Using Scrollbars'.
  • Microsoft's KB, INFO article: Q262954 titled 'The parts of a Windows Combo Box and How they Relate'.
  • MSDN's December 2000 article 'ActiveX and Visual Basic: Enhance the Display of Long Text Strings in a Combobox or Listbox' by John Calvert.
  • The Code Project - Subclassing in .NET -The pure .NET way by Sameers (theAngrycodeR).

History

Initial version of the article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Ireland Ireland
B.Sc. in Information Systems.
Languages: C, Assembly 80x86, VB6, Databases, .NET, Linux, Win32 API.

Short Note:
Having worked with IT systems spanning over 14 years, he still can remember writing a TSR to trap the three-finger salute in the old days of DOS with Turbo C. Smile | :) Having worked or hacked with AS/400 system while in his college days, graduating to working with Cobol on IBM MVS/360, to RS/6000 AIX/C. He can remember obtaining OS/2 version 2 and installing it on an antique 80386. Boy it ran but crawled! Smile | :) Keen to dabble in new technologies. A self-taught programmer, he is keen to relinquish and acquire new bits and bytes, but craves for the dinosaur days when programmers were ultimately in control over the humble DOS and hacking it!! Smile | :)

Comments and Discussions

 
AnswerRe: Why would you want to do this? Pin
Tomas Brennan14-Feb-05 4:37
Tomas Brennan14-Feb-05 4:37 
GeneralRe: Why would you want to do this? Pin
Dan McCarty14-Feb-05 5:14
Dan McCarty14-Feb-05 5:14 
GeneralRe: Why would you want to do this? Pin
Tomas Brennan16-Feb-05 4:30
Tomas Brennan16-Feb-05 4:30 
GeneralRe: Why would you want to do this? Pin
Anonymous10-Jun-05 9:48
Anonymous10-Jun-05 9:48 
GeneralRe: Why would you want to do this? Pin
Jasp Software10-Jun-05 10:08
Jasp Software10-Jun-05 10:08 
AnswerRe: Why would you want to do this? Pin
GreenShoes9-Feb-07 2:03
GreenShoes9-Feb-07 2:03 
GeneralRe: Why would you want to do this? Pin
GreenShoes9-Feb-07 2:08
GreenShoes9-Feb-07 2:08 
GeneralRe: Why would you want to do this? Pin
AaronKh4-Sep-07 21:15
AaronKh4-Sep-07 21:15 
QuestionHave sample code in MFC? Pin
inderst11-Feb-05 5:28
inderst11-Feb-05 5:28 
AnswerRe: Have sample code in MFC? Pin
Tomas Brennan14-Feb-05 5:08
Tomas Brennan14-Feb-05 5:08 
GeneralThat Little Empty Box In The Lower Right Corner Pin
Marc Clifton1-Feb-05 3:44
mvaMarc Clifton1-Feb-05 3:44 
GeneralRe: That Little Empty Box In The Lower Right Corner Pin
Tomas Brennan2-Feb-05 4:43
Tomas Brennan2-Feb-05 4:43 
GeneralNeat : ] Pin
Liran__1-Feb-05 3:41
sussLiran__1-Feb-05 3:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.