Introduction[ | ]
This is a tutorial on creating and using frames without the use of XML. In other words, all frames and their sub-elements are created completely in Lua script, contrary to what seems to be a popular belief.
This guide will assume some knowledge of both frames and Lua programming and therefore should be considered for intermediate to advanced add-on authors. However, if the reader has completed and understood the Guide to XML Frames, and has a working knowledge of Lua scripting for Runes of Magic, this tutorial should not present too much trouble. The example add-on we will create in this guide will be similar (but not identical to) the example add-on for the Guide to XML Frames so that comparisons can be made between the two.
Given this, the guide will not be spending much time on specific features of frames or Lua script as they are essentially the same whether the frames are created via XML or Lua. Instead, the guide will cover the methods needed to achieve the same results in Lua script as can be done via XML, and some of the differences, advantages and pitfalls that may arise by doing so.
What are Dynamic Frames[ | ]
A dynamic frame is a frame that is created via Lua script, instead of through an XML file. There are several reasons we would want to do this, including better control over frame creation, ability to control global versus local access to the frames, but above all is simply the ability to create frames on-the-fly. Hence the name dynamic frame, as we can create them dynamically.
As an example of this last point, imagine creating an add-on to display buff bars. Each buff bar has an icon, the bar itself, the name of the buff displayed on the bar, as well as the time remaining on this buff. If we were to create our frames in XML, we would need to create all the frames, textures and FontStrings we will ever need in advance within the XML file, or set a maximum number of buffs that we could track. With dynamic frames, we can create new frames as we need them, and therefore don't waste memory with unneeded frames, nor require a cap to accommodate a lower number of pre-defined frames.
Starting the Example[ | ]
The add-on we will create is similar to the one given in the Guide to XML Frames, however we will only have the close button and will not bother with saving and restoring the frame's position as this is mostly done in code anyway. This is also the reason we will not be setting the frame's position or size.
To start, create the add-on folder and TOC file. We'll call this add-on MemViewerDyn
to differentiate it from, yet still have the obvious connection to the previous version. As there is no XML file for this example add-on, we only need to list the Lua file in here, and optionally the comments indicating version etc.
Here's the TOC file contents:
## Interface: 3.0.8.2349
## Title: MemViewerDyn
## Version: 1.0
## Description: Displays the Lua memory usage on screen. Uses dynamic frame creation.
## Author: Peryl
MemViewerDyn.lua
Save this file then create and open MemViewerDyn.lua
. We'll start the code with a header and creating a variable to hold the our functions and variables.
--[[
MemViewerDyn
Displays the Lua memory usage on a dynamically allocated frame.
This is the example add-on created from the Guide to Dynamic
Frames on the Runes of Magic Wiki.
Version: 1.0
Author: Peryl
License: Public Domain
]]--
--[[ Create a local namespace for our function and variables ]]--
local MemViewerDyn = {};
_G.MemViewerDyn = MemViewerDyn;
The First Big Difference[ | ]
Now we come to the first difference between creating frames in Lua and creating them in an XML file. Somehow we need to get a frame into the game engine that we can create to start things off. We don't need to create all the elements we need, but we need to get at least one frame in here otherwise what is going to call the functions we create in here?
The solution to this little dilemma comes from the fact that the game engine will, upon loading a Lua file, compile then run the file in question. Yes, you read that correctly. The Lua virtual machine, the part of the game engine that actually executes the Lua code, does not interpret the source code directly, instead it compiles the source into a binary form called byte code, then executes that result (this is similar to what Java and other interpreted languages do). Therefore, we can create at least one basic frame outside of any function and the game will end up executing our code, and hence create a frame.
Once that is done, we can have the rest of the initialization as part of the OnLoad
code snippet. We could of course define all our elements in the same manner, but it would not properly show the creation of dynamic frames.
Creating a frame, or any other UI element is done via the CreateUIComponent
function. This function takes three strings, and an optional fourth string, as parameters. The first is the type of component to be created, and has the same value as the XML tags for the same element type. The second parameter is the name to give this component, and the third is the name of the parent object for this element. The optional forth parameter is the name of a virtual template to use during creation and functions just like the inherits
attribute in the XML tags.
As we wish to create a frame, out type is "Frame"
, we'll call our frame MemViewerDyn_Frame
and we'll use "UIParent"
as the parent frame. So our code will be:
--[[ Create the base frame ]]--
local MemViewerDynFrame = CreateUIComponent("Frame", "MemViewerDyn_Frame", "UIParent");
We also need to initialize a few things for our frame. Again we don't need to intialize everything here, but we can at least give it a size, set its position set the OnLoad
and OnUpdate
code snippets. Here's the code to handle this:
-- Initialize some frame settings
MemViewerDynFrame:SetSize(200, 48);
MemViewerDynFrame:ClearAllAnchors();
MemViewerDynFrame:SetAnchor("TOP", "TOP", "UIParent", 0, 50);
MemViewerDynFrame:SetScripts("OnLoad", [=[ MemViewerDyn.OnLoadHandler(); ]=]);
MemViewerDynFrame:SetScripts("OnUpdate", [=[ MemViewerDyn.OnUpdateHandler(elapsedTime); ]=]);
The call to the ClearAllAnchors
may be redundant here. It is still unclear at this time if components created in this way have a default anchor setup or not, but as it certainly doesn't hurt to have the call here, it is safe to always have it here.
The call to the SetAnchor
method works like the XML version, The first parameter is the relative location on this object (equates to the point
attribute in XML), the second is the relative point on the object we are aligning to (this is equivalent to the relativePoint
attribute in XML). The third parameter is the object we are aligning to (XML equivalent is the relativeTo
attribute), and finally the x and y offsets respectively. The SetAnchor
method also allows us to specify the object we are aligning to directly instead of just the name of the object.
It should be noted that attempting to set the OnUpdate
script snippet from within the OnLoad
code snippet will not work. It is uncertain at the time of writing if this is because it is blocked completely or if only changing code snippets from within a snippet of the same object is blocked. More testing in this area needs to be done, but it seems to only be blocked for the current object. That is, if inside the code for the OnLoad
of MemViewerDynFrame
, we can't set MemViewerDynFrame
's OnUpdate
, but some other object's OnUpdate
seems to work (at least on creation of other objects).
Setting The Handlers[ | ]
So we'll now create the two handler functions themselves. First is the OnUpdate
function. The code will be almost identical to the previous version, except we will need to verify if the variables we need to access are available because it is possible for the OnUpdate handler to be called before everything is ready. This is because by default, all frames and elements created with CreateUIComponent
are visible by default, and therefore can trigger calls to the OnUpdate handler.
--[[ OnUpdate Handler ]]--
function MemViewerDyn.OnUpdateHandler(elapsed)
if(MemViewerDyn.UpdateTime) then
MemViewerDyn.UpdateTime = MemViewerDyn.UpdateTime + elapsed;
if(MemViewerDyn.UpdateTime >= 1.0) then
local curmem = collectgarbage("count");
SetMemViewerDynText(curmem, MemViewerDyn.PrevMem);
MemViewerDyn.PrevMem = curmem;
MemViewerDyn.UpdateTime = 0;
end
end
end
Our OnLoad handler function however will be extended in order to initialize the rest of our add-on, including the creation of all the sub-elements for our frame. We'll create some helper functions for creating these sub-elements, so as we will be adding a FontString for the text, a texture for decoration, and a close button, our OnLoad handler becomes:
--[[ OnLoad handler ]]--
function MemViewerDyn.OnLoadHandler()
-- Create sub-elements for our frame
CreateFontString();
CreateTexture();
CreateCloseButton();
-- Init add-on variables
MemViewerDyn.PrevMem = collectgarbage("count");
SetMemViewerDynText(MemViewerDyn.PrevMem, MemViewerDyn.PrevMem);
MemViewerDyn.UpdateTime = 0;
end
Now we create the helper function, or at least the skeleton of these functions even if they don't do anything yet. Place these before the handler functions, but after the creation of the base frame.
--[[ Helper functions ]]--
-- Set Lua memory usage text on our frame's FontString
local function SetMemViewerDynText(current, previous)
end
-- Create a FontString and attach it to our frame
local function CreateFontString()
end
-- Create a texture and attach it to our frame
local function CreateTexture()
end
-- Create the close button
local function CreateCloseButton()
end
Adding the FontString[ | ]
Now we can start filling in our helper functions and create each element for our frame. We'll start with the FontString since that will also allow us to fill in the helper function to set the text as well.
Creating the FontString is not really different from creating the frame itself, however we'll need to initialize a few things. Instead of using a font template, we'll initialize the FontString manually to show how it can be done. Change our CreateFontString
function to this:
local function CreateFontString()
MemViewerDynFrame.Text = CreateUIComponent("FontString", "MemViewerDyn_Text", "MemViewerDyn_Frame");
MemViewerDynFrame.Text:SetSize(200, 14);
MemViewerDynFrame.Text:SetFont("Fonts/DFHEIMDU.TTF", 12, "NORMAL", "NORMAL"); -- file, size, weight, outline
MemViewerDynFrame.Text:SetJustifyHType("CENTER");
MemViewerDynFrame.Text:ClearAllAnchors();
MemViewerDynFrame.Text:SetAnchor("BOTTOM", "BOTTOM", MemViewerDynFrame, 0, -7);
MemViewerDynFrame.Text:SetText("---");
MemViewerDynFrame:SetLayers(3, MemViewerDynFrame.Text); -- Attach to our base frame
end
Here we first create the FontString itself. We are assigning it to the variable Text
inside our local MemViewerDynFrame
. This is being done on purpose for later in this tutorial.
We then give our FontString a size. This will be the area the text can occupy, and can be thought of as the window size for the FontString to be displayed in. If we set text that would display wider than this area, the game will automatically cut the string short and add the ellipses (...) at the end.
Next we set the font file to use. Here the parameters are the actual font file, the point size to use, the weight to use for the text, and the outline mode. The valid values for the weight parameter are
- THIN
- NORMAL
- BOLD
The values for the outline parameter, which affects the shadow around the text, are:
- NONE
- NORMAL
- THICK
The SetJustifyHType
method sets the horizontal justification of the text within the sized area given. Valid values are
- LEFT
- CENTER
- RIGHT
Note how the SetAnchor
method is being used here. Instead of giving the name of the frame to align to, the actual object itself is being passed. Both methods work.
The call to SetText
here is merely putting in place holder text and is somewhat redundant.
The last line in this function is rather important. This is where we set which layer of our frame this FontString should appear at. The first parameter is the level the layer should be set to, and the second parameter is the FontString itself. It is likely that this function will take a table of layers for setting multiple layers at the same time, but this has not been determined as of this writing. What is known is that calling SetLayers
in the fashion shown works. A word of caution should be given here. Do not attempt to give SetLayers
a frame or frame derived element as its second parameter. Though the game will appear to function correctly, doing so will crash the game when exiting or returning to the character selection screen.
With the FontString created, we can now fill in the code for the helper function that sets the text with the memory display. It really isn't any different from the previous version so no more will be said about it. Here's the code:
local function SetMemViewerDynText(current, previous)
local change = current - previous;
MemViewerDynFrame.Text:SetText(string.format("Mem: %.2f KB (%.4f KB)", current, change), 1, 1, 1);
end
Try this version out if you wish.
Texture Goodness[ | ]
Much as was done in the XML guide to frames, we'll now place a texture behind the text for a glow effect. We once again use CreateUIComponent
to create the texture object itself, then call various methods to initialize the settings. Change our CreateTexture
helper function as follows:
local function CreateTexture()
MemViewerDynFrame.Texture = CreateUIComponent("Texture", "MemViewerDyn_Texture", "MemViewerDyn_Frame");
MemViewerDynFrame.Texture:SetSize(175, 16);
MemViewerDynFrame.Texture:SetTexture("interface/transportbook/tb_highlight-01");
MemViewerDynFrame.Texture:SetTexCoord(0.0, 1.0, 0.0, 1.0); -- left, right, top, bottom
MemViewerDynFrame.Texture:SetColor(1.0, 1.0, 1.0);
MemViewerDynFrame.Texture:SetAlpha(1.0);
MemViewerDynFrame.Texture:SetAlphaMode("ADD");
MemViewerDynFrame.Texture:ClearAllAnchors();
MemViewerDynFrame.Texture:SetAnchor("BOTTOM", "BOTTOM", MemViewerDynFrame, 0, -6);
MemViewerDynFrame:SetLayers(2, MemViewerDynFrame.Texture);
end
First we create the texture object itself, then set a size and tell the game what actual image file to use via the SetTexture
method. This is followed by a call to SetTexCoord
with the coordinates to use as the visible area of the image. The values here work like the XML counterpart, but the order of the parameters is left side, right side, top, then bottom.
We then set a color (this is optional) that affects the tinting of the texture, then set the alpha transparency for the texture as well as the blending mode. We then set the position of the texture relative to our frame.
Lastly, we again perform a SetLayers
on our frame to set which layer this texture will be drawn at. As we want the texture to be behind the text, we set a layer value that is less than the one we specified for the text.
Once these changes are done, try out the add-on.
Closing the Frame[ | ]
Time to add a simple close button. We'll use the existing UIPanelCloseButtonTemplate
template as we did in the XML frames guide. This will allow us to create a much simpler creation function as most of the settings are already done for us. Modify the CreateCloseButton
helper function like this:
local function CreateCloseButton()
MemViewerDynFrame.CloseButton = CreateUIComponent("Button", "MemViewerDyn_Close", "MemViewerDyn_Frame", "UIPanelCloseButtonTemplate");
MemViewerDynFrame.CloseButton:ClearAllAnchors();
MemViewerDynFrame.CloseButton:SetAnchor("TOPRIGHT", "TOPRIGHT", MemViewerDynFrame, -5, 6);
end
Here we create the button as expected, but we also add the fourth parameter to the call giving the name of the template we wish to use. Now all that is needed is to set the button's position relative to our frame. Note that we do not perform a SetLayers
call because buttons are frame derived objects themselves and therefore doing so would cause the game to crash on exit.
Try this version out. To get the frame back after closing it, type /run MemViewerDyn_Frame:Show() in the chat edit box.
Placing a Backdrop[ | ]
Our close button now looks kind of ridiculous floating there in the middle of nowhere. So lets place a backdrop on this frame. We can easily set a backdrop on a frame by calling the SetBackdrop
method of our frame, but this method requires a table with the appropriate information. So first, we'll create this table.
Add the following right before we create the base frame:
--[[ Define a table for setting the frame backdrop ]]--
local FrameBackdrop = {
edgeFile = "Interface/Tooltips/Tooltip-border",
bgFile = "Interface/Tooltips/Tooltip-background",
BackgroundInsets = { top = 4, left = 4, bottom = 4, right = 4 },
EdgeSize = 16,
TileSize = 16,
};
This table contains all the information we'll need for setting a backdrop. Note the similarities between this and how the backdrop information is specified in XML.
Now that we have this table, we can add a call to SetBackdrop
in our OnLoad handler. Add the following after the calls to create the sub-elements:
-- Set the backdrop for our frame
MemViewerDynFrame:SetBackdrop(FrameBackdrop);
Save and try this version out.
Going Green[ | ]
At the start of this guide, an example was given about a fictitious buff bar add-on. Now suppose we were creating this add-on. As mentioned, dynamically creating frames solves the problem of forcing a cap on the maximum number of buffs that can be tracked, it also allows us to not pre-define a set number of frames. But if all we did was create a new frame for every buff that gets applied, we'll eventually run out of memory as there doesn't appear to be any convenient way to delete frames. This is where the technique known as frame recycling comes into play.
The idea behind frame recycling is to maintain a pool of old, currently unused frames, and always check if there is a frame available in this pool prior to creating a frame. If so, remove one from the pool and use that. If not, then we create a new frame. Once a frame is no longer needed, hide the frame and add it back to the pool. In this fashion, we only ever create the maximum number of frames needed, and thereby save memory.
Stopping Global Pollution[ | ]
One of the main problems with creating frames via XML is that all frames and their sub-elements are created in the global namespace. Not only does this make accessing these frames slower, but it also creates an enormous global namespace. Last time I checked (back in version 3.0.4 or so), the _G table contained well over 60000 entries. This will slow access down even more since the Lua virtual machine much slog through this table for each global variable accessed (well OK, due to how Lua works, it doesn't need to search all 60000 entries but it still slows it down). To make matters worse, each time we create a frame or frame element using CreateUIComponent
, the game also creates a global variable with the same name as the element name provided. This constant use of global variables is known as polluting the global namespace.
When we create frames dynamically, we have an opportunity to stop this effect. The way we do this is to ensure that we always assign the objects we create to a local variable or at least to some other object or table that will be a single point of access from global namespace. As our example add-on already has such a table, we can set our frame in there, as well as all sub-objects. Though if you've been paying attention, you will notice that all sub-objects are already being set inside the main frame's object. So all we need to do is set the base frame into our namespace variable, and remove all the global variables created by CreateUIComponent
.
So first, add the following two lines immediately after creating the base frame.
_G.MemViewerDyn_Frame = nil; -- Remove global created by CreateUIComponent
MemViewerDyn.Frame = MemViewerDynFrame; -- Save a reference for global access
It is safe to nil
the global in this fashion since there is still another reference to the object within our file (namely the local MemViewerDynFrame
).
We can now do the same for all the sub-elements we crete as well. For each call to CreateUIComponent
, add a line to nil the global that it creates. Remember to use the name given in the call itself and not the name of the variable. It is also a good idea to place the _G
here to ensure it is the global reference that is being removed. The three lines will be (to be placed after each corresponding call to CreateUIComponent
):
_G.MemViewerDyn_Text = nil;
_G.MemViewerDyn_Texture = nil;
_G.MemViewerDyn_Close = nil;
In the case of the close button, because we are using a template, we should also ensure that any sub-objects created in the template is also removed from global space where appropriate. This is not required for this template so no other changes need be done for our code. However, some sub-elements created by templates may still need to reside in global space for the predefined functions to work correctly, so handle templates with care.
Once the changes are done, try the new version. You will notice that the add-on runs fine, but if you close the window you will now need to type /run MemViewerDyn.Frame:Show() in the chat edit box to get it back.
If you parse this last version of out example add-on, you will see that only one global variable (MemViewerDyn
) is visible yet the entire add-on works as before.
Avoiding a Potential Problem[ | ]
There is one problem with using dynamic frames as described here that may show up. Due to how we create our frame, it is quite possible to not receive the VARIABLES_LOADED
event. This is likely caused by not having an OnEvent
code snippet setup in time to receive the event. This may at first seem to be a problem since add-ons often rely on this event to see if their variables are ready.
A workaround for this problem is to register for both the VARIABLES_LOADED
and LOADING_END
events. In the OnEvent
handler, un-register from the LOADING_END
event once either the VARIABLES_LOADED
or LOADING_END
event has fired.
LOADING_END
is fired after all loading is complete and the game is about to transfer control over to the player after a loading screen. At this point the loading screen is still visible, but everything has now been loaded and therefore any variables that your add-on may require will also be loaded. The reason we un-register from the LOADING_END
event is that this event is triggered for every load screen, so it will get triggered when teleporting, when entering an instance, etc. So unless these are places you are also interested in, you should un-register from this event.
Reference Code[ | ]
Here is the completed example add-on for reference.
MemViewerDyn.toc[ | ]
## Interface: 3.0.8.2349
## Title: MemViewerDyn
## Version: 1.0
## Description: Displays the Lua memory usage on screen. Uses dynamic frame creation.
## Author: Peryl
MemViewerDyn.lua
MemViewerDyn.lua[ | ]
--[[
MemViewerDyn
Displays the Lua memory usage on a dynamically allocated frame.
This is the example add-on created from the Guide to Dynamic Frames on
the Runes of Magic Wiki.
Version: 1.0
Author: Peryl
License: Public Domain
]]--
--[[ Create a local namespace for our function and variables ]]--
local MemViewerDyn = {};
_G.MemViewerDyn = MemViewerDyn;
--[[ Define a table for setting the frame backdrop ]]--
local FrameBackdrop = {
edgeFile = "Interface/Tooltips/Tooltip-border",
bgFile = "Interface/Tooltips/Tooltip-background",
BackgroundInsets = { top = 4, left = 4, bottom = 4, right = 4 },
EdgeSize = 16,
TileSize = 16,
};
--[[ Create the base frame ]]--
local MemViewerDynFrame = CreateUIComponent("Frame", "MemViewerDyn_Frame", "UIParent");
_G.MemViewerDyn_Frame = nil; -- Remove global created by CreateUIComponent
MemViewerDyn.Frame = MemViewerDynFrame; -- Save a reference for global access
-- Initialize some frame settings
MemViewerDynFrame:SetSize(200, 48);
MemViewerDynFrame:ClearAllAnchors();
MemViewerDynFrame:SetAnchor("TOP", "TOP", "UIParent", 0, 50);
MemViewerDynFrame:SetScripts("OnLoad", [=[ MemViewerDyn.OnLoadHandler(); ]=]);
MemViewerDynFrame:SetScripts("OnUpdate", [=[ MemViewerDyn.OnUpdateHandler(elapsedTime); ]=]);
--[[ Helper functions ]]--
-- Set Lua memory usage text on our frame's FontString
local function SetMemViewerDynText(current, previous)
local change = current - previous;
MemViewerDynFrame.Text:SetText(string.format("Mem: %.2f KB (%.4f KB)", current, change), 1, 1, 1);
end
-- Create a FontString and attach it to our frame
local function CreateFontString()
MemViewerDynFrame.Text = CreateUIComponent("FontString", "MemViewerDyn_Text", "MemViewerDyn_Frame");
_G.MemViewerDyn_Text = nil;
MemViewerDynFrame.Text:SetSize(200, 14);
MemViewerDynFrame.Text:SetFont("Fonts/DFHEIMDU.TTF", 12, "NORMAL", "NORMAL");
MemViewerDynFrame.Text:SetJustifyHType("CENTER");
MemViewerDynFrame.Text:ClearAllAnchors();
MemViewerDynFrame.Text:SetAnchor("BOTTOM", "BOTTOM", MemViewerDynFrame, 0, -7);
MemViewerDynFrame.Text:SetText("---");
MemViewerDynFrame:SetLayers(3, MemViewerDynFrame.Text); -- Attach to our base frame
end
-- Create a texture and attach it to our frame
local function CreateTexture()
MemViewerDynFrame.Texture = CreateUIComponent("Texture", "MemViewerDyn_Texture", "MemViewerDyn_Frame");
_G.MemViewerDyn_Texture = nil;
MemViewerDynFrame.Texture:SetSize(175, 16);
MemViewerDynFrame.Texture:SetTexture("interface/transportbook/tb_highlight-01");
MemViewerDynFrame.Texture:SetTexCoord(0.0, 1.0, 0.0, 1.0); -- left, right, top, bottom
MemViewerDynFrame.Texture:SetColor(1.0, 1.0, 1.0);
MemViewerDynFrame.Texture:SetAlpha(1.0);
MemViewerDynFrame.Texture:SetAlphaMode("ADD");
MemViewerDynFrame.Texture:ClearAllAnchors();
MemViewerDynFrame.Texture:SetAnchor("BOTTOM", "BOTTOM", MemViewerDynFrame, 0, -6);
MemViewerDynFrame:SetLayers(2, MemViewerDynFrame.Texture);
end
-- Create the close button
local function CreateCloseButton()
MemViewerDynFrame.CloseButton = CreateUIComponent("Button", "MemViewerDyn_Close", "MemViewerDyn_Frame", "UIPanelCloseButtonTemplate");
_G.MemViewerDyn_Close = nil;
MemViewerDynFrame.CloseButton:ClearAllAnchors();
MemViewerDynFrame.CloseButton:SetAnchor("TOPRIGHT", "TOPRIGHT", MemViewerDynFrame, -5, 6);
end
--[[ OnLoad handler ]]--
function MemViewerDyn.OnLoadHandler()
-- Create sub-elements for our frame
CreateFontString();
CreateTexture();
CreateCloseButton();
-- Set the backdrop for our frame
MemViewerDynFrame:SetBackdrop(FrameBackdrop);
-- Init add-on variables
MemViewerDyn.PrevMem = collectgarbage("count");
SetMemViewerDynText(MemViewerDyn.PrevMem, MemViewerDyn.PrevMem);
MemViewerDyn.UpdateTime = 0;
end
--[[ OnUpdate Handler ]]--
function MemViewerDyn.OnUpdateHandler(elapsed)
if(MemViewerDyn.UpdateTime) then
MemViewerDyn.UpdateTime = MemViewerDyn.UpdateTime + elapsed;
if(MemViewerDyn.UpdateTime >= 1.0) then
local curmem = collectgarbage("count");
SetMemViewerDynText(curmem, MemViewerDyn.PrevMem);
MemViewerDyn.PrevMem = curmem;
MemViewerDyn.UpdateTime = 0;
end
end
end