WoW:Creating GUI configuration options
Making a GUI Config/Options dialog for your addon is always a bit tricky, however this HOWTO will hopefully help you in such an endeavor. Since a config dialog is usually added to an existing addon, this HOWTO will use an existing addon as an example, but we encourage you to use your own addon while following this HOWTO.
- Reference Material (not needed for HOWTO)
- myClock by Scheid
This HOWTO is not meant to reproduce myClock in any shape or form, and you'll notice that the code has been changed in a number of places. Nevertheless this HOWTO is based on it, and if you filled in the gaps would give you a nice basic myClock like addon.
The TOC fileEdit
- Reference Material (optional reading)
- The TOC Format
In the TOC file we need to add your config dialog's .xml file. Also to have a config dialog that remembers it's settings between game sessions, you will need to use the SavedVariables directive.
myClock.toc: Before changes
## Interface: 1600 ## Title: myClock ## Notes: Clock replacing day-night indicator myClockIndicatorFrame.xml
We need to:
- Add the config frame .xml
- Add a SavedVariables directive
- Add myAddOns optional dependency
This HOWTO uses myAddOns to show the config dialog because it is much simpler then creating a slash command. If you wish to use a slash command, it is recommended you read HOWTO: Create a Slash Command as it is not covered in this HOWTO.
myClock.toc: After changes
## Interface: 1600 ## Title: myClock ## Notes: Clock replacing day-night indicator ## OptionalDeps: myAddOns ## SavedVariables: myClockConfig myClockIndicatorFrame.xml myClockConfigFrame.xml
- OptionalDeps
- Denotes optional dependencies. myAddOns is not required, but recommended.
- SavedVariables
- Allow us to use a variable that is saved between game sessions, on a per-account basis.
- myClockIndicatorFrame.xml
- Main addon frame
- myClockConfigFrame.xml
- Config addon frame
Load config from SavedVariablesEdit
- Reference Material (optional reading) : Saving variables between game sessions
Using the SavedVariable 'myClockConfig' we declared in the .toc file, we can now load and save our addon's configuration between game sessions. Remember that SavedVariable is per-account, not per-realm and not per-character, so we will need to use tables to store individual settings.
Main frame: .xmlEdit
We will be modifying the main addon frame, in this case 'myClockIndicatorFrame.xml' which is used to display the current time.
We need to:
- Add a OnLoad Script action
- Add a OnEvent Script action
The OnLoad is needed to start watching the 'VARIABLES_LOADED' event (when myClockConfig will have its saved values set). Next, we use OnEvent to actually do something when the event fires.
myClockIndicatorFrame.xml
<Ui ...> <Frame name="myClockIndicatorFrame" ...> ... <Scripts> <OnLoad> myClockIndicatorFrame_OnLoad(); </OnLoad> <OnEvent> myClockIndicatorFrame_OnEvent(); </OnEvent> ... </Scripts> </Frame> </Ui>
Main frame: .luaEdit
GlobalsEdit
We need to:
- Get the Realm and Character names for later
- Ensure SavedVariable 'myClockConfig' exists
- Create addon details for myAddOns
- Create default config settings
myClockIndicatorFrame.lua
... -- Globals -- so we know when our configuration is loaded myClock_variablesLoaded = false; -- for configuration saving myClockRealm = GetCVar("realmName"); myClockChar = UnitName("player"); -- details used by myAddOns myClock_details = { name = "myClock", frame = "myClockIndicatorFrame", optionsframe = "myClockConfigFrame" }; -- default config settings local myClockConfig_defaultOn = true; -- addon enabled? local myClockConfig_defaultTime24 = true; -- 24 hour format? local myClockConfig_defaultOffset = 0; -- time offset? ...
- myClock_variablesLoaded, myClock_realm, myClock_char
- These are needed to create a table of settings, per-realm, per-character. By default a SavedVariable is only per-account. This way changing configuration options on one character does not change them for all your characters!
- myClock_details
- myAddOns will display information about your AddOn, and needs the name of your main and config frames to display the configuration dialog properly.
- myClockConfig_default
- Each of these variables is a default setting for a configuration option the user will be able to change in our config dialog. They are also used the first time a character uses our addon, as they have no saved settings yet.
Registering EventsEdit
Since we will use the VARIABLES_LOADED event to know when it is safe to use our SavedVariable,
We need to:
- Register the VARIABLES_LOADED event
- Call a function when the event fires
myClockIndicatorFrame.lua: anywhere inside
... -- OnLoad function myClockIndicatorFrame_OnLoad() -- register events this:RegisterEvent("VARIABLES_LOADED"); -- eventually will call OnEvent ... end ...
myClockIndicatorFrame.lua: anywhere inside
... -- OnEvent function myClockIndicatorFrame_OnEvent() -- VARIABLES_LOADED event if ( event == "VARIABLES_LOADED" ) then -- execute event code in this function myClockIndicatorFrame_VARIABLES_LOADED(); end end ...
Custom Update FunctionsEdit
We now have a function that will be called when our variables are loaded.
It will need to:
- load default SavedVariable values
- register addon with myAddOns
- record that configuration was loaded
within myClockIndicatorFrame.lua
... function myClockIndicatorFrame_VARIABLES_LOADED() -- initialize our SavedVariable if ( not myClockConfig ) then myClockConfig = {}; end if ( not myClockConfig[myClockRealm] ) then myClockConfig[myClockRealm] = {}; end if ( not myClockConfig[myClockRealm][myClockChar] ) then myClockConfig[myClockRealm][myClockChar] = {}; end -- load each option, set default if not there if ( not myClockConfig[myClockRealm][myClockChar].on ) then myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn; end if ( not myClockConfig[myClockRealm][myClockChar].time24 ) then myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24; end if ( not myClockConfig[myClockRealm].offset ) then myClockConfig[myClockRealm].offset = myClockConfig_defaultOffset; end -- record that we have been loaded myClock_variablesLoaded = true; -- we know other addons have been "loaded" now -- optional dependance on myAddOns, leads to our config panel if( myAddOnsFrame_Register ) then myAddOnsFrame_Register( myClock_details ); end -- configuration might have changed myClock_ConfigChange(); end ...
So this function is very simple, it created a new table for storing our configuration on a addon-wide, realm-wide and character basis, then checked for every config option and made sure it was loaded, if not then set it to the default. We added myAddons support, telling myAddons what our mod's name, main frame, and config frame was using a table declared in our globals.
The last line is interesting, we make a call to myClock_ConfigChange(). This is the function that will actually change our mod based on our settings. In the case of a clock most of those options ('time24', 'offset') are actually in OnUpdate, but we would handle the 'on' in this function.
Our config change should:
- Turn the mod on and off
within myClockIndicatorFrame.lua
... function myClock_ConfigChange() -- make sure that our profile has been loaded before allowing this function to be called if ( not myClock_variablesLoaded ) then -- config not loaded myClockIndicatorFrame:Hide(); -- turn our mod off return; end -- make sure to use the frame's name here, cannot rely on 'this' to mean the main frame if ( myClockConfig[myClockRealm][myClockChar].on ) then myClockIndicatorFrame:Show(); -- show our mod frame else myClockIndicatorFrame:Hide(); -- hide our mod frame end end ...
It can also be useful to have a function to set everything back to defaults... here is one you might use in this case
within myClockIndicatorFrame.lua
... -- reset to defaults function grokClock_ConfigToDefault() -- make sure that our profile has been loaded before allowing this function to be called if ( not myClock_variablesLoaded ) then -- config not loaded myClockIndicatorFrame:Hide(); -- turn our mod off return; end -- set our profile to defaults myClockConfig[myClockRealm][myClockChar].on = myClockConfig_defaultOn; myClockConfig[myClockRealm][myClockChar].time24 = myClockConfig_defaultTime24; myClockConfig[myClockRealm][myClockChar].offset = myClockConfig_defaultOffset; -- frame name used since called from various places -- set the location of the frame... in case they dragged it away myClockIndicatorFrame:SetPoint("TOPLEFT", "MinimapCluster", "TOPLEFT", 122, -28); -- wow automatically saves the location of frames on the screen! -- make sure the defaults are loaded onto our mod frame myClock_ConfigChange(); end ...
Making your config frameEdit
Now that your mod is using your SavedVariable, and is both loading it's settings from it (or using the defaults), you need a configuration frame to allow users to modify their settings! There are many popular ways of doing this, you can create slash commands (learn more at HOWTO: Create a Slash Command), a right click menu, but for now we will stay simple, and just have a frame popup when you use myAddons to display the options. From inside the game, you can hit the 'ESCAPE' key and chose the 'Addons' item to have myAddons open. From there you can click on the name for your addon, and then click the 'Options' button on the bottom right. But it won't open your options right now, because you haven't made the frame yet!
config frame .xmlEdit
Basic Frame with BackdropEdit
Let's flesh out a basic config frame:
myClockConfigFrame.xml
<Ui ...> <Script file="myClockConfigFrame.lua"/> <Frame name="myClockConfigFrame" toplevel="true" parent="UIParent" frameStrata="DIALOG" hidden="true" enableMouse="true"> <Size><AbsDimension x="260" y="280"/></Size> <Anchors><Anchor point="CENTER"/></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> </Frame> </Ui>
We now have a config frame, and if you go in-game you can see it in all it's splendor. Feel free to make it any size, and the ANCHOR point="CENTER" will make sure it is dead center on the screen no matter the resolution.
You'll probably want some kind of title on it to say what mod's options it is, so here's some Layers info to do that:
within myClockConfigFrame.xml
... <Frame ...> ... <Backdrop ...> ... </Backdrop> <Layers> <Layer level="ARTWORK"> <Texture file="Interface\DialogFrame\UI-DialogBox-Header"> <Size><AbsDimension x="256" y="64"/></Size> <Anchors> <Anchor point="TOP"> <Offset><AbsDimension x="0" y="12"/></Offset> </Anchor> </Anchors> </Texture> </Layer> <Layer level="OVERLAY"> <FontString inherits="GameFontNormal" text="Clock Config"> <Anchors> <Anchor point="TOP" relativeTo="$parent"></Anchor> </Anchors> </FontString> </Layer> </Layers> </Frame> ...
Adding Close and Default ButtonsEdit
That should make a small title box appear, and you'll notice the FontString's text attribute is set to "Clock Config" -- you can change this to anything you want. If you're doing localization (ui), maybe even a constant! You might notice that once you display your config frame, there's no way to get rid of it! Let's fix that right away by adding a `Close' button:
within myClockConfigFrame.xml
... <Frame ...> ... <Frames> ... <Button name="$parentButtonClose" inherits="OptionsButtonTemplate" text="Close"> <Anchors> <Anchor point="BOTTOMRIGHT"> <Offset><AbsDimension x="-12" y="16"/></Offset> </Anchor> </Anchors> <Scripts> <OnClick> myClockConfigFrame:Hide(); </OnClick> </Scripts> </Button> ... </Frames> ... </Frame ...> ...
This button, which you'll notice is anchored at the bottom right of the config frame, simply tells our configuration frame to hide.
While we're at it, let's add a defaults button in the bottom left of the frame.
within myClockConfigFrame.xml
... <Frame ...> ... <Frames> ... <Button name="$parentButtonToDefault" inherits="OptionsButtonTemplate" text="Defaults"> <Anchors> <Anchor point="BOTTOMLEFT"> <Offset><AbsDimension x="13" y="16"/></Offset> </Anchor> </Anchors> <Scripts> <OnClick> grokClock_ConfigToDefault(); </OnClick> </Scripts> </Button> ... </Frames> ... </Frame ...> ...
You'll notice all we really had to do here was call our ConfigToDefault function we made in 'myClockIndicatorFrame.lua' -- it will take care of updating our addon for us! Now we should be able to undo any damage should we mess around with our mod's config settings.
Checkboxes and SlidersEdit
With most of the graphical frame stuff out of the way, it's time to add our checkboxes, and a slider for the offset (Dropdown menus are a lot more complicated, so this HOWTO won't be covering that).
Your basic checkbox config will look like:
within myClockConfigFrame.xml
... <Frame ...> ... <Frames> ... <CheckButton name="$parentCheckButtonOn" inherits="OptionsCheckButtonTemplate"> <Anchors> <Anchor point="TOPLEFT" relativeTo="$parent"> <Offset><AbsDimension x="20" y="-30"/></Offset> </Anchor> </Anchors> <Scripts> <OnLoad> getglobal(this:GetName().."Text"):SetText("On"); </OnLoad> <OnClick> myClockConfigFrameOption_OnClick(); </OnClick> </Scripts> </CheckButton> ... </Frames> ... </Frame ...> ...
You'll notice I'm using the '$parent' and 'this' variables here to make this code generic, able to be used anywhere. If this was the 2nd checkbox, my Anchor relativeTo would be '$parentCheckButtonOn', and so forth, so that each item in the config dialog was just a bit bellow the others. Of course you could use absolute positioning by just getting rid of the 'relativeTo' altogether.
You should also see that the CheckButton has a OnLoad to set its text, since this will be an automatically created FontString. Again I use 'this:GetName().."Text"' to make this code generic. I've set the text to "On" in this case, but you can change this to whatever you want, even a localized (ui) variable!
The OnClick simply loads a lua function, but it is important to note that we will use this function for ALL our CheckButtons. That's right, we can use the same function for all our CheckButton's!
In our case we'll need to repeat this CheckButton xml code twice, once for 'on' and again for 'time24'. So go ahead and try to create another checkbox. Watch out for the offset! Look at the slider example bellow for how the offset should look after the 1rst item.
Here's what our slider code might look like:
within myClockConfigFrame.xml
... <Frame ...> ... <Frames> ... <Slider name="$parentSliderOffset" inherits="OptionsSliderTemplate"> <Size> <AbsDimension x="220" y="16"/> </Size> <Anchors> <Anchor point="TOPLEFT" relativeTo="$parentCheckButtonTime24"> <Offset><AbsDimension x="0" y="-40"/></Offset> </Anchor> </Anchors> <Scripts> <OnLoad> getglobal(this:GetName().."Text"):SetText("Offset"); getglobal(this:GetName().."High"):SetText("12"); getglobal(this:GetName().."Low"):SetText("-12"); this:SetMinMaxValues(-12,12); this:SetValueStep(1); </OnLoad> <OnValueChanged> grokClockConfigFrameOption_OnClick(); </OnValueChanged> </Scripts> </Slider> ... </Frames> ... </Frame> ...
You'll again notice that we're using the Anchor relativeTo on the last checkbox, which will be $parentCheckButtonTime24. We're again dynamically getting the FontStrings that WoW will create for us, like the 'Text' one set to "Offset". As well, because this is a slider, we have "High" and "Low" FontStrings, have to set the minimum and maximum values the slider has, and how many values are added or subtracted when you move the slider. What will most surprise you is that.. yes... we can still use that option function for our slider too! One function for all the basic config options!
Frame ScriptsEdit
It's also important that all our current settings are loaded into our config frame, and since they might change when we use a slash command or something, the best way would be to use a OnShow event.
within myClockConfigFrame.xml
... <Frame ...> ... <Scripts> <OnShow> myClockConfigFrame_OnShow(); </OnShow> </Scripts> ... </Frame> ...
config frame .luaEdit
Our lua file for the config frame then breaks down into just 2 functions, one for when the frame is shown, the other for when an option in the config frame (like a checkbox, or a slider is moved) changes.
within myClockConfigFrame.lua
-- OnShow function myClockConfigFrame_OnShow() -- make sure our profile has been loaded if ( not myClock_variablesLoaded ) then -- config not loaded this:Hide(); -- hide our config pane return; end -- read settings from profile, and change our checkbuttons and slider to represent them getglobal(this:GetName().."CheckButtonOn"):SetChecked( myClockConfig[myClockRealm][myClockChar].on ); getglobal(this:GetName().."CheckButtonTime24"):SetChecked( myClockConfig[myClockRealm][myClockChar].time24 ); getglobal(this:GetName().."SliderOffset"):SetValue( myClockConfig[myClockRealm].offset ); end
You'll notice again that we have to make sure that the profile is there (or variables loaded). The code also uses the getglobal() function to construct the names of the controls, so it doesn't matter what we called our frame. After that it's a simple matter of using SetChecked() or SetValue() with our profile settings to make our config dialog look right.
The last part is making sure that when they change something in the config dialog, it changes the profile. Here's what that would look like:
within myClockConfigFrame.lua
-- OnClick function grokClockConfigFrameOption_OnClick() -- make sure our profile has been loaded if ( not myClock_variablesLoaded ) then -- config not loaded this:GetParent():Hide(); -- hide our config pane (this is now a checkbox) return; end -- read setting out of checkbox (or slider) and put into profile -- use this:GetName() to know which checkbox was hit. if ( this:GetName() == (this:GetParent():GetName().."CheckButtonOn" ) ) then myClockConfig[myClockRealm][myClockChar].on = this:GetChecked(); -- set profile elseif ( this:GetName() == (this:GetParent():GetName().."CheckButtonTime24" ) ) then myClockConfig[myClockRealm][myClockChar].time24 = this:GetChecked(); elseif ( this:GetName() == (this:GetParent():GetName().."SliderOffset" ) ) then myClockConfig[myClockRealm].offset = this:GetValue(); end -- configuration was changed, make sure our addon changes too! -- notice our addon is changed right away, not when we hit 'done'. myClock_ConfigChange(); end
Again we have a check to make sure our profile is loaded, and then a mock switch/case statement to determine which CheckButton was pressed. Once we know, it's a simple issue of using this:GetChecked() or this:GetValue() to set our profile config! At the end we make sure our main addon knows that something was changed.
Alternatively you could leave out the 'myClock_ConfigChange();' of this function, and add it to the Done button OnClick event to make a Save/Cancel dialog. Generally speaking for a config people want to see the changes happen live so they can tweak them.
You're done! Hopefully this provided some concrete examples on how to make a config dialog, and much thanks to Scheid for being the ginipig! You could easily go from here to make a more complex configuration pane, using tabs, multiple pages, or even using Cosmos' Khaos (another howto someday).