If you're reading this, we've just migrated servers! If anything looks broken please email dion@thinkmoult.com :)

[Blender] Create your first Blender add-on

24

Comments

  • @Coen you can use the update function to run once a change has been made. See https://docs.blender.org/api/current/bpy.props.html#update-example

    Coen
  • edited December 2021

    it is common to, instead of defining the properties in the UI, define properties as a separate PropertyGroup. This property group is, when installing the add-on, registered on a certain namespace (for example the 'scene'), so you can access it globally. So for you that could be:

    from bpy.props import BoolProperty, StringProperty, IntProperty, EnumProperty
    from bpy.types import PropertyGroup
    
    class ExcelProperties(PropertyGroup):
        ifc_product: BoolProperty(name="IfcProduct", default=False)
        ifc_building_story: BoolProperty(name="IfcBuildingStory", default=False)
        name: BoolProperty(name="Name", default=False)
        excel_path: stringProperty(name="Excel path", subtype='FILE_PATH')
    

    Afterwards you register the property in some namespace:

    def register():
        bpy.types.Scene.excel_properties = bpy.props.PointerProperty(type=ExcelProperties)
    

    Now, in the UI or in the Operator, you can access the properties in the execute/draw functions:

    UI:

    def draw(self, context):
        excel_properties = context.scene.excel_properties
        row = layout.row(align=True)
        row.prop(excel_properties, "ifc_product")
    

    Operator:

    def execute(self, context):
        excel_properties = context.scene.excel_properties
        if excel_properties.ifc_product:
            do_something()
    

    It's quite nice because you can separate the properties from the ui and from the functions. BlenderBIM does this nicely (I use this too for my own add-ons, it might also be the standard structure, I don't know), by making the following structure for each package:
    package_name:

    • ui.py
    • prop.py
    • operator.py
    • __init__.py (for registering all the classes from the three python files.

    You can for example look at the GIS package in the BlenderBIM add-on: https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/blenderbim/blenderbim/bim/module/gis (the other packages are built the same way but more extensive so this is a simple example)

    GorgiousCoenMassimo
  • @LaurensJN
    @vpajic

    Thanks for the help.
    I use this method now, in the BlenderBIMXLSXPanel in the draw method put a row and colums for the ui

    class BlenderBIMXLSXPanel(bpy.types.Panel):
        """Creates a Panel in the Object properties window"""
        bl_label = "BlenderBIM .xlsx"
        bl_idname = "OBJECT_PT_blenderbiMxlsxpanel"  # this is not strictly necessary
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
        bl_category = "Tools"
    
        def draw(self, context):
    
            scene = context.scene
            layout = self.layout
            col = layout.column(align=True)
            row = col.row(align=True)
    
    
    
            col.prop(scene, "my_ifcproduct")
    
    

    then in register

    def register():
    
    
        bpy.types.Scene.my_ifcproduct = bpy.props.BoolProperty(name="IfcProduct",description="Export IfcProduct",default = True)
    
    

    then in the WritetoXLSX class I can call the checkbox.

    if context.scene.my_ifcproduct == True:
                ifc_dictionary['IfcProduct'] = ifc_product_type_list
    

    I think I need to group some of them, start to get a bit crowdy. And I hardcoded the sum formulas in the header, they don't work anymore. Because the colums move when the user decides to check and uncheck some of the exports needed. Need to find a method for this.

  • I like this layout, any suggestions for a good intuive UI are very welcome. GUI is not my strong suit.

  • My two cents, @Coen i think last ui is nice, with the possibility of hiding the checkboxes of the same group

    Coen
  • edited December 2021

    You can create sub-panels :

    See the last part of that answer : https://blender.stackexchange.com/a/155517/86891 or the very minimal amount of code there : https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/UI_API#Sub_Panels

    note you can add a class attribute bl_order = 0/1/2/3 etc if you want to force the ordering of the sub-panels. see https://docs.blender.org/api/current/bpy.types.Panel.html#bpy.types.Panel.bl_order

    And use
    bl_options = {'DEFAULT_CLOSED'} for the sub panels to be folded by default

    You can try to add layout.prop(scene, "my_prop", toggle=True) if it's a boolean to display as a toggle rather than a checkbox. Matter of taste https://blender.stackexchange.com/a/117791/86891

    Coen
  • How do you bundle a Blender BIM add-on in a zip if the add-on has third party python dependencies?

  • Hmm I've actually never done that. Theoretically it should be as easy as copy / pasting the third party module and all its dependencies in a folder in the zip, and then access it like you would any other module you created in the addon folder. Blenderbim does that in libs/site/packages

    Coen
  • These are the dependencies as where they are stored:

    openpyxl 3.0.9 C:\Program Files\Blender Foundation\Blender 3.0\3.0\python\lib\openpyxl\__init__.py
    pandas 1.3.5 C:\Program Files\Blender Foundation\Blender 3.0\3.0\python\lib\pandas\__init__.py
    xlsxwriter 3.0.2 C:\Program Files\Blender Foundation\Blender 3.0\3.0\python\lib\xlsxwriter\__init__.py
    

    I found this SO post that there are numerous ways of importing dependencies.

    I copied pasted the modules openpyxl, pandas and xlsxwriter to a new relative folder
    BlenderBIMOpenOfficeXML\lib\site\packages

    Now I started reading that thread on Stack Overflow.

    and I got completely lost...

  • edited January 2022

    You should be able to use from BlenderBIMOpenOfficeXML.lib.site.packages import openpyxl.
    Note python modules should be named in lower case, and I think it's not mandatory but you can use snake_case like you would regular python modules.

    Coen
  • I have never done that either, but the BlenderBIM add-on does that with a great deal of packages:
    See https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/blenderbim/blenderbim/init.py
    If you open any BlenderBIM add-on folder, you see all packages can be found under /libs/site/packages. Therefore this line probably does the trick:

    import site
    site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
    

    If I am wrong, probably @Moult can explain this better :)

    MeetlatCoen
  • edited January 2022

    I used the method @LaurensJN described.
    I added this to my script

    import site
    site.addsitedir(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libs", "site", "packages"))
    

    Then copy pasted the modules from the python site-packages to my own folder called with the following folder struture libs>sites>packages ( I have a vague memory of reading this somewhere in Blender documentation too)
    I placed the openpyxl, pandas and xlsxwriter modules in there
    did a clean install of Blender 3.0 and BlenderBIM on my old brick laptop.

    validated where the imports were coming from with [module_name_here].__file__ to see where they are located.
    And it works! :-D
    They are now placed in

    C:\Users\C.C.J. Claus\AppData\Roaming\Blender Foundation\Blender\3.0\scripts\addons\libs\site\packages\openpyxl\__init__.py
    C:\Users\C.C.J. Claus\AppData\Roaming\Blender Foundation\Blender\3.0\scripts\addons\libs\site\packages\pandas\__init__.py
    C:\Users\C.C.J. Claus\AppData\Roaming\Blender Foundation\Blender\3.0\scripts\addons\blenderbim\libs\site\packages\xlsxwriter\__init__.py
    

    When I try to install the add-on Linux Ubuntu I get a specific pandas error. Going to look at that another time.

    LaurensJNGorgious
  • edited November 2022

    Has this been documented somewhere? This thread is gold.

    EDIT: Never mind, I should read the thread more carefully, found it here:

    https://wiki.osarch.org/index.php?title=Create_your_first_Blender_add-on

    • ui.py
    • prop.py
    • operator.py
    • init.py (for registering all the classes from the three python files.

    Is this the 'default' stander add-on convention for Blender? all the BIM modules seem to be structured in this way

  • @Coen oh wow I forgot I wrote that :)

    Nope there is AFAIK no convention on how to structure an add-on in Blender, apart from the mandatory dictionary with bl_info somewhere in the file if it is a single script or in __init__.py if it's a multi-file addon.

    That being said as a general rule in programming if a class or a module is doing more than one thing, it's a good idea to split it up, and keep dependencies between modules to a minimum. Most beginner classes / tutorials will tell you about the SOLID principle which is an interesting mantra to try to follow, at least until you know when it makes sense to break the rules. When you'll know, you'll know. :)

    The ui / prop / operator / tool separation makes sense because usually data storage, data manipulation, and data display can be done by independent systems so there is no reason they should be scripted in the same place. The great benefit is that it's easier to test, easier to debug, easier to extend and easier to swap when a new technology for either module emerges.

    Coentheoryshaw
  • @Gorgious

    Nope there is AFAIK no convention on how to structure an add-on in Blender, apart from the mandatory dictionary with bl_info somewhere in the file if it is a single script or in init.py if it's a multi-file addon.

    How do I run a live development environment when I have three init.py, prop.py, operator.py and ui.py files from VS Code?
    I only need to register the __init
    .py in Blender somewhere?

  • @vpajic said:
    @Coen you can use the update function to run once a change has been made. See https://docs.blender.org/api/current/bpy.props.html#update-example

    In which file should I put this update code? or just place it a sepertae python file with the add-on?

  • edited February 2023

    Hey @Coen do you have it over on github or such ? I can try giving you hints. I think there are fundamentally 3 types of addon strucutres.

    If your addon does a single thing, you can write all your code in a single file and install that file as an addon.

    If your addon does a few thing, you want to have a __init__.py file with the mandatory addon dictionary so Blender knows how to set it up. Then you can have in the same folder your other features in different python files. You can import them with a simple from . import my_other_file.

    If your addon is expansive, you can structure it with folders, like you would files in your computer. You can then navigate through them with import my_addon_directory.my_secondary_directory.my_other_file. You still need to have a __init__.py file with the mandatory dictionary.

    A lot of blender addons are available over on github, you can study how they're structure and see what you want to go for : https://github.com/topics/blender-addon

    or even the official blender addons https://github.com/blender/blender-addons

    AcetheoryshawCoenChubbyQuark
  • @Gorgious

    do you have it over on github or such ?

    Yes, I think i've added you to my private repo, it's a work in progress of the blenderbim spreadsheet add-on completely rewritten with the ifcopenshell.api

    What I want to do is to just change the code of the add-on from vs code and see instant changes in the ui of blender.

  • AFAIK there are several methods to achieve that, I've only ever used one and never had any problem with it. It's not that hard to setup, it uses the Vscode development addon by Jacques Lucke one of the devs of Geometry Nodes. Here's a good explanation how to set it up https://b3d.interplanety.org/en/using-microsoft-visual-studio-code-as-external-ide-for-writing-blender-scripts-add-ons/

    Coen
  • Thanks, this was suprisingly easy, however I always get lost in the seas of documentation with Blender
    What I did was this:
    I edited these settings:

    each time I save in VS code the add-on is reloaded

    result

    Gorgious
  • Awesome !! I really hate registering / unregistering classes. That's why I pretty much always use this cool module by Jacques Lucke to auto load / unload blender classes. https://gist.github.com/JacquesLucke/11fecc6ea86ef36ea72f76ca547e795b

    Copy / paste the file in your addon root folder, boom. Everything is automatically registered. One thing you do absolutely need to do, if you're using subfolders, is add a file called __init__.py in every subfolder otherwise the utility won't be able to scan it and the classes won't register. It doesn't matter if the file is blank. It also automatically runs any function in your files named register at startup and unregister when you disable the addon, so you still can define custom behaviour on register / unregister.

    Take a look at one of my addons if you want to see how it works. https://github.com/Gorgious56/asset_browser_utilities

    Here for instance I don't register my menus, I only append them to the asset browser header. The rest is taken care of by the utility. https://github.com/Gorgious56/asset_browser_utilities/blob/master/core/ui/menu/main.py#L64-L69

    Coen
  • @Gorgious

    How do you acces the values in operator.py if you have defined them in prop.py?

  • If they're in the same directory, you can do from . prop import my_prop

    Coen
  • I think I am doing something wrong, I have the following in prop.py

    import bpy
    from bpy.types import Scene
    
    from bpy.props import BoolProperty
    
    def register():
        Scene.property_ifcproduct = BoolProperty(name="IfcProduct",description="Export IfcProduct",default=True)
    def unregister():
        del Scene.property_ifcproduct
    

    then in ui.py I have:

    import bpy
    from bpy.types import Panel
    
    class BlenderBIMSpreadSheetPanel(Panel):
        bl_label = "BlenderBIM Spreadsheet"
        bl_idname = "OBJECT_PT_blenderbimxlsxpanel"
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
        bl_category = "Tools"
    
        def draw(self, context):
            row = self.layout.row()
            row.prop(context.scene, 'property_ifcproduct')
    
    def register():
        bpy.utils.register_class(BlenderBIMSpreadSheetPanel)
    
    def unregister():
        bpy.utils.unregister_class(BlenderBIMSpreadSheetPanel)
    
    

    Now I want to acces this in operator.py

    import bpy
    
    from . prop import property_ifcproduct
    

    The python files are all in the same folder.
    When I try from . prop import property_ifcproduct the add-on fails to load from VS code, it says:

    RuntimeError: Error: Traceback (most recent call last):
      File "c:\Program Files\Blender Foundation\Blender 3.4\3.4\scripts\modules\addon_utils.py", line 333, in enable
        mod = __import__(module_name)
      File "C:\Users\cclaus\AppData\Roaming\Blender Foundation\Blender\3.4\scripts\addons\blenderbim_spreadsheet\__init__.py", line 20, in <module>
        from . import ui, prop, operator
      File "C:\Users\cclaus\AppData\Roaming\Blender Foundation\Blender\3.4\scripts\addons\blenderbim_spreadsheet\operator.py", line 4, in <module>
        print (bpy.props.property_ifcproduct)
    AttributeError: module 'bpy.props' has no attribute 'property_ifcproduct'
    
  • edited February 2023

    Well you can't do that in Python, could you elaborate on what you're trying to do ? You can access the property in the operator from the context passed in the execute method. context.scene.property_ifcproduct

    Coen
  • Thanks for answering:
    This I what I want to do:
    I want to create a checkbox in the ui, but I want to use the name of the checkbox in operator.py rather then the true or false value. My aim is to create only the 'hardcoded' variables in the ui.py.
    I made this littel UI:

    in ui.py:

    import bpy
    from bpy.types import Panel
    
    from . import  prop, operators
    
    class BlenderBIMSpreadSheetPanel(Panel):
        bl_idname = "OBJECT_PT_BlenderBIMSpreadSheet_panel"
        bl_label = "BlenderBIM Spreadsheet"
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
        bl_category = "BlenderBIM | Spreadsheet"
    
        def draw(self, context):
    
            ifc_properties = context.scene.ifc_properties
            layout = self.layout
            layout.label(text="General")
            box = layout.box()
            row = box.row()
            row.prop(ifc_properties, "my_ifcproduct")
            row = box.row()
            row.prop(ifc_properties, "my_ifcbuildingstorey")
            layout.operator("export.tospreadsheet")
    
    def register():
        bpy.utils.register_class(BlenderBIMSpreadSheetPanel)
    
    def unregister():
        bpy.utils.unregister_class(BlenderBIMSpreadSheetPanel)
    

    in prop.py:

    import bpy
    from bpy.types import Scene
    from bpy.props import BoolProperty, StringProperty
    
    class IFCProperties(bpy.types.PropertyGroup):
        prop_ifc_product: StringProperty(name="My String Property")
    
        my_ifcproduct: bpy.props.BoolProperty(name="IfcProduct",description="Export IfcProduct",default=True)
        my_ifcbuildingstorey: bpy.props.BoolProperty(name="IfcBuildingStorey",description="Export IfcBuildingStorey",default = True)
    
    
    def register():
        bpy.utils.register_class(IFCProperties)
        bpy.types.Scene.ifc_properties = bpy.props.PointerProperty(type=IFCProperties)
    
    def unregister():
        bpy.utils.unregister_class(IFCProperties)
        del bpy.types.Scene.ifc_properties
    

    in operator.py

    import bpy
    
    from . import prop
    
    class ExportToSpreadSheet(bpy.types.Operator):
        bl_idname = "export.tospreadsheet"
        bl_label = "Export to Spreadsheet"
    
        def execute(self, context):
            #print("Hello, world!")
            #print (context.scene)
            #ifc_properties = context.scene
            my_bool_name = context.scene.ifc_properties.my_ifcproduct #.prop_ifc_product)
    
            # = bpy.types.Scene.bl_rna.properties["my_ifcproduct"].name
    
     )
            return {'FINISHED'}
    
    def register():
        bpy.utils.register_class(ExportToSpreadSheet)
    
    def unregister():
        bpy.utils.unregister_class(ExportToSpreadSheet)
    

    I would like the variable my_bool_name just to return IfcProduct as a dumb string in operator.py

    Ace
  • Oh, okay, I understand. You may be going a little too fast here, but it's possible. FWIW I don't think it's that bad to hardcode values, as long as you can replace them easily with a "Search & Replace" across all your files.

    You need to access a barely documented construct through bl_rna.

    I didn't test this but you should be able to access it with my_bool_name = context.scene.bl_rna.properties["my_ifcproduct"].name. You can explore dir(context.scene.bl_rna.properties["my_ifcproduct"]), it lets you fetch some information about the property definition.

    Coen
  • @Gorgious

    Thanks, I think I've exhaused every option, read the documentation thoroughfully but to no avail. I eventually came up with this workaround.
    The idea now is just to hardcode the variables only in prop.py

    import bpy
    from bpy.types import Scene
    from bpy.props import BoolProperty, StringProperty
    
    prop_ifcproduct = 'IfcProduct'
    
    class IFCProperties(bpy.types.PropertyGroup):
        my_ifcproduct: bpy.props.BoolProperty(name=prop_ifcproduct,description="Export IfcProduct",default=True)
        my_ifcbuildingstorey: bpy.props.BoolProperty(name="IfcBuildingStorey",description="Export IfcBuildingStorey",default = True)
    
    def register():
        bpy.utils.register_class(IFCProperties)
        bpy.types.Scene.ifc_properties = bpy.props.PointerProperty(type=IFCProperties)
    
    def unregister():
        bpy.utils.unregister_class(IFCProperties)
        del bpy.types.Scene.ifc_properties
    

    in operator.py

    import bpy
    from . import prop
    
    class ExportToSpreadSheet(bpy.types.Operator):
        bl_idname = "export.tospreadsheet"
        bl_label = "Export to Spreadsheet"
    
        def execute(self, context):
    
            print (prop.prop_ifcproduct)
    
            return {'FINISHED'}
    
    def register():
        bpy.utils.register_class(ExportToSpreadSheet)
    
    def unregister():
        bpy.utils.unregister_class(ExportToSpreadSheet)
    

    Might not be good practice and a bit risky maybe, but it works and I only have one place where I define variables

  • What would be best practice for filling the UI in blender with data coming from an IFC file?
    For example, I want to create a dropdown classification menu, so end users can select which classification they would like to export. Because there can be more than 1 classificatoin system:

Sign In or Register to comment.