16位实模式下的嵌入式GUI开发:eCGUI for DOS实践解析
2026/9/21 0:19: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 上做 GUI

1.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 从无到有的这条路,我已经替你走了一遍。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询