A compound class, you will recall, is a reusable abstract class composed of multiple native FoxPro classes.
Collections make abstraction easy because they allow us to address and manipulate groups of objects without actually knowing much about the objects in the group. We don't need to know the name of an object or its place in the object hierarchy. We can read and set object properties through the object reference housed in the collection, and execute object methods the same way.
One of the most universally useful of these compound abstract classes is a tab bar.
Unfortunately, Visual FoxPro does not have a native tab bar class. The VFP designers evidently figured that since we have a page frame class, we don't need a tab bar because a page frame works like a tab bar.
Well, kinda, sortta.
Page frames have a few notable shortcomings.
First, the pages on a page frame are like columns in a grid -- objects we can't modify in the IDE. So we get the look, feel and function VFP provides, and no other. That look, feel and function has certainly improved in Version 8.0, but it still needs a lot of tinkering.
Second, tabs and pages are not separate objects. You cannot display a collection of tabs on a page frame and have the page load only when the tab is clicked. The tab and page are the same object -- a page with a "thingy" sticking up top (or to the side or bottom in Version 8.0). To display a tab, you need to instantiate a whole page.
Unfortunately, this means page frames are often slow to load. The more objects on the pages, the slower the load. In the age 3 GHz computers, this is increasingly less a problem than it was in the era of 300 MHz boxes (about 3 years ago). But it is still not good design or best practice.
Remember the software designer's guiding principal of parsimony? It says (loosely translated) "thou shall not instantiate stuff you don't need right now", which suggests that loading four or five pages of objects so the user can take a quick look at the first page is a waste of time and a lot of Windows resources.
The usual work around is to treat the page as if it were just a tab. Put nothing on the page initially, then load a container of controls onto the page only when the tab is clicked. In doing this, we divorce the tabs from the pages. And that's the trick. If the tabs are separate objects, then we do not have to display the pages until we want to, and we don't want to until the user selects the tab. Now we have better control over the process. But why then load the containers onto a page -- which is already a variety of container? Why not just add them to the form itself and bypass the cumbersome pageframe altogether?
So, what we need is something that holds a collection of tabs. That's what a tab bar is -- a framework for a collection of tabs.
This article looks at some issues involved in designing and implementing a tab bar class using purely FoxPro classes. There are not many issues. A tab bar is a relatively simple compound object.
We need a collection class, two container classes, a shape and a label, and we're in business. If you have never written a custom abstract class before, this is a excellent one to begin with. It is fairly straightforward, but complex enough to be challenging, and the result is a fully customizable abstract class that you can drop on any container or form and immediately use. The Source Code contains a fully functioning, if rather simple, horizontal tab bar class. You can used this as the basis for your own abstract class -- or just use it as it comes out of the box if you don't want to write your own class.
There are lots of possibilities for expansion of the class. An obvious one is to adopt the class so it will display either as a horizontal or vertical tab bar. This means, unfortunately, replacing the VFP label on the Tab container with an ActiveX label that supports vertical text.
I have developed a vertical tab bar using an .ocx label, but it's uses are so limited in my applications that I have not refined it for a few years. Still it is certain possible and where you have use for it, I urge you to do it.
Another expansion is to add the ability to wrap tabs on the bar. An example of wrapped tabs are shown in the illustration below. This increases the number of tabs that may be displayed without running off the edge of the form.
Figure 1: Wrapped Horizontal Tab Bar
The illustration also shows some other compound abstract classes.
What is a Tab Bar?
A tab bar is very simply a framework that displays tabs. It must obey a very few rules (these are in no particular order):
Designing a Tab Bar
I based the tab bar frame on a container class. The tabs on the bar are again just containers. A shape object is used on the tab container to make it look like the classic "tab", and a label is used to display text. A collection is added to allow us to manipulate the tabs as a group. That's all there is to it.
This is intended to be an abstract class that may be put to multiple disparate uses in our applications. If you are developing applications for multiple clients, it is important to be able to adapt the look and feel of the program to the client's preferences without having to write a lot of additional code.
Accordingly, we want to build into the abstract class as much flexibility as is reasonable.
Abstract Behavior
Tab behavior at this abstract level is very limited. All the native tab does is react to being selected -- repainting the tabs on the tab bar accordingly and passing information to a "hook" method (m_Tab_Action). If the TabMouseEffect property (see below) is set to a value greater than zero, a tab will also display certain visual effects when it is under the mouse cursor.
When a tab is selected, three things need to happen.
In our tab bar, all tabs are identified by a integer assigned to the tab's TabIndex property. As a tab is added to the bar, it receives the next available index. The first tab added is index 1, the last tab's index equals the TabCount.
When as tab is selected by mouse clicking the tab, the tab's Click() event assigns its TabIndex to the tab bar's SelectedTab property. This triggers the SelectedTab_Assign() event. From this event the colors and other visual effects of all of the tabs are reset through the Tabs collection.
Any tab that is not the selected tab is repainted as a unselected tab. This means that the formerly selected tab is repainted, but it also means that all of the formerly unselected tabs are also repainted -- unnecessarily since they are already the correct color and size. Very true -- but experimentation proved that there is little benefit and a lot more complexity involved in trying to keep track of which tab was formerly selected and repaint just the formerly selected tab. This "brute force" approach works, is very fast, and is very simple.
This is the code in SelectedTab_Assign() that sets the SelectedTab and calls the m_Set_Properties() method of each Tab object through the Tabs collection of the TabBar class. This method of the tab class takes care of sizing and painting each tab. Note that because each tab is addressed through the collection, we don't have to know anything about the tab. We don't even know its name -- and don't care. All we need to know is that a reference to each tab resides in the collection and can be addressed through the collection object.
* SelectedTab_Assign() * * OVERVIEW: Handles display of the selected tab. The selected tab is normally a different * color and size and may have a distinct border -- all of which indicates on * visual inspection that it is the currently selected tab. LPARAMETERS tnSelectedTab WITH this * SelectedTab has to be set here so tab colors will be painted correctly * by the m_Set_Colors() method of each tab. .SelectedTab = tnSelectedTab * Scan the Tabs collection and execute the m_Set_Properties() method of each * tab to reset tab heights, if necessary, and repaint the tabs. WITH .Tabs FOR lnI = 1 TO .Count * Set the tab's forecolor, backcolor, borderstyle, and top * properties to reflect whether the tab is or is not selected. .Item[ lnI ].m_Set_Properties() NEXT ENDWITH ENDWITH RETURN
This is the code in the m_Set_Properties() method of the Tab class. It calls two Tab class methods to set the appearance of the Tab. The m_Set_Colors() method sets the appropriate text and background colors for the tab based on whether it is or is not the selected tab. The m_Set_Tab_Top() method set the Top property of the tab. Selected tabs are drawn a few pixels taller than unselected tabs to make them more distinguishable.
The code ensures that the selected tab is always moved to the front of the Z-Order. Otherwise the selected tab might be overlapped by a unselected tab. This is not the look we want.
* Tab.m_Set_Properties() * WITH this * Assigns backcolor, bordercolor and forecolor of the tab * depending on whether it is the selected tab. .m_Set_Colors() * Set the top of the TabShape. .m_Set_Tab_Top() * If this tab is the selected tab, ensure it is in front of * the z-order so it is not overlapped by a unselected tab. IF .parent.SelectedTab = .TabIndex .Zorder() ENDIF ENDWITH RETURN
The code in m_Set_Colors() ensures that the various components of the compound tab object (container, shape and label) are painted correctly. Colors are set selectively to minimize processing time. The only color settings that affect the appearance of the tab are the shape BackColor and BorderColor properties and the text color of the label. No other color need be set.
The colors to be used are obtained from various tab color properties of the tab bar (see the property list below).
* Tab.m_Set_Colors * * OVERVIEW: This method is used just to assign colors to various tab * components base on whether this tab is the selected or * not selected. * LOCAL llEnabled, lnTabIndex, lnBackColor, lnBorderColor, lnForeColor WITH this * The visible part of the tab are the tabShape and tabLabel objects. * The tab container itself is invisible, so any color setting in the * container has no effect on its appearance. * * BackColor The only BackColor visible is the that of the TabShape, so any BackColor * assignment is made only to the shape. * BorderColor The only Border visible is the that of the TabShape, so any BorderColor or * BorderWidth assignment is made only to the shape. * ForeColor The only ForeColor visible is the text of the TabLabel, so the ForeColor * assignment is made only to the label. * llEnabled = .Enabled lnTabIndex = .TabIndex
WITH .parent DO CASE CASE !( llEnabled ) lnBackcolor = .DisabledTabBackColor lnBordercolor = .DisabledTabBorderColor lnForecolor = .DisabledTabForeColor CASE .SelectedTab = lnTabIndex lnBackcolor = .SelectedTabBackColor lnBordercolor = .SelectedTabBorderColor lnForecolor = .SelectedTabForeColor OTHERWISE && Normal, unselected tab lnBackcolor = .TabBackColor lnBordercolor = .TabBorderColor lnForecolor = .TabForeColor ENDCASE ENDWITH && .parent * Now that we have the colors, actually set the color properties * of the objects on the Tab container. WITH .TabShape .BackColor = lnBackColor .BorderColor = lnBorderColor ENDWITH .TabLabel.Forecolor = lnForeColor ENDWITH RETURN
The selected tab is painted a few pixels taller than unselected tabs. This is done by exposing a few more pixels of the shape object on the selected tab by setting the selected tab Top property. This is taken care of by the Tab.m_Set_Tab_Top() method.
* Tab.m_Tab_Top() * * OVERVIEW: Sets the tab's Top property depending in whether the tab is or is not * selected. Selected tabs are shown slightly taller than unselected tabs. WITH this * Tab orientation is 0 (Up) or 2 (Down) IF .parent.SelectedTab = .TabIndex * Selected tab is 2 pixels taller than unselected tabs. .TabShape.Top = IIF( .TabOrientation = 0, 1, 0 - .Height ) ELSE .TabShape.Top = IIF( .TabOrientation = 0, 3, -2 - .Height ) ENDIF ENDWITH RETURN
Appearance
The appearance of the tabs on the tab bar is intended to be determined solely by setting tab bar properties. These affect such factors as the colors to be applied to selected and unselected tabs, the shape of the tab, the tab orientation (up or down), the amount of tab overlap and so on.
Quite dramatic appearance changes are possible just be setting a few properties. See the examples below. All of these appearances result just from modifying tab bar property settings. No customization of code is involved.
Figure 2: Some Appearance Changes Possible Through Property Settings
Here is a table of properties of the tab bar that affect appearance with illustrations of some of the effects of changing appearance properties.
CaptionList and SelectedTab
The two most important tab bar properties are CaptionList and SelectedTab.
Caption list contains a comma-delimited list of captions to be displayed on tabs. For example: "Army,Navy,Air Force,Marines"
The tab bar determines the number of tabs to display by counting the number of individual captions included in the comma-delimited CaptionList. The caption list shown above tells the tab bar to display four tabs and set the TabCount to 4. If you do not enter a CaptionList, a warning message will be displayed by the tab bar and it will not instantiate. The tab bar must contain at least one caption.
Captions may be entered as blanks and filled in later in code. To Display four tabs with blank captions, merely enter: ",,," in the caption list. This will result in a tab caption of "*" on each of the four tabs.
If you set a large number of captions, or your captions are long, you may run out of room in the property sheet. The proprty sheet allows no more than 254 characters for the CaptionList property. In this case the CaptionList may be set in code in the Init() of your derived tab class -- but it must be set before DODEFAULT(), for example,
* MyTabBar.Init() * This.CaptionList = "Army,Navy,Air Force,Marines" RETURN DODEFAULT()
Fortunately, there is virtually no limit to the length of text when a property is set in code rather than on the property sheet, so very long captions are possible. However, there is the practical matter of having them all fit on your form.
The same technique may be used to set long text in the ToolTipList property. This property specifies the tool tip message to be displayed for each tab in the CaptionList and is itself a comma-delimited list. The message for a specific tab may be omitted. For example,
* MyTabBar.Init() * This.ToolTipList = "U.S. Army,U.S. Navy,,U.S. Marines" RETURN DODEFAULT()
Army, Navy and Marines would display a message the mouse moves over those tabs, but Air Force would not. Presumably everyone know what the Air Force is.
The SelectedTab property is the other important setting if you want a tab to be initially selected when the tab bar is instantiated. As we discussed above, the SelectedTab identifies the tab on the tab bar that is currently selected. When the tab bar is first instantiated, however, it has the additional function of identifying the initial tab to be selected.
If a value between 1 and TabCount is entered, the tab corresponding to the index will be displayed as selected. Tabs are indexed from left to right, so an initial SelectedTab of 3 means that the third tab will be selected when the tab bar first appears.
If you don't want any tab initially selected, leave SelectedTab at its default zero setting.
TabClass and TabLibrary
The tab class used to instantiate tabs on the tab bar is specified in the TabClass property. The default tab class is "Tab". The TabLibrary specifies the class library in which TabClass is located. Its default is "TabBar".
You can design your own tab class and place the class in any library in your file path. Merely change the defaults in TabClass and TabLibray to point to the new tab class and new library.
Gimme Some Action: Using The Tab Bar Class in Your Application
Now we have a splendid abstract tab bar that doesn't actually do anything. Drop it on a form and give it some captions in its CaptionList property and it will happily instantiate. Move the mouse over a tab and it displays interesting visual effects, click on a tab and it changes its appearance to look selected. But otherwise nothing happens.
When a tab is selected all that occurs in the abstract class is the selected tab calls the m_Tab_Action() method of the tab bar class and passes its TabIndex.
If you look at the code in m_Tab_Action() you will see that there is no default code. It is an empty method. Like the Click() event of a command button, the method is waiting for you to do something with it, but it has no default response of any kind. This is the "hook" method. You can put any code in the m_Tab_Action() method of your derived class to implement the behavior you want to occur when a tab is selected.
Dig around in the source code and open the top tab bar on the TabBarDemo form. Edit the m_Tab_Action() method. This is the code that makes the this particular instance of the tab bar do work.
Very simply, the code displays one of six containers on a form. When tab is selected, the code
The form uses a collection object to hold references to the containers on the form. The collection object is added to the form in the Load() event with the following statement:
this.NewObject( "CntCollection", "Collection", "TabBar" )
.CntCollection.AddItem( loCnt )
The AddItem() method of the collection class adds the object reference passed in parameters to the collection.
This is certainly not the only solution to addressing object on the form. We could, for example, use the form's Controls collection. This requires a lot more work, however, because some of the objects in the Controls collection are not the containers we want to address. So we would have to write additional code to identify the right containers, then set properties only on those containers.
Using the custom container collection is a much simpler and more elegant solution. Since the only objects in the collection are the container objects we want to address, we do not have to be concerned with determining the provenance of the objects before we alter their property settings.
* TopTabBar.m_Tab_Action() * LPARAMETER tnSelectedTab LOCAL loCnt, lcCntName, lnI, lcImgName WITH thisform * Let's keep visual changes from appearing until we are done. .LockScreen = .T. * See if there are any containers on the form yet. The container collection, * CntCollection, added in the Load() of the form, contains an object reference * to each container placed on the form. IF .CntCollection.Count > 0
* Set all existing containers invisible. SetAll() is a method of the * collection class that sets a property of all objects in the collection * to a specified value. If you are using a VFP native collection in * Version 8.0 or later, that has no SetAll() method, you must either add * the method or change the code here. .CntCollection.SetAll( "Visible", .F. ) ENDIF * The BottomTabBar is made visible only for the "Marines" tab. .BottomTabBar.Visible = ( tnSelectedTab = 4 ) * Get the name of the container that is to be displayed when this * tab is clicked. lcCntName = "tabcnt" + TRANSFORM( tnSelectedTab ) * If the container has not already been instantiated, add it to the * form now. IF !PEMSTATUS( thisform, lcCntName, 5 ) and .NewObject( lcCntName, "tabcnt", "tabbardemo.vcx" ) * Get a reference to the container loCnt = EVALUATE( "." + lcCntName ) IF VARTYPE( loCnt ) = "O" * Determine the image to be displayed lcImgName = "img" + TRANSFORM( tnSelectedTab ) + ".jpg" * Assign the correct image to the image control * on the container. All other properties are already set * in the propertry sheet for the TabCnt class. loCnt.Image.Picture = lcImgName * Add the new container to the container collection. .CntCollection.AddItem( loCnt ) ENDIF ENDIF * If the container has just been added, we already have a reference to * the container in loCnt, otherwise get the object reference now. IF VARTYPE( loCnt ) # "O" loCnt = EVALUATE( "." + lcCntName ) ENDIF * Make the container visible -- it is now the only visible container * on the form. IF VARTYPE( loCnt ) = "O" loCnt.Visible = .T. ENDIF * Resize the form to position the container on the form. .Resize() * Unlock the screen so the changes will be painted by Windows. .LockScreen = .F. ENDWITH RETURN
If you download the source code, and add the TabBar library to your working libraries, you will have an instantly functioning tab bar. Just drop it on a form or other container, set the CaptionList property, add code to m_Tab_Action() to handle the tab selections, and you are in the tab bar business.
To run the demo form (and view the effects of the m_Tab_Action() code shown above), type DO TABBARDEMO at the command line.
Figure 3: Tab Bar Demo
Conclusion
That's just about it. Download the tab bar class and demo. Try it out, use it, modify it. Whatever you want. I only ask that you not sell it and if you include the code in a commercial application, proper attribution be given.
As always I give no warranties, express or implied, of any kind. It may work, it may not. It works for me. I hope it works for you.
Source Code