#!/usr/bin/env python3 # ============================================================================== # gen_part3.py — 生成 Part3(Qt 桌面应用开发)的全部源码、.qrc 与 CMakeLists # # 输入:tools/wiki_data/ 中 9 个 Qt 页面(pageId 58954515–58954552,共 73 块)。 # 策略: # * 每块按内容形态分类(见 BLOCK_TREATMENT): # - 'program' 原文已含 main+QApplication,作为目标近乎原样(补 include/修笔误)。 # - 'harness' 有意义片段(控件/信号槽/事件/绘图),用 main+QApplication+show # 外壳包裹,必要时补所需 QWidget/QMainWindow 子类骨架。 # - 'snippet' 纯签名/单语句/伪代码,不单独建目标,折进各章 README 作文档。 # * 每个目标 = 一个独立 add_executable;开 AUTOMOC(Q_OBJECT 类需 moc)。 # * 离屏运行友好:每个窗口类目标注入一个 QTimer::singleShot,仅当检测到 # QT_QPA_PLATFORM=offscreen 时才在 200ms 后自动退出;正常运行(有显示环境) # 窗口保持打开,不会自动关闭。 # * 资源依赖(:/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 自动退出验证。 # ------------------------------------------------------------------------------ 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 再放 body,main 实例化 # 每块的 include 需求(按片段用到的 Qt 类确定;标准模板会再加 QApplication/Widget) INC = { "app": "#include ", "widget": "#include ", "mwin": "#include ", "pushbtn": "#include ", "label": "#include ", "dialog": "#include ", "msgbox": "#include ", "filedlg": "#include ", "dock": "#include ", "textedit": "#include ", "menubar": "#include ", "action": "#include ", "qfile": "#include ", "qfileinfo": "#include ", "qtextstream": "#include ", "qdatastream": "#include ", "qdebug": "#include ", "qiodevice": "#include ", "qpainter": "#include ", "qpixmap": "#include ", "qbitmap": "#include ", "qimage": "#include ", "qpicture": "#include ", "qpaintevent": "#include ", "qmovie": "#include ", "qspinbox": "#include ", "qslider": "#include ", "qboxlayout": "#include ", "qevent": "#include ", "qmouseevent": "#include ", "qstring": "#include ", "qtimer": "#include ", "qstringlist": "#include ", "qcoreapp": "#include ", } # 构建时的逐块处理决策表(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-ctor,connect 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 handler,exec()) ("58954535", 0): ("harness", "modal_dialog", {"includes": [INC["app"], INC["widget"], INC["dialog"]], "body_kind": "widget-ctor-handler"}), # 1,2,3: 非模态对话框(widget-ctor handler,show) ("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_pixmap,3 是签名 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: 自定义控件 SmallWidget(Q_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 override(member-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: QPicture(Windows 路径 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 = ( " // 仅在离屏自动化验证时(QT_QPA_PLATFORM=offscreen)200ms 后自动退出;\n" " // 正常运行(有显示环境)窗口保持打开,不自动退出。\n" " if (qgetenv(\"QT_QPA_PLATFORM\") == \"offscreen\") {\n" " QTimer::singleShot(200, &app, &QApplication::quit);\n" " }\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 \nclass Widget : public QWidget { Q_OBJECT public: Widget(QWidget* p=nullptr):QWidget(p){} };\n" parts = [header_comment(pid, idx, stem)] parts.append("#include \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 (自动退出)\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 # 典型: ...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 」「设置区域范围」)", "剥离行内中文散文,保留代码部分", "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 \n" "#include \n" "#include \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 QWidget(Q_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 」;中文文本在字符串内, 通用 prose 剥离会误伤字符串,故整体重写。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \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 」,且含中文字符串", "重写为可编译 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 铆接部件 QDockWidget:addDockWidget + setAllowedAreas。 原文尾部混散文「设置区域范围」;含中文标题,整体重写。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \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 \n" "#include \n" "#include \n" "#include \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 \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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(\"

Move: (%1, %2)

\")\n" " .arg(event->x()).arg(event->y()));\n" " }\n" " void mousePressEvent(QMouseEvent* event) override {\n" " setText(QString(\"

Press: (%1, %2)

\")\n" " .arg(event->x()).arg(event->y()));\n" " }\n" " void mouseReleaseEvent(QMouseEvent* event) override {\n" " setText(QString(\"

Release: (%1, %2)

\")\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 + MyWidget,emit + connect。 原文散文混入严重;给出可编译的完整对照程序。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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::of 显式选择重载\n" " connect(t, QOverload::of(&Teacher::hungry),\n" " s, QOverload::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 \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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 \n" "#include \n" "#include \n" "#include \n" "#include \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* 却用 . 调用 setPixmap(C++ 指针须用 ->);三块拆开不可编译。") 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 \n" "#include \n" "#include \n" "#include \n" "#include \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 自定义控件 SmallWidget:spin+slider+layout+两 connect(Q_OBJECT)。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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(&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 \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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(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 \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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", "事件过滤器 MainWindow(eventFilter 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 QPainter:PaintedWidget (Q_OBJECT) + paintEvent(drawLine/Rect/Ellipse)。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \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)分两块", "合并为单 .cpp(Q_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 \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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 绘 QImage(setPixel 循环)。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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 绘 QImage(setPixel 循环逐像素设色)片段", "补 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 QPicture:save/load。原路径 D:\\ndrawing.pic(含误入换行)。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \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 \n" "#include \n" "#include \n" "#include \n" "#include \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.dat:QDataStream write + read 合并演示。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \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.txt:QTextStream write + read 合并演示。""" code = ( header_comment(pid, idx, stem) + "#include \n" "#include \n" "#include \n" "#include \n" "#include \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 ..., one (prefix defaults to "/"). # The "alias" makes the file reachable at "://" — here prefix is "/" # so ":/Image/boat.jpg" needs the alias to include the sub-path, i.e. alias="Image/boat.jpg". qrc_lines = ["", " "] 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' {os.path.join("assets", asset_name)}') qrc_lines.append(" ") qrc_lines.append("") 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 ".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 ".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()