QActions, QToolbars, and QActionGroups

11.7.1. The Command Pattern

The Command pattern, as described in [Gamma95] encapsulates operations as objects with a common execution interface. This can make it possible to place operations in a queue, log operations, and undo the results of an already executed operation.

Because an application might provide a variety of different ways for the user to issue the same command (e.g., menus, toolbar buttons, keyboard shortcuts), encapsulating each command as an action helps to ensure consistent, synchronized behavior across the application. QAction is, therefore, an ideal base class for implementing the Command pattern.

In Qt GUI applications, actions are typically "triggered" in one of the following ways:

There are several overloaded forms of QMenu::addAction(). We will use the version inherited from QWidget, addAction(QAction*) in Example 11.19. Here we see how to add actions to menus, action groups, and toolbars. We start by deriving a class from QMainWindow and equipping it with several QAction members plus a QActionGroup and a QToolBar.

Example 11.19. src/widgets/menus/study.h

[ . . . . ] class Study : public QMainWindow { Q_OBJECT public: Study(); public slots: void actionEvent(QAction* act); private: QActionGroup* actionGroup; <-- 1 QToolBar *toolbar; <-- 2 QAction *useTheForce; QAction *useTheDarkSide; QAction *studyWithObiWan; QAction *studyWithYoda; QAction *studyWithEmperor; QAction *fightYoda; QAction *fightDarthVader; QAction *fightObiWan; QAction *fightEmperor; protected: QAction* addChoice(QString name, QString text); }; [ . . . . ]  

(1)for catching the signals

(2)for displaying the actions as buttons

The constructor for this class sets up the menus and installs them in the QMenuBar that is already part of the base class. (See Example 11.20.)

Example 11.20. src/widgets/menus/study.cpp

[ . . . . ] Study::Study() { actionGroup = new QActionGroup(this); actionGroup->setExclusive(false); statusBar(); QWidget::setWindowTitle( "to become a jedi, you wish?" ); <-- 1 QMenu* useMenu = new QMenu("&Use", this); QMenu* studyMenu = new QMenu("&Study", this); QMenu* fightMenu = new QMenu("&Fight", this); useTheForce = addChoice("useTheForce", "Use The &Force"); useTheForce->setStatusTip("This is the start of a journey..."); useTheForce->setEnabled(true); useMenu->addAction(useTheForce); <-- 2 [ . . . . ] studyWithObiWan = addChoice("studyWithObiWan", "&Study With Obi Wan"); studyMenu->addAction(studyWithObiWan); studyWithObiWan->setStatusTip("He will certainly open doors for you..."); fightObiWan = addChoice("fightObiWan", "Fight &Obi Wan"); fightMenu->addAction(fightObiWan); fightObiWan->setStatusTip( "You'll learn some tricks from him that way, for sure!"); [ . . . . ] QMainWindow::menuBar()->addMenu(useMenu); QMainWindow::menuBar()->addMenu(studyMenu); QMainWindow::menuBar()->addMenu(fightMenu); toolbar = new QToolBar("Choice ToolBar", this); <-- 3 toolbar->addActions(actionGroup->actions()); QMainWindow::addToolBar(Qt::LeftToolBarArea, toolbar); QObject::connect(actionGroup, SIGNAL(triggered(QAction*)), this, SLOT(actionEvent(QAction*))); <-- 4 QWidget::move(300, 300); QWidget::resize(300, 300); }  

(1)The ClassName:: prefixes we use in methods here are not necessary, because the methods can be called on "this". We list the classname only to show the human reader from which class the method was inherited.

(2)It's already in a QActionGroup, but we also add it to a QMenu.

(3)This gives us visible buttons in a dockable widget for each of the QActions.

(4)Instead of connecting each individual action's signal, we perform one connect to an actionGroup that contains them all.

It is possible to connect individual QAction triggered() signals to individual slots. It is also possible to group related QActions together in a QActionGroup, as we have just done. QActionGroup offers a single signal triggered(QAction*), which makes it possible to handle the group of actions in a uniform way.

After being created, each QAction is added to three other objects (via addAction()):

  1. A QActionGroup, for signal handling
  2. A QMenu, one of three possible pull-down menus in a QMenuBar
  3. A QToolBar, where it is rendered as a button

Example 11.21. src/widgets/menus/study.cpp

[ . . . . ] // Factory method for creating QActions initialized in a uniform way QAction* Study::addChoice(QString name, QString text) { QAction* retval = new QAction(text, this); retval->setObjectName(name); retval->setEnabled(false); retval->setCheckable(true); actionGroup->addAction(retval); <-- 1 return retval; }  

(1)Add every action we create to a QActionGroup so that we only connect one signal to a slot.

To make this example a bit more interesting, we established some logical dependencies between the menu choices so that they were consistent with the plot of the various movies. This logic is expressed in the actionEvent() function. (See Example 11.22).

Example 11.22. src/widgets/menus/study.cpp

[ . . . . ] void Study::actionEvent(QAction* act) { QString name = act->objectName(); QString msg = QString(); if (act == useTheForce ) { studyWithObiWan->setEnabled(true); fightObiWan->setEnabled(true); useTheDarkSide->setEnabled(true); } if (act == useTheDarkSide) { studyWithYoda->setEnabled(false); fightYoda->setEnabled(true); studyWithEmperor->setEnabled(true); fightEmperor->setEnabled(true); fightDarthVader->setEnabled(true); } if (act == studyWithObiWan) { fightObiWan->setEnabled(true); fightDarthVader->setEnabled(true); studyWithYoda->setEnabled(true); } [ . . . . ] if (act == fightObiWan ) { if (studyWithEmperor->isChecked()) { msg = "You are victorious!"; } else { msg = "You lose."; act->setChecked(false); studyWithYoda->setEnabled(false); } } [ . . . . ] if (msg != QString()) { QMessageBox::information(this, "Result", msg, "ok"); } }

Because all actions are in a QActionGroup, a single triggered(QAction*) signal can be connected to the actionEvent() slot.

The client code in Example 11.23 shows how the program starts.

Example 11.23. src/widgets/menus/study.cpp

[ . . . .] int main( int argc, char ** argv ) { QApplication a( argc, argv ); Study study; study.show(); return a.exec(); }

Here is a screenshot of the running program.

All menu choices except one are initially disabled. As the user selects from the available choices, other options become enabled or disabled. Also, notice that the there is consistency between the buttons and the choices in the menus. Clicking on an enabled button causes the corresponding menu item to be checked. QAction stores the state (enabled/checked), and the QMenu and QToolBar provide views of the QAction.

Exercises: QActions, QMenus, and QMenuBars

1.

(Discussion question) There are QActions as children of QWidgets all over the place. How do you gather them all for a ShortcutView widget that lets the user display and change all of the keyboard shortcuts in the application?

2.

Revisit the 15 puzzle application in Section 11.5.2 and add QActions for:

  • Shuffle puzzle
  • Reset puzzle
  • Quit

For Quit, pop up a message box asking whether the user is sure before actually quitting.

3.

Write a "login" application using QMainWindow. Start with these QActions:

  • Login
  • Create New User
  • Edit Preferences
  • Change Password

These choices should be available by pull-down menu as well as toolbar. Create QActions and a QActionGroup for them all.

The last two choices should be disabled unless the user is logged in.

The initial login screen should have QLineEdits for a user id and a password. If the user chooses "Create New User" or "Change Password", the program should display a new form with QLineEdits. It should ask for the password twice, and make sure the passwords match, before actually performing the operation. If the passwords mismatch, try again.

For "Login", it should check that the user exists and that the password is valid, before letting you log in, and enabling the other QActions.

If the user chooses "Edit Preferences" it should ask the following questions:

  • What is your name?
  • What is your quest?
  • What is your favorite color? (Use a QComboBox with preset colors to red, green, blue, black, pink, and chartreuse to choose from.)

It should remember these for each user from previous sessions.

The program should load the user data from a file called "users.xml" on startup and save to the same file when finished, without any interaction. Load and save in any format you want, using QTextStream and QFile.

Exercise: Card Game GUI

Write a blackjack game, with the following actions:

  1. New game
  2. Deal new hand
  3. Shuffle deck
  4. Hit meask for another card
  5. Stayevaluate my hand
  6. Quitstop playing

These actions and the rules of the game are explained below.

When the game starts, the user and the dealer are each dealt a "hand" of cards. Each hand initially consists of two cards. The user plays her hand first by deciding to add cards to her hand with the "Hit me" action zero or more times. Each Hit adds one card to her hand. The user signals that she wants no more cards with the "Stay" action.

For the purposes of evaluation of a hand, a "face card" (Jack, Queen, and King) counts as 10 points, an Ace can count as 1 or 11 points, whichever is best. Each other card has a number and a point value equal to that number. If the hand consists of an Ace plus a Jack, then it is best to count the Ace as 11 so that the total score is 21. But if the hand consists of an 8 plus a 7, and an Ace is added to the hand, it is best to count the Ace as 1.

The object of the game is to achieve the highest point total that is not greater than 21. If a player gets a point total greater than 21, that player is "busted" (loses) and the hand is finished.

If a player gets five cards in her hand with a total that is not greater than 21, then that player wins the hand.

After the user either wins, loses, or Stays, the dealer can take as many hits as necessary to obtain a point total greater than 18. When that state is reached the dealer must Stay and the hand is finished. The player whose score is closer to, but not greater than, 21 wins. If the two scores are equal, the dealer wins.

When the hand is over, the user can only select "Deal hand", "New game", or "Quit" (i.e., Hit and Stay are disabled).

After the user selects "Deal hand", that choice should be disabled until the hand is finished.

Keep track of the number of games won by the dealer and by the user, starting with zero for each player, by adding one point to the total for the winner of each hand. Display these totals above the cards.

Deal more cards without resetting the deck until the deck becomes empty or the user chooses "Shuffle deck". Try to reuse or extend the CardDeck and related classes that you developed earlier in Section 10.3. Add a graphical representation to your game by showing a QLabel with a QPixMap for each card, as was done in Chapter 11.

Provide a pull-down menu and a toolbar for each of the QActions. Make sure that Hit and Stay are only enabled after the game has started.

Show how many cards are left in the deck in a read only QSpinBox at the top of the window.

New game should zero the games won totals and reset the deck.

BlackJack UML Diagram

11.7.1.1. Design Suggestions

Try to keep the model classes separate from the view classes, rather than adding GUI to the model classes. Keeping a strong separation between model and view will give us benefits later.

Figure 11.10 is only a starting point. You need to decide on the base class(es) to extend for defining the model classes, as well as which containers to reuse.

To modify the UML diagram you can try loading the diagram file, cardgame.xmi, into umbrello.

Категории