恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
16位实模式下的嵌入式GUI开发:eCGUI for DOS实践解析
首页
资讯中心
/
16位实模式下的嵌入式GUI开发:eCGUI for DOS实践解析
16位实模式下的嵌入式GUI开发:eCGUI for DOS实践解析
发布时间:2026/9/2 11:12:58
简介eCGUI是一套面向DOS 16位平台的微型嵌入式GUI开发库作者历时四年自主开发以ANSI C为主编写编译后体积仅100KB~180KB可在Borland等环境下构建适合嵌入式开发者、DOS编程爱好者及图形界面学习者用以解决DOS下多窗口、中文输入显示及常用控件的界面开发需求。资源包共59个文件约881KB包含24个h头文件与16个c源文件、lib库文件、可执行演示程序、编译批处理脚本、JPEG与BMP示例图片、HZK16中文字库及TXT说明文档便于直接编译运行和按需裁剪集成。目前已有643人下载学习。从中可获得完整可编译的eCGUI源码与库既能参考多窗口、菜单、文本框、滚动条等控件API的实现又能学习JPEG图像解码在嵌入式GUI中的接入方式、中文字库的挂载方法以及向DOS/Linux/uc/OS-II移植的关键思路对自制小型GUI系统很有参考价值。 如果你还愿意打开 DOSBox或者像我一样翻出一台 486 老机器会发现今天很多“现代化”的思路在 16 位实模式下全都得推倒重来。我在整理eCGUI 0.3-Beta for DOS 16bit这个项目时最大的感受是图形界面这件事放在 8086/80286 那个内存和速度都极端紧张的环境里其实是非常硬核的嵌入式开发。eCGUI 是一套面向DOS 16 位实模式环境的图形界面开发库目标很直接让 DOS 下的应用程序能跑出带窗口、按钮、菜单、输入框的图形界面而不是停留在蓝底白字的字符菜单。它不是为了怀旧而是因为现实中仍然存在大量 DOS 时代的工控设备、实验仪器、POS 终端和老旧教学系统它们的维护者时常需要一个体面的人机交互界面。如果你在做类似的事情这篇文章会很有参考价值。1. 为什么要在 16 位 DOS 上做 GUI1.1 时代背景DOS 应用不止有字符界面很多人以为 DOS 程序就等于字符界面其实 90 年代中期的商业 DOS 软件早就做到了完整的图形界面比如 Norton Utilities、Turbo C 的 IDE、各种 CAD 软件。这些软件能在 640KB 常规内存里跑出窗口、下拉菜单、对话框靠的都是各自实现的 GUI 库。问题是这些库几乎都不开源、不可裁剪也没有文档教你如何把控件嵌到自己的业务程序里。这就回到了 eCGUI 存在的理由。我给某类老式设备做维护程序时需求非常简单设备通过串口上报数据现场工程师需要看到实时曲线和报警按钮同时能通过鼠标点击完成参数修改。用字符界面当然也能做但体验差一个量级。用现成的图形库又牵扯到授权和移植成本。最终我决定自己写一套薄薄的 GUI 层这就是 eCGUI 的起点。1.2 自研 eCGUI 而不是移植现成 GUI市面上不是没有可选的方案比如 GEM、GEOS它们都是优秀的 DOS 图形环境但问题也很突出一是代码庞大对运行环境要求高二是授权不清晰不能随便改三是它们更像“操作系统外壳”而不是能嵌入自己 C 程序的控件库。eCGUI 从设计之初就定位为“库”不是“环境”它不接管你的主程序逻辑只负责把窗口和控件画出来并把事件交给你处理。另一个现实因素是硬件兼容性。eCGUI 在标准 VGA 和 VBE 显卡上都能工作不依赖特定厂商扩展也不需要加载额外的驱动。它直接操作中断和显存把硬件差异压缩在一个很小的适配层里。这个思路后来被证明非常实用因为老设备的显卡五花八门越接近底层越不容易踩坑。1.3 0.3 Beta 版的能力边界0.3 Beta 这个版本号很诚实核心框架已经可以稳定运行但距离完整产品还有距离。目前已经支持窗口、按钮、标签、编辑框、复选框、菜单栏、滚动条这些基础控件鼠标和键盘事件处理也做了统一封装双缓冲渲染和定时器回调都可用。尚未完善的是中文输入法模块、控件拖拽布局工具、以及一些高级控件如树形列表。换句话说如果你只是想给自己的 DOS 工具加一个图形设置界面0.3 Beta 完全够用。如果你想做一套类似 Windows 3.1 的完整桌面环境那就得再等几个版本了。2. eCGUI 的整体设计与关键技术选型2.1 三段式架构驱动层、核心层、控件层eCGUI 的分层非常明确从上到下分别是控件层、核心层、驱动层。驱动层负责一切和硬件打交道的操作包括设置图形模式、读写显存、调色板、鼠标驱动、键盘扫描、定时器中断以及串口和打印机的底层接口。核心层维护窗口管理器、消息队列、事件分发、焦点控制、区域裁剪、双缓冲管理。控件层则建立在核心层之上每个控件本质上是注册了绘制回调和事件回调的结构体。这个分层最大的好处是替换成本低。比如我在某台只有 Trident 显卡的老机器上测试时VBE 2.0 的线性帧缓冲不能用我只需要修改驱动层的显存映射函数上层代码完全不用动。核心层的裁剪算法在控件重叠时非常关键否则每次重绘都会闪烁或者出现残影。2.2 16 位实模式下的内存策略16 位实模式的硬件限制是真正的硬约束常规内存一共就 640KB除了 DOS 和驱动占用留给应用程序的通常只有 500 多 KB。一个 640x480x256 的屏幕缓冲就要 300KB这在常规内存里几乎不可接受。eCGUI 的应对策略是屏幕缓冲根据需要动态切换小分辨率用系统的 far 堆分配大分辨率尽量复用显存中的隐藏页或改用压缩位图。这里尤其要小心 64KB 段边界问题。16 位实模式下 far 指针由段和偏移组成一个数组如果跨越段的边界直接的指针运算就会出错。eCGUI 内部统一使用 far 内存分配器并封装了一组安全的读写函数所有涉及跨段拷贝的操作都走 rep movsw 或者字节循环。这套封装写起来不复杂但没有它控件绘制时随机崩溃就会让你怀疑人生。2.3 事件驱动中断、轮询与消息循环面向过程的 DOS 程序通常是一个大循环eCGUI 也是但它在循环里嵌入了事件驱动模型。鼠标通过 INT 33h 驱动获取位置和按键键盘通过 INT 16h 读取按键码定时器通过挂接 INT 1Ch 实现 55 毫秒的周期性回调。这些中断源在消息循环里被统一转成 eCGUI 内部消息比如 EC_MOUSE_MOVE、EC_KEY_DOWN、EC_TIMER。为什么不用纯中断回调因为 16 位模式下中断处理函数里做复杂操作非常危险嵌套中断、段寄存器变化、栈空间不足都会导致崩溃。我的做法是中断处理函数只做数据收集把原始状态写到全局标志位和缓冲区主循环轮询到后再打包成消息。这个设计虽然增加了一点延迟但换来了极高的稳定性实测在 33MHz 的 486 上也能流畅响应用户操作。3. 从图形初始化到第一个可交互窗口3.1 图形模式切换与调色板加载 eCGUI 后第一步是初始化图形模式。我封装了ec_video_init()函数它优先尝试 VBE 模式如果显卡不支持再回退到标准 VGA Mode 13h。Mode 13h 是 320x200 分辨率、256 色虽然分辨率低但显存线性连续处理起来最简单。VBE 模式则支持 640x480 或 800x600适合需要显示更多信息的界面。void ec_video_init(void) { union REGS regs; regs.x.ax 0x4F02; /* VBE 设置模式 */ regs.x.bx 0x0101; /* 640x480x256 */ int86(0x10, regs, regs); if (regs.h.ah ! 0x00) { regs.x.ax 0x0013; /* 回退 Mode 13h */ int86(0x10, regs, regs); } ec_load_default_palette(); }调色板的设置同样被封装。VGA 的 DAC 寄存器通过端口 0x3C8 和 0x3C9 控制可以逐项改写 256 种颜色的 RGB 值。eCGUI 默认加载一套适合界面显示的调色板把前 16 色保留为标准 VGA 色后面 128 色分配给窗口边框、按钮高光和阴影最后的颜色留给业务绘图。3.2 绘制按钮、窗口和内置字体eCGUI 的控件绘制全是用矩形填充和像素操作完成的没有使用任何图形加速。一个按钮的绘制分三步先用背景色填充整个矩形区域然后画亮色和暗色的 3D 边框最后在按钮中央写出文字。3D 边框的效果来自左上亮色、右下暗色的对比这个是经典 GUI 的通用做法代码量很少但视觉上非常接近 Windows 3.x 的风格。文字渲染依赖内置的 8x8 和 8x16 点阵字库。我直接把 ASCII 字模放进一个静态数组绘制时逐位判断像素。这样做的好处是不依赖外部字体文件任何启动 DOS 的环境都能跑。代价是一套 8x16 的 96 个字符字模要占 12KB 左右内存但相比加载一套字体引擎这点开销完全值得。3.3 消息循环把鼠标键盘接进来初始化完成后控件并不能自动响应鼠标真正让界面“活”起来的是消息循环。eCGUI 提供了一个ec_run()函数内部是标准的事件循环外部程序只需要注册业务回调。事件循环伪代码如下while (!ec_quit_flag) { ec_poll_events(); /* 收集中断数据生成消息 */ ec_dispatch_message(); /* 分发消息到对应控件 */ ec_draw_all(); /* 重绘需要更新的控件 */ ec_copy_buffer_to_vram(); /* 双缓冲拷贝 */ }鼠标消息的分发依赖命中测试系统根据当前鼠标坐标遍历窗口和控件树找到最上层的可见控件然后发送点击或移动消息。键盘消息则走焦点机制每个窗口容器维护一个焦点控件的索引Tab 键在控件之间切换焦点Enter 键触发默认按钮动作。这套机制说起来简单实现在 16 位环境下还是花了不少时间调试。4. 性能优化与稳定性排障实录4.1 双缓冲解决闪烁第一次把窗口拖动起来时画面闪烁得让人完全没法看。原因很直接每次控件状态变化都直接写显存屏幕上的旧画面和新画面交替出现肉眼就看到了“闪”。解决办法就是双缓冲先在系统内存中生成一帧完整的画面然后一次性拷贝到显存。void ec_copy_buffer_to_vram(void) { _fmemcpy((void far *)0xA0000000L, screen_buffer, screen_width * screen_height); }在 320x200x256 模式下屏幕缓冲 64KB正好可以被一个段完整覆盖使用 far 指针非常方便。640x480 模式就要 300KB必须在远堆中分配我用的是farmalloc()并在拷贝时用 Huge 指针避免段边界问题。实测下来整体刷新速度从每秒 20 帧提升到 50 帧以上闪烁问题基本消失。4.2 软件光标与绘制协作另一个烦人问题是鼠标光标会随机消失。原因是鼠标在微动时驱动层的硬件光标和我们的 UI 绘制发生了重叠绘制过程把光标区域覆盖掉了鼠标驱动却没有感知到。解决方法是把光标纳入渲染流程在双缓冲拷贝完成后再根据当前鼠标坐标重新绘制光标图案。这样光标永远在最上层不会再被控件覆盖。代价是每次鼠标移动都要重绘光标区域如果界面同时有大范围刷新性能会略有下降。我在实现时做了脏矩形判断只有光标所在的小区域发生变化才重绘这样大部分场景下性能损耗可以忽略。4.3 常规内存不够用怎么办项目做到一半我发现串口数据缓冲区、图形资源和控件对象同时存在时常规内存很容易超过 500KB。这时候必须对内存做精细管理。eCGUI 的方案是所有动态对象都从远堆分配不占用默认数据段空间大块图片资源使用压缩格式存储需要显示时才解压到临时缓冲。图像资源尽量在上电初始化时加载一次运行时不去重复申请。另外编译器的内存模型也很重要。我统一使用 large 模型并把运行库指定为紧凑模式这样数据指针和代码指针都是 far 类型能访问的内存范围更大。对于特别大的数组我用 Huge 指针直接跨段访问绕过了 64KB 的限制。这一套组合拳下来eCGUI 加上业务程序总共能控制在 400KB 以内在 640KB 常规内存的老机器上跑得很从容。5. 常见问题速查与实用建议5.1 问题排查速查表现象可能原因解决办法屏幕严重闪烁未启用双缓冲检查ec_video_init后是否调用ec_enable_double_buffer()鼠标光标消失绘制流程覆盖了硬件光标在双缓冲拷贝后重新绘制软件光标调用 VBE 模式后花屏显卡不支持该模式或 VBE 版本过旧回退 Mode 13h或改用 640x480 标准 VBE 模式程序启动即崩溃far 指针越界或栈空间不足检查大数组是否跨越 64KB 段边界改用 Huge 指针Tab 键无法切换控件窗口焦点管理未初始化确保创建窗口时调用ec_window_set_focus()定时器回调不执行未挂接 INT 1Ch检查ec_timer_init()是否成功按钮点击没反应鼠标坐标与控件区域换算不一致检查热区坐标是否加入了窗口边框偏移量5.2 给想入坑的人几个建议如果你也想在 DOS 16 位环境下做 GUI第一条建议是先从最简单的 Mode 13h 开始把按钮画出来、让鼠标能点到再考虑提升分辨率和增加控件类型。第二条建议是所有硬件访问都封装成独立函数命名统一、参数固定这样遇到不兼容的显卡时只需要改一个文件。第三条建议是建立一个可复现的测试环境我现在基本都用 DOSBox 配合一个自动化脚本做回归测试上真机前先把大部分问题挡在模拟器外面。工具链方面我用的是 Borland C 3.1理由是可生成纯净的 16 位实模式代码调试器支持也够用。如果你更习惯开源环境可以考虑 Open Watcom但要注意它生成的代码模式和 Borland 不完全一样一些内联汇编需要微调。调试时推荐在程序里内置一个日志输出函数把关键路径写到串口或内存循环缓冲区很多疑难问题靠打印日志就能定位。写在最后做 eCGUI 这个项目最大的收获不是代码本身而是对 16 位系统底层运行机制有了更深的体感段寄存器怎么切换、中断怎么协作、内存怎么抠出来用。这些知识在今天看似乎已经边缘但一旦你遇到需要维护老设备或者做嵌入式底层开发的场景它们就会变成相当稀缺的经验。如果你正好也在折腾 DOS 下的图形界面建议先别急着追求复杂的特效和控件库把一个窗口、一个按钮、一次点击事件完整跑通后面的路会顺畅很多。至少 eCGUI 从无到有的这条路我已经替你走了一遍。本文还有配套的精品资源点击获取