本文合并了两篇开发文档: Easy_Desktop 载体几何插件开发文档 标记规则编写文档
本文合并了两篇开发文档:
本文档面向前来开发 Easy_Desktop 载体几何插件的开发者。内容包括接口说明、开发流程、常见坑与注意事项。
载体几何插件(Carrier Geometry Plugin)是 Easy_Desktop 提供的一类扩展插件,运行在可载体(Container)内部,用于对可载体内的 Basic_Widget 施加动画、几何变换、布局控制、视觉效果等。
Basic_Widget
插件与宿主之间通过两套接口通信:
Ext_Carrier_Geometry_PluginInterface
Carrier_Geometry_Interface
插件通过 QPluginLoader 动态加载,通过 Q_DECLARE_INTERFACE 做接口识别。
QPluginLoader
Q_DECLARE_INTERFACE
Easy_Desktop 提供三个可载体:
Desktop_Main
My_Process_Carrier
Self_Carrier
每个可载体实例只能挂载一个几何插件。
宿主是 Basic_Widget 的唯一所有者,生命周期由 Qt 父子树管理。
插件只能借用 Basic_Widget *,不能 delete,不能 setParent 到非宿主期望的容器。插件对控件的所有修改必须通过 Carrier_Geometry_Interface。
Basic_Widget *
delete
setParent
插件不 include 宿主内部头文件,如 basic_widget.h、desktop_main.h。插件头文件里 Basic_Widget 是前置声明的不完整类型。
basic_widget.h
desktop_main.h
插件工程只依赖 interfaces/carrier-geometry/ 和 interfaces/easy_desktop_class.h。
interfaces/carrier-geometry/
interfaces/easy_desktop_class.h
插件必须在 on_widget_removed 里清空对该控件的所有引用。控件析构函数会调用宿主 unregister_widget,宿主再调用插件 on_widget_removed,此时对象完整,插件可以安全访问。
on_widget_removed
unregister_widget
视觉变换(旋转、倾斜、缩放、透明度、伪透视)只影响渲染,不影响命中。宿主在鼠标靠近时会把控件的变换临时重置为 identity,保证点击命中正确。
插件不自己 new QTimer 做动画。所有周期性任务通过 tick_interval_ms() 和 on_tick() 由宿主统一调度。
new QTimer
tick_interval_ms()
on_tick()
插件只能操作当前挂载的可载体。当前页的直接子项如果是另一个可载体(Self_Carrier / My_Process_Carrier),插件只能当普通 Basic_Widget 处理,不能进入它内部。
宿主只保存插件 .so 路径。插件内部状态由插件自己保存,宿主通过 on_host_save() 通知。插件通常用 QStandardPaths::AppDataLocation 或固定的 ~/.local/lib/easy_desktop_geometry_plugins/ 存到自己的路径。
.so
on_host_save()
QStandardPaths::AppDataLocation
~/.local/lib/easy_desktop_geometry_plugins/
插件可以拿裸指针做黑魔法,比如 reinterpret_cast、直接访问宿主内部。宿主不拦,但崩了是插件自己的问题。
reinterpret_cast
插件 .json 文件:
.json
{ "Ext_Carrier_Geometry_Plugin": "" }
注意事项:
api
Plugin_Version
插件类里:
P_Version Plugin_Version{0, 0, 1};
宿主检查 Plugin_Version >= P_Version{0, 0, 1},不满足则拒绝加载。
Plugin_Version >= P_Version{0, 0, 1}
IID 必须与宿主头文件里的 Q_DECLARE_INTERFACE 字符串一致,否则 qobject_cast 失败。 四、接口说明 4.1 必须实现的三个接口 以下三个方法是纯虚函数,插件必须实现,否则链接失败:
IID 必须与宿主头文件里的 Q_DECLARE_INTERFACE 字符串一致,否则 qobject_cast 失败。
qobject_cast
以下三个方法是纯虚函数,插件必须实现,否则链接失败:
void RemovePlugin()
QString Plugin_Name()
void init(Carrier_Geometry_Interface *ptr)
ptr
以下方法都有默认空实现,插件按需 override:
void clear()
int tick_interval_ms() const
void on_tick(qint64 elapsed_ms)
void on_widget_added(Basic_Widget *widget, int page)
void on_widget_removed(Basic_Widget *widget, int page)
void on_widget_page_changed(Basic_Widget *widget, int old_page, int new_page)
void on_widget_geometry_changed(Basic_Widget *widget, const QRect &old_rect, const QRect &new_rect)
void on_widget_selected_changed(Basic_Widget *widget, bool selected)
void on_widget_z_changed(Basic_Widget *widget, int old_z, int new_z)
void on_widget_pressed(Basic_Widget *widget)
void on_widget_released(Basic_Widget *widget)
void on_page_changed(int old_page, int new_page)
void on_container_resized(const QSize &old_size, const QSize &new_size)
void on_host_save()
void call_settings_widget()
void update_style(...)
void Trans_Update()
void Get_Loader(const QStringList &loaderNames, const P_Sender *seek_loader_sender, const P_Sender *recive_loader_sender)
void easy_desktop_version(const P_Version &Easy_Desktop_Version, const QString &AKA_name)
插件通过 Carrier_Geometry_Interface 操作可载体内的控件:
container_widget
QWidget *
container_rect
QRect(0, 0, w, h)
carrier_id
page_count
current_page
set_current_page
on_page_changed
list_widgets(page)
list_all_widgets
widget_page
get_widget_info
is_widget_pressed
any_widget_pressed
pressed_widgets
get_geometry
QRect
set_geometry
on_widget_geometry_changed
move_by
set_geometry(widget, rect.translated(delta))
set_transform
get_transform
set_visible
set_selected
is_selected
set_z_index
get_z_index
send_signal
Basic_Widget::dispatch_signal
take_in
take_out
mouse_global_pos
mouse_in_container
capabilities
宿主 QPluginLoader::load() ↓ 元数据校验 (缺失则询问用户) ↓ qobject_cast 到插件接口 ↓ 版本校验 ↓ 宿主调用 plugin->easy_desktop_version(版本, AKA) ↓ 宿主设置 plugin->your_plugin_loader / plugin_path / inited ↓ 宿主调用 host->set_plugin(plugin) ├─ 调用 plugin->init(interface) ├─ 读 plugin->tick_interval_ms(), 启动 tick 定时器 └─ 遍历已有控件, 调 plugin->on_widget_added ↓ 宿主调用 plugin->Get_Loader(loaderNames, seek, recv) ↓ 宿主启动 tick 定时器, 周期调 plugin->on_tick ↓ 用户操作触发 on_widget_* / on_page_changed / on_container_resized ↓ 用户保存触发 on_host_save ↓ 用户点"几何插件设置"触发 call_settings_widget ↓ 用户卸载插件或可载体销毁 ↓ 宿主调用 plugin->RemovePlugin ↓ QPluginLoader::unload()
重要时机说明:
easy_desktop_version()
init()
Get_Loader()
on_tick
on_widget_added
on_widget_added()
Carrier_Transform 字段:
Carrier_Transform
rotation
qreal
0.0
tilt_x
tilt_y
scale_x
1.0
scale_y
opacity
perspective
enabled
bool
false
allow_suppress
true
通过 set_transform(widget, transform) 设置,通过 get_transform(widget, &t) 读取。
set_transform(widget, transform)
get_transform(widget, &t)
enabled = false 时,宿主会移除该控件的视觉效果,控件恢复默认绘制。
enabled = false
宿主通过 capabilities() 返回支持的功能位。插件应根据能力位降级:
capabilities()
enum Carrier_Geometry_Capability { Cap_None = 0, Cap_Move = 1 << 0, Cap_Resize = 1 << 1, Cap_Rotate = 1 << 2, Cap_Scale = 1 << 3, Cap_Opacity = 1 << 4, Cap_Perspective = 1 << 5, Cap_ZIndex = 1 << 6, Cap_PageControl = 1 << 7, Cap_MouseSuppress = 1 << 8, Cap_TakeInOut = 1 << 9, Cap_SendSignal = 1 << 10, Cap_PressState = 1 << 11 };
插件:
quint64 caps = m_interface->capabilities(); if (caps & Cap_Rotate) { // 支持旋转 }
QWidget 没有 setZValue。宿主在 Basic_Widget 里实现:
QWidget
setZValue
int m_z_index
set_z_index(int)
update_z_order()
raise()
save
load
跨页、跨载体的控件不参与排序,因为不在同一父对象下。
on_widget_page_changed
on_widget_selected_changed
on_widget_z_changed
on_widget_pressed
on_widget_released
on_container_resized
on_host_save
QEvent::ParentChange
register_widget
插件推荐用:
QHash> m_widgets; QHash m_page_map; QHash m_states;
Basic_Widget * 做 key 便于 O(1) 查找,QPointer 做值兜底对象销毁。
QPointer
on_widget_removed 里三个 map 都要清空对应项。
carrier_geometry_plugin_path
插件内部状态由插件自己保存。宿主通过 on_host_save() 通知,无参数。
插件通常用:
QString dir = QDir::homePath() + "/.local/lib/easy_desktop_geometry_plugins"; QDir().mkpath(dir); QString path = dir + "/My_Plugin.ini"; QSettings settings(path, QSettings::IniFormat); settings.setValue("key", value);
保存时建议只写与默认值不同的键:
if (m_gravity != k_default_gravity) settings.setValue("gravity", m_gravity);
宿主 load() 时:
load()
plugin->init(interface)
插件在 init() 里自己读参数,不需要宿主通知 load。
宿主菜单"几何插件设置"点击后:
void Carrier_Geometry_Plugin_Root::call_plugin_settings() { if (m_plugin == nullptr) { return; } m_plugin->call_settings_widget(); }
void My_Plugin::call_settings_widget() { if (m_settings_dialog != nullptr) { m_settings_dialog->show(); m_settings_dialog->raise(); m_settings_dialog->activateWindow(); return; } My_Settings_Dialog *dialog = new My_Settings_Dialog(nullptr); connect(dialog, &My_Settings_Dialog::settings_changed, this, [this](qreal value) { m_value = value; save_settings(); }); m_settings_dialog = dialog; dialog->show(); }
m_settings_dialog
nullptr
hide
RemovePlugin
.pro
QT = core gui widgets TEMPLATE = lib CONFIG += plugin c++11 debug CONFIG -= qml quick ltcg release HEADERS += \ my_plugin.h \ my_settings_dialog.h \ interfaces/easy_desktop_class.h \ interfaces/carrier-geometry/carrier_geometry_types.h \ interfaces/carrier-geometry/carrier_geometry_interface.h \ interfaces/carrier-geometry/carrier_geometry_plugin_interface.h SOURCES += \ my_plugin.cpp \ my_settings_dialog.cpp \ interfaces/easy_desktop_class.cpp \ interfaces/carrier-geometry/carrier_geometry_interface.cpp \ interfaces/carrier-geometry/carrier_geometry_plugin_interface.cpp
关键点:
CONFIG += plugin c++11 debug
CONFIG -= qml quick ltcg release
debug
qmake my_plugin.pro make -j$(nproc)
若 .pro 里已经写明了 CONFIG += debug,则 qmake 命令行里可以省去 CONFIG+=debug。
CONFIG += debug
qmake
CONFIG+=debug
产物是 libmy_plugin.so(Linux)。
libmy_plugin.so
Easy_Desktop 右键可载体 → "载入几何插件" → 选中 .so。
症状:
undefined symbol
原因:
-O2/-O3
解法:在 .pro 里写 CONFIG += debug,CONFIG -= release ltcg。
CONFIG -= release ltcg
症状:[Geo] fail: no Ext_Carrier_Geometry_Plugin key。
[Geo] fail: no Ext_Carrier_Geometry_Plugin key
原因:.json 里也写了
解法:
症状:用 QPointer 编译不过。
原因:插件头文件里只前置声明 class Basic_Widget;,编译器不知道它继承 QObject。
class Basic_Widget;
QObject
QObject *
static inline QObject *basic_widget_as_qobject(Basic_Widget *widget) { return reinterpret_cast(widget); }
症状:控件被关闭后,插件在下一帧 on_tick 里访问悬空指针,崩溃。
原因:控件析构时宿主会调 on_widget_removed,但插件忘记在这个回调里删除自己的缓存。
解法:每个存储了 Basic_Widget * 的容器,都要在 on_widget_removed 里删除对应项。
void My_Plugin::on_widget_removed(Basic_Widget *widget, int page) { (void) page; m_widgets.remove(widget); m_states.remove(widget); m_velocities.remove(widget); }
症状:on_tick 里随机崩溃,或漏处理部分控件。
原因:遍历 QHash 时直接 remove 会破坏迭代器。
QHash
remove
解法:先收集要删的,循环外统一删:
void My_Plugin::on_tick(qint64 elapsed_ms) { QList dead; for (auto it = m_widgets.begin(); it != m_widgets.end(); ++it) { if (it.value().isNull()) { dead.append(it.key()); continue; } // 正常处理 } for (Basic_Widget *w : dead) { m_widgets.remove(w); m_states.remove(w); } }
症状:移除插件时崩,栈里出现 sendPostedEvents、QObject::event。
sendPostedEvents
QObject::event
原因:dialog 用了 WA_DeleteOnClose,close() 只是排队了 DeferredDelete。RemovePlugin 返回后宿主 m_loader->unload() 卸载 .so,事件循环再处理 DeferredDelete 时,dialog 的析构函数在已卸载内存里。
WA_DeleteOnClose
close()
DeferredDelete
m_loader->unload()
解法:RemovePlugin 里同步删除:
void My_Plugin::RemovePlugin() { if (m_settings_dialog != nullptr) { m_settings_dialog->disconnect(this); // 如果有 QComboBox, 先关掉 popup const QList combos = m_settings_dialog->findChildren(); for (QComboBox *combo : combos) { if (combo != nullptr) { combo->hidePopup(); } } m_settings_dialog->hide(); delete m_settings_dialog; m_settings_dialog = nullptr; } // 清空所有缓存 m_widgets.clear(); m_states.clear(); m_interface = nullptr; }
同时 call_settings_widget 里不要设 WA_DeleteOnClose。
call_settings_widget
QComboBox
症状:只有带 QComboBox 的插件崩溃,纯 QDoubleSpinBox 的插件不崩。
QDoubleSpinBox
原因:QComboBox 的下拉菜单是一个独立的顶层 QWidget popup,不受 dialog 父子树管理。dialog 析构时 popup 被 deleteLater,但如果 .so 已卸载,popup 的析构函数就跳到了已卸载内存。
deleteLater
解法:dialog 析构前显式 combo->hidePopup(),让 popup 立即关闭并释放。
combo->hidePopup()
dynamic_cast
症状:编不过,或运行崩溃。
原因:插件侧 Basic_Widget 是不完整类型,qobject_cast 或 dynamic_cast 需要完整类型定义。
解法:不要这样做。所有操作走 Carrier_Geometry_Interface。
症状:插件卸载后宿主定时器还在触发,回调打进已卸载内存。
原因:插件自己建 timer,卸载时没停。
解法:用宿主提供的 on_tick。所有周期性任务通过 tick_interval_ms() 和 on_tick() 由宿主统一调度。
症状:宿主整体卡顿,甚至主窗口响应迟滞。
原因:on_tick 在主线程调用,遍历每个控件调 set_geometry / set_transform 都会触发宿主内部排序、重绘。
症状:调用设置窗口后,宿主所有动画停止。
原因:用了 dialog->exec() 而不是 dialog->show()。exec() 会启动一个嵌套事件循环,阻塞当前调用栈。
dialog->exec()
dialog->show()
exec()
解法:用 show(),不要用 exec()。
show()
症状:调 send_signal(widget, Signal::xxx, data) 编译报错。
send_signal(widget, Signal::xxx, data)
原因:send_signal 的参数是 int signal,不是 Signal 枚举。
int signal
Signal
m_interface->send_signal(widget, static_cast(Signal::Run), QVariant());
症状:每帧 set_z_index 导致宿主反复排序和 raise(),性能下降。
int target_z = qRound((1.0 - depth) * 1000.0); if (qAbs(state.current_z - target_z) > 10) { m_interface->set_z_index(w, target_z); state.current_z = target_z; }
症状:升级宿主或插件后,变换效果错乱。
原因:Carrier_Transform 是 POD 结构,宿主和插件各自编译一份。如果两边版本不同,内存布局不一致。
解法:宿主和插件的 interfaces/carrier-geometry/ 目录内容必须完全一致,改的时候两边一起改,一起编译。
症状:插件卸载后宿主对象还在用,或宿主对象销毁后插件还在用。
原因:插件从宿主那拿了一个 QObject * 并长期持有。
解法:不要从宿主拿宿主内部的对象指针。所有跨边界的操作走 Carrier_Geometry_Interface。
update_style
Trans_Update
症状:插件界面文字不跟随系统语言,或主题色不更新。
原因:没有 override 这两个接口。
解法:如果插件有界面,要 override 这两个方法:
void My_Plugin::update_style(...) { // 把主题应用到 dialog } void My_Plugin::Trans_Update() { // 重新设置界面文字 }
宿主侧 load_plugin 有 [Geo] 前缀的调试日志。加载失败时看控制台会知道卡在哪一步:
load_plugin
[Geo]
fail: QPluginLoader::load error = ...
fail: no Ext_Carrier_Geometry_Plugin key
fail: qobject_cast to interface failed
Q_INTERFACES
load_plugin success
gdb ./Easy_Desktop (gdb) run # 复现崩溃 (gdb) bt full
看完整调用栈和局部变量。
-O2
即使宿主可以开优化,插件也不要。见坑 1。
插件开发完,交付前检查:
Ext_Carrier_Geometry_Plugin
hidePopup()
interfaces/*.cpp
signal
static_cast
interfaces/carrier-geometry/carrier_geometry_plugin_interface.h
interfaces/carrier-geometry/carrier_geometry_interface.h
interfaces/carrier-geometry/carrier_geometry_types.h
P_Version
P_Sender
本文档说明如何为 Mark_Rule_Highlighter 编写自定义语法标记规则。规则文件支持 JSON 与 INI 两种格式,加载器按文件扩展名自动识别:
Mark_Rule_Highlighter
.ini
.conf
.cfg
两种格式表达能力完全相同,只是书写风格不同。选哪种取决于个人偏好:规则里正则多、字段杂时 JSON 更清晰;规则短、想快速改时 INI 更省事。
一份规则文件必须包含 版本号 与 至少一条规则。
{ "version": "0.0.1", "language": "MyLang", "rules": [ { "name": "keyword", "pattern": "\\b(?:if|else|while|for)\\b", "color": "#0000ff", "bold": true } ] }
加载后,文本里出现的 if / else / while / for 会被染成蓝色粗体。 二、文件结构 JSON 结构 { "version": "0.0.1", "language": "语言显示名", "rules": [ /* 规则数组 */ ] }
加载后,文本里出现的 if / else / while / for 会被染成蓝色粗体。
if
else
while
for
{ "version": "0.0.1", "language": "语言显示名", "rules": [ /* 规则数组 */ ] }
version
language
rules
rule_count
[rule_0]
[rule_N-1]
rule_序号
INI 段名必须是连续的 rule_0、rule_1……加载器按 rule_count 逐个查找,缺一个就报错。
rule_0
rule_1
每条规则支持以下字段。JSON 里是键值对,INI 里是 键=值。两种格式含义完全一致。
键=值
name
rule_N
kind
"singleline"
"multiline"
pattern
start
end
capture
0
color
#RRGGBB
#AARRGGBB
background
bold
italic
underline
strikeout
color 与 background 都可省略;若两者都省略且未设任何样式,规则不会产生任何视觉效果(等价于无效规则)。
singleline
在一行文本内扫描正则表达式,对每个匹配上色。不跨行。
{ "name": "keyword", "pattern": "\\bif\\b", "color": "#0000ff" }
对应 INI:
[rule_0] name=keyword kind=singleline pattern=\bif\b color=#0000ff
kind 字段可省略,默认就是 singleline。
multiline
用于跨行的成对结构,比如块注释、多行字符串、JSDoc 等。
{ "name": "block_comment", "kind": "multiline", "start": "/\\*", "end": "\\*/", "color": "#808080", "italic": true }
[rule_0] name=block_comment kind=multiline start=/\* end=\*/ color=#808080 italic=true
行为:
多行规则的状态通过 Qt 的 setCurrentBlockState() 保存,因此不同多行规则之间互不干扰(内部用 规则索引 + 1 作为状态码)。
setCurrentBlockState()
规则索引 + 1
这是最有用、也最容易被忽略的字段。
假设要标记 C++ 函数名 foo,直接写:
foo
{ "name": "function", "pattern": "[A-Za-z_]\\w*\\s*\\(", "color": "#0066cc" }
这会连同函数名后面的 ( 一起上色,视觉上很难看。且若正则加了捕获组,比如 ([A-Za-z_]\\w*)\\s*\\(,仍然会把整段(含空格、括号)一起染。
(
([A-Za-z_]\\w*)\\s*\\(
{ "name": "function", "pattern": "([A-Za-z_]\\w*)\\s*\\(", "capture": 1, "color": "#0066cc", "bold": true }
capture: 1 表示"只把第 1 个捕获组的内容上色"。匹配结果是整段 foo(,但只有 foo 被染色,( 不受影响。
capture: 1
foo(
capture: 0
(...)
capture: 2
若捕获组匹配到空内容(例如 (\w*) 匹配到零长度),该匹配会被跳过,不会产生视觉效果。
(\w*)
(?:...) 是非捕获组,不占用编号:
(?:...)
{ "pattern": "(?:foo|bar)(\\d+)", "capture": 1 }
上面第 1 个捕获组是 (\d+),不是 (?:foo|bar)。
(\d+)
(?:foo|bar)
{ "name": "function", "pattern": "(?
(?:负向后顾,确保函数名前不是字母或数字,避免从 xfoo( 里截出 foo。 ([a-z_]\\w*):捕获以小写或下划线开头的标识符(约定的函数命名)。 \\s*\\(:后面跟左括号。 capture: 1:只给函数名染色,( 保持默认色。 这样 bar(x) 中 bar 蓝色粗体,x 无色,( 无色。 六、规则顺序与优先级 QSyntaxHighlighter 是"后应用覆盖先应用"。加载器按 rules 数组顺序(INI 按 rule_0、rule_1……)逐条应用,后一条的颜色会覆盖前一条。 6.1 一般排列原则 宽泛规则在前,具体规则在后。 这样具体规则可以覆盖宽泛规则,得到视觉上更精确的结果。 典型顺序(从先到后): 关键字 类型名 数字 运算符 标点 函数名 预处理指令 字符串 字符 行注释 块注释(多行) 把注释、字符串放最后,可以保证字符串里的 for、注释里的 if 不被关键字规则误染。 6.2 反例 若把关键字放在字符串之后: [ { "name": "string", "pattern": "\".*?\"" }, { "name": "keyword", "pattern": "\\bfor\\b" } ] 则 "for" 这段字符串会先被字符串规则上色,随后 for 又被关键字规则覆盖成蓝色,显示为 "for"(引号红、for 蓝),这显然不对。把关键字放前面,字符串在后,就能正确覆盖。 七、正则表达式语法 底层使用 QRegularExpression,语法是 PCRE 兼容的。常用要点: 7.1 转义 JSON:正则里的反斜杠要写成 \\。\\b 表示正则里的 \b,\\d 表示 \d。 INI:值原样读取,反斜杠 不需要 二次转义,直接写 \b、\d。 这也是两种格式最容易踩坑的地方。 7.2 常用元字符 写法 含义 \\b / \b 单词边界 \\w / \w 字母、数字、下划线 \\d / \d 数字 \\s / \s 空白 . 除换行外任意字符 * + ? 重复,? 加在量词后表示非贪婪 (?:...) 非捕获组 (...) 捕获组 (?=...) 正向前瞻 (?!...) 负向前瞻 (?<=...) 正向后顾 (? 负向后顾 [...] 字符类 [^...] 排除字符类 `\ /|` 7.3 正则中的分隔符问题 INI 里若正则里含 =(等号),要小心键值对解析。加载器按 第一个 = 拆分键值,所以值里的 = 不受影响: pattern=[;#][^\r\n]* 值里的 = 无需转义;但若正则的键名部分也想写 =,那就得注意:键名走的是第一个 = 之前的字符串,不会出问题。 若值里含 # 或 ; 开头,且 不在行首,也不会被当注释,因为加载器只把 整行以 ; 或 # 开头 的行当注释。 7.4 值两端引号会被剥离(注意!) INI 值若 两端都是双引号,加载器会把引号剥掉: pattern="\bif\b" 与 pattern=\bif\b 等价。但这个特性有副作用:如果正则本身以双引号开头结尾(比如要匹配 """ 三引号),写 start=""" 会被剥成 start="(只剩一个引号),完全偏离原意。 因此: 含引号的正则,建议改用 JSON 书写。 或者在 INI 里给值最外层再套一层引号(但这需要正则本身不以引号开头结尾),例如要表达正则 \bif\b 时写 pattern="\bif\b" 是安全的。 绝对不要在 INI 里写 start=""" 这种"正好两端都是引号、而且中间还是引号"的形式。 7.5 单行注释与行首字符 加载器只把 整行以 ; 或 # 开头 的行当注释。所以: pattern=;[^\n]* 是合法的,; 在值的开头不会被当注释行。但如果写成: ; 这是注释 则整行会被跳过。 八、颜色写法 支持两种十六进制格式: 格式 示例 含义 #RRGGBB #0066cc 不透明,6 位 #AARRGGBB #80ff0000 带透明度,8 位 不写 # 也接受,但建议始终带上,可读性更好。 background 建议用 8 位格式加透明度,避免遮住文本: { "name": "highlight", "pattern": "TODO", "background": "#40ff0000" } 40 是 alpha,约 25% 不透明,红色背景淡淡一层。 九、完整示例 9.1 JSON:简单配置语言 { "version": "0.0.1", "language": "SimpleConfig", "rules": [ { "name": "section", "pattern": "^\\s*\\[[^\\]]*\\]", "color": "#0070c0", "bold": true }, { "name": "key", "pattern": "^\\s*([^=;#\\[\\]]+?)\\s*(?==)", "capture": 1, "color": "#c05000", "bold": true }, { "name": "equals", "pattern": "=", "color": "#808080" }, { "name": "value", "pattern": "=\\s*(.*)$", "capture": 1, "color": "#a31515" }, { "name": "comment", "pattern": "[;#][^\\r\\n]*", "color": "#808080", "italic": true } ] } 9.2 INI:等价版本 9.3 多行字符串示例(Python 风格)—— 只能用 JSON 要匹配 Python 的三引号字符串 """...""",正则本身以 """ 开头结尾,在 INI 里无法正确书写(会被引号剥离逻辑误伤,见 7.4 节)。这类规则必须用 JSON: { "name": "triple_string", "kind": "multiline", "start": "\"\"\"", "end": "\"\"\"", "color": "#a31515" } 反例(不要这样写): [rule_0] name=triple_string kind=multiline start=""" end=""" color=#a31515 这段 INI 会被解析为: start = "(两端引号被剥掉后只剩一个引号) end = " 结果 start / end 都成了单引号 ",只能匹配单个双引号,而不是三引号。 十、加载与清空 在编辑器里: 右键 → 自动标记规则 → 加载标记规则:弹出文件对话框,选择 .json / .ini 等文件即可加载。加载成功后弹窗提示语言名与版本号。 右键 → 自动标记规则 → 清空标记规则:清除所有规则,文本恢复默认着色。 加载失败时会弹窗给出具体错误,例如"段 [rule_3] 缺少 pattern 字段"、"JSON 解析失败:…"。 十一、常见问题 Q:规则写对了但没效果? 检查三件事:① color 是否合法颜色;② kind 是否拼错(必须是 singleline 或 multiline);③ 是否被后面的规则覆盖了(把这条规则往后挪)。 Q:为什么字符串里的关键字也被染色了? 关键字规则在字符串规则之前,字符串规则没能覆盖它。把字符串规则放到关键字规则之后。 Q:多行注释不生效? 检查 start 和 end 是否都非空且是合法正则。JSON 里 \* 要写 \\*。 Q:INI 里的 \b 要不要转义? 不要。INI 值原样读取,\b 就是正则的 \b。JSON 里才需要写 \\b。 Q:capture 填了但整段仍被染色? 检查正则里是否真的有对应数量的捕获组。(?:...) 不算捕获组。 Q:规则文件版本必须递增吗? 不强制,但建议每次修改都递增,便于排查问题时确认版本。 Q:可以在一个规则里同时写 pattern 和 start/end 吗? 不可以。kind 决定用哪一组:singleline 只读 pattern,multiline 只读 start/end,另一组会被忽略。 Q:INI 值里的引号怎么处理? 仅当值两端都是双引号时才会被剥掉。若正则本身以引号开头结尾(比如 """),会被误剥。含引号的正则建议改用 JSON。 Q:我想在 INI 里写一个含引号的正则怎么办? 没办法安全地写。改用 JSON。 十二、字段速查表 字段 JSON 写法 INI 写法 是否必填 版本 "version": "0.0.1" version=0.0.1 ✅ 语言名 "language": "C++11" language=C++11 ❌ 规则条数 由 rules 长度决定 rule_count=N ✅(INI) 规则名 "name": "keyword" name=keyword ❌ 类型 "kind": "singleline" kind=singleline ❌(默认单行) 单行正则 "pattern": "..." pattern=... 单行规则 ✅ 多行起始 "start": "..." start=... 多行规则 ✅ 多行结束 "end": "..." end=... 多行规则 ✅ 捕获组 "capture": 1 capture=1 ❌(默认 0) 前景色 "color": "#0000ff" color=#0000ff ❌ 背景色 "background": "#40ff0000" background=#40ff0000 ❌ 粗体 "bold": true bold=true ❌ 斜体 "italic": true italic=true ❌ 下划线 "underline": true underline=true ❌ 删除线 "strikeout": true strikeout=true ❌ 十三、小结 写规则的核心是三步: 确定高亮对象:关键字?函数名?字符串?注释?分别用独立规则。 写好正则:合理使用 \b、(?、(?:...)、(...),尽量用 capture 精确锁定要染色的部分。 排好顺序:宽泛在前,具体在后;注释和字符串放最后,避免被关键字规则覆盖。 按这套方法,任何语言的语法都能拆成若干条规则描述出来。若规则复杂、含大量引号或反斜杠,一律用 JSON;若规则简单、想快速改,用 INI。
(?:负向后顾,确保函数名前不是字母或数字,避免从 xfoo( 里截出 foo。
xfoo(
([a-z_]\\w*):捕获以小写或下划线开头的标识符(约定的函数命名)。 \\s*\\(:后面跟左括号。 capture: 1:只给函数名染色,( 保持默认色。
([a-z_]\\w*)
\\s*\\(
这样 bar(x) 中 bar 蓝色粗体,x 无色,( 无色。 六、规则顺序与优先级 QSyntaxHighlighter 是"后应用覆盖先应用"。加载器按 rules 数组顺序(INI 按 rule_0、rule_1……)逐条应用,后一条的颜色会覆盖前一条。 6.1 一般排列原则 宽泛规则在前,具体规则在后。 这样具体规则可以覆盖宽泛规则,得到视觉上更精确的结果。 典型顺序(从先到后): 关键字 类型名 数字 运算符 标点 函数名 预处理指令 字符串 字符 行注释 块注释(多行) 把注释、字符串放最后,可以保证字符串里的 for、注释里的 if 不被关键字规则误染。 6.2 反例 若把关键字放在字符串之后: [ { "name": "string", "pattern": "\".*?\"" }, { "name": "keyword", "pattern": "\\bfor\\b" } ] 则 "for" 这段字符串会先被字符串规则上色,随后 for 又被关键字规则覆盖成蓝色,显示为 "for"(引号红、for 蓝),这显然不对。把关键字放前面,字符串在后,就能正确覆盖。 七、正则表达式语法 底层使用 QRegularExpression,语法是 PCRE 兼容的。常用要点: 7.1 转义 JSON:正则里的反斜杠要写成 \\。\\b 表示正则里的 \b,\\d 表示 \d。 INI:值原样读取,反斜杠 不需要 二次转义,直接写 \b、\d。 这也是两种格式最容易踩坑的地方。 7.2 常用元字符 写法 含义 \\b / \b 单词边界 \\w / \w 字母、数字、下划线 \\d / \d 数字 \\s / \s 空白 . 除换行外任意字符 * + ? 重复,? 加在量词后表示非贪婪 (?:...) 非捕获组 (...) 捕获组 (?=...) 正向前瞻 (?!...) 负向前瞻 (?<=...) 正向后顾 (? 负向后顾 [...] 字符类 [^...] 排除字符类 `\ /|` 7.3 正则中的分隔符问题 INI 里若正则里含 =(等号),要小心键值对解析。加载器按 第一个 = 拆分键值,所以值里的 = 不受影响: pattern=[;#][^\r\n]* 值里的 = 无需转义;但若正则的键名部分也想写 =,那就得注意:键名走的是第一个 = 之前的字符串,不会出问题。 若值里含 # 或 ; 开头,且 不在行首,也不会被当注释,因为加载器只把 整行以 ; 或 # 开头 的行当注释。 7.4 值两端引号会被剥离(注意!) INI 值若 两端都是双引号,加载器会把引号剥掉: pattern="\bif\b" 与 pattern=\bif\b 等价。但这个特性有副作用:如果正则本身以双引号开头结尾(比如要匹配 """ 三引号),写 start=""" 会被剥成 start="(只剩一个引号),完全偏离原意。 因此: 含引号的正则,建议改用 JSON 书写。 或者在 INI 里给值最外层再套一层引号(但这需要正则本身不以引号开头结尾),例如要表达正则 \bif\b 时写 pattern="\bif\b" 是安全的。 绝对不要在 INI 里写 start=""" 这种"正好两端都是引号、而且中间还是引号"的形式。 7.5 单行注释与行首字符 加载器只把 整行以 ; 或 # 开头 的行当注释。所以: pattern=;[^\n]* 是合法的,; 在值的开头不会被当注释行。但如果写成: ; 这是注释 则整行会被跳过。 八、颜色写法 支持两种十六进制格式: 格式 示例 含义 #RRGGBB #0066cc 不透明,6 位 #AARRGGBB #80ff0000 带透明度,8 位 不写 # 也接受,但建议始终带上,可读性更好。 background 建议用 8 位格式加透明度,避免遮住文本: { "name": "highlight", "pattern": "TODO", "background": "#40ff0000" } 40 是 alpha,约 25% 不透明,红色背景淡淡一层。 九、完整示例 9.1 JSON:简单配置语言 { "version": "0.0.1", "language": "SimpleConfig", "rules": [ { "name": "section", "pattern": "^\\s*\\[[^\\]]*\\]", "color": "#0070c0", "bold": true }, { "name": "key", "pattern": "^\\s*([^=;#\\[\\]]+?)\\s*(?==)", "capture": 1, "color": "#c05000", "bold": true }, { "name": "equals", "pattern": "=", "color": "#808080" }, { "name": "value", "pattern": "=\\s*(.*)$", "capture": 1, "color": "#a31515" }, { "name": "comment", "pattern": "[;#][^\\r\\n]*", "color": "#808080", "italic": true } ] } 9.2 INI:等价版本 9.3 多行字符串示例(Python 风格)—— 只能用 JSON 要匹配 Python 的三引号字符串 """...""",正则本身以 """ 开头结尾,在 INI 里无法正确书写(会被引号剥离逻辑误伤,见 7.4 节)。这类规则必须用 JSON: { "name": "triple_string", "kind": "multiline", "start": "\"\"\"", "end": "\"\"\"", "color": "#a31515" } 反例(不要这样写): [rule_0] name=triple_string kind=multiline start=""" end=""" color=#a31515 这段 INI 会被解析为: start = "(两端引号被剥掉后只剩一个引号) end = " 结果 start / end 都成了单引号 ",只能匹配单个双引号,而不是三引号。 十、加载与清空 在编辑器里: 右键 → 自动标记规则 → 加载标记规则:弹出文件对话框,选择 .json / .ini 等文件即可加载。加载成功后弹窗提示语言名与版本号。 右键 → 自动标记规则 → 清空标记规则:清除所有规则,文本恢复默认着色。 加载失败时会弹窗给出具体错误,例如"段 [rule_3] 缺少 pattern 字段"、"JSON 解析失败:…"。 十一、常见问题 Q:规则写对了但没效果? 检查三件事:① color 是否合法颜色;② kind 是否拼错(必须是 singleline 或 multiline);③ 是否被后面的规则覆盖了(把这条规则往后挪)。 Q:为什么字符串里的关键字也被染色了? 关键字规则在字符串规则之前,字符串规则没能覆盖它。把字符串规则放到关键字规则之后。 Q:多行注释不生效? 检查 start 和 end 是否都非空且是合法正则。JSON 里 \* 要写 \\*。 Q:INI 里的 \b 要不要转义? 不要。INI 值原样读取,\b 就是正则的 \b。JSON 里才需要写 \\b。 Q:capture 填了但整段仍被染色? 检查正则里是否真的有对应数量的捕获组。(?:...) 不算捕获组。 Q:规则文件版本必须递增吗? 不强制,但建议每次修改都递增,便于排查问题时确认版本。 Q:可以在一个规则里同时写 pattern 和 start/end 吗? 不可以。kind 决定用哪一组:singleline 只读 pattern,multiline 只读 start/end,另一组会被忽略。 Q:INI 值里的引号怎么处理? 仅当值两端都是双引号时才会被剥掉。若正则本身以引号开头结尾(比如 """),会被误剥。含引号的正则建议改用 JSON。 Q:我想在 INI 里写一个含引号的正则怎么办? 没办法安全地写。改用 JSON。 十二、字段速查表 字段 JSON 写法 INI 写法 是否必填 版本 "version": "0.0.1" version=0.0.1 ✅ 语言名 "language": "C++11" language=C++11 ❌ 规则条数 由 rules 长度决定 rule_count=N ✅(INI) 规则名 "name": "keyword" name=keyword ❌ 类型 "kind": "singleline" kind=singleline ❌(默认单行) 单行正则 "pattern": "..." pattern=... 单行规则 ✅ 多行起始 "start": "..." start=... 多行规则 ✅ 多行结束 "end": "..." end=... 多行规则 ✅ 捕获组 "capture": 1 capture=1 ❌(默认 0) 前景色 "color": "#0000ff" color=#0000ff ❌ 背景色 "background": "#40ff0000" background=#40ff0000 ❌ 粗体 "bold": true bold=true ❌ 斜体 "italic": true italic=true ❌ 下划线 "underline": true underline=true ❌ 删除线 "strikeout": true strikeout=true ❌ 十三、小结 写规则的核心是三步: 确定高亮对象:关键字?函数名?字符串?注释?分别用独立规则。 写好正则:合理使用 \b、(?、(?:...)、(...),尽量用 capture 精确锁定要染色的部分。 排好顺序:宽泛在前,具体在后;注释和字符串放最后,避免被关键字规则覆盖。 按这套方法,任何语言的语法都能拆成若干条规则描述出来。若规则复杂、含大量引号或反斜杠,一律用 JSON;若规则简单、想快速改,用 INI。
这样 bar(x) 中 bar 蓝色粗体,x 无色,( 无色。
bar(x)
bar
x
QSyntaxHighlighter 是"后应用覆盖先应用"。加载器按 rules 数组顺序(INI 按 rule_0、rule_1……)逐条应用,后一条的颜色会覆盖前一条。
QSyntaxHighlighter
宽泛规则在前,具体规则在后。 这样具体规则可以覆盖宽泛规则,得到视觉上更精确的结果。
典型顺序(从先到后):
把注释、字符串放最后,可以保证字符串里的 for、注释里的 if 不被关键字规则误染。
若把关键字放在字符串之后:
[ { "name": "string", "pattern": "\".*?\"" }, { "name": "keyword", "pattern": "\\bfor\\b" } ]
则 "for" 这段字符串会先被字符串规则上色,随后 for 又被关键字规则覆盖成蓝色,显示为 "for"(引号红、for 蓝),这显然不对。把关键字放前面,字符串在后,就能正确覆盖。
"for"
底层使用 QRegularExpression,语法是 PCRE 兼容的。常用要点:
QRegularExpression
\\
\\b
\b
\\d
\d
这也是两种格式最容易踩坑的地方。
\\w
\w
\\s
\s
.
*
+
?
(?=...)
(?!...)
(?<=...)
(?
[...]
[^...]
/
INI 里若正则里含 =(等号),要小心键值对解析。加载器按 第一个 = 拆分键值,所以值里的 = 不受影响:
=
pattern=[;#][^\r\n]*
值里的 = 无需转义;但若正则的键名部分也想写 =,那就得注意:键名走的是第一个 = 之前的字符串,不会出问题。
若值里含 # 或 ; 开头,且 不在行首,也不会被当注释,因为加载器只把 整行以 ; 或 # 开头 的行当注释。
#
;
INI 值若 两端都是双引号,加载器会把引号剥掉:
pattern="\bif\b"
与 pattern=\bif\b 等价。但这个特性有副作用:如果正则本身以双引号开头结尾(比如要匹配 """ 三引号),写 start=""" 会被剥成 start="(只剩一个引号),完全偏离原意。
pattern=\bif\b
"""
start="""
start="
因此:
\bif\b
加载器只把 整行以 ; 或 # 开头 的行当注释。所以:
pattern=;[^\n]*
是合法的,; 在值的开头不会被当注释行。但如果写成:
; 这是注释
则整行会被跳过。
支持两种十六进制格式:
#0066cc
#80ff0000
不写 # 也接受,但建议始终带上,可读性更好。
background 建议用 8 位格式加透明度,避免遮住文本:
{ "name": "highlight", "pattern": "TODO", "background": "#40ff0000" }
40 是 alpha,约 25% 不透明,红色背景淡淡一层。
40
{ "version": "0.0.1", "language": "SimpleConfig", "rules": [ { "name": "section", "pattern": "^\\s*\\[[^\\]]*\\]", "color": "#0070c0", "bold": true }, { "name": "key", "pattern": "^\\s*([^=;#\\[\\]]+?)\\s*(?==)", "capture": 1, "color": "#c05000", "bold": true }, { "name": "equals", "pattern": "=", "color": "#808080" }, { "name": "value", "pattern": "=\\s*(.*)$", "capture": 1, "color": "#a31515" }, { "name": "comment", "pattern": "[;#][^\\r\\n]*", "color": "#808080", "italic": true } ] }
9.3 多行字符串示例(Python 风格)—— 只能用 JSON 要匹配 Python 的三引号字符串 """...""",正则本身以 """ 开头结尾,在 INI 里无法正确书写(会被引号剥离逻辑误伤,见 7.4 节)。这类规则必须用 JSON: { "name": "triple_string", "kind": "multiline", "start": "\"\"\"", "end": "\"\"\"", "color": "#a31515" } 反例(不要这样写): [rule_0] name=triple_string kind=multiline start=""" end=""" color=#a31515 这段 INI 会被解析为:
要匹配 Python 的三引号字符串 """...""",正则本身以 """ 开头结尾,在 INI 里无法正确书写(会被引号剥离逻辑误伤,见 7.4 节)。这类规则必须用 JSON:
"""..."""
{ "name": "triple_string", "kind": "multiline", "start": "\"\"\"", "end": "\"\"\"", "color": "#a31515" }
反例(不要这样写):
[rule_0] name=triple_string kind=multiline start=""" end=""" color=#a31515
这段 INI 会被解析为:
"
结果 start / end 都成了单引号 ",只能匹配单个双引号,而不是三引号。
在编辑器里:
加载失败时会弹窗给出具体错误,例如"段 [rule_3] 缺少 pattern 字段"、"JSON 解析失败:…"。
Q:规则写对了但没效果? 检查三件事:① color 是否合法颜色;② kind 是否拼错(必须是 singleline 或 multiline);③ 是否被后面的规则覆盖了(把这条规则往后挪)。
Q:为什么字符串里的关键字也被染色了? 关键字规则在字符串规则之前,字符串规则没能覆盖它。把字符串规则放到关键字规则之后。
Q:多行注释不生效? 检查 start 和 end 是否都非空且是合法正则。JSON 里 \* 要写 \\*。
\*
\\*
Q:INI 里的 \b 要不要转义? 不要。INI 值原样读取,\b 就是正则的 \b。JSON 里才需要写 \\b。
Q:capture 填了但整段仍被染色? 检查正则里是否真的有对应数量的捕获组。(?:...) 不算捕获组。
Q:规则文件版本必须递增吗? 不强制,但建议每次修改都递增,便于排查问题时确认版本。
Q:可以在一个规则里同时写 pattern 和 start/end 吗? 不可以。kind 决定用哪一组:singleline 只读 pattern,multiline 只读 start/end,另一组会被忽略。
Q:INI 值里的引号怎么处理? 仅当值两端都是双引号时才会被剥掉。若正则本身以引号开头结尾(比如 """),会被误剥。含引号的正则建议改用 JSON。
Q:我想在 INI 里写一个含引号的正则怎么办? 没办法安全地写。改用 JSON。
"version": "0.0.1"
"language": "C++11"
"name": "keyword"
name=keyword
"kind": "singleline"
kind=singleline
"pattern": "..."
pattern=...
"start": "..."
start=...
"end": "..."
end=...
"capture": 1
capture=1
"color": "#0000ff"
color=#0000ff
"background": "#40ff0000"
background=#40ff0000
"bold": true
bold=true
"italic": true
italic=true
"underline": true
underline=true
"strikeout": true
strikeout=true
写规则的核心是三步:
(?、(?:...)、(...),尽量用 capture 精确锁定要染色的部分。
排好顺序:宽泛在前,具体在后;注释和字符串放最后,避免被关键字规则覆盖。
按这套方法,任何语言的语法都能拆成若干条规则描述出来。若规则复杂、含大量引号或反斜杠,一律用 JSON;若规则简单、想快速改,用 INI。
No replies yet
Featured Collection
Popular Ranking
Popular Events
Easy_Desktop 开发文档:载体几何插件 + 标记规则
一、Easy_Desktop 载体几何插件开发文档
本文档面向前来开发 Easy_Desktop 载体几何插件的开发者。内容包括接口说明、开发流程、常见坑与注意事项。
一、概述
载体几何插件(Carrier Geometry Plugin)是 Easy_Desktop 提供的一类扩展插件,运行在可载体(Container)内部,用于对可载体内的
Basic_Widget施加动画、几何变换、布局控制、视觉效果等。插件与宿主之间通过两套接口通信:
Ext_Carrier_Geometry_PluginInterface:插件实现,宿主调用。Carrier_Geometry_Interface:宿主实现,插件调用。插件通过
QPluginLoader动态加载,通过Q_DECLARE_INTERFACE做接口识别。Easy_Desktop 提供三个可载体:
Desktop_Main(桌面主载体)My_Process_Carrier(进程/文件按钮载体)Self_Carrier(自载体)每个可载体实例只能挂载一个几何插件。
二、设计原则
2.1 单向所有权
宿主是
Basic_Widget的唯一所有者,生命周期由 Qt 父子树管理。插件只能借用
Basic_Widget *,不能delete,不能setParent到非宿主期望的容器。插件对控件的所有修改必须通过Carrier_Geometry_Interface。2.2 接口隔离
插件不 include 宿主内部头文件,如
basic_widget.h、desktop_main.h。插件头文件里Basic_Widget是前置声明的不完整类型。插件工程只依赖
interfaces/carrier-geometry/和interfaces/easy_desktop_class.h。2.3 生命周期显式
插件必须在
on_widget_removed里清空对该控件的所有引用。控件析构函数会调用宿主unregister_widget,宿主再调用插件on_widget_removed,此时对象完整,插件可以安全访问。2.4 效果与命中分离
视觉变换(旋转、倾斜、缩放、透明度、伪透视)只影响渲染,不影响命中。宿主在鼠标靠近时会把控件的变换临时重置为 identity,保证点击命中正确。
2.5 帧循环统一
插件不自己
new QTimer做动画。所有周期性任务通过tick_interval_ms()和on_tick()由宿主统一调度。2.6 嵌套不递归
插件只能操作当前挂载的可载体。当前页的直接子项如果是另一个可载体(
Self_Carrier/My_Process_Carrier),插件只能当普通Basic_Widget处理,不能进入它内部。2.7 保存分离
宿主只保存插件
.so路径。插件内部状态由插件自己保存,宿主通过on_host_save()通知。插件通常用QStandardPaths::AppDataLocation或固定的~/.local/lib/easy_desktop_geometry_plugins/存到自己的路径。2.8 崩溃自负
插件可以拿裸指针做黑魔法,比如
reinterpret_cast、直接访问宿主内部。宿主不拦,但崩了是插件自己的问题。三、加载与元数据
3.1 元数据
插件
.json文件:注意事项:
api字段,版本通过Plugin_Version字段暴露。3.2 版本
插件类里:
宿主检查
Plugin_Version >= P_Version{0, 0, 1},不满足则拒绝加载。3.3 IID
插件类里:
void RemovePlugin():插件卸载时由宿主调用,释放所有资源。QString Plugin_Name():返回插件名称。void init(Carrier_Geometry_Interface *ptr):宿主绑定插件时调用,保存ptr。4.2 按需 override 的接口
以下方法都有默认空实现,插件按需 override:
void clear()int tick_interval_ms() constvoid on_tick(qint64 elapsed_ms)void on_widget_added(Basic_Widget *widget, int page)void on_widget_removed(Basic_Widget *widget, int page)void on_widget_page_changed(Basic_Widget *widget, int old_page, int new_page)void on_widget_geometry_changed(Basic_Widget *widget, const QRect &old_rect, const QRect &new_rect)void on_widget_selected_changed(Basic_Widget *widget, bool selected)void on_widget_z_changed(Basic_Widget *widget, int old_z, int new_z)void on_widget_pressed(Basic_Widget *widget)void on_widget_released(Basic_Widget *widget)void on_page_changed(int old_page, int new_page)void on_container_resized(const QSize &old_size, const QSize &new_size)void on_host_save()void call_settings_widget()void update_style(...)void Trans_Update()void Get_Loader(const QStringList &loaderNames, const P_Sender *seek_loader_sender, const P_Sender *recive_loader_sender)void easy_desktop_version(const P_Version &Easy_Desktop_Version, const QString &AKA_name)4.3 宿主控制接口
插件通过
Carrier_Geometry_Interface操作可载体内的控件:container_widgetQWidget *container_rectQRect(0, 0, w, h)carrier_idpage_countcurrent_pageset_current_pageon_page_changedlist_widgets(page)list_all_widgetswidget_pageget_widget_infois_widget_pressedany_widget_pressedpressed_widgetsget_geometryQRectset_geometryon_widget_geometry_changedmove_byset_geometry(widget, rect.translated(delta))set_transformget_transformset_visibleset_selected/is_selectedset_z_index/get_z_indexsend_signalBasic_Widget::dispatch_signaltake_intake_outmouse_global_posmouse_in_containercapabilities五、生命周期
重要时机说明:
easy_desktop_version()是最早被调用的回调,早于init()。插件可以在这个回调里做版本兼容判断,但此时还拿不到Carrier_Geometry_Interface。init()会在Get_Loader()之前被调用。如果init()里想用 Loader,需要延后到on_tick或第一次on_widget_added。on_widget_added()对已有控件的回调会在init()里同步触发,也在Get_Loader()之前。Get_Loader()是唯一一个可以拿到内部 Loader 指针的入口,之后on_tick/on_widget_added等才会被周期或事件触发。六、视觉变换
Carrier_Transform字段:rotationqreal0.0tilt_xqreal0.0tilt_yqreal0.0scale_xqreal1.0scale_yqreal1.0opacityqreal1.0perspectiveqreal0.0enabledboolfalsefalse时宿主会把控件恢复为 identityallow_suppressbooltruetrue宿主会临时把控件变换设为 identity,保证点击命中;false宿主不抑制,变换始终生效,但可能影响点击命中通过
set_transform(widget, transform)设置,通过get_transform(widget, &t)读取。enabled = false时,宿主会移除该控件的视觉效果,控件恢复默认绘制。七、能力位
宿主通过
capabilities()返回支持的功能位。插件应根据能力位降级:插件:
八、Z-Index 实现
QWidget没有setZValue。宿主在Basic_Widget里实现:Basic_Widget持有int m_z_index。set_z_index(int)修改后调用update_z_order()。update_z_order()对同一父对象下的兄弟Basic_Widget按 z 值稳定排序,然后依次raise()。save/load里存 z 值,加载后延迟调用update_z_order()。跨页、跨载体的控件不参与排序,因为不在同一父对象下。
九、通知机制
9.1 通知类型
on_widget_addedon_widget_removedon_widget_page_changedon_widget_geometry_changedon_widget_selected_changedon_widget_z_changedon_widget_pressedon_widget_releasedon_page_changedon_container_resizedon_host_save9.2 生命周期协议
on_widget_added。unregister_widget,宿主再调用on_widget_removed。此时对象完整,插件可以安全访问。QEvent::ParentChange检测,更新页号并调用on_widget_page_changed。unregister_widget,新宿主register_widget,幂等。on_widget_released,再调on_widget_removed。9.3 插件侧存储建议
插件推荐用:
Basic_Widget *做 key 便于 O(1) 查找,QPointer做值兜底对象销毁。on_widget_removed里三个 map 都要清空对应项。十、保存与加载
10.1 宿主保存什么
.so路径,key 是carrier_geometry_plugin_path。10.2 插件保存什么
插件内部状态由插件自己保存。宿主通过
on_host_save()通知,无参数。插件通常用:
保存时建议只写与默认值不同的键:
10.3 加载时机
宿主
load()时:carrier_geometry_plugin_path,加载.so。plugin->init(interface)。register_widget,触发on_widget_added。take_in时补充注册。插件在
init()里自己读参数,不需要宿主通知load。十一、设置窗口
11.1 入口
宿主菜单"几何插件设置"点击后:
11.2 插件实现
11.3 关键点
m_settings_dialog用QPointer。nullptr,让窗口成为独立顶层窗口。hide,不删除。RemovePlugin里同步delete。十二、插件工程
12.1
.pro文件关键点:
CONFIG += plugin c++11 debug:强制 debug 模式。CONFIG -= qml quick ltcg release:移除 release 相关默认配置。.pro里直接写明debug,即使用户忘了在 qmake 命令行里传,也能保证是 debug。12.2 编译
若
.pro里已经写明了CONFIG += debug,则qmake命令行里可以省去CONFIG+=debug。产物是
libmy_plugin.so(Linux)。12.3 加载
Easy_Desktop 右键可载体 → "载入几何插件" → 选中
.so。十三、开发中会遇到的坑
坑 1:不要用 release 模式编译插件
症状:
undefined symbolqobject_cast到接口失败原因:
-O2/-O3后,编译器可能把 inline 函数完全展开,丢弃部分符号。解法:在
.pro里写CONFIG += debug,CONFIG -= release ltcg。坑 2:元数据不要写
症状:
[Geo] fail: no Ext_Carrier_Geometry_Plugin key。原因:.json 里也写了
解法:
坑 3:
Basic_Widget在插件侧是不完整类型症状:用
QPointer编译不过。原因:插件头文件里只前置声明
class Basic_Widget;,编译器不知道它继承QObject。解法:
QPointer而不是QPointer。Basic_Widget *转QObject *用reinterpret_cast(单继承链,地址一致,安全)。坑 4:
on_widget_removed必须清空引用症状:控件被关闭后,插件在下一帧
on_tick里访问悬空指针,崩溃。原因:控件析构时宿主会调
on_widget_removed,但插件忘记在这个回调里删除自己的缓存。解法:每个存储了
Basic_Widget *的容器,都要在on_widget_removed里删除对应项。坑 5:
on_tick遍历容器时不要直接删除元素症状:
on_tick里随机崩溃,或漏处理部分控件。原因:遍历
QHash时直接remove会破坏迭代器。解法:先收集要删的,循环外统一删:
坑 6:
RemovePlugin里必须同步删除 dialog症状:移除插件时崩,栈里出现
sendPostedEvents、QObject::event。原因:dialog 用了
WA_DeleteOnClose,close()只是排队了DeferredDelete。RemovePlugin返回后宿主m_loader->unload()卸载.so,事件循环再处理DeferredDelete时,dialog 的析构函数在已卸载内存里。解法:
RemovePlugin里同步删除:同时
call_settings_widget里不要设WA_DeleteOnClose。坑 7:
QComboBoxpopup 是顶层窗口症状:只有带
QComboBox的插件崩溃,纯QDoubleSpinBox的插件不崩。原因:
QComboBox的下拉菜单是一个独立的顶层QWidgetpopup,不受 dialog 父子树管理。dialog 析构时 popup 被deleteLater,但如果.so已卸载,popup 的析构函数就跳到了已卸载内存。解法:dialog 析构前显式
combo->hidePopup(),让 popup 立即关闭并释放。坑 8:不要在插件里
dynamic_cast到具体控件类型症状:编不过,或运行崩溃。
原因:插件侧
Basic_Widget是不完整类型,qobject_cast或dynamic_cast需要完整类型定义。解法:不要这样做。所有操作走
Carrier_Geometry_Interface。坑 9:不要在插件里自己
new QTimer症状:插件卸载后宿主定时器还在触发,回调打进已卸载内存。
原因:插件自己建 timer,卸载时没停。
解法:用宿主提供的
on_tick。所有周期性任务通过tick_interval_ms()和on_tick()由宿主统一调度。坑 10:
on_tick不能做重活症状:宿主整体卡顿,甚至主窗口响应迟滞。
原因:
on_tick在主线程调用,遍历每个控件调set_geometry/set_transform都会触发宿主内部排序、重绘。解法:
set_z_index。设阈值过滤,只有目标 z 与当前 z 差别大于阈值时才调。坑 11:
call_settings_widget不要阻塞症状:调用设置窗口后,宿主所有动画停止。
原因:用了
dialog->exec()而不是dialog->show()。exec()会启动一个嵌套事件循环,阻塞当前调用栈。解法:用
show(),不要用exec()。坑 12:
send_signal的参数类型症状:调
send_signal(widget, Signal::xxx, data)编译报错。原因:
send_signal的参数是int signal,不是Signal枚举。解法:
坑 13:
set_z_index阈值过滤症状:每帧
set_z_index导致宿主反复排序和raise(),性能下降。解法:
坑 14:宿主和插件的
Carrier_Transform布局必须一致症状:升级宿主或插件后,变换效果错乱。
原因:
Carrier_Transform是 POD 结构,宿主和插件各自编译一份。如果两边版本不同,内存布局不一致。解法:宿主和插件的
interfaces/carrier-geometry/目录内容必须完全一致,改的时候两边一起改,一起编译。坑 15:不要在插件里持有宿主的对象指针
症状:插件卸载后宿主对象还在用,或宿主对象销毁后插件还在用。
原因:插件从宿主那拿了一个
QObject *并长期持有。解法:不要从宿主拿宿主内部的对象指针。所有跨边界的操作走
Carrier_Geometry_Interface。坑 16:
update_style和Trans_Update的实现症状:插件界面文字不跟随系统语言,或主题色不更新。
原因:没有 override 这两个接口。
解法:如果插件有界面,要 override 这两个方法:
十四、调试方法
14.1 日志
宿主侧
load_plugin有[Geo]前缀的调试日志。加载失败时看控制台会知道卡在哪一步:fail: QPluginLoader::load error = ...→.so加载失败fail: no Ext_Carrier_Geometry_Plugin key→ 元数据不对fail: qobject_cast to interface failed→ IID 不对或Q_INTERFACES漏了load_plugin success→ 加载成功14.2 gdb
看完整调用栈和局部变量。
14.3 不要用
-O2编译插件即使宿主可以开优化,插件也不要。见坑 1。
十五、检查清单
插件开发完,交付前检查:
.json只有Ext_Carrier_Geometry_Plugin一个键,没有Q_INTERFACES已写,IID 与宿主一致on_widget_removed里清空了所有对该 widget 的引用RemovePlugin里同步删除了 dialog,且没有WA_DeleteOnCloseQComboBox在删除前调了hidePopup()CONFIG+=debug编译,没用 releaseinterfaces/*.cppon_tick遍历时先收集 dead 再统一清理set_z_index有阈值过滤Basic_Widget *转QObject *用reinterpret_castsend_signal的signal参数用static_cast转换basic_widget.hQPointer管生命周期十六、参考文件
interfaces/carrier-geometry/carrier_geometry_plugin_interface.hinterfaces/carrier-geometry/carrier_geometry_interface.hinterfaces/carrier-geometry/carrier_geometry_types.hinterfaces/easy_desktop_class.hP_Version、P_Sender二、标记规则编写文档
本文档说明如何为
Mark_Rule_Highlighter编写自定义语法标记规则。规则文件支持 JSON 与 INI 两种格式,加载器按文件扩展名自动识别:.json.ini/.conf/.cfg两种格式表达能力完全相同,只是书写风格不同。选哪种取决于个人偏好:规则里正则多、字段杂时 JSON 更清晰;规则短、想快速改时 INI 更省事。
目录
一、最小可用规则
一份规则文件必须包含 版本号 与 至少一条规则。
JSON 版
INI 版
versionlanguagerulesINI 结构
version与rule_count必须存在[rule_0]…[rule_N-1]rule_序号INI 段名必须是连续的
rule_0、rule_1……加载器按rule_count逐个查找,缺一个就报错。三、规则字段
每条规则支持以下字段。JSON 里是键值对,INI 里是
键=值。两种格式含义完全一致。namerule_Nkind"singleline"/"multiline""singleline"patternstartendcapture00表示整个匹配color#RRGGBB或#AARRGGBBbackground#RRGGBB或#AARRGGBBboldfalseitalicfalseunderlinefalsestrikeoutfalsecolor与background都可省略;若两者都省略且未设任何样式,规则不会产生任何视觉效果(等价于无效规则)。四、规则类型
4.1 单行规则
singleline在一行文本内扫描正则表达式,对每个匹配上色。不跨行。
对应 INI:
kind字段可省略,默认就是singleline。4.2 多行规则
multiline用于跨行的成对结构,比如块注释、多行字符串、JSDoc 等。
对应 INI:
行为:
start后,从起始位置开始上色,直到同一行内找到end。end,则整行剩余部分上色,并把状态标记为"在此规则内",继续对下一行上色。end,则该行从头到end结尾上色,之后该规则状态结束。多行规则的状态通过 Qt 的
setCurrentBlockState()保存,因此不同多行规则之间互不干扰(内部用规则索引 + 1作为状态码)。五、capture 字段:只给捕获组上色
这是最有用、也最容易被忽略的字段。
5.1 为什么需要它
假设要标记 C++ 函数名
foo,直接写:这会连同函数名后面的
(一起上色,视觉上很难看。且若正则加了捕获组,比如([A-Za-z_]\\w*)\\s*\\(,仍然会把整段(含空格、括号)一起染。5.2 用
capture精确控制capture: 1表示"只把第 1 个捕获组的内容上色"。匹配结果是整段foo(,但只有foo被染色,(不受影响。5.3 捕获组编号
capture: 0(默认):整个匹配上色。capture: 1:第 1 个(...)的内容上色。capture: 2:第 2 个(...)的内容上色。若捕获组匹配到空内容(例如
(\w*)匹配到零长度),该匹配会被跳过,不会产生视觉效果。5.4 非捕获组不算编号
(?:...)是非捕获组,不占用编号:上面第 1 个捕获组是
(\d+),不是(?:foo|bar)。5.5 实战:C++ 函数名与类型区分
(?:负向后顾,确保函数名前不是字母或数字,避免从xfoo(里截出foo。([a-z_]\\w*):捕获以小写或下划线开头的标识符(约定的函数命名)。\\s*\\(:后面跟左括号。capture: 1:只给函数名染色,(保持默认色。这样
bar(x)中bar蓝色粗体,x无色,(无色。六、规则顺序与优先级
QSyntaxHighlighter是"后应用覆盖先应用"。加载器按rules数组顺序(INI 按rule_0、rule_1……)逐条应用,后一条的颜色会覆盖前一条。6.1 一般排列原则
宽泛规则在前,具体规则在后。 这样具体规则可以覆盖宽泛规则,得到视觉上更精确的结果。
典型顺序(从先到后):
把注释、字符串放最后,可以保证字符串里的
for、注释里的if不被关键字规则误染。6.2 反例
若把关键字放在字符串之后:
则
"for"这段字符串会先被字符串规则上色,随后for又被关键字规则覆盖成蓝色,显示为"for"(引号红、for蓝),这显然不对。把关键字放前面,字符串在后,就能正确覆盖。七、正则表达式语法
底层使用
QRegularExpression,语法是 PCRE 兼容的。常用要点:7.1 转义
\\。\\b表示正则里的\b,\\d表示\d。\b、\d。这也是两种格式最容易踩坑的地方。
7.2 常用元字符
\\b/\b\\w/\w\\d/\d\\s/\s.*+??加在量词后表示非贪婪(?:...)(...)(?=...)(?!...)(?<=...)(?[...][^...]/|`7.3 正则中的分隔符问题
INI 里若正则里含
=(等号),要小心键值对解析。加载器按 第一个=拆分键值,所以值里的=不受影响:值里的
=无需转义;但若正则的键名部分也想写=,那就得注意:键名走的是第一个=之前的字符串,不会出问题。若值里含
#或;开头,且 不在行首,也不会被当注释,因为加载器只把 整行以;或#开头 的行当注释。7.4 值两端引号会被剥离(注意!)
INI 值若 两端都是双引号,加载器会把引号剥掉:
与
pattern=\bif\b等价。但这个特性有副作用:如果正则本身以双引号开头结尾(比如要匹配"""三引号),写start="""会被剥成start="(只剩一个引号),完全偏离原意。因此:
\bif\b时写pattern="\bif\b"是安全的。start="""这种"正好两端都是引号、而且中间还是引号"的形式。7.5 单行注释与行首字符
加载器只把 整行以
;或#开头 的行当注释。所以:是合法的,
;在值的开头不会被当注释行。但如果写成:则整行会被跳过。
八、颜色写法
支持两种十六进制格式:
#RRGGBB#0066cc#AARRGGBB#80ff0000不写
#也接受,但建议始终带上,可读性更好。background建议用 8 位格式加透明度,避免遮住文本:40是 alpha,约 25% 不透明,红色背景淡淡一层。九、完整示例
9.1 JSON:简单配置语言
9.2 INI:等价版本
start="(两端引号被剥掉后只剩一个引号)end="结果
start/end都成了单引号",只能匹配单个双引号,而不是三引号。十、加载与清空
在编辑器里:
.json/.ini等文件即可加载。加载成功后弹窗提示语言名与版本号。加载失败时会弹窗给出具体错误,例如"段 [rule_3] 缺少 pattern 字段"、"JSON 解析失败:…"。
十一、常见问题
Q:规则写对了但没效果?
检查三件事:①
color是否合法颜色;②kind是否拼错(必须是singleline或multiline);③ 是否被后面的规则覆盖了(把这条规则往后挪)。Q:为什么字符串里的关键字也被染色了?
关键字规则在字符串规则之前,字符串规则没能覆盖它。把字符串规则放到关键字规则之后。
Q:多行注释不生效?
检查
start和end是否都非空且是合法正则。JSON 里\*要写\\*。Q:INI 里的
\b要不要转义?不要。INI 值原样读取,
\b就是正则的\b。JSON 里才需要写\\b。Q:
capture填了但整段仍被染色?检查正则里是否真的有对应数量的捕获组。
(?:...)不算捕获组。Q:规则文件版本必须递增吗?
不强制,但建议每次修改都递增,便于排查问题时确认版本。
Q:可以在一个规则里同时写
pattern和start/end吗?不可以。
kind决定用哪一组:singleline只读pattern,multiline只读start/end,另一组会被忽略。Q:INI 值里的引号怎么处理?
仅当值两端都是双引号时才会被剥掉。若正则本身以引号开头结尾(比如
"""),会被误剥。含引号的正则建议改用 JSON。Q:我想在 INI 里写一个含引号的正则怎么办?
没办法安全地写。改用 JSON。
十二、字段速查表
"version": "0.0.1""language": "C++11"rules长度决定"name": "keyword"name=keyword"kind": "singleline"kind=singleline"pattern": "..."pattern=..."start": "..."start=..."end": "..."end=..."capture": 1capture=1"color": "#0000ff"color=#0000ff"background": "#40ff0000"background=#40ff0000"bold": truebold=true"italic": trueitalic=true"underline": trueunderline=true"strikeout": truestrikeout=true十三、小结
写规则的核心是三步:
\b、(?、(?:...)、(...),尽量用capture精确锁定要染色的部分。按这套方法,任何语言的语法都能拆成若干条规则描述出来。若规则复杂、含大量引号或反斜杠,一律用 JSON;若规则简单、想快速改,用 INI。