Using UiLists in Blender

Hello fel­low Pythonistas, in this tuto­r­i­al we’ll find out how to use UILists in our scripts and add-ons. If your users need to han­dle large amounts (or vari­able) amounts of data, this is the wid­get you want.

While you could just use a loop to draw wid­gets UILists have sev­er­al ben­e­fits over cus­tom-made solu­tions, like fil­ter­ing, search­ing and man­ag­ing space cor­rect­ly (less scrolling). It’s also how it’s done through­out the UI, so you can stay con­sis­tent with the rest of Blender.

By the way, I’m assum­ing you already know your way around the bpy and Python. This is an inter­me­di­ate tutorial.

Define the properties

First we have to cre­ate some prop­er­ties to hold the data on our list first. If we are pop­u­lat­ing the list with data of our own, we’ll need to define the struc­ture of the items in the list. You don’t need to define an item if you’re using exist­ing dat­a­blocks like mate­ri­als, 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 prop­er­ty to hold the index num­ber. Let’s add them in our reg­is­ter() 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 cre­ate the actu­al wid­get by extend­ing 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 pret­ty much like any oth­er draw­ing call­back in Blender. You can use labels, check box­es, oper­a­tor but­tons, etc.

draw_item() includes sev­er­al para­me­ters we can use:

  • data: Object con­tain­ing the col­lec­tion prop­er­ty (in our case, the scene)
  • item: The cur­rent item being drawn
  • icon: Calculated icon for the cur­rent item as an inte­ger (could be one of Blender’s or an icon for a material/texture/etc. depend­ing on the item)
  • active_data: Object con­tain­ing the active prop­er­ty of the col­lec­tion (in our case, the scene too)
  • active_propname: The name of the active prop­er­ty (index). In our case, it would be “list_index”)
  • index: Index of the cur­rent item (this one is optional)

Don’t for­get to reg­is­ter your UIList sub­class­es, 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 any­thing with it? We’ll let users add, delete and move items up and down. These are the most basic oper­a­tions, you can eas­i­ly use them as a tem­plate 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 oper­a­tor is pret­ty straight­for­ward. 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 some­thing 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 dis­able the oper­a­tor but­ton in the ui automatically.

Deleting is done by call­ing remove() with the list index (which will be what­ev­er we have select­ed 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 val­ue (larg­er than the length of the list), so we always move it back one posi­tion 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 con­sid­er­a­tion because we have to make sure the index stays valid.

The move() method allows us to swap items at two dif­fer­ent index­es. We only have to fig­ure out which index to swap with. Remember we start count­ing from zero, increas­ing downwards.

Finally we call our own move_index() method to change the index while clamp­ing it between 0 and the list length.

Item Data

Showing data from a list item is easy too, just use the index. Don’t for­get 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 ful­ly func­tion­al UIList. If you’re inter­est­ed in more advanced top­ics like cus­tom fil­ters you can check the tem­plates in the text edi­tor (tem­plates > python > ui list).

All the posts you can read
TutorialsBlender, Python03.03.2021