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(bpy.types.PropertyGroup):
    """ Group of properties representing an item in the list """

    name = prop.StringProperty(
           name="Name",
           description="A name for this item",
           default="Untitled")

    random_prop = 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 = prop.CollectionProperty(type = ListItem)
    bpy.types.Scene.list_index = prop.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(bpy.types.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(item.name, icon = custom_icon)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label("", 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(bpy.types.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(bpy.types.Operator):
    """ Delete the selected item from the list """

    bl_idname = "my_list.delete_item"
    bl_label = "Deletes an item"

    @classmethod
    def poll(self, context):
        """ Enable if there's something in the list """
        return len(context.scene.my_list) > 0

    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        list.remove(index)

        if index > 0:
            index = index - 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(bpy.types.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(self, context):
        """ Enable if there's something in the list. """

        return context.scene.my_list > 0


    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 = 0

        if self.direction == 'UP':
            new_index = index - 1
        elif self.direction == 'DOWN':
            new_index = index + 1

        new_index = max(0, min(new_index, list_length))
        index = new_index


    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        if self.direction == 'DOWN':
            neighbor = index + 1
            queue.move(index,neighbor)
            self.move_index()

        elif self.direction == 'UP':
            neighbor = index - 1
            queue.move(neighbor, index)
            self.move_index()
        else:
            return{'CANCELLED'}

        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 len(scene.my_list) > 0:
    item = scene.my_list[scene.list_index]

    row = layout.row()
    row.prop(item, "name")
    row.prop(item, "random_property")

The Final code

import bpy
import bpy.props as prop


class ListItem(bpy.types.PropertyGroup):
    """ Group of properties representing an item in the list """

    name = prop.StringProperty(
           name="Name",
           description="A name for this item",
           default="Untitled")

    random_prop = prop.StringProperty(
           name="Any other property you want",
           description="",
           default="")
           
           

class MY_UL_List(bpy.types.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(item.name, icon = custom_icon)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label("", icon = custom_icon)


class LIST_OT_NewItem(bpy.types.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(bpy.types.Operator):
    """ Delete the selected item from the list """

    bl_idname = "my_list.delete_item"
    bl_label = "Deletes an item"

    @classmethod
    def poll(self, context):
        """ Enable if there's something in the list """
        return len(context.scene.my_list) > 0

    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        list.remove(index)

        if index > 0:
            index = index - 1

        return{'FINISHED'}


class LIST_OT_MoveItem(bpy.types.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(self, context):
        """ Enable if there's something in the list. """

        return context.scene.my_list > 0


    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 = 0

        if self.direction == 'UP':
            new_index = index - 1
        elif self.direction == 'DOWN':
            new_index = index + 1

        new_index = max(0, min(new_index, list_length))
        index = new_index


    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        if self.direction == 'DOWN':
            neighbor = index + 1
            queue.move(index,neighbor)
            self.move_index()

        elif self.direction == 'UP':
            neighbor = index - 1
            queue.move(neighbor, index)
            self.move_index()
        else:
            return{'CANCELLED'}

        return{'FINISHED'}(context.scene.my_list) > 0

    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        list.remove(index)

        if index > 0:
            index = index - 1

        return{'FINISHED'}


class LIST_OT_MoveItem(bpy.types.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(self, context):
        """ Enable if there's something in the list. """

        return context.scene.my_list > 0


    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 = 0

        if self.direction == 'UP':
            new_index = index - 1
        elif self.direction == 'DOWN':
            new_index = index + 1

        new_index = max(0, min(new_index, list_length))
        index = new_index


    def execute(self, context):
        list = context.scene.my_list
        index = context.scene.list_index

        if self.direction == 'DOWN':
            neighbor = index + 1
            queue.move(index,neighbor)
            self.move_index()

        elif self.direction == 'UP':
            neighbor = index - 1
            queue.move(neighbor, index)
            self.move_index()
        else:
            return{'CANCELLED'}

        return{'FINISHED'}




class PT_ListExample(bpy.types.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 len(scene.my_list) > 0:
            item = scene.my_list[scene.list_index]

            row = layout.row()
            row.prop(item, "name")
            row.prop(item, "random_property")


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 = prop.CollectionProperty(type = ListItem)
    bpy.types.Scene.list_index = prop.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).

  • maes thierry

    Thanks this tutorial looks great. Only suggestion, maybe join the finished script to see it work before decript/learn.
    regards,


    tmaes

    • Hey, good idea! I’ve posted the a script at the end.

  • iq bwv

    thanks

  • Marvin K. Breuer

    The final code greate some errors: LIST_OT_DeleteItem….

    • Fixed, thanks for spotting that!

      • Marvin K. Breuer

        I use blender 2.76b.
        when i run the code in the texteditor i have some trouble with the props.
        And it don´t want to register the my_list…
        Can you check this?

        • Sorry about that. I don’t know why the editor has been eating so many lines and idents. It’s working now, I also fixed the UI labels.