C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7
模块 A · 讲授 35min
paintEvent
里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式
绘图(想画就立刻画到屏幕)是不同的模型。update()(异步,排队到下次事件循环处理)而不是
直接调用 paintEvent();QPainter 只是"在哪个设备上画"的画笔,
画布可以是控件本身,也可以是 QImage/QPixmap 等离屏设备。paintEvent 之外的地方直接 new QPainter(this) 画东西:
要么无效要么未定义行为,绘制必须在 paintEvent 回调期间进行。update():界面不会自动感知"要重画了",看起来像是
"画错了",其实是"根本没重画"。
void paintEvent(QPaintEvent*) override {
QPainter painter(this);
painter.drawLine(QLine(10, 10, 200, 10));
painter.drawRect(QRect(10, 30, 100, 50));
painter.drawEllipse(QRect(150, 30, 100, 80));
}
所有绘制都发生在 paintEvent 里,由 Qt 在需要重绘时自动调用。
出处:p03/ch10/qpainter_basic.cpp;绘图设备
(QImage/QPixmap/QPicture)见
paint_device_qimage.cpp、
paint_device_pixmaps.cpp
moveToThread 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须
始终归属主线程,这是 Qt 线程模型的核心约束。moveToThread 到工作线程),而不是继承 QThread 重写
run()——前者能继续用信号槽通信,后者容易把线程亲和性搞混。label->setText()):Qt 的 GUI
模块不是线程安全的,这是未定义行为/崩溃的常见来源——正确做法永远是发信号,排队回主线程处理。moveToThread 要在 connect 和 thread.start()
之前完成;对象一旦有耗时任务在跑,中途再挪线程会有竞态。
class Worker : public QObject {
Q_OBJECT
public slots:
void process() {
qint64 sum = 0;
for (int i = 1; i <= 1000000; ++i) sum += i;
emit resultReady(sum); // 排队连接自动把结果带回主线程
}
signals:
void resultReady(qint64 result);
};
worker.moveToThread(&workerThread); // 官方推荐写法:worker-object + moveToThread
connect(&workerThread, &QThread::started, &worker, &Worker::process);
出处:docs/teaching/examples/qthread_worker/qt_thread_worker.cpp
(配套 README)——
仓库 p03/ 的 wiki 抓取内容没有多线程,这是本轮补写的教学补充示例,已验证跑通。
模块 A · 练习 95min
qpainter_basic.cpp,在 paintEvent 里画一个组合图形
(如棋盘格/仪表盘刻度)qt_thread_worker.cpp 里的耗时计算换成一个更长的循环或
QThread::msleep,观察主线程/事件循环没有被卡住moveToThread 起的作用模块 B · 讲授 35min
Q_PROPERTY,同样靠
Day2 讲过的 moc 生成)给每个属性配了
统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的
setter",所以任何声明成 Q_PROPERTY 的属性(不只是内置的 geometry/opacity)
都能被动画驱动。QEasingCurve 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……),
动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。Q_PROPERTY(有 setter),随手传一个字符串
属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。QAbstractAnimation::DeleteWhenStopped 能让动画结束后自动释放自身,但不管
目标对象的生命周期,目标对象仍需自己保证不提前销毁。
auto* anim = new QPropertyAnimation(button, "geometry", &window);
anim->setDuration(800);
anim->setStartValue(QRect(10, 10, 100, 30));
anim->setEndValue(QRect(180, 140, 120, 40));
anim->setEasingCurve(QEasingCurve::OutBounce);
anim->start(QAbstractAnimation::DeleteWhenStopped);
出处:docs/teaching/examples/property_animation/qt_property_animation.cpp (配套 README)
bool 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象
(QState),每个状态自己声明"进入这个状态要怎样"(assignProperty,
又用到了 Qt 属性系统)。addTransition)本质是"信号触发的转移"——转移条件就是
一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。machine.setInitialState(...) 或忘记调用 machine.start():
状态机对象存在但从未真正运行,所有 assignProperty/addTransition
都不会生效。
auto* redState = new QState(&machine);
auto* greenState = new QState(&machine);
redState->assignProperty(&button, "text", QStringLiteral("红灯:禁止通行"));
greenState->assignProperty(&button, "text", QStringLiteral("绿灯:允许通行"));
redState->addTransition(&button, &QPushButton::clicked, greenState);
greenState->addTransition(&button, &QPushButton::clicked, redState);
machine.setInitialState(redState);
machine.start();
出处:docs/teaching/examples/state_machine/qt_state_machine.cpp
(配套 README)——
QStateMachine/QState 属于 QtCore,不需要额外模块。以上两个
(动画、状态机)都是仓库 p03/ 原本没有的教学补充示例,已离屏验证跑通。
模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案)
mouse_event.cpp)+ 多线程计时
(Day4 qt_thread_worker.cpp),做一个"状态变化后限时点击"的最小交互游戏QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口
分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。
明天 Day5:团队协同开发 / 综合案例复盘 / OpenCV(可选自学) —— 大纲:docs/teaching/OUTLINE.md