Scribble 示例
Scribble 示例展示了如何重新实现QWidget 的部分事件处理程序,以接收应用程序部件生成的事件。
我们重新实现了鼠标事件处理程序以实现绘制,油漆事件处理程序以更新应用程序,调整大小事件处理程序以优化应用程序的外观。此外,我们还重新实现了关闭事件处理程序,以便在终止应用程序之前拦截关闭事件。
该示例还演示了如何使用QPainter 实时绘制图像以及重新绘制部件。
通过 Scribble 应用程序,用户可以绘制图像。通过File 菜单,用户可以打开和编辑现有图像文件、保存图像并退出应用程序。在绘画过程中,Options 菜单允许用户选择笔的颜色和宽度,以及清除屏幕。此外,Help 菜单还为用户提供了有关 Scribble 示例以及 Qt 的一般信息。
该示例由两个类组成:
ScribbleArea
是一个自定义 widget,用于显示 并允许用户在其上绘图。QImageMainWindow
在 的上方提供一个菜单。ScribbleArea
我们将首先回顾ScribbleArea
类。然后,我们将回顾使用ScribbleArea
的MainWindow
类。
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; };
ScribbleArea
类继承自QWidget 。我们重新实现mousePressEvent()
、mouseMoveEvent()
和mouseReleaseEvent()
函数来实现绘图。我们重新实现paintEvent()
函数来更新涂鸦区域,并重新实现resizeEvent()
函数来确保我们绘制的QImage 在任何时候都至少与窗口小部件一样大。
我们需要几个公共函数:openImage()
从文件中加载图像到涂鸦区域,允许用户编辑图像;save()
将当前显示的图像写入文件;clearImage()
槽清除涂鸦区域中显示的图像。我们需要私有的drawLineTo()
函数来实际执行绘制,还需要resizeImage()
来改变QImage 的大小。print()
槽负责处理打印。
我们还需要以下私有变量:
modified
如果涂鸦区域显示的图像有未保存的更改,则为 。true
scribbling
当用户在涂鸦区域内按下鼠标左键时,是 。true
penWidth
和 保存当前设置的宽度和颜色。penColor
image
存储用户绘制的图像。lastPoint
保存上次按下鼠标或移动鼠标时光标的位置。
涂鸦区域类的实现
ScribbleArea::ScribbleArea(QWidget *parent) : QWidget(parent) { setAttribute(Qt::WA_StaticContents); }
在构造函数中,我们为 widget 设置了Qt::WA_StaticContents 属性,表示 widget 内容根植于左上角,并且在调整 widget 大小时不会改变。Qt 使用此属性来优化调整大小时的绘制事件。这纯粹是一种优化,只能用于内容静态且根植于左上角的部件。
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()
函数中,我们加载给定的图片。然后,我们使用私有resizeImage()
函数调整加载的QImage 的大小,使其在两个方向上都至少与窗口部件一样大,并将image
成员变量设置为加载的图片。最后,我们调用QWidget::update() 来安排一次重绘。
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; }
saveImage()
函数创建了一个QImage 对象,该对象只覆盖实际image
的可见部分,并使用QImage::save() 将其保存。如果图像保存成功,我们会将涂鸦区域的modified
变量设置为false
,因为没有未保存的数据。
void ScribbleArea::setPenColor(const QColor &newColor) { myPenColor = newColor; } void ScribbleArea::setPenWidth(int newWidth) { myPenWidth = newWidth; }
setPenColor()
和setPenWidth()
函数设置当前钢笔的颜色和宽度。这些值将用于未来的绘制操作。
void ScribbleArea::clearImage() { image.fill(qRgb(255, 255, 255)); modified = true; update(); }
公共clearImage()
槽清除涂鸦区域中显示的图像。我们只需用白色填充整个图像,即对应于 RGB 值(255, 255, 255)。像往常一样,当我们修改图像时,我们会将modified
设置为true
,并安排一次重绘。
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() 函数来查找是哪个按钮导致了该事件。对于鼠标移动事件,我们使用QMouseEvent::buttons() 查找当前按住的按钮(作为 OR 组合)。
如果用户按下鼠标左键,我们会将鼠标光标的位置存储在lastPoint
中。我们还会记录用户当前正在涂鸦。(scribbling
变量是必要的,因为我们不能假定在鼠标移动和鼠标释放事件之前,同一部件上总会发生鼠标按下事件)。
如果用户按下左键移动鼠标或松开鼠标,我们就会调用私有的drawLineTo()
函数进行绘制。
void ScribbleArea::paintEvent(QPaintEvent *event) { QPainter painter(this); QRect dirtyRect = event->rect(); painter.drawImage(dirtyRect, image, dirtyRect); }
在重新实现paintEvent() 函数时,我们只需为涂鸦区域创建一个QPainter ,然后绘制图像。
说到这里,您可能会问,为什么我们不直接在 widget 上绘图,而要在QImage 中绘图,然后在paintEvent()
中将QImage 复制到屏幕上呢?这至少有三个很好的理由:
- 窗口系统要求我们能够随时重绘窗口部件。例如,如果窗口被最小化并恢复,窗口系统可能会忘记窗口部件的内容,并向我们发送一个绘制事件。换句话说,我们不能依赖窗口系统记住我们的图像。
- Qt XML 通常不允许我们在
paintEvent()
之外进行绘制。尤其是,我们不能从鼠标事件处理程序中进行绘制。(不过可以使用Qt::WA_PaintOnScreen widget 属性来改变这种行为)。 - 如果初始化正确,QImage 可以保证每个颜色通道(红、绿、蓝和 alpha)都使用 8 位,而QWidget 的颜色深度可能更低,这取决于显示器的配置。这意味着,如果我们加载一幅 24 位或 32 位图像并将其绘制到QWidget 上,然后再将QWidget 复制到QImage 中,可能会丢失一些信息。
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); }
当用户启动涂鸦应用程序时,会产生一个调整大小事件,并在涂鸦区域创建和显示一幅图像。我们让这个初始图像略大于应用程序的主窗口和涂鸦区域,以避免在用户调整主窗口大小时总是调整图像大小(这样效率会很低)。但当主窗口大于初始大小时,就需要调整图像大小。
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()
中,我们从最后一次按下鼠标或移动鼠标时鼠标所在的位置开始画一条线,然后将modified
设置为 true,生成重绘事件,并更新lastPoint
,以便下次调用drawLineTo()
时继续从原点开始绘制。
我们可以调用不带参数的update()
函数,但作为一种简单的优化,我们会传递一个QRect ,指定需要更新的涂鸦内的矩形区域,以避免完全重绘部件。
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 但在使用《ASP.NET》时,却没有一个很好的 API 来调整图片大小。有一个 () 函数可以实现这一功能,但当用于扩展图像时,它会将新区域填充为黑色,而我们需要的是白色。QImage::copy
因此,我们需要创建一个大小合适的全新QImage ,填充白色,然后使用QPainter 将旧图像绘制到新图像上。新图像采用QImage::Format_RGB32 格式,即每个像素存储为 0xffRRGGBB(其中 RR、GG 和 BB 分别为红、绿和蓝颜色通道,ff 为十六进制值 255)。
打印由print()
插槽处理:
void ScribbleArea::print() { #if defined(QT_PRINTSUPPORT_LIB) && QT_CONFIG(printdialog) QPrinter printer(QPrinter::HighResolution); QPrintDialog printDialog(&printer, this);
我们为所需的输出格式构建一个高分辨率 QPrinter 对象,使用 QPrintDialog 要求用户指定页面大小,并指出输出在页面上的格式。
如果对话框被接受,我们将执行打印到绘画设备的任务:
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) }
以这种方式将图像打印到文件中,只是在 QPrinter 上绘画而已。在将图像绘制到绘画设备上之前,我们会缩放图像,使其适合页面上的可用空间。
主窗口类定义
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; };
MainWindow
类继承自QMainWindow 。我们从QWidget 重新实现了closeEvent() 处理程序。open()
、save()
、penColor()
和penWidth()
插槽与菜单条目相对应。此外,我们还创建了四个私有函数。
我们使用布尔maybeSave()
函数来检查是否有未保存的更改。如果有未保存的更改,我们会给用户保存这些更改的机会。如果用户点击Cancel ,函数将返回false
。我们使用saveFile()
函数让用户保存当前在涂鸦区域显示的图像。
主窗口类的实现
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), scribbleArea(new ScribbleArea(this)) { setCentralWidget(scribbleArea); createActions(); createMenus(); setWindowTitle(tr("Scribble")); resize(500, 500); }
在构造函数中,我们创建了一个涂鸦区域,并将其作为MainWindow
部件的中心部件。然后创建相关的操作和菜单。
void MainWindow::closeEvent(QCloseEvent *event) { if (maybeSave()) event->accept(); else event->ignore(); }
关闭事件会发送到用户希望关闭的部件,通常是通过单击File|Exit 或单击X 标题栏按钮。通过重新实施事件处理程序,我们可以拦截试图关闭应用程序的尝试。
在本例中,我们使用关闭事件要求用户保存任何未保存的更改。相关逻辑位于maybeSave()
函数中。如果maybeSave()
返回 true,说明没有修改或用户已成功保存修改,我们就接受该事件。应用程序就可以正常终止。如果maybeSave()
返回 false,说明用户点击了Cancel ,因此我们 "忽略 "该事件,应用程序不受其影响。
void MainWindow::open() { if (maybeSave()) { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::currentPath()); if (!fileName.isEmpty()) scribbleArea->openImage(fileName); } }
在open()
插槽中,我们首先让用户有机会保存对当前显示图像的任何修改,然后再将新图像载入涂鸦区域。然后,我们要求用户选择一个文件,并在ScribbleArea
中加载该文件。
void MainWindow::save() { QAction *action = qobject_cast<QAction *>(sender()); QByteArray fileFormat = action->data().toByteArray(); saveFile(fileFormat); }
当用户选择Save As 菜单项,然后从格式菜单中选择一个条目时,就会调用save()
槽。我们首先要做的是使用QObject::sender() 找出发送信号的操作。该函数以QObject 指针的形式返回发送者。由于我们知道发送者是一个动作对象,因此可以安全地对QObject 进行转置。我们可以使用 C 风格转置或 C++static_cast<>()
,但作为一种防御性编程技巧,我们使用qobject_cast()。这样做的好处是,如果对象类型错误,就会返回一个空指针。由于空指针导致的崩溃比由于不安全施转导致的崩溃更容易诊断。
获得操作后,我们就可以使用QAction::data() 提取所选格式。(在创建操作时,我们使用QAction::setData() 来设置附加到操作的自定义数据,即QVariant 。更多信息请参阅createActions()
)。
既然已经知道了格式,我们就调用saveFile()
私有函数来保存当前显示的图像。
void MainWindow::penColor() { QColor newColor = QColorDialog::getColor(scribbleArea->penColor()); if (newColor.isValid()) scribbleArea->setPenColor(newColor); }
我们使用penColor()
插槽通过QColorDialog 从用户处获取新颜色。如果用户选择了新颜色,我们就将其作为涂鸦区域的颜色。
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); }
要在penWidth()
插槽中获取新的笔宽,我们使用QInputDialog 。QInputDialog 类提供了一个简单方便的对话框,用于从用户处获取单个值。我们使用静态QInputDialog::getInt() 函数,该函数将QLabel 和QSpinBox 结合在一起。QSpinBox 用涂鸦区域的笔宽进行初始化,允许的范围为 1 至 50,步长为 1(这意味着向上和向下箭头将数值递增或递减 1)。
如果用户点击OK ,布尔值ok
变量将被设置为true
;如果用户按下Cancel ,布尔值 变量将被设置为false
。
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()
槽来创建一个消息框,描述示例的设计意图。
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()
函数中,我们创建了代表菜单项的操作,并将它们连接到相应的插槽。我们特别创建了Save As 子菜单中的操作。我们使用QImageWriter::supportedImageFormats() 获取支持格式的列表(如QList<QByteArray>)。
然后,我们遍历该列表,为每种格式创建一个操作。我们使用文件格式调用QAction::setData() ,这样以后就可以用QAction::data() 来检索它。我们也可以通过截断"...... "来从操作的文本中推断出文件格式,但这样做会显得不够优雅。
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()
函数中,我们将之前创建的格式操作添加到saveAsMenu
中。然后,我们将其他操作以及saveAsMenu
子菜单添加到File 、Options 和Help 菜单中。
QMenu 类提供了一个菜单部件,可用于菜单栏、上下文菜单和其他弹出式菜单。QMenuBar 类提供了一个带有下拉菜单列表的水平菜单栏QMenu。最后,我们将File 和Options 菜单放入MainWindow
的菜单栏中,并使用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()
中,我们会检查是否有任何未保存的更改。如果有,我们将使用QMessageBox 向用户发出图片已被修改的警告,并提供保存修改的机会。
与QColorDialog 和QFileDialog 一样,创建QMessageBox 的最简单方法是使用其静态函数。QMessageBox 提供了一系列不同的信息,这些信息按两个轴排列:严重性(问题、信息、警告和严重)和复杂性(必要响应按钮的数量)。在这里,我们使用warning()
函数,因为该信息相当重要。
如果用户选择保存,我们将调用私人saveFile()
函数。为简单起见,我们使用 PNG 作为文件格式;用户可以随时按Cancel 并使用其他格式保存文件。
如果用户点击Cancel ,则maybeSave()
函数返回false
;否则返回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()
中,我们弹出了一个文件对话框,其中包含一个文件名建议。静态QFileDialog::getSaveFileName() 函数返回用户选择的文件名。文件不必存在。
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.