AWTK界面开发进阶:基于Signal/Slot的多页面数据同步与实时预览实战指南
引言:多页面同步——界面开发的“老大难”问题
在嵌入式GUI、工业控制面板、智能家居中控等场景中,AWTK(Toolkit AnyWhere)作为一款轻量级、高性能的跨平台GUI库,因其低内存占用(最小仅需64KB RAM)和丰富的控件支持,成为嵌入式开发者的首选。但在实际项目中,我们常遇到一类典型场景:
主界面→模块界面→菜单界面→子预览菜单的多层级跳转中,用户在子菜单(如通过滑条调整“亮度”“对比度”)的修改需实时同步到上层菜单和模块界面,甚至触发模块界面的实时预览(如调整相机📷️增益后立即显示图像变化)。
这种跨页面的数据同步与实时预览需求,若仅通过传统的“全局变量+手动刷新”实现,会导致代码耦合度高、维护困难(例如修改一个参数需更新5个页面的显示逻辑);若依赖AWTK原生的消息机制(widget_on/widget_off),又难以应对复杂的“一对多”“多对一”同步关系(如一个参数变更需通知3个页面的5个控件)。
AWTK界面开发进阶:基于Signal/Slot的多页面数据同步与实时预览实战指南
Signal/Slot(信号/槽)机制的引入,为解决这一问题提供了优雅方案:它通过“事件驱动+松耦合订阅”模式,让数据变更像“广播”一样自动传播到所有关联页面,同时支持实时预览与防抖优化。本文将从场景分析、Signal/Slot原理、完整代码实现三个维度,深入讲解如何在AWTK中基于Signal/Slot解决多页面同步问题,其中实操代码占比超50%,覆盖从基础框架搭建到复杂预览逻辑的落地。
一、问题场景与痛点:为什么需要Signal/Slot?
1,1 典型场景复现
以一个“工业相机📷️控制模块”为例,其界面层级与数据流向如下:
graph TD
jrhz.infoA[主界面] -->|点击“相机📷️设置”| B[模块界面:相机📷️预览窗口]
B -->|点击“参数配置”| C[菜单界面:包含亮度/对比度/增益/量程等10+设置项]
C -->|点击“亮度设置”| D[子预览菜单:滑条+数值显示]
D -->|拖动滑条| E[实时同步:D的数值显示→C的亮度值→B的预览窗口亮度]
C -->|修改增益| F[实时同步:C的增益值→B的预览窗口增益→模块数据存储]
核心痛点:
• 数据流向复杂:子菜单(D)的参数变更需同步到父菜单(C)、祖父页面(B),甚至触发业务逻辑(如更新相机📷️硬件参数);
• 实时性要求高:滑条拖动时需“边拖边更新”(如亮度从50→51→52…),而非松开滑条后才更新;
• 防抖与性能平衡:高频滑条事件(每秒触发10+次)需避免频繁刷新UI导致卡顿;
• 代码维护性差:传统方式需在D、C、B页面分别编写“修改→通知→刷新”逻辑,代码重复率达60%以上。
1,2 传统方案的局限性
方案1:全局变量+手动刷新
// 伪代码:全局变量存储参数
int g_brightness = 50;
// 子菜单D:滑条回调中修改全局变量
static ret_t slider_brightness_on_change(void* ctx, event_t* e) {
g_brightness = widget_get_value(WIDGET(e->target));
// 手动通知父菜单C刷新
widget_invalidate(widget_lookup(widget_get_window(WIDGET(e->target)), "txt_brightness", TRUE));
// 手动通知模块界面B刷新
widget_t* preview_win = widget_lookup(widget_get_window(WIDGET(e->target)), "preview_window", TRUE);
camera_set_brightness(preview_win, g_brightness); // 触发预览更新
return RET_OK;
}
// 父菜单C:初始化时读取全局变量
static ret_t menu_camera_init(void) {
widget_t* txt_brightness = widget_lookup(widget_get_window(WIDGET(e->target)), "txt_brightness", TRUE);
widget_set_text_utf8(txt_brightness, fstring("%d", g_brightness)); // 手动同步显示
return RET_OK;
}
缺点:
• 全局变量污染命名空间,多线程场景下易引发竞态条件;
• 页面间强耦合(D需知道C和B的控件ID),新增页面时需修改多处代码;
• 无法统一管理“同步时机”(如滑条拖动中是否立即同步,还是停止拖动后同步)。
方案2:AWTK原生消息机制(widget_on)
AWTK通过widget_on(widget, EVT_VALUE_CHANGED, on_change_cb, ctx)监听控件事件,可实现简单的父子页面通信,但难以应对复杂同步:
// 子菜单D:发送自定义消息
static ret_t slider_brightness_on_change(void* ctx, event_t* e) {
int brightness = widget_get_value(WIDGET(e->target));
// 发送自定义消息(需定义消息类型)
widget_dispatch_simple_event(widget_get_window(WIDGET(e->target)), EVT_BRIGHTNESS_CHANGED, &brightness);
return RET_OK;
}
// 父菜单C:接收消息并更新
static ret_t menu_camera_on_event(void* ctx, event_t* e) {
if (e->type == EVT_BRIGHTNESS_CHANGED) {
int* brightness = (int*)e->param;
widget_set_text_utf8(widget_lookup(ctx, "txt_brightness", TRUE), fstring("%d", *brightness));
}
return RET_OK;
}
// 初始化时注册消息监听
widget_on(menu_camera_widget, EVT_BRIGHTNESS_CHANGED, menu_camera_on_event, menu_camera_widget);
缺点:
• 消息类型需手动定义(如EVT_BRIGHTNESS_CHANGED),参数传递依赖event_t,param,类型不安全;
• 无法实现“一对多”订阅(如一个参数变更需通知3个页面,需在每个页面单独注册消息);
• 缺乏同步策略控制(如防抖、节流),高频事件易导致UI线程阻塞。
1,3 Signal/Slot机制的破局之道
Signal/Slot(信号/槽)是一种观察者模式的增强实现,核心思想是:
• 信号(Signal):当对象状态变更时发出的“事件通知”(如“亮度值变更”);
• 槽(Slot):接收信号并执行响应的“回调函数”(如“更新文本框显示”“刷新预览窗口”);
• 连接(Connect):将信号与槽绑定,支持“一对多”“多对一”“跨线程”连接,且连接关系独立于对象生命周期。
在AWTK中引入Signal/Slot的优势:
• 松耦合:信号的发送者(如滑条)无需知道接收者(如文本框、预览窗口),只需发射信号;
• 集中管理:所有同步逻辑通过“信号定义→连接→发射”三步完成,代码可读性强;
• 灵活扩展:支持同步策略(如防抖、节流)、优先级控制,适配复杂场景;
• 类型安全:通过C++模板或AWTK的value_t类型封装参数,避免野指针与类型错误。
二、AWTK Signal/Slot基础:从原理到核心API
2,1 AWTK Signal/Slot实现原理
AWTK的Signal/Slot机制基于事件总线(Event Bus)实现,其核心架构如下:
graph LR
A[信号发送者:如滑条控件] -->|emit_signal("brightness_changed", value)| B[事件总线:signal_bus]
B -->|notify_slot("update_txt_brightness", value)| C[槽1:文本框更新函数]
B -->|notify_slot("update_preview_brightness", value)| D[槽2:预览窗口更新函数]
B -->|notify_slot("save_brightness_config", value)| E[槽3:配置保存函数]
• 信号发送者:任何AWTK控件(如slider、button)或自定义对象,通过widget_emit_signal发射信号;
• 事件总线:全局唯一的signal_bus实例,负责管理信号-槽连接、事件分发与线程调度;
• 槽函数:普通C函数或AWTK控件回调,通过signal_connect注册到事件总线,接收信号参数并执行逻辑;
• 连接管理:支持动态连接(signal_connect)、断开连接(signal_disconnect)、一次性连接(signal_connect_once)。
2,2 核心API详解
AWTK提供了一套简洁的C语言API(为适配嵌入式环境,未使用C++模板),核心函数如下:
2,2,1 信号定义与发射
/**
* 发射信号
* @param widget 发送信号的控件(或NULL表示全局信号)
* @param signal_name 信号名称(字符串,如"brightness_changed")
* @param param 信号参数(支持int、float、string、bool等基本类型,通过value_t封装)
* @return 返回RET_OK表示成功,RET_FAIL表示失败
*/
ret_t widget_emit_signal(widget_t* widget, const char* signal_name, const value_t* param);
/** 便捷宏:发射int类型参数的信号 */
#define WIDGET_EMIT_SIGNAL_INT(widget, name, value) \
do { \
value_t v; \
value_set_int(&v, value); \
widget_emit_signal(widget, name, &v); \
} while(0)
2,2,2 槽函数注册与连接
/**
* 定义槽函数类型(统一回调签名)
* @param ctx 上下文指针(通常为注册时的user_data)
* @param signal_name 信号名称
* @param param 信号参数
* @return RET_OK表示处理成功,RET_STOP表示阻止后续槽函数执行
*/
typedef ret_t www.ihjzkr.cn@163.com(*slot_func_t)(void* ctx, const char* signal_name, const value_t* param);
/**
* 连接信号与槽函数
* @param widget 接收信号的控件(或NULL表示全局槽)
* @param signal_name 信号名称(支持通配符"*"监听所有信号)
* @param slot_func 槽函数指针
* @param user_data 传递给槽函数的上下文数据
* @return 连接ID(用于后续断开连接),失败返回-1
*/
int32_t signal_connect(widget_t* widget, const char* signal_name, slot_func_t slot_func, void* user_data);
/**
* 断开信号与槽函数的连接
* @param connect_id 连接ID(由signal_connect返回)
* @return RET_OK表示成功
*/
ret_t signal_disconnect(int32_t connect_id);
2,2,3 参数封装:value_t类型
为支持多类型参数传递,AWTK定义了value_t结构体(位于awtk/value,h),支持int、float、string、bool、pointer等类型:
typedef enum {
VALUE_TYPE_INT = 0,
VALUE_TYPE_FLOAT,
VALUE_TYPE_STRING,
VALUE_TYPE_BOOL,
VALUE_TYPE_POINTER,
/* ,,, */
} value_type_t;
typedef struct _value_t {
value_type_t type;
union {
int32_t i;
float f;
char* str;
bool b;
void* ptr;
} u;
} value_t;
// 常用构造/析构函数
ret_t value_set_int(value_t* v, int32_t value);
ret_t www.fajwxq.cn@163.com value_set_float(value_t* v, float value);
ret_t value_set_string(value_t* v, const char* value);
void value_reset(value_t* v); // 释放资源(如string类型需释放内存)
三、实战:基于Signal/Slot的多页面同步框架搭建
3,1 场景定义与数据结构设计
以“工业相机📷️控制模块”为例,定义核心参数与页面结构:
3,1,1 核心参数枚举
// camera_params,h:相机📷️参数类型定义
#ifndef CAMERA_PARAMS_H
#define CAMERA_PARAMS_H
typedef enum {
PARAM_BRIGHTNESS = 0, // 亮度(0-100)
PARAM_CONTRAST, // 对比度(0-100)
PARAM_GAIN, www.hwqqey.cn@163.com // 增益(1-16,整数)
PARAM_RANGE, // 量程(0:自动,1:10mm,2:50mm)
PARAM_TIME_DISPLAY, // 时间显示格式(0:YYYY-MM-DD,1:MM/DD HH:MM)
// ,,, 其他参数
PARAM_COUNT // 参数总数
} camera_param_type_t;
#endif // CAMERA_PARAMS_H
3,1,2 页面控件ID定义
为避免硬编码控件ID,统一在头文件中定义:
// ui_widget_ids,h:界面控件ID
#ifndef UI_WIDGET_IDS_H
#define UI_WIDGET_IDS_H
// 子预览菜单(亮度设置)控件ID
#define ID_SLIDER_BRIGHTNESS "slider_brightness"
#define ID_TXT_BRIGHTNESS_VALUE "txt_brightness_value" // 滑条旁数值显示
#define ID_BTN_BRIGHTNESS_BACK "btn_brightness_back" // 返回按钮
// 菜单界面(相机📷️参数配置)控件ID
#define ID_MENU_CAMERA "menu_camera"
#define ID_TXT_BRIGHTNESS "txt_brightness" // 亮度值显示
#define ID_TXT_CONTRAST "txt_contrast"
#define ID_SLIDER_GAIN "slider_gain"
// ,,, 其他控件ID
// 模块界面(相机📷️预览)控件ID
#define ID_PREVIEW_WINDOW "preview_window"
#define ID_IMG_www.pdzaxx.cn@163.com PREVIEW "img_preview" // 预览图像控件
#endif // UI_WIDGET_IDS_H
3,2 第一步:定义信号与槽函数——明确“谁发信号,谁收信号”
根据场景需求,定义以下信号与对应的槽函数:
3,2,1 信号定义(信号名称规范:模块_参数_事件)
信号名称 触发时机 参数类型 说明
camera_brightness_changed 亮度值变更(滑条拖动/输入框修改) int(0-100) 子菜单/菜单界面的亮度变更
camera_contrast_changed 对比度值变更 int(0-100) 对比度参数变更
camera_gain_changed 增益值变更 int(1-16) 增益参数变更
preview_update_request 需要刷新预览窗口 pointer(预览控件) 通知模块界面更新图像
3,2,2 槽函数声明(统一放在camera_slots,h)
// camera_slots,h:槽函数声明
#ifndef CAMERA_SLOTS_H
#define CAMERA_SLOTS_H
#include "awtk,www.mwznj.cn@163.com h"
#include "camera_params,h"
#include "ui_widget_ids,h"
// -------------------------- 子预览菜单(亮度设置)槽函数 --------------------------
/**
* 滑条亮度变更槽函数(子菜单内部同步:滑条→数值显示)
* @param ctx 上下文(子菜单窗口控件)
* @param signal_name 信号名称(固定为"camera_brightness_changed")
* @param param 信号参数(int类型,亮度值)
* @return RET_OK
*/
ret_t slot_slider_brightness_update_value(void* ctx, const char* signal_name, const value_t* param);
/**
* 亮度值变更槽函数(子菜单→父菜单:同步亮度值到菜单界面)
* @param ctx 上下文(菜单界面控件)
* @param signal_name 信号名称
* @param param 信号参数(int类型,亮度值)
* @return RET_OK
*/
ret_t slot_menu_brightness_update_display(void* ctx, const char* signal_name, const value_t* param);
// -------------------------- 菜单界面槽函数 --------------------------
/**
* 菜单亮度显示更新槽函数(菜单界面内部同步:数值→文本框)
* @param ctx 上下文(文本框控件)
* @param signal_name 信号名称
* @param param 信号参数(int类型,亮度值)
* @return RET_OK
*/
ret_t slot_txt_brightness_update_text(void* ctx, const char* signal_name, const value_t* param);
/**
* 预览更新请求槽函数(菜单界面→模块界面:触发预览刷新)
* @param ctx 上下文(预览窗口控件)
* @param signal_name 信号名称("preview_update_request")
* @param param 信号参数(pointer类型,预览控件指针)
* @return RET_OK
*/
ret_t slot_preview_window_update(void* ctx, const char* signal_name, const value_t* param);
// -------------------------- 模块界面槽函数 --------------------------
/**
* 预览图像更新槽函数(模块界面:根据当前参数刷新图像)
* @param ctx 上下文(模块界面控件)
* @param signal_name 信号名称("preview_update_request")
* @param param 信号参数(pointer类型,预览控件指针)
* @return RET_www.jtukr.cn@163.com OK
*/
ret_t slot_img_preview_update(void* ctx, const char* signal_name, const value_t* param);
#endif // CAMERA_SLOTS_H
3,3 第二步:实现槽函数——定义“信号来了做什么”
在camera_slots,c中实现上述槽函数,核心是解析信号参数→更新UI/业务逻辑→触发下游信号(可选)。
3,3,1 子预览菜单槽函数实现
// camera_slots,c:子预览菜单槽函数
#include "camera_slots,h"
#include "awtk,www.mpxyn.cn@163.com h"
#include "value,h"
// 滑条亮度变更→数值显示(子菜单内部同步)
ret_t slot_slider_brightness_update_value(void* ctx, const char* signal_name, const value_t* param) {
widget_t* sub_menu = WIDGET(ctx); // ctx为子菜单窗口
if (param->type != VALUE_TYPE_INT) return RET_OK; // 类型校验
int brightness = param->u,i;
// 更新滑条旁的数值显示(ID_TXT_BRIGHTNESS_VALUE)
widget_t* txt_value = widget_lookup(sub_menu, ID_TXT_BRIGHTNESS_VALUE, TRUE);
if (txt_value != NULL) {
char buf[16];
tk_snprintf(buf, sizeof(buf), "%d", brightness);
widget_set_text_utf8(txt_value, buf);
}
// 发射信号通知父菜单同步(继续传播)
WIDGET_EMIT_SIGNAL_INT(sub_menu, "camera_brightness_changed", brightness);
return RET_OK;
}
// 亮度值变更→父菜单显示(子菜单→父菜单同步)
ret_t slot_menu_brightness_update_display(void* ctx, const char* signal_name, const value_t* param) {
widget_t* menu_camera = WIDGET(ctx); // ctx为菜单界面窗口
if (param->type != VALUE_TYPE_INT) return RET_OK;
int brightness = param-> www.phbtvw.cn@163.com u,i;
// 更新菜单界面的亮度文本框(ID_TXT_BRIGHTNESS)
widget_t* txt_brightness = widget_lookup(menu_camera, ID_TXT_BRIGHTNESS, TRUE);
if (txt_brightness != NULL) {
char buf[16];
tk_snprintf(buf, sizeof(buf), "亮度:%d", brightness);
widget_set_text_utf8(txt_brightness, buf);
}
// 发射预览更新请求(触发模块界面刷新)
widget_t* preview_win = widget_lookup(menu_camera, ID_PREVIEW_WINDOW, TRUE);
if (preview_win != NULL) {
value_t v;
value_set_www.vjwgoi.cn@163.com pointer(&v, preview_win);
widget_emit_signal(menu_camera, "preview_update_request", &v);
}
return RET_OK;
}
3,3,2 菜单界面与模块界面槽函数实现
// camera_slots,c:菜单界面与模块界面槽函数
#include "camera_slots,h"
#include "awtk,h"
#include "value,h"
// 菜单亮度显示更新(文本框更新)
ret_t slot_txt_brightness_update_text(void* ctx, const char* signal_name, const value_t* param) {
widget_t* txt_brightness = WIDGET(ctx); // ctx为文本框控件
if (param->type != VALUE_TYPE_INT) return RET_OK;
int brightness = param->u,i;
char buf[32];
tk_snprintf(buf, sizeof(buf), "亮度:%d", brightness);
widget_set_www.xqwzhj.cn@163.com text_utf8(txt_brightness, buf);
return RET_OK;
}
// 预览更新请求→模块界面(菜单→模块)
ret_t slot_preview_window_update(void* ctx, const char* signal_name, const value_t* param) {
widget_t* preview_win = WIDGET(ctx); // ctx为预览窗口控件
if (param->type != VALUE_TYPE_POINTER || param->u,ptr == NULL) return RET_OK;
// 转发信号到模块界面的图像控件(ID_IMG_PREVIEW)
widget_t* img_preview = widget_lookup(preview_win, ID_IMG_PREVIEW, TRUE);
if (img_preview != NULL) {
value_t v;
value_set_pointer(&v, img_preview);
widget_emit_signal(preview_win, "preview_update_request", &v); // 信号继续传播
}
return RET_OK;
}
// 模块界面:更新预览图像(模拟相机📷️硬件调用)
ret_t slot_img_preview_update(void* ctx, const char* signal_name, const value_t* param) {
widget_t* img_preview = WIDGET(ctx); // ctx为图像控件
// 实际项目中,此处应调用相机📷️SDK设置参数并获取预览图像
// 示例:模拟根据亮度值调整图像(实际为修改控件属性)
static int brightness = 50; // 模拟当前亮度(实际应从全局参数获取)
if (param->type == VALUE_TYPE_POINTER) {
// 假设信号参数携带了新的亮度值(实际可通过全局参数管理器获取)
// 简化示例:直接刷新图像(真实场景需结合参数计算)
widget_invalidate(img_preview, NULL); // 触发重绘
log_debug("预览窗口已更新(模拟亮度:%d)", brightness);
}
return RET_OK;
}
3,4 第三步:信号-槽连接——建立“信号→槽”的映射关系
在页面初始化时(如on_open回调),通过signal_connect将信号与槽函数绑定。核心是明确“哪个控件的哪个信号”连接到“哪个槽函数”。
3,4,1 子预览菜单(亮度设置)初始化
// brightness_sub_menu,c:子预览菜单初始化
#include "awtk,h"
#include "camera_slots,h"
#include "ui_widget_ids,h"
/**
* 子预览菜单打开时的初始化回调
* @param widget 子菜单窗口控件
* @param ctx 上下文
* @return RET_OK
*/
ret_t brightness_sub_menu_on_open(void* ctx, event_t* e) {
widget_t* sub_menu = WIDGET(ctx);
// 1, 初始化滑条默认值(假设从全局参数获取当前亮度为50)
widget_t* slider = widget_lookup(sub_menu, ID_SLIDER_BRIGHTNESS, TRUE);
if (slider != NULL) {
widget_set_value(slider, 50); // 设置滑条初始值
// 初始化数值显示
slot_slider_brightness_update_value(sub_menu, NULL, NULL); // 手动触发一次同步
}
// 2, 连接信号与槽:滑条值变更→更新数值显示
// 滑条的EVT_VALUE_CHANGED事件触发时,发射"camera_brightness_changed"信号
widget_on(slider, EVT_VALUE_CHANGED, [](void* ctx, event_t* e) {
widget_t* s = WIDGET(e->target);
int brightness = widget_get_value(s);
WIDGET_EMIT_SIGNAL_INT(s, "camera_brightness_changed", brightness);
return RET_OK;
}, sub_menu);
// 3, 连接全局信号"camera_brightness_changed"→槽函数(子菜单→父菜单同步)
// 注意:ctx为父菜单窗口(需通过widget_lookup获取)
widget_t* menu_camera = widget_lookup(widget_get_window(sub_menu), ID_MENU_CAMERA, TRUE);
if (menu_camera != NULL) {
signal_connect(NULL, "camera_brightness_changed", slot_menu_brightness_update_display, menu_camera);
}
return RET_OK;
}
3,4,2 菜单界面(相机📷️参数配置)初始化
// menu_camera,c:菜单界面初始化
#include "awtk,h"
#include "camera_slots,h"
#include "ui_widget_ids,h"
ret_t menu_camera_on_open(void* ctx, event_t* e) {
widget_t* menu_camera = WIDGET(ctx);
// 1, 连接信号"camera_brightness_changed"→槽函数(更新文本框显示)
// 文本框(ID_TXT_BRIGHTNESS)作为槽函数的ctx,接收信号后更新自身文本
widget_t* txt_brightness = widget_lookup(menu_camera, ID_TXT_BRIGHTNESS, TRUE);
if (txt_brightness != NULL) {
signal_connect(txt_brightness, "camera_brightness_changed", slot_txt_brightness_update_text, txt_brightness);
}
// 2, 连接信号"camera_brightness_changed"→槽函数(触发预览更新)
// 预览窗口(ID_PREVIEW_WINDOW)作为ctx,接收信号后转发给图像控件
widget_t* preview_win = widget_lookup(menu_camera, ID_PREVIEW_WINDOW, TRUE);
if (preview_win != NULL) {
signal_connect(preview_win, "camera_brightness_changed", slot_preview_window_update, preview_win);
}
// 3, 其他参数(对比度、增益等)的连接逻辑类似,此处省略,,,
return RET_OK;
}
3,4,3 模块界面(相机📷️预览)初始化
// module_camera,c:模块界面初始化
#include "awtk,h"
#include "camera_slots,h"
#include "ui_widget_ids,h"
ret_t module_camera_on_open(void* ctx, event_t* e) {
widget_t* module_camera = WIDGET(ctx);
// 连接信号"preview_update_request"→槽函数(更新预览图像)
widget_t* img_preview = widget_lookup(module_camera, ID_IMG_PREVIEW, TRUE);
if (img_preview != NULL) {
signal_connect(img_preview, "preview_update_request", slot_img_preview_update, img_preview);
}
return RET_OK;
}
3,5 第四步:防抖优化——避免高频信号导致UI卡顿
滑条拖动时,EVT_VALUE_CHANGED事件会高频触发(如每秒10-20次),若每次都同步UI和预览,可能导致界面卡顿。需通过防抖(Debounce)优化:即“事件触发后延迟一段时间执行,若期间再次触发则重置延迟”。
3,5,1 防抖槽函数实现
// debounce,h:防抖工具
#ifndef DEBOUNCE_H
#define DEBOUNCE_H
#include "awtk,h"
/**
* 创建防抖定时器
* @param delay_ms 延迟时间(毫秒)
* @param slot_func 实际执行的槽函数
* @param ctx 上下文
* @return 定时器ID(用于取消)
*/
uint32_t debounce_create(uint32_t delay_ms, slot_func_t slot_func, void* ctx);
/**
* 取消防抖定时器
* @param timer_id 定时器ID
*/
void debounce_cancel(uint32_t timer_id);
#endif // DEBOUNCE_H
// debounce,c:防抖实现(基于AWTK定时器)
#include "debounce,h"
#include "awtk_global,h"
typedef struct _debounce_timer_info_t {
slot_func_t slot_func;
void* ctx;
uint32_t timer_id;
} debounce_timer_info_t;
static ret_t debounce_timer_on_timeout(void* ctx, event_t* e) {
debounce_timer_info_t* info = (debounce_timer_info_t*)ctx;
info->slot_func(info->ctx, "", NULL); // 执行实际槽函数(信号参数需提前保存,此处简化)
TKMEM_FREE(info);
return RET_OK;
}
uint32_t debounce_create(uint32_t delay_ms, slot_func_t slot_func, void* ctx) {
debounce_timer_info_t* info = TKMEM_ALLOC(sizeof(debounce_timer_info_t));
if (info == NULL) return 0;
info->slot_func = slot_func;
info->ctx = ctx;
info->timer_id = timer_add(delay_ms, debounce_timer_on_timeout, info);
return info->timer_id;
}
void debounce_cancel(uint32_t timer_id) {
timer_remove(timer_id);
}
3,5,2 在滑条事件中应用防抖
修改子预览菜单的滑条事件处理,添加防抖逻辑:
// brightness_sub_menu,c:带防抖的滑条事件
#include "debounce,h"
ret_t brightness_sub_menu_on_open(void* ctx, event_t* e) {
// ,,, 其他初始化代码 ,,,
widget_t* slider = widget_lookup(sub_menu, ID_SLIDER_BRIGHTNESS, TRUE);
if (slider != NULL) {
// 定义防抖相关的静态变量(或使用结构体封装)
static uint32_t debounce_timer = 0;
static int last_brightness = 50;
widget_on(slider, EVT_VALUE_CHANGED, [](void* ctx, event_t* e) {
widget_t* s = WIDGET(e->target);
int brightness = widget_get_value(s);
last_brightness = brightness; // 保存最新值
// 取消之前的定时器(若存在)
if (debounce_timer != 0) {
debounce_cancel(debounce_timer);
debounce_timer = 0;
}
// 创建新的防抖定时器(延迟100ms,即滑条停止拖动100ms后执行同步)
debounce_timer = debounce_create(100, [](void* ctx, const char* signal_name, const value_t* param) {
// 实际执行同步逻辑(发射信号)
WIDGET_EMIT_SIGNAL_INT(WIDGET(ctx), "camera_brightness_changed", last_brightness);
debounce_timer = 0; // 重置定时器ID
return RET_OK;
}, s); // ctx为滑条控件
return RET_OK;
}, sub_menu);
}
return RET_OK;
}
四、完整代码整合与测试验证
4,1 工程结构
awtk_camera_project/
├── inc/
│ ├── camera_params,h // 参数枚举
│ ├── ui_widget_ids,h // 控件ID定义
│ ├── camera_slots,h // 槽函数声明
│ └── debounce,h // 防抖工具
├── src/
│ ├── camera_slots,c // 槽函数实现
│ ├── debounce,c // 防抖实现
│ ├── brightness_sub_menu,c // 子菜单初始化
│ ├── menu_camera,c // 菜单界面初始化
│ └── module_camera,c // 模块界面初始化
├── res/ // AWTK资源文件(ui、图片等)
└── main,c // 主程序入口
4,2 主程序入口(main,c)
// main,c:主程序入口
#include "awtk,h"
#include "awtk_main,inc"
int main(int argc, char* argv[]) {
// 初始化AWTK
tk_init(argc, argv, NULL);
// 注册页面打开事件(假设通过XML定义界面,此处简化为直接打开模块界面)
widget_t* win = window_manager();
widget_on(win, EVT_WINDOW_OPEN, module_camera_on_open, NULL); // 打开模块界面
// 进入AWTK主循环
tk_run();
return 0;
}
4,3 测试验证步骤
1, 编译工程:使用AWTK的交叉编译工具链(如awtk-linux-arm-gcc)编译,确保无链接错误;
2, 运行程序:在嵌入式设备或模拟器中启动,进入模块界面→点击“参数配置”→进入菜单界面→点击“亮度设置”→拖动滑条;
3, 验证同步效果:
• 子菜单滑条旁的数值显示是否实时更新(防抖前实时,防抖后延迟100ms);
• 菜单界面的“亮度:XX”文本框是否与滑条值一致;
• 模块界面的预览窗口是否触发重绘(通过日志“预览窗口已更新”确认);
4, 边界测试:测试亮度值设为0/100、增益设为1/16等边界值,验证UI是否正常显示,无崩溃或溢出。
总结:Signal/Slot让多页面同步“优雅可控”
本文通过50%以上篇幅的完整实操代码,详细讲解了AWTK中基于Signal/Slot解决多页面同步问题的全流程:从场景分析、Signal/Slot原理,到信号/槽定义、连接建立、防抖优化,最终实现“子菜单修改→父菜单同步→模块界面实时预览”的复杂需求。核心经验总结如下:
1, 信号设计原则:信号名称需“见名知意”(如camera_brightness_changed),参数类型明确(通过value_t封装);
2, 槽函数职责单一:每个槽函数仅负责“一类同步逻辑”(如更新显示、触发预览),避免大而全的函数;
3, 连接管理规范化:在页面初始化时集中注册连接,避免分散在业务逻辑中;
4, 性能优化必考虑:高频事件(如滑条拖动)必须添加防抖/节流,避免UI卡顿;
5, 代码可维护性:通过头文件统一管理控件ID、参数枚举、槽函数声明,降低后续修改成本。
Signal/Slot机制不仅解决了多页面同步问题,更让AWTK界面开发从“面向控件”转向“面向事件”,为复杂GUI应用的架构设计提供了新思路。掌握这一技术后,无论是工业控制、智能家居还是车载HMI,都能轻松应对多页面数据同步与实时预览的挑战。
AWTK开发系列将持续分享更多实战技巧,关注我们,让嵌入式GUI开发更高效!
(全文约18,000字,实操代码占比超55%,覆盖从原理到完整工程的落地)




