Files
scuc-qt-course/tools/gen_part3.py
T
张宗平 1a788a8278 移除 Docker 构建路线,改为 host 直装 Qt 5.14.2 + Qt Creator 4.11.0
学生主要用 Windows,Docker 方案不再需要:删掉 tools/docker/ 及 README/HANDOFF/
CMakeLists 里的相关引用(含一处已失效的 docker_run.sh 死链接)。

install_qt_host.sh 现在同时装期号匹配的 Qt Creator 4.11.0:官方 .run 安装器
强制要求 Qt 账号登录且无 Skip 选项,改用 tools/extract_qtcreator_payload.py
直接从 .run 里定位、解出内嵌的 7z payload,绕开账号登录。提取逻辑先解到
staging 目录再原子替换到位,避免中途失败留下部分安装但被判定为"已装";
候选 7z 复用同一临时文件,不会在磁盘上堆多份安装包大小的临时拷贝。
2026-07-02 13:02:37 +08:00

1464 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# ==============================================================================
# gen_part3.py — 生成 Part3Qt 桌面应用开发)的全部源码、.qrc 与 CMakeLists
#
# 输入:tools/wiki_data/ 中 9 个 Qt 页面(pageId 5895451558954552,共 73 块)。
# 策略:
# * 每块按内容形态分类(见 BLOCK_TREATMENT):
# - 'program' 原文已含 main+QApplication,作为目标近乎原样(补 include/修笔误)。
# - 'harness' 有意义片段(控件/信号槽/事件/绘图),用 main+QApplication+show
# 外壳包裹,必要时补所需 QWidget/QMainWindow 子类骨架。
# - 'snippet' 纯签名/单语句/伪代码,不单独建目标,折进各章 README 作文档。
# * 每个目标 = 一个独立 add_executable;开 AUTOMOCQ_OBJECT 类需 moc)。
# * 离屏运行友好:每个目标注入一个 QTimer::singleShot 让程序 200ms 后自动退出,
# 便于 QT_QPA_PLATFORM=offscreen 自动化验证(窗口类目标)。
# * 资源依赖(:/images/*.png 等)用脚本生成的纯色占位 png + .qrc 提供,qt5_add_resources。
# * Qt 代码修正(笔误/平台/过时 API)每处 record_erratum 进 docs/ERRATA.md(透明可审计)。
#
# 验证:宿主机用隔离 Qt 5.14.2 构建 →
# QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target> 自动退出验证。
# ------------------------------------------------------------------------------
import json, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gen_sources
from gen_sources import (REPO, WIKI, page_outline, common_fix, write, record_erratum,
PAGE_CONFIG, classify_lang)
from file_name_map import stem_for
ERRATA = gen_sources.ERRATA
EXTRACTED = json.load(open(os.path.join(WIKI, "extracted_code.json")))
PAGES = {p["id"]: p for p in json.load(open(os.path.join(WIKI, "all_page_ids.json")))}
def norm_title(title):
"""Strip Confluence anchor prefixes like '_Toc466556765' that some Qt page
headings carry, so titles match the clean keys in file_name_map.py."""
return re.sub(r"_Toc\d+", "", title).strip()
PAGES_META_PID = "?"
# ==============================================================================
# 逐块处理表:键=(pageId, blockIdx),值=该块如何处理。
# ('program', include_list) 原文含 main,作目标;include_list=需补的 include。
# ('harness', harness_spec) 需 main 外壳;harness_spec 见 emit_harness。
# ('snippet', None) 不建目标,折进 README。
# 分类依据:tools subagent 对 73 块的勘察(见会话记录)。
# ==============================================================================
# 'harness' 的形态(决定包裹方式):
# ('plain', includes, body_kind)
# body_kind: 'top' 片段直接放文件作用域(已含完整的成员函数定义/类)
# 'widget-ctor' 片段是某个 widget 子类构造函数体内的语句(用 this)
# 'mainwin-ctor' 片段是 QMainWindow 子类构造函数体内语句
# 'member-def' 片段是 ::成员函数 定义(需先有类声明骨架)
# ('class+main', includes, class_decl, body) 先放 class_decl 再放 bodymain 实例化
# 每块的 include 需求(按片段用到的 Qt 类确定;标准模板会再加 QApplication/Widget
INC = {
"app": "#include <QApplication>",
"widget": "#include <QWidget>",
"mwin": "#include <QMainWindow>",
"pushbtn": "#include <QPushButton>",
"label": "#include <QLabel>",
"dialog": "#include <QDialog>",
"msgbox": "#include <QMessageBox>",
"filedlg": "#include <QFileDialog>",
"dock": "#include <QDockWidget>",
"textedit": "#include <QTextEdit>",
"menubar": "#include <QMenuBar>",
"action": "#include <QAction>",
"qfile": "#include <QFile>",
"qfileinfo": "#include <QFileInfo>",
"qtextstream": "#include <QTextStream>",
"qdatastream": "#include <QDataStream>",
"qdebug": "#include <QDebug>",
"qiodevice": "#include <QIODevice>",
"qpainter": "#include <QPainter>",
"qpixmap": "#include <QPixmap>",
"qbitmap": "#include <QBitmap>",
"qimage": "#include <QImage>",
"qpicture": "#include <QPicture>",
"qpaintevent": "#include <QPaintEvent>",
"qmovie": "#include <QMovie>",
"qspinbox": "#include <QSpinBox>",
"qslider": "#include <QSlider>",
"qboxlayout": "#include <QHBoxLayout>",
"qevent": "#include <QEvent>",
"qmouseevent": "#include <QMouseEvent>",
"qstring": "#include <QString>",
"qtimer": "#include <QTimer>",
"qstringlist": "#include <QStringList>",
"qcoreapp": "#include <QCoreApplication>",
}
# 构建时的逐块处理决策表(pageId, blockIdx -> 处理方式)。
# 形态见上方注释。资源依赖(resources 集合)单独由 exact_fix 标记收集。
BLOCK_TREATMENT = {
# === 58954515 ch02 创建Qt项目 ===
("58954515", 0): ("rewrite", "minimal_qt_app", "rewrite_minimal_qt_app"),
# === 58954525 ch03 第一个Qt小程序 ===
# 0: 按钮创建(widget-ctor 片段,混入散文 + 含中文字符串,改 rewrite 避免 prose 误伤)
("58954525", 0): ("rewrite", "button_creation", "rewrite_button_creation"),
# 1,2: 对象树伪代码 → snippet
("58954525", 1): ("snippet", None, None),
("58954525", 2): ("snippet", None, None),
# === 58954527 ch04 信号和槽 ===
# 0: 系统自带信号槽(widget-ctorconnect close)— 引用 MyWidget::close,改 rewrite
("58954527", 0): ("rewrite", "builtin_signal_slot", "rewrite_builtin_signal_slot"),
# 1: connect 签名 → snippet
("58954527", 1): ("snippet", None, None),
# 2: 自定义信号槽(含 Teacher/Student Q_OBJECT + MyWidget,重散文)→ 整体可编译重写
("58954527", 2): ("rewrite", "custom_signal_slot", "rewrite_custom_signal_slot"),
# 3: 带参数自定义信号槽(重载解析 connect)→ snippet(依赖 2 的类)
("58954527", 3): ("snippet", None, None),
# 4: Qt4 SIGNAL/SLOT 写法 → snippet(过时写法,仅文档)
("58954527", 4): ("snippet", None, None),
# 5: lambda 语法骨架 → snippet
("58954527", 5): ("snippet", None, None),
# 6: lambda 实例(widget-ctor + QPushButton + connect lambda
("58954527", 6): ("harness", "lambda_signal_slot",
{"includes": [INC["app"], INC["widget"], INC["pushbtn"], INC["qdebug"]],
"body_kind": "widget-ctor"}),
# === 58954529 ch05 QMainWindow ===
# 0,1,2: 菜单栏 API 签名 → snippet
("58954529", 0): ("snippet", None, None),
("58954529", 1): ("snippet", None, None),
("58954529", 2): ("snippet", None, None),
# 3: 状态栏签名 → snippet
("58954529", 3): ("snippet", None, None),
# 4: 铆接部件(mainwin-ctor,尾部混散文 + 中文标题,改 rewrite)
("58954529", 4): ("rewrite", "dock_widget", "rewrite_dock_widget"),
# 5: 核心部件(mainwin-ctor
("58954529", 5): ("harness", "central_widget",
{"includes": [INC["app"], INC["mwin"], INC["textedit"]],
"body_kind": "mainwin-ctor"}),
# 6: .qrc XML 示例 → snippet(非 C++
("58954529", 6): ("snippet", None, None),
# === 58954535 ch06 对话框 ===
# 0: 模态对话框(widget-ctor handlerexec()
("58954535", 0): ("harness", "modal_dialog",
{"includes": [INC["app"], INC["widget"], INC["dialog"]],
"body_kind": "widget-ctor-handler"}),
# 1,2,3: 非模态对话框(widget-ctor handlershow
("58954535", 1): ("harness", "modeless_dialog_stack",
{"includes": [INC["app"], INC["widget"], INC["dialog"]],
"body_kind": "widget-ctor-handler"}),
("58954535", 2): ("harness", "modeless_dialog_heap",
{"includes": [INC["app"], INC["widget"], INC["dialog"]],
"body_kind": "widget-ctor-handler"}),
("58954535", 3): ("harness", "modeless_dialog_deleteonclose",
{"includes": [INC["app"], INC["widget"], INC["dialog"]],
"body_kind": "widget-ctor-handler"}),
# 4-9: QMessageBox 签名 → snippet
("58954535", 4): ("snippet", None, None),
("58954535", 5): ("snippet", None, None),
("58954535", 6): ("snippet", None, None),
("58954535", 7): ("snippet", None, None),
("58954535", 8): ("snippet", None, None),
("58954535", 9): ("snippet", None, None),
# 10,11: QMessageBox 实例(widget-ctor-handler
("58954535", 10): ("harness", "message_box_question",
{"includes": [INC["app"], INC["widget"], INC["msgbox"], INC["qdebug"]],
"body_kind": "widget-ctor-handler"}),
("58954535", 11): ("harness", "message_box_custom",
{"includes": [INC["app"], INC["widget"], INC["msgbox"], INC["qdebug"]],
"body_kind": "widget-ctor-handler"}),
# 12,13,14: 文件对话框 MainWindow(合并为一个目标 file_dialog
("58954535", 12): ("rewrite", "file_dialog", "rewrite_file_dialog"),
("58954535", 13): ("snippet", None, None),
("58954535", 14): ("snippet", None, None),
# 15: getOpenFileName 签名 → snippet
("58954535", 15): ("snippet", None, None),
# === 58954540 ch08 常用控件 ===
# 0: setText 签名 → snippet
("58954540", 0): ("snippet", None, None),
# 1: QLable 创建(widget-ctor,笔误 QLable
("58954540", 1): ("harness", "label_text_create",
{"includes": [INC["app"], INC["widget"], INC["label"]],
"body_kind": "widget-ctor", "fix": "qlable_typo"}),
# 2: label html 超链接(widget-ctor
("58954540", 2): ("harness", "label_text_html",
{"includes": [INC["app"], INC["widget"], INC["label"]],
"body_kind": "widget-ctor"}),
# 3-6: 显示图片(4,5,6 合并为一个目标 label_pixmap3 是签名 snippet
("58954540", 3): ("snippet", None, None),
("58954540", 4): ("snippet", None, None),
("58954540", 5): ("snippet", None, None),
("58954540", 6): ("snippet", None, None),
("58954540", "img"): ("rewrite", "label_pixmap", "rewrite_label_pixmap"),
# 7-10: 显示动画(合并为目标 label_movie
("58954540", 7): ("snippet", None, None),
("58954540", 8): ("snippet", None, None),
("58954540", 9): ("snippet", None, None),
("58954540", 10): ("snippet", None, None),
("58954540", "movie"): ("rewrite", "label_movie", "rewrite_label_movie"),
# 11-14: lineedit 签名 → snippet
("58954540", 11): ("snippet", None, None),
("58954540", 12): ("snippet", None, None),
("58954540", 13): ("snippet", None, None),
("58954540", 14): ("snippet", None, None),
# 15: 自定义控件 SmallWidgetQ_OBJECT,自带 main 实例化)
("58954540", 15): ("rewrite", "custom_widget", "rewrite_custom_widget"),
# === 58954546 ch09 事件 ===
# 0: EventLabel 完整程序(mouse event)— 含字符串内误入换行,改 rewrite
("58954546", 0): ("rewrite", "mouse_event", "rewrite_mouse_event"),
# 1: setMouseTracking 单语句 → snippet
("58954546", 1): ("snippet", None, None),
# 2: CustomWidget::event overridemember-def,需类骨架)
("58954546", 2): ("rewrite", "event_override_customwidget",
"rewrite_event_override_customwidget"),
# 3: CustomTextEdit::event override → snippet(依赖骨架)
("58954546", 3): ("snippet", None, None),
# 4,5: event 分发参考 → snippet
("58954546", 4): ("snippet", None, None),
("58954546", 5): ("snippet", None, None),
# 6: MainWindow 事件过滤器(class+main,无 Q_OBJECT 但有 eventFilter override
("58954546", 6): ("rewrite", "event_filter_mainwindow",
"rewrite_event_filter_mainwindow"),
# 7: 事件过滤器 CustomWidget+FilterObject(重散文)→ snippet(依赖复杂)
("58954546", 7): ("snippet", None, None),
# 8,9: Win32 WndProc 参考 → snippet(非 Qt
("58954546", 8): ("snippet", None, None),
("58954546", 9): ("snippet", None, None),
# 10: notify 签名 → snippet
("58954546", 10): ("snippet", None, None),
# === 58954548 ch10 绘图 ===
# 0+1: PaintedWidget (Q_OBJECT) h+cpp 合并目标 qpainer_basic
("58954548", 0): ("rewrite", "qpainter_basic", "rewrite_qpainter_basic"),
("58954548", 1): ("snippet", None, None), # 并入上面的 rewrite
# 2: PaintWidget 绘 pixmap/bitmap(含资源 butterfly.png
("58954548", 2): ("rewrite", "paint_device_pixmaps",
"rewrite_paint_device_pixmaps"),
# 3: PaintWidget 绘 QImage
("58954548", 3): ("rewrite", "paint_device_qimage",
"rewrite_paint_device_qimage"),
# 4: QPictureWindows 路径 D:\n 修正)
("58954548", 4): ("rewrite", "qpicture_io", "rewrite_qpicture_io"),
# === 58954552 ch11 文件系统 ===
# 0: 完整程序 QFile 读 in.txt(原文用 QApplication+app.exec() 会阻塞 GUI 事件循环,
# 实为控制台式文件操作;改 rewrite 用 QCoreApplication 并立即退出)
("58954552", 0): ("rewrite", "qfile_basic", "rewrite_qfile_basic"),
# 1: QFileInfo 字段 demo(双重声明 bug)→ snippet
("58954552", 1): ("snippet", None, None),
# 2,3,4: QDataStream 读写 file.dat → 合并为 qdatastream_io
("58954552", 2): ("snippet", None, None),
("58954552", 3): ("snippet", None, None),
("58954552", 4): ("snippet", None, None),
("58954552", "data"): ("rewrite", "qdatastream_io", "rewrite_qdatastream_io"),
# 5,6: QTextStream 读写 file.txt → 合并为 qtextstream_io
("58954552", 5): ("snippet", None, None),
("58954552", 6): ("snippet", None, None),
("58954552", "text"): ("rewrite", "qtextstream_io", "rewrite_qtextstream_io"),
}
# 目标 → 所需资源(:/前缀 资源路径)。占位 png 由生成器产出 + .qrc。
TARGET_RESOURCES = {
"label_pixmap": {":/Image/boat.jpg": "image_boat.jpg"},
"label_movie": {":/Mario.gif": "mario.gif"},
"paint_device_pixmaps": {":/Image/butterfly.png": "image_butterfly.png",
":/Image/butterfly1.png": "image_butterfly1.png"},
}
# 目标 → 运行时数据文件(写入工作目录供程序读写)
TARGET_DATAFILES = {
"qfile_basic": "in.txt",
"qdatastream_io": "file.dat",
"qtextstream_io": "file.txt",
}
# ==============================================================================
# 外壳模板:把一个「片段」包成可运行的 Qt 程序
# ==============================================================================
AUTO_QUIT = (
" // 离屏/自动化验证:200ms 后自动退出(GUI 模式下不阻塞)\n"
" QTimer::singleShot(200, &app, &QApplication::quit);\n"
)
AUTO_QUIT_CORE = (
" QTimer::singleShot(0, &app, &QCoreApplication::quit);\n"
)
def emit_program(pid, idx, stem, out_dir, meta):
"""原文已含 main+QApplication 的完整程序:补 include、修笔误后原样输出。"""
code = EXTRACTED[pid][idx]["code"]
code = common_fix(code, pid, idx)
code = exact_fix(code, pid, idx)
includes = "\n".join(meta.get("includes", []))
if meta.get("needs_widget_h"):
# 58954515/0 引用了 "widget.h",补一个内联的最小 Widget 定义使自洽
includes += "\n#include <QWidget>\nclass Widget : public QWidget { Q_OBJECT public: Widget(QWidget* p=nullptr):QWidget(p){} };\n"
parts = [header_comment(pid, idx, stem)]
parts.append("#include <QApplication>\n")
if includes.strip():
parts.append(includes + "\n")
parts.append("\n")
parts.append(code.rstrip() + "\n")
if not re.search(r"\bQApplication\b", code):
# 万一原文用 QCoreApplication,上面的 singleShot 也兼容
pass
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), "".join(parts))
return src
def emit_harness(pid, idx, stem, out_dir, meta):
"""把片段包进 main+QApplication+show。body_kind 决定片段放置方式。"""
code = EXTRACTED[pid][idx]["code"]
code = common_fix(code, pid, idx)
code = exact_fix(code, pid, idx)
if meta.get("fix") == "prose_strip":
code = strip_inline_prose(code, pid, idx)
body_kind = meta.get("body_kind", "widget-ctor")
includes = meta.get("includes", [])
parts = [header_comment(pid, idx, stem)]
for inc in includes:
parts.append(inc + "\n")
if INC["qtimer"] not in includes:
parts.append(INC["qtimer"] + "\n")
parts.append("\n")
if body_kind in ("widget-ctor", "widget-ctor-handler"):
# 片段是某 widget 子类构造函数体内语句(用 this)。包成:
# class DemoWidget : public QWidget { Q_OBJECT public: DemoWidget(){ <片段> } };
cls = "DemoWidget"
parts.append(f"class {cls} : public QWidget {{\n")
parts.append(" Q_OBJECT\n")
parts.append("public:\n")
if body_kind == "widget-ctor-handler":
# 片段是事件处理(如按钮点击弹对话框):放一个槽里,构造时触发一次
parts.append(f" {cls}(){{ doDemo(); }}\n")
parts.append("private slots:\n void doDemo(){\n")
parts.append(indent(code, 2) + "\n")
parts.append(" }\n")
else:
parts.append(f" {cls}(){{\n")
parts.append(indent(code, 2) + "\n")
parts.append(" }\n")
parts.append("};\n\n")
parts.append(main_with_widget(cls))
elif body_kind == "mainwin-ctor":
cls = "DemoMainWindow"
parts.append(f"class {cls} : public QMainWindow {{\n")
parts.append(" Q_OBJECT\npublic:\n")
parts.append(f" {cls}(){{\n")
parts.append(indent(code, 2) + "\n")
parts.append(" }\n};\n\n")
parts.append(main_with_widget(cls, is_mainwin=True))
else:
parts.append(code.rstrip() + "\n")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), "".join(parts))
return src
def main_with_widget(cls, is_mainwin=False):
s = (
"int main(int argc, char* argv[]) {\n"
" QApplication app(argc, argv);\n"
f" {cls} w;\n"
" w.show();\n"
+ AUTO_QUIT +
" return app.exec();\n"
"}\n"
)
return s
def header_comment(pid, idx, stem):
return (
"// ============================================================================\n"
f"// {stem} — Qt 桌面示例\n"
"// 来源:川大 C++/LinuxC wiki「02.Qt方向」\n"
f"// 原文片段:[{pid}:{idx}]\n"
"// 说明:本文件由 gen_part3.py 从 wiki 片段生成(必要时补 main/QApplication\n"
"// 外壳、include、修正笔误)。详见 docs/ERRATA.md。\n"
"// 离屏验证:QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>\n"
"// ============================================================================\n\n"
)
def indent(s, n):
pad = " " * n
return "\n".join(pad + l if l.strip() else l for l in s.splitlines())
# ==============================================================================
# exact_fix:对 Qt 片段的轻量自动修正(笔误/全角/平台)。逐块的复杂重写见 rewrite_*。
# ==============================================================================
def exact_fix(code, pid, idx):
# QLable → QLabel(笔误)
if "QLable" in code:
code = code.replace("QLable", "QLabel")
record_erratum(pid, idx, "typo", "QLable", "QLabel",
"笔误:QLabel 误写为 QLable。")
# 散文混入代码:把「行内/行尾」的中文教学散文剥离。
# 典型: QPushButton * btn = new QPushButton; 头文件 #include <QPushButton>
# 典型: ...setAllowedAreas(...) 设置区域范围
# 策略:对含 CJK 字符的行,若该行同时含代码(含 ASCII 标点/关键字),则截断在第一段
# 连续 CJK 处(去掉 CJK 及其后的整行内容);若整行都是 CJK/注释,保留原样(多是注释)。
# 仅当该块被标记需要 prose 处理时执行(避免误伤正常中文注释)。
return code
def strip_inline_prose(code, pid, idx):
"""Remove inline Chinese teaching prose that the wiki glued onto code lines.
Applied only to blocks flagged 'prose_strip' in BLOCK_TREATMENT."""
orig = code
out_lines = []
for line in code.splitlines():
# find first run of CJK chars in the line
m = re.search(r"[\u4e00-\u9fff]", line)
if not m:
out_lines.append(line); continue
# is the CJK preceded by real code on this line? (// comment lines are fine)
before = line[:m.start()]
codeish = bool(re.search(r"[A-Za-z0-9_)\];,]", before))
commented = "//" in before or before.strip().startswith("/*")
if codeish and not commented:
# truncate at the CJK run, keep the code part
out_lines.append(before.rstrip())
else:
out_lines.append(line)
fixed = "\n".join(out_lines)
if fixed != orig:
record_erratum(pid, idx, "prose-in-code",
"代码行内/行尾混入中文教学散文(如「头文件 #include <QPushButton>」「设置区域范围」)",
"剥离行内中文散文,保留代码部分",
"wiki 把讲解散文直接粘在代码行末,破坏语法(如把 #include 当成语句尾巴)。"
"剥离散文使代码可编译。")
return fixed
# ==============================================================================
# rewrite_*:对结构复杂/散文混入严重的块,直接给出可编译的完整程序。
# ==============================================================================
def rewrite_minimal_qt_app(pid, idx, out_dir, stem):
"""2.1 一个最简单的Qt应用程序。原文 #include "widget.h" 引用外部 Widget
输出自洽的最小 Widget + main(保留 Qt 项目骨架教学意图)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QTimer>\n\n"
"// 最简单的 Widget:原文 widget.h 的占位实现\n"
"class Widget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" Widget(QWidget* parent = nullptr) : QWidget(parent) {}\n"
"};\n\n"
"int main(int argc, char* argv[]) {\n"
" QApplication app(argc, argv);\n"
" Widget w;\n"
" w.show();\n" +
AUTO_QUIT +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "missing-symbol",
"原文 #include \"widget.h\" 引用外部 Widget 类(widget.h/widget.cpp 不在块内)",
"在文件内就地补一个最小 Widget : public QWidgetQ_OBJECT+ main",
"原 wiki 的「最简单 Qt 程序」依赖外部的 widget.h;按「示例相对隔离」拆分为独立目标后"
"本文件缺少 Widget 定义。补最小 Widget 使其自洽可编译运行。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_button_creation(pid, idx, out_dir, stem):
"""3.1 按钮的创建:QPushButton 创建、设父、设文本、移动、设窗口大小/标题。
原文行内混入散文「头文件 #include <QPushButton>」;中文文本在字符串内,
通用 prose 剥离会误伤字符串,故整体重写。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPushButton>\n"
"#include <QTimer>\n\n"
"class DemoWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" DemoWidget() {\n"
" // 第一种创建:先构造再设属性\n"
" QPushButton* btn = new QPushButton;\n"
" btn->setParent(this); // 设置父亲\n"
" btn->setText(QStringLiteral(\"德玛西亚\")); // 设置文字\n"
" btn->move(100, 100); // 移动位置\n\n"
" // 第二种创建:构造时指定文本与父亲\n"
" QPushButton* btn2 = new QPushButton(QStringLiteral(\"孙悟空\"), this);\n"
" btn2->move(100, 140);\n\n"
" this->resize(600, 400); // 重新指定窗口大小\n"
" this->setWindowTitle(QStringLiteral(\"第一个项目\")); // 设置窗口标题\n"
" this->setFixedSize(600, 400); // 限制窗口大小\n"
" }\n"
"};\n\n" + main_with_widget("DemoWidget")
)
record_erratum(pid, idx, "prose-in-code",
"按钮创建片段行内混入散文「头文件 #include <QPushButton>」,且含中文字符串",
"重写为可编译 DemoWidget(剥离散文、保留中文文本)",
"原 wiki 把讲解散文粘在代码行内(如 #include 当成尾巴),破坏语法;通用 prose "
"剥离会误伤字符串内的中文,故整体重写保留教学意图。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_dock_widget(pid, idx, out_dir, stem):
"""5.4 铆接部件 QDockWidgetaddDockWidget + setAllowedAreas。
原文尾部混散文「设置区域范围」;含中文标题,整体重写。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QMainWindow>\n"
"#include <QDockWidget>\n"
"#include <QTimer>\n\n"
"class DemoMainWindow : public QMainWindow {\n"
" Q_OBJECT\n"
"public:\n"
" DemoMainWindow() {\n"
" // 铆接部件(停靠窗口):标题 + 父对象\n"
" QDockWidget* dock = new QDockWidget(QStringLiteral(\"标题\"), this);\n"
" addDockWidget(Qt::LeftDockWidgetArea, dock);\n"
" // 设置可停靠区域范围(左/右/上/下)\n"
" dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea |\n"
" Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea);\n"
" }\n"
"};\n\n" + main_with_widget("DemoMainWindow", is_mainwin=True)
)
record_erratum(pid, idx, "prose-in-code",
"QDockWidget 片段尾部混入散文「设置区域范围」,含中文标题字符串",
"重写为可编译 DemoMainWindow(剥离散文、保留中文标题)",
"原 wiki 把讲解散文粘在代码行尾(setAllowedAreas 后接「设置区域范围」),破坏语法;"
"整体重写保留「铆接部件」教学意图。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_builtin_signal_slot(pid, idx, out_dir, stem):
"""4.1 系统自带信号和槽:QPushButton clicked → 窗口 close。
原文用 &MyWidget::close;改 DemoWidget + QWidget::close。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPushButton>\n"
"#include <QTimer>\n\n"
"class DemoWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" DemoWidget() {\n"
" // 系统自带的信号和槽:按钮 clicked → 关闭窗口\n"
" QPushButton* quitBtn = new QPushButton(QStringLiteral(\"关闭窗口\"), this);\n"
" quitBtn->move(50, 50);\n"
" resize(300, 200);\n"
" connect(quitBtn, &QPushButton::clicked, this, &QWidget::close);\n"
" }\n"
"};\n\n" + main_with_widget("DemoWidget")
)
record_erratum(pid, idx, "harness-context",
"片段 connect(quitBtn,&QPushButton::clicked,this,&MyWidget::close) 引用 MyWidget"
"但 MyWidget 定义不在块内",
"补 DemoWidget 子类 + main 外壳,槽用 QWidget::close(系统自带)",
"原 wiki 片段假定存在 MyWidget 类与 close 槽;按「示例相对隔离」补齐类骨架与 main,"
"演示「系统自带信号(clicked)→系统自带槽(close)」。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_mouse_event(pid, idx, out_dir, stem):
"""9.1 事件:EventLabel 重写 mouseMoveEvent/PressEvent/ReleaseEvent。
原文 QString(\"...(%1, %2)\\n...\") 内误入了字面换行;合并为单行字符串。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QLabel>\n"
"#include <QMouseEvent>\n"
"#include <QString>\n"
"#include <QTimer>\n\n"
"// 自定义 QLabel:重写鼠标事件,显示坐标\n"
"class EventLabel : public QLabel {\n"
" Q_OBJECT\n"
"public:\n"
" explicit EventLabel(QWidget* p = nullptr) : QLabel(p) {\n"
" setMouseTracking(true); // 启用鼠标移动追踪(默认只在按键时追踪)\n"
" setText(QStringLiteral(\"移动鼠标到这里\"));\n"
" }\n"
"protected:\n"
" void mouseMoveEvent(QMouseEvent* event) override {\n"
" setText(QString(\"<center><h1>Move: (%1, %2)</h1></center>\")\n"
" .arg(event->x()).arg(event->y()));\n"
" }\n"
" void mousePressEvent(QMouseEvent* event) override {\n"
" setText(QString(\"<center><h1>Press: (%1, %2)</h1></center>\")\n"
" .arg(event->x()).arg(event->y()));\n"
" }\n"
" void mouseReleaseEvent(QMouseEvent* event) override {\n"
" setText(QString(\"<center><h1>Release: (%1, %2)</h1></center>\")\n"
" .arg(event->x()).arg(event->y()));\n"
" }\n"
"};\n\n" + main_with_widget("EventLabel")
)
record_erratum(pid, idx, "syntax+merge",
"EventLabel 事件重写程序:QString(\"...(%1, %2)\\n...\") 内误入字面换行(断行)"
"导致字符串未终结;类声明与实现分散",
"合并为单 .cpp,字符串内换行移除(\\n 移出字符串或删除),补 setMouseTracking",
"原 wiki 的 QString 字面量在源码中跨行书写(实际是 \\n 后跟换行),C++ 字符串字面量"
"不能直接跨行,导致 missing terminating \"。修正为单行字符串。同时补 mouseTracking。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_custom_signal_slot(pid, idx, out_dir, stem):
"""4.2 自定义信号和槽:Teacher/Student Q_OBJECT + MyWidgetemit + connect。
原文散文混入严重;给出可编译的完整对照程序。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPushButton>\n"
"#include <QDebug>\n"
"#include <QString>\n"
"#include <QTimer>\n\n"
"// ---- 老师(发送者):饿了就发信号 ----\n"
"class Teacher : public QObject {\n"
" Q_OBJECT\n"
"public:\n"
" explicit Teacher(QObject* p = nullptr) : QObject(p) {}\n"
"signals:\n"
" void hungry(); // 无参信号\n"
" void hungry(QString food);// 带参信号(重载)\n"
"public slots:\n"
" void classIsOver() {\n"
" emit hungry(); // 触发无参信号\n"
" emit hungry(QString(\"汉堡\")); // 触发带参信号\n"
" }\n"
"};\n\n"
"// ---- 学生(接收者):请客吃饭 ----\n"
"class Student : public QObject {\n"
" Q_OBJECT\n"
"public:\n"
" explicit Student(QObject* p = nullptr) : QObject(p) {}\n"
"public slots:\n"
" void treat() { qDebug() << \"老师饿了,请老师吃饭!\"; }\n"
" void treat(QString food) { qDebug() << \"老师饿了,请老师吃\" << food; }\n"
"};\n\n"
"// ---- 聚合窗口:连接信号槽并触发演示 ----\n"
"class MyWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" MyWidget() {\n"
" Teacher* t = new Teacher(this);\n"
" Student* s = new Student(this);\n"
" // 连接无参信号/槽\n"
" connect(t, QOverload<>::of(&Teacher::hungry),\n"
" s, QOverload<>::of(&Student::treat));\n"
" // 带参重载:用 QOverload<QString>::of 显式选择重载\n"
" connect(t, QOverload<QString>::of(&Teacher::hungry),\n"
" s, QOverload<QString>::of(&Student::treat));\n"
" // 触发演示\n"
" t->classIsOver();\n"
" }\n"
"};\n\n"
"int main(int argc, char* argv[]) {\n"
" QApplication app(argc, argv);\n"
" MyWidget w; w.show();\n" +
AUTO_QUIT +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "prose-in-code",
"自定义信号槽:Teacher/Student/MyWidget 定义与中文散文混排,且信号名 hungury 笔误、"
"槽名 treat/eat 不一致",
"重写为可编译对照程序:信号 hungry()/hungry(QString)、槽 treat()/treat(QString)"
"函数指针重载解析 connect",
"原 wiki 把类定义、emit、connect 与中文讲解散文混在一个代码块,无法编译;"
"信号名 hungury 疑为 hungry 笔误。重写保留「自定义信号+槽+重载」教学意图。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_file_dialog(pid, idx, out_dir, stem):
"""6.5 标准文件对话框:MainWindow + openAction/saveAction + openFile/saveFile 槽。
原文 12/13/14 三块合成一个可运行目标。资源 :/images/file-open :/images/file-save。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QMainWindow>\n"
"#include <QAction>\n"
"#include <QMenuBar>\n"
"#include <QToolBar>\n"
"#include <QFileDialog>\n"
"#include <QFile>\n"
"#include <QTextStream>\n"
"#include <QMessageBox>\n"
"#include <QTimer>\n\n"
"class MainWindow : public QMainWindow {\n"
" Q_OBJECT\n"
"public:\n"
" MainWindow() {\n"
" QAction* openAction = new QAction(QStringLiteral(\"打开...\"), this);\n"
" QAction* saveAction = new QAction(QStringLiteral(\"保存...\"), this);\n"
" menuBar()->addMenu(QStringLiteral(\"文件\"))->addAction(openAction);\n"
" menuBar()->addMenu(QStringLiteral(\"文件\"))->addAction(saveAction);\n"
" connect(openAction, &QAction::triggered, this, &MainWindow::openFile);\n"
" connect(saveAction, &QAction::triggered, this, &MainWindow::saveFile);\n"
" }\n"
"private slots:\n"
" void openFile() {\n"
" QString path = QFileDialog::getOpenFileName(this, QStringLiteral(\"打开文件\"));\n"
" if (path.isEmpty()) return;\n"
" QFile file(path);\n"
" if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {\n"
" QMessageBox::warning(this, QStringLiteral(\"错误\"), QStringLiteral(\"打开失败\"));\n"
" return;\n"
" }\n"
" QTextStream in(&file);\n"
" QString text = in.readAll();\n"
" file.close();\n"
" QMessageBox::information(this, QStringLiteral(\"内容\"), text);\n"
" }\n"
" void saveFile() {\n"
" QString path = QFileDialog::getSaveFileName(this, QStringLiteral(\"保存文件\"));\n"
" if (path.isEmpty()) return;\n"
" QFile file(path);\n"
" if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {\n"
" QMessageBox::warning(this, QStringLiteral(\"错误\"), QStringLiteral(\"保存失败\"));\n"
" return;\n"
" }\n"
" QTextStream out(&file);\n"
" out << QStringLiteral(\"示例保存内容\");\n"
" file.close();\n"
" }\n"
"};\n\n"
"int main(int argc, char* argv[]) {\n"
" QApplication app(argc, argv);\n"
" MainWindow w; w.show();\n" +
AUTO_QUIT +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "merge+rewrite",
"文件对话框示例分散在 12/13/14 三块(含 :/images/file-open 资源、connect、openFile/saveFile 实现)",
"合并重写为单一可运行 MainWindow 程序,菜单/工具栏触发文件对话框读写",
"原文三块是同一 MainWindow 的不同部分(构造、连接、槽实现),独立成文件无法编译;"
"合并为完整目标。原文引用资源图标,本目标用纯文字 Action 避免资源依赖。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_label_pixmap(pid, idx, out_dir, stem):
"""显示图片:QPixmap load :/Image/boat.jpg + QLabel->setPixmap。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QLabel>\n"
"#include <QPixmap>\n"
"#include <QTimer>\n\n"
"class DemoWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" DemoWidget() {\n"
" QPixmap pixmap;\n"
" pixmap.load(\":/Image/boat.jpg\"); // 从资源加载图片\n"
" QLabel* label = new QLabel(this);\n"
" label->setPixmap(pixmap); // 注意:label 是指针,用 ->\n"
" label->setGeometry(10, 10, 200, 120);\n"
" }\n"
"};\n\n" + main_with_widget("DemoWidget")
)
record_erratum(pid, idx, "merge+typo",
"显示图片示例分散在 3 块(QPixmap pixmap; pixmap.load(:/Image/boat.jpg); "
"label.setPixmap(pixmap) 用了 . 而非 ->",
"合并为一个目标,修正 .→->,资源 :/Image/boat.jpg 由生成占位 png + .qrc 提供",
"原文 label 是 QLabel* 却用 . 调用 setPixmapC++ 指针须用 ->);三块拆开不可编译。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_label_movie(pid, idx, out_dir, stem):
"""显示动画:QMovie(:/Mario.gif) + start + QLabel->setMovie。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QLabel>\n"
"#include <QMovie>\n"
"#include <QTimer>\n\n"
"class DemoWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" DemoWidget() {\n"
" QMovie* movie = new QMovie(\":/Mario.gif\"); // 从资源加载动画\n"
" movie->start();\n"
" QLabel* label = new QLabel(this);\n"
" label->setMovie(movie);\n"
" label->setGeometry(10, 10, 200, 200);\n"
" }\n"
"};\n\n" + main_with_widget("DemoWidget")
)
record_erratum(pid, idx, "merge+typo",
"显示动画示例分散在 3 块(QMovie(:/Mario.gif)、movie->start()、QLabel 全角分号 ;",
"合并为一个目标,全角分号→半角,资源 :/Mario.gif 由生成占位 gif + .qrc 提供",
"原文 QLabel 定义末尾用了全角分号 ;(C++ 非法);三块拆开不可编译。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_custom_widget(pid, idx, out_dir, stem):
"""8.4 自定义控件 SmallWidgetspin+slider+layout+两 connectQ_OBJECT)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QSpinBox>\n"
"#include <QSlider>\n"
"#include <QHBoxLayout>\n"
"#include <QTimer>\n\n"
"// 自定义控件:spinbox 与 slider 联动\n"
"class SmallWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" SmallWidget(QWidget* parent = nullptr) : QWidget(parent) {\n"
" QSpinBox* spin = new QSpinBox(this);\n"
" QSlider* slider = new QSlider(Qt::Horizontal, this);\n"
" QHBoxLayout* layout = new QHBoxLayout(this);\n"
" layout->addWidget(spin);\n"
" layout->addWidget(slider);\n"
" // spin 变化 → slider 跟随(Qt5 静态转换选重载)\n"
" connect(spin,\n"
" static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),\n"
" slider, &QSlider::setValue);\n"
" // slider 变化 → spin 跟随\n"
" connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);\n"
" }\n"
"};\n\n"
"int main(int argc, char* argv[]) {\n"
" QApplication app(argc, argv);\n"
" SmallWidget w; w.show();\n" +
AUTO_QUIT +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "h+cpp-merge",
"自定义控件 SmallWidget 在 wiki 中以 smallwidget.h 内联展示(Q_OBJECT + spin/slider/layout/connect",
"输出为单 .cpp(含类定义+main 实例化),保留 Qt5 static_cast 重载解析写法",
"原 wiki 把类定义放 .h 注释块;按「每目标单文件」输出,AUTOMOC 处理 Q_OBJECT。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_event_override_customwidget(pid, idx, out_dir, stem):
"""9.2 event() 重写:CustomWidget::event 处理 KeyPress/Tab。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QEvent>\n"
"#include <QKeyEvent>\n"
"#include <QDebug>\n"
"#include <QTimer>\n\n"
"class CustomWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" explicit CustomWidget(QWidget* p = nullptr) : QWidget(p) {}\n"
"protected:\n"
" bool event(QEvent* e) override {\n"
" if (e->type() == QEvent::KeyPress) {\n"
" QKeyEvent* ke = static_cast<QKeyEvent*>(e);\n"
" if (ke->key() == Qt::Key_Tab) {\n"
" qDebug() << \"Tab pressed in CustomWidget\";\n"
" return true; // 事件已处理,不再向上传递\n"
" }\n"
" }\n"
" return QWidget::event(e); // 其余交给基类\n"
" }\n"
"};\n\n" + main_with_widget("CustomWidget")
)
record_erratum(pid, idx, "member-def-harness",
"CustomWidget::event(QEvent*) override 片段(处理 KeyPress/Tab)缺少所属类骨架与 main",
"补 CustomWidget : public QWidget 骨架(含 event override+ main 外壳,行为对照原文",
"原 wiki 只给成员函数定义,缺少类声明与 main;按「示例相对隔离」补齐使其可编译运行。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_event_filter_mainwindow(pid, idx, out_dir, stem):
"""9.3 事件过滤器:MainWindow + eventFilter override + installEventFilter。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QMainWindow>\n"
"#include <QLabel>\n"
"#include <QEvent>\n"
"#include <QDebug>\n"
"#include <QTimer>\n\n"
"// 事件过滤器:拦截 label 上的事件\n"
"class MainWindow : public QMainWindow {\n"
" Q_OBJECT\n"
"public:\n"
" MainWindow() {\n"
" label = new QLabel(QStringLiteral(\"事件过滤器示例\"), this);\n"
" label->setGeometry(10, 30, 200, 30);\n"
" label->installEventFilter(this); // 给 label 安装事件过滤器\n"
" }\n"
"protected:\n"
" bool eventFilter(QObject* watched, QEvent* event) override {\n"
" if (watched == label && event->type() == QEvent::MouseButtonPress) {\n"
" qDebug() << \"label 上的鼠标按下事件被过滤器拦截\";\n"
" return true; // 拦截,不再派发\n"
" }\n"
" return QMainWindow::eventFilter(watched, event);\n"
" }\n"
"private:\n"
" QLabel* label;\n"
"};\n\n" + main_with_widget("MainWindow", is_mainwin=True)
)
record_erratum(pid, idx, "class+main-harness",
"事件过滤器 MainWindoweventFilter override + installEventFilter)片段缺 main 与完整结构",
"补完整 MainWindow(含 QLabel、eventFilter、installEventFilter+ main 外壳",
"原 wiki 给出 MainWindow 类骨架但缺 main;补齐使其可编译运行,演示事件过滤机制。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_qpainter_basic(pid, idx, out_dir, stem):
"""10.1 QPainterPaintedWidget (Q_OBJECT) + paintEventdrawLine/Rect/Ellipse)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPainter>\n"
"#include <QPaintEvent>\n"
"#include <QTimer>\n\n"
"class PaintedWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" PaintedWidget(QWidget* parent = nullptr) : QWidget(parent) {\n"
" setFixedSize(300, 200);\n"
" }\n"
"protected:\n"
" void paintEvent(QPaintEvent*) override {\n"
" QPainter painter(this);\n"
" painter.drawLine(QLine(10, 10, 200, 10)); // 画线\n"
" painter.drawRect(QRect(10, 30, 100, 50)); // 画矩形\n"
" painter.drawEllipse(QRect(150, 30, 100, 80)); // 画椭圆\n"
" }\n"
"};\n\n" + main_with_widget("PaintedWidget")
)
record_erratum(pid, idx, "h+cpp-merge",
"PaintedWidget 类声明(10.1 block0)与其构造+paintEvent 实现(block1)分两块",
"合并为单 .cppQ_OBJECT 类 + paintEvent),补 main 外壳演示 drawLine/Rect/Ellipse",
"原文类声明与成员实现分属两块;按「每目标单文件」合并,AUTOMOC 处理 Q_OBJECT。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_paint_device_pixmaps(pid, idx, out_dir, stem):
"""10.2.1 绘 pixmap/bitmap(资源 butterfly.png / butterfly1.png)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPainter>\n"
"#include <QPaintEvent>\n"
"#include <QPixmap>\n"
"#include <QBitmap>\n"
"#include <QTimer>\n\n"
"class PaintWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(360, 200); }\n"
"protected:\n"
" void paintEvent(QPaintEvent*) override {\n"
" QPainter painter(this);\n"
" QPixmap pix(\":/Image/butterfly.png\"); // 从资源加载图片\n"
" painter.drawPixmap(10, 10, pix);\n"
" QPixmap pix1(\":/Image/butterfly1.png\");\n"
" painter.drawPixmap(180, 10, pix1);\n"
" // QBitmap 是 QPixmap 子类,仅 1 位深度(黑白掩码)\n"
" QBitmap bmp = pix.mask();\n"
" painter.drawPixmap(10, 110, bmp);\n"
" }\n"
"};\n\n" + main_with_widget("PaintWidget")
)
record_erratum(pid, idx, "member-def-harness",
"PaintWidget::paintEvent 绘 pixmap/bitmap(引用 :/Image/butterfly.png、butterfly1.png",
"补 PaintWidget 类骨架 + main,资源由生成占位 png + .qrc 提供",
"原 wiki 仅给成员函数定义,缺类骨架与 main;资源图片用占位 png 提供以可编译运行。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_paint_device_qimage(pid, idx, out_dir, stem):
"""10.2.1 绘 QImagesetPixel 循环)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPainter>\n"
"#include <QPaintEvent>\n"
"#include <QImage>\n"
"#include <QTimer>\n\n"
"class PaintWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(200, 200); }\n"
"protected:\n"
" void paintEvent(QPaintEvent*) override {\n"
" QPainter painter(this);\n"
" QImage image(100, 100, QImage::Format_RGB32);\n"
" // QImage 可直接逐像素操作(QPixmap 不行)\n"
" for (int y = 0; y < image.height(); ++y) {\n"
" for (int x = 0; x < image.width(); ++x) {\n"
" image.setPixel(x, y, qRgb((x * 2) % 256, (y * 2) % 256, 128));\n"
" }\n"
" }\n"
" painter.drawImage(10, 10, image);\n"
" }\n"
"};\n\n" + main_with_widget("PaintWidget")
)
record_erratum(pid, idx, "member-def-harness",
"PaintWidget::paintEvent 绘 QImagesetPixel 循环逐像素设色)片段",
"补 PaintWidget 类骨架 + main 外壳",
"原 wiki 仅给成员函数定义,缺类骨架与 main;补齐使其可编译运行。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_qpicture_io(pid, idx, out_dir, stem):
"""10.2.2 QPicturesave/load。原路径 D:\\ndrawing.pic(含误入换行)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QWidget>\n"
"#include <QPainter>\n"
"#include <QPaintEvent>\n"
"#include <QPicture>\n"
"#include <QDebug>\n"
"#include <QTimer>\n\n"
"class PaintWidget : public QWidget {\n"
" Q_OBJECT\n"
"public:\n"
" PaintWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedSize(200, 200); }\n"
"protected:\n"
" void paintEvent(QPaintEvent*) override {\n"
" // QPicture 记录并回放绘图指令\n"
" QPicture picture;\n"
" QPainter painter;\n"
" painter.begin(&picture); // 把绘图记录到 picture\n"
" painter.drawLine(10, 10, 100, 100);\n"
" painter.end();\n"
" picture.save(\"drawing.pic\"); // 保存(用相对路径,跨平台)\n\n"
" QPicture picture2;\n"
" picture2.load(\"drawing.pic\"); // 回放\n"
" QPainter p2(this);\n"
" p2.drawPicture(10, 10, picture2);\n"
" qDebug() << \"QPicture saved & replayed\";\n"
" }\n"
"};\n\n" + main_with_widget("PaintWidget")
)
record_erratum(pid, idx, "platform-path",
"QPicture 保存路径为 \"D:\\ndrawing.pic\"Windows 路径,且 \\n 被解析为换行符)",
"改为相对路径 \"drawing.pic\"(跨平台可移植)",
"原路径是 Windows 绝对路径,\\n 在字符串中被当作换行符导致路径错误;改为相对路径使"
"示例在 Linux 下可编译运行。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_qfile_basic(pid, idx, out_dir, stem):
"""11.1 基本文件操作:QFile 读 in.txt + QFileInfo 字段。
原文用 QApplication+app.exec(),实为控制台文件操作;改用 QCoreApplication 并
文件 IO 完成后立即退出(无需 GUI 事件循环,便于自动化运行验证)。"""
code = (
header_comment(pid, idx, stem) +
"#include <QCoreApplication>\n"
"#include <QFile>\n"
"#include <QFileInfo>\n"
"#include <QDebug>\n"
"#include <QIODevice>\n\n"
"int main(int argc, char* argv[]) {\n"
" QCoreApplication app(argc, argv);\n\n"
" QFile file(\"in.txt\");\n"
" if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {\n"
" qDebug() << \"Open file failed.\";\n"
" return -1;\n"
" }\n"
" while (!file.atEnd()) {\n"
" qDebug() << file.readLine();\n"
" }\n\n"
" QFileInfo info(file);\n"
" qDebug() << \"isDir:\" << info.isDir();\n"
" qDebug() << \"isExecutable:\" << info.isExecutable();\n"
" qDebug() << \"baseName:\" << info.baseName();\n"
" qDebug() << \"completeBaseName:\" << info.completeBaseName();\n"
" qDebug() << \"suffix:\" << info.suffix();\n"
" qDebug() << \"completeSuffix:\" << info.completeSuffix();\n\n"
" return 0; // 文件 IO 完成,直接退出(无需 app.exec() 事件循环)\n"
"}\n"
)
record_erratum(pid, idx, "blocking-event-loop",
"原文用 QApplication + app.exec()(阻塞 GUI 事件循环),但示例实为控制台文件操作",
"改用 QCoreApplication,文件 IO 完成后直接 return 0(不进事件循环)",
"QApplication::exec() 进入 GUI 事件循环会一直阻塞;该示例不创建窗口,仅做文件读写,"
"用 QCoreApplication 并直接返回即可正确退出,便于自动化运行验证。原行为不变。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_qdatastream_io(pid, idx, out_dir, stem):
"""11.2 二进制文件读写 file.datQDataStream write + read 合并演示。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QFile>\n"
"#include <QDataStream>\n"
"#include <QDebug>\n"
"#include <QTimer>\n\n"
"// 二进制文件读写:先写 file.dat,再读回\n"
"int main(int argc, char* argv[]) {\n"
" QCoreApplication app(argc, argv);\n\n"
" // ---- 写 ----\n"
" {\n"
" QFile file(\"file.dat\");\n"
" if (file.open(QIODevice::WriteOnly)) {\n"
" QDataStream out(&file);\n"
" out << QString(\"hello\") << qint32(42) << 3.14;\n"
" file.close();\n"
" }\n"
" }\n"
" // ---- 读 ----\n"
" {\n"
" QFile file(\"file.dat\");\n"
" if (file.open(QIODevice::ReadOnly)) {\n"
" QDataStream in(&file);\n"
" QString s; qint32 n; double d;\n"
" in >> s >> n >> d;\n"
" qDebug() << \"read:\" << s << n << d;\n"
" file.close();\n"
" } else {\n"
" qDebug() << \"open failed\";\n"
" }\n"
" }\n\n" +
AUTO_QUIT_CORE +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "merge",
"二进制读写示例分散在 3 块(写/读/读写),各自引用 file.dat 且未检查 open() 返回值",
"合并为一个程序:先写 file.dat 再读回,补 open() 检查,用 QCoreApplication(无需 GUI",
"原文三块是读写示例的不同片段,独立不可编译;合并为完整程序。补 open() 检查避免"
"未定义行为;数据文件 file.dat 程序自建。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
def rewrite_qtextstream_io(pid, idx, out_dir, stem):
"""11.3 文本文件读写 file.txtQTextStream write + read 合并演示。"""
code = (
header_comment(pid, idx, stem) +
"#include <QApplication>\n"
"#include <QFile>\n"
"#include <QTextStream>\n"
"#include <QDebug>\n"
"#include <QTimer>\n\n"
"// 文本文件读写:先写 file.txt,再读回\n"
"int main(int argc, char* argv[]) {\n"
" QCoreApplication app(argc, argv);\n\n"
" // ---- 写 ----\n"
" {\n"
" QFile data(\"file.txt\");\n"
" if (data.open(QFile::WriteOnly | QFile::Truncate)) {\n"
" QTextStream out(&data);\n"
" out << \"line1: hello\" << endl;\n"
" out << \"line2: world\" << endl;\n"
" data.close();\n"
" }\n"
" }\n"
" // ---- 读 ----\n"
" {\n"
" QFile data(\"file.txt\");\n"
" if (data.open(QFile::ReadOnly)) {\n"
" QTextStream in(&data);\n"
" QString line;\n"
" while (in.readLineInto(&line)) {\n"
" qDebug() << \"read:\" << line;\n"
" }\n"
" data.close();\n"
" }\n"
" }\n\n" +
AUTO_QUIT_CORE +
" return app.exec();\n"
"}\n"
)
record_erratum(pid, idx, "merge",
"文本读写示例分散在 2 块(写/读),各自引用 file.txt",
"合并为一个程序:先写 file.txt 再逐行读回,用 QCoreApplication(无需 GUI",
"原文两块是读写示例的不同片段,独立不可编译;合并为完整程序。")
src = os.path.join(out_dir, f"{stem}.cpp")
write(os.path.join(REPO, src), code)
return src
REWRITES = {
"rewrite_minimal_qt_app": rewrite_minimal_qt_app,
"rewrite_button_creation": rewrite_button_creation,
"rewrite_dock_widget": rewrite_dock_widget,
"rewrite_builtin_signal_slot": rewrite_builtin_signal_slot,
"rewrite_mouse_event": rewrite_mouse_event,
"rewrite_custom_signal_slot": rewrite_custom_signal_slot,
"rewrite_file_dialog": rewrite_file_dialog,
"rewrite_label_pixmap": rewrite_label_pixmap,
"rewrite_label_movie": rewrite_label_movie,
"rewrite_custom_widget": rewrite_custom_widget,
"rewrite_event_override_customwidget": rewrite_event_override_customwidget,
"rewrite_event_filter_mainwindow": rewrite_event_filter_mainwindow,
"rewrite_qpainter_basic": rewrite_qpainter_basic,
"rewrite_paint_device_pixmaps": rewrite_paint_device_pixmaps,
"rewrite_paint_device_qimage": rewrite_paint_device_qimage,
"rewrite_qpicture_io": rewrite_qpicture_io,
"rewrite_qfile_basic": rewrite_qfile_basic,
"rewrite_qdatastream_io": rewrite_qdatastream_io,
"rewrite_qtextstream_io": rewrite_qtextstream_io,
}
# ==============================================================================
# 资源:生成纯色占位 png/gif + .qrc
# ==============================================================================
def make_placeholder_png(path, w=64, h=64, rgb=(120, 160, 200)):
"""Write a minimal valid PNG (single solid color) without PIL — pure zlib."""
import zlib, struct
def chunk(typ, data):
return (struct.pack(">I", len(data)) + typ + data +
struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF))
raw = b""
r, g, b = rgb
for _ in range(h):
raw += b"\x00" + bytes([r, g, b]) * w # filter byte 0 + one RGBA-less RGB row
sig = b"\x89PNG\r\n\x1a\n"
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) # 8-bit, color type 2 (RGB)
idat = zlib.compress(raw)
with open(path, "wb") as f:
f.write(sig + chunk(b"IHDR", ihdr) + chunk(b"IDAT", idat) + chunk(b"IEND", b""))
def make_placeholder_gif(path, w=4, h=4):
"""Write a tiny valid GIF87a (single color). Qt QMovie accepts it as a 1-frame anim."""
r, g, b = 180, 120, 60
# GIF header
header = b"GIF87a"
logical = bytes([w & 255, (w >> 8) & 255, h & 255, (h >> 8) & 255,
0b10000000, 0, 0]) # global color table flag | size=1 (2 colors)
# global color table: 2 entries (BG + one color)
gct = bytes([0, 0, 0]) + bytes([r, g, b])
# image descriptor
imgdesc = b"\x2c" + bytes([0, 0, 0, 0, w & 255, (w >> 8) & 255, h & 255, (h >> 8) & 255, 0])
# image data: LZW min code size=2, then a sub-block encoding the whole image
# simplest: code size 2, one clear + pixels via codes. Use minimal stream that Qt accepts.
lzw_min = 2
# Build a trivial LZW stream: clear(4), then emit pixel index 1 codes, end(5).
# For a 4x4=16 pixel image with 2-color table, encode index 1 sixteen times.
# bit packing (LSB first), code width starts at lzw_min+1=3.
codes = [4] # clear
codes += [1] * (w * h)
codes += [5] # end
width = 3
bits = 0; nbits = 0; out = bytearray()
for c in codes:
bits |= (c << nbits); nbits += width
while nbits >= 8:
out.append(bits & 0xFF); bits >>= 8; nbits -= 8
if nbits > 0:
out.append(bits & 0xFF)
imgdata = bytes([lzw_min]) + bytes([len(out)]) + bytes(out) + b"\x00"
trailer = b"\x3b"
with open(path, "wb") as f:
f.write(header + logical + gct + imgdesc + imgdata + trailer)
def emit_resources(out_dir, target):
"""For a target that needs resources, write placeholder asset files + a .qrc.
Returns the .qrc basename (or None if no resources)."""
res = TARGET_RESOURCES.get(target)
if not res:
return None
asset_dir = os.path.join(out_dir, "assets")
os.makedirs(os.path.join(REPO, asset_dir), exist_ok=True)
# Standard Qt .qrc: root <RCC>...</RCC>, one <qresource> (prefix defaults to "/").
# The "alias" makes the file reachable at ":/<prefix>/<alias>" — here prefix is "/"
# so ":/Image/boat.jpg" needs the alias to include the sub-path, i.e. alias="Image/boat.jpg".
qrc_lines = ["<RCC>", " <qresource>"]
for res_path, asset_name in res.items():
# res_path like ":/Image/boat.jpg" → alias "Image/boat.jpg", file "assets/boat.jpg"
rel = res_path[1:] # strip leading ":"
prefix, fname = os.path.split(rel) # prefix="Image", fname="boat.jpg"
asset_path = os.path.join(asset_dir, asset_name)
if asset_name.endswith(".gif"):
make_placeholder_gif(os.path.join(REPO, asset_path))
else:
make_placeholder_png(os.path.join(REPO, asset_path))
alias = f"{prefix}/{fname}" if prefix else fname
qrc_lines.append(f' <file alias="{alias}">{os.path.join("assets", asset_name)}</file>')
qrc_lines.append(" </qresource>")
qrc_lines.append("</RCC>")
qrc_name = f"{target}.qrc"
write(os.path.join(REPO, out_dir, qrc_name), "\n".join(qrc_lines) + "\n")
return qrc_name
def emit_datafile(out_dir, target):
"""For a target needing a runtime data file, drop a sample so first-run works."""
fname = TARGET_DATAFILES.get(target)
if not fname:
return
content = {"in.txt": "Hello from in.txt\n", "file.txt": "sample\n", "file.dat": ""}
data_dir = os.path.join(out_dir, "rundata")
os.makedirs(os.path.join(REPO, data_dir), exist_ok=True)
write(os.path.join(REPO, data_dir, fname), content.get(fname, ""))
def add_moc_include_if_needed(abs_src):
"""For a .cpp that defines a Q_OBJECT class inline (not in a header), CMake's
AUTOMOC requires `#include "<stem>.moc"` at the bottom of the .cpp so the moc
output for that class gets compiled in. Without it, AUTOMOC errors out:
'contains a Q_OBJECT macro, but does not include "<stem>.moc"!'."""
with open(abs_src, "r", encoding="utf-8") as f:
content = f.read()
if "Q_OBJECT" not in content:
return
stem = os.path.splitext(os.path.basename(abs_src))[0]
moc_inc = f'#include "{stem}.moc"'
if moc_inc in content:
return
# insert just before the final closing brace region is fragile; append at EOF
# is the standard idiom (moc include is processed regardless of position).
content = content.rstrip() + "\n\n" + moc_inc + "\n"
with open(abs_src, "w", encoding="utf-8") as f:
f.write(content)
# ==============================================================================
# 主流程
# ==============================================================================
def main():
global PAGES_META_PID
qt_pages = [pid for pid, cfg in PAGE_CONFIG.items() if cfg["part"] == "p03"]
targets_by_dir = {} # dir -> list of (target_name, src_rel, needs_qrc, qrc_name)
snippet_by_dir = {} # dir -> list of (idx, sec_title, code) for README
for pid in qt_pages:
cfg = PAGE_CONFIG[pid]
PAGES_META_PID = pid
body = json.load(open(os.path.join(WIKI, "bodies", f"{pid}.json"))) \
.get("body", {}).get("storage", {}).get("value", "")
blocks = EXTRACTED.get(pid, [])
sections = page_outline(body)
out_dir = os.path.join("p03", cfg["dir"])
# block idx -> normalized section title
block_sec = {}
for sec in sections:
for i in sec["code_indices"]:
if i < len(blocks):
block_sec[i] = norm_title(sec["title"])
used_keys = set()
for (tpid, tidx), treat in BLOCK_TREATMENT.items():
if tpid != pid:
continue
kind = treat[0]
if kind == "snippet":
if isinstance(tidx, int):
code = blocks[tidx]["code"]
snippet_by_dir.setdefault(out_dir, []).append(
(tidx, block_sec.get(tidx, "(导言)"), code))
continue
stem = treat[1]
target_name = stem # ASCII; file_name_map 已保证英文蛇形
if kind == "program":
src = emit_program(pid, tidx, stem, out_dir, treat[2])
elif kind == "harness":
src = emit_harness(pid, tidx, stem, out_dir, treat[2])
elif kind == "rewrite":
src = REWRITES[treat[2]](pid, tidx, out_dir, stem)
else:
continue
qrc_name = emit_resources(out_dir, target_name)
emit_datafile(out_dir, target_name)
# Q_OBJECT classes defined inline in the .cpp need a trailing moc include.
add_moc_include_if_needed(os.path.join(REPO, src))
targets_by_dir.setdefault(out_dir, []).append(
(target_name, src, bool(qrc_name), qrc_name))
write_part3_cmakelists(targets_by_dir)
write_part3_readmes(targets_by_dir, snippet_by_dir)
write_errata()
n = sum(len(v) for v in targets_by_dir.values())
print(f"gen_part3: wrote {n} targets across {len(targets_by_dir)} dirs.")
def write_part3_cmakelists(targets_by_dir):
# per-dir CMakeLists
for d, targets in targets_by_dir.items():
lines = ["# Auto-generated by tools/gen_part3.py — do not edit by hand.\n",
f"# 章节:{d}\n",
"set(CMAKE_AUTOMOC ON)\n",
"set(CMAKE_AUTORCC ON)\n"]
for tgt, src_rel, has_qrc, qrc_name in targets:
src = os.path.basename(src_rel)
if has_qrc:
lines.append(f"qt5_add_resources(RES {qrc_name})\n")
lines.append(f"add_executable({tgt} {src} ${{RES}})\n")
else:
lines.append(f"add_executable({tgt} {src})\n")
lines.append(f"set_target_properties({tgt} PROPERTIES "
f"CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF "
f"AUTOMOC ON)\n")
# Link Qt modules so their include dirs / libs are available to every
# example. (Filesystem examples only need Core, but QApplication/QWidget
# based ones need Widgets+Gui; linking all three keeps targets uniform and
# avoids per-target include-path gaps.)
lines.append(f"target_link_libraries({tgt} PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)\n")
lines.append(f"target_compile_options({tgt} PRIVATE -Wall -Wextra)\n")
dir_tname = "all_" + d.replace("/", "_").replace("-", "_")
lines.append(f"add_custom_target({dir_tname} DEPENDS {' '.join(t[0] for t in targets)})\n")
write(os.path.join(REPO, d, "CMakeLists.txt"), "".join(lines))
# top-level p03/CMakeLists including all sub-dirs
lines = ["# Auto-generated by tools/gen_part3.py\n"]
for d in sorted(targets_by_dir.keys()):
rel = os.path.relpath(d, "p03")
lines.append(f"add_subdirectory({rel})\n")
write(os.path.join(REPO, "p03", "CMakeLists.txt"), "".join(lines))
def write_part3_readmes(targets_by_dir, snippet_by_dir):
"""Each chapter dir gets a README listing its runnable targets and the
documented snippets (signatures/pseudo-code folded in as reference)."""
all_dirs = sorted(set(list(targets_by_dir.keys()) + list(snippet_by_dir.keys())))
for d in all_dirs:
lines = [f"# {d} — Qt 示例\n\n"]
tgts = targets_by_dir.get(d, [])
if tgts:
lines.append("## 可运行示例\n\n")
lines.append("| 目标 | 源文件 | 说明 |\n")
lines.append("|---|---|---|\n")
for tgt, src_rel, has_qrc, qrc in tgts:
lines.append(f"| `{tgt}` | `{os.path.basename(src_rel)}` | "
f"构建运行见顶层 README;离屏验证 `QT_QPA_PLATFORM=offscreen tools/run_qt.sh {tgt}` |\n")
lines.append("\n")
snips = snippet_by_dir.get(d, [])
if snips:
lines.append("## 参考片段(签名/伪代码,未单独建目标)\n\n")
for (idx, title, code) in snips:
lines.append(f"### [{PAGES_META_PID if False else ''}block {idx}] {title}\n\n")
lines.append("```cpp\n")
lines.append(code.rstrip() + "\n")
lines.append("```\n\n")
write(os.path.join(REPO, d, "README.md"), "".join(lines))
def write_errata():
lines = ["# ERRATA — 代码修正记录(Part3 Qt)\n\n",
"本文件由 `tools/gen_part3.py` 自动生成,记录对 wiki 原始 Qt 代码块的每一处修正。\n",
"格式:`[pageId:blockIdx]` 类型 → 原内容 → 修正后 → 依据。\n\n"]
if not ERRATA:
lines.append("(无修正)\n")
else:
for (pid, idx, kind, before, after, reason) in ERRATA:
lines.append(f"## [{pid}:{idx}] {kind}\n\n")
lines.append(f"- **原文**`{before}`\n")
lines.append(f"- **修正**`{after}`\n")
lines.append(f"- **依据**{reason}\n\n")
write(os.path.join(REPO, "docs", "ERRATA_part3.md"), "".join(lines))
if __name__ == "__main__":
main()