QStateMachine - QMouseEvent

2019-03-03 16:14发布

问题:

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 ?

回答1:

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

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"


回答2:

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.