egui_subject 观察者系统¶
概述¶
egui_subject 是 EmbeddedGUI 提供的轻量 Subject-Observer 机制,用于在嵌入式 UI 中做简单的数据绑定和发布订阅。
它把“数据发生变化”和“UI 如何响应变化”解耦:业务代码更新模型数据后,只需要调用 egui_subject_notify();所有订阅该数据源的观察者会按订阅顺序收到通知,并在回调中更新 label、按钮状态、进度条等 UI。
典型数据流如下:
用户操作 / 业务逻辑
-> 修改模型数据
-> egui_subject_notify()
-> observer callback
-> 更新 UI 控件
这个机制主要用于以下场景:
一个数据源需要驱动多个 UI 控件,例如计数值同时更新文本和进度条。
希望业务逻辑只维护模型,不直接散落大量控件更新代码。
示例、测试、调试面板或小型动态 UI 需要一个低成本的数据变化通知机制。
资源受限平台上需要避免 heap、链表节点动态分配和大型事件总线。
启用方式¶
egui_subject 受配置宏 EGUI_CONFIG_FUNCTION_SUBJECT_OBSERVER 控制,默认关闭。
// app_egui_config.h
#define EGUI_CONFIG_FUNCTION_SUBJECT_OBSERVER 1
每个 subject 可订阅的观察者数量由 EGUI_CONFIG_SUBJECT_MAX_OBSERVERS 控制,默认值为 4。
// app_egui_config.h
#define EGUI_CONFIG_SUBJECT_MAX_OBSERVERS 4
启用后可以使用 src/core/egui_subject.h 中的 API。
核心对象¶
egui_subject_t¶
egui_subject_t 表示一个可被观察的数据源。它内部保存一个固定长度的 egui_observer_t * 指针数组和当前观察者数量。
struct egui_subject
{
egui_observer_t *observers[EGUI_CONFIG_SUBJECT_MAX_OBSERVERS];
uint8_t count;
};
它不持有具体数据,只负责通知。通知时传入的 data 指针会原样转发给观察者回调。
egui_observer_t¶
egui_observer_t 表示一个订阅者节点,由调用方分配,通常放在静态变量或业务对象结构体中。
struct egui_observer
{
egui_observer_callback_t callback;
void *user_data;
};
一个订阅关系应对应一个 egui_observer_t。订阅后不要复制或移动该 observer;如果 UI 对象销毁或不再需要接收通知,应先调用 egui_subject_unsubscribe()。
API 说明¶
初始化¶
void egui_subject_init(egui_subject_t *subject);
初始化 subject,清空观察者列表并把计数置为 0。使用其它 subject API 前必须先初始化。
订阅¶
int egui_subject_subscribe(egui_subject_t *subject,
egui_observer_t *observer,
egui_observer_callback_t callback,
void *user_data);
订阅成功返回 0。以下情况返回 -1:
subject、observer或callback为空。同一个 observer 已经订阅到该 subject。
subject 的观察者数组已满。
user_data 会保存到 observer 中,并在每次通知时传回 callback,适合传入控件指针或业务上下文。
取消订阅¶
int egui_subject_unsubscribe(egui_subject_t *subject, egui_observer_t *observer);
取消订阅成功返回 0。如果参数为空或没有找到该 observer,返回 -1。
取消订阅后,后续观察者会向前压缩,剩余观察者的相对顺序保持不变。
通知¶
void egui_subject_notify(egui_subject_t *subject, const void *data);
通知所有已订阅 observer。data 可以是新值指针,也可以为 NULL,由具体回调约定解释。
通知行为:
observer 按订阅顺序调用。
data指针不被复制、不被释放,只原样传递。通知开始时会记录当前 observer 数量,本轮通知中新追加的 observer 不会在本轮被调用。
subject == NULL时直接返回,不会崩溃。
建议不要在 callback 中修改同一个 subject 的订阅关系;如果确实需要订阅或取消订阅,放到本轮通知结束后的流程中处理。
清空¶
void egui_subject_clear(egui_subject_t *subject);
移除全部 observer,不调用任何 observer callback。
查询数量¶
uint8_t egui_subject_observer_count(const egui_subject_t *subject);
返回当前 observer 数量。subject == NULL 时返回 0。
使用示例¶
下面示例展示一个计数器模型如何通知 label 更新文本。
#include "core/egui_subject.h"
static int32_t s_counter;
static egui_subject_t s_counter_subject;
static egui_observer_t s_label_observer;
static char s_count_buf[24];
static void on_counter_changed(egui_subject_t *subject, const void *data, void *user_data)
{
egui_view_t *label = (egui_view_t *)user_data;
const int32_t *value = (const int32_t *)data;
EGUI_UNUSED(subject);
snprintf(s_count_buf, sizeof(s_count_buf), "Count: %d", (int)*value);
egui_view_label_set_text(label, s_count_buf);
}
void counter_page_init(egui_view_t *label)
{
s_counter = 0;
egui_subject_init(&s_counter_subject);
egui_subject_subscribe(&s_counter_subject, &s_label_observer, on_counter_changed, label);
}
void counter_inc(void)
{
s_counter++;
egui_subject_notify(&s_counter_subject, &s_counter);
}
完整示例可参考 example/HelloBasic/subject_observer/test.c。
和事件系统的区别¶
egui_subject 和事件系统关注点不同:
模块 |
主要用途 |
|---|---|
事件系统 |
处理触摸、点击、按键、滚动等 UI 输入或控件事件 |
|
处理数据变化后的通知和 UI 同步 |
例如按钮点击仍然使用点击回调处理;点击回调中修改业务数据后,再通过 egui_subject_notify() 通知相关 UI 刷新。
内存和体积特点¶
egui_subject 的设计目标是小而确定:
默认关闭,关闭时不引入运行时代码,也不会给 view 结构体增加字段。
不使用 heap,observer 和 subject 都由调用方分配。
每个 subject 的 RAM 开销约为
EGUI_CONFIG_SUBJECT_MAX_OBSERVERS * sizeof(pointer) + 1字节,再加上结构体对齐。每个 observer 节点保存一个 callback 指针和一个
user_data指针。最大订阅数量固定,满了以后
egui_subject_subscribe()返回-1,不会动态扩容。
因此它适合资源受限场景下的小型数据绑定,不适合替代完整的全局消息总线。
设计边界¶
egui_subject 只负责通知,不负责数据生命周期。
需要注意:
data指针由调用方拥有,subject 不复制、不释放、不缓存。observer 由调用方拥有,必须保证订阅期间一直有效。
没有线程安全保护,应在 EmbeddedGUI 主循环线程中使用。
不做类型检查,callback 需要按约定把
data和user_data转回正确类型。订阅数量是编译期固定值,不适合观察者数量不确定的大型场景。
相关单测位于 example/HelloUnitTest/test/test_subject.c,覆盖初始化、订阅、通知、取消订阅、清空、重复订阅、容量上限和空参数行为。