QStateMachine - QMouseEvent
QStateMachine - QMouseEvent
在另一个问题中你告诉我使用 QStateMachine。
我是 Qt 的新手,这是我第一次使用这些对象,所以我犯了很多逻辑错误,所以使用 QStateMachine 是个大问题...
这是唯一的方法吗?我试着解释一下我的程序:
我想创建一个纸牌游戏,在以前的版本中,我使用了一个带有以下命令序列的旧图形库:
-> print cards on the scene
-> wait for a mouse input (with a do-while)
-> if(isMouseClick(WM_LBUTTONDOWN))
-> if(mouse position is on the first card)
-> select that card. So i wish to do the same thing with QGraphics.
这样我告诉程序:
-> print cards
-> wait for a mouse event
-> print the card that I've selected with that event.
现在我想改变程序图形,我引入了QGraphics。
我创建了一个场景并在上面打印了所有对象 "card" 所以现在我想告诉程序:
-> print the object and wait the mouse input
-> if a card is to selected with the left clik
-> print that card in scene, wait 1/2 second and go ahead with the program
问题是我使用 for
1 到 20(我必须 运行 在一场比赛中使用 20 次)。
我尝试使用随机 G1 和 COM 播放启动程序,但应用程序冻结,直到 for
最后一次执行,我只在现场打印最后一次配置的卡片。
这是因为之前我说过我要程序停止...
可以不用 QStateMachine 吗?
简单的告诉他:"pause",打印这个情况,等鼠标继续?
在 qt 中,您不需要主动等待事件(通常不应该)。只需将作为主界面一部分的小部件的事件处理方法子类化即可。
例如,这是使用 QGraphicsItem
的子类来改变游戏状态的代码。你可以对场景本身、小部件等做同样的事情……但它通常应该是这样的。
void CardGameGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::RightButton)
{
makeQuickChangesToGameState();
scene()->update(); //ask for a deffered ui update
}
QGraphicsItem::mousePressEvent(event);
}
即使您以某种方式使用状态机,makeQuickChangesToGameState()
也应该触发机器状态更改,然后尽快返回。
下面是一个完整的例子,长 71 行,呈现在 literate programming style. It is also available on github 中。该示例由一个 qmake .pro
文件(未显示)和 main.cpp
组成,完整显示如下。该示例具有以下结构:
- Header
- 卡片项目
- 状态机行为
- 主要
- 页脚
主要
首先,让我们设置场景:
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QGraphicsScene scene;
QGraphicsView view{&scene};
scene.addItem(new CardItem(0, 0, "A"));
scene.addItem(new CardItem(20, 0, "B"));
状态机有三种状态:
QStateMachine machine;
QState s_idle{&machine}; // idle - no card selected
QState s_selected{&machine}; // card selected, waiting 1/2 second
QState s_ready{&machine}; // ready with card selected
machine.setInitialState(&s_idle);
我们将使用辅助函数以声明方式向机器添加行为。这不是唯一可能的模式,但它有效并且很容易应用。首先,当任何项目被选中时,状态从 s_idle
变为 s_selected
:
on_selected(&s_idle, &scene, true, &s_selected);
然后,超时后,状态变为 s_ready
:
on_delay(&s_selected, 500, &s_ready);
如果项目被取消选择,我们返回到 s_idle
:
on_selected(&s_selected, &scene, false, &s_idle);
on_selected(&s_ready, &scene, false, &s_idle);
因为我们没有更好的事情要做,所以我们可以在进入 s_ready
状态后简单地取消选择所有项目。这清楚地表明已进入该状态。当然,由于选择被清除,它会立即离开,我们在上面指出 s_idle
是没有选择项目时的状态。
QObject::connect(&s_ready, &QState::entered, &scene, &QGraphicsScene::clearSelection);
我们现在可以启动机器和运行我们的应用程序:
machine.start();
view.show();
return app.exec();
}
注意最少使用显式动态内存分配,并且没有任何手动内存管理。
卡片项目
CardItem
class 是一个简单的卡片图形项目。该项目是可选择的。它也可以是可移动的。交互由图形视图框架自动处理:您不必手动处理解释鼠标 presses/drags/releases - 至少现在不需要。
class CardItem : public QGraphicsObject {
Q_OBJECT
const QRect cardRect { 0, 0, 80, 120 };
QString m_text;
QRectF boundingRect() const Q_DECL_OVERRIDE { return cardRect; }
void paint(QPainter * p, const QStyleOptionGraphicsItem*, QWidget*) {
p->setRenderHint(QPainter::Antialiasing);
p->setPen(Qt::black);
p->setBrush(isSelected() ? Qt::gray : Qt::white);
p->drawRoundRect(cardRect.adjusted(0, 0, -1, -1), 10, 10);
p->setFont(QFont("Helvetica", 20));
p->drawText(cardRect.adjusted(3,3,-3,-3), m_text);
}
public:
CardItem(qreal x, qreal y, const QString & text) : m_text(text) {
moveBy(x, y);
setFlags(QGraphicsItem::ItemIsSelectable);
}
};
状态机行为
将状态机行为分解为可用于声明给定状态下的行为的函数很有帮助。
首先,延迟 - 一旦进入 src
状态,并且经过了给定的毫秒数,机器就会转换到目标状态:
void on_delay(QState * src, int ms, QAbstractState * dst) {
auto timer = new QTimer(src);
timer->setSingleShot(true);
timer->setInterval(ms);
QObject::connect(src, &QState::entered, timer, static_cast<void (QTimer::*)()>(&QTimer::start));
QObject::connect(src, &QState::exited, timer, &QTimer::stop);
src->addTransition(timer, SIGNAL(timeout()), dst);
}
要拦截选择信号,我们需要一个助手 class 来发出通用信号:
class SignalSource : public QObject {
Q_OBJECT
public:
Q_SIGNAL void sig();
SignalSource(QObject * parent = Q_NULLPTR) : QObject(parent) {}
};
然后,我们利用这种通用信号源来描述当给定场景有选择当且仅当 selected
为真或没有选择当且仅当 selected
为假时转换到目标状态的行为:
void on_selected(QState * src, QGraphicsScene * scene, bool selected, QAbstractState * dst) {
auto signalSource = new SignalSource(src);
QObject::connect(scene, &QGraphicsScene::selectionChanged, signalSource, [=] {
if (scene->selectedItems().isEmpty() == !selected) emit signalSource->sig();
});
src->addTransition(signalSource, SIGNAL(sig()), dst);
}
Header 和页脚
示例以以下内容开头 header:
// https://github.com/KubaO/Whosebugn/tree/master/questions/sm-cards-37656060
#include <QtWidgets>
它以以下页脚结束,包括 moc-generated 信号的实现和 SignalSource
class.
的 object 元数据
#include "main.moc"
在另一个问题中你告诉我使用 QStateMachine。
我是 Qt 的新手,这是我第一次使用这些对象,所以我犯了很多逻辑错误,所以使用 QStateMachine 是个大问题...
这是唯一的方法吗?我试着解释一下我的程序:
我想创建一个纸牌游戏,在以前的版本中,我使用了一个带有以下命令序列的旧图形库:
-> print cards on the scene
-> wait for a mouse input (with a do-while)
-> if(isMouseClick(WM_LBUTTONDOWN))
-> if(mouse position is on the first card)
-> select that card. So i wish to do the same thing with QGraphics.
这样我告诉程序:
-> print cards
-> wait for a mouse event
-> print the card that I've selected with that event.
现在我想改变程序图形,我引入了QGraphics。 我创建了一个场景并在上面打印了所有对象 "card" 所以现在我想告诉程序:
-> print the object and wait the mouse input
-> if a card is to selected with the left clik
-> print that card in scene, wait 1/2 second and go ahead with the program
问题是我使用 for
1 到 20(我必须 运行 在一场比赛中使用 20 次)。
我尝试使用随机 G1 和 COM 播放启动程序,但应用程序冻结,直到 for
最后一次执行,我只在现场打印最后一次配置的卡片。
这是因为之前我说过我要程序停止...
可以不用 QStateMachine 吗? 简单的告诉他:"pause",打印这个情况,等鼠标继续?
在 qt 中,您不需要主动等待事件(通常不应该)。只需将作为主界面一部分的小部件的事件处理方法子类化即可。
例如,这是使用 QGraphicsItem
的子类来改变游戏状态的代码。你可以对场景本身、小部件等做同样的事情……但它通常应该是这样的。
void CardGameGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::RightButton)
{
makeQuickChangesToGameState();
scene()->update(); //ask for a deffered ui update
}
QGraphicsItem::mousePressEvent(event);
}
即使您以某种方式使用状态机,makeQuickChangesToGameState()
也应该触发机器状态更改,然后尽快返回。
下面是一个完整的例子,长 71 行,呈现在 literate programming style. It is also available on github 中。该示例由一个 qmake .pro
文件(未显示)和 main.cpp
组成,完整显示如下。该示例具有以下结构:
- Header
- 卡片项目
- 状态机行为
- 主要
- 页脚
主要
首先,让我们设置场景:
int main(int argc, char ** argv) {
QApplication app{argc, argv};
QGraphicsScene scene;
QGraphicsView view{&scene};
scene.addItem(new CardItem(0, 0, "A"));
scene.addItem(new CardItem(20, 0, "B"));
状态机有三种状态:
QStateMachine machine;
QState s_idle{&machine}; // idle - no card selected
QState s_selected{&machine}; // card selected, waiting 1/2 second
QState s_ready{&machine}; // ready with card selected
machine.setInitialState(&s_idle);
我们将使用辅助函数以声明方式向机器添加行为。这不是唯一可能的模式,但它有效并且很容易应用。首先,当任何项目被选中时,状态从 s_idle
变为 s_selected
:
on_selected(&s_idle, &scene, true, &s_selected);
然后,超时后,状态变为 s_ready
:
on_delay(&s_selected, 500, &s_ready);
如果项目被取消选择,我们返回到 s_idle
:
on_selected(&s_selected, &scene, false, &s_idle);
on_selected(&s_ready, &scene, false, &s_idle);
因为我们没有更好的事情要做,所以我们可以在进入 s_ready
状态后简单地取消选择所有项目。这清楚地表明已进入该状态。当然,由于选择被清除,它会立即离开,我们在上面指出 s_idle
是没有选择项目时的状态。
QObject::connect(&s_ready, &QState::entered, &scene, &QGraphicsScene::clearSelection);
我们现在可以启动机器和运行我们的应用程序:
machine.start();
view.show();
return app.exec();
}
注意最少使用显式动态内存分配,并且没有任何手动内存管理。
卡片项目
CardItem
class 是一个简单的卡片图形项目。该项目是可选择的。它也可以是可移动的。交互由图形视图框架自动处理:您不必手动处理解释鼠标 presses/drags/releases - 至少现在不需要。
class CardItem : public QGraphicsObject {
Q_OBJECT
const QRect cardRect { 0, 0, 80, 120 };
QString m_text;
QRectF boundingRect() const Q_DECL_OVERRIDE { return cardRect; }
void paint(QPainter * p, const QStyleOptionGraphicsItem*, QWidget*) {
p->setRenderHint(QPainter::Antialiasing);
p->setPen(Qt::black);
p->setBrush(isSelected() ? Qt::gray : Qt::white);
p->drawRoundRect(cardRect.adjusted(0, 0, -1, -1), 10, 10);
p->setFont(QFont("Helvetica", 20));
p->drawText(cardRect.adjusted(3,3,-3,-3), m_text);
}
public:
CardItem(qreal x, qreal y, const QString & text) : m_text(text) {
moveBy(x, y);
setFlags(QGraphicsItem::ItemIsSelectable);
}
};
状态机行为
将状态机行为分解为可用于声明给定状态下的行为的函数很有帮助。
首先,延迟 - 一旦进入 src
状态,并且经过了给定的毫秒数,机器就会转换到目标状态:
void on_delay(QState * src, int ms, QAbstractState * dst) {
auto timer = new QTimer(src);
timer->setSingleShot(true);
timer->setInterval(ms);
QObject::connect(src, &QState::entered, timer, static_cast<void (QTimer::*)()>(&QTimer::start));
QObject::connect(src, &QState::exited, timer, &QTimer::stop);
src->addTransition(timer, SIGNAL(timeout()), dst);
}
要拦截选择信号,我们需要一个助手 class 来发出通用信号:
class SignalSource : public QObject {
Q_OBJECT
public:
Q_SIGNAL void sig();
SignalSource(QObject * parent = Q_NULLPTR) : QObject(parent) {}
};
然后,我们利用这种通用信号源来描述当给定场景有选择当且仅当 selected
为真或没有选择当且仅当 selected
为假时转换到目标状态的行为:
void on_selected(QState * src, QGraphicsScene * scene, bool selected, QAbstractState * dst) {
auto signalSource = new SignalSource(src);
QObject::connect(scene, &QGraphicsScene::selectionChanged, signalSource, [=] {
if (scene->selectedItems().isEmpty() == !selected) emit signalSource->sig();
});
src->addTransition(signalSource, SIGNAL(sig()), dst);
}
Header 和页脚
示例以以下内容开头 header:
// https://github.com/KubaO/Whosebugn/tree/master/questions/sm-cards-37656060
#include <QtWidgets>
它以以下页脚结束,包括 moc-generated 信号的实现和 SignalSource
class.
#include "main.moc"