Using UiLists in Blender

17.09.2014 @ Tutorials(Blender, Python)

Hel­lo fel­low Python­istas, 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 tuto­r­i­al.

Update May-2018:  I’ve updat­ed the code to a much clean­er ver­sion. I also added a check in the delete oper­a­tor to make sure the index is low­er than the size of the list (total­ly for­got about it the first time!).

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 tex­tures.

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 Col­lec­tion Prop­er­ty that will hold our list data, and an Inte­ger prop­er­ty to hold the index num­ber. Let’s add them in our reg­is­ter() func­tion.

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: Cal­cu­lat­ed 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 option­al)

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(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() func­tion.

        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 oper­a­tors.

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 auto­mat­i­cal­ly.

Delet­ing is done by call­ing remove() with the list index (which will be what­ev­er we have select­ed in the ui).

Final­ly 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'}

Mov­ing 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. Remem­ber we start count­ing from zero, increas­ing down­wards.

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

Item Data

Show­ing 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(item.name, icon = custom_icon)

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'
            layout.label("", 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_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 = 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

11 Comments

  1. maes thierry(4 years ago)

    Thanks this tuto­r­i­al looks great. Only sug­ges­tion, maybe join the fin­ished script to see it work before decript/learn.
    regards,


    tmaes

    1. Diego Gangl(4 years ago)

      Hey, good idea! I’ve post­ed the a script at the end.

  2. iq bwv(4 years ago)

    thanks

  3. Marvin K. Breuer(3 years ago)

    The final code greate some errors: LIST_OT_DeleteItem.…

    1. Diego Gangl(3 years ago)

      Fixed, thanks for spot­ting that!

      1. Marvin K. Breuer(3 years ago)

        I use blender 2.76b.
        when i run the code in the texte­d­i­tor i have some trou­ble with the props.
        And it don´t want to reg­is­ter the my_list…
        Can you check this?

      2. Diego Gangl(3 years ago)

        Sor­ry about that. I don’t know why the edi­tor has been eat­ing so many lines and idents. It’s work­ing now, I also fixed the UI labels.

  4. Max Villafranca(1 year ago)

    Is the Col­lec­tion­Prop­er­ty doing the same job of Point­er­Prop­ery, I have seen sev­er­al exam­ples and they seem inter­changable, do you mind elab­o­rat­ing?

    1. Diego Gangl(1 year ago)

      A Point­er­Prop­ery points to a sin­gle instance of a Prop­er­ty­Group, while a Col­lec­tion­Prop­er­ty holds mul­ti­ple instances. If you’re only going to have one instance they could prob­a­bly be inter­change­able but in the case of a list you have to use a Col­lec­tion­Prop­er­ty.

  5. Rombout Versluijs(7 months ago)

    PS how would we imple­ment the dou­ble or ctrl click func­tion to update with­in the list? I find that works a bit nicer or more con­ve­nient

    1. Diego Gangl(7 months ago)

      You would have to use a layout.prop() with­out emboss instead of a label: layout.prop(item, ‘name’, text=”, emboss=False, icon=icon)

Leave a Reply

Your email address will not be published.