TABSTRIP AND CONTAINER CONTROLS

© 2003, Alyce Watson

Alyce's Restaurant

Home

Collision Simulator

Foon's Tips

Encryption

Lesson Browser

Cookie DLL

LB NoteBoard

LB Clipboard Commands

Clipboard API Demos

Compiled HTML Help

Tabstrip

Tabstrip Demo

Scrolling Controls

Why API?

Sprite in a Box

Newsletter Help

Index


Tab Control --- Container Controls -- Demo Program

Tabstrips

What is a tabstrip? You've probably seen programs that have dialog windows with tabstrip controls. They appear to be a set of 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:

tabstrip

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 all caps.

    '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 our window, and then get the 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 long,_    'parent window handle
    _GWL_HINSTANCE As long,_'flag to retrieve instance handle
    hInstance As long       'instance handle

We can now create our tabstrip control. We aren't using an extended style flag in this example. 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 "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 following arguments 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 long,_         ' parent hWnd
        0 as long,_                  ' menu
        hInstance as long,_          ' hInstance
        "" as ptr,_                  ' window creation data - not used
        hwndTab as long              ' 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 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"+chr$(0)
    'TCITEM.txtMax.struct=len("First Tab")+1 'used when retrieving text, not needed here

    'add first tab:
    calldll #user32, "SendMessageA",_
        hwndTab as long,_
        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"+chr$(0)
    'TCITEM.txtMax.struct=len("Second Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as long,_
        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"+chr$(0)
    'TCITEM.txtMax.struct=len("Third Tab")+1  'used when retrieving text, not needed here
    calldll #user32, "SendMessageA",_
        hwndTab as long,_
        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 long

    'set the font to the control:
    CallDLL #user32, "SendMessageA",_
        hwndTab As long,_       '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 message 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 long,_       '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 tab(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. Read about Container Controls below.


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 rememebered 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 and every 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 tab(3)  'hold tab window handles in array
    tab(0)=hTab1:tab(1)=hTab2:tab(2)=hTab3
 
   'set popups to be children of main program window
    for i = 0 to 2
        call SetParent hwndParent,tab(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 tab(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 Long,_
     hWnd As Long, result As Long
    End Sub

Sub MoveWindow hWnd,x,y,w,h
    CallDLL #user32, "MoveWindow",hWnd As Long,_
         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

Look at the whole demo here.


Home

Collision Simulator

Foon's Tips

Encryption

Lesson Browser

Cookie DLL

LB NoteBoard

LB Clipboard Commands

Clipboard API Demos

Compiled HTML Help

Tabstrip

Tabstrip Demo

Scrolling Controls

Why API?

Sprite in a Box

Newsletter Help

Index