Making scrollable frames
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 withScrollView:SetElementExtent
, or by supplying your own calculator function toScrollView: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
- You can hide the ScrollBar when the frame isn't scrollable using
ScrollBar:SetHideIfUnscrollable(true)
- View the source code for the scroll view used in this example.
- View the source code for data providers.
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
- View the source code for the scroll view used in this example.
- View the source code for tree data providers.
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 defaultfactory
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 toScrollView:SetElementExtentCalculator