面向 网易我的世界(基岩版)ModSDK 的 Python UI 声明式渲染框架。
提供类似 React 的组件函数 + Hooks 写法,将组件树(VNode)经过 Diff 与布局计算后,渲染为原生控件集合。
- 函数式组件 - 通过
@Component装饰器声明组件 - Hooks -
useState/useEffect/useMemo/useCallback/useRef - Flexbox 布局 - 支持
width/height/padding/margin/flexDirection/justifyContent/alignItems等 - 基础控件 -
Panel/Image/Label/Button/Input/Scroll/Item/PaperDoll - 运行时优化 - Typed Grid 批量创建、控件池复用、跨帧延迟渲染
行为包(behavior_pack)添加:
pyreact/ # 框架核心
PyreactRuntimeScript/ # 运行时系统
资源包(resource_pack)的 ui/ 目录添加:
PyreactBase.json # 基础控件模板
YourScreen.json # 你的 Screen 定义
# -*- coding: utf-8 -*-
# YourClientSystem.py
import mod.client.extraClientApi as clientApi
ClientSystem = clientApi.GetClientSystemCls()
class YourClientSystem(ClientSystem):
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
self.ListenForEvent(
clientApi.GetEngineNamespace(),
clientApi.GetEngineSystemName(),
'UiInitFinished', self, self.OnUiInitFinished
)
def OnUiInitFinished(self, args):
# 注册 UI
clientApi.RegisterUI(
'YourMod', 'YourUI',
"YourMod.YourScreen.YourScreenNode",
"YourNamespace.main"
)
# 在需要显示的时机显示界面
clientApi.PushScreen('YourMod', 'YourUI', {"isHud": 1, "data": {}})# -*- coding: utf-8 -*-
# YourScreen.py
import mod.client.extraClientApi as clientApi
from pyreact import (
Component, Panel, Label, Button, Scroll,
Style, Color, Colors, FontSize,
AlignItems, JustifyContent, FlexDirection,
useState, useRef, render_app,
)
ScreenNode = clientApi.GetScreenNodeCls()
@Component
def CounterApp():
"""计数器示例组件"""
count, set_count = useState(0)
return Panel(
style=Style(
width='100%',
height='100%',
alignItems=AlignItems.center,
justifyContent=JustifyContent.center,
),
children=[
Label(
content='Count: %d' % count,
color=Colors.white,
fontSize=FontSize.extraLarge,
),
Button(
style=Style(width=120, height=36, marginTop=16),
onClick=lambda: set_count(count + 1),
children=[
Label(content='Click Me', color=Colors.white)
],
),
],
)
class YourScreenNode(ScreenNode):
def __init__(self, namespace, name, param):
ScreenNode.__init__(self, namespace, name, param)
self.app_id = 'your_app_id'
def Create(self):
render_app(
root=CounterApp,
bind={
'screen': self,
'root': '/root',
'app_id': self.app_id,
'base_namespace': 'PyreactBase',
},
)
def Destroy(self):
runtime = clientApi.GetSystem('PyreactRuntimeMod', 'PyreactRuntimeClientSystem')
if runtime:
runtime.UnmountApp({'app_id': self.app_id})以下公共能力适用于所有公开组件;各组件小节里不再重复列出。
- 用途:给节点提供稳定身份,便于列表复用、diff 和状态对齐
- 建议:动态列表优先使用业务唯一 ID,不要用随机值
- 说明:自定义组件不需要在形参声明key,@Component会自动注入
- 用途:传入子节点内容
- 支持:单个节点,或
list/tuple节点列表 - 说明:所有框架自带的
primitives和composites组件都支持children,自定义组件可选在形参支持children
- 用途:弹性盒布局、定位、尺寸、层级、透明度等所有组件通用属性
- 支持:
Style(...)(推荐,有代码提示) 或dict - 说明:style 中均为所有组件通用属性,比如图片的src,文本的fontSize都要用各自组件的props传入
- 说明:所有框架自带的
primitives和composites组件都支持style,自定义组件可选在形参支持style
- 用途:获取组件对应的原生控件实例,访问底层 API
- 支持:
useRef创建的 ref 对象 - 说明:ref 由
@Component注入,对于composites组件会透传到内部跟组件上,最终到叶子primitives组件上
尺寸相关:
| 字段 | 类型 | 说明 |
|---|---|---|
width |
int / str |
宽度,可写数值或 '100%' |
height |
int / str |
高度,可写数值或 '100%' |
minWidth |
int / str |
最小宽度 |
maxWidth |
int / str |
最大宽度 |
minHeight |
int / str |
最小高度 |
maxHeight |
int / str |
最大高度 |
间距相关:
| 字段 | 类型 | 说明 |
|---|---|---|
padding |
int / float |
统一内边距 |
paddingTop |
int / float |
上内边距 |
paddingRight |
int / float |
右内边距 |
paddingBottom |
int / float |
下内边距 |
paddingLeft |
int / float |
左内边距 |
margin |
int / float |
统一外边距 |
marginTop |
int / float |
上外边距 |
marginRight |
int / float |
右外边距 |
marginBottom |
int / float |
下外边距 |
marginLeft |
int / float |
左外边距 |
Flex 相关:
| 字段 | 类型 | 说明 |
|---|---|---|
flex |
int / float |
Flex 比例 |
flexDirection |
str |
主轴方向,通常为 FlexDirection.row / column |
justifyContent |
str |
主轴对齐 |
alignItems |
str |
交叉轴对齐 |
alignSelf |
str |
当前节点自身对齐 |
flexWrap |
str |
换行策略 |
定位与显示相关:
| 字段 | 类型 | 说明 |
|---|---|---|
position |
str |
定位方式,默认按 Position.relative 处理,也可显式写 Position.absolute |
top |
int / float |
顶部偏移 |
left |
int / float |
左侧偏移 |
right |
int / float |
右侧偏移 |
bottom |
int / float |
底部偏移 |
opacity |
float |
透明度 |
display |
str |
显示状态,例如 'none' |
zIndex |
int |
层级 |
说明:
- 未显式传
position时,当前实现默认按Position.relative处理 position=Position.absolute时,top/right/bottom/left按父内容区域做绝对定位position=Position.relative时,节点仍保留原本的流式占位,再额外应用top/right/bottom/left作为视觉偏移relative下若同时给出同轴的两个方向,当前实现优先使用left胜过right、top胜过bottom
Panel 是最基础的布局容器,只参与布局和 children 组织,不会单独创建原生 panel 控件。
除公共 props 外,无额外 props。
Image 用于贴图、纯色底板、图标和按钮背景。
| prop | 类型 | 说明 |
|---|---|---|
src |
str |
图片路径;默认纯白图片 textures/ui/white_bg |
color |
Color |
颜色蒙版 |
grayscale |
bool |
是否灰度化 |
clipRatio |
float |
裁剪比例 |
uv |
tuple |
UV 起点 |
uvSize |
tuple |
UV 尺寸 |
resizeMode |
str |
图片缩放模式 |
imageAdaptionType |
str |
图片适配类型 |
nineSlice |
tuple |
九宫格切片参数 |
nineSliceType |
str |
九宫格类型 |
rotation |
float |
旋转角度 |
rotatePivot |
tuple |
旋转中心 |
onClick |
callable |
点击回调 |
Label 用于文本展示。
| prop | 类型 | 说明 |
|---|---|---|
content |
str |
文本内容 |
color |
Color |
文本颜色 |
fontSize |
int |
字号 |
textAlign |
str |
对齐方式 |
linePadding |
float |
行间距 |
shadow |
bool |
是否显示阴影 |
Item 用于渲染物品图标,对应 inventory_item_renderer。
| prop | 类型 | 说明 |
|---|---|---|
identifier |
str |
物品标识符 |
aux |
int |
物品附加值 |
enchant |
bool |
是否显示附魔效果 |
userData |
object |
额外物品数据 |
itemDict |
dict |
完整物品字典,可被上述字段覆盖 |
Button 是可点击容器,支持 default / hover / pressed 三态。
| prop | 类型 | 说明 |
|---|---|---|
onClick |
callable |
点击回调 |
buttonBuilder |
callable |
背景构造器,签名为 builder(state) -> ComponentNode |
Input 用于文本输入。
| prop | 类型 | 说明 |
|---|---|---|
value |
str |
当前输入值 |
onChange |
callable |
输入变化回调 |
placeholder |
str |
占位文本 |
Scroll 用于滚动列表容器。
| prop | 类型 | 说明 |
|---|---|---|
showScrollbar |
bool |
是否显示滚动条 |
PaperDoll 对应 netease_paper_doll_renderer,用于实体、骨骼模型和方块几何模型预览。
| prop | 类型 | 说明 |
|---|---|---|
renderType |
str |
渲染类型,可选枚举 RenderType.entity(默认) / skeleton / blockGeometry |
entityId |
int |
实体 id |
entityIdentifier |
str |
实体 identifier,例如 minecraft:cow |
skeletonModelName |
str |
骨骼模型名 |
animation |
str |
骨骼动画名 |
animationLooped |
bool |
骨骼动画是否循环 |
blockGeometryModelName |
str |
方块几何模型名 |
scale |
float |
模型缩放 |
renderDepth |
float |
渲染深度微调 |
initRotX |
float |
初始 X 轴旋转 |
initRotY |
float |
初始 Y 轴旋转 |
initRotZ |
float |
初始 Z 轴旋转 |
molangDict |
dict |
Molang 参数字典 |
rotationAxis |
tuple |
旋转轴向量 |
lightDirection |
tuple |
光照方向 |
FilledButton 是对 Button 的纯色封装,适合“纯色底板 + 内容”的按钮场景。
| prop | 类型 | 说明 |
|---|---|---|
default |
Color |
默认态背景色 |
hover |
Color |
悬浮态背景色;不传时按回退规则补齐 |
pressed |
Color |
按下态背景色;不传时按回退规则补齐 |
onClick |
callable |
点击回调,透传给内部 Button |
状态回退规则:
- 只传
default:hover和pressed都回退到default - 传
default + pressed:hover回退到pressed - 传
default + hover:pressed回退到hover
ImageButton 是对 Button 的图片态封装,适合“给三态贴图值,再由 builder 生成背景图”的场景。
| prop | 类型 | 说明 |
|---|---|---|
default |
str |
默认态贴图路径 |
hover |
str |
悬浮态贴图路径;不传时按回退规则补齐 |
pressed |
str |
按下态贴图路径;不传时按回退规则补齐 |
imageBuilder |
callable |
图片构造器,支持 imageBuilder(src) 或 imageBuilder(src, state) |
onClick |
callable |
点击回调,透传给内部 Button |
状态回退规则:
- 只传
default:hover和pressed都回退到default - 传
default + pressed:hover回退到pressed - 传
default + hover:pressed回退到hover imageBuilder必须返回Image(...);返回结果会自动注入width='100%'和height='100%'
# 状态管理
count, set_count = useState(0)
# 副作用(可选依赖数组)
useEffect(lambda: (print('mounted'), lambda: print('unmount')), [])
useEffect(lambda: print('count changed'), [count])
# 缓存计算
memo_value = useMemo(lambda: expensive_calc(dep), [dep])
# 缓存回调
handler = useCallback(lambda x: process(x, dep), [dep])
# 引用原生控件
scroll_ref = useRef(None)
scroll_ref.current.asScrollView().SetScrollViewPercentValue(0)clone_component 用于基于现有 ComponentNode 创建一个新的组件节点副本,并按需覆盖部分 props。它适合像 ImageButton 这类“拿模板节点做变体”的场景,避免直接修改原节点带来的共享引用污染。
公开导入方式:
from pyreact import clone_component常见用法:
base_image = Image(
style=Style(width='100%', height='100%'),
src='textures/ui/store/button_default',
)
hover_image = clone_component(
base_image,
src='textures/ui/store/button_hover',
)说明:
clone_component的输入必须是ComponentNode- 它会复制组件的
props,并递归复制其中的dict/list/tuple/ 子组件节点 - 传入的覆盖参数会写到新节点上,不会修改原组件
- 对于
style/children/ 嵌套子节点模板复用场景,比手写浅拷贝更安全
Style(...) 的完整字段已经在上面的公共 props style 表格中列全;这里不再重复写示例代码块。
# 1. 直接传 32-bit ARGB 整数(0xAARRGGBB)
primary = Color(0xFF2563EB)
danger = Color(0x80FF0000)
# 2. 用 RGB / RGBA 工厂函数创建
bg = Color.fromRGB(15, 23, 42)
mask = Color.fromRGBA(37, 99, 235, 0.5)
# 3. 用 Hex 字符串创建
accent = Color.fromHex('#F59E0B')
overlay = Color.fromHex('#80334155')
# 4. 预置颜色常量
Colors.white
Colors.black
Colors.lightGreyColor 是不可变颜色对象,内部值为 32-bit ARGB 整数;Colors 提供一组可直接复用的预置颜色常量。
Color(0xAARRGGBB):直接传入 32-bit ARGB 整数Color.fromRGB(r, g, b):r/g/b为0~255,alpha 固定为255Color.fromRGBA(r, g, b, a):r/g/b为0~255,a为0.0~1.0的透明度Color.fromHex(text):支持#RGB/#ARGB/#RRGGBB/#AARRGGBB,也支持0x/0X前缀
注意:
Color.fromRGBA(..., a)的a是 0.0~1.0 浮点透明度,不是0~255Color.fromHex('#RRGGBB')会自动补成不透明色(alpha=FF)- 非法值会抛异常;不会静默生成未知颜色
| 属性 | 类型 | 说明 |
|---|---|---|
value |
int |
原始 32-bit ARGB 整数 |
alpha8 |
int |
0~255 alpha 通道 |
red |
int |
0~255 红色通道 |
green |
int |
0~255 绿色通道 |
blue |
int |
0~255 蓝色通道 |
opacity |
float |
0.0~1.0 透明度 |
alpha |
float |
opacity 的别名 |
所有修改方法都会返回新的 Color,不会原地修改:
base = Color.fromRGB(37, 99, 235)
soft = base.withOpacity(0.25) # 按 0.0~1.0 设置透明度
alpha8 = base.withAlpha8(128) # 按 0~255 设置 alpha
redder = base.withRed(255)
greener = base.withGreen(160)
bluer = base.withBlue(255)withOpacity(opacity)/withAlpha(opacity):按0.0~1.0修改透明度withAlpha8(alpha8):按0~255修改 alphawithRed()/withGreen()/withBlue():修改单个颜色通道
toRGBUnitTuple():返回(r, g, b),每项范围0.0~1.0toRGBAUnitTuple():返回(r, g, b, a),每项范围0.0~1.0
Colors 中内置了大量可直接使用的颜色常量,包含基础色和常见 CSS 命名色,例如:
- 基础色:
Colors.white、Colors.black、Colors.red、Colors.blue - 灰阶/别名:
Colors.gray、Colors.grey、Colors.lightGray、Colors.lightGrey - 扩展命名色:
Colors.aliceBlue、Colors.gold、Colors.rebeccaPurple等
常见写法:
Image(color=Colors.black.withOpacity(0.35))
Label(color=Colors.white)
FilledButton(default=Colors.blue, pressed=Colors.navy)使用建议:
- 组件的颜色能力都走 props,不是
style - 图片/底板颜色写在
Image(color=...) - 文本颜色写在
Label(color=...) - 需要半透明效果时,优先复用现有颜色再调用
withOpacity(...) - runtime 只会把真正的
Color对象识别为颜色值;不要传字符串、tuple 或裸十六进制文本 - 最终原生透明度 =
style.opacity * color.alpha,两者会叠加生效
声明式动画全部由 Python runtime 驱动,通过监听 GameRenderTickEvent 每帧插值;不依赖任何原生动画 API。
from pyreact import Animated, fadeIn, fadeOut, slideInUp
Animated(
enter=slideInUp(distance=30, duration=300),
exit=fadeOut(duration=220),
children=Panel(
style=Style(width=260, height=120),
children=[...],
),
)enter:节点首次挂载时播放一次;exit:节点被 render 移除时延迟销毁,先把动画跑完再真正RemoveChildControl;animate:连续过渡,随目标值变化自动补间(见下)。
Animated 必须包裹 单个 ComponentNode(Panel / Image / Label / Button / Input / Image / Item / PaperDoll 均可,复合组件如 FilledButton 也行)。多个子元素先用 Panel 聚合。
from pyreact import Animated, Transition, Easing
Animated(
animate=Transition(
values={"opacity": 0.3 if dimmed else 1.0},
duration=250,
easing=Easing.easeOutQuad,
),
children=Panel(style=Style(...), children=[...]),
)- 目标值发生变化时,runtime 从"当前已应用值"补间到新目标;
- 也可直接传 dict:
animate={"opacity": alpha}使用默认 200ms / easeOut。
| 字段 | 生效方式 | 说明 |
|---|---|---|
opacity |
SetAlpha |
与 style.opacity 互斥:动画生效期间接管 |
translateX / translateY |
基于 layout 位置的偏移 | 不影响兄弟布局 |
width / height |
SetSize |
Label 跳过(其尺寸由文字自动测量) |
全部位于 pyreact.animation(已在顶层 pyreact.* 再导出):
from pyreact import (
fadeIn, fadeOut,
slideInUp, slideInDown, slideInLeft, slideInRight,
slideOutUp, slideOutDown, slideOutLeft, slideOutRight,
Easing, Animation, Transition,
)参数均带默认值,可按需覆盖:
fadeIn(duration=300, delay=0, easing=None)
slideInUp(distance=20, duration=300, delay=0, easing=None)from pyreact import Animation, Easing
Animation(
duration=400,
delay=50,
easing=Easing.easeOutCubic,
from_={"opacity": 0.0, "translateY": 20.0, "width": 0.0},
to={"opacity": 1.0, "translateY": 0.0, "width": 240.0},
onComplete=lambda: print('done'),
)Easing.linear / easeIn / easeOut / easeInOut / easeInQuad / easeOutQuad / easeInOutQuad / easeInCubic / easeOutCubic / easeInOutCubic / easeOutBack / easeInBack。
自定义:任意 (t: float) -> float 函数,f(0)=0, f(1)=1。
Pyreact 对未显式 key 的节点用路径(位置)生成 node_id。列表中元素在中间删除/插入时位置会变化,无 key 时 runtime 会把"移位"误判为"删除+新建",导致动画错乱。
# 正确
for item in items:
list_children.append(
Animated(
key=item.id, # key 必加
enter=fadeIn(),
exit=fadeOut(),
children=Panel(...),
)
)- 出场期间节点上的按钮
onClick不再响应(防止误触已开始告别的控件)。 - 无活跃动画时
GameRenderTickEvent的 tick 开销接近零(首行判空直接 return)。 - 动画正在控制的字段(opacity / position / size)在此期间不会被 render pipeline 的样式应用覆盖;动画结束自动解锁。
- 初次挂载时
enter.from_值会在同一个 native batch 内写入,不会出现"完成态→起点"的错闪。
pyreact/
├── components/ # 基础控件、Style、Color、enums
├── composites/ # 复合组件(FilledButton、ImageButton、Animated)
├── animation/ # 动画 API(Animation、Easing、Transition、预设)
├── core/ # 核心(VNode、Reconciler、Hooks、TreeBuilder)
├── layout/ # 布局引擎(Flexbox 计算、ShadowNode)
└── renderer/ # 文本测量等渲染辅助
PyreactRuntimeScript/
├── modMain.py # 运行时入口
├── PyreactNativeRuntime.py # 原生渲染桥接
└── native_runtime/ # 渲染细节
├── lifecycle_mixin.py # 生命周期 + 扁平渲染调度
├── props_mixin.py # 属性映射
├── native_api_mixin.py # 网易 API 封装
└── animation_mixin.py # 动画管理器 + 每帧 tick
PyreactExampleScript/
├── modMain.py # 示例入口
├── PyreactExampleClientSystem.py
├── PyreactExampleUi.py
└── examples/ # 示例组件
├── AnimationDemo.py # 动画示例(入场 / 出场 / animate)
├── FriendApp.py # 好友面板(筛选、搜索、详情)
├── BedwarStoreApp.py # 商店界面(分类、Scroll、Item)
└── BattlePassApp.py # 战令界面(双档位、任务、奖励)
切换示例:修改 PyreactExampleScript/PyreactExampleUi.py 中的 render_app 调用:
# 切换挂载不同的示例
render_app(root=CounterApp, bind=bind)
render_app(root=BattlePassApp, bind=bind)
render_app(root=FriendApp, bind=bind)
render_app(root=BedwarStoreApp, bind=bind)| 示例 | 演示内容 |
|---|---|
CounterApp |
基础 useState 计数、按钮点击更新、重置交互 |
FriendApp |
Tab 切换、搜索筛选、列表选择、详情面板、Scroll 滚动、useRef 控制 |
BedwarStoreApp |
商品分类、Item 物品展示、价格标签、购买交互 |
BattlePassApp |
多档位切换、任务列表、等级奖励轨道、Item 奖励 |
你要挂载pyreact的控件一定要继承@PyreactBase.rootBase才能使用
{
"main": {
"type": "screen",
"controls": [
{
"root@PyreactBase.rootBase": {}
}
]
},
"namespace": "YourNamespace"
}sync_to_test.cmd修改脚本参数可覆盖默认同步路径。