Making scrollable frames

From Warcraft Wiki
Jump to navigation Jump to search

Frames can be made scrollable by using a combination of a scroll box, scroll bar, and a scroll view. There are a few different types of scrollable frames including list views, tree views, content views, and grid views. These can be created either through Lua, or through XML.

Linear List

Creating the frames

To get started, we need to do a few things.

  • Create our ScrollBox, ScrollBar, and ScrollView
  • Create a DataProvider to feed our ScrollView with data to display
  • Initialize our frames with ScrollUtil.InitScrollBoxListWithScrollBar

Using Lua

local ScrollBox = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
ScrollBox:SetPoint("CENTER")
ScrollBox:SetSize(300, 300)

local ScrollBar = CreateFrame("EventFrame", nil, UIParent, "MinimalScrollBar")
ScrollBar:SetPoint("TOPLEFT", ScrollBox, "TOPRIGHT")
ScrollBar:SetPoint("BOTTOMLEFT", ScrollBox, "BOTTOMRIGHT")

local DataProvider = CreateDataProvider()
local ScrollView = CreateScrollBoxListLinearView()
ScrollView:SetDataProvider(DataProvider)

ScrollUtil.InitScrollBoxListWithScrollBar(ScrollBox, ScrollBar, ScrollView)

Initializing the elements

Next, we need to tell our ScrollView how to initialize each element by passing our own initializer function to ScrollView:SetElementInitializer. However, it's important to understand how our list is going to be populated before going further. For each element, we'll create a data table containing any data we'd need in order to populate or initialize our frame - in this example, we're just going to pass a character's name, and class. We'll then add that data table to our DataProvider, which will call the aforementioned initializer function to set up the frame in our list.

Our initializer function is responsible for configuring our frame with the data we provide. In this example, we'll make each element a button that prints a character's name and class, both of which will be contained in our data table.

  • It's important to keep in mind when initializing your elements that your frames are reused by the ScrollView, so if you add persistent data to the frame, it's going to stick around unless you explicitly overwrite it later.
  • At this step, you'll want to plan out how you're going to structure your data and how it'll be used. Here, our data only contains two elements, but you can add anything you'd like to the data table, i.e. a callback function, button style info, button text etc.
  • When calling ScrollView:SetElementInitializer, you can pass either a frame type or a frame template. However, when passing a frame type, you'll need to tell the ScrollView each element's extent, or how tall it should be. Do this with ScrollView:SetElementExtent, or by supplying your own calculator function to ScrollView:SetElementExtentCalculator.
-- The 'button' argument is the frame that our data will inhabit in our list
-- The 'data' argument will be the data table mentioned above
local function Initializer(button, data)
    local playerName = data.PlayerName
    local playerClass = data.PlayerClass
    button:SetScript("OnClick", function()
        print(playerName .. ": " .. playerClass)
    end)
    button:SetText(playerName)
end

-- The first argument here can either be a frame type or frame template. We're just passing the "UIPanelButtonTemplate" template here
ScrollView:SetElementInitializer("UIPanelButtonTemplate", Initializer)
  • As your addon's requirements become more complex, it's advisable to create your own frame template with a mixin containing an :Init() method that you can call in the initializer to simplify setup.

Adding data

Now that we have everything set up, we can start adding some data. We'll do this by creating a table containing our data, and adding it to our DataProvider we defined earlier, this will create a new button in your ScrollBox. You can continue to add as many elements as you want in this way.

local myData = {
    PlayerName = "Ghost",
    PlayerClass = "Priest"
}

DataProvider:Insert(myData)
  • Inserting data into the DataProvider will call our previously defined element initializer function, passing in our supplied data.

Notes

Complete Example

local ScrollBox = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
ScrollBox:SetPoint("CENTER")
ScrollBox:SetSize(300, 300)

local ScrollBar = CreateFrame("EventFrame", nil, UIParent, "MinimalScrollBar")
ScrollBar:SetPoint("TOPLEFT", ScrollBox, "TOPRIGHT")
ScrollBar:SetPoint("BOTTOMLEFT", ScrollBox, "BOTTOMRIGHT")

local DataProvider = CreateDataProvider()
local ScrollView = CreateScrollBoxListLinearView()
ScrollView:SetDataProvider(DataProvider)

ScrollUtil.InitScrollBoxListWithScrollBar(ScrollBox, ScrollBar, ScrollView)

-- The 'button' argument is the frame that our data will inhabit in our list
-- The 'data' argument will be the data table mentioned above
local function Initializer(button, data)
    local playerName = data.PlayerName
    local playerClass = data.PlayerClass
    button:SetScript("OnClick", function()
        print(playerName .. ": " .. playerClass)
    end)
    button:SetText(playerName)
end

-- The first argument here can either be a frame type or frame template. We're just passing the "UIPanelButtonTemplate" template here
ScrollView:SetElementInitializer("UIPanelButtonTemplate", Initializer)

local myData = {
    PlayerName = "Ghost",
    PlayerClass = "Priest"
}

DataProvider:Insert(myData)

Tree List

A tree list is much like a linear list, but allows it's elements to expand or collapse, and contain more elements within them. An example of a tree list can be seen in the Blizzard reputation frame.

As a tree list is a bit more complex than a linear list, it's recommended to understand how linear lists work before creating a tree list.

Creating the frames

Setup for a tree list is almost identical to that of the linear list, but uses a different type of data provider and scroll view. In a tree list, each element is a node, rather than just an element containing some data. To get started, we'll essentially do the same as we did with the linear list.

  • Create the ScrollBox, ScrollBar, and ScrollView
  • Create a TreeDataProvider to feed our ScrollView with data, and to manage our nodes
  • Initialize our frames with ScrollUtil.InitScrollBoxListWithScrollBar

Using Lua

local ScrollBox = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
ScrollBox:SetPoint("CENTER")
ScrollBox:SetSize(300, 300)

local ScrollBar = CreateFrame("EventFrame", nil, UIParent, "MinimalScrollBar")
ScrollBar:SetPoint("TOPLEFT", ScrollBox, "TOPRIGHT")
ScrollBar:SetPoint("BOTTOMLEFT", ScrollBox, "BOTTOMRIGHT")

local DataProvider = CreateTreeDataProvider()
local ScrollView = CreateScrollBoxListTreeListView()
ScrollView:SetDataProvider(DataProvider)

ScrollUtil.InitScrollBoxListWithScrollBar(ScrollBox, ScrollBar, ScrollView)

Initializing the elements

Next, we need to tell our ScrollView how to initialize our nodes. We'll again be using ScrollView:SetElementInitializer here to set our initializer function.

One key difference between the linear list and a tree list, is that tree lists use nodes instead of elements. A node is a table that contains methods related to navigating the tree structure and collapsing/uncollapsing nodes. It will also contain the data we pass in for each element.

Our initializer function will once again serve the role of initializing our frames in the list. Like the linear list example, our initializer function will be passed a frame, and instead of just the data, it will be given a node.

  • Frames are reused by the ScrollView, be sure to clear persistent data when initializing new frames
  • Unlike with a linear list, a tree list can contain nodes, which can in turn contain more nodes. You'll want to have a good understanding of your data's structure, and where your data should be in this hierarchy.
-- The 'button' argument is the frame that our data will inhabit in our list
-- The 'node' argument will be a node table, as explained above
local function Initializer(button, node)
    local data = node:GetData() -- get our data from the node with :GetData()
    local text = data.ButtonText
    button:SetText(text)
    button:SetScript("OnClick", function()
        node:ToggleCollapsed()
    end)
end

-- The first argument here can either be a frame type or frame template. We're just passing the "UIPanelButtonTemplate" template here
ScrollView:SetElementInitializer("UIPanelButtonTemplate", Initializer)
  • With tree lists, you'll likely want to define different initialization behavior depending on where the node is in the list.

Adding data

As before, we'll create a table of arbitrary data which we will add to our DataProvider. An important difference with a tree list, however, is that adding an element to our DataProvider will return a node. We can use this node to add child elements to that entry.

Because of this difference, we'll be using a more generic ButtonText key instead of something more descriptive. This is to provide flexibility for different levels of the list, and to simplify the initialization function above.

local topLevelData = {
    ButtonText = "Ghost"
}
local GhostElement = DataProvider:Insert(topLevelData)

local nestedData = {
    ButtonText = "Class: Priest"
}
GhostElement:Insert(nestedData)
  • The example above is merely to illustrate one way you can create child elements on the nodes of your tree list.
  • For tree lists, it's highly recommended that you create either more robust initialization functions to account for the different types of data it will receive, or to create your own frame template with a mixin containing an :Init() method that can handle all the different types of data it will receive.

Notes

Complete Example

local ScrollBox = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
ScrollBox:SetPoint("CENTER")
ScrollBox:SetSize(300, 300)

local ScrollBar = CreateFrame("EventFrame", nil, UIParent, "MinimalScrollBar")
ScrollBar:SetPoint("TOPLEFT", ScrollBox, "TOPRIGHT")
ScrollBar:SetPoint("BOTTOMLEFT", ScrollBox, "BOTTOMRIGHT")

local DataProvider = CreateTreeDataProvider()
local ScrollView = CreateScrollBoxListTreeListView()
ScrollView:SetDataProvider(DataProvider)

ScrollUtil.InitScrollBoxListWithScrollBar(ScrollBox, ScrollBar, ScrollView)

-- The 'button' argument is the frame that our data will inhabit in our list
-- The 'node' argument will be a node table, as explained above
local function Initializer(button, node)
    local data = node:GetData() -- get our data from the node with :GetData()
    local text = data.ButtonText
    button:SetText(text)
    button:SetScript("OnClick", function()
        node:ToggleCollapsed()
    end)
end

-- The first argument here can either be a frame type or frame template. We're just passing the "UIPanelButtonTemplate" template here
ScrollView:SetElementInitializer("UIPanelButtonTemplate", Initializer)

local topLevelData = {
    ButtonText = "Ghost"
}
local GhostElement = DataProvider:Insert(topLevelData)

local nestedData = {
    ButtonText = "Class: Priest"
}
GhostElement:Insert(nestedData)

Other Concepts

Managed Scroll Bar Visibility

With scrollable frames, frame anchors can be adjusted dynamically based on scroll bar visiblity. For example, you want your scrollbox to fill the parent frame, but move over to make room for the scroll bar when the content becomes scrollable. To configure this behavior, we need to do two things.

  • Define our anchors, both with and without a scroll bar
  • Register the behavior with ScrollUtil.AddManagedScrollBarVisibilityBehavior

The anchors we'll define are the scroll box's anchors, and will be created with the same syntax as ScriptRegionResizing:SetPoint.

local anchorsWithScrollBar = {
    CreateAnchor("TOPLEFT", 4, -4);
    CreateAnchor("BOTTOMRIGHT", ScrollBar, -13, 4),
};

local anchorsWithoutScrollBar = {
    CreateAnchor("TOPLEFT", 4, -4),
    CreateAnchor("BOTTOMRIGHT", -4, 4);
};

ScrollUtil.AddManagedScrollBarVisibilityBehavior(ScrollBox, ScrollBar, anchorsWithScrollBar, anchorsWithoutScrollBar);

This will allow our scroll box to dynamically change size based on whether or not the content is scrollable.

Custom Element Factories

A custom element factory is one step above the element initializer function. One use of a custom factory is to change the frame template used by the element based on the data passed into the data provider. Here, we'll still need to define an initializer function, but we won't register it with the scroll view directly.

  • The example below assumes you're using a frame template that contains an :Init() method. It also uses the tree list initialization pattern of passing a node instead of just the data.
  • The custom factory function create takes in the default factory function, and a node. The default factory is the function used internally to obtain a frame for our node.
  • Here, we're expecting the data passed in to contain a key telling us which template to use when creating the frame. We'll then pass this template, along with our initializer function to the default factory function we're given in order to obtain and initialize our frame in the list.
local function Initializer(frame, node)
    frame:Init(node)
end

local function CustomFactory(factory, node)
    local data = node:GetData()
    local template = data.Template
    factory(template, Initializer)
end

ScrollView:SetElementFactory(CustomFactory)
  • If you'd rather pass in a frame type, be sure to set the element extent with ScrollView:SetElementExtent or provide your own calculator to ScrollView:SetElementExtentCalculator