# QLineEdit 自绘小眼睛 / echo mode 切换 教学示例 —— 设计

- **日期**：2026-07-08
- **对应课程**：Day4 上午「消息机制和事件 + 绘图事件」（OUTLINE_4DAY.md §2 Day4 上午）
- **目标产物**：`docs/teaching/examples/lineedit_eye_toggle/`，交付为两种形态——
  ① 仓库统一构建里的 `qt_lineedit_eye_toggle` target；
  ② 可独立 `cmake -S . -B build` 构建的单独 CMake 工程。

## 1. 背景与目标

需要一个 Day4 上午课堂演示示例，**同时覆盖两个主题**：组件绘制（`paintEvent`）、
事件捕捉（`mousePressEvent` / `mouseMoveEvent`）。载体是 QLineEdit 扩展——在右侧加一个
小眼睛图标，点击动态切换 `echoMode`（`Password` ↔ `Normal`），并显式处理与 QLineEdit
内置 clear button 的位置冲突。

> 选 QLineEdit 而非自造控件：密码框是「自绘内嵌图标 + 点击切换」最真实的工业场景，
> 且复用 QLineEdit 的文本/编辑能力，让示例聚焦在「绘制 + 事件」本身。

## 2. 关键设计决策

### 2.1 组织形态：梯度对照版
窗口分三区，先简后深，让学生看清「为什么」：

| 区 | 内容 | 教学作用 |
|---|---|---|
| ① | 4 种 echo mode 静态对照（Normal/NoEcho/Password/PasswordEchoOnEdit） | 先给 echo mode 全貌，眼睛只是切换载体 |
| ② | `addAction(QIcon, TrailingPosition)` 一行版 | Qt 内置做法，1 行实现——「为什么还要自绘」的引子 |
| ③ | `PasswordLineEdit : public QLineEdit` 自绘版 + 「启用 clear button」复选框 | **主题落地**：自绘 + 事件捕获 + clear button 避让 |

### 2.2 实现方式锁定：子类化 + 自绘 + 自捕获
`addAction` 那条路由 Qt 全权处理位置/点击/避让，**碰不到**绘制与事件这两个主题。
故核心交付物必须是子类化 QLineEdit、覆写 `paintEvent`/`mouse*Event`。addAction 仅作
②区对照存在。

### 2.3 clear button 避让：读 clearButton 子 widget 几何
QLineEdit 内置 clear button 的宽度随主题/DPI/字号变化，**不能硬编码偏移**（坑卡）。
clear button 是 QLineEdit 的一个 `objectName=="clearButton"` 直接子 widget（Qt5 私有
`QLineEditIconButton`），用 `findChild<QWidget*>("clearButton")` 读其 `geometry()`，眼睛紧贴
其左侧；未启用或无文本时贴右边。
> 注：初版误用 `QStyle::subControlRect(CC_LineEdit, …, SC_LineEditClearButton)`，编译期即报错
> ——`QStyle` 没有这两个成员，QLineEdit 不经 complex-control 绘制，无对应 subcontrol。读子 widget 是唯一可靠途径。

### 2.4 眼睛图标：QPainter 纯代码绘制
- 不引入 `.qrc` 资源（YAGNI），完全契合「组件绘制」主题。
- 两态：`closed=true`（密码态）= 眼眶椭圆 + 斜杠；`closed=false`（明文态）= 眼眶椭圆 + 实心瞳孔。
- 绘制逻辑抽成自由函数 `paintEye(QPainter*, QRect, closed, hover)`，③区自绘与②区
  `QIcon` pixmap 共用（DRY，并让对照更明显）。
- hover 态描边加深为蓝色（`#0078D7`），需 `setMouseTracking(true)` 才能在无按键时收到 move。

### 2.5 双形态 CMake（源文件零重复）
- **唯一源文件**：`qt_lineedit_eye_toggle.cpp`。
- **两个 CMake 上下文**引用它：
  - 独立工程：`lineedit_eye_toggle/CMakeLists.txt`（手写规范版，C++17，非 Qt Creator 模板）。
  - 仓库集成：`examples/CMakeLists.txt` 的 `add_teaching_example(qt_lineedit_eye_toggle lineedit_eye_toggle)`。
- **零污染**：顶层 `CMakeLists.txt:58` `add_subdirectory(docs/teaching/examples)` 不递归，
  子目录 `CMakeLists.txt` 永不被统一构建触发；`examples/CMakeLists.txt` 无 GLOB。
  不计入 README/CLAUDE.md 里「173 目标」的生成器计数。

## 3. `PasswordLineEdit` 子类要点

```
构造: setEchoMode(Password); setMouseTracking(true); setTextMargins(0,0,iconSize+8,0)
paintEvent:    base::paintEvent 先画原生; 再 recomputeEyeRect(); QPainter 画眼睛
mousePressEvent:  recomputeEyeRect(); 命中眼睛 → 切 m_passwordMode + setEchoMode + accept;
                  否则 base::mousePressEvent（光标定位/双击选词不受影响）
mouseMoveEvent:   recomputeEyeRect(); hit-test 更新 m_eyeHover + setCursor; base 保留拖选
leaveEvent:       清 m_eyeHover 局部刷新
recomputeEyeRect: findChild("clearButton") 读其几何 → 眼睛贴其左 / 贴右
信号: passwordModeChanged(bool)  供窗口状态栏显示
```

**关键不变量**：命中眼睛时 `e->accept()` 并 `return`，**不**调基类——否则 QLineEdit 会
把点击当光标定位；非命中时务必调基类，保留原生编辑行为。这是「事件捕获」教学点的核心。

## 4. 文件清单

```
docs/teaching/examples/lineedit_eye_toggle/
├── qt_lineedit_eye_toggle.cpp   # 唯一源文件（paintEye + PasswordLineEdit + ShowcaseWidget + main）
├── CMakeLists.txt               # 独立顶层工程（手写规范版，可 cmake -S . -B build）
└── README.md                    # 教学点 / 目录结构 / 构建（集成+独立两条）/ 坑卡
```
另：`docs/teaching/examples/CMakeLists.txt` 注册目标 + 加入 `all_teaching_examples` 依赖列表。

## 5. 验证标准（可证伪）

1. **集成构建**：`cmake --build build_mw --target qt_lineedit_eye_toggle` 绿。
2. **独立构建**：`cmake -S lineedit_eye_toggle -B lineedit_eye_toggle/build ... && cmake --build` 绿。
3. **离屏**：两路径产物 `QT_QPA_PLATFORM=offscreen` 跑 200ms 后退出码 0。
4. **真实交互**（本机 Qt 实跑，截图为证）：③区眼睛可见；点击切 `Password↔Normal`；
   勾选 clear button 后眼睛正确让到其左侧不重叠；hover 高亮生效。

## 6. 显式假设

- 眼睛用 QPainter 代码绘制，不引入资源文件（契合主题 + YAGNI）。
- 独立 `CMakeLists.txt` 手写规范版（C++17 / `cmake_minimum_required(3.16)` / 无 ANDROID 噪音），
  不沿用子目录里 Qt Creator 导出的低质量模板。
- ①区 4 个对照框 `setReadOnly(true)` 防误改；`PasswordEchoOnEdit` 的「编辑时明文」效果在
  README 注明需取消只读才能体验。
- **不提交 git**（项目红线：未经请求不操作 git）。
