在 SerenityOS 中使用 REGISTER_WIDGET 注册库级与应用级自定义 Widget:GML 自定义组件完全指南
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
SerenityOS 的图形界面基于GML(GUI Markup Language)描述,而 GML 之所以能实例化任意控件,核心机制就是REGISTER_WIDGET()宏的组件注册体系。本文以 Base/usr/share/man/man5/GML/Define-widget.md 手册页为主线,结合 LibGUI 的 Widget.h 宏实现 与 LibWebView 的真实使用案例,完整讲解如何让库或应用程序自定义的 Widget 获得 GML 实例化能力,读完即可在自己的应用或库中落地自定义控件。
什么是"库或应用程序自定义 Widget"
SerenityOS 的 GML 文件通过@命名空间::类名 { ... }的形式声明控件树(参见 GML 语法手册),例如:
@GUI::Button { text: "Hello" }其中GUI::Button是LibGUI 库内置控件。但一些应用和库会发现,把自己定义的控件写进 GML 往往非常有用——例如浏览器应用需要<WebView>这样的自定义视图,系统监控器需要GraphWidget、MemoryStatsWidget这类专用组件。这正是REGISTER_WIDGET()宏存在的意义:它把自定义 C++ 控件类"登记"进 GML 的类注册表,使 GML 解析器能够在运行时按名字构造出对应的控件实例。
在 GML 语法手册 中可以看到这种能力的直接体现——注册过的应用专属控件可以直接写进 GML:
// 注册后,即可在 GML 中使用应用自己的控件 @SystemMonitor::GraphWidget { stack_values: true name: "memory_graph" } @SystemMonitor::MemoryStatsWidget { name: "memory_stats" memory_graph: "memory_graph" }REGISTER_WIDGET 宏的语法与要求
宏签名
REGISTER_WIDGET()的调用方式与LibGUI内部注册内置控件的方式完全一致:
REGISTER_WIDGET(namespace, class_name)该宏在 Userland/Libraries/LibGUI/Widget.h 中定义,其完整展开逻辑为:
#define REGISTER_WIDGET(namespace_, class_name) \ namespace GUI::Registration { \ GUI::ObjectClassRegistration registration_##class_name( \ #namespace_ "::" #class_name##sv, []() -> ErrorOr<NonnullRefPtr<GUI::Object>> { return static_ptr_cast<GUI::Object>(TRY(namespace_::class_name::try_create())); }, ®istration_Widget); \ }从展开代码可以看出三件事:
- 宏生成一个全局对象
GUI::Registration::registration_<class_name>,类型为GUI::ObjectClassRegistration; - 该对象以字符串
"命名空间::类名"(如"Web::OutOfProcessWebView")作为注册键,并关联一个工厂 lambda; - 工厂 lambda 调用
namespace_::class_name::try_create()来构造实例,父类注册项指向registration_Widget(即GUI::Widget),从而继承 Widget 的全部属性。
命名空间是硬性要求
宏展开后类名以"namespace_::class_name"的完整限定形式作为注册键,因此每个注册的 Widget 都必须放在某个命名空间中。对于通常没有专属命名空间的应用,社区通用的做法是直接使用应用名作为命名空间,例如浏览器应用使用Web(当前仓库中对应代码为 LibWebView/OutOfProcessWebView.cpp 中的REGISTER_WIDGET(WebView, OutOfProcessWebView),即以库名WebView作为命名空间),系统监控器使用SystemMonitor。这样既能避免命名冲突,也能让 GML 中的@命名空间::类名与 C++ 类型一一对应。
必须提供无参构造函数
注册的 Widget必须能够在没有任何参数的情况下被构造(no-argument constructor)。原因从宏展开中即可看出:GML 解析器只知道"类名 + 属性键值对",它无法猜测构造参数,只能依赖无参构造 + 随后设置属性的流程来还原控件。这条约束同时适用于:
- 类自身的构造函数(如
OutOfProcessWebView::OutOfProcessWebView()); - 注册工厂所调用的
try_create()(可返回ErrorOr,便于在构造阶段报错)。
注意:手册页示例写作
REGISTER_WIDGET(Web, OutOfProcessWebView),而当前仓库 LibWebView/OutOfProcessWebView.cpp 的实际代码为REGISTER_WIDGET(WebView, OutOfProcessWebView)——请以仓库源码为准,命名空间取WebView。
一个完整的实战示例:OutOfProcessWebView
GML 侧:声明自定义控件
注册完成后,GML 文件就能像使用内置控件一样使用自定义控件:
@Web::OutOfProcessWebView { name: "web_view" min_width: 340 min_height: 160 visible: false }属性含义与内置控件完全一致:
name:控件标识,供 C++ 侧通过find_descendant_of_type_named查找(详见 GML 使用手册);min_width/min_height:最小尺寸约束(支持ui_dimension类型,见 UI Dimensions 手册);visible:初始可见性。
C++ 侧:注册与构造
对应实现位于 Userland/Libraries/LibWebView/OutOfProcessWebView.cpp:
// OutOfProcessWebView.cpp REGISTER_WIDGET(WebView, OutOfProcessWebView) ... OutOfProcessWebView::OutOfProcessWebView() { set_focus_policy(GUI::FocusPolicy::StrongFocus); initialize_client(CreateNewClient::Yes); on_ready_to_paint = [this]() { update(); }; // ... 其余回调初始化 }值得注意的实现细节:
- 注册语句放在命名空间外、源文件顶部:
REGISTER_WIDGET展开后自建GUI::Registration命名空间,因此在.cpp文件顶部(namespace WebView {之前)直接书写即可,无需包裹在任何命名空间内; - 构造函数中完成默认初始化:由于 GML 实例化走无参构造,任何"必须存在的默认行为"(焦点策略、客户端创建、回调绑定)都要在构造函数里完成,而不是依赖外部传入参数;
- 手册页原示例中的
set_should_hide_unnecessary_scrollbars(true)在当前仓库版本已由滚动条机制统一管理,但"构造函数内做默认初始化"的原则不变。
注册机制与 GML 解析器的联动原理
从注册表到实例化
REGISTER_WIDGET生成的GUI::ObjectClassRegistration对象在程序启动时完成静态初始化,把"命名空间::类名"注册进全局类注册表。GML 解析器(位于 LibGUI 的GML子目录)遇到@Foo::Bar {}时,会按字符串在注册表中查找对应的注册项,并调用其工厂 lambda 完成try_create()构造。这在 Userland/Libraries/LibGUI/GML/AutocompleteProvider.cpp 中有直接佐证——GML 的自动补全与校验正是通过GUI::ObjectClassRegistration::find()与for_each()遍历所有已注册控件类来工作的。
控件树加载方式
新式加载采用编译期生成 C++ 代码的方式:在应用的 CMakeLists 中加入compile_gml(),生成实现try_create()的源文件(详见 GML 使用手册)。加载后即可用find_descendant_of_type_named按name属性取回控件:
@MyApp::Widget { @GUI::Button { name: "mem_add_button" text: "M+" } }m_mem_add_button = *find_descendant_of_type_named<GUI::Button>("mem_add_button");在运行时还会自动调用类的initialize()成员(若存在),用于绑定回调、挂接数据模型等初始化逻辑。
配套机制:自定义属性(REGISTER_PROPERTY 系列宏)
自定义控件只有控件结构往往还不够,通常还需要暴露控件专属属性,例如SystemMonitor::MemoryStatsWidget的memory_graph属性。这由REGISTER_*_PROPERTY系列宏完成,其通用语法为(详见 GML 属性定义手册):
REGISTER_TYPENAME_PROPERTY(property_name, getter, setter [, additional parameters...]);示例:
REGISTER_STRING_PROPERTY("alt_text", alt_text, set_alt_text); // enum 属性必须列出所有枚举值与其 GML 字符串表示 REGISTER_ENUM_PROPERTY( "button_style", button_style, set_button_style, Gfx::ButtonStyle, { Gfx::ButtonStyle::Normal, "Normal" }, { Gfx::ButtonStyle::Coolbar, "Coolbar" });关键约束:
- 宏在构造函数中调用;
property_name是 GML 可用的字符串键; getter不接收参数、返回属性类型值;setter接收属性类型值(int/string/readonly_string/enum/bool为 C++ 值,其余类型为 JSON 值),两者都不可省略但可做成 no-op;- 只读字符串用
REGISTER_READONLY_STRING_PROPERTY; - 大多数宏定义在
Core::Object头文件中,布局相关属性位于GUI::Layout头文件中; - 自定义控件自动继承父类(
GUI::Widget)的所有属性,如x、y、name、visible、min_width等。
实践要点速查
| 要点 | 说明 |
|---|---|
| 宏位置 | 在.cpp文件顶部、命名空间外调用REGISTER_WIDGET(ns, Class) |
| 命名空间 | 必须存在;应用无专属命名空间时,惯例用应用名作命名空间 |
| 构造函数 | 必须无参可构造;默认行为初始化一律放入构造函数 |
| 注册键 | "命名空间::类名",GML 中必须用@命名空间::类名完全一致地引用 |
| 属性继承 | 自定义控件自动获得GUI::Widget的全部通用属性 |
| 自定义属性 | 用REGISTER_*_PROPERTY系列宏在构造函数中注册专属属性 |
| 运行时查找 | 通过find_descendant_of_type_named<T>("name")获取 GML 中的实例 |
参见
- GML 属性定义手册(Define-property):
REGISTER_*_PROPERTY宏详解 - GML 语法手册(Syntax):GML 文件结构、属性类型与布局语法
- GML 使用手册(Usage):
compile_gml()与try_create()的加载流程 - GML 内置控件参考(Widget):LibGUI 各内置控件的可用属性
- UI Dimensions 手册:
min_width等尺寸属性的取值规则 - 源码:宏定义见 Userland/Libraries/LibGUI/Widget.h,实际使用案例见 Userland/Libraries/LibWebView/OutOfProcessWebView.cpp
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考