Runes of Magic Wiki
Register
Advertisement

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
Advertisement