TABSTRIPS AND CONTAINER CONTROLS
by Alyce Watson - http://alycesrestaurant.com/
- Alyce Alyce



Tabstrips

What is a tabstrip? You've probably seen programs that have dialog windows with tabstrip controls. They appear to be a set of index cards, each with a tab at the top. Click a tab and bring the attached card to the front of the pile. Here is one example that shows the second tab card clicked and brought to the front:
tab2.jpg

Let's Make a Tabstrip!

The tabstrip is part of the common control DLL, comctl32.dll. When we want to access the DLL, we must first make a call to initialize it:
    'initialize DLL
    calldll #comctl32, "InitCommonControls", ret as void

Once we've done that, we use CreateWindowExA to create the control. You may be asking why we use a "CreateWindow" function to create a control. Both windows and controls are created with this function. We need to establish a struct and some constants for creating and manipulating the control. Liberty BASIC doesn't have true constants. We can mimic them by using variables that we take care not to change within our code. To differentiate them from variables, we type them in uppercase.

    'constants:
    TCIF.TEXT = 1
    TCIF.IMAGE =2
    TCS.MULTILINE = 512
    TCM.INSERTITEMA = 4871
    TCM.GETCURSEL = 4875
    TCM.SETCURSEL = 4876
 
    struct TCITEM,_
    mask as ulong,_
    dwState as ulong,_
    dwStateMask as ulong,_
    pszText$ as ptr,_
    txtMax as long,_
    iImage as long,_
    lParam as long

We need to get the handle of the program window, and then get its instance handle with GetWindowLongA. The instance handle is needed by the CreateWindowExA function.

    hwndParent = hwnd(#1)    'retrieve window handle
 
    ' Get window instance handle
    CallDLL #user32, "GetWindowLongA",_
    hwndParent As ulong,_    'parent window handle
    _GWL_HINSTANCE As long,_ 'flag to retrieve instance handle
    hInstance As ulong       'instance handle

We can now create our tabstrip control. We aren't using an extended style flag in this example, so the first argument is passed as "0." We use a class name of "SysTabControl32". This tells the function that we want to creat a tab control. The next argument can be null, since the tab control doesn't have a caption.

The next argument is important. It sets the style flag for the control. The style bits are put together with the bitwise "OR" operator. All controls must have the _WS_CHILD style, since controls are children of the parent window. To make the control visible, we must also include the _WS_VISIBLE flag. _WS_CLIPSIBLINGS clips child windows relative to each other; that is, when a particular child window receives a WM_PAINT message, the WS_CLIPSIBLINGS style clips all other overlapping child windows out of the region of the child window to be updated. If WS_CLIPSIBLINGS is not specified and child windows overlap, it is possible, when drawing within the client area of a child window, to draw within the client area of a neighboring child window. We'll also use the style for multiline tab controls.

The next arguments required by the function set the location and size of the control. These are relative to the client area of the parent window. We also need the handle and instance handle of the parent window. The argument for the menu is null, because tabstrips don't have a menu. The function returns the handle to the tab control.

    ' Create control
    style =  _WS_CHILD or _WS_CLIPSIBLINGS or _WS_VISIBLE _
        or TCS.MULTILINE
    calldll #user32, "CreateWindowExA",_
        0 As long,_                  ' extended style
        "SysTabControl32" as ptr,_   ' class name
        "" as ptr,_                  ' title
        style as long,_              ' style
        10 as long,_                 ' left x
        10 as long,_                 ' top y
        370 as long,_                ' width
        250 as long,_                ' height
        hwndParent as ulong,_        ' parent hWnd
        0 as ulong,_                 ' menu
        hInstance as ulong,_         ' hInstance
        "" as ptr,_                  ' window creation data - not used
        hwndTab as ulong             ' tab control handle

We now have a control in place, but it doesn't have any tabs! We'll have to send messages to the tab control to add tabs, using the SendMessageA function. This requires that TCITEM struct that we created earlier. We fill the struct with information about the tab to be added. The mask member requires bits to be set that indicate which members of the struct are to be valid in the API call. These bits are for TCIF.TEXT and TCIF.IMAGE. The iImage member is set to -1, since no images will be displayed on the tabs in this demo. The pszText$ member is filled with the desired tab label. The txtMax member is not strictly needed for this function. It would be used to retrieve the tab label, however, so it is placed here for reference. Once the struct is filled, the tab is added by sending the tab control the message TCM.INSERTITEMA. One argument is the index of the tab being added. Remember that indexes are zero-based, so the first tab has an index of 0, the second tab has an index of 1 and so on.
    'set mask and fill struct members:
    TCITEM.mask.struct = TCIF.TEXT or TCIF.IMAGE
    TCITEM.iImage.struct = -1 'no image
    TCITEM.pszText$.struct = "First Tab"
    'TCITEM.txtMax.struct=len("First Tab")+1 'used when retrieving text, not needed here
 
    'add first tab:
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        0 as long,_     'zero-based, so 0=first tab
        TCITEM as struct,_
        ret as long

We add additional tabs in exactly the same way. We'll have three tabs in our demo. Here is the way we add the remaining two tabs.
    'add second tab:
    TCITEM.pszText$.struct = "Second Tab"
    'TCITEM.txtMax.struct=len("Second Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        1 as long,_         'zero-based, so 1=second tab
        TCITEM as struct,_
        ret as long
 
    'add third tab:
    TCITEM.pszText$.struct = "Third Tab"
    'TCITEM.txtMax.struct=len("Third Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        2 as long,_         'zero-based, so 2=third tab
        TCITEM as struct,_
        ret as long

If you had a look at the control right now, you would notice that the font used for the captions of the tabstrips is rather ugly. That is easily fixed. We can get the default gui font on the user's machine with a simple call to GetStockObject. This retrieves the handle to the font, which we then use in SendMessageA with a message of _WM_SETFONT to change the font on the captions.
    calldll #gdi32, "GetStockObject",_
        _DEFAULT_GUI_FONT as long, hFont as ulong
 
    'set the font to the control:
    CallDLL #user32, "SendMessageA",_
        hwndTab As ulong,_      'tab control handle
        _WM_SETFONT As long,_   'message
        hFont As long,_         'handle of font
        1 As long,_             'repaint flag
        ret As long

We need to have some way to know when the user clicks on the tabs so that we can rearrange our tab pages. Liberty BASIC cannot read messages sent from the tab control to the parent window. We can, instead, use a timer to determine which tab has been clicked. We keep track of the current tab and if the selected tab is different from the current tab, we do our changeover routine. We use SendMessageA with a message of TCM.GETCURSEL and the function returns the ID of the tab that is selected.
    timer 300, [checkForTab]
 
'.............
 
[checkForTab]   'see if selected tab is the same
                'as previously selected tab and
                'change controls if tab has changed
    timer 0     'turn off timer
 
    'get the current tab ID
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_      'tab control handle
        TCM.GETCURSEL as long,_ 'message to get current selection
        0 as long, 0 as long,_  'always 0's
        tabID as long           'returns selected tab ID
 
    if tabID <> oldTab then     'change page displayed
        oldTab = tabID          'for next check of selected tab
        gosub [clear]
        call MoveWindow winTab(tabID), 20,40,350,210
    end if
 
    print #1, "refresh"
    timer 300, [checkForTab]    'reactivate timer
    wait
Now that we know how to create and manage the tab control itself, we'll need to know how to handle the other controls that are to appear on the tab pages. One easy way to do this is to include all needed controls in the window, placing the commands before the "open" statement for the window. Then we'll need to move the correct controls onto the window depending upon which tab is selected, and move all of the others off the window. We can do this with the "locate" command, being sure to "refresh" the window after the controls are moved. This is easy to do, but it requires quite a few lines of code to move each single control every time the user selects a tab. We'll use a different method that simulates "container controls" that are available in some other languages.



Container Controls

A container control holds other controls. Whenever anything happens to the container, the controls contained upon it are affected as well. Move the container and the child controls move with it. Hide the container and the child controls are also hidden. At first I didn't think we had this capability in Liberty BASIC, but then I remembered that we have a window with style "dialog_popup". This style has no titlebar. We can create a dialog_popup window for each tab and use if for that tab's page. Any controls on this window will move with it, so when we move a container window onto the program window, all of its controls move with it. We only need to make one call to move a control for each tab. We don't have to move each individual control used by the program.
Let's set up three dialog_popup windows to act as our three tab pages. We'll put a few controls on each one.

    'first page
    Statictext #tab1.s1, "First Tab Page!", 145, 75, 180, 30
    Button #tab1.b1, "Button 1", [buttonOne], UL, 145, 140, 90, 24
    open "" for window_popup as #tab1
 
    'second page
    Textbox #tab2.t2, 40, 40, 180, 30
    Button #tab2.b2, "Button 2", [buttonTwo], UL, 40, 80, 90, 24
    open "" for window_popup as #tab2
 
    'third page
    graphicbox #tab3.g, 0, 0, 350, 210
    open "" for window_popup as #tab3
We can make a call to SetParent to make our dialog_popup windows children of the main program window. To handle this in a loop, we can get the window handles to these "container" windows and store them in an array.
    hTab1=hwnd(#tab1):hTab2=hwnd(#tab2):hTab3=hwnd(#tab3)
    dim winTab(3)  'hold tab window handles in array
    winTab(0)=hTab1:winTab(1)=hTab2:winTab(2)=hTab3
 
   'set popups to be children of main program window
    for i = 0 to 2
        call SetParent hwndParent,winTab(i)
    next

Whenever we want to change the page that is displayed, we can access a subroutine that moves all of the container windows offscreen in a loop. This gives us a blank tab control.
[clear] 'hide all windows
    for i = 0 to 2
        call MoveWindow winTab(i), 3000,3000,350,210
    next
    return
Once the tab control is clear, we can move the desired container window onto it.
    call MoveWindow hTab1, 20,40,350,210

We've wrapped the SetParent and MoveWindow functions in Liberty BASIC functions like so:
Sub SetParent hWnd,hWndChild
    CallDLL #user32, "SetParent", hWndChild As uLong,_
     hWnd As uLong, result As uLong
     style = _WS_CHILD or _WS_VISIBLE
     CallDLL #user32, "SetWindowLongA",_
       hWndChild As ulong, _GWL_STYLE As long,_
       style As Long, r As long
     End Sub
 
Sub MoveWindow hWnd,x,y,w,h
    CallDLL #user32, "MoveWindow",hWnd As uLong,_
         x As Long, y As Long, w As Long, h As Long,_
         1 As Boolean, r As Boolean
    End Sub
That is just about all we need to know. There is one "gotcha" though. If we include a graphicbox on one of the container windows, we will generate an error when the program ends. To avoid this, we do a GetParent call to get the parent window of the graphicbox. We'll store this handle in a variable for use later. When the program ends, we use SetParent to give the graphicbox its proper parent window again.
    'because of graphicbox, get parent on third tab window for use later
    hTab3Parent=GetParent(hTab3)
 
'........................
 
 
[quit]
    timer 0
    'because of graphicbox, restore parent to third tab window
    call SetParent hTab3Parent, hTab3
    close #1:close #tab1:close #tab2:close #tab3:end
 
'........................
 
Function GetParent(hWnd)
    calldll #user32, "GetParent",hWnd as ulong,_
        GetParent as ulong
    End Function


TabStrip Demo


- Alyce Alyce Jul 21, 2006
'tab control demo
'use dialog_popup windows to hold controls
'set parent of popups to be main program window
'when tab is clicked, use MoveWindow to move popups on and off
'if a graphicbox is used, use GetParent on popup
'if graphicbox is used, restore parent of popup at close
'doesn't work properly with type window_popup
 
nomainwin
    'constants:
    TCIF.TEXT = 1
    TCIF.IMAGE =2
    TCS.MULTILINE = 512
    TCM.INSERTITEMA = 4871
    TCM.GETCURSEL = 4875
    TCM.SETCURSEL = 4876
 
    tabID = 1   'current tab
    oldTab = 0  'previously selected tab
 
    struct TCITEM,_
    mask as ulong,_
    dwState as ulong,_
    dwStateMask as ulong,_
    pszText$ as ptr,_
    txtMax as long,_
    iImage as long,_
    lParam as long
 
    'initialize DLL
    calldll #comctl32, "InitCommonControls", ret as void
 
    WindowWidth=350:WindowHeight=210
    'example controls:
    'first page
    Statictext #tab1.s1, "First Tab Page!", 145, 75, 180, 30
    Button #tab1.b1, "Button 1", [buttonOne], UL, 145, 140, 90, 24
    open "" for window_popup as #tab1
 
    'second page
    Textbox #tab2.t2, 40, 40, 180, 30
    Button #tab2.b2, "Button 2", [buttonTwo], UL, 40, 80, 90, 24
    open "" for window_popup as #tab2
 
    'third page
    graphicbox #tab3.g, 0, 0, 350, 210
    open "" for window_popup as #tab3
 
    'main program window
    WindowWidth = 400:WindowHeight = 300
    open "Tab Demo" for window_nf as #1
 
    print #1, "trapclose [quit]"
    print #1, "font ms_sans_serif 10"
    #tab2.t2 "Second Tab Page!"
 
    print #tab3.g, "down; fill blue; color white"
    print #tab3.g, "backcolor blue"
    print #tab3.g, "place 30 50;\Third page!\Click Me!"
    print #tab3.g, "flush"
    print #tab3.g, "setfocus; when leftButtonDown [mouseClick]"
 
    hwndParent = hwnd(#1)    'retrieve window handle
    hTab1=hwnd(#tab1):hTab2=hwnd(#tab2):hTab3=hwnd(#tab3)
    dim winTab(3)  'hold tab window handles in array
    winTab(0)=hTab1:winTab(1)=hTab2:winTab(2)=hTab3
 
    'because of graphicbox, get parent on third tab window for use later
    hTab3Parent=GetParent(hTab3)
 
    'set popups to be children of main program window
    for i = 0 to 2
        call SetParent hwndParent,winTab(i)
    next
 
    'move child windows
    gosub [clear]
    call MoveWindow hTab1, 20,40,350,210
 
    ' Get window instance handle
    CallDLL #user32, "GetWindowLongA",_
    hwndParent As ulong,_   'parent window handle
    _GWL_HINSTANCE As long,_'flag to retrieve instance handle
    hInstance As ulong      'instance handle
 
    ' Create control
    style =  _WS_CHILD or _WS_CLIPSIBLINGS or _WS_VISIBLE _
        or TCS.MULTILINE
    calldll #user32, "CreateWindowExA",_
        0 As long,_                  ' extended style
        "SysTabControl32" as ptr,_   ' class name
        "" as ptr,_
        style as long,_              ' style
        10 as long,_                 ' left x
        10 as long,_                 ' top y
        370 as long,_                ' width
        250 as long,_                ' height
        hwndParent as ulong,_        ' parent hWnd
        0 as long,_
        hInstance as ulong,_         ' hInstance
        "" as ptr,_
        hwndTab as ulong             ' tab control handle
 
    'set mask and fill struct members:
    TCITEM.mask.struct = TCIF.TEXT or TCIF.IMAGE
    TCITEM.iImage.struct = -1 'no image
    TCITEM.pszText$.struct = "First Tab"
    'TCITEM.txtMax.struct=len("First Tab")+1 'used when retrieving text, not needed here
 
    'add first tab:
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        0 as long,_     'zero-based, so 0=first tab
        TCITEM as struct,_
        ret as long
 
    'add second tab:
    TCITEM.pszText$.struct = "Second Tab"
    'TCITEM.txtMax.struct=len("Second Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        1 as long,_         'zero-based, so 1=second tab
        TCITEM as struct,_
        ret as long
 
    'add third tab:
    TCITEM.pszText$.struct = "Third Tab"
    'TCITEM.txtMax.struct=len("Third Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_
        TCM.INSERTITEMA as long,_
        2 as long,_         'zero-based, so 2=third tab
        TCITEM as struct,_
        ret as long
 
    calldll #gdi32, "GetStockObject",_
        _DEFAULT_GUI_FONT as long, hFont as ulong
 
    'set the font to the control:
    CallDLL #user32, "SendMessageA",_
        hwndTab As ulong,_      'tab control handle
        _WM_SETFONT As long,_   'message
        hFont As ulong,_        'handle of font
        1 As long,_             'repaint flag
        ret As long
 
    timer 300, [checkForTab]
    calldll #user32, "SetFocus",hwndParent as ulong,re as ulong
    wait
 
[quit]
    timer 0
    'because of graphicbox, restore parent to third tab window
    call SetParent hTab3Parent, hTab3
    close #1:close #tab1:close #tab2:close #tab3:end
 
[checkForTab]   'see if selected tab is the same
                'as previously selected tab and
                'change controls if tab has changed
    timer 0     'turn off timer
 
    'get the current tab ID
    calldll #user32, "SendMessageA",_
        hwndTab as ulong,_      'tab control handle
        TCM.GETCURSEL as long,_ 'message to get current selection
        0 as long, 0 as long,_  'always 0's
        tabID as long           'returns selected tab ID
 
    if tabID <> oldTab then     'change page displayed
        oldTab = tabID          'for next check of selected tab
        gosub [clear]
        call MoveWindow winTab(tabID), 20,40,350,210
    end if
 
    print #1, "refresh"
    timer 300, [checkForTab]    'reactivate timer
    wait
 
[buttonOne]
    timer 0
    notice "First page."
    timer 300, [checkForTab]
    wait
 
[buttonTwo]
    timer 0
    #tab2.t2 "!contents? txt$"
    notice "Textbox contents: ";txt$
    timer 300, [checkForTab]
    wait
 
[mouseClick]
    timer 0
    notice "Mouse clicked on third page."
    timer 300, [checkForTab]
    wait
 
[clear] 'hide all windows
    for i = 0 to 2
        call MoveWindow winTab(i), 3000,3000,350,210
    next
    return
 
Function GetParent(hWnd)
    calldll #user32, "GetParent",hWnd as ulong,_
        GetParent as ulong
    End Function
 
Sub SetParent hWnd,hWndChild
    CallDLL #user32, "SetParent", hWndChild As uLong,_
     hWnd As uLong, result As uLong
     style = _WS_CHILD or _WS_VISIBLE
     CallDLL #user32, "SetWindowLongA",_
       hWndChild As ulong, _GWL_STYLE As long,_
       style As Long, r As long
     End Sub
 
Sub MoveWindow hWnd,x,y,w,h
    CallDLL #user32, "MoveWindow",hWnd As uLong,_
         x As Long, y As Long,w As Long, h As Long,_
         1 As Boolean, r As Boolean
    End Sub