Blender : sous-panneaux
Dans Blender, on peut ajouter des panneaux à peu près partout, y compris à l'intérieur d'autres panneaux. Mais comment créer un nombre variable du même panneau ? Quelques pistes.
Groupe de données à afficher/masquer
La question se pose lorsque l'on veut afficher des groupes de données, que le nombre de groupe est variable, et que les données doivent/peuvent être affichées de la même manière.
Exemple de données :
data = {
"group01": ["data 1-1", "data 1-2", "data 1-3"],
"group02": ["data 2-1", "data 2-2", "data 2-3"],
"group03": ["data 3-1", "data 3-2", "data 3-3"]
}
On voudrait dés lors afficher les groupes dans des panneaux dédiés, répétés autant de fois qu'il y a de groupe ; les panneaux serait pliables/dépliables, un type de widget graphique nommé « disclosure ».
En web, on utiliserait par exemple une suite d'éléments <details>
:
Panneau principal
group01
- data 1-1
- data 1-2
- data 1-3
group02
- data 2-1
- data 2-2
- data 2-3
group03
- data 3-1
- data 3-2
- data 3-3
Un panneau simple
Dans Blender, on créé un panneau en déclarant une classe étendant bpy.types.Panel
et en précisant la zone dans laquelle on souhaite l'afficher (bl_space_type
), la région de l'interface graphique (bl_region_type
) et l'onglet auquel il doit appartenir (bl_category
). La méthode draw
sert a dessiner ce qui sera à l'intérieur.
class TEST_PT_panel(bpy.types.Panel):
bl_label = "Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Test"
def draw(self, context):
pass
Et c'est tout : pas d'instanciation, pas d'injection, c'est Blender qui se charge de ça.
Voyons maintenant comment créer des sous-panneaux.
Simuler des sous-panneaux
Une première approche consiste à simuler des sous-panneaux : on définit une ligne représentant l'en-tête de groupe puis, en fonction du click utilisateur sur cette ligne, on affiche ou masque les items du groupe.
Pour ce faire, on créé d'abord un opérateur pour gérer cet état plier / déplier de chaque groupe :
expanded = {
"group01": False,
"group02": False,
"group03": False
}
class TEST_OT_toggler(bpy.types.Operator):
"""toggler"""
bl_idname = "test.toggler"
bl_label = "toggle"
bl_description = "Toggle"
value: bpy.props.StringProperty()
def execute(self, context):
expanded[self.value] = not expanded[self.value]
return {"FINISHED"}
Ensuite, on dessine ou pas des lignes pour le contenu de chaque groupe en fonction de l'état étendu correspondant :
class TEST_PT_panel(bpy.types.Panel):
bl_label = "Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tests"
def draw(self, context):
for (name, values) in data.items():
box = self.layout.box()
row = box.row()
# toggler operator
toggler = row.operator("test.toggler",
text = name,
icon = "DOWNARROW_HLT" if expanded[name] else "RIGHTARROW",
emboss = False
)
toggler.value = name
# draw lines if necessary
if expanded[name]:
for value in values:
box.label(text = value)
Cette solution a un défaut majeur : à chaque fois qu'une donnée de groupe est modifiée (si elle est liée à une data, ce qui n'est pas le cas dans l'exemple, ultra-simple, qui se contente de dessiner un libellé), que l'on plie/déplie un groupe ou même que l'on passe juste la souris au-dessus d'un item, blender va redessiner le panneau entier, donc redessiner tous les groupes et toutes les lignes (celles visibles néanmoins).
C'est même un peu perturbant de voir combien de fois la méthode draw
est appelée ; On dirait du React (ce n'est pas un compliment).
Partons cependant de l'idée que les devs blender savent ce qu'ils font, que c'est comme ça que l'IHM est le mieux dessiné, et essayons de voir comment créer de vrais sous-panneaux, pas seulement une illusion.
Créer un vrai sous-panneau
En fait, un sous-panneau est simplement un panneau auquel on aura indiqué un panneau parent à l'aide de la propriété bl_parent_id
:
class TEST_PT_subpanel(bpy.types.Panel):
bl_label = "Subpanel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Test"
bl_parent_id = "TEST_PT_panel"
def draw(self, context):
pass
Maintenant, on pourrait créer trois sous-panneaux pour nos trois groupes de données. Mais qu'arrivera-t-il si on à un quatrième groupe de données ? Où si le nombre de groupe de données varie au cours du temps ?
On ne peut pas créer a priori une représentation graphique de données quand on n'en connait pas le nombre, où plutôt quand on ne connait le nombre qu'au runtime, lorsque l'utilisateur lance l'application.
Pour répondre à ce problème, il faut créer les panneaux à la volé, et le nombre dont on a besoin.
Créer des sous-panneaux dynamiques
On stocke les panneaux à créer dans un tableau :
subpanels = []
On créé une classe chargée de porter le comportement des sous-panneaux et que chacune des itérations devra étendre. Le sous-panneau affichera les données du groupe dont il porte le nom (bl_label
) :
class TEST_PT_subpanel(bpy.types.Panel):
bl_label = "Subpanel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Test"
bl_context = "Test"
bl_parent_id = "TEST_PT_panel"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
for name in data[self.bl_label]:
row = self.layout.row()
row.label(text=name)
On créé ensuite un panneau (en fait un type python) pour chaque groupe de données :
for group in data:
subpanels.append(type(
f"TEST_PT_panel_{group}",
(TEST_PT_subpanel,),
{"bl_label": group}
))
Enfin il faut s'assurer que les panneaux sont enregistrés auprès de blender au démarrage et désenregistrer quand on quitte l'application ou quand l'addon est désinstallé.
def register():
for subpanel in subpanels:
bpy.utils.register_class(subpanel)
def unregister():
for subpanel in subpanels:
bpy.utils.unregister_class(subpanel)
L'avantage de cette méthode est que les panneaux blender sont des unités graphiques indépendantes : agir sur l'un ne demande pas de redessiner les autres. Donc quand on plie/déplie le groupe01, il n'y aura pas de « repaint » sur les autres groupes, en tout cas pas de la manière dont la méthode précédente nécessitait de redessiner toutes les lignes.
Code complet de test
import bpy
ICO_OPEN = "DOWNARROW_HLT"
ICO_CLOSE = "RIGHTARROW"
SIM = False
data = {
"group01": ["data 1-1", "data 1-2", "data 1-3"],
"group02": ["data 2-1", "data 2-2", "data 2-3"],
"group03": ["data 3-1", "data 3-2", "data 3-3"]
}
expanded = {
"group01": False,
"group02": False,
"group03": False
}
class TEST_OT_toggler(bpy.types.Operator):
bl_idname = "test.toggler"
bl_label = "toggle"
bl_description = "Toggle"
value: bpy.props.StringProperty()
def execute(self, context):
expanded[self.value] = not expanded[self.value]
return {"FINISHED"}
class TEST_PT_panel(bpy.types.Panel):
bl_label = "Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tests"
def draw(self, context):
if SIM:
for (name, values) in data.items():
box = self.layout.box()
row = box.row()
toggler = row.operator("test.toggler",
text = name,
icon = ICO_OPEN if expanded[name] else ICO_CLOSE,
emboss = False
)
toggler.value = name
if expanded[name]:
for value in values:
box.label(text = value)
class TEST_PT_subpanel(bpy.types.Panel):
bl_label = "Panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Tests"
bl_parent_id = "TEST_PT_panel"
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
if self.bl_label in data:
for name in data[self.bl_label]:
row = self.layout.row()
row.label(text=name)
subpanels = []
if not SIM:
for group in data:
subpanels.append(type(
f"TEST_PT_panel_{group}",
(TEST_PT_subpanel,),
{"bl_label": group,}
))
def register():
bpy.utils.register_class(TEST_OT_toggler)
bpy.utils.register_class(TEST_PT_panel)
if not SIM:
bpy.utils.register_class(TEST_PT_subpanel)
for subpanel in subpanels:
bpy.utils.register_class(subpanel)
def unregister():
bpy.utils.unregister_class(TEST_OT_toggler)
bpy.utils.unregister_class(TEST_PT_panel)
if not SIM:
bpy.utils.unregister_class(TEST_PT_subpanel)
for subpanel in subpanels:
bpy.utils.unregister_class(subpanel)
from . import subpanels
bl_info = {
"name": "panel_tests",
"description": "",
"version": (1, 0, 0),
"blender": (3, 0, 0),
"category": "UI",
}
def register():
subpanels.register()
def unregister():
subpanels.unregister()