Qt文档翻译:Scribble例程

以前翻译的一篇Qt文档,从163博客上迁移过来。

本例程中,我们重写鼠标事件处理程序来实现绘图,重写Paint事件处理程序来更新应用程序,并重写resize事件,以优化应用程序的外观。此外,重写了close事件处理程序,在程序关闭之前处理截获的close事件。

本例程还演示了如何使用QPainter的进行实时图像绘制和窗口组件(widget)的重绘制。

Scribble例程是一个绘图例程。文件菜单使用户可以打开并编辑现有的图像文件,保存图像和退出应用程序。在绘图时, 选项菜单可以使用户选择画笔颜色和画笔宽度,或清除屏幕。此外帮助菜单提供关于Scribble例程和Qt的信息。

本例程包括两个类: - ScribbleArea是一个显示QImage并能让用户在该组件上绘图的自定义组件。 - MainWindowScribbleArea上方提供了一个菜单。

首先学习ScribbleArea类。然后是使用ScribbleAreaMainWindow类。

ScribbleArea类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ScribbleArea : public QWidget
{
Q_OBJECT

public:
ScribbleArea(QWidget *parent = 0);

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) Q_DECL_OVERRIDE;
void mouseMoveEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
void resizeEvent(QResizeEvent *event) Q_DECL_OVERRIDE;

private:
void drawLineTo(const QPoint &endPoint);
void resizeImage(QImage *image, const QSize &newSize);

bool modified;
bool scribbling;
int myPenWidth;
QColor myPenColor;
QImage image;
QPoint lastPoint;
};

ScribbleArea类继承自QWidget。重写mousePressEvent()mouseMoveEvent()mouseReleaseEvent()函数来实现绘图。重写paintEvent()函数和resizeEvent()函数以更新绘图区,并保证绘图的QImagewidget组件在所有情况下同样大小。

公有函数: - openImage()从文件中导入图像到绘图区,使用户能够编辑图像; - save()函数保存当前显示的图像文件; - clearImage()槽函数将清除程序显示在绘图区中的图像。

私有函数drawLineTo()函数负责实际的图像绘制工作,resizeImage()函数处理QImage的大小的变化。 槽函数print()处理打印。

我们还需要以下私有变量: - modified:为true,绘图区的图像有未保存的更改。 - scribbling:为true,用户在绘图区内按下鼠标左键。 - penWidthpenColor:保存当前设置宽度和颜色的应用程序中使用的笔。 - image:存储由用户所绘制的图像。 - lastPoint:保存上一次鼠标按下或移动事件的光标位置。

ScribbleArea 类的实现

1
2
3
4
5
6
7
8
9
ScribbleArea::ScribbleArea(QWidget *parent)
: QWidget(parent)
{
setAttribute(Qt::WA_StaticContents);
modified = false;
scribbling = false;
myPenWidth = 1;
myPenColor = Qt::blue;
}

在构造函数中,设置widget的Qt::WA_StaticContents属性,以表明widget的内容以左上角为起点,且在widget尺寸变化时保持不变。Qt使用该属性优化尺寸变化时的paint事件。这仅仅是一种优化,且只用于内容为静态并起始于左上角的widget。

1
2
3
4
5
6
7
8
9
10
11
12
13
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()函数进行重绘。
1
2
3
4
5
6
7
8
9
10
11
12
bool ScribbleArea::saveImage(const QString &fileName, const char *fileFormat)
{
QImage visibleImage = image;
resizeImage(&visibleImage, size());

if (visibleImage.save(fileName, fileFormat)) {
modified = false;
return true;
} else {
return false;
}
}
saveImage()函数创建一个QImage对象,该对象仅覆盖实际image的可见部分 ,并使用QImage::save()函数保存。如果图像保存成功,因为没有未保存的数据,绘图区modified变量被设置为false
1
2
3
4
5
6
7
8
9
void ScribbleArea::setPenColor(const QColor &newColor)
{
myPenColor = newColor;
}

void ScribbleArea::setPenWidth(int newWidth)
{
myPenWidth = newWidth;
}
setPenColor()setPenWidth()函数设置当前画笔的颜色和宽度。这些值会在后面的绘图操作中使用。
1
2
3
4
5
6
void ScribbleArea::clearImage()
{
image.fill(qRgb(255, 255, 255));
modified = true;
update();
}
公有clearImage()槽函数清除在绘图区中显示的图像。该函数简单地用RGB值为(255,255,255)的白色填充整个图像。和以前一样,当图像修改时,设置modifiedtrue 并重绘。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ScribbleArea::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
lastPoint = event->pos();
scribbling = true;
}
}

void ScribbleArea::mouseMoveEvent(QMouseEvent *event)
{
if ((event->buttons() & Qt::LeftButton) && scribbling)
drawLineTo(event->pos());
}

void ScribbleArea::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && scribbling) {
drawLineTo(event->pos());
scribbling = false;
}
}
对于鼠标按下和鼠标释放事件,QMouseEvent::button()函数可以判断是哪个按键触发的该事件。对于鼠标移动事件,我们使用QMouseEvent::buttons()函数来查找当前按住的按键(即“或”组合)。

如果用户按下鼠标左键,鼠标位置存储于lastPoint中。同时做一个标记:用户正在绘画。(这里必须有scribbling变量,因为我们不能确定鼠标移动和鼠标释放事件是否是在同一个widget上完成。)

如果用户用左键移动鼠标按下或释放按钮时,调用私有drawLineTo()函数进行绘制。

1
2
3
4
5
6
void ScribbleArea::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QRect dirtyRect = event->rect();
painter.drawImage(dirtyRect, image, dirtyRect);
}
在重写的paintEvent()函数中,我们简单地为绘图区域创建一个QPainter对象,并绘制图像。

在这一点,可能会有疑问,在paintEvent()函数中为什么不直接在widget上进行绘制,而是绘制在一个QImage对象上并复制该QImage对象到屏幕上。理由有三点:

  • 窗口系统要求widget组件能够随时进行重绘。例如,窗口最小化后恢复原状态时,窗口系统会清空widget上的内容,并发送一个paint事件。换句话说,不能依赖于窗口系统记住程序内绘制的图片内容。
  • 一般情况下,Qt禁止在paintEvent()函数外进行绘制工作。特别需要注意,不能在鼠标事件处理函数中进行paint(绘制)工作。(虽然可以使用widget的Qt::WA_PaintOnScreen的属性改变上述行为。)
  • 如果初始化正确,QImage对象可以保证每个颜色通道(红,绿,蓝,和α)使用8位的色深,而QWidget对象则可能只有较低的色深,这取决于显示器的配置。这意味着,如果载入一个24位或32位图像,并将其绘制到QWidget对象上 ,然后复制该QWidget到QImage时,图像会失去一些信息。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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);
    }
    当用户启动Scribble程序时,会产生一个resize事件,同时一个image对象会被创建并显示在绘图区。我们使这个初始图像大小比应用程序的主窗口和绘图区稍大些,这可以避免用户在调整主窗口大小时,图像大小的频繁调整(这会非常低效)。但当主窗口比该初始尺寸大时,图像必须重新调整大小。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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()函数中,绘制一条从上一次鼠标按下到鼠标移动事件发生时鼠标位置的直线,同时设置modifiedtrue,并创建一个重绘事件,然后更新lastPoint,这样下一次调用drawLineTo()函数时,绘图将继续下去。

update()函数可以不带参数调用(注:不带参数执行会进行整个窗口的重绘,这会带来闪烁的问题),一个简单的优化方法是,传递一个绘图区内需要更新的QRect对象进行更新,这可以避免widget的完整重绘(防止闪烁)。

1
2
3
4
5
6
7
8
9
10
11
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没有很好的调整图像大小API函数。利用QImage::copy()函数,可以完成这个功能,但在扩大图像大小时,空白区域会使用黑色背景进行填充,而这里希望使用的是白色背景。

所以,在这里创建一个尺寸合适的新QImage对象并使用白色背景进行填充,并使用QPainter将原来的图像绘制到这个新的QImage对象上。新图像使用QImage::Format_RGB32格式,即每个象素会被存储为0xffRRGGBB(其中RR,GG和BB是红色,绿色和蓝色通道,ff为十六进制值255)。

打印由print()槽函数处理:

1
2
3
4
5
6
void ScribbleArea::print()
{
#if !defined(QT_NO_PRINTER) && !defined(QT_NO_PRINTDIALOG)
QPrinter printer(QPrinter::HighResolution);

QPrintDialog printDialog(&printer, this);
首先为要求输出的格式创建一个高解析度的QPrinter对象,使用QPrintDialog提示用户指定一个页面尺寸和输出格式。

如果对话框返回accepted,打印任务会在绘制设备上执行:

1
2
3
4
5
6
7
8
9
10
11
    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_NO_PRINTER
}
用这种方式进行图像打印,简单来说,就是在QPrinter上进行绘图。在绘制到绘图设备前,需要将图像缩放到页面可提供的空间大小。

MainWindow类的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class MainWindow : public QMainWindow
{
Q_OBJECT

public:
MainWindow();

protected:
void closeEvent(QCloseEvent *event) Q_DECL_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()函数用于检测是否有未保存的更改。如果有,则提示用户保存更改。如果用户点击取消 ,则该函数数返回falsesaveFile()函数让用户保存当前显示在绘图区中的图像。

MainWindow类的实现

1
2
3
4
5
6
7
8
9
10
11
MainWindow::MainWindow()
{
scribbleArea = new ScribbleArea;
setCentralWidget(scribbleArea);

createActions();
createMenus();

setWindowTitle(tr("Scribble"));
resize(500, 500);
}

在构造函数中,首先创建一个绘图区作为MainWindow的中央widget(central widget)。然后创建相关的动作(actions)和菜单。

1
2
3
4
5
6
7
8
void MainWindow::closeEvent(QCloseEvent *event)
{
if (maybeSave()) {
event->accept();
} else {
event->ignore();
}
}
当用户希望关闭程序时,一般通常单击文件|退出或单击X标题栏按钮进行,此时程序会向widget发送close事件。通过重写事件处理函数,可以拦截应用程序的关闭事件。

在这个例子中,close事件用于提示用户保存所有未保存的更改。主要逻辑在maybeSave()函数中实现。如果maybeSave()返回true,表明没有修改或用户已成功保存,则接受该事件。应用程序可以正常终止。如果maybeSave()返回false,用户点击了取消 ,则“忽略”该事件,程序仍然正常运行不受影响。

1
2
3
4
5
6
7
8
9
void MainWindow::open()
{
if (maybeSave()) {
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open File"), QDir::currentPath());
if (!fileName.isEmpty())
scribbleArea->openImage(fileName);
}
}
open()槽函数中,在新的图像载入到绘图区前,首先提示用户保存当前显示图片中所有未保存的修改。然后提示用户选择一个文件,进而架在该文件至ScribbleArea 。
1
2
3
4
5
6
void MainWindow::save()
{
QAction *action = qobject_cast<QAction *>(sender());
QByteArray fileFormat = action->data().toByteArray();
saveFile(fileFormat);
}
save()槽函数在用户选择另存为菜单项时被调用,然后在格式菜单中选择适当的格式。首先需要做的第一件事就是找出哪一个action使用QObject::sender发出的信号。该函数以QObject指针的形式返回发送者。既然发送者是一个动作的对象,因此可以安全的将该指针转换为QObject (因为QAction派生于QObject类)。可以使用C语言风格或C++风格的static_cast<>()进行强制类型转换,但作为一个安全编程技术,这里使用qobject_cast()进行该类型转换。用这个方法的优点是,如果该对象是错误的类型,会返回一个空指针。空指针造成的程序崩溃比不安全的类型转换造成的程序崩溃更加易于诊断。

一旦获得了发送的action,可通过的QAction::data()提取选定的格式。(当创建actions时,使用QAction::setData()以QVariant类型设置附加到该action上的自定义数据。更多内容,在createActions()函数中讨论

现在,已经获得了存储格式,那么可以调用私有的saveFile()函数保存当前显示的图像。

1
2
3
4
5
6
void MainWindow::penColor()
{
QColor newColor = QColorDialog::getColor(scribbleArea->penColor());
if (newColor.isValid())
scribbleArea->setPenColor(newColor);
}
penColor()槽函数返回用户通过QColorDialog选取的新颜色 。如果用户选择了新颜色,则将其作为绘图区的颜色。
1
2
3
4
5
6
7
8
9
10
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类提供了一种简单的对话框获得用户输入的单个值。这里使用包含QLabel和QSpinBox 的静态QInputDialog::getInt()函数。QSpinBox使用绘图区笔宽进行初始化,允许的范围内从1至50,步进值为1(即以1为向上和向下箭头递增或递减的值)。

如果用户点击OK,布尔型的ok变量被设为true,如果用户点击Cancel,则该变量被设为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()槽函数用于创建一个描述本例程设计的消息框。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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()));
}
createAction()函数中,创建各个菜单项并把它们连接到相应的槽函数中。特别是创建另存为菜单子菜单中的action。QImageWriter::supportedImageFormats()函数来获取支持格式的列表(返回类型为QList<QByteArray>对象)。

然后遍历该列表,为每种格式创建一个action。进而调用QAction::setData()函数设置文件格式,以便在调用QAction::data()函数时返回文件格式。当然,也可以通过截去action文本中的“...”获得文件格式,只是有点笨。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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);
}
createMenu()函数中,saveAs添加了上面已创建的格式actions 。然后,向文件,选项和帮助菜单添加剩下的actions,以及saveAs菜单的子菜单

QMenu类在菜单栏、下文菜单和弹出菜单中提供一个菜单widget。QMenuBar类提供一个具有下拉列表的QMenu菜单的水平菜单栏。最后,将文件和选项菜单置于MainWindow的菜单栏,该菜单栏可通过QMainWindow::menuBar()函数获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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作为文件格式;用户可以随时按取消 ,并使用其他格式保存文件。

maybeSave()函数返回false,如果用户点击取消;否则返回true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
} else {
return scribbleArea->saveImage(fileName, fileFormat.constData());
}
}
saveFile()函数弹出一个带有建议文件名的文件对话框。静态QFileDialog::GetSaveFileName ()函数返回由用户选择的文件名。该文件并非一定要存在。

文件列表:

widgets/scribble/mainwindow.cpp widgets/scribble/mainwindow.h widgets/scribble/scribblearea.cpp widgets/scribble/scribblearea.h widgets/scribble/main.cpp widgets/scribble/scribble.pro

本文来自:[Qt文档翻译]Scribble例程