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 tuto­r­i­al.

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 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() 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: 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 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(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() 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.

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 down­wards.

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).

Tutorials(Blender, Python)Last updated 07.08.2020
All the posts you can read

26 Comments

  1. maes thierry(5 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(5 years ago)

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

  2. iq bwv(5 years ago)

    thanks

  3. Marvin K. Breuer(5 years ago)

    The final code greate some errors: LIST_OT_DeleteItem.…

    1. Diego Gangl(5 years ago)

      Fixed, thanks for spot­ting that!

      1. Marvin K. Breuer(5 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(5 years ago)

        Sorry 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(3 years ago)

    Is the CollectionProperty doing the same job of PointerPropery, I have seen sev­er­al exam­ples and they seem inter­changable, do you mind elab­o­rat­ing?

    1. Diego Gangl(3 years ago)

      A PointerPropery points to a sin­gle instance of a PropertyGroup, while a CollectionProperty 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 CollectionProperty.

  5. Rombout Versluijs(2 years 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(2 years 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)

  6. Rombout Versluijs(6 months ago)

    Been a while and was look­ing for info about point­er­groups and point­er­prop­er­ties and all.
    PS there was anoth­er issue, the StringProperty random_property in PT_ListExample. It should be random_prop as you declared the StringProperty in the begin­ning

    1. Diego Gangl(6 months ago)

      Fixed, Thanks for men­tion­ing!

  7. Jj(1 month ago)

    Hi

    I tried this under 2.83 While it works the list­ings do not show up any names, any added item int he list comes out name­less

    1. Diego Gangl(1 month ago)

      Hi, looks like I for­got to update this one for 2.8x. Fixed all the code, the only change is that the labels’ text has to be passed as a key­word argu­ment like this: layout.label(text=item.name, icon = custom_icon).
      Thanks for let­ting me know!

  8. jj(1 month ago)

    Hi

    That works thank you!

  9. sarka(3 weeks ago)

    Hi Diego
    Thanks for this tuto­r­i­al, sim­ple and well explained 🙂
    I would like to ini­tial­ize the list with data, but I can’t find how to do it.
    For exam­ple, from a vari­able:
    MyVar = [‘azer­ty’, ‘qsd­fgh’, ‘wxcvbn’, ‘poiuyt’, ‘mlkjhg’]
    Can you help me?

    Thanks

    1. Diego Gangl(3 weeks ago)

      Hi Sarka, you would need to ini­tial­ize them in the CollectionProperty you use to hold the val­ues for the list. You could do this in the reg­is­ter() func­tion of your addon, or at any point after you have reg­is­tered the col­lec­tion prop­er­ty into a vari­able (some­thing like bpy.types.Scene = my_collection). Then you can call my_collection.add(), which returns a new item in the col­lec­tion, and set the dif­fer­ent prop­er­ties there.

  10. Sarka(3 weeks ago)

    Thank you for your quick response Diego.
    I will test and I will come back to you if I can’t 🙂
    I’m start­ing in blender pro­gram­ming 😉

  11. Sarka(3 weeks ago)

    I used your exam­ple to test.
    In the Register func­tion, the CollectionProperty is declared with the line “bpy.types.Scene.my_list = CollectionProperty(type = ListItem)”. I added the line “bpy.types.Scene.my_list.add()”, but I get the error “AttributeError: ‘tuple’ object has no attribute ‘add’ ”.
    Sorry i’m very new­bie 🙁

  12. Sarka(3 weeks ago)

    Ok, i found my error 🙂

    Thanks a lot for your help Diego 😉

    1. Diego Gangl(3 weeks ago)

      Ah you got it faster than I could reply.
      Glad to hear! Welcome to Blender script­ing 🙂

  13. Passion3D(3 weeks ago)

    Hi Diego
    I fol­lowed your tuto­r­i­al and the direc­tions you gave to Sarka.
    In Scripting mode, every­thing works fine, but when I acti­vate my addon I get an error, When the RenderSettings_Init() func­tion is exe­cut­ed
    Here’s the code:
    #=========================
    def RenderSettings_Init():
    #=========================
    bpy.context.scene.my_list.clear()
    bpy.context.scene.active_index = 0
    for f in os.listdir(rendersettings_path):
    if f[-3::] == “dat”:
    item = bpy.context.scene.my_list.add()
    item.name = bpy.path.display_name(f).title()
    item.pathname = rendersettings_path

    #==================
    def reg­is­ter():
    #==================
    for cls in class­es:
    bpy.utils.register_class(cls)
    bpy.types.Scene.my_list = CollectionProperty(type = ListItem)
    bpy.types.Scene.list_index = IntProperty(name = “Index for my_list”, default = 0)
    bpy.types.Scene.active_index = IntProperty(name = “Active set­tings”, default = 0)
    RenderSettings_Init()

  14. Passion3D(3 weeks ago)

    Here trace­back:
    Traceback (most recent call last):
    File “/usr//share/bforartists/2.91/scripts/modules/addon_utils.py”, line 382, in enable
    mod.register()
    File “/home/passion3d/.config/bforartists/2.91/scripts/addons/RenderSettings/__init__.py”, line 465, in reg­is­ter
    RenderSettings_Init()
    File “/home/passion3d/.config/bforartists/2.91/scripts/addons/RenderSettings/__init__.py”, line 289, in RenderSettings_Init
    bpy.context.scene.my_list.clear()
    AttributeError: ‘_RestrictContext’ object has no attribute ‘scene’

    1. Diego Gangl(3 weeks ago)

      Hi! hmm I see. The con­text in the reg­is­ter func­tion does­n’t include the scene yet (makes sense actu­al­ly). The oth­er option is to “lazy load” the list, in oth­er words, ini­tial­ize it when it is drawn for the first time. There’s two ways you can do that: you can add a flag (set to True ini­tial­ly) or you can check if the list is emp­ty. In either case you would add this as this at the top of the draw() func­tion in the UIList class. Something like:

      if first_run:
      RenderSettings_Init()
      first_run = False

  15. Passion3D(3 weeks ago)

    Hi
    Ok thanks Diego 😉

Leave a Reply

Your email address will not be published.