2.4 PropertyGroup与上下文机制


2.4 PropertyGroup 与上下文机制

本节摘要:PropertyGroup 是插件数据的契约容器,属性工厂是它的语法,作用域选择是它的第一设计决策;上下文机制则是它得以被界面、动画、驱动系统共享的运行时土壤。本节讲属性声明的语义约束(范围、选项、存取描述符)、三大作用域的选型、嵌套与集合属性的用法,以及上下文的分层与惰性绑定如何让"当前选中"变成可编程实体。

本节导读

阅读完本节,你应当能够:

  1. 为不同数据形态选择正确的属性类型与约束参数;
  2. 解释 Scene、Object、WindowManager 三种挂载作用域的语义差异;
  3. 用嵌套属性组与集合属性表达"一组参数"与"参数列表";
  4. 用存取描述符封装单位转换等复杂语义;
  5. 说出上下文三层作用域与惰性绑定的行为含义。

一、属性声明:每个参数都是一条契约

属性工厂远不止类型声明工具。每个属性定义都在编织一张语义约束网:minmax 不仅限制滑块,序列化时也作为校验断言;描述文本既是悬浮提示也是辅助功能接口;subtype(如距离、角度、颜色)决定控件渲染与单位显示;选项集合控制行为边界——隐藏属性不进界面、跳过保存的属性不进文件。

先看一份"参数清单"怎么写成属性组:

class ScatterSettings(bpy.types.PropertyGroup): density: bpy.props.IntProperty( name="密度", default=10, min=1, max=1000, description="每平方米的实例数量", ) scale_jitter: bpy.props.FloatProperty( name="缩放抖动", default=0.1, min=0.0, max=1.0, subtype='FACTOR', ) align_to_normal: bpy.props.BoolProperty(name="对齐法线", default=False) distribution: bpy.props.EnumProperty( name="分布方式", items=[ ('RANDOM', "随机", "均匀随机撒布"), ('POISSON', "泊松盘", "保持最小间距的均匀分布"), ('GRID', "网格", "规则网格加抖动"), ], default='RANDOM', ) seed: bpy.props.IntProperty(name="随机种子", default=0, min=0, max=10000)

注意枚举属性的设计:每一项是"标识、显示名、描述"三元组。标识是代码里比较的值,显示名给用户看,描述进悬浮提示——三个语义层不要混用。种子显式声明成参数,呼应 2.3 节的幂等要求:同样种子同样结果,重做不失真。

二、作用域选择:数据挂在哪,决定它跟谁走

属性组定义好后,挂载位置是第一设计决策。三大作用域语义迥异:

挂载目标 生命周期 随文件保存 典型内容
Scene 随场景 本场景的工具参数、渲染预设
Object 随物体 物体级元数据、绑定信息
WindowManager 随会话 否(默认) 界面临时状态、跨场景开关
bpy.types.Scene.scatter_tool = bpy.props.PointerProperty(type=ScatterSettings) bpy.types.Object.scatter_meta = bpy.props.PointerProperty(type=ObjectMeta) bpy.types.WindowManager.ui_state = bpy.props.PointerProperty(type=UIState)

选型判断只有一句话:这份数据属于谁,就挂到谁身上。属于这个工程场景的(密度参数)挂 Scene,换场景就该换一套;属于物体的(是否已绑定)挂 Object,物体复制时跟着走;只属于本次界面会话的(面板展开状态)挂 WindowManager,不该写进文件。

⚠️ 常见坑:把工程参数挂到 WindowManager。用户精心调好参数、保存文件、发给同事——同事打开一看全是默认值。窗口管理器层面的属性默认不序列化,这不是 bug,是你把数据放错了家。

三、嵌套与集合:表达结构化数据

真实参数很少是扁平的。属性组支持两种组织方式。嵌套:属性组的某个成员本身是另一个属性组的指针,形成树。集合:集合属性维护同类型属性组的动态列表,表达"一组条目":

class PresetEntry(bpy.types.PropertyGroup): name: bpy.props.StringProperty(name="名称") density: bpy.props.IntProperty(name="密度", default=10) class ScatterSettings(bpy.types.PropertyGroup): # 嵌套:一套全局选项 advanced: bpy.props.PointerProperty(type=AdvancedOptions) # 集合:可增删的预设列表 presets: bpy.props.CollectionProperty(type=PresetEntry) active_index: bpy.props.IntProperty(default=0)

集合属性的界面绑定有固定套路:一个模板列表控件加索引属性,即可得到带增删按钮的标准列表。增删条目要成对——界面加了一条,逻辑上对应的数据处理(如清理派生缓存)要跟上,否则出现"幽灵条目"。

四、存取描述符:把领域语义藏进属性

属性工厂还支持自定义 get 与 set 描述符,实现惰性计算、单位转换、权限检查:

def get_quality(self): return ['草稿', '标准', '高', '极致'][self.internal_level] def set_quality(self, value): mapping = {'草稿': 0, '标准': 1, '高': 2, '极致': 3} self.internal_level = mapping.get(value, 1) class RenderSettings(bpy.types.PropertyGroup): internal_level: bpy.props.IntProperty(default=1) quality: bpy.props.EnumProperty( name="质量", items=[('草稿', "草稿", ""), ('标准', "标准", ""), ('高', "高", ""), ('极致', "极致", "")], get=get_quality, set=set_quality, )

这样用户面对的是"质量档位",内部存储的是整数等级——领域语义与存储结构解耦,后者随时可以重构而不动界面。这就是把配置容器当"领域对象"来写:不暴露内部细节,以用户心智模型为接口。

💡 关键直觉:属性组是"带行为的数据"——描述是文档、范围是校验、更新回调是反应、存取描述符是封装。四件套用全,你的参数自己会保护自己。

五、上下文机制:让"当前"可编程

属性组之所以能被界面直接绑定、被动画系统驱动,靠的是上下文机制这层土壤。上下文的三层作用域在 2.1 已有铺垫,这里补两个与属性协作的关键行为。

惰性绑定context.object 不是预构造的实例,而是惰性求值代理——首次访问其属性时,才按当前视图层与活动对象指针动态查找并包装 C 对象。这大幅降低上下文初始化开销,也让不同作用域可以安全地各持快照。

属性路径解析:RNA 支持把 "scatter_tool.density" 这样的字符串解析为嵌套引用链。动画关键帧、驱动器变量、界面的属性绑定,全部基于这套路径系统。你把属性组挂到 Scene,驱动器就能引用它的某个成员做动画——不用写一行胶水代码。

六、实战:把第 1 章的插件参数化

用本章知识重构批量重命名插件:把散在 Operator 上的属性收进属性组挂到 Scene,Operator 只读它。好处立现——参数在场景里持久(存文件后还在)、多个 Operator 可共享同一份设置、第 3 章可以直接给这个属性组配完整界面,第 6 章可以对纯逻辑部分做零依赖单元测试(属性组的默认值语义不依赖运行中的 Blender 即可推理)。

class RenameSettings(bpy.types.PropertyGroup): prefix: bpy.props.StringProperty(name="前缀", default="asset_") padding: bpy.props.IntProperty(name="序号位数", default=3, min=1, max=6) start: bpy.props.IntProperty(name="起始序号", default=1, min=0) bpy.types.Scene.rename_tool = bpy.props.PointerProperty(type=RenameSettings) class OBJECT_OT_batch_rename(bpy.types.Operator): bl_idname = "object.batch_rename_v2" bl_label = "批量重命名 参数化" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.selected_objects) > 0 def execute(self, context): cfg = context.scene.rename_tool for i, obj in enumerate(context.selected_objects, start=cfg.start): obj.name = f"{cfg.prefix}{i:0{cfg.padding}d}" return {'FINISHED'}

对比第 1 章的版本:参数从"每次执行临时输入"升级为"场景级持久配置",Operator 变薄、数据变实——这就是契约思维带来的结构进化。

属性作用域决策图

属性作用域决策图

七、属性设计的实战问答

属性越加越多,怎么防止参数面板失控?

三步治理。第一步分层:按使用频率把参数分成"常用、进阶、专家"三档,常用进主面板、进阶进折叠子面板、专家进偏好设置——对应 3.1 节的渐进式界面。第二步分组:用嵌套属性组把语义相关的参数聚簇(散射的密度与间距是一簇,随机性与种子是一簇),界面按簇绘制。第三步减法:每加一个参数问自己"删掉它用户会察觉吗",答案是否就先删。参数的数量就是用户的学习成本,没有任何例外。

枚举属性的标识能改吗?

不能随便改。标识是序列化进文件的存储值,改了标识,旧文件里存的旧标识就找不到对应项,轻则显示空白、重则读取报错。显示名与描述随时可改,标识一旦发布就是对外承诺。确实要重构时,保留旧标识作为隐藏项做过渡,或写迁移逻辑在文件加载时转换——第 6 章的版本策略会展开这条。

上下文在批量脚本里为什么经常取不到活动对象?

批量脚本常在无界面或非标准上下文中执行(后台模式、渲染回调),此时"活动对象"这个概念本身就不成立——没有用户的焦点,哪来的活动。正确做法:批量逻辑不依赖活动对象,入参显式传目标列表;入口操作符从上下文收集目标后传给纯逻辑函数。这也顺手完成了逻辑与界面的解耦,第 6 章的零依赖单测因此成为可能。

属性组能存任意 Python 对象吗?

不能。属性工厂只支持固定的一组类型:数值、字符串、布尔、枚举、向量、集合、指针。字典、自定义类这些通通不行——这是序列化契约的边界。需要存复杂结构时,把展平成嵌套属性组加集合属性;确实放不下的(如临时缓存),就承认它是运行时状态,放到模块级管理器里并接受"不进文件"的设定。硬塞的代价是保存时静默丢数据。

八、属性系统的演化预案

属性组发布之后一定会演化——加参数、改默认值、拆分旧字段。提前做三件小事,未来的迁移成本天差地别。

**给结构留版本字段。**在属性组里放一个整数版本号属性,首次使用时写入当前版本。将来读到旧版本号,就知道要跑迁移逻辑。一行代码的成本,换来的是"新旧文件混存"时的从容。

**发布前做字段冻结清单。**把"标识已定、不可再改"的字段列进文档:枚举标识、属性名、集合条目结构。这份清单是你与所有已存工程文件之间的合同——清单内的改动都需要迁移代码,清单外的(显示名、描述、默认值)随时可改。

**拆分字段时保留兼容读法。**把一个字段拆成两个时,旧字段不要立刻删:先让它作为隐藏属性存在,加载时若发现旧字段有值而新字段为默认,自动搬运——一个版本后再考虑移除。温柔对待用户的历史数据,是持久化模块最深的教养。

九、属性系统的反模式图鉴

三个流传甚广的反模式,见到就该重构。反模式一:上帝属性组——一个属性组塞五十个参数,服务于完全无关的功能。症状是界面上出现无意义的滚动长页。解法按功能域拆组成树。反模式二:影子状态——属性组存了一份配置,模块里又用变量缓存了一份"优化版",两份各自演化。症状是改了设置不生效、重启后又变了。解法是砍掉影子,缓存必须挂失效机制。反模式三:枚举当常量——把不会变的内部状态也做成枚举属性暴露给界面。症状是用户面对一堆无意义的下拉框。解法是内部状态用普通代码常量,属性只放用户真正要调的东西。三条共性:都是局部便利换全局混乱,重构成本都随时间指数上涨。

一节小结

  • 要点一:属性声明是契约——范围即校验、描述即文档、子类型即单位、选项即边界;
  • 要点二:作用域选型一句话:数据属于谁就挂到谁身上,挂错家是持久化事故第一来源;
  • 要点三:嵌套指针表达参数树,集合属性表达条目列表,增删要成对;
  • 要点四:存取描述符把领域语义封装进属性,存储结构与界面解耦;
  • 要点五:属性路径系统让界面、关键帧、驱动器共享同一数据源,无需胶水;
  • 要点六:把第 1 章插件参数化是本章的收束练习——Operator 变薄,数据变实。

数据契约就位。第 3 章给这些参数配上体面的门面:面板、菜单、快捷键与三维手柄。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U