编程语言研究书库

窗口与闪烁 — 第一个窗口、局部重绘、后置缓冲区与拖动

窗口与闪烁 — 第一个窗口、局部重绘、后置缓冲区与拖动

这一章讲什么: 上一章有了图层,本章长出窗口(标题栏 + 关闭按钮 + 背景的组合件), 以及 GUI 的两个永恒课题:闪烁和拖动。 闪烁治了两轮:先「只重画该画的区域」(局部重绘),再「画完再整体搬上屏」(后置缓冲区), 后者慢 30% 但彻底根除;拖动则是鼠标事件的第一次「消费」——按键状态、命中测试、可拖标志。 读完你会拥有一个可以上手玩的迷你窗口系统。

1. 先看现象:闪烁是怎么被一步步治好的

主线例子,快速计数器窗口——每帧数字 +1:

  症状(10.3):数字每变一次就全屏 Draw() 一次
      → 背景→窗口→鼠标依次上屏,窗口「不存在」的时间比存在还长 → 肉眼可见地闪
  第一轮(10.4):局部重绘
      Draw(图层ID) 只画「该图层及其以上」;Draw(矩形) 只画该区域
      数字变了 → 只重画计数器窗口那一小块 → 不闪了
  第二轮(10.5):鼠标悬停在计数器上时又闪
      重画区域里有鼠标,绘制过程中光标缺席
  根治:后置缓冲区
      所有图层先画到一块与屏幕等大的幕后内存(back_buffer_),
      画完一次性 memcpy 上屏 → 屏幕上任何时刻都是完整画面
      代价:每次多搬一整屏,实测速度约为原来的 0.7 倍(慢 30%)——明码标价

  图说:闪烁的本质是「屏幕被画了一半就被人看」;局部重绘缩小暴露窗口,后置缓冲区关闭暴露窗口。

2. 顶层全景

  10.1 修鼠标:光标停屏幕边缘(ElementMin/Max 夹紧;顺手修 DrawTo 越界 bug)
  10.2 第一个窗口:160×68 显示区 + DrawWindow()(标题栏/关闭按钮/背景)
  10.3 快速计数器:主循环计数 + sprintf 显示(去掉了 hlt——全速空转,故意的)
  10.4 局部重绘:Draw(id)/Draw(area) 两个重载 + Rectangle 交集(& 运算符)→ Move 自动重绘
  10.5 后置缓冲区:back_buffer_(mutable)+ 画完 Copy 上屏,慢 30% 换不闪
  10.6 拖动窗口:buttons 进 MouseObserver、FindLayerByPosition 命中测试
  10.7 仅拖动窗口:draggable_ 标志(默认 false,只有主窗口设 true)

  图说:每一节都是「上一节的新功能暴露新问题」——GUI 就是这样一层层长出来的。

3. 核心原理

3.1 鼠标修边与 DrawTo 的越界 bug

两个边缘问题先修:① 光标飞出屏幕会从另一侧冒出来—— 用 ElementMin/ElementMax 把坐标夹紧到 [0, screen-1]1; ② 更深处有个 bug:Window::DrawTo() 在窗口部分超出屏幕、又设了透明色时, 坐标计算错会让它在屏幕另一侧幽灵再现——修复是对绘制范围做删减2。 教训:裁剪(clipping)逻辑是 GUI 代码的第一大 bug 源,此后几章还会反复撞上。

3.2 第一个窗口与快速计数器

窗口本体是 160×68 的 Window,DrawWindow() 往上面画三样:标题栏、关闭按钮(一个字符点阵数组, 一个字符一个像素)、背景色3。颜色用 6 位十六进制码(0x848484,同网页 #848484), ToColor() 用 constexpr 在编译时完成转换4。

然后是关键道具:快速计数器——主循环每转一圈 count++、sprintf 成串、写进窗口、Draw(窗口)。 为了全速,连 hlt 都拿掉了(空转烧 CPU,故意的,后面省电章节会回收这个伏笔)5。 闪烁随之而来:每次全屏 Draw,「首先绘制背景,然后绘制窗口,因此有时窗口并不显示」6。

3.3 局部重绘:范围与交集

治闪第一招:别画不该画的。LayerManager::Draw() 加两个重载(函数重载 = 同名不同参)7:

  • Draw(Rectangle area):只画这个矩形范围,从下往上;
  • Draw(unsigned int id):找到该图层,只画它和它之上的图层。

矩形运算的核心是交集:area & window_area 算出「该画的区域 ∩ 窗口实际占的区域」—— 重叠就画重叠部分,不重叠返回面积 0 的矩形(运算符 & 重载实现)8。 FrameBuffer::Copy() 也加上源矩形参数,三层矩形换算(src_area_shifted、src_outline、 dst_outline)目的只有一个:算清实际要拷的 copy_area,超出源或目标任何一个都是内存损坏9。 Move() 升级:移动图层后自动重绘移动前后的区域——谁移动谁负责补画10。

3.4 后置缓冲区:用一整块内存买「原子上屏」

局部重绘把暴露窗口缩小了,但没关死:鼠标光标悬停在重绘区里,绘制过程中它缺席。 **后置缓冲区(back buffer)**是根治:一块与帧缓冲区等大的幕后内存; Draw() 把所有图层画到它上面,画完一次性 Copy 上屏—— 屏幕上任何时刻都是完整画面,不存在「画了一半」的状态11。

实现细节有个 C++ 知识点:LayerManager::Draw() 是 const 方法,却要改成员 back_buffer_—— mutable 修饰符允许「在 const 方法里改这个成员」,前提是它不影响对象的外部逻辑12。 代价明码标价:每帧多搬一整屏,「速度大约是原来的 0.7 倍,即慢了大约 30%……不过它不再闪烁了, 所以这很好」13——第 15 章还会回来继续抠这段性能。

3.5 拖动:命中测试与可拖标志

拖动 = 按下左键 → 按住移动 → 松开。需要三个新机制14:

  1. 按键状态进观察者:USB 鼠标的 OnDataReceived() 解析字节流,NotifyMouseMove() 增加 buttons 参数(按位:左键 0 位、右键 1 位、中键 2 位);
  2. 命中测试 FindLayerByPosition(pos, exclude_id):在光标位置、从顶往底 (rbegin→rend 逆序)找第一个带窗口的图层——exclude_id 用来跳过鼠标自己;
  3. 状态机:mouse_drag_layer_id 记拖动目标(0=没在拖),previous_buttons 区分 「刚按下 / 一直按着 / 刚松开」三种沿。

跑起来发现控制台、甚至背景图都能拖走——「这有点儿不太酷」15。 补丁是 draggable_ 标志:默认 false,只有主窗口图层设 true; 按下时检查 IsDraggable(),不可拖的图层根本不进入拖动状态16。

4. 作者的判断与证据

判断:闪烁优先于速度。 后置缓冲区慢 30% 也上——「不再闪烁了,所以这很好」。 在 GUI 上,正确性(画面完整)先于性能,这个排序贯穿全书。

判断:新功能先暴露 bug 再完善机制。 拖动功能一上线,「连控制台也能移动」 这种越界行为自己冒出来,然后才补 draggable 约束——典型的「先放开,再收紧」开发节奏。

证据: 都是可复现的观察:截图、计时(0.7 倍)、以及「窗口跳出屏幕也不故障」 (证明交集计算经得起越界)17。

5. 边界与局限

  • 局部重绘只解决「不该画的」,没解决「重叠区被上层的下层内容透出」—— 后置缓冲区才是完整解;但 30% 的税一直都在。
  • 命中测试只看图层不看窗口内容:点在窗口透明区也算命中(真正的 GUI 会区分); 关闭按钮此刻还不能点——第 30 章才给它接上功能。
  • 拖动是瞬移式(Move 到新坐标 + 重绘前后区域),没有动画插值——教学系统的简洁取舍。
  • draggable 是唯一的「对象类型」概念:没有最小化/最大化/焦点这些窗口管理语义,第 15 章补焦点。

6. 可带走的

本章主线例子,一行写完: 计数器每帧全屏 Draw 闪 → Draw(id) 局部重绘不闪 → 鼠标入区又闪 → 后置缓冲区一次上屏根治(慢 30%)→ 左键按住拖窗口(draggable 限定主窗口)。

  1. 闪烁 = 屏幕被画了一半就亮出来;两条治路:少画(局部重绘)、幕后再上屏(后置缓冲区);
  2. 局部重绘的数学是矩形交集(& 运算符);Copy 的三矩形换算防越界,越界即内存损坏;
  3. 后置缓冲区 = 一整屏内存换「原子上屏」;GUI 的「正确性 > 性能」排序;
  4. mutable 让 const 方法能改「不改变对象外部语义」的成员;
  5. 拖动的三件套:按键状态随事件走、命中测试(逆序找最上层)、可拖标志;
  6. 事件沿检测要存 previous_buttons——「当前状态」和「状态变化」是两种信息;
  7. GUI 代码的第一大 bug 源是裁剪/越界;每个坐标运算都要问「超出屏幕怎么办」。

7. 原文地图

主题原书章原文位置
光标停边缘、DrawTo 越界 bug10.1 再论鼠标(osbook_day10a)text/72-ch10-01-10-1-osbook-day10a.txt:8(搜「另一侧移出」) · text/72-ch10-01-10-1-osbook-day10a.txt:7(搜「错误」) · text/72-ch10-01-10-1-osbook-day10a.txt:22(搜「ElementMin」)
第一个窗口与 DrawWindow10.2 第一个窗口(osbook_day10b)text/73-ch10-02-10-2-osbook-day10b.txt:17(搜「显示一个窗口」) · text/73-ch10-02-10-2-osbook-day10b.txt:31(搜「标题栏」)
constexpr ToColor10.3 快速计数器(osbook_day10c)text/74-ch10-03-10-3-osbook-day10c.txt:3(搜「常量表达式」)
计数器与闪烁现象10.3 快速计数器(osbook_day10c)text/74-ch10-03-10-3-osbook-day10c.txt:55(搜「停止了hlt」) · text/74-ch10-03-10-3-osbook-day10c.txt:59(搜「闪烁」)
局部重绘两个重载10.4 消除闪烁(osbook_day10d)text/75-ch10-04-10-4-osbook-day10d.txt:7(搜「限制绘制」) · text/75-ch10-04-10-4-osbook-day10d.txt:15(搜「Rectangle<int>& area」)
矩形交集 operator&10.4 消除闪烁(osbook_day10d)text/75-ch10-04-10-4-osbook-day10d.txt:54(搜「交集」) · text/75-ch10-04-10-4-osbook-day10d.txt:66(搜「operator&」)
Copy 三矩形与内存损坏警告10.4 消除闪烁(osbook_day10d)text/75-ch10-04-10-4-osbook-day10d.txt:87(搜「src_area_shifted」) · text/75-ch10-04-10-4-osbook-day10d.txt:89(搜「内存损坏」)
Move 自动重绘10.4 消除闪烁(osbook_day10d)text/75-ch10-04-10-4-osbook-day10d.txt:94(搜「自动重绘」)
后置缓冲区与 mutable10.5 后置缓冲区(osbook_day10e)text/76-ch10-05-10-5-osbook-day10e.txt:7(搜「后置缓冲区是一个」) · text/76-ch10-05-10-5-osbook-day10e.txt:11(搜「mutable」)
慢 30% 的取舍10.5 后置缓冲区(osbook_day10e)text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」)
拖动三态与 buttons10.6 拖动窗口(osbook_day10f)text/77-ch10-06-10-6-osbook-day10f.txt:25(搜「拖动窗口是指」) · text/77-ch10-06-10-6-osbook-day10f.txt:45(搜「左键为0」) · text/77-ch10-06-10-6-osbook-day10f.txt:54(搜「mouse_drag_layer_id」)
FindLayerByPosition 逆序查找10.6 拖动窗口(osbook_day10f)text/77-ch10-06-10-6-osbook-day10f.txt:70(搜「FindLayerByPosition」) · text/77-ch10-06-10-6-osbook-day10f.txt:78(搜「rbegin」)
控制台也能拖10.6 拖动窗口(osbook_day10f)text/77-ch10-06-10-6-osbook-day10f.txt:84(搜「控制台突然移动了」)
draggable 标志10.7 仅拖动窗口(osbook_day10g)text/78-ch10-07-10-7-osbook-day10g.txt:4(搜「是否可拖动」) · text/78-ch10-07-10-7-osbook-day10g.txt:18(搜「IsDraggable」)

Footnotes

  1. 出处:「10.1 再论鼠标(osbook_day10a)」(text/72-ch10-01-10-1-osbook-day10a.txt:8(搜「另一侧移出」); text/72-ch10-01-10-1-osbook-day10a.txt:7(搜「错误」); text/72-ch10-01-10-1-osbook-day10a.txt:22(搜「ElementMin」))。 ↩

  2. 出处:「10.2 第一个窗口(osbook_day10b)」(text/73-ch10-02-10-2-osbook-day10b.txt:17(搜「显示一个窗口」); text/73-ch10-02-10-2-osbook-day10b.txt:31(搜「标题栏」))。 ↩

  3. 出处:「10.3 快速计数器(osbook_day10c)」(text/74-ch10-03-10-3-osbook-day10c.txt:3(搜「常量表达式」))。 ↩

  4. 出处:「10.3 快速计数器(osbook_day10c)」(text/74-ch10-03-10-3-osbook-day10c.txt:55(搜「停止了hlt」); text/74-ch10-03-10-3-osbook-day10c.txt:59(搜「闪烁」))。 ↩

  5. 出处:「10.4 消除闪烁(osbook_day10d)」(text/75-ch10-04-10-4-osbook-day10d.txt:7(搜「限制绘制」); text/75-ch10-04-10-4-osbook-day10d.txt:15(搜「Rectangle<int>& area」))。 ↩

  6. 出处:「10.4 消除闪烁(osbook_day10d)」(text/75-ch10-04-10-4-osbook-day10d.txt:54(搜「交集」); text/75-ch10-04-10-4-osbook-day10d.txt:66(搜「operator&」))。 ↩

  7. 出处:「10.4 消除闪烁(osbook_day10d)」(text/75-ch10-04-10-4-osbook-day10d.txt:87(搜「src_area_shifted」); text/75-ch10-04-10-4-osbook-day10d.txt:89(搜「内存损坏」))。 ↩

  8. 出处:「10.4 消除闪烁(osbook_day10d)」(text/75-ch10-04-10-4-osbook-day10d.txt:94(搜「自动重绘」))。 ↩

  9. 出处:「10.5 后置缓冲区(osbook_day10e)」(text/76-ch10-05-10-5-osbook-day10e.txt:7(搜「后置缓冲区是一个」); text/76-ch10-05-10-5-osbook-day10e.txt:11(搜「mutable」))。 ↩

  10. 出处:「10.5 后置缓冲区(osbook_day10e)」(text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」))。 ↩

  11. 出处:「10.6 拖动窗口(osbook_day10f)」(text/77-ch10-06-10-6-osbook-day10f.txt:25(搜「拖动窗口是指」); text/77-ch10-06-10-6-osbook-day10f.txt:45(搜「左键为0」); text/77-ch10-06-10-6-osbook-day10f.txt:54(搜「mouse_drag_layer_id」))。 ↩

  12. 出处:「10.6 拖动窗口(osbook_day10f)」(text/77-ch10-06-10-6-osbook-day10f.txt:70(搜「FindLayerByPosition」); text/77-ch10-06-10-6-osbook-day10f.txt:78(搜「rbegin」))。 ↩

  13. 出处:「10.6 拖动窗口(osbook_day10f)」(text/77-ch10-06-10-6-osbook-day10f.txt:84(搜「控制台突然移动了」))。 ↩

  14. 出处:「10.1 再论鼠标(osbook_day10a)」(text/72-ch10-01-10-1-osbook-day10a.txt:8(搜「另一侧移出」); text/72-ch10-01-10-1-osbook-day10a.txt:7(搜「错误」); text/72-ch10-01-10-1-osbook-day10a.txt:22(搜「ElementMin」))。 ↩

  15. 出处:「10.7 仅拖动窗口(osbook_day10g)」(text/78-ch10-07-10-7-osbook-day10g.txt:4(搜「是否可拖动」); text/78-ch10-07-10-7-osbook-day10g.txt:18(搜「IsDraggable」))。 ↩

  16. 出处:「10.4 消除闪烁(osbook_day10d)」(text/75-ch10-04-10-4-osbook-day10d.txt:7(搜「限制绘制」); text/75-ch10-04-10-4-osbook-day10d.txt:15(搜「Rectangle<int>& area」))。 ↩

  17. 出处:「10.5 后置缓冲区(osbook_day10e)」(text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」))。 ↩

On this page