从零开始玩转 emWin:手把手教你实现多页面平滑跳转
你有没有遇到过这样的场景?
刚把 LCD 屏点亮,画了个按钮、显示个温度值,心里正美滋滋,老板突然说:“这个界面太单调了,加个设置菜单,点进去还能返回来。”
然后你就懵了——怎么跳?跳完怎么回来?要不要清屏?会不会闪?内存够不够?
别慌。今天我们就用emWin,从零开始,不讲虚的,一步步带你实现一个真正可用的多页面跳转系统。不需要懂太多底层原理,也不需要啃完几百页手册,只需要你会写 C 语言、能跑通 STM32 工程,就能照着做出来。
先搞清楚一件事:为什么不能“手动清屏重绘”?
很多初学者在做 GUI 时,第一反应是:
“我当前显示主界面 → 用户一按按钮 → 我
GUI_Clear()→ 然后重新画一个新的界面。”
听起来没问题?其实坑很多:
- 清屏会闪烁(尤其是没有双缓冲的情况下);
- 控件状态全丢了(比如滑动条位置、输入框内容);
- 返回时得再画一遍原界面,效率低;
- 代码耦合严重,改一个页面影响全局。
而 emWin 早就为你准备好了更聪明的办法:窗口管理机制(Window Manager, WM)。
它就像手机上的 App 切换——微信和微博都在后台运行,你只是切到前台显示而已,并不需要每次点击都重启一次应用。
emWin 的“页面”到底是什么?
在 emWin 里,没有“页面”这个概念,只有“窗口(Window)”。
每个窗口是一个独立的绘制区域,有自己的坐标系、事件处理函数和生命周期。我们所谓的“页面”,其实就是一个全屏大小的顶层窗口,通常用FRAMEWIN来封装。
关键角色介绍
| 组件 | 作用 |
|---|---|
WM_HWIN | 窗口句柄,相当于窗口的“身份证号” |
FRAMEWIN_CreateEx() | 创建一个可作为页面容器的框架窗口 |
WM_CALLBACK_FUNC | 回调函数,负责处理消息(如按钮被点了) |
WM_ShowWindow()/WM_HideWindow() | 显示/隐藏某个窗口 |
WM_BringToTop() | 把某个窗口提到最前面 |
这些 API 加起来,就是你实现页面跳转的全套工具箱。
实战:两个页面互相跳转
我们来做个最简单的例子:
有两个页面——主菜单页和设置页,主菜单有个“进入设置”按钮,设置页有个“返回”按钮,点击即可来回切换。
第一步:定义页面创建函数(接口先行)
先建两个头文件,声明每个页面的创建函数:
// page_main.h #ifndef PAGE_MAIN_H #define PAGE_MAIN_H WM_HWIN CreateMainWindow(void); #endif// page_settings.h #ifndef PAGE_SETTINGS_H #define PAGE_SETTINGS_H WM_HWIN CreateSettingsWindow(void); #endif这样做的好处是模块清晰,后期想加“关于页”、“网络配置页”也方便扩展。
第二步:主页面实现(带跳转逻辑)
// page_main.c #include "DIALOG.h" #include "page_main.h" #include "page_settings.h" static WM_HWIN hMainWnd; // 保存主页面句柄 // 主页回调函数 static void _cbMain(WM_MESSAGE *pMsg) { WM_HWIN hItem; int Id; switch (pMsg->MsgId) { case WM_CREATE: // 标题文本 TEXT_CreateEx(100, 10, 120, 20, pMsg->hWin, WM_CF_SHOW, 0, 0, "主菜单"); // 跳转按钮 BUTTON_CreateEx(100, 50, 100, 40, pMsg->hWin, 0, 0, 0, "进入设置"); break; case WM_NOTIFY_PARENT: // 子控件发来的通知 Id = WM_GetId(pMsg->hWinSrc); // 获取哪个控件触发 if (pMsg->Data.v == WM_NOTIFY_CHILD_CLICKED) { if (Id == GUI_ID_BUTTON0) { // 假设这是第一个按钮 WM_HideWindow(hMainWnd); // 隐藏自己 CreateSettingsWindow(); // 显示设置页 } } break; default: WM_DefaultProc(pMsg); // 其他消息交给默认处理器 } } WM_HWIN CreateMainWindow(void) { // 创建全屏 FRAMEWIN 作为主页面 hMainWnd = FRAMEWIN_CreateEx(0, 0, 320, 240, WM_CF_SHOW, 0, 0, 0, "Main", _cbMain); FRAMEWIN_SetClientColor(hMainWnd, GUI_WHITE); // 设置背景色 return hMainWnd; }注意几个细节:
- 使用FRAMEWIN_CreateEx创建窗口,并指定回调函数_cbMain;
- 按钮通过BUTTON_CreateEx添加到主窗口中;
- 点击事件在WM_NOTIFY_PARENT中捕获;
- 当前页面隐藏后才创建新页面,避免叠加错乱。
第三步:设置页实现(支持返回)
// page_settings.c #include "DIALOG.h" #include "page_settings.h" #include "page_main.h" static WM_HWIN hSettingsWnd; static void _cbSettings(WM_MESSAGE *pMsg) { int Id; switch (pMsg->MsgId) { case WM_CREATE: TEXT_CreateEx(100, 10, 120, 20, pMsg->hWin, WM_CF_SHOW, 0, 0, "设置页"); BUTTON_CreateEx(100, 50, 100, 40, pMsg->hWin, 0, 0, 0, "返回"); break; case WM_NOTIFY_PARENT: Id = WM_GetId(pMsg->hWinSrc); if (pMsg->Data.v == WM_NOTIFY_CHILD_CLICKED) { if (Id == GUI_ID_BUTTON0) { WM_HideWindow(hSettingsWnd); // 隐藏自己 CreateMainWindow(); // 重建主页面(或复用) } } break; default: WM_DefaultProc(pMsg); } } WM_HWIN CreateSettingsWindow(void) { hSettingsWnd = FRAMEWIN_CreateEx(0, 0, 320, 240, WM_CF_SHOW, 0, 0, 0, "Settings", _cbSettings); FRAMEWIN_SetClientColor(hSettingsWnd, GUI_LIGHTGRAY); return hSettingsWnd; }这里有个关键点:
当我们从设置页返回时,再次调用了CreateMainWindow()。但如果主页面之前只是被隐藏(没销毁),其实可以直接用句柄恢复显示,而不是重新创建。
那怎么优化?往下看。
进阶技巧:让页面“活着”,提升响应速度
频繁创建/销毁页面不仅慢,还容易造成内存碎片。更好的做法是:
常驻核心页面 + 按需加载次要页面
我们可以加一个简单的“页面管理器”:
// page_manager.h #ifndef PAGE_MANAGER_H #define PAGE_MANAGER_H void Page_SwitchToMain(void); void Page_SwitchToSettings(void); #endif// page_manager.c #include "page_main.h" #include "page_settings.h" static WM_HWIN hMain = 0; static WM_HWIN hSetting = 0; void Page_SwitchToMain(void) { if (!hMain) { hMain = CreateMainWindow(); } else { WM_ShowWindow(hMain); WM_BringToTop(hMain); } if (hSetting) { WM_HideWindow(hSetting); } } void Page_SwitchToSettings(void) { if (!hSetting) { hSetting = CreateSettingsWindow(); } else { WM_ShowWindow(hSetting); WM_BringToTop(hSetting); } if (hMain) { WM_HideWindow(hMain); } }现在你的跳转逻辑变成了:
// 在主页面按钮点击处: Page_SwitchToSettings(); // 在设置页返回按钮处: Page_SwitchToMain();优点非常明显:
- 主页只创建一次,返回极快;
- 内存使用可控;
- 结构清晰,易于维护。
如何防止“越点越卡”?内存与资源管理建议
emWin 默认使用动态内存分配(GUI_ALLOC)。如果你发现跳几次就卡住甚至死机,大概率是内存不足或泄漏了。
几条铁律请牢记:
- 检查返回值
所有Create函数都可能返回0,说明创建失败(内存不够):
c hWin = FRAMEWIN_CreateEx(...); if (!hWin) { GUI_DEBUG_ERROROUT("Failed to create window!"); return; }
- 启用内存设备减少闪烁
在初始化时加上这句:
c WM_SetCreateFlags(WM_CF_MEMDEV);
它会让每个窗口自带“离屏缓冲”,绘制时不直接操作屏幕,大幅降低闪烁。
- 监控剩余内存
开发阶段可以定期打印:
c GUI_ALLOC_GetNumFreeBytes()
如果低于几 KB,就要考虑限制页面数量或释放非活跃页面。
- 公共资源统一注册
字体、图标等不要在每个页面重复加载:
c GUI_SetFont(&GUI_Font32_ASCII); // 在 GUI_Init 后统一设置
高级玩法:模拟“返回栈”,支持多级跳转
如果你要做三级菜单(主页 → 设置 → 时间设置 → 返回 → 返回),怎么办?
可以用一个页面栈(Page Stack)来记录历史:
#define MAX_PAGE_STACK 5 static WM_HWIN hPageStack[MAX_PAGE_STACK]; static int stackIndex = -1; void Page_Push(WM_HWIN hNext) { if (stackIndex < MAX_PAGE_STACK - 1) { hPageStack[++stackIndex] = hNext; } } WM_HWIN Page_Pop(void) { return (stackIndex >= 0) ? hPageStack[stackIndex--] : 0; }跳转时压栈,返回时出栈:
// 前进 WM_HideWindow(current); Page_Push(current); WM_ShowWindow(next); // 返回 WM_HideWindow(current); WM_HWIN prev = Page_Pop(); if (prev) WM_ShowWindow(prev);是不是有点像 Android 的 Activity 栈?没错,思想是一样的。
常见问题 & 解决方案(避坑指南)
| 问题 | 可能原因 | 解法 |
|---|---|---|
| 页面切换后按钮没反应 | 新页面未正确关联父窗口 | 确保控件创建时传入pMsg->hWin作为父窗口 |
| 屏幕闪烁严重 | 未启用内存设备 | 加上WM_SetCreateFlags(WM_CF_MEMDEV) |
| 返回后画面错乱 | 多个页面同时显示且层级混乱 | 使用WM_HideWindow隐藏旧页,必要时调WM_BringToTop |
| 内存耗尽崩溃 | 页面反复创建未销毁 | 引入页面管理器,控制实例数量 |
| 文字显示乱码 | 字体未正确加载 | 提前注册字体并确保编码匹配(如 GB2312) |
还有一个隐藏大坑:触摸校准不准会导致点击无响应!
务必确认你的 X/Y 坐标映射正确,否则你以为是 emWin 的锅,其实是驱动没调好。
最后一点思考:emWin 到底强在哪?
很多人觉得 emWin 就是个“画图库”,其实不然。
它真正的价值在于提供了一整套嵌入式 UI 框架能力:
- 分层的窗口管理系统
- 消息队列与事件分发机制
- 支持动画、透明度、抗锯齿等高级特性
- 与 RTOS(如 FreeRTOS)无缝集成
- 成熟的调试工具链(GUIBuilder、Simulation)
掌握多页面跳转,只是迈出了第一步。后面你还可以做:
- 滑动切换动画(配合GUI_ANIM)
- 模态对话框(弹窗确认)
- 动态布局更新
- 主题切换(白天/夜间模式)
如果你正在做一个智能仪表、工业控制面板或者 IoT 设备的人机交互界面,这套方案完全可以直接用上去。
而且你会发现,一旦结构搭好了,加新功能就像搭积木一样简单。
所以别再手动Clear和Draw了,学会用 emWin 的窗口机制,让你的嵌入式 GUI 真正“活”起来。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。