MainWindow Class Implementation
MainWindow::MainWindow()
{
scribbleArea = new ScribbleArea;
setCentralWidget(scribbleArea);
createActions();
createMenus();
setWindowTitle(tr("Scribble"));
resize(500, 500);
}
In the constructor, we create a scribble area which we make the central widget of the MainWindow widget. Then we create the associated actions and menus.
void MainWindow::closeEvent(QCloseEvent *event)
{
if (maybeSave()) {
event->accept();
} else {
event->ignore();
}
}
Close events are sent to widgets that the users want to close, usually by clicking File|Exit or by clicking the X title bar button. By reimplementing the event handler, we can intercept attempts to close the application.
In this example, we use the close event to ask the user to save any unsaved changes. The logic for that is located in the maybeSave() function. If maybeSave() returns true, there are no modifications or the users successfully saved them, and we accept the event. The application can then terminate normally. If maybeSave() returns false, the user clicked Cancel, so we "ignore" the event, leaving the application unaffected by it.
void MainWindow::open()
{
if (maybeSave()) {
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open File"), QDir::currentPath());
if (!fileName.isEmpty())
scribbleArea->openImage(fileName);
}
}
In the open() slot we first give the user the opportunity to save any modifications to the currently displayed image, before a new image is loaded into the scribble area. Then we ask the user to choose a file and we load the file in the ScribbleArea.
void MainWindow::save()
{
QAction *action = qobject_cast<QAction *>(sender());
QByteArray fileFormat = action->data().toByteArray();
saveFile(fileFormat);
}
The save() slot is called when the users choose the Save As menu entry, and then choose an entry from the format menu. The first thing we need to do is to find out which action sent the signal using QObject::sender(). This function returns the sender as a QObject pointer. Since we know that the sender is an action object, we can safely cast the QObject. We could have used a C-style cast or a C++ static_cast<>(), but as a defensive programming technique we use a qobject_cast(). The advantage is that if the object has the wrong type, a null pointer is returned. Crashes due to null pointers are much easier to diagnose than crashes due to unsafe casts.
Once we have the action, we extract the chosen format using QAction::data(). (When the actions are created, we use QAction::setData() to set our own custom data attached to the action, as a QVariant. More on this when we review createActions().)
Now that we know the format, we call the private saveFile() function to save the currently displayed image.
void MainWindow::penColor()
{
QColor newColor = QColorDialog::getColor(scribbleArea->penColor());
if (newColor.isValid())
scribbleArea->setPenColor(newColor);
}
We use the penColor() slot to retrieve a new color from the user with a QColorDialog. If the user chooses a new color, we make it the scribble area's color.
void MainWindow::penWidth()
{
bool ok;
int newWidth = QInputDialog::getInteger(this, tr("Scribble"),
tr("Select pen width:"),
scribbleArea->penWidth(),
1, 50, 1, &ok);
if (ok)
scribbleArea->setPenWidth(newWidth);
}
To retrieve a new pen width in the penWidth() slot, we use QInputDialog. The QInputDialog class provides a simple convenience dialog to get a single value from the user. We use the static QInputDialog::getInt() function, which combines a QLabel and a QSpinBox. The QSpinBox is initialized with the scribble area's pen width, allows a range from 1 to 50, a step of 1 (meaning that the up and down arrow increment or decrement the value by 1).
The boolean ok variable will be set to true if the user clicked OK and to false if the user pressed Cancel.
void MainWindow::about()
{
QMessageBox::about(this, tr("About Scribble"),
tr("<p>The <b>Scribble</b> example shows how to use QMainWindow as the "
"base widget for an application, and how to reimplement some of "
"QWidget's event handlers to receive the events generated for "
"the application's widgets:</p><p> We reimplement the mouse event "
"handlers to facilitate drawing, the paint event handler to "
"update the application and the resize event handler to optimize "
"the application's appearance. In addition we reimplement the "
"close event handler to intercept the close events before "
"terminating the application.</p><p> The example also demonstrates "
"how to use QPainter to draw an image in real time, as well as "
"to repaint widgets.</p>"));
}
We implement the about() slot to create a message box describing what the example is designed to show.
void MainWindow::createActions()
{
openAct = new QAction(tr("&Open..."), this);
openAct->setShortcuts(QKeySequence::Open);
connect(openAct, SIGNAL(triggered()), this, SLOT(open()));
foreach (QByteArray format, QImageWriter::supportedImageFormats()) {
QString text = tr("%1...").arg(QString(format).toUpper());
QAction *action = new QAction(text, this);
action->setData(format);
connect(action, SIGNAL(triggered()), this, SLOT(save()));
saveAsActs.append(action);
}
printAct = new QAction(tr("&Print..."), this);
connect(printAct, SIGNAL(triggered()), scribbleArea, SLOT(print()));
exitAct = new QAction(tr("E&xit"), this);
exitAct->setShortcuts(QKeySequence::Quit);
connect(exitAct, SIGNAL(triggered()), this, SLOT(close()));
penColorAct = new QAction(tr("&Pen Color..."), this);
connect(penColorAct, SIGNAL(triggered()), this, SLOT(penColor()));
penWidthAct = new QAction(tr("Pen &Width..."), this);
connect(penWidthAct, SIGNAL(triggered()), this, SLOT(penWidth()));
clearScreenAct = new QAction(tr("&Clear Screen"), this);
clearScreenAct->setShortcut(tr("Ctrl+L"));
connect(clearScreenAct, SIGNAL(triggered()),
scribbleArea, SLOT(clearImage()));
aboutAct = new QAction(tr("&About"), this);
connect(aboutAct, SIGNAL(triggered()), this, SLOT(about()));
aboutQtAct = new QAction(tr("About &Qt"), this);
connect(aboutQtAct, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
}
In the createAction() function we create the actions representing the menu entries and connect them to the appropiate slots. In particular we create the actions found in the Save As sub-menu. We use QImageWriter::supportedImageFormats() to get a list of the supported formats (as a QList<QByteArray>).
Then we iterate through the list, creating an action for each format. We call QAction::setData() with the file format, so we can retrieve it later as QAction::data(). We could also have deduced the file format from the action's text, by truncating the "...", but that would have been inelegant.
void MainWindow::createMenus()
{
saveAsMenu = new QMenu(tr("&Save As"), this);
foreach (QAction *action, saveAsActs)
saveAsMenu->addAction(action);
fileMenu = new QMenu(tr("&File"), this);
fileMenu->addAction(openAct);
fileMenu->addMenu(saveAsMenu);
fileMenu->addAction(printAct);
fileMenu->addSeparator();
fileMenu->addAction(exitAct);
optionMenu = new QMenu(tr("&Options"), this);
optionMenu->addAction(penColorAct);
optionMenu->addAction(penWidthAct);
optionMenu->addSeparator();
optionMenu->addAction(clearScreenAct);
helpMenu = new QMenu(tr("&Help"), this);
helpMenu->addAction(aboutAct);
helpMenu->addAction(aboutQtAct);
menuBar()->addMenu(fileMenu);
menuBar()->addMenu(optionMenu);
menuBar()->addMenu(helpMenu);
}
In the createMenu() function, we add the previously created format actions to the saveAsMenu. Then we add the rest of the actions as well as the saveAsMenu sub-menu to the File, Options and Help menus.
The QMenu class provides a menu widget for use in menu bars, context menus, and other popup menus. The QMenuBar class provides a horizontal menu bar with a list of pull-down QMenus. At the end we put the File and Options menus in the MainWindow's menu bar, which we retrieve using the QMainWindow::menuBar() function.
bool MainWindow::maybeSave()
{
if (scribbleArea->isModified()) {
QMessageBox::StandardButton ret;
ret = QMessageBox::warning(this, tr("Scribble"),
tr("The image has been modified.\n"
"Do you want to save your changes?"),
QMessageBox::Save | QMessageBox::Discard
| QMessageBox::Cancel);
if (ret == QMessageBox::Save) {
return saveFile("png");
} else if (ret == QMessageBox::Cancel) {
return false;
}
}
return true;
}
In mayBeSave(), we check if there are any unsaved changes. If there are any, we use QMessageBox to give the user a warning that the image has been modified and the opportunity to save the modifications.
As with QColorDialog and QFileDialog, the easiest way to create a QMessageBox is to use its static functions. QMessageBox provides a range of different messages arranged along two axes: severity (question, information, warning and critical) and complexity (the number of necessary response buttons). Here we use the warning() function sice the message is rather important.
If the user chooses to save, we call the private saveFile() function. For simplicitly, we use PNG as the file format; the user can always press Cancel and save the file using another format.
The maybeSave() function returns false if the user clicks Cancel; otherwise it returns true.
bool MainWindow::saveFile(const QByteArray &fileFormat)
{
QString initialPath = QDir::currentPath() + "/untitled." + fileFormat;
QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"),
initialPath,
tr("%1 Files (*.%2);;All Files (*)")
.arg(QString(fileFormat.toUpper()))
.arg(QString(fileFormat)));
if (fileName.isEmpty()) {
return false;
} else {
return scribbleArea->saveImage(fileName, fileFormat);
}
}
In saveFile(), we pop up a file dialog with a file name suggestion. The static QFileDialog::getSaveFileName() function returns a file name selected by the user. The file does not have to exist.