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:
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.
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.