My add-on performs diverse measurements on an arbitrary number of selected objects. The measurements results are printed to the console and saved to drive as tabulated data and .csv files respectively. I'd like to also display them in the GUI in a pop-up window.

I have not found any way to display multi-lines messages in a panel and nothing in the API (v2.81) struck me as related.

I am currently going around by showing in the panel the 5 first and last measurements (head()-like and tail()-like previews).

Would anybody have an idea on how to display tabular data of arbitrary length in a panel?

Many thanks in advance.

EDIT (in response to @RayMairlot):

The message to be displayed has n-lines (each line being a set of measurements - fixed width - for one object). I can't know beforehand how many lines there will be (depends on the number of objects measured) and row.label() only display text on a single line (\n are not recognized). Thus, I am currently building my pop-up panel with a a fixed number of row.label()statements (e.g. 10), that I populate with the 5 first and last lines of the message (measurements of the 5 first and last objects). I am not satisfied with this solution, it is just a workaround (better a preview of the results than no results displayed at all :-) ).

What I would need is either:

  • a way to dynamically add row = layout.row() and row.label(text) statements to the panel to match the number of rows to display as it is invoked
  • OR a way to display multilines messages (containing \n) in a single row.label(text)or equivalent.

I hope this clarifies.

  • $\begingroup$ Can you elaborate on why you currently have to display the 5 first and last messages instead of all the data? $\endgroup$ – Ray Mairlot Dec 15 '19 at 0:16
  • $\begingroup$ edited my original question to clarify this point. $\endgroup$ – amaizel Dec 16 '19 at 7:51
  • 2
    $\begingroup$ blender.stackexchange.com/questions/159503/… $\endgroup$ – batFINGER Dec 16 '19 at 8:37

Example using an UIList based on the answer of Create an interface which is similar to the material list box? and my modification for Displaying text inside an addon Panel to get some kind of log window as part of a popup. I've removed all superfluous operators and added a CUSTOM_OT_popup operator calling invoke_props_dialog in its invoke method as well as a check method for proper drawing:

enter image description here

#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

bl_info = {
    "name": "log-uilist-dev",
    "description": "",
    "author": "p2or, brockmann",
    "version": (0, 1),
    "blender": (2, 80, 0),
    "location": "Text Editor",
    "warning": "", # used for warning icon and text in addons panel
    "wiki_url": "",
    "tracker_url": "",
    "category": "Development"

import bpy

from bpy.props import (IntProperty,

from bpy.types import (Operator,

# -------------------------------------------------------------------
#   Operators
# -------------------------------------------------------------------

class CUSTOM_OT_actions(Operator):
    """Move items up and down, add and remove"""
    bl_idname = "custom.list_action"
    bl_label = "List Actions"
    bl_description = "Move items up and down, add and remove"
    bl_options = {'REGISTER'}

    action: EnumProperty(
            ('UP', "Up", ""),
            ('DOWN', "Down", ""),
            ('REMOVE', "Remove", ""),
            ('ADD', "Add", "")))

    list_item: StringProperty()

    def invoke(self, context, event):
        scn = context.scene
        idx = scn.custom_index

            item = scn.custom[idx]
        except IndexError:
            if self.action == 'DOWN' and idx < len(scn.custom) - 1:
                item_next = scn.custom[idx+1].name
                scn.custom.move(idx, idx+1)
                scn.custom_index += 1
                info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
                self.report({'INFO'}, info)

            elif self.action == 'UP' and idx >= 1:
                item_prev = scn.custom[idx-1].name
                scn.custom.move(idx, idx-1)
                scn.custom_index -= 1
                info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1)
                self.report({'INFO'}, info)

            elif self.action == 'REMOVE':
                info = 'Item "%s" removed from list' % (scn.custom[idx].name)
                scn.custom_index -= 1
                self.report({'INFO'}, info)

        if self.action == 'ADD':
            if self.list_item:
                item = scn.custom.add()
                item.name = self.list_item
                item.coll_id = len(scn.custom)
                scn.custom_index = len(scn.custom)-1
                info = '"%s" added to list' % (item.name)
                self.report({'INFO'}, info)

        return {"FINISHED"}

class CUSTOM_OT_clearList(Operator):
    """Clear all items of the list"""
    bl_idname = "custom.clear_list"
    bl_label = "Clear List"
    bl_description = "Clear all items of the list"
    bl_options = {'INTERNAL'}

    def poll(cls, context):
        return bool(context.scene.custom)

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)

    def execute(self, context):
        if bool(context.scene.custom):
            self.report({'INFO'}, "All items removed")
            self.report({'INFO'}, "Nothing to remove")

# -------------------------------------------------------------------
#   Drawing
# -------------------------------------------------------------------

class CUSTOM_UL_items(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        split = layout.split(factor=0.2)
        split.label(text="%d" % (index))
        #split.prop(item, "name", text="", emboss=False, translate=False, icon=custom_icon)
        split.label(text=item.name) # avoids renaming the item by accident

    def invoke(self, context, event):

class CUSTOM_OT_popup(Operator):
    bl_idname = "custom.call_popup"
    bl_label = "Log Window"
    bl_options = {'REGISTER'}

    def poll(cls, context):
        return True

    def execute(self, context):
        return {'FINISHED'}

    def check(self, context):
        return True

    def invoke(self, context, event):
        return context.window_manager.invoke_props_dialog(self, width=500)

    def draw(self, context):
        layout = self.layout
        scn = bpy.context.scene

        rows = 7
        row = layout.row()
        row.template_list("CUSTOM_UL_items", "", scn, "custom", scn, "custom_index", rows=rows)

        row = layout.row()
        add = row.operator(CUSTOM_OT_actions.bl_idname, icon="ADD", text="Add Log Line")
        add.list_item = "Error %d" % (len(scn.custom))
        add.action = 'ADD'

        row = layout.row().operator("custom.clear_list", icon="X")
        row = layout.row()

# -------------------------------------------------------------------
#   Collection
# -------------------------------------------------------------------

class CUSTOM_objectCollection(PropertyGroup):
    #name: StringProperty() -> Instantiated by default
    coll_type: StringProperty()
    coll_id: IntProperty()

# -------------------------------------------------------------------
#   Register & Unregister
# -------------------------------------------------------------------

classes = (

def register():
    from bpy.utils import register_class
    for cls in classes:

    # Custom scene properties
    bpy.types.Scene.custom = CollectionProperty(type=CUSTOM_objectCollection)
    bpy.types.Scene.custom_index = IntProperty()

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):

    del bpy.types.Scene.custom
    del bpy.types.Scene.custom_index

if __name__ == "__main__":

Note: the naming of the classes and variables might be off a little.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.