- Game Programming Using Qt Beginner's Guide
- Witold Wysota Lorenz Haas
- 1605字
- 2025-04-04 20:19:16
Time for action – functionality of a tic-tac-toe board
We need to implement a function that will be called upon by clicking on any of the nine buttons on the board. It has to change the text of the button that was clicked on—either X
or O
—based on which player made the move; then, it has to check whether the move resulted in winning the game by the player (or a draw if no more moves are possible), and if the game ended, it should emit an appropriate signal, informing the environment about the event.
When the user clicks on a button, the clicked()
signal is emitted. Connecting this signal to a custom slot lets us implement the mentioned functionality, but since the signal doesn't carry any parameters, how do we tell which button caused the slot to be triggered? We could connect each button to a separate slot but that's an ugly solution. Fortunately, there are two ways of working around this problem. When a slot is invoked, a pointer to the object that caused the signal to be sent is accessible through a special method in QObject
called sender()
. We can use that pointer to find out which of the nine buttons stored in the board list is the one that caused the signal to fire:
void TicTacToeWidget::someSlot() {
QObject *btn = sender();
int idx = board.indexOf(btn);
QPushButton *button = board.at(idx);
// ...
}
While sender()
is a useful call, we should try to avoid it in our own code as it breaks some principles of object-oriented programming. Moreover, there are situations where calling this function is not safe. A better way is to use a dedicated class called QSignalMapper
, which lets us achieve a similar result without using sender()
directly. Modify the setupBoard()
method in TicTacToeWidget
as follows:
QGridLayout *gridLayout = new QGridLayout; QSignalMapper *mapper = new QSignalMapper(this); for(int row = 0; row < 3; ++row) { for(int column = 0; column < 3; ++column) { QPushButton *button = new QPushButton; button->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); button->setText(" "); gridLayout->addWidget(button, row, column); board.append(button); mapper->setMapping(button, board.count()-1); connect(button, SIGNAL(clicked()), mapper, SLOT(map())); } } connect(mapper, SIGNAL(mapped(int)), this, SLOT(handleButtonClick(int))); setLayout(gridLayout);
Here, we first created an instance of QSignalMapper
and passed a pointer to the board widget as its parent so that the mapper is deleted when the widget is deleted. Then, when we create buttons, we "teach" the mapper that each of the buttons has a number associated with it—the first button will have the number 0
, the second one will be bound to the number 1
, and so on. By connecting the clicked()
signal from the button to mapper's map()
slot, we tell the mapper to do its magic upon receiving that signal. What the mapper will do is that it will then find the mapping of the sender of the signal and emit another signal—mapped()
—with the mapped number as its parameter. This allows us to connect to that signal with a slot (handleButtonClick
) that takes the index of the button in the board list.
Now it is time to implement the slot itself (remember to declare it in the header file!). However, before we do that, let's add a useful enum and a few helper methods to the class:
enum Player { Invalid, Player1, Player2, Draw };
This enum lets us specify information about players in the game. We can use it immediately to mark whose move it is now. To do so, add a private field to the class:
Player m_currentPlayer;
Then, add the two public methods to manipulate the value of this field:
Player currentPlayer() const { return m_currentPlayer; } void setCurrentPlayer(Player p) { if(m_currentPlayer == p) return; m_currentPlayer = p; emit currentPlayerChanged(p); }
The last method emits a signal, so we have to add the signal declaration to the class definition along with another signal that we are going to use:
signals: void currentPlayerChanged(Player); void gameOver(TicTacToeWidget::Player);
Tip
Note that we only emit the currentPlayerChanged
signal when the current player really changes. You always have to pay attention that you don't emit a "changed" signal when you set a value to a field to the same value that it had before the function was called. Users of your classes expect that if a signal is called changed, it is emitted when the value really changes. Otherwise, this can lead to an infinite loop in signal emissions if you have two objects that connect their value setters to the other object's changed signal.
Now let's declare the handleButtonClick
slot:
public slots: void handleButtonClick(int);
And then implement it in the .cpp
file:
void TicTacToeWidget::handleButtonClick(int index) { if(index < 0 || index >= board.size()) return; // out of bounds check QPushButton *button = board.at(index); if(button->text() != " ") return; // invalid move button->setText(currentPlayer() == Player1 ? "X" : "O"); Player winner = checkWinCondition(index / 3, index % 3); if(winner == Invalid) { setCurrentPlayer(currentPlayer() == Player1 ? Player2 : Player1); return; } else { emit gameOver(winner); } }
Here, we first retrieve a pointer to the button based on its index. Then, we check whether the button contains any text—if so, then this means that it doesn't participate in the game anymore, so we return from the method so that the player can pick another field in the board. Next, we set the current player's mark on the button. Then, we check whether the player has won the game, passing it the row (index / 3
) and column (index % 3
) index of the current move. If the game didn't end, we switch the current player and return. Otherwise, we emit a gameOver()
signal, telling our environment who won the game. The checkWinCondition()
method returns Player1
, Player2
, or Draw
if the game has ended and Invalid
otherwise. We will not show the implementation of this method here as it is quite complex. Try implementing it on your own and if you encounter problems, you can see the solution in the code bundle that accompanies this book.
Properties
Apart from signals and slots, Qt meta-objects also give programmers an ability to use the so-called properties that are essentially named attributes that can be assigned values of a particular type. They are useful to express important features of an object—like text of a button, size of a widget, player names in games, and so on.
Declaring a property
To create a property, we first need to declare it in a private section of a class that inherits QObject
using a special Q_PROPERTY
macro, which lets Qt know how to use the property. A minimal declaration contains the type of the property, its name, and information about a method name that is used to retrieve a value of the property. For example, the following code declares a property of the type double
that is called height
and uses a method called height
to read the property value:
Q_PROPERTY(double height READ height)
The getter method has to be declared and implemented as usual. Its prototype has to comply with these rules: it has to be a public method that returns a value or constant reference of a type of the property, and it can't take any input parameters and the method itself has to be constant. Typically, a property will manipulate a private member variable of the class:
class Tower : public QObject { Q_OBJECT // enable meta-object generation Q_PROPERTY(double height READ height) // declare the property public: Tower(QObject *parent = 0) : QObject(parent) { m_height = 6.28; } double height() const { return m_height; } // return property value private: double m_height; // internal member variable holding the property value };
Such a property is practically useless because there is no way to change its value. Luckily, we can extend the declaration to include the information about how to write a value to the property:
Q_PROPERTY(double height READ height WRITE setHeight)
Again, we have to declare and implement setHeight
so that it behaves as the setter method for the property—it needs to be a public method that takes a value or constant reference of the type of the property and returns void:
void setHeight(double newHeight) { m_height = newHeight; }
Tip
Property setters are good candidates for public slots so that you can easily manipulate property values using signals and slots.
We will learn about some of the other extensions to Q_PROPERTY
declarations in the later chapters of this book.
Using a property
There are two ways in which you can access properties. One is of course, to use getter and setter methods that we declared with READ
and WRITE
keywords in the Q_PROPERTY
macro—this will naturally work since they are regular C++ methods.
The other way is to use facilities offered by QObject
and the meta-object system. They allow to us access properties by name using two methods that accept property names as strings. A generic property getter (which returns the property value) is a method called property
. Its setter counterpart (that takes the value and returns void) is setProperty
. Since we can have properties with different data types, what is the data structure that is used by those two methods that hold values for different kinds of properties? Qt has a special class for this called QVariant
, which behaves a lot like a C union in the way that it can store values of different types. There are a couple of advantages to using a union though—the three most important are that you can ask the object what type of data it currently holds, you can convert some of the types to other types (for example, a string to an integer), and you can teach it to operate on your own custom types.