1a788a8278
学生主要用 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 复用同一临时文件,不会在磁盘上堆多份安装包大小的临时拷贝。
1464 lines
70 KiB
Python
1464 lines
70 KiB
Python
#!/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 让程序 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 再放 body,main 实例化
|
||
|
||
# 每块的 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-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 = (
|
||
" // 离屏/自动化验证: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 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 <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 铆接部件 QDockWidget:addDockWidget + 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 + MyWidget,emit + 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* 却用 . 调用 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 <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 自定义控件 SmallWidget:spin+slider+layout+两 connect(Q_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",
|
||
"事件过滤器 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 <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)分两块",
|
||
"合并为单 .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 <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 绘 QImage(setPixel 循环)。"""
|
||
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 绘 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 <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.dat:QDataStream 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.txt:QTextStream 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()
|