Introduction[]
As your UI is growing more and more complex you will spend quite some time organizing your UI elements in a way that's user-friendly but still leaves enough capabilities to make use of the more advanced features in your AddOns. A great way to achieve this with very little effort is the use of Tabs.
You probably know these from the Character or Friendsframe windows in WoW. With Tabs you have the ability to store multiple windows inside of a single one. Tabs are a very important feature in UI design, since their introduction in early MacOS they became kind of a pseudo-standard in recent graphical applications, such as Mozilla Firefox.
Unfortunately the amount of code necessary to generate even a simple tabbing system in WoW is quite large. However, the guys at Blizzard implemented an easy-to-use library that makes the task of handling tabs a whole lot more comfortable.
The Theory behind it[]
Blizzard's approach on Tabs is pretty straightforward: The buttons for switching between the different Tabs are simply just that, buttons! The OnClick handler of each button hides the current UI-elements and shows the ones that correspond to the selected Tab. Blizzard's library adds some eye candy to it, so that it's easier to identify which tab is active at the moment. Take a peek at some of the tabbed windows that are in the game and you'll see what i mean.
Getting our hands dirty[]
First of all: The library for Tabs is included in the UIPanelTemplates XML and LUA files. If you are unsure about how certain things work, take a look at them. For an excellent example of tabbed interface implementation, check out the source code for the CT_ExpenseHistory mod [1]
Tabs in XML[]
Let's start off with the frame that will serve as the outermost container of our tabbed interface. Inside this frame, you will embed child frames that will each define a single "page" of the interface, and you will write button click handlers that show a single page at a time and hide the rest.
This might look like a lot of code, and it is. Nature of the beast with the WoW XML UI.
<Frame name="myTabContainerFrame" toplevel="true" frameStrata="DIALOG" movable="true" enableMouse="true" hidden="false" parent="UIParent"> <Size> <AbsDimension x="480" y="325"/> </Size> <Anchors> <Anchor point="CENTER"> <Offset><AbsDimension x="-200" y="200"/></Offset> </Anchor> </Anchors> <Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true"> <BackgroundInsets> <AbsInset left="11" right="12" top="12" bottom="11"/> </BackgroundInsets> <TileSize> <AbsValue val="32"/> </TileSize> <EdgeSize> <AbsValue val="32"/> </EdgeSize> </Backdrop> <Layers> <Layer level="ARTWORK"> <Texture name="myFrameHeader" file="Interface\DialogFrame\UI-DialogBox-Header"> <Size> <AbsDimension x="356" y="64"/> </Size> <Anchors> <Anchor point="TOP"> <Offset> <AbsDimension x="0" y="12"/> </Offset> </Anchor> </Anchors> </Texture> <FontString inherits="GameFontNormal" text="My Frame"> <Anchors> <Anchor point="TOP" relativeTo="myFrameHeader"> <Offset> <AbsDimension x="0" y="-14"/> </Offset> </Anchor> </Anchors> </FontString> </Layer> </Layers> <Frames> <Frame name="myTabPage1" hidden="false"> <Anchors> <Anchor point="TOPLEFT"/> <Anchor point="BOTTOMRIGHT"/> </Anchors> <Layers> <Layer level="ARTWORK"> <FontString inherits="GameFontNormal" text="My Frame 1"> <Anchors> <Anchor point="TOPLEFT" relativeTo="$parent"> <Offset> <AbsDimension x="20" y="-30"/> </Offset> </Anchor> </Anchors> </FontString> </Layer> </Layers> <Frames> </Frames> </Frame> <Frame name="myTabPage2" hidden="true"> <Anchors> <Anchor point="TOPLEFT"/> <Anchor point="BOTTOMRIGHT"/> </Anchors> <Layers> <Layer level="ARTWORK"> <FontString inherits="GameFontNormal" text="My Frame 2"> <Anchors> <Anchor point="TOPLEFT" relativeTo="$parent"> <Offset> <AbsDimension x="20" y="-30"/> </Offset> </Anchor> </Anchors> </FontString> </Layer> </Layers> <Frames> </Frames> </Frame> <Button name="$parentTab1" inherits="CharacterFrameTabButtonTemplate" id="1" text="My Tab 1"> <Anchors> <Anchor point="CENTER" relativePoint="BOTTOMLEFT"> <Offset> <AbsDimension x="60" y="-12"/> </Offset> </Anchor> </Anchors> <Scripts> <OnClick> PanelTemplates_SetTab(myTabContainerFrame, 1); myTabPage1:Show(); myTabPage2:Hide(); </OnClick> </Scripts> </Button> <Button name="$parentTab2" inherits="CharacterFrameTabButtonTemplate" id="2" text="My Tab 2"> <Anchors> <Anchor point="LEFT" relativeTo="$parentTab1" relativePoint="RIGHT"> <Offset> <AbsDimension x="-16" y="0"/> </Offset> </Anchor> </Anchors> <Scripts> <OnClick> PanelTemplates_SetTab(myTabContainerFrame, 2); myTabPage1:Hide(); myTabPage2:Show(); </OnClick> </Scripts> </Button> </Frames> <Scripts> <OnLoad> this.elapsed = 0; PanelTemplates_SetNumTabs(myTabContainerFrame, 2); PanelTemplates_SetTab(myTabContainerFrame, 1); </OnLoad> <OnShow> PlaySound("UChatScrollButton"); PanelTemplates_SetTab(myTabContainerFrame, 1); myTabPage1:Show() myTabPage2:Hide() </OnShow> <OnHide> PlaySound("UChatScrollButton"); </OnHide> </Scripts> </Frame>
This is a completely functional, self contained Tab Frame which you should be able to plop into any WoW XML UI file and run (as of Apr. 23, 2008). There are a lot of redundancies here. Things like the tab button properties should be templated and the logic controlling the selection of tabs is best handled by a custom function.
Things like the tab button properties should be templated if you're going to have lots of tabs. As mentioned before, from an UI's point of view, tabs are simply some buttons at the bottom of your frame. You should define a template like this in XML, and then implement a smart click handler in Lua:
<Button name="myFrameTabTemplate" inherits="CharacterFrameTabButtonTemplate" virtual="true"> <Scripts> <OnClick> myButtonHandler(this:GetName()); </OnClick> </Scripts> </Button>
This will reduce the amount of code you need per tab. As an example, your first tab would be only:
<Button name="$parentTab1" inherits="myFrameTabTemplate" id="1" text="My Tab 1"> <Anchors> <Anchor point="CENTER" relativePoint="BOTTOMLEFT"> <Offset> <AbsDimension x="60" y="-8"/> </Offset> </Anchor> </Anchors> </Button>
I'll leave the implementation of myButtonHandler() as an exercise for the reader.
We are inheriting from the template that's used for the CharacterFrame. You can find it at the top of the CharacterFrame.XML file. By using this template we basically provide WoW with the data structure that is specified by the Tab library. If you take a look at the source of the template, you'll notice that this is quite complex.
Let's look at our existing buttons:
<Button name="$parentTab1" inherits="CharacterFrameTabButtonTemplate" id="1" text="My Tab 1"> <Anchors> <Anchor point="CENTER" relativePoint="BOTTOMLEFT"> <Offset> <AbsDimension x="60" y="-8"/> </Offset> </Anchor> </Anchors> <Scripts> <OnClick> PanelTemplates_SetTab(myTabContainerFrame, 1); myTabPage1:Show(); myTabPage2:Hide(); </OnClick> </Scripts> </Button> <Button name="$parentTab2" inherits="CharacterFrameTabButtonTemplate" id="2" text="My Tab 2"> <Anchors> <Anchor point="LEFT" relativeTo="$parentTab1" relativePoint="RIGHT"> <Offset> <AbsDimension x="-16" y="0"/> </Offset> </Anchor> </Anchors> <Scripts> <OnClick> PanelTemplates_SetTab(myTabContainerFrame, 2); myTabPage1:Hide(); myTabPage2:Show(); </OnClick> </Scripts> </Button>
NOTE: The names of these buttons must be exactly "$parentTab1", "$parentTab2", etc... otherwise the calls to PanelTemplates_UpdateTabs() will fail with an error.
Most of the Tab management WoW does, is done by 'guessing' names based on specific naming conventions. Notice the id Attribute in the declaration. This specifies the number of your tab, so for the third tab, the id would have to be 3 and so on. Don't forget to adjust the ids for every new button! You won't need any more eventhandlers in there, it's all in the template.
So far, so good. Now, let's step over to LUA.
Tabs in LUA[]
Look at the OnLoad code which registers our tabbed frame with Blizzard's UI code, sets which tab should be selected, and then shows and hides the appropriate pages:
PanelTemplates_SetNumTabs(myTabContainerFrame, 2); -- 2 because there are 2 frames total. PanelTemplates_SetTab(myTabContainerFrame, 1); -- 1 because we want tab 1 selected. myTabPage1:Show(); -- Show page 1. myTabPage2:Hide(); -- Hide all other pages (in this case only one).
These lines should be executed before your frame is displayed for the first time. myFrame is the name of your frame, n is the number of tabs you specified.
Take your time to understand what the PanelTemplates_SetTab()-function does. This is basically the whole trick behind it all. PanelTemplates_SetTab() goes some graphics magic, making the selected tab appear to be selected, while you're left to actually display the proper page.
Now, let's take a look at the Tab_OnClick() function that we used when creating our button template. What it basically does is setting the selectedTab and then calling UpdateTabs()! You know what that means, don't you? Go ahead and fire up WoW right now (you should outcomment the myButtonHandler() call in the template to avoid errors). You will notice that your tabs are already working just fine! Beside one little thing: They don't do anything yet!
In your Lua button handler (the one I left for an exercise), you probably already know what to do: Extract the number of the selected tab from the button's name which was passed as a parameter (this:GetID()), then show the UI elements of the new tab and hide all of the other UI elements. Shouldn't be any problem now, since all of the hard work is already done by the library. So rather than boring you with the details of the implementation, I'll close with some tips on efficient techniques for writing button handlers:
- Always wrap up your tabs inside of separate Frame-tags. Thus you'll only have to hide that very frame once, instead of iterating through every single UI element and hiding it by hand.
- Use a string table that contains the names of these frames. Thus you can iterate through the table with a single for loop, instead of writing hundreds of lines. Your code becomes a whole lot more clear, making maintenance so much easier
- Notice that you can also change the background textures when switching tabs. This is done a lot in WoW and is extremely useful when you have very different UIs in the various tabs
Conclusion[]
As you have seen in the above sections, using tabs ain't that hard at all. As long as you're paying attention to certain naming conventions, making use of Blizzard's library is both powerful and easy to use. Since the OnClick-buttonhandler is specified completely independent from the rest of the library, you may also do some very weird and unexpected stuff with tabs if you wish to.
I hope you enjoyed this tutorial and did not despair over my dessolute style of writing. I'd love to read some feedback in the discussions section and hope that many of you will benefit from the great capabilities that Tabs provide.