窗口与闪烁 — 第一个窗口、局部重绘、后置缓冲区与拖动¶
这一章讲什么: 上一章有了图层,本章长出窗口(标题栏 + 关闭按钮 + 背景的组合件), 以及 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:
- 按键状态进观察者:USB 鼠标的 OnDataReceived() 解析字节流,NotifyMouseMove() 增加 buttons 参数(按位:左键 0 位、右键 1 位、中键 2 位);
- 命中测试 FindLayerByPosition(pos, exclude_id):在光标位置、从顶往底 (rbegin→rend 逆序)找第一个带窗口的图层——exclude_id 用来跳过鼠标自己;
- 状态机: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 限定主窗口)。
- 闪烁 = 屏幕被画了一半就亮出来;两条治路:少画(局部重绘)、幕后再上屏(后置缓冲区);
- 局部重绘的数学是矩形交集(
&运算符);Copy 的三矩形换算防越界,越界即内存损坏; - 后置缓冲区 = 一整屏内存换「原子上屏」;GUI 的「正确性 > 性能」排序;
mutable让 const 方法能改「不改变对象外部语义」的成员;- 拖动的三件套:按键状态随事件走、命中测试(逆序找最上层)、可拖标志;
- 事件沿检测要存 previous_buttons——「当前状态」和「状态变化」是两种信息;
- GUI 代码的第一大 bug 源是裁剪/越界;每个坐标运算都要问「超出屏幕怎么办」。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 光标停边缘、DrawTo 越界 bug | 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」) |
| 第一个窗口与 DrawWindow | 10.2 第一个窗口(osbook_day10b) | text/73-ch10-02-10-2-osbook-day10b.txt:17(搜「显示一个窗口」) · text/73-ch10-02-10-2-osbook-day10b.txt:31(搜「标题栏」) |
| constexpr ToColor | 10.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(搜「自动重绘」) |
| 后置缓冲区与 mutable | 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」) |
| 慢 30% 的取舍 | 10.5 后置缓冲区(osbook_day10e) | text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」) |
| 拖动三态与 buttons | 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」) |
| 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」) |
-
出处:「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」))。 ↩ -
出处:「10.2 第一个窗口(osbook_day10b)」(
text/73-ch10-02-10-2-osbook-day10b.txt:17(搜「显示一个窗口」);text/73-ch10-02-10-2-osbook-day10b.txt:31(搜「标题栏」))。 ↩ -
出处:「10.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」))。 ↩ -
出处:「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&」))。 ↩ -
出处:「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(搜「内存损坏」))。 ↩ -
出处:「10.4 消除闪烁(osbook_day10d)」(
text/75-ch10-04-10-4-osbook-day10d.txt:94(搜「自动重绘」))。 ↩ -
出处:「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.5 后置缓冲区(osbook_day10e)」(
text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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(搜「控制台突然移动了」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「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」))。 ↩ -
出处:「10.5 后置缓冲区(osbook_day10e)」(
text/77-ch10-06-10-6-osbook-day10f.txt:16(搜「0.7倍」))。 ↩