This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

MSDN Magazine

ActiveX and Visual Basic: Enhance the Display of Long Text Strings in a Combobox or Listbox

John Calvert
This article assumes you�re familiar with Windows and Visual Basic
Level of Difficulty    1   2   3 
Download the code for this article: Calvert.exe (38KB)
Browse the code for this article at Code Center: ComboBox
SUMMARYThe combobox and listbox controls in Visual Basic have no built-in support for displaying text strings that are too long to fit in the visible area of either control. This article describes several techniques to improve the readability of long strings in each control. The first technique uses tooltips to display a combobox or listbox item. The other techniques, which use various Windows APIs, include adding a horizontal scrollbar for a listbox and determining the necessary width of the longest string given the font attributes of the text. The required steps are described for both comboboxes and listboxes.

S ome intrinsic controls in Visual Basic® and some ActiveX® controls can accommodate long text strings. For example, the intrinsic textbox control can be multiline, with both vertical and horizontal scrollbars. This allows long text to either wrap or scroll so that the user can view it in its entirety. In addition, grid controls have resizable columns, multiline cells, and sometimes even a tooltip that lets a user more easily view the full contents of the active cell.
      The intrinsic combobox and listbox controls in Visual Basic, however, do not gracefully handle data that is too long to fit in the visible area of either control. Only part of the data will be visible. Even if a form is resizable, the layout of the controls and the size of the user's computer monitor may not allow a combobox or listbox to resize sufficiently to accommodate the longest items in their list, as you can see in Figure 1.

Figure 1 No-scroll Controls
Figure 1 No-scroll Controls

      After looking at a technique for using tooltips to display the contents of a combobox or listbox using the Windows® API, I will show you several ways to improve the readability of long textual data in these two controls. I'll explain how to determine the maximum length of data that an application needs to display and how to implement vertical and horizontal scrollbars to effectively display the long text. The Visual Basic project available for download from the link at the top of this article demonstrates these techniques.

Tooltips for a Combobox or Listbox

      One way to view all of the textual data in a combobox or listbox is to use tooltips to display the entire string, however long it may be. In the simple case, when the ComboBox Style property is set to Dropdown Combo or Dropdown List, there is only one list item visible at a time, except when the control is dropped down. To display a tooltip, set the ToolTipText property to the current value of the combobox by placing the following code in the control's Click and Change events:

  Combo1.ToolTipText = Combo1
  

 

You can see how this works in the Visual Basic project provided with this article. Toggle the Enhanced checkbox to execute the code in Sub chkEnhanced_Click.
      When the Style property is set to Simple Combo things get more complicated because the combobox control appears as a combined textbox and listbox. I will return to this topic after I explain how to use tooltips with listbox controls.
      A listbox control may have many long list items visible at the same time. Thus, you can't just display the current value of the listbox as a tooltip and expect to see all of the text inside. Rather, a tooltip needs to be displayed for whichever list item the mouse is currently over. As the user moves the mouse over a series of list items, the full text for each successive item will appear in the tooltip. The window message that makes this possible is LB_ITEMFROMPOINT. This message takes the (x,y) coordinates of a point within the listbox and returns the index of the list item under this point. A call to SendMessage from the listbox MouseMove event returns a value that can be used to assign a list item to the ToolTipText property.
      The only tricky part is scaling the (x,y) coordinates and packaging them properly as required by the LB_ITEMFROMPOINT message. Regardless of the value of the parent form's ScaleMode property, the values of the x and y parameters to the MouseMove event are scaled in twips (a unit of measure used in printing, equal to approximately 1/72 of an inch). However, LB_ITEMFROMPOINT expects the coordinates in pixels. Thus, they must be converted. Furthermore, the (x,y) coordinates must be passed as the low and high order words, respectively, of a variable of type long. This translates to Visual Basic code as follows:

  lParam = (CInt(Y / Screen.TwipsPerPixelY) * 2 ^ 16) _
  
+ CInt(X / Screen.TwipsPerPixelX)
lResult = SendMessage(List1.hwnd, LB_ITEMFROMPOINT, _
0, ByVal lParam)

 

      Similarly, the return value from the SendMessage contains two integers packaged in a long. To interpret the result, use the following code:

  '(X,Y) is outside listbox
  
If (lResult \ 2 ^ 16) <> 0 Then Exit Sub

nIndex = CInt(lResult)
List1.ToolTipText = List1.List(nIndex)

 

      For brevity, basic error checking was omitted after each API call in these short code examples. However, in a production environment you should always check API return codes. You never know in advance when or why an API call will fail. Check the Windows SDK for each message for details on error codes returned by the SendMessage function.
      Earlier I mentioned the case of a combobox with the Style property set to Simple Combo. In order to apply the listbox tooltip technique just described to this style of combobox, the code would need to respond to MouseMove events for the listbox portion of the combobox. Unfortunately, Visual Basic does not expose combobox mouse events. It's possible to hook the mouse events using message hooking, but that topic is beyond the scope of this article.
      Now that you've seen the options for using tooltips, let's look at how scrollbars can be used to display long text.

Horizontal Scrollbars for Listbox Controls

      The intrinsic listbox control, unlike a multiline textbox, does not have a Scrollbars property to control the appearance of vertical and horizontal scrollbars. If there are more list items than can fit vertically within the control, the listbox automatically displays a vertical scrollbar. However, if the list items are longer than what can fit horizontally within the control, the listbox does nothing for you. The textbox control, on the other hand, is much more helpful. If a multiline textbox has no horizontal scrollbar, it will wrap long strings automatically, provided there is white space for the line breaks. There is no such functionality with the listbox.
      Fortunately, a horizontal scrollbar can be added to the listbox using code. This is possible because the appearance of scrollbars is a window property that can be modified after the window has been drawn. At first, it might seem that all you need to do is modify the window style of the control using GetWindowLong and SetWindowLong to add WS_HSCROLL to the GWL_STYLE property, as shown in this code:

  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)

 

      However, this doesn't work. If you add long strings to the listbox, a horizontal scrollbar will appear, but the scrollbar will not have any scrollable room. The thumb box will be as wide as the scrollbar, making scrolling impossible (see Figure 2). Furthermore, if you add the scrollbar after items already appear in the listbox, no horizontal scrollbar will appear at all.

Figure 2 Wide Thumb Box
Figure 2 Wide Thumb Box

      The appearance of a horizontal scrollbar in a listbox is correctly controlled using the message LB_SETHORIZONTALEXTENT. This message sets the width in pixels by which the listbox can be scrolled horizontally. The value to assign is the length of the longest string in the listbox, expressed in pixels. If the scrollable width is greater than the actual width of the control, a horizontal scrollbar appears. If, by resizing the parent form, the listbox becomes wider than the assigned scrollable width, the horizontal scrollbars will disappear until the control is resized again. Sending the message is as simple as:

  lResult = SendMessage(List1.hwnd, LB_SETHORIZONTALEXTENT, lLength, _
  
ByVal 0)

 

      The tricky part is determining the necessary width in pixels to accommodate the longest string in the list. In order to do this, you need to find the longest string and determine its width, given the font attributes of the control.

Determining String Length

      Suppose for a moment that the longest listbox list item in a string variable is sString. The code in Figure 3 will determine this string's width in pixels in the listbox. The key function in Figure 3 is GetTextExtentPoint32. It takes a device context and string as arguments, and determines the width and height of the string. A device context describes the display or print context in which the string will appear. This includes the font attributes, which affect the appearance and hence the width of the string. GetTextExtentPoint32 can account for the point size, proportional spacing, bold, italics, and intercharacter spacing of the font, among other thingsâ€"thanks to the device context argument.
      Before calling GetTextExtentPoint32, a device context object needs to be created with the appropriate font properties belonging to the listbox. The first several lines of code in Figure 3 take care of this. Although you can obtain the device context object directly from the control by calling GetDC(List1.hwnd), the listbox font information must be explicitly assigned to the device context as a separate step. The easiest way to do this is to obtain a handle to the font using the window message WM_GETFONT, then assign the font to the device context with SelectObject(hDC, lFont). If for some reason you want to use a different font from the listbox's current font when calling GetTextExtentPoint32, you can use CreateFont to obtain a handle to a custom-tailored font. This function will give you a wealth of options.
      When the device context and font handles have been obtained, you must add code to explicitly release or delete them, as appropriate. This can be a little confusing, since what you do with the font handle depends on whether you obtained it with the WM_GETFONT message or by using the CreateFont function. If you used WM_GETFONT, you don't need to do anything further. If you obtained the font handle using CreateFont, you must release it using DeleteFont. This is important because the automatic garbage collection of pointers (or handles) in Visual Basic is generally limited to variables of type Object, which these handles are not. You have to be sure to properly release handles that you allocate.
      Once the length of the longest string in the listbox device context has been calculated, I simply send the LB_SETHORIZONTALEXTENT message to the listbox in order to enable the horizontal scrollbar.
      There is more than one way to obtain the longest string in the listbox in order to perform the calculations in Figure 3. The simplest approach is to loop over all the list items to search for the item with the greatest Len value:

  For i = 0 To List1.ListCount - 1
  
If (Len(List1.List(i)) > lItemMaxLen) Then
sItemText = List1.List(i)
lItemMaxLen = Len(List1.List(i))
End If
Next i

 

      This code has two drawbacks. First, it determines the list item with the greatest number of characters, not the list item with the greatest length in the current font. These two are by no means equal. Consider the difference between the strings "iiiiii" and "wwwww". In a proportionally spaced font, the string of i characters is much shorter than the same number of w's. However, the difference in the width of individual letters usually averages out, so you don't have to worry about this.
      The second drawback in the code snippet is that it refers to List1.List(i) multiple times. Any reference to or use of the long string data type can decrease performance, especially when repeated in a loop. I ran some experiments comparing the expression Len(List1.List(i)) with the message LB_GETTEXTLEN, which returns the length of the i'th list item. Using LB_GETTEXTLEN was on average about twice as fast as calling the Len function. Here's a modified version of the previous loop using the LB_GETTEXTLEN message:

  For i = 0 To List1.ListCount - 1
  
lResult = SendMessage(List1.hwnd, LB_GETTEXTLEN, _
i, ByVal 0)

If (lResult > lItemMaxLen) Then
sItemText = List1.List(i)
lItemMaxLen = lResult
End If
Next i

 

      Once you pull all of this code together, the listbox will appear with a horizontal scrollbar, as shown in Figure 4.

Figure 4 Scrollable Listbox
Figure 4 Scrollable Listbox

Displaying Long Strings in Comboboxes

      Now let's consider how to deal with the display of long strings in the dropdown portion of a combobox. When a combobox contains long strings, these strings may not be entirely visible in the dropdown (see Figure 5).

Figure 5 Long Strings
Figure 5 Long Strings

      Since Visual Basic does not size the dropdown window to fit the longest list item, you have to write code to do this yourself. If you know the size you want, changing the width of the combobox's dropdown window is easy. It is simply a matter of sending the message CB_SETDROPPEDWIDTH to the control, with the width you need expressed in pixels:

  lResult = SendMessage(cboCombo.hwnd, CB_SETDROPPEDWIDTH, _
  
lDropdownWidth, ByVal 0)

 

      To determine the correct width of the dropdown window, first calculate the length of the longest string in the combobox. This is done exactly the same way as for the listbox discussed earlier (see Figure 3). However, the length of the string is not enough to determine the required width for the dropdown window. You must also account for the border of the dropdown window, which is one pixel on either side, and the width of the vertical scrollbar. The following code takes care of these two issues:

  'Account for the dropdown window's 
  
' border (left and right)
lDropdownWidth = lStringLenInControl + 2

'Account for the scrollbar width
lVerticalScrollbarWidth =
GetSystemMetrics(SM_CXVSCROLL)
If (lVerticalScrollbarWidth <> 0) Then
lDropdownWidth = lDropdownWidth + _
lVerticalScrollbarWidth
End If

 

      The border width of a standard window is not a constant. It can be configured in the Control Panel Display applet, and consists of the border itself and the sizing frame. However, neither of these applies to the dropdown window, so you can safely use the value of one pixel for the width of each of the left and right borders.
      To make these calculations work, you need to know the longest string in the combobox. Fortunately, you can use the same approach as in the previous section to find the longest string in the listbox. Be sure to adjust the code to use the combobox version of the message sent to retrieve the length of list items. This is CB_GETLBTEXTLEN. The resulting code looks like this:

   For i = 0 To Combo1.ListCount - 1
  
lResult = SendMessage(Combo1.hwnd, CB_GETLBTEXTLEN, _
i, ByVal 0)

If (lResult > lItemMaxLen) Then
sItemText = Combo1.List(i)
lItemMaxLen = lResult
End If
Next i

 

      Now you can call SendMessage with CB_SETDROPPEDWIDTH and with the value calculated for lDropdownWidth. As the font name, point size, bold, italics, and other properties are varied, the calculation for lDropdownWidth will account for all of these factors. The results are shown in Figure 6.

Figure 6 Combobox Displaying Long Text
Figure 6 Combobox Displaying Long Text

      I could add one additional check to ensure that the dropdown window does not extend beyond the edges of the screen. Otherwise, the vertical scrollbar on the dropdown may not be visible. To do this, I would need to account for the fact that the OS automatically locates the dropdown window flush to the left edge of the combobox, which may leave less than the full screen width for the dropdown. I could also combine setting the width of the dropdown with adding a horizontal scrollbar to the dropdown using the message CB_SETHORIZONTALEXTENT. I'll leave this as an exercise for the reader.

Conclusion

      The intrinsic combobox and listbox in Visual Basic both have shortcomings when you need to display long textual data. Fortunately, with the straightforward API calls I've mentioned in this article, you can enhance the functionality of these controls to display a tooltip, a horizontal scrollbar, and an auto-sizing dropdown list.
      Nevertheless, don't go overboard. If you find that you have many cases of long textual data in a combobox or listbox, it would be wise to review your overall application design. The screens may need to be redesigned to present the data in a format that's easier to view, without lots of scrolling or large dropdowns. Used judiciously, these enhancements will contribute a polished look to your application.

For related articles see:
Calculating the Logical Height and Point Size of a Font
Add a Horizontal Scroll Bar to a Visual Basic ListBox
John Calvert lives in Ottawa, Ontario, and is an independent IT consultant specializing in client/server and three-tier applications. He works extensively with Microsoft products and is an MCSD. You can reach him at john-m-calvert@home.com.

From the December 2000 issue of MSDN Magazine