Skip to content

Latest commit

 

History

History
1010 lines (749 loc) · 43 KB

File metadata and controls

1010 lines (749 loc) · 43 KB

四、画家与 2D 图形

本章涵盖的主题列表如下:

  • 在屏幕上绘制基本形状
  • 将形状导出到可缩放矢量图形 ( SVG )文件
  • 坐标变换
  • 在屏幕上显示图像
  • 将图像效果应用于图形
  • 创建基本的绘画程序
  • 在 QML 渲染 2D 画布

介绍

在本章中,我们将学习如何用 Qt 在屏幕上渲染 2D 图形。在内部,Qt 使用一个名为 QPainter 的低级类在主窗口上渲染它的小部件。Qt 允许我们访问和使用QPainter类来绘制矢量图形、文本、2D 图像,甚至三维图形。

您可以使用QPainter类来创建自己的自定义小部件,或者创建严重依赖于渲染计算机图形的程序,如视频游戏、照片编辑器和 3D 建模工具。

技术要求

本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10

本章使用的所有代码均可从本章 GitHub 资源库下载,网址为:https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-第二版/tree/master/Chapter04

查看以下视频,查看正在运行的代码:http://bit.ly/2FrVYeq

在屏幕上绘制基本形状

在本节中,我们将学习如何使用QPainter类绘制简单的矢量形状(直线、矩形、圆形等)并在主窗口上显示文本。我们还将学习如何使用QPen类更改这些矢量形状的绘制样式。

怎么做…

让我们按照这里列出的步骤在 Qt 窗口中显示基本形状:

  1. 首先,让我们创建一个新的Qt Widgets Application项目。
  2. 打开mainwindow.ui并移除菜单栏、主工具栏和状态栏对象,这样我们就得到一个干净、空的主窗口。右键单击工具栏小部件,并从弹出菜单中选择删除菜单栏:

  1. 然后,打开mainwindow.h文件,添加以下代码以包含QPainter头文件:
#include <QMainWindow>
#include <QPainter>
  1. 然后,声明类析构函数下面的paintEvent()事件处理程序:
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void paintEvent(QPaintEvent *event);
  1. 接下来,打开mainwindow.cpp文件,定义paintEvent()事件处理程序:
void MainWindow::paintEvent(QPaintEvent *event) {}
  1. 之后,我们将使用paintEvent()事件处理程序中的QPainter类向屏幕添加文本。在屏幕上(20, 30)位置绘制文本之前,我们先设置文本字体设置:
QPainter textPainter;
textPainter.begin(this);
textPainter.setFont(QFont("Times", 14, QFont::Bold));
textPainter.drawText(QPoint(20, 30), "Testing");
textPainter.end();
  1. 然后,我们画一条从(50, 60)开始,到(100, 100)结束的直线:
QPainter linePainter;
linePainter.begin(this);
linePainter.drawLine(QPoint(50, 60), QPoint(100, 100));
linePainter.end();
  1. 我们也可以通过使用QPainter类调用drawRect()函数来轻松绘制矩形。但是,这一次,我们还在绘制形状之前对其应用背景图案:
QPainter rectPainter;
rectPainter.begin(this);
rectPainter.setBrush(Qt::BDiagPattern);
rectPainter.drawRect(QRect(40, 120, 80, 30));
rectPainter.end();
  1. 接下来,声明一个QPen类,将其颜色设置为红色,并将其绘制样式设置为Qt::DashDotLine。然后将QPen类应用于QPainter,在(80, 200)处画一个水平半径为50、垂直半径为20的椭圆:
QPen ellipsePen;
ellipsePen.setColor(Qt::red);
ellipsePen.setStyle(Qt::DashDotLine);

QPainter ellipsePainter;
ellipsePainter.begin(this);
ellipsePainter.setPen(ellipsePen);
ellipsePainter.drawEllipse(QPoint(80, 200), 50, 20);
ellipsePainter.end();
  1. 我们也可以使用QPainterPath类来定义一个形状,然后将其传递给QPainter类进行渲染:
QPainterPath rectPath;
rectPath.addRect(QRect(150, 20, 100, 50));

QPainter pathPainter;
pathPainter.begin(this);
pathPainter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));
pathPainter.setBrush(Qt::yellow);
pathPainter.drawPath(rectPath);
pathPainter.end();
  1. 您也可以使用QPainterPath绘制任何其他形状,例如椭圆:
QPainterPath ellipsePath;
ellipsePath.addEllipse(QPoint(200, 120), 50, 20);

QPainter ellipsePathPainter;
ellipsePathPainter.begin(this);
ellipsePathPainter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
ellipsePathPainter.setBrush(QColor(122, 163, 39));
ellipsePathPainter.drawPath(ellipsePath);
ellipsePathPainter.end();
  1. QPainter也可以用来在屏幕上绘制一个图像文件。在以下示例中,我们加载了一个名为tux.png的图像文件,并将其绘制在屏幕上的(100, 150)位置:
QImage image;
image.load("tux.png");

QPainter imagePainter(this);
imagePainter.begin(this);
imagePainter.drawImage(QPoint(100, 150), image);
imagePainter.end();
  1. 最终结果应该如下所示:

它是如何工作的...

如果你想用QPainter在屏幕上画一些东西,你只需要告诉它应该画什么类型的图形(如文本、矢量形状、图像、多边形)和想要的位置和大小。QPen类决定了图形的轮廓应该是什么样的,例如它的颜色、线宽、线条样式(实线、虚线或虚线)、帽样式、连接样式等等。另一方面,QBrush设置图形背景的样式,如背景颜色、图案(纯色、渐变、密集画笔和交叉对角线)和位图。

图形的选项应该在调用绘制函数之前设置(如drawLine()drawRect()drawEllipse())。如果您的图形没有出现在屏幕上,并且您在 Qt Creator 中的应用输出窗口上看到诸如 QPainter::setPen: Painter 未激活和 QPainter::setBrush: Painter 未激活之类的警告,这意味着QPainter类当前未激活,并且您的程序不会触发其 paint 事件。要解决这个问题,请将主窗口设置为QPainter类的父窗口。通常,如果你在mainwindow.cpp文件中写代码,你所需要做的就是在初始化QPainter时把这个放在括号中。例如,请注意以下几点:

QPainter linePainter(this);

QImage可以从计算机目录和程序资源加载图像。

还有更多…

QPainter想象成一个拿着笔和空画布的机器人。你只需要告诉机器人它应该画什么类型的形状以及它在画布上的位置,然后机器人就会根据你的描述来完成它的工作。为了让您的生活更轻松, QPainter 类还提供了众多功能,例如drawArc()drawEllipse()drawLine()drawRect()drawPie(),让您可以轻松渲染预定义的形状。在 Qt 中,所有的小部件类(包括主窗口)都有一个名为QWidget::paintEvent()的事件处理程序。每当操作系统认为主窗口应该重新绘制小部件时,就会触发这个事件处理程序。很多事情可以导致这个决定,比如主窗口被缩放,一个小部件改变它的状态(也就是一个按钮被按下),或者像repaint()update()这样的功能在代码中被手动调用。在决定是否在同一组条件下触发更新事件时,不同的操作系统可能会有不同的行为。如果您正在制作一个需要持续一致的图形更新的程序,请使用计时器手动调用repaint()update()

将形状导出到 SVG 文件

SVG 是一种基于 XML 的语言,用于描述二维矢量图形。Qt 提供了将矢量形状保存为 SVG 文件的类。此功能可用于创建类似于 Adobe Illustrator 和 Inkscape 的简单矢量图形编辑器。在下一个示例中,我们将继续使用上一个示例中的相同项目文件。

怎么做…

让我们学习如何创建一个在屏幕上显示 SVG 图形的简单程序:

  1. 首先,让我们通过右键单击层次窗口上的主窗口小部件并从弹出菜单中选择创建菜单栏选项来创建菜单栏。之后,在菜单栏中添加一个文件选项,并在它下面添加一个另存为 SVG 操作:

  1. 之后,您将在 Qt 创建器窗口底部的操作编辑器窗口中看到一个名为 actionSave_as_SVG 的项目。右键单击该项目,然后从弹出菜单中选择“转到插槽...”。现在将出现一个窗口,其中包含可用于特定操作的插槽列表。选择默认信号,称为触发(),然后单击确定按钮:

  1. 单击确定按钮后,Qt 创建者将切换到脚本编辑器。你会意识到一个名为on_actionSave_as_SVG_triggered()的槽已经被自动添加到你的主窗口类中。在您的mainwindow.h文件的底部,您将看到如下内容:
void MainWindow::on_actionSave_as_SVG_triggered() {}
  1. 当您单击菜单栏中的另存为 SVG 选项时,将调用前面的函数。我们将在这个函数中编写代码,将所有矢量图形保存到一个 SVG 文件中。为此,我们需要首先在源文件的顶部包含一个名为QSvgGenerator的类头。这个头非常重要,因为它是生成 SVG 文件所必需的。然后,我们还需要包含另一个名为QFileDialog的类头,用于打开保存对话框:
#include <QtSvg/QSvgGenerator>
#include <QFileDialog>
  1. 我们还需要将svg模块添加到我们的项目文件中,如下所示:
QT += core gui svg
  1. 然后,在mainwindow.h文件内创建一个名为paintAll()的新函数,如下代码所示:
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void paintEvent(QPaintEvent *event);
    void paintAll(QSvgGenerator *generator = 0);
  1. 之后,在mainwindow.cpp文件中,将所有代码从paintEvent()移到paintAll()功能。然后,用一个单一的、统一的QPainter来替换所有单独的QPainter对象,以绘制所有图形。另外,在绘制任何东西之前调用begin()功能,在完成绘制之后调用end()功能。代码应该如下所示:
void MainWindow::paintAll(QSvgGenerator *generator) {
    QPainter painter;
    if (engine)
        painter.begin(engine);
    else
        painter.begin(this);
    painter.setFont(QFont("Times", 14, QFont::Bold));
    painter.drawText(QPoint(20, 30), "Testing");
    painter.drawLine(QPoint(50, 60), QPoint(100, 100));
    painter.setBrush(Qt::BDiagPattern);
    painter.drawRect(QRect(40, 120, 80, 30));
  1. 我们接着创建ellipsePenrectPath:
    QPen ellipsePen;
    ellipsePen.setColor(Qt::red);
    ellipsePen.setStyle(Qt::DashDotLine);
    painter.setPen(ellipsePen);
    painter.drawEllipse(QPoint(80, 200), 50, 20);

    QPainterPath rectPath;
    rectPath.addRect(QRect(150, 20, 100, 50));
    painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));
    painter.setBrush(Qt::yellow);
    painter.drawPath(rectPath);
  1. 然后,我们继续创建ellipsePathimage:
    QPainterPath ellipsePath;
    ellipsePath.addEllipse(QPoint(200, 120), 50, 20);
    painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
    painter.setBrush(QColor(122, 163, 39));
    painter.drawPath(ellipsePath);

    QImage image;
    image.load("tux.png");
    painter.drawImage(QPoint(100, 150), image);
    painter.end();
}
  1. 由于我们已经将所有代码从paintEvent()移动到了paintAll(),我们现在将调用paintEvent()中的paintAll()函数,如下所示:
void MainWindow::paintEvent(QPaintEvent *event) {
    paintAll();
}
  1. 然后,我们将编写用于将图形导出为 SVG 文件的代码。代码将被写入由 Qt 生成的名为on_actionSave_as_SVG_triggered()的槽函数中。我们首先调用保存文件对话框,并从用户那里获得具有所需文件名的目录路径:
void MainWindow::on_actionSave_as_SVG_triggered() {
    QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)");
    if (filePath == "")
        return;
}
  1. 之后,创建一个QSvgGenerator对象并将图形保存到一个 SVG 文件中,方法是将QSvgGenerator对象传递到paintAll()函数:
void MainWindow::on_actionSave_as_SVG_triggered() {
    QString filePath = QFileDialog::getSaveFileName(this, "Save SVG", "", "SVG files (*.svg)");
    if (filePath == "")
        return;
    QSvgGenerator generator;
    generator.setFileName(filePath);
    generator.setSize(QSize(this->width(), this->height()));
    generator.setViewBox(QRect(0, 0, this->width(), this->height()));
    generator.setTitle("SVG Example");
    generator.setDescription("This SVG file is generated by Qt.");
    paintAll(&generator);
 }
  1. 现在编译并运行程序,您应该可以通过转到文件|另存为 SVG 来导出图形:

它是如何工作的...

默认情况下,QPainter将使用其父对象的绘制引擎来绘制分配给它的图形。如果没有给QPainter分配任何父级,可以手动给它分配一个绘制引擎,这就是我们在这个例子中所做的。

我们之所以将代码放入paintAll()是因为我们希望将相同的代码重用于两个不同的目的:在窗口上显示图形和将图形导出到一个 SVG 文件。请注意,paintAll()函数中生成器变量的默认值设置为0,这意味着除非指定,否则不需要QSvgGenerator对象来运行该函数。稍后,在paintAll()功能中,我们检查生成器对象是否存在。如果它确实存在,请将其用作油漆工的绘画引擎,如以下代码所示:

if (engine)
    painter.begin(engine);
else
    painter.begin(this);

否则,将主窗口传递给begin()函数(因为我们是在mainwindow.cpp文件中编写代码,所以可以直接用这个来引用主窗口的指针),这样它就会使用主窗口本身的绘制引擎,也就是说图形会被绘制到主窗口的表面上。在本例中,需要使用单个QPainter对象将图形保存到 SVG 文件中。如果您使用多个QPainter对象,生成的 SVG 文件将包含多个 XML 头定义,因此该文件将被任何图形编辑器软件视为无效。

QFileDialog::getSaveFileName()将打开本机保存文件对话框,供用户选择保存目录并设置所需的文件名。一旦用户完成该操作,完整路径将作为字符串返回,我们将能够将该信息传递给QSvgGenerator对象以导出图形。

请注意,在之前的截图中,SVG 文件中的企鹅已经被裁剪了。这是因为 SVG 的画布大小被设置为跟随主窗口的大小。为了帮助可怜的企鹅找回它的身体,在导出 SVG 文件之前,请将窗口放大。

还有更多…

SVG 以 XML 格式定义图形。由于它是矢量图形的一种形式,如果放大或调整大小,SVG 文件不会失去任何质量。SVG 格式不仅允许您在工作文件中存储矢量图形,还允许您存储光栅图形和文本,这或多或少类似于 Adobe Illustrator 的格式。SVG 还允许您将图形对象分组、设置样式、转换和合成到以前渲染的对象中。

You can check out the full specification of SVG graphics at https://www.w3.org/TR/SVG.

坐标变换

在本例中,我们将学习如何使用坐标转换和计时器来创建实时时钟显示。

怎么做…

要创建我们的第一个图形时钟显示,让我们按照以下步骤操作:

  1. 首先,创建一个新的Qt Widgets Application项目。然后,打开mainwindow.ui并像之前一样移除菜单栏、主工具栏和状态栏。
  2. 之后,打开mainwindow.h文件,包括以下标题:
#include <QTime>
#include <QTimer>
#include <QPainter>
  1. 然后,声明paintEvent()函数,如是:
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void paintEvent(QPaintEvent *event);
  1. mainwindow.cpp文件中,创建三个数组来存储时针、分针和秒针的形状,其中每个数组包含三组坐标:
void MainWindow::paintEvent(QPaintEvent *event) {
    static const QPoint hourHand[3] = {
        QPoint(4, 4),
        QPoint(-4, 4),
        QPoint(0, -40)
    };
    static const QPoint minuteHand[3] = {
        QPoint(4, 4),
        QPoint(-4, 4),
        QPoint(0, -70)
    };
    static const QPoint secondHand[3] = {
        QPoint(2, 2),
        QPoint(-2, 2),
        QPoint(0, -90)
    };
}
  1. 之后,在数组下面添加以下代码来创建画师,并将其移动到主窗口的中心。此外,我们调整了画师的大小,使其非常适合主窗口,即使在调整窗口大小时也是如此:
int side = qMin(width(), height());
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(width() / 2, height() / 2);
painter.scale(side / 250.0, side / 250.0);
  1. 完成后,我们将使用for循环开始绘制表盘。每个刻度盘旋转6度,因此60刻度盘将完成一整圈。此外,每隔5分钟的表盘看起来会稍长一些:
for (int i = 0; i < 60; ++ i) {
    if ((i % 5) != 0)
        painter.drawLine(92, 0, 96, 0);
    else
        painter.drawLine(86, 0, 96, 0);
    painter.rotate(6.0);
}
  1. 然后,我们继续画时钟的指针。每只手的旋转都是根据当前时间和其各自在360度上的等效位置来计算的:
QTime time = QTime::currentTime();

// Draw hour hand
painter.save();
painter.rotate((time.hour() * 360) / 12);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(hourHand, 3);
painter.restore();
  1. 让我们继续画时钟的分针:
// Draw minute hand
painter.save();
painter.rotate((time.minute() * 360) / 60);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(minuteHand, 3);
painter.restore();
  1. 然后,我们还画了几秒钟的手:
// Draw second hand
painter.save();
painter.rotate((time.second() * 360) / 60);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawConvexPolygon(secondHand, 3);
painter.restore();
  1. 最后但同样重要的是,创建一个计时器来每秒刷新图形,以便程序像真正的时钟一样工作:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
    QTimer* timer = new QTimer(this);
    timer->start(1000);
    connect(timer, QTimer::timeout, this, MainWindow::update);
}
  1. 现在编译并运行程序,您应该会看到如下内容:

它是如何工作的...

每个数组包含三个QPoint数据实例,它们形成一个细长三角形的形状。然后将数组传递给绘制者,并使用drawConvexPolygon()函数将其渲染为凸多边形。在绘制每个时钟指针之前,我们使用painter.save()保存QPainter对象的状态,然后使用坐标变换继续绘制指针。

一旦完成绘制,我们通过调用painter.restore()将绘制者恢复到其先前的状态。该功能将撤销painter. restore()之前的所有转换,这样下一个时针将不会继承上一个时针的转换。在不使用painter.save()painter.restore()的情况下,我们将不得不在绘制下一只手之前手动改变位置、旋转和缩放。

不使用painter.save()painter.restore()的一个很好的例子是在绘制表盘时。由于每个表盘的旋转比前一个增加了 6 度,我们根本不需要保存画师的状态。我们只需要循环调用painter.rotate(6.0),每个表盘将继承前一个表盘的旋转。我们还使用模数运算符(%)来检查刻度盘所代表的单位是否可以除以 5。如果可以的话,我们把它画得稍微长一点。

不使用定时器不断调用update()槽,时钟将无法正常工作。这是因为当父小部件的状态没有变化时paintEvent()不会被 Qt 调用,在这种情况下,父小部件是主窗口。因此,我们需要手动告诉 Qt,我们需要通过每秒调用update() 来刷新图形。我们使用painter.setRenderHint(QPainter::Antialiasing)功能在渲染时钟时启用抗锯齿。如果没有抗锯齿,图形将看起来非常参差不齐和像素化:

还有更多…

QPainter类在屏幕上渲染图形之前,使用坐标系来确定图形的位置和大小。可以改变这些信息,使图形出现在不同的位置、旋转和大小。这个改变图形坐标信息的过程就是我们所说的坐标变换。有几种类型的转换;其中包括平移、旋转、缩放和剪切:

Qt 使用的坐标系原点位于左上角,这意味着 x 值向右增加,而 y 值向下增加。该坐标系可能与物理设备(如计算机屏幕)使用的坐标系不同。Qt 通过使用QPaintDevice类自动处理这个问题,该类将 Qt 的逻辑坐标映射到物理坐标。

QPainter提供四种变换操作来执行不同类型的变换:

  • QPainter::translate():将图形的位置偏移一组给定的单位
  • QPainter::rotate():顺时针方向围绕原点旋转图形
  • QPainter::scale():以给定的因子偏移图形的大小
  • QPainter::shear():围绕原点扭曲图形的坐标系

在屏幕上显示图像

Qt 不仅允许我们在屏幕上绘制形状和图像,还允许我们将多个图像叠加在一起,并使用不同类型的算法组合来自所有图层的像素信息,以创建非常有趣的结果。在这个例子中,我们将学习如何将图像叠加在一起,并对它们应用不同的合成效果。

怎么做…

让我们按照以下步骤创建一个简单的演示,展示不同图像合成的效果:

  1. 首先,建立一个新的Qt Widgets Application项目,删除菜单栏、主工具栏和状态栏,就像我们在第一个食谱中做的那样。
  2. 接下来,将QPainter类头添加到mainwindow.h文件中:
#include <QPainter>
  1. 之后,声明paintEvent()虚函数,如是:
virtual void paintEvent(QPaintEvent* event);
  1. mainwindow.cpp中,我们将首先使用QImage类加载几个图像文件:
void MainWindow::paintEvent(QPaintEvent* event) {
    QImage image;
    image.load("checker.png");

    QImage image2;
    image2.load("tux.png");

    QImage image3;
    image3.load("butterfly.png");
}
  1. 然后,创建一个QPainter对象,并使用它绘制两对图像,其中一个图像位于另一个图像之上:
QPainter painter(this);
painter.drawImage(QPoint(10, 10), image);
painter.drawImage(QPoint(10, 10), image2);
painter.drawImage(QPoint(300, 10), image);
painter.drawImage(QPoint(300, 40), image3);
  1. 现在编译并运行程序,您应该会看到如下内容:

  1. 接下来,我们将在屏幕上绘制每个图像之前设置合成模式:
QPainter painter(this);
painter.setCompositionMode(QPainter::CompositionMode_Difference);
painter.drawImage(QPoint(10, 10), image);
painter.setCompositionMode(QPainter::CompositionMode_Multiply);
painter.drawImage(QPoint(10, 10), image2);
painter.setCompositionMode(QPainter::CompositionMode_Xor);
painter.drawImage(QPoint(300, 10), image);
painter.setCompositionMode(QPainter::CompositionMode_SoftLight);
painter.drawImage(QPoint(300, 40), image3);
  1. 再次编译并运行该程序,现在您将看到如下内容:

它是如何工作的...

用 Qt 绘制图像时,调用drawImage()函数的顺序将决定哪个图像先渲染,哪个图像后渲染。这将影响图像的深度顺序,并产生不同的结果。 在前面的例子中,我们四次调用drawImage()函数,在屏幕上绘制了四个不同的图像。第一个drawImage()功能渲染checker.png,第二个drawImage()功能渲染tux.png(企鹅)。稍后渲染的图像将总是出现在其他图像的前面,这就是企鹅出现在格子图案前面的原因。右边的蝴蝶和格子也是如此。即使蝴蝶呈现在其前面,您仍然可以看到棋盘的原因是因为蝴蝶图像不是完全不透明的。

现在,让我们反转渲染序列,看看会发生什么。我们将尝试先渲染企鹅,然后渲染格子。右边的另一对图像也是如此:首先渲染蝴蝶,然后是方格框:

要对图像应用构图效果,我们必须在绘制图像之前设置画家的构图模式,方法是调用painter.setCompositionMode()函数。您可以通过键入QPainter::CompositionMode从自动完成菜单中选择所需的合成模式。

在前面的例子中,我们将QPainter::CompositionMode_Difference应用到左边的方格框中,它反转了颜色。接下来,我们将QPainter::CompositionMode_Overlay应用于企鹅,使其与棋盘混合,并且能够看到两个图像相互重叠。在右侧,我们将QPainter::CompositionMode_Xor应用于检查器,如果源和目标之间存在差异,则会显示颜色;否则,它将被渲染为黑色。因为它比较的是白色背景的差异,格子的不透明部分变成了完全黑色。我们还将QPainter::CompositionMode_SoftLight应用于蝴蝶图像。这将像素与对比度降低的背景混合在一起。如果您想在进行下一个渲染之前禁用您刚刚为上一个渲染设置的合成模式,只需将其设置回默认模式,即QPainter::CompositionMode_SourceOver

还有更多…

例如,我们可以将多个图像叠加在一起,并使用 Qt 的图像合成功能将它们合并在一起,并根据我们使用的合成模式计算屏幕上的结果像素。这通常用于图像编辑软件,如 Photoshop 和 GIMP,以合成图像层。 Qt 中有 30 多种作曲模式可供选择。一些最常用的模式如下:

  • Clear:目的地的像素设置为全透明,与源无关。

  • Source:输出为源像素。该模式与CompositionMode_Destination相反。

  • Destination:输出是目的像素。这意味着混合没有效果。该模式与CompositionMode_Source相反。

  • Source Over:这通常被称为阿尔法混合。源的 alpha 用于混合目标顶部的像素。这是QPainter使用的默认模式。

  • Destination Over:输出是源像素之上的目的地阿尔法之间的混合。这种模式的反面是CompositionMode_SourceOver

  • Source In:输出是源,其中 alpha 被目的地的 alpha 缩小。

  • Destination In:输出是目的地,这里的 alpha 被源的 alpha 缩小。该模式与CompositionMode_SourceIn相反。

  • Source Out:输出是源,其中 alpha 被目的地的倒数减少。

  • Destination Out:输出是目的地,在这里阿尔法被源的倒数减少。该模式与CompositionMode_SourceOut相反。

  • Source Atop:源像素混合在目的像素之上,源像素的 alpha 减去目的像素的 alpha。

  • Destination Atop:目的像素混合在源之上,源像素的 alpha 减去目的像素的 alpha。该模式与CompositionMode_SourceAtop相反。

  • Xor:这是 Exclusive OR 的缩写,是一种高级的混合模式,主要用于图像分析。使用这种合成模式要复杂得多。首先,源的阿尔法被目标阿尔法的倒数减少。然后,目的地的α被源α的倒数减少。最后,源和目标被合并以产生输出。

更多,可以访问此链接: pyside.github.io

下图显示了使用不同合成模式叠加两幅图像的结果:

将图像效果应用于图形

Qt 为使用QPainter类绘制的任何图形添加图像效果提供了一种简单的方法。在本例中,我们将学习如何在屏幕上显示图形之前,对图形应用不同的图像效果,如投影、模糊、着色和不透明度效果。

怎么做…

让我们按照以下步骤学习如何将图像效果应用于文本和图形:

  1. 创建一个新的Qt Widgets Application项目,并移除菜单栏、主工具栏和状态栏。

  2. 通过转到文件|新文件或项目并添加项目所需的所有图像来创建新的资源文件:

  1. 接下来,打开mainwindow.ui并在窗口中添加四个标签。其中两个标签是文本,另外两个标签将加载我们刚刚添加到资源文件中的图像:

  1. 您可能已经注意到字体大小比默认大小大得多。这可以通过向标签小部件添加样式表来实现,例如,如下所示:
font: 26pt "MS Shell Dlg 2";
  1. 之后,打开mainwindow.cpp并在源代码顶部包含以下标题:
#include <QGraphicsBlurEffect>
#include <QGraphicsDropShadowEffect>
#include <QGraphicsColorizeEffect>
#include <QGraphicsOpacityEffect>
  1. 然后,在MainWindow类的构造函数中,添加以下代码来创建一个DropShadowEffect,并将其应用于其中一个标签:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
    QGraphicsDropShadowEffect* shadow = new
    QGraphicsDropShadowEffect();
    shadow->setXOffset(4);
    shadow->setYOffset(4);
    ui->label->setGraphicsEffect(shadow);
}
  1. 接下来,我们将创建一个ColorizedEffect并将其应用于其中一个图像,在本例中是蝴蝶。我们还将效果颜色设置为红色:
QGraphicsColorizeEffect* colorize = new QGraphicsColorizeEffect();
colorize->setColor(QColor(255, 0, 0));
ui->butterfly->setGraphicsEffect(colorize);
  1. 完成后,创建一个BlurEffect并将其radius设置为12。然后,将图形效果应用于另一个标签:
QGraphicsBlurEffect* blur = new QGraphicsBlurEffect();
blur->setBlurRadius(12);
ui->label2->setGraphicsEffect(blur);
  1. 最后,创建一个alpha效果并将其应用到penguin图像。我们将opacity值设置为0.2,这意味着 20%的不透明度:
QGraphicsOpacityEffect* alpha = new QGraphicsOpacityEffect();
alpha->setOpacity(0.2);
ui->penguin->setGraphicsEffect(alpha);
  1. 现在编译并运行该程序,您应该能够看到如下内容:

它是如何工作的...

每个图形效果都是自己的类,继承了QGraphicsEffect父类。您可以通过创建一个继承QGraphicsEffect的新类并重新实现其中的一些功能来创建自己的自定义效果。 每个效果都有一组专门为其创建的变量。例如,您可以设置彩色效果的颜色,但模糊效果中没有这样的变量。这是因为每个效果与其他效果有很大的不同,这也是为什么它需要成为自己的一个类,而不是对所有不同的效果使用同一个类。

一次只能向小部件添加一个图形效果。如果您添加了多个效果,只有最后一个效果会应用到小部件,因为它会替换前一个效果。除此之外,请注意,如果您创建一个图形效果,比如投影效果,您也不能将其分配给两个不同的小部件,因为它只会分配给您应用它的最后一个小部件。如果需要将相同类型的效果应用于几个不同的小部件,可以创建几个相同类型的图形效果,并将它们分别应用于各自的小部件。

还有更多…

目前,Qt 支持模糊、阴影、着色和不透明效果。这些效果可以通过调用以下类来使用:QGraphicsBlurEffectQGraphicsDropShadowEffectQGraphicsColorizeEffectQGraphicsOpacityEffect。所有这些类都是从QGraphicsEffect类继承而来的。您也可以通过创建QGrapicsEffect的子类(或任何其他现有效果)并重新实现draw()功能来创建自己的自定义图像效果。

图形效果仅改变源的边框。如果想增加边框的边距,重新实现虚拟boundingRectFor()功能,每当这个矩形发生变化时,调用updateBoundingRect()通知框架。

创建基本的绘画程序

既然我们已经了解了这么多关于QPainter课程以及如何使用它在屏幕上显示图形的知识,我想是时候让我们做一些有趣的事情来将我们的知识付诸实践了。

在这个食谱中,我们将学习如何制作一个基本的绘画程序,允许我们用不同的画笔大小和颜色在画布上绘制线条。我们还将学习如何使用QImage类和鼠标事件来构建绘画程序。

怎么做…

让我们通过以下步骤开始我们有趣的项目:

  1. 同样,我们从创建一个新的Qt Widgets Application项目并移除工具栏和状态栏开始。这次我们将保留菜单栏。
  2. 之后,像这样设置菜单栏:

  1. 我们将暂时保持菜单栏不变,因此让我们进入mainwindow.h文件。首先,包含项目所需的下列头文件:
#include <QPainter>
#include <QMouseEvent>
#include <QFileDialog>
  1. 接下来,声明我们将用于这个项目的变量,如下所示:
private:
    Ui::MainWindow *ui;
    QImage image;
    bool drawing;
    QPoint lastPoint;
    int brushSize;
    QColor brushColor;
  1. 然后,声明从QWidget类继承的事件回调函数。当相应的事件发生时,Qt 将触发这些功能。我们将覆盖这些函数,并告诉 Qt 在调用这些事件时该做什么:
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
    virtual void mousePressEvent(QMouseEvent *event);
    virtual void mouseMoveEvent(QMouseEvent *event);
    virtual void mouseReleaseEvent(QMouseEvent *event);
    virtual void paintEvent(QPaintEvent *event);
    virtual void resizeEvent(QResizeEvent *event);
  1. 之后,转到mainwindow.cpp文件,将以下代码添加到类构造函数中,以设置一些变量:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);
    image = QImage(this->size(), QImage::Format_RGB32);
    image.fill(Qt::white);
    drawing = false;
    brushColor = Qt::black;
    brushSize = 2;
}
  1. 接下来,我们将构造mousePressEvent()事件,并告诉 Qt 在按下鼠标左键时要做什么:
void MainWindow::mousePressEvent(QMouseEvent *event) {
    if (event->button() == Qt::LeftButton) {
        drawing = true;
        lastPoint = event->pos();
    }
}
  1. 然后,我们将构造mouseMoveEvent()事件,并告诉 Qt 当鼠标移动时该做什么。在这种情况下,如果按住鼠标左键,我们希望在画布上绘制线条:
void MainWindow::mouseMoveEvent(QMouseEvent *event) {
    if ((event->buttons() & Qt::LeftButton) && drawing) {
        QPainter painter(&image);
        painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
        painter.drawLine(lastPoint, event->pos());
        lastPoint = event->pos();
        this->update();
    }
}
  1. 之后,我们还将构造mouseReleaseEvent()事件,当鼠标按钮被释放时将被触发:
void MainWindow::mouseReleaseEvent(QMouseEvent *event) {
    if (event->button() == Qt::LeftButton) {
        drawing = false;
    }
}
  1. 完成后,我们将进入paintEvent()事件,与我们在前面章节中看到的其他示例相比,这个事件非常简单:
void MainWindow::paintEvent(QPaintEvent *event) {
    QPainter canvasPainter(this);
    canvasPainter.drawImage(this->rect(), image, image.rect());
}
  1. 还记得我们有一个无所事事的菜单栏吗?让我们右键单击图形用户界面编辑器下面的每个操作,并在弹出菜单中选择转到插槽。我们想告诉 Qt 当菜单栏上的每个选项都被选中时该怎么做:

  1. 然后,选择名为已触发()的默认插槽,并按下确定按钮。Qt 会在你的mainwindow.hmainwindow.cpp文件中自动生成一个新的槽函数。完成所有操作后,您应该会在您的mainwindow.h文件中看到类似以下内容:
private slots:
    void on_actionSave_triggered();
    void on_actionClear_triggered();
    void on_action2px_triggered();
    void on_action5px_triggered();
    void on_action10px_triggered();
    void on_actionBlack_triggered();
    void on_actionWhite_triggered();
    void on_actionRed_triggered();
    void on_actionGreen_triggered();
    void on_actionBlue_triggered();
  1. 接下来,我们将告诉 Qt 当这些槽中的每一个被触发时该做什么:
void MainWindow::on_actionSave_triggered() {
    QString filePath = QFileDialog::getSaveFileName(this, "Save Image", "", "PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)");
    if (filePath == "")
        return;
    image.save(filePath);
}
void MainWindow::on_actionClear_triggered() {
    image.fill(Qt::white);
    this->update();
}
  1. 然后,我们继续实施其他插槽:
void MainWindow::on_action2px_triggered() {
    brushSize = 2;
}
void MainWindow::on_action5px_triggered() {
    brushSize = 5;
}
void MainWindow::on_action10px_triggered() {
    brushSize = 10;
}
void MainWindow::on_actionBlack_triggered() {
    brushColor = Qt::black;
}
  1. 最后,我们实现剩余的槽函数:
void MainWindow::on_actionWhite_triggered() {
    brushColor = Qt::white;
}
void MainWindow::on_actionRed_triggered() {
    brushColor = Qt::red;
}
void MainWindow::on_actionGreen_triggered() {
    brushColor = Qt::green;
}
void MainWindow::on_actionBlue_triggered() {
    brushColor = Qt::blue;
}
  1. 如果我们现在编译并运行程序,我们将得到一个简单但可用的画图程序:

它是如何工作的...

在这个例子中,我们在程序启动时创建了一个QImage小部件。这个小部件充当画布,当窗口调整大小时,它将跟随窗口的大小。为了在画布上绘制一些东西,我们需要使用 Qt 提供的鼠标事件。这些事件将告诉我们光标的位置,我们将能够使用这些信息来改变画布上的像素。

我们使用一个叫做drawing的布尔变量,让程序知道当按下鼠标按钮时是否应该开始绘图。在这种情况下,当按下鼠标左键时,drawing变量将被设置为true。我们还在鼠标左键按下时将当前光标位置保存到lastPoint变量,这样 Qt 就知道应该从哪里开始绘图了。当鼠标移动时,mouseMoveEvent()事件将由 Qt 触发。这是我们需要检查drawing变量是否设置为true的地方。如果是,那么QPainter可以根据我们提供的笔刷设置开始在QImage小部件上画线。画笔设置由brushColorbrushSize **组成。**这些设置被保存为变量,可以通过从菜单栏中选择不同的设置进行更改。

用户在画布上画画时,请记得调用update()功能。否则,即使我们更改了画布的像素信息,画布也会保持空白。当我们从菜单栏中选择文件|清除来重置画布时,我们还必须调用update()功能。

在这个例子中,我们使用QImage::save()来保存图像文件,非常简单明了。我们使用文件对话框让用户决定在哪里保存图像及其所需的文件名。然后,我们把信息传递给QImage,剩下的就由它自己来做了。如果我们没有为QImage::save()函数指定文件格式,QImage将通过查看所需文件名的扩展名来尝试找出它。

在 QML 渲染 2D 画布

在本章前面的所有示例中,我们已经讨论了使用 Qt 的 C++ API 渲染 2D 图形的方法和技术。然而,我们还没有学会如何使用强大的 QML 脚本来实现类似的结果。

怎么做…

在这个项目中,我们将做一些完全不同的事情:

  1. 像往常一样,我们应该做的第一步是通过转到文件|新文件或项目并选择 Qt 快速应用-空作为项目模板来创建一个新项目:

  1. 创建完新项目后,打开main.qml,它列在项目窗格的qml.qrc下。之后,为窗口设置一个标识,并将其widthheight调整为更大的值,如下所示:
import QtQuick 2.11
import QtQuick.Window 2.11

Window {
    id: myWindow
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")
}
  1. 然后,在myWindow下添加一个Canvas对象,称之为myCanvas。之后,我们将其widthheight设为与myWindow相同:
Window {
    id: myWindow
    visible: true
    width: 640
    height: 480
    Canvas {
 id: myCanvas
 width: myWindow.width
 height: myWindow.height
 }
}
  1. 接下来,我们定义onPaint事件被触发时会发生什么;在这种情况下,我们将在窗口上画一个十字:
Canvas {
    id: myCanvas
    width: myWindow.width
    height: myWindow.height
    onPaint: {
 var context = getContext('2d')
 context.fillStyle = 'white'
 context.fillRect(0, 0, width, height)
 context.lineWidth = 2
 context.strokeStyle = 'black'
  1. 让我们继续编写代码,就像这样:
 // Draw cross
 context.beginPath()
 context.moveTo(50, 50)
 context.lineTo(100, 100)
 context.closePath()
 context.stroke()
 context.beginPath()
 context.moveTo(100, 50)
 context.lineTo(50, 100)
 context.closePath()
 context.stroke()
 }
}
  1. 之后,我们添加以下代码在十字旁边画一个勾号:
// Draw tick
context.beginPath()
context.moveTo(150, 90)
context.lineTo(158, 100)
context.closePath()
context.stroke()
context.beginPath()
context.moveTo(180, 100)
context.lineTo(210, 50)
context.closePath()
context.stroke()
  1. 然后,通过添加以下代码绘制一个三角形:
// Draw triangle
context.lineWidth = 4
context.strokeStyle = "red"
context.fillStyle = "salmon"
context.beginPath()
context.moveTo(50,150)
context.lineTo(150,150)
context.lineTo(50,250)
context.closePath()
context.fill()
context.stroke()
  1. 然后,用下面的代码画一个半圆和一个整圆:
// Draw circle
context.lineWidth = 4
context.strokeStyle = "blue"
context.fillStyle = "steelblue"
var pi = 3.141592653589793
context.beginPath()
context.arc(220, 200, 60, 0, pi, true)
context.closePath()
context.fill()
context.stroke()
  1. 然后,我们画一条弧线:
context.beginPath()
context.arc(220, 280, 60, 0, 2 * pi, true)
context.closePath()
context.fill()
context.stroke()
  1. 最后,我们从一个文件中绘制一幅 2D 图像:
// Draw image
context.drawImage("tux.png", 280, 10, 150, 174)
  1. 但是,仅使用前面的代码无法在屏幕上成功渲染图像,因为您还必须事先加载图像文件。在Canvas对象内添加以下代码,要求 QML 在程序启动时加载图像文件,然后在加载图像时调用requestPaint()信号,从而触发onPaint()事件槽:
onImageLoaded: requestPaint();
onPaint: {
    // The code we added previously
}
  1. 然后在项目面板上右键打开qml.qrc,选择在编辑器中打开。之后,将tux.png图像文件添加到我们的项目资源中:

  1. 现在构建并运行程序,您应该会得到以下结果:

这不是很好吗?