0

I'm trying to build a simple memory game with Qt 5.11.1 and C++, where you get a few tiles on screen and you have to click on two and try to match the images they show.

My tiles are implemented as QPushButtons. Each time you click on one an image is displayed (by calling a showImage()method that changes the button background). When a second tile is clicked, if there is a match the two buttons are disabled so you can't click on them again (and you get a higher score). However, if you didn't get a match, the two tiles you just clicked will go back to their initial state (showing no image) after 1 second (this allows the user to "memorize" which image was showing up on each tile).

Whenever you click on a "tile" (button) it becomes disabled (button->setEnabled(false)). If after clicking a second tile there was no match, then both tiles are turned back and then setEnabled(true) again. I'm using a single shot QTimer to call the method that will turn back the tiles:

QTimer::singleShot(1000, this, SLOT(turnTilesBack()));
firstTile->setEnabled(true);
secondTile->setEnabled(true);

Everything is working as expected, except for one thing: as QTimer runs in its own thread (or so I understand from what I read) all of the available tiles remain enabled during the 1000 milisecond lapse, allowing the user to continue clicking on them. However, when there is no match, I'd like to "freeze" the buttons until the QTimer has timed out so the user can't continue playing until the tiles have turned back.

So instead of using the QTimer I've trying this solution which I saw on this question (How do I create a pause/wait function using Qt?):

QTime dieTime= QTime::currentTime().addSecs(1);
while (QTime::currentTime() < dieTime)
    turnTilesBack();

although I removed this line: QCoreApplication::processEvents(QEventLoop::AllEvents, 100); as this would cause the main thread not to freeze and buttons would still be clickable.

But with this approach, whenever the user clicks on the second tile, if there is no match the image is not even displayed, even when my showImage() method is called before the code above, and I'm not sure why this is. So the user knows there was no match because after 1 second the tiles go back to their initial state, but they never got to see the image on the second button.

As another approach, I also though of disabling all buttons and then after the single shot QTimer times out, re-enabling back only the ones that have not been matched yet. But this would require additional logic to keep track of which tiles have been matched. So for now I'm sticking to the

Is there a cleaner solution? Maybe there's a way to make the QTimer freeze the main thread until it times out?

Floella
  • 1,279
  • 1
  • 22
  • 41
  • 1
    Why don't you keep your first solution and reenable in the turnTilesBack() slot? This way you don't have a blocking wait function and get the desired behavior. – Herr von Wurst Jan 04 '19 at 20:15
  • 1
    You will need the extra logic you mention in your 2nd to last paragraph. A simple array of button states should be all you need. Or perhaps you could set a flag while the timer is active and ignore button clicks while that flag is set. – Jim Rhodes Jan 04 '19 at 20:32
  • @HerrvonWurst I have tried that, but with no luck :( My turnTilesBack() method only sets an empty style for both buttons that have been clicked and then I'm calling setEnabled(true) on both buttons, but still the rest of the buttons remain enabled. – Floella Jan 04 '19 at 20:37

1 Answers1

3

An easy way to enable/disable the entire group of QPushButtons is to place them on an intermediate widget (in the below example I've used a QFrame)

If you want to disable all the QPushButtons, you just disable the frame, and all its child widgets will be disabled.

When you want to re-enable them, you enable the frame.

Any widgets inside the frame which are already disabled won't be enabled when the frame is re-enabled, so you won't lose your enabled/disabled state on the individual buttons

Here is a simple example. Note that I've used explicit enable/disable buttons which act as a proxy for your timer.

#include <QApplication>
#include <QMainWindow>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QFrame>

int main(int argc, char** argv)
{
    QApplication* app = new QApplication(argc, argv);
    QMainWindow*  window = new QMainWindow();
    window->setFixedSize(1024, 200);

    QWidget* widget = new QWidget();
    QHBoxLayout layout(widget);

    QPushButton* enable  = new QPushButton("enable");
    QPushButton* disable = new QPushButton("disable");
    QFrame*      frame   = new QFrame();

    layout.addWidget(enable);
    layout.addWidget(disable);
    layout.addWidget(frame);

    QVBoxLayout frame_layout(frame);
    for (int i = 0; i < 5; ++i)
        frame_layout.addWidget(new QPushButton("click"));

    // this shows that an already disabled button remains disabled
    QPushButton* already_chosen = new QPushButton("click");
    frame_layout.addWidget(already_chosen);
    already_chosen->setEnabled(false);

    QObject::connect(enable,  &QPushButton::clicked, [&]{ frame->setEnabled(true); });
    QObject::connect(disable, &QPushButton::clicked, [&]{ frame->setEnabled(false); });

    window->setCentralWidget(widget);
    window->show();
    return app->exec();
}
Steve Lorimer
  • 27,059
  • 17
  • 118
  • 213
  • Thanks Steve. I was actually iterating over each QPushButton to change the state. But the thing is I don't want to re-enable every button. I only need to re-enable those that have not been matched yet. That's why I was trying to find a different solution where the QTimer blocks the thread, before taking this approach. – Floella Jan 04 '19 at 21:22
  • 1
    @Floella yes, this is why enabling/disabling the parent widget of all your QPushButtons works so well. You don't need to keep track of which are enabled and which are disabled. You set each QPushButton enabled/disabled according to your successfully matched rules, while at the same time once the user has selected 2 tiles, disabling the parent widget prevents any further tiles being chosen... and it doesn't mess with your matched state – Steve Lorimer Jan 05 '19 at 02:49
  • Ohh, sorry, I hadn't fully got what you meant. That sounds like a nice and clean approach. I'm struggling a bit with the implementation, but trying it right now. Thanks! – Floella Jan 08 '19 at 21:27