I was watching this video on making a custom UI tab using Python, and in the line from bpy.types import Menu, Panel, UIList
, I see the names of objects that look like they can all be used to make different UI elements. Does bpy.types
contain all the objects that people use when they want to make custom UI elements?
2 Answers
Designing an UI or Add-on is basically a combination of supplying Properties and the inheritance of built-in Type classes (Panel, Operator, Menu etc.).
Properties
Start by defining your Properties first. Properties are basically 'data types', they can be displayed in the UI for basic user interaction and are accessible from almost anywhere. To populate a complete list use python's dir()
method on bpy.props
:
Appearance
- Define a
BoolProperty
for a 'Checkbox' - Define a
FloatProperty
orIntProperty
to get a 'Slider' - Define a
StringProperty
for all different kinds of 'Character Input' or 'File Paths' - Define an
EnumProperty
to get a 'Dropdown Menu' or 'Radio Selection' - Define a
FloatVectorProperty
orIntVectorProperty
for compound values eg. 'Color Pickers', 'Location Coordinates', 'Velocity Vectors', 'Matrices' etc.
...
Property Definition
from bpy.props import (StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
EnumProperty,
)
my_bool: BoolProperty(
name="Enable or Disable",
description="Bool property",
default = False
)
my_int: IntProperty(
name = "Set a value",
description="Integer property",
default = 23,
min = 10,
max = 100
)
...
Note that as of Blender 2.8x, properties should be assigned to variables using a single colon :
instead of the usual assignment operator =
like in Blender 2.7x or older versions of Blender.
Types
The contents of bpy.types
are class templates built for inheritance.
Panel
Panels are everywhere in Blender so it's the most basic element of the user interface. Where the panel is going to be used is defined by bl_space_type
. Blenders interface is 'context sensitive' so you can define bl_context
to get the Panel in one respective Mode (Object Mode, Edit Mode etc.). Further reading: How can I add a checkbox in the tools UI?
class HelloWorldPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_hello_world"
bl_label = "Hello World"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
self.layout.label(text="Hello World")
bpy.utils.register_class(HelloWorldPanel)
Sub Panels
As of Blender 2.8x we can have sub panels by assigning a certain panel (parent) to bl_parent_id
:
import bpy
class HelloWorldPanel:
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tools"
bl_options = {"DEFAULT_CLOSED"}
class HELLO_PT_World1(HelloWorldPanel, bpy.types.Panel):
bl_idname = "HELLO_PT_World1"
bl_label = "Panel 1"
def draw(self, context):
layout = self.layout
layout.label(text="This is the main panel.")
class HELLO_PT_World2(HelloWorldPanel, bpy.types.Panel):
bl_parent_id = "HELLO_PT_World1"
bl_label = "Panel 2"
def draw(self, context):
layout = self.layout
layout.label(text="First Sub Panel of Panel 1.")
class HELLO_PT_World3(HelloWorldPanel, bpy.types.Panel):
bl_parent_id = "HELLO_PT_World1"
bl_label = "Panel 3"
def draw(self, context):
layout = self.layout
layout.label(text="Second Sub Panel of Panel 1.")
classes = (
HELLO_PT_World1,
HELLO_PT_World2,
HELLO_PT_World3
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
See also: Text Editor > Templates > Python > UI Panel.
Operator
The Operator is the most important bit to understand. You can display 'operators' as Buttons and once registered and you can call it from everywhere via bpy.ops.IDNAME()
. That's also the way blender is designed, all real Buttons are 'operators' under the hood, mostly written in C but then exposed to python. See also: Text Editor > Templates > Python > Operator....
import bpy
class WM_OT_HelloWorld(bpy.types.Operator):
bl_idname = "wm.hello_world"
bl_label = "Minimal Operator"
def execute(self, context):
# Report "Hello World" to the Console
self.report({'INFO'}, "Hello World")
return {'FINISHED'}
bpy.utils.register_class(WM_OT_HelloWorld)
# test call the operator
bpy.ops.wm.hello_world()
The Operator class template comes with predefined methods, in fact poll
, invoke
, execute
, draw
, modal
, and cancel
which can be used along custom properties for all different kinds of operations and also to provide user interaction, see: Is there an addon for renaming an object with a keyboard shortcut? for a complete example of an operator.
import bpy
class WM_OT_HelloWorld(bpy.types.Operator):
bl_idname = "wm.hello_world"
bl_label = "Minimal Operator"
bl_options = {'REGISTER'}
# Operator user properties, should be assigned using a single colon :
# instead of using an equal sign = in Blender 2.8
report_flag: bpy.props.BoolProperty(
name = "Report",
default = True)
@classmethod # Will never run when poll returns false
def poll(cls, context):
return context.object
def invoke(self, context, event): # Used for user interaction
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context): # Draw options (typically displayed in the tool-bar)
row = self.layout
row.prop(self, "report_flag", text="Report Hello World")
def execute(self, context): # Runs by default
if self.report_flag:
self.report({'INFO'}, "Hello World")
else:
print ("Hello World")
return {'FINISHED'}
bpy.utils.register_class(WM_OT_HelloWorld)
# For interaction, pass 'INVOKE_DEFAULT' when calling
# the operator, this way invoke runs before execute method
bpy.ops.wm.hello_world('INVOKE_DEFAULT')
Further reading: How to call a confirmation dialog box? (for tremendously dangerous operators).
Menu
For a custom menu define/inherit a Menu Class. Add your operators and properties to the draw()
function properly (see also: Text Editor > Templates > Python > UI Menu templates).
import bpy
class MyCustomMenu(bpy.types.Menu):
bl_label = "Simple Custom Menu"
bl_idname = "MY_MT_custom_menu"
def draw(self, context):
layout = self.layout
layout.operator("wm.open_mainfile")
layout.operator("wm.save_as_mainfile")
bpy.utils.register_class(MyCustomMenu)
# The menu can also be called from scripts
bpy.ops.wm.call_menu(name=MyCustomMenu.bl_idname)
You can also draw a button to call the menu without declaring any extra operator by layout.operator("wm.call_menu").name="bl_idname"
Submenu
To get a submenu, call the second one within the parent menu via layout.menu(bl_idname)
.
class MyCustomMenu(bpy.types.Menu):
bl_label = "First Menu"
bl_idname = "OBJECT_MT_custom_menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello First Menu!", icon='WORLD_DATA')
# call the second custom menu
layout.menu("OBJECT_MT_sub_menu", icon="COLLAPSEMENU")
class MyCustomSubMenu(bpy.types.Menu):
bl_label = "Sub Menu"
bl_idname = "OBJECT_MT_sub_menu"
def draw(self, context):
layout = self.layout
layout.label(text="Hello Second Menu!", icon='WORLD_DATA')
# call another predefined menu
layout.operator("wm.call_menu", text="Unwrap").name = "VIEW3D_MT_uv_map"
# draw a button within the panel to call the first menu
class OBJECT_PT_my_panel(bpy.types.Panel):
...
def draw(self, context):
layout.operator("wm.call_menu", text="Call My Menu").name = "OBJECT_MT_custom_menu"
...
Add-on Template
When creating an Add-on, usually a lot of properties are requiered. For a better organisation you can create a 'settings class' by using a PropertyGroup.
class MySettings(PropertyGroup):
my_bool: BoolProperty()
my_int: IntProperty()
my_float: FloatProperty()
...
Note that as of Blender 2.8x, module/class registration has changed to prevent name conflicts. bpy.utils.register_module(__name__)
isn't available anymore so you basically have to register/unregister each class separately or within a loop (best practice). In addition to the old naming conventions you also have to add a seperator like _OT_
, _MT_
or _PT_
to the name of your class based on the inherited class type (Operator
, Menu
, Panel
).
classes = (
WM_OT_HelloWorld,
OBJECT_PT_CustomPanel,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
The following add-on adds a custom panel to the Tool Shelf of the 3D View and prints the current 'user values' of all custom properties to the console:
# ##### BEGIN GPL LICENSE BLOCK #####
#
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Add-on Template",
"description": "A demo that adds a custom panel to the 'Tool Shelf' of the '3d View'",
"author": "p2or",
"version": (0, 4),
"blender": (3, 0, 0),
"location": "3D View > Tools",
"warning": "", # used for warning icon and text in addons panel
"doc_url": "https://blender.stackexchange.com/a/57332",
"tracker_url": "https://gist.github.com/p2or/2947b1aa89141caae182526a8fc2bc5a",
"support": "COMMUNITY",
"category": "Development"
}
import bpy
from bpy.props import (StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
EnumProperty,
PointerProperty,
)
from bpy.types import (Panel,
Menu,
Operator,
PropertyGroup,
)
# ------------------------------------------------------------------------
# Scene Properties
# ------------------------------------------------------------------------
class MY_PG_SceneProperties(PropertyGroup):
my_bool: BoolProperty(
name="Bool",
description="A bool property",
default = False
)
my_int: IntProperty(
name = "Int",
description="Integer property",
default = 23,
min = 10,
max = 100
)
my_float: FloatProperty(
name = "Float",
description = "Float Property",
default = 23.7,
min = 0.01,
max = 30.0
)
my_float_vector: FloatVectorProperty(
name = "Float Vector",
description="Float Vector Property",
default=(0.0, 0.0, 0.0),
#subtype='COLOR',
min= 0.0, # float
max = 0.1
)
my_string: StringProperty(
name="String",
description="String Property",
default="",
maxlen=1024,
)
my_path: StringProperty(
name = "Directory",
description="Choose a Directory:",
default="",
maxlen=1024,
subtype='DIR_PATH' # FILE_PATH
)
my_enum: EnumProperty(
name="Enum",
description="Enum Property",
items=[ ('OP1', "Option 1", ""),
('OP2', "Option 2", ""),
]
)
# ------------------------------------------------------------------------
# Operators
# ------------------------------------------------------------------------
class WM_OT_HelloWorld(Operator):
bl_label = "Print Values to the Console"
bl_idname = "wm.hello_world"
# WindowManager namespace (wm.hello...) serves as example,
# You could also use a custom one like: my_category.hello_world
def execute(self, context):
scene = context.scene
mytool = scene.my_tool
# print the values to the console
print("Hello World")
print("bool state:", mytool.my_bool)
print("int value:", mytool.my_int)
print("float value:", mytool.my_float)
print("string value:", mytool.my_string)
print("enum selection:", mytool.my_enum)
return {'FINISHED'}
# ------------------------------------------------------------------------
# Menus
# ------------------------------------------------------------------------
class OBJECT_MT_CustomMenu(Menu):
bl_label = "Select"
bl_idname = "OBJECT_MT_custom_menu"
def draw(self, context):
layout = self.layout
# Built-in operators
layout.operator("object.select_all", text="Select/Deselect All").action = 'TOGGLE'
layout.operator("object.select_all", text="Inverse").action = 'INVERT'
layout.operator("object.select_random", text="Random")
# ------------------------------------------------------------------------
# Panel in Object Mode
# ------------------------------------------------------------------------
class OBJECT_PT_CustomPanel(Panel):
bl_label = "My Panel"
bl_idname = "OBJECT_PT_custom_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tools"
bl_context = "objectmode"
@classmethod
def poll(self,context):
return context.object is not None
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
scene = context.scene
mytool = scene.my_tool
layout.prop(mytool, "my_bool")
layout.prop(mytool, "my_enum", expand=True)
layout.prop(mytool, "my_int")
layout.prop(mytool, "my_float")
layout.prop(mytool, "my_float_vector")
layout.prop(mytool, "my_string")
layout.prop(mytool, "my_path")
layout.separator(factor=1.5)
layout.menu(OBJECT_MT_CustomMenu.bl_idname, text="Presets", icon="SCENE")
layout.operator("wm.hello_world")
layout.separator()
# ------------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------------
classes = (
WM_OT_HelloWorld,
MY_PG_SceneProperties,
OBJECT_MT_CustomMenu,
OBJECT_PT_CustomPanel
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.my_tool = PointerProperty(type=MY_PG_SceneProperties)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
del bpy.types.Scene.my_tool
if __name__ == "__main__":
register()
Note: There is also a gist of this template that can easily be forked. For the Blender 2.7+ or 2.8+ versions of the template, please have look into to the revisions of this answer.
More complex examples
-
2$\begingroup$ Goodness, this was really thorough! There's way more to Python UI than just bpy.types then haha. Thanks! $\endgroup$ Commented Jul 6, 2016 at 17:23
-
8$\begingroup$ This is an amazingly concise and clear overview that would be great as part of the docs. It took me weeks to figure this out over many, many examples. $\endgroup$ Commented Dec 7, 2018 at 18:54
-
3$\begingroup$ A lot better than official document. You saved my day! $\endgroup$– cliwoCommented Dec 7, 2020 at 11:15
-
2$\begingroup$ I wanted to start messing around with Blender UI, to get familiar in preparation for developing some basic music sequencing/sequencing for Blender (I just love Blender's UI style and highly-consistent hotkey style). A really minimal LMMS-like addon. After reading this mammoth of an answer, I think I'm ready to go straight to prototyping! Thanks! $\endgroup$ Commented Mar 5, 2021 at 19:49
Modified version -- for blender 2.80
# https://blender.stackexchange.com/q/57306/3710
# https://blender.stackexchange.com/q/79779/3710
#
# modified for blender 2.80
# last modification: 2019-09-12 -- add custom-preferences panel -- Emanuel Rumpf --
bl_info = {
"name": "Add-on Template",
"description": "",
"author": "",
"version": (0, 0, 2),
"blender": (2, 80, 0),
"location": "3D View > Tools",
"warning": "", # used for warning icon and text in addons panel
"wiki_url": "",
"tracker_url": "",
"category": "Development"
}
"""
This is an addon - template for blender 2.80
Use it as base for new addons.
--
Some changes made for blender 2.80 version (from 2.79):
- Properties are annotations now, assigned with : not =
- bl_region_type now is "UI" not "TOOLS"
- Registration procedure changed:
Use bpy.utils.register_class() not register_module()
More information see: python api blender 2.80
"""
import bpy
#import collections
#import importlib
#import mathutils
#import math
from bpy.utils import ( register_class, unregister_class )
from bpy.props import ( StringProperty,
BoolProperty,
IntProperty,
FloatProperty,
FloatVectorProperty,
EnumProperty,
PointerProperty,
)
from bpy.types import ( Panel,
AddonPreferences,
Operator,
PropertyGroup,
)
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
addon_name = __name__ # when single file
#addon_name = __package__ # when file in package
# ------------------------------------------------------------------------
# settings in addon-preferences panel
# ------------------------------------------------------------------------
# panel update function for PREFS_PT_MyPrefs panel
def _update_panel_fnc (self, context):
#
# load addon custom-preferences
print( addon_name, ': update pref.panel function called' )
#
main_panel = OBJECT_PT_my_panel
#
main_panel .bl_category = context .preferences.addons[addon_name] .preferences.tab_label
# re-register for update
unregister_class( main_panel )
register_class( main_panel )
class PREFS_PT_MyPrefs( AddonPreferences ):
''' Custom Addon Preferences Panel - in addon activation panel -
menu / edit / preferences / add-ons
'''
bl_idname = addon_name
tab_label: StringProperty(
name="Tab Label",
description="Choose a label-name for the panel tab",
default="New Addon",
update=_update_panel_fnc
)
def draw(self, context):
layout = self.layout
row = layout.row()
col = row.column()
col.label(text="Tab Label:")
col.prop(self, "tab_label", text="")
# ------------------------------------------------------------------------
# properties visible in the addon-panel
# ------------------------------------------------------------------------
class PG_MyProperties (PropertyGroup):
my_bool : BoolProperty(
name="Enable or Disable",
description="A bool property",
default = False
)
my_int : IntProperty(
name = "Int Value",
description="A integer property",
default = 23,
min = 10,
max = 100
)
my_float : FloatProperty(
name = "Float Value",
description = "A float property",
default = 23.7,
min = 0.01,
max = 30.0
)
my_float_vector : FloatVectorProperty(
name = "Float Vector Value",
description="Something",
default=(0.0, 0.0, 0.0),
min= 0.0, # float
max = 0.1
)
my_string : StringProperty(
name="User Input",
description=":",
default="",
maxlen=1024,
)
my_enum : EnumProperty(
name="Dropdown:",
description="Apply Data to attribute.",
items=[ ('OP1', "Option 1", ""),
('OP2', "Option 2", ""),
('OP3', "Option 3", ""),
]
)
# ------------------------------------------------------------------------
# operators
# ------------------------------------------------------------------------
class OT_HelloWorldOperator (bpy.types.Operator):
bl_idname = "wm.hello_world"
bl_label = "Print Values Operator"
def execute(self, context):
scene = context.scene
mytool = scene.my_tool
# print the values to the console
print("Hello World")
print("bool state:", mytool.my_bool)
print("int value:", mytool.my_int)
print("float value:", mytool.my_float)
print("string value:", mytool.my_string)
print("enum state:", mytool.my_enum)
return {'FINISHED'}
# ------------------------------------------------------------------------
# menus
# ------------------------------------------------------------------------
class MT_BasicMenu (bpy.types.Menu):
bl_idname = "OBJECT_MT_select_test"
bl_label = "Select"
def draw(self, context):
layout = self.layout
# built-in example operators
layout.operator("object.select_all", text="Select/Deselect All").action = 'TOGGLE'
layout.operator("object.select_all", text="Inverse").action = 'INVERT'
layout.operator("object.select_random", text="Random")
# ------------------------------------------------------------------------
# addon - panel -- visible in objectmode
# ------------------------------------------------------------------------
class OBJECT_PT_my_panel (Panel):
bl_idname = "OBJECT_PT_my_panel"
bl_label = "My Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tool" # note: replaced by preferences-setting in register function
bl_context = "objectmode"
# def __init(self):
# super( self, Panel ).__init__()
# bl_category = bpy.context.preferences.addons[__name__].preferences.category
@classmethod
def poll(self,context):
return context.object is not None
def draw(self, context):
layout = self.layout
scene = context.scene
mytool = scene.my_tool
layout.prop( mytool, "my_bool")
layout.prop( mytool, "my_enum", text="")
layout.prop( mytool, "my_int")
layout.prop( mytool, "my_float")
layout.prop( mytool, "my_float_vector", text="")
layout.prop( mytool, "my_string")
layout.operator( "wm.hello_world")
layout.menu( "OBJECT_MT_select_test", text="Presets", icon="SCENE")
# ------------------------------------------------------------------------
# register and unregister
# ------------------------------------------------------------------------
classes = (
PG_MyProperties,
#
OT_HelloWorldOperator,
MT_BasicMenu,
OBJECT_PT_my_panel,
#
PREFS_PT_MyPrefs,
)
def register():
#
for cls in classes:
register_class(cls)
#
bpy.types.Scene.my_tool = PointerProperty(type=PG_MyProperties)
#
def unregister():
#
for cls in reversed(classes):
unregister_class(cls)
#
del bpy.types.Scene.my_tool # remove PG_MyProperties
if __name__ == "__main__":
pass
#register()
Some changes made for blender 2.80 version:
- Properties are annotations now, assigned with
:
not=
bl_region_type
now is "UI" not "TOOLS"- Registration procedure changed:
- Use
bpy.utils.register_class()
notregister_module()
More information: python api blender 2.80
-
$\begingroup$ OMG. Thank you so much. I am learning python in blender. I code a lot in VB.net & C#. Was struggling with the UI stuff. This explained it very well and $\endgroup$– user2850Commented Sep 29, 2019 at 17:02
-
$\begingroup$ hi, i would like to know what
scene.my_tool
was refering to? because I didnt see where it was first defined? $\endgroup$ Commented Apr 17, 2020 at 10:21 -
$\begingroup$ oh now i get it, so i hv to register a pointer property to store say some other properties? $\endgroup$ Commented Apr 17, 2020 at 10:24
-
$\begingroup$ Late to the party. Check the quickUI add on. The version on gumroad is free [1]. [1] gumroad.com/l/quickUI $\endgroup$ Commented Apr 23, 2021 at 16:53
-
$\begingroup$ good synthesis, it would be perfect to add some updates on properties in property group to see where to put them $\endgroup$ Commented Aug 8, 2021 at 16:21
class MyPanel(bpy.types.Panel):
blender.org/api/blender_python_api_current/bpy.types.html . My suggestion is look thru the "Text Editor > templates > python > Ui *" examples that come with blender. $\endgroup$