Using UiLists in Blender
Hello fellow Pythonistas, in this tutorial we’ll find out how to use UILists in our scripts and add-ons. If your users need to handle large amounts (or variable) amounts of data, this is the widget you want.
While you could just use a loop to draw widgets UILists have several benefits over custom-made solutions, like filtering, searching and managing space correctly (less scrolling). It’s also how it’s done throughout the UI, so you can stay consistent with the rest of Blender.
By the way, I’m assuming you already know your way around the bpy and Python. This is an intermediate tutorial.
Define the properties
First we have to create some properties to hold the data on our list first. If we are populating the list with data of our own, we’ll need to define the structure of the items in the list. You don’t need to define an item if you’re using existing datablocks like materials, or textures.
class ListItem(PropertyGroup):
"""Group of properties representing an item in the list."""
name: StringProperty(
name="Name",
description="A name for this item",
default="Untitled")
random_prop: StringProperty(
name="Any other property you want",
description="",
default="")
We’ll also need a Collection Property that will hold our list data, and an Integer property to hold the index number. Let’s add them in our register() function.
def register():
bpy.types.Scene.my_list = CollectionProperty(type = ListItem)
bpy.types.Scene.list_index = IntProperty(name = "Index for my_list",
default = 0)
Make the list widget
Now we can create the actual widget by extending the UIList class. The UIList has one method (that we care about) called draw_item(). This method is called to draw each item in the list and it works pretty much like any other drawing callback in Blender. You can use labels, check boxes, operator buttons, etc.
draw_item() includes several parameters we can use:
- data: Object containing the collection property (in our case, the scene)
- item: The current item being drawn
- icon: Calculated icon for the current item as an integer (could be one of Blender’s or an icon for a material/texture/etc. depending on the item)
- active_data: Object containing the active property of the collection (in our case, the scene too)
- active_propname: The name of the active property (index). In our case, it would be “list_index”)
- index: Index of the current item (this one is optional)
Don’t forget to register your UIList subclasses, or Blender won’t know about them.
class MY_UL_List(UIList):
"""Demo UIList."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
# We could write some code to decide which icon to use here...
custom_icon = 'OBJECT_DATAMODE'
# Make sure your code supports all 3 layout types
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name, icon = custom_icon)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
# ( inside register() )
bpy.utils.register_class(MY_UL_List)
Now we can call it in our UI with the template_list() function.
row = layout.row()
row.template_list("MY_UL_List", "The_List", scene,
"my_list", scene, "list_index")
Let users modify the list
What good is a list if users can’t do anything with it? We’ll let users add, delete and move items up and down. These are the most basic operations, you can easily use them as a template to make your own.
Let’s begin by adding the operators.
class LIST_OT_NewItem(Operator):
"""Add a new item to the list."""
bl_idname = "my_list.new_item"
bl_label = "Add a new item"
def execute(self, context):
context.scene.my_list.add()
return{'FINISHED'}
The add operator is pretty straightforward. Just call the add() method in the list.
class LIST_OT_DeleteItem(Operator):
"""Delete the selected item from the list."""
bl_idname = "my_list.delete_item"
bl_label = "Deletes an item"
@classmethod
def poll(cls, context):
return context.scene.my_list
def execute(self, context):
my_list = context.scene.my_list
index = context.scene.list_index
my_list.remove(index)
context.scene.list_index = min(max(0, index - 1), len(my_list) - 1)
return{'FINISHED'}
For the delete op we first have to check we have something to delete. We use the poll() method to check that there’s at least one item in the list. The cool thing about using poll() is that you it will also disable the operator button in the ui automatically.
Deleting is done by calling remove() with the list index (which will be whatever we have selected in the ui).
Finally we have to fix the index. If we delete at the end of the list the index will have an invalid value (larger than the length of the list), so we always move it back one position unless it’s zero.
class LIST_OT_MoveItem(Operator):
"""Move an item in the list."""
bl_idname = "my_list.move_item"
bl_label = "Move an item in the list"
direction = bpy.props.EnumProperty(items=(('UP', 'Up', ""),
('DOWN', 'Down', ""),))
@classmethod
def poll(cls, context):
return context.scene.my_list
def move_index(self):
""" Move index of an item render queue while clamping it. """
index = bpy.context.scene.list_index
list_length = len(bpy.context.scene.my_list) - 1 # (index starts at 0)
new_index = index + (-1 if self.direction == 'UP' else 1)
bpy.context.scene.list_index = max(0, min(new_index, list_length))
def execute(self, context):
my_list = context.scene.my_list
index = context.scene.list_index
neighbor = index + (-1 if self.direction == 'UP' else 1)
my_list.move(neighbor, index)
self.move_index()
return{'FINISHED'}
Moving an item around takes some more consideration because we have to make sure the index stays valid.
The move() method allows us to swap items at two different indexes. We only have to figure out which index to swap with. Remember we start counting from zero, increasing downwards.
Finally we call our own move_index() method to change the index while clamping it between 0 and the list length.
Item Data
Showing data from a list item is easy too, just use the index. Don’t forget to make sure it’s valid though.
if scene.list_index >= 0 and scene.my_list:
item = scene.my_list[scene.list_index]
row = layout.row()
row.prop(item, "name")
row.prop(item, "random_property")
The Final code
import bpy
from bpy.props import StringProperty, IntProperty, CollectionProperty
from bpy.types import PropertyGroup, UIList, Operator, Panel
class ListItem(PropertyGroup):
"""Group of properties representing an item in the list."""
name: StringProperty(
name="Name",
description="A name for this item",
default="Untitled")
random_prop: StringProperty(
name="Any other property you want",
description="",
default="")
class MY_UL_List(UIList):
"""Demo UIList."""
def draw_item(self, context, layout, data, item, icon, active_data,
active_propname, index):
# We could write some code to decide which icon to use here...
custom_icon = 'OBJECT_DATAMODE'
# Make sure your code supports all 3 layout types
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name, icon = custom_icon)
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon = custom_icon)
class LIST_OT_NewItem(Operator):
"""Add a new item to the list."""
bl_idname = "my_list.new_item"
bl_label = "Add a new item"
def execute(self, context):
context.scene.my_list.add()
return{'FINISHED'}
class LIST_OT_DeleteItem(Operator):
"""Delete the selected item from the list."""
bl_idname = "my_list.delete_item"
bl_label = "Deletes an item"
@classmethod
def poll(cls, context):
return context.scene.my_list
def execute(self, context):
my_list = context.scene.my_list
index = context.scene.list_index
my_list.remove(index)
context.scene.list_index = min(max(0, index - 1), len(my_list) - 1)
return{'FINISHED'}
class LIST_OT_MoveItem(Operator):
"""Move an item in the list."""
bl_idname = "my_list.move_item"
bl_label = "Move an item in the list"
direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""),
('DOWN', 'Down', ""),))
@classmethod
def poll(cls, context):
return context.scene.my_list
def move_index(self):
""" Move index of an item render queue while clamping it. """
index = bpy.context.scene.list_index
list_length = len(bpy.context.scene.my_list) - 1 # (index starts at 0)
new_index = index + (-1 if self.direction == 'UP' else 1)
bpy.context.scene.list_index = max(0, min(new_index, list_length))
def execute(self, context):
my_list = context.scene.my_list
index = context.scene.list_index
neighbor = index + (-1 if self.direction == 'UP' else 1)
my_list.move(neighbor, index)
self.move_index()
return{'FINISHED'}
class PT_ListExample(Panel):
"""Demo panel for UI list Tutorial."""
bl_label = "UI_List Demo"
bl_idname = "SCENE_PT_LIST_DEMO"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
def draw(self, context):
layout = self.layout
scene = context.scene
row = layout.row()
row.template_list("MY_UL_List", "The_List", scene,
"my_list", scene, "list_index")
row = layout.row()
row.operator('my_list.new_item', text='NEW')
row.operator('my_list.delete_item', text='REMOVE')
row.operator('my_list.move_item', text='UP').direction = 'UP'
row.operator('my_list.move_item', text='DOWN').direction = 'DOWN'
if scene.list_index >= 0 and scene.my_list:
item = scene.my_list[scene.list_index]
row = layout.row()
row.prop(item, "name")
row.prop(item, "random_prop")
def register():
bpy.utils.register_class(ListItem)
bpy.utils.register_class(MY_UL_List)
bpy.utils.register_class(LIST_OT_NewItem)
bpy.utils.register_class(LIST_OT_DeleteItem)
bpy.utils.register_class(LIST_OT_MoveItem)
bpy.utils.register_class(PT_ListExample)
bpy.types.Scene.my_list = CollectionProperty(type = ListItem)
bpy.types.Scene.list_index = IntProperty(name = "Index for my_list",
default = 0)
def unregister():
del bpy.types.Scene.my_list
del bpy.types.Scene.list_index
bpy.utils.unregister_class(ListItem)
bpy.utils.unregister_class(MY_UL_List)
bpy.utils.unregister_class(LIST_OT_NewItem)
bpy.utils.unregister_class(LIST_OT_DeleteItem)
bpy.utils.unregister_class(LIST_OT_MoveItem)
bpy.utils.unregister_class(PT_ListExample)
if __name__ == "__main__":
register()
That’s it guys! You now have a basic but fully functional UIList. If you’re interested in more advanced topics like custom filters you can check the templates in the text editor (templates > python > ui list).