涂鸦范例展示如何重实现某些 QWidget 's event handlers to receive the events generated for the application's widgets.
We reimplement the mouse event handlers to implement 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.
The example also demonstrates how to use QPainter to draw an image in real time, as well as to repaint widgets.
With the Scribble application the users can draw an image. The File menu gives the users the possibility to open and edit an existing image file, save an image and exit the application. While drawing, the 选项 menu allows the users to choose the pen color and pen width, as well as clear the screen. In addition the 帮助 menu provides the users with information about the Scribble example in particular, and about Qt in general.
范例由 2 个类组成:
ScribbleArea
is a custom widget that displays a
QImage
and allows to the user to draw on it.
MainWindow
provides a menu above the
ScribbleArea
.
We will start by reviewing the
ScribbleArea
class. Then we will review the
MainWindow
class, which uses
ScribbleArea
.
class ScribbleArea : public QWidget { Q_OBJECT public: ScribbleArea(QWidget *parent = nullptr); bool openImage(const QString &fileName); bool saveImage(const QString &fileName, const char *fileFormat); void setPenColor(const QColor &newColor); void setPenWidth(int newWidth); bool isModified() const { return modified; } QColor penColor() const { return myPenColor; } int penWidth() const { return myPenWidth; } public slots: void clearImage(); void print(); protected: void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; private: void drawLineTo(const QPoint &endPoint); void resizeImage(QImage *image, const QSize &newSize); bool modified = false; bool scribbling = false; int myPenWidth = 1; QColor myPenColor = Qt::blue; QImage image; QPoint lastPoint; };
The
ScribbleArea
类继承自
QWidget
。重实现
mousePressEvent()
,
mouseMoveEvent()
and
mouseReleaseEvent()
functions to implement the drawing. We reimplement the
paintEvent()
function to update the scribble area, and the
resizeEvent()
function to ensure that the
QImage
on which we draw is at least as large as the widget at any time.
需要几个公共函数:
openImage()
loads an image from a file into the scribble area, allowing the user to edit the image;
save()
writes the currently displayed image to file;
clearImage()
slot clears the image displayed in the scribble area. We need the private
drawLineTo()
function to actually do the drawing, and
resizeImage()
to change the size of a
QImage
。
print()
slot handles printing.
还需要下列私有变量:
modified
is
true
if there are unsaved changes to the image displayed in the scribble area.
scribbling
is
true
while the user is pressing the left mouse button within the scribble area.
penWidth
and
penColor
hold the currently set width and color for the pen used in the application.
image
存储由用户绘制的图像。
lastPoint
holds the position of the cursor at the last mouse press or mouse move event.
ScribbleArea::ScribbleArea(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); }
在构造函数中,设置 Qt::WA_StaticContents attribute for the widget, indicating that the widget contents are rooted to the top-left corner and don't change when the widget is resized. Qt uses this attribute to optimize paint events on resizes. This is purely an optimization and should only be used for widgets whose contents are static and rooted to the top-left corner.
bool ScribbleArea::openImage(const QString &fileName) { QImage loadedImage; if (!loadedImage.load(fileName)) return false; QSize newSize = loadedImage.size().expandedTo(size()); resizeImage(&loadedImage, newSize); image = loadedImage; modified = false; update(); return true; }
在
openImage()
function, we load the given image. Then we resize the loaded
QImage
to be at least as large as the widget in both directions using the private
resizeImage()
function and we set the
image
member variable to be the loaded image. At the end, we call
QWidget::update
() to schedule a repaint.
bool ScribbleArea::saveImage(const QString &fileName, const char *fileFormat) { QImage visibleImage = image; resizeImage(&visibleImage, size()); if (visibleImage.save(fileName, fileFormat)) { modified = false; return true; } return false; }
The
saveImage()
函数创建
QImage
object that covers only the visible section of the actual
image
and saves it using
QImage::save
(). If the image is successfully saved, we set the scribble area's
modified
变量到
false
, because there is no unsaved data.
void ScribbleArea::setPenColor(const QColor &newColor) { myPenColor = newColor; } void ScribbleArea::setPenWidth(int newWidth) { myPenWidth = newWidth; }
The
setPenColor()
and
setPenWidth()
functions set the current pen color and width. These values will be used for future drawing operations.
void ScribbleArea::clearImage() { image.fill(qRgb(255, 255, 255)); modified = true; update(); }
公共
clearImage()
slot clears the image displayed in the scribble area. We simply fill the entire image with white, which corresponds to RGB value (255, 255, 255). As usual when we modify the image, we set
modified
to
true
and schedule a repaint.
void ScribbleArea::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { lastPoint = event->position().toPoint(); scribbling = true; } } void ScribbleArea::mouseMoveEvent(QMouseEvent *event) { if ((event->buttons() & Qt::LeftButton) && scribbling) drawLineTo(event->position().toPoint()); } void ScribbleArea::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && scribbling) { drawLineTo(event->position().toPoint()); scribbling = false; } }
对于鼠标按下和鼠标释放事件,使用 QMouseEvent::button () function to find out which button caused the event. For mouse move events, we use QMouseEvent::buttons () to find which buttons are currently held down (as an OR-combination).
If the users press the left mouse button, we store the position of the mouse cursor in
lastPoint
. We also make a note that the user is currently scribbling. (The
scribbling
variable is necessary because we can't assume that a mouse move and mouse release event is always preceded by a mouse press event on the same widget.)
If the user moves the mouse with the left button pressed down or releases the button, we call the private
drawLineTo()
function to draw.
void ScribbleArea::paintEvent(QPaintEvent *event) { QPainter painter(this); QRect dirtyRect = event->rect(); painter.drawImage(dirtyRect, image, dirtyRect); }
In the reimplementation of the paintEvent () function, we simply create a QPainter for the scribble area, and draw the image.
At this point, you might wonder why we don't just draw directly onto the widget instead of drawing in a
QImage
and copying the
QImage
onto screen in
paintEvent()
. There are at least three good reasons for this:
paintEvent()
. In particular, we can't paint from the mouse event handlers. (This behavior can be changed using the
Qt::WA_PaintOnScreen
widget attribute, though.)
void ScribbleArea::resizeEvent(QResizeEvent *event) { if (width() > image.width() || height() > image.height()) { int newWidth = qMax(width() + 128, image.width()); int newHeight = qMax(height() + 128, image.height()); resizeImage(&image, QSize(newWidth, newHeight)); update(); } QWidget::resizeEvent(event); }
When the user starts the Scribble application, a resize event is generated and an image is created and displayed in the scribble area. We make this initial image slightly larger than the application's main window and scribble area, to avoid always resizing the image when the user resizes the main window (which would be very inefficient). But when the main window becomes larger than this initial size, the image needs to be resized.
void ScribbleArea::drawLineTo(const QPoint &endPoint) { QPainter painter(&image); painter.setPen(QPen(myPenColor, myPenWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); painter.drawLine(lastPoint, endPoint); modified = true; int rad = (myPenWidth / 2) + 2; update(QRect(lastPoint, endPoint).normalized() .adjusted(-rad, -rad, +rad, +rad)); lastPoint = endPoint; }
在
drawLineTo()
, we draw a line from the point where the mouse was located when the last mouse press or mouse move occurred, we set
modified
to true, we generate a repaint event, and we update
lastPoint
so that next time
drawLineTo()
is called, we continue drawing from where we left.
可以调用
update()
function with no parameter, but as an easy optimization we pass a
QRect
that specifies the rectangle inside the scribble are needs updating, to avoid a complete repaint of the widget.
void ScribbleArea::resizeImage(QImage *image, const QSize &newSize) { if (image->size() == newSize) return; QImage newImage(newSize, QImage::Format_RGB32); newImage.fill(qRgb(255, 255, 255)); QPainter painter(&newImage); painter.drawImage(QPoint(0, 0), *image); *image = newImage; }
QImage has no nice API for resizing an image. There's a QImage::copy () function that could do the trick, but when used to expand an image, it fills the new areas with black, whereas we want white.
So the trick is to create a brand new QImage with the right size, to fill it with white, and to draw the old image onto it using QPainter . The new image is given the QImage::Format_RGB32 format, which means that each pixel is stored as 0xffRRGGBB (where RR, GG, and BB are the red, green and blue color channels, ff is the hexadecimal value 255).
打印的处理是通过
print()
槽:
void ScribbleArea::print() { #if defined(QT_PRINTSUPPORT_LIB) && QT_CONFIG(printdialog) QPrinter printer(QPrinter::HighResolution); QPrintDialog printDialog(&printer, this);
We construct a high resolution QPrinter object for the required output format, using a QPrintDialog to ask the user to specify a page size and indicate how the output should be formatted on the page.
If the dialog is accepted, we perform the task of printing to the paint device:
if (printDialog.exec() == QDialog::Accepted) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = image.size(); size.scale(rect.size(), Qt::KeepAspectRatio); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(image.rect()); painter.drawImage(0, 0, image); } #endif // QT_CONFIG(printdialog) }
Printing an image to a file in this way is simply a matter of painting onto the QPrinter. We scale the image to fit within the available space on the page before painting it onto the paint device.
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); protected: void closeEvent(QCloseEvent *event) override; private slots: void open(); void save(); void penColor(); void penWidth(); void about(); private: void createActions(); void createMenus(); bool maybeSave(); bool saveFile(const QByteArray &fileFormat); ScribbleArea *scribbleArea; QMenu *saveAsMenu; QMenu *fileMenu; QMenu *optionMenu; QMenu *helpMenu; QAction *openAct; QList<QAction *> saveAsActs; QAction *exitAct; QAction *penColorAct; QAction *penWidthAct; QAction *printAct; QAction *clearScreenAct; QAction *aboutAct; QAction *aboutQtAct; };
The
MainWindow
类继承自
QMainWindow
。重实现
closeEvent
() handler from
QWidget
。
open()
,
save()
,
penColor()
and
penWidth()
slots correspond to menu entries. In addition we create four private functions.
使用布尔
maybeSave()
function to check if there are any unsaved changes. If there are unsaved changes, we give the user the opportunity to save these changes. The function returns
false
若用户点击
Cancel
. We use the
saveFile()
function to let the user save the image currently displayed in the scribble area.
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), scribbleArea(new ScribbleArea(this)) { 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()
函数。若
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); } }
在
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
另存为
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()
)。
现在知道格式,调用私有
saveFile()
函数以保存目前显示图像。
void MainWindow::penColor() { QColor newColor = QColorDialog::getColor(scribbleArea->penColor()); if (newColor.isValid()) scribbleArea->setPenColor(newColor); }
使用
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::getInt(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()
槽,使用
QInputDialog
。
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
和
QSpinBox
。
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).
布尔
ok
变量会被设为
true
若用户点击
OK
and to
false
若用户按下
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>")); }
实现
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, &QAction::triggered, this, &MainWindow::open); const QList<QByteArray> imageFormats = QImageWriter::supportedImageFormats(); for (const QByteArray &format : imageFormats) { QString text = tr("%1...").arg(QString::fromLatin1(format).toUpper()); QAction *action = new QAction(text, this); action->setData(format); connect(action, &QAction::triggered, this, &MainWindow::save); saveAsActs.append(action); } printAct = new QAction(tr("&Print..."), this); connect(printAct, &QAction::triggered, scribbleArea, &ScribbleArea::print); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcuts(QKeySequence::Quit); connect(exitAct, &QAction::triggered, this, &MainWindow::close); penColorAct = new QAction(tr("&Pen Color..."), this); connect(penColorAct, &QAction::triggered, this, &MainWindow::penColor); penWidthAct = new QAction(tr("Pen &Width..."), this); connect(penWidthAct, &QAction::triggered, this, &MainWindow::penWidth); clearScreenAct = new QAction(tr("&Clear Screen"), this); clearScreenAct->setShortcut(tr("Ctrl+L")); connect(clearScreenAct, &QAction::triggered, scribbleArea, &ScribbleArea::clearImage); aboutAct = new QAction(tr("&About"), this); connect(aboutAct, &QAction::triggered, this, &MainWindow::about); aboutQtAct = new QAction(tr("About &Qt"), this); connect(aboutQtAct, &QAction::triggered, qApp, &QApplication::aboutQt); }
在
createAction()
function we create the actions representing the menu entries and connect them to the appropriate slots. In particular we create the actions found in the
另存为
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); for (QAction *action : std::as_const(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); }
在
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
,
选项
and
帮助
菜单。
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
QMenu
s. At the end we put the
File
and
选项
menus in the
MainWindow
's menu bar, which we retrieve using the
QMainWindow::menuBar
() 函数。
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; }
在
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.
就像
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()
函数返回
false
若用户点击
Cancel
;否则,返回
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::fromLatin1(fileFormat.toUpper())) .arg(QString::fromLatin1(fileFormat))); if (fileName.isEmpty()) return false; return scribbleArea->saveImage(fileName, fileFormat.constData()); }
在
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.