Search code examples
c++qtqeventqmouseeventqstatemachine

QStateMachine - QMouseEvent


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 ?


Solution

  • 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:

    1. Header
    2. Card Item
    3. State Machine Behaviors
    4. Main
    5. Footer

    screenshot of the example

    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"