In another question you tell me to use QStateMachine.
I'm new to Qt and it's the first time i use the objects so I make a lot of logical mistake, so using QStateMachine it's a big problem...
It's the only way to do thath ? I try to explain my program:
I want to create a card's game and in the previous version I've used an old graphics library with this sequence of commands:
-> 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.
In this way I tell the program:
-> print cards
-> wait for a mouse event
-> print the card that I've selected with that event.
Now I want to change the program graphics and I've introduced QGraphics.
I've created a scene and print all the objects "card" on it so now i want to tell the program:
-> 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
The problem is that I use a for
1 to 20 (I must run that 20 times in a match).
I've tried to lauch the program with a random G1 and COM play but the application freeze until the last execution of the for
and I print on the scene only the last configuration of cards.
That is the reason because previously I said I want the program to stop...
It is possible to do without QStateMachine ?
Simply telling him: "pause", print this situation, wait for mouse and go ahead ?
The below is a complete example, 71 lines long, presented in the literate programming style. It is also available on github. The example consists of a qmake .pro
file, not shown, and main.cpp
, shown in the entirety below. The example has the following structure:
- Header
- Card Item
- State Machine Behaviors
- Main
- Footer
Main
First, let's set up our scene:
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"));
The state machine has three states:
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);
We'll use helper functions to declaratively add behaviors to the machine. This isn't the only possible pattern, but it works and is fairly easy to apply. First, when any items are selected, the state changes from s_idle
to s_selected
:
on_selected(&s_idle, &scene, true, &s_selected);
Then, after a timeout, the state changes to s_ready
:
on_delay(&s_selected, 500, &s_ready);
If the items are deselected, we go back to s_idle
:
on_selected(&s_selected, &scene, false, &s_idle);
on_selected(&s_ready, &scene, false, &s_idle);
Since we don't have much better to do, we can simply deselect all items once the s_ready
state has been entered. This makes it clear that the state was entered. Of course, it'll be immediately left since the selection is cleared, and we indicated above that s_idle
is the state to be when no items are selected.
QObject::connect(&s_ready, &QState::entered, &scene, &QGraphicsScene::clearSelection);
We can now start the machine and run our application:
machine.start();
view.show();
return app.exec();
}
Note the minimal use of explicit dynamic memory allocation, and no manual memory management whatsoever.
Card Item
The CardItem
class is a simple card graphics item. The item is selectable. It could also be movable. The interaction is handled automatically by the graphics view framework: you don't have to deal with interpreting mouse presses/drags/releases manually - at least not yet.
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);
}
};
State Machine Behaviors
It is helpful to factor out the state machine behaviors into functions that can be used to declare the behaviors on a given state.
First, the delay - once the src
state is entered, and a given number of millisconds elapses, the machine transitions to the destination state:
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);
}
To intercept the selection signals, we'll need a helper class that emits a generic signal:
class SignalSource : public QObject {
Q_OBJECT
public:
Q_SIGNAL void sig();
SignalSource(QObject * parent = Q_NULLPTR) : QObject(parent) {}
};
We then leverage such universal signal source to describe the behavior of transitioning to the destination state when the given scene has a selection iff selected
is true, or has no selection iff selected
is false:
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 and Footer
The example begins with the following header:
// https://github.com/KubaO/stackoverflown/tree/master/questions/sm-cards-37656060
#include <QtWidgets>
It ends with the following footer, consisting of moc-generated implementations of the signals and object metadata for the SignalSource
class.
#include "main.moc"
In qt you don't need to actively wait for an event (and usually shouldn't). Just subclass the event handling method of a widget which is part of the main interface.
For instance this is the code which use a subclass of a QGraphicsItem
to change the game state. You could do the same with the scene itself, widgets, etc... but it should usually be like this.
void CardGameGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
if(event->button() == Qt::RightButton)
{
makeQuickChangesToGameState();
scene()->update(); //ask for a deffered ui update
}
QGraphicsItem::mousePressEvent(event);
}
even if you are somehow using a state machine, makeQuickChangesToGameState()
should just trigger the machine state change, and go back asap.