Tabbed List Boxes

by Stephan Grieger - GUI Computing
Image of Line Break

Anyone who has had a MSDN disk and done a search on the disk for a particular string has seen the Search Result window. For those of you who have not, here is a sample :

Image of screen snap

As you can see we have three columns tabbed out nice and neat. The interesting thing is that when you select an item, the entire row is highlighted as though the area containing the items is one list box. I have been asked a few times how to emulate this effect in Visual Basic. As far as I can see there are about four methods you can employ.

The first method, and the messiest, is to use three list boxes and when the ListIndex of one changes, it sets the other two accordingly. The problems with this is that when you select an item and you then set the ListIndex properties of the others, you can actually see the other two change position independently of the first. You can, I guess, use the LockWindowUpdate API to minimise this but there is still another problem. When you scroll down one of the list boxes, you need to trap the scroll event and move the others as well. Once that's accomplished, you need to use the SetWindowWord API call to get rid of the scroll bars on the two list boxes that don't need them. I did say this was messy.

The second method is to simply add tabs to the text so that it all lines up. That's OK if you know what the maximum length of any string is going to be so that it doesn't infringe onto the next tab position. Using TextWidth will, to some degree, enable you to adjust the number of tabs you add to keep the alignment. Even though, in theory, this technique sounds good, those that have tried it will say that there are still a lot of circumstances that you will need to accommodate for to make it work, for example, proportional fonts. Using a fixed width font will go a long way to rectify this, but if all your controls are in Arial, the Courier font looks awfully out of place.

You also want the columns to be sizeable? We now need to determine the width of the string, trim it off when it gets too long, and add tabs to suit. We also need to line-up the text so that it aligns with the left edge of the title.

Obviously these two methods are not ideal solutions. Using a grid is probably the best answer to the whole issue, given that it automatically aligns text in a list format. TrueGrid would be my choice as it will enable you to resize and even reposition the columns.

"But I don't want to buy another control". OK, letís assume that the organisation you work for, even if it's your own, can't justify the measly $99.00 for TrueGrid (standard), what now? You will need to manually reposition the default tabs for the list box using the API. The steps for doing this are quite simple. First you need to set up an array which will store the new tab positions. Then you need to send the Listbox a message telling it that the tabs are to change and that these are the ones you should be using.

Only one API call is required.

  Declare Function SendMessage Lib "User" (ByVal hWnd as Integer, 
                                           ByVal wMsg As Integer, 
                                           ByVal wParam As Integer, 
                                           lParam As Any) As Long

Two globals are required.

  Global Const WM_User = &H400
  Global Const EM_SetTabStops = &H400 + 19

Create the array to house the new tab positions.

  ReDim TabStops (0 To 2) As Integer
  TabStops(0) = 20
  TabStops(1) = 50
  TabStops(2) = 150

Call the API function.

  er% = SendMessage(ListBox.hWnd, EM_SetTabStops, 3, TabStops(0))

Add items to the list box.

  ListBox.AddItem "Stephan" & Chr$(9) & "The" & Chr$(9) & "Grieger"

Be sure to use the & rather that the + sign when adding tabs. Using the + sign will add the character's screen representation rather than an actual tab. This brings me to another little tip. When adding numbers to strings, use the & to get rid of the leading space that Str$() adds.

For example:

  use a$ = "Fred " & Age%

rather than

  a$ = "Fred " +  Trim$(Str$(Age%))

As we've come this far, we might as well build the rest of the search result screen. My window, while not quite as attractive, employs nothing but standard VB controls. The column headings are simply buttons inside a picture control. The black space in between the buttons are image controls that are really thin.

Screen Snap of Form1

The effect we want to emulate is the resizing of the columns. On the mouse move event of the image controls I set the mouse pointer to SizeWE and the buttons back to normal.

I won't bother going through the mathematics of resizing the buttons, you can do that, but I will show you how I managed to get a vertical bar appear down the page as I resized the controls.

The code below is a routine I wrote which simply takes the control and draws a line down it. We will need to get the handle to the Device Context of the ListBox as it is not normally a graphical control and does not support graphics in its native form. Once we have it, we draw the line using the DrawFocusRect API and then we release the hDC again.

  Sub DrawLine (ctlDrawTo As Control)
  ' Dim a few variables.
    Dim LinePos As Rect
    Dim liDeviceContext As Integer
    Dim liEr As Integer

    ' Fill the LinePos structure with the correct values.
    LinePos.Left = fiLineLeft%
    LinePos.Right = fiLineLeft% + 2
    LinePos.Top = 0
    LinePos.Bottom = ctlDrawTo.Height

    ' Get the Device Context of the draw to control.
    liDeviceContext% = GetDC(ctlDrawTo.hWnd)

    ' Draw the line.
    Call DrawFocusRect(liDeviceContext%, LinePos)

    ' Release the device context.
    liEr% = ReleaseDC(ctlDrawTo.hWnd, liDeviceContext)
  End Sub

The DrawFocusRect API has been explained in a previous edition of the forum, however, for those that missed it, here it is again.

The API call takes two parameters. The first is the hDC. The second is the bounds of the line to draw. Actually I should explain this. The API call really draws a rectangle. However, you can use it to draw a single line if you wish.

The Rect structure is found in the API help file that is shipped with Visual Basic. It basically describes the rectangle to draw. In my case I set the Top to 0, the Left to be where the mouse is, the Right to be Left + 2 and the bottom is the height of the list box.

All you need to do now is set the fiLineLeft variable and call this function. Oh, and don't forget to set the Scale mode of the form to either Points or Pixels.

Each time you move the mouse, this function will need to be called twice. Once to remove the line that we drew, and again to draw the line in the new position. When you call the function to erase the line, call the function with the same parameters that you used to create the line in the first place. The variable fiLineLeft if a form level integer which stores the previous position of the line. ie :


' Draw the line.
    fiLineLeft = ImageControl.Left
    Call DrawLine(ListBox)


' If the left button is depressed then...
  If Button = 1 Then
    ' Erase the line.
    Call DrawLine(ListBox)
    ' Draw the new line.
    fiLineLeft = MousePosition
    Call DrawLine(ListBox)
  End If


  If Button = 1 Then
    ' Erase the line.
    Call DrawLine(ListBox)
  End If

Itís a good idea, when you eventually get around to resizing the labels, that you use the LockWindowUpdate API call to make the whole effect smooth.

Written by: Stephan Grieger
August 1995

Image of arrow to previous article Image of arrow to next article