画家路径示例

画家路径示例展示了如何使用画家路径来构建用于渲染的复杂形状。

QPainterPath 类提供了一个绘制操作的容器,能够构建和重复使用图形形状。

画家路径由多个图形构建块(如矩形、椭圆、线和曲线)组成,可用于填充、轮廓和裁剪。与普通绘图操作相比,画家路径的主要优势是复杂形状只需要创建一次,但可以通过对 QPainter::drawPath() 的调用多次进行绘制。

该示例包含两个类

  • RenderArea 类,它是一个自定义小部件,用于显示一个画家路径。
  • Window 类,它是应用程序的主窗口,显示几个 RenderArea 小部件,并允许用户操作画家路径的填充、画笔、颜色和旋转角度。

首先,我们将审查 Window 类,然后我们将看看 RenderArea 类。

窗口类定义

Window 类从 QWidget 继承而来,是应用程序的主窗口,显示几个 RenderArea 小部件,并允许用户操作画家路径的填充、画笔、颜色和旋转角度。

class Window : public QWidget
{
    Q_OBJECT

public:
    Window();

private slots:
    void fillRuleChanged();
    void fillGradientChanged();
    void penColorChanged();

我们声明了三个私有槽来响应用户关于填充和颜色的输入: fillRuleChanged()fillGradientChanged()penColorChanged()

当用户更改画笔宽度和旋转角度时,新值将直接通过 QSpinBox::valueChanged() 信号传递给 RenderArea 小部件。我们必须实现更新填充和颜色的槽的原因是,QComboBox 不提供类似将新值作为参数传递的信号;因此,在能够更新 RenderArea 小部件之前,我们需要检索新值或值。

private:
    void populateWithColors(QComboBox *comboBox);
    QVariant currentItemData(QComboBox *comboBox);

我们还声明了一些私有便利函数:populateWithColors() 将与 Qt 知道的颜色名称对应的项填充到给定的 QComboBox 中,而 currentItemData() 返回给定 QComboBox 的当前项。

    QList<RenderArea*> renderAreas;
    QLabel *fillRuleLabel;
    QLabel *fillGradientLabel;
    QLabel *fillToLabel;
    QLabel *penWidthLabel;
    QLabel *penColorLabel;
    QLabel *rotationAngleLabel;
    QComboBox *fillRuleComboBox;
    QComboBox *fillColor1ComboBox;
    QComboBox *fillColor2ComboBox;
    QSpinBox *penWidthSpinBox;
    QComboBox *penColorComboBox;
    QSpinBox *rotationAngleSpinBox;
};

然后,我们声明了主窗口小部件的各个组件。我们还声明了一个便利常量,指定了 RenderArea 小部件的数量。

窗口类实现

Window 构造函数中,我们定义了各种画家路径并创建了相应的 RenderArea 小部件,它们将渲染图形形状

Window::Window()
{
    QPainterPath rectPath;
    rectPath.moveTo(20.0, 30.0);
    rectPath.lineTo(80.0, 30.0);
    rectPath.lineTo(80.0, 70.0);
    rectPath.lineTo(20.0, 70.0);
    rectPath.closeSubpath();

我们使用QPainterPath::moveTo()和QPainterPath::lineTo()函数构建一个带锐角的矩形。

QPainterPath::moveTo()将当前点移动到参数点。画家路径是由多个图形构建块组成的对象,即子路径。移动当前点也将开始一个新的子路径(在开始新的路径时隐式地关闭先前当前路径)。QPainterPath::lineTo()函数从当前点到给定的终点添加一条直线。线条绘制后,当前点更新为线条的终点。

我们首先移动当前点开始一个新的子路径,并绘制矩形的三个侧面。然后我们调用QPainterPath::closeSubpath()函数,该函数绘制一条线到当前子路径的起点。关闭当前子路径后,将自动开始一个新的子路径。新路径的当前点是(0, 0)。我们也可以使用QPainterPath::lineTo()来绘制最后一条线,然后使用QPainterPath::moveTo()函数显式地开始一个新的子路径。

QPainterPath还提供了QPainterPath::addRect()方便函数,将给定的矩形作为闭合子路径添加到路径中。矩形以顺时针线段集的形式添加。在矩形添加后,画家的当前位置在矩形的右上角。

    QPainterPath roundRectPath;
    roundRectPath.moveTo(80.0, 35.0);
    roundRectPath.arcTo(70.0, 30.0, 10.0, 10.0, 0.0, 90.0);
    roundRectPath.lineTo(25.0, 30.0);
    roundRectPath.arcTo(20.0, 30.0, 10.0, 10.0, 90.0, 90.0);
    roundRectPath.lineTo(20.0, 65.0);
    roundRectPath.arcTo(20.0, 60.0, 10.0, 10.0, 180.0, 90.0);
    roundRectPath.lineTo(75.0, 70.0);
    roundRectPath.arcTo(70.0, 60.0, 10.0, 10.0, 270.0, 90.0);
    roundRectPath.closeSubpath();

然后我们使用带圆角的矩形。和之前一样,我们使用QPainterPath::moveTo()和QPainterPath::lineTo()函数来绘制矩形的侧面。为了创建圆角,我们使用QPainterPath::arcTo()函数。

QPainterPath::arcTo()创建一个占据给定矩形(通过QRect或矩形的坐标)的弧,以给定起始角度开始,顺时针扩展给定的度数。角度以度为单位。可以使用负角度指定顺时针弧。如果当前点未与弧的起始点相连,则函数将连接当前点。

    QPainterPath ellipsePath;
    ellipsePath.moveTo(80.0, 50.0);
    ellipsePath.arcTo(20.0, 30.0, 60.0, 40.0, 0.0, 360.0);

我们还使用QPainterPath::arcTo()函数来构建椭圆路径。首先,我们将当前点移动到一个新的路径上。然后我们使用起始角度0.0和最后一参数360.0度调用QPainterPath::arcTo(),创建一个椭圆。

同样地,QPainterPath提供了一个方便函数(QPainterPath::addEllipse),它在一个给定的边界矩形内创建一个椭圆,并将其添加到画家路径中。如果在当前子路径关闭后,将开始一个新的子路径。椭圆由顺时针的曲线组成,起始和结束在零度(3点钟位置)。

    QPainterPath piePath;
    piePath.moveTo(50.0, 50.0);
    piePath.arcTo(20.0, 30.0, 60.0, 40.0, 60.0, 240.0);
    piePath.closeSubpath();

在构建饼图路径时,我们继续使用提到的函数组合:首先移动当前点,开始一个新的子路径。然后从图表中心创建一条线到弧,以及弧本身。当我们关闭子路径时,我们将隐式地构建回到图表中心的最后一条线。

    QPainterPath polygonPath;
    polygonPath.moveTo(10.0, 80.0);
    polygonPath.lineTo(20.0, 10.0);
    polygonPath.lineTo(80.0, 30.0);
    polygonPath.lineTo(90.0, 70.0);
    polygonPath.closeSubpath();

构建多边形与构建矩形等效。

QPainterPath还提供了一个方便函数QPainterPath::addPolygon,它将给定的多边形作为新的子路径添加到路径中。在多边形添加后的当前位置是多边形的最后一个点。

    QPainterPath groupPath;
    groupPath.moveTo(60.0, 40.0);
    groupPath.arcTo(20.0, 20.0, 40.0, 40.0, 0.0, 360.0);
    groupPath.moveTo(40.0, 40.0);
    groupPath.lineTo(40.0, 80.0);
    groupPath.lineTo(80.0, 80.0);
    groupPath.lineTo(80.0, 40.0);
    groupPath.closeSubpath();

然后我们创建一个由多个子路径组成的路径:首先移动当前点,并使用 QPainterPath::arcTo() 函数创建一个圆,起始角度为 0.0,最后一个参数使用 360 度,就像我们在创建椭圆路径时做的那样。然后我们再次移动当前点,开始一个新的子路径,并使用 QPainterPath::lineTo() 函数构建正方形的三边。

现在,当我们调用 QPainterPath::closeSubpath() 函数时,最后一边被创建。请记住,QPainterPath::closeSubpath() 函数通过绘制一条线到 当前 子路径的开始,即正方形。

QPainterPath 提供了一个便利函数,QPainterPath::addPath(),它将给定的路径添加到调用该函数的路径中。

    QPainterPath textPath;
    QFont timesFont("Times", 50);
    timesFont.setStyleStrategy(QFont::ForceOutline);
    textPath.addText(10, 70, timesFont, tr("Qt"));

在创建文本路径时,我们首先创建字体。然后我们设置字体的样式策略,它告诉字体匹配算法应使用何种类型的字体来找到一个合适的基本家族。QFont::ForceOutline 强制使用轮廓字体。

为了构造文本,我们使用 QPainterPath::addText() 函数,该函数将给定的文本作为一个由提供的字体生成的封闭子路径集添加到路径中。子路径的位置使得文本的基线左侧位于指定的点上。

    QPainterPath bezierPath;
    bezierPath.moveTo(20, 30);
    bezierPath.cubicTo(80, 0, 50, 50, 80, 80);

要创建贝塞尔路径,我们使用 QPainterPath::cubicTo() 函数,该函数在当前点与给定的终点之间添加一个带有给定控制点的贝塞尔曲线。添加曲线后,当前点更新为曲线的终点。

在这种情况下,我们省略了关闭子路径,因此我们只有一个简单的曲线。但仍然有一条逻辑线从曲线的终点回到子路径的开始;在填充路径时它会变得可见,正如可以在应用程序的主窗口中看到的那样。

    QPainterPath starPath;
    starPath.moveTo(90, 50);
    for (int i = 1; i < 5; ++i) {
        starPath.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),
                        50 + 40 * std::sin(0.8 * i * M_PI));
    }
    starPath.closeSubpath();

我们构建的最终路径显示,您可以使用仅提到的先前函数 QPainterPath::moveTo(),QPainterPath::lineTo() 和 QPainterPath::closeSubpath() 来构建相当复杂的形状。

    renderAreas.push_back(new RenderArea(rectPath));
    renderAreas.push_back(new RenderArea(roundRectPath));
    renderAreas.push_back(new RenderArea(ellipsePath));
    renderAreas.push_back(new RenderArea(piePath));
    renderAreas.push_back(new RenderArea(polygonPath));
    renderAreas.push_back(new RenderArea(groupPath));
    renderAreas.push_back(new RenderArea(textPath));
    renderAreas.push_back(new RenderArea(bezierPath));
    renderAreas.push_back(new RenderArea(starPath));

现在我们已创建了所需的所有绘图路径,我们为每个路径创建相应的 RenderArea 小部件。最后,我们使用 Q_ASSERT() 宏确保渲染区域数量正确。

    fillRuleComboBox = new QComboBox;
    fillRuleComboBox->addItem(tr("Odd Even"), Qt::OddEvenFill);
    fillRuleComboBox->addItem(tr("Winding"), Qt::WindingFill);

    fillRuleLabel = new QLabel(tr("Fill &Rule:"));
    fillRuleLabel->setBuddy(fillRuleComboBox);

然后我们创建了与绘图路径填充规则相关联的小部件。

Qt 中有两种可用的填充规则:Qt::OddEvenFill 规则通过从点绘制一条水平线到一个位于形状外部的位置来决定一个点是否在形状内部,并计算交点的数量。如果交点的数量是奇数,则点在形状内部。这是默认规则。

Qt::WindingFill 规则通过从点绘制一条水平线到一个位于形状外部的位置来决定一个点是否在形状内部。然后它确定在每个交点处线的方向是向上还是向下。通过计算每个交点的方向来决定转向数。如果该数非零,则点在形状内部。

Qt::WindingFill 规则通常可以被视为封闭形状的交点。

    fillColor1ComboBox = new QComboBox;
    populateWithColors(fillColor1ComboBox);
    fillColor1ComboBox->setCurrentIndex(fillColor1ComboBox->findText("mediumslateblue"));

    fillColor2ComboBox = new QComboBox;
    populateWithColors(fillColor2ComboBox);
    fillColor2ComboBox->setCurrentIndex(fillColor2ComboBox->findText("cornsilk"));

    fillGradientLabel = new QLabel(tr("&Fill Gradient:"));
    fillGradientLabel->setBuddy(fillColor1ComboBox);

    fillToLabel = new QLabel(tr("to"));
    fillToLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);

    penWidthSpinBox = new QSpinBox;
    penWidthSpinBox->setRange(0, 20);

    penWidthLabel = new QLabel(tr("&Pen Width:"));
    penWidthLabel->setBuddy(penWidthSpinBox);

    penColorComboBox = new QComboBox;
    populateWithColors(penColorComboBox);
    penColorComboBox->setCurrentIndex(penColorComboBox->findText("darkslateblue"));

    penColorLabel = new QLabel(tr("Pen &Color:"));
    penColorLabel->setBuddy(penColorComboBox);

    rotationAngleSpinBox = new QSpinBox;
    rotationAngleSpinBox->setRange(0, 359);
    rotationAngleSpinBox->setWrapping(true);
    rotationAngleSpinBox->setSuffix(QLatin1String("\xB0"));

    rotationAngleLabel = new QLabel(tr("&Rotation Angle:"));
    rotationAngleLabel->setBuddy(rotationAngleSpinBox);

我们还创建了与填充、画笔和旋转角度相关的其他小部件。

    connect(fillRuleComboBox, &QComboBox::activated,
            this, &Window::fillRuleChanged);
    connect(fillColor1ComboBox, &QComboBox::activated,
            this, &Window::fillGradientChanged);
    connect(fillColor2ComboBox, &QComboBox::activated,
            this, &Window::fillGradientChanged);
    connect(penColorComboBox, &QComboBox::activated,
            this, &Window::penColorChanged);

    for (RenderArea *area : std::as_const(renderAreas)) {
        connect(penWidthSpinBox, &QSpinBox::valueChanged,
                area, &RenderArea::setPenWidth);
        connect(rotationAngleSpinBox, &QSpinBox::valueChanged,
                area, &RenderArea::setRotationAngle);
    }

我们将组合框的 activated() 信号连接到 Window 类相关插槽,同时将旋转框的 valueChanged() 信号直接连接到相应 RenderArea 小部件的插槽。

    QGridLayout *topLayout = new QGridLayout;

    int i = 0;
    for (RenderArea *area : std::as_const(renderAreas)) {
        topLayout->addWidget(area, i / 3, i % 3);
        ++i;
    }

    QGridLayout *mainLayout = new QGridLayout;
    mainLayout->addLayout(topLayout, 0, 0, 1, 4);
    mainLayout->addWidget(fillRuleLabel, 1, 0);
    mainLayout->addWidget(fillRuleComboBox, 1, 1, 1, 3);
    mainLayout->addWidget(fillGradientLabel, 2, 0);
    mainLayout->addWidget(fillColor1ComboBox, 2, 1);
    mainLayout->addWidget(fillToLabel, 2, 2);
    mainLayout->addWidget(fillColor2ComboBox, 2, 3);
    mainLayout->addWidget(penWidthLabel, 3, 0);
    mainLayout->addWidget(penWidthSpinBox, 3, 1, 1, 3);
    mainLayout->addWidget(penColorLabel, 4, 0);
    mainLayout->addWidget(penColorComboBox, 4, 1, 1, 3);
    mainLayout->addWidget(rotationAngleLabel, 5, 0);
    mainLayout->addWidget(rotationAngleSpinBox, 5, 1, 1, 3);
    setLayout(mainLayout);

我们将 RenderArea 小部件添加到一个单独的布局中,然后将它添加到主布局中,与其它小部件一起。

    fillRuleChanged();
    fillGradientChanged();
    penColorChanged();
    penWidthSpinBox->setValue(2);

    setWindowTitle(tr("Painter Paths"));
}

最后,我们通过调用 fillRuleChanged()fillGradientChanged()penColorChanged() 插槽来初始化 RenderArea 小部件,并设置初始笔宽和窗口标题。

void Window::fillRuleChanged()
{
    Qt::FillRule rule = (Qt::FillRule)currentItemData(fillRuleComboBox).toInt();

    for (RenderArea *area : std::as_const(renderAreas))
        area->setFillRule(rule);
}

void Window::fillGradientChanged()
{
    QColor color1 = qvariant_cast<QColor>(currentItemData(fillColor1ComboBox));
    QColor color2 = qvariant_cast<QColor>(currentItemData(fillColor2ComboBox));

    for (RenderArea *area : std::as_const(renderAreas))
        area->setFillGradient(color1, color2);
}

void Window::penColorChanged()
{
    QColor color = qvariant_cast<QColor>(currentItemData(penColorComboBox));

    for (RenderArea *area : std::as_const(renderAreas))
        area->setPenColor(color);
}

私有空槽被实现以从相关组合框检索新的值或数据,并更新渲染区域小部件。

首先,我们使用私有 currentItemData() 函数和 qvariant_cast() 模板函数确定新值或值。然后调用每个 RenderArea 小部件的相关槽来更新画家路径。

void Window::populateWithColors(QComboBox *comboBox)
{
    const QStringList colorNames = QColor::colorNames();
    for (const QString &name : colorNames)
        comboBox->addItem(name, QColor(name));
}

populateWithColors() 函数使用 Qt 已知的颜色名称填充给定的组合框,这些名称由静态 QColor::colorNames() 函数提供。

QVariant Window::currentItemData(QComboBox *comboBox)
{
    return comboBox->itemData(comboBox->currentIndex());
}

currentItemData() 函数仅仅返回给定组合框的当前项目。

RenderArea 类定义

RenderArea 类继承自 QWidget,是一个自定义小部件,用于显示单个画家路径。

class RenderArea : public QWidget
{
    Q_OBJECT

public:
    explicit RenderArea(const QPainterPath &path, QWidget *parent = nullptr);

    QSize minimumSizeHint() const override;
    QSize sizeHint() const override;

public slots:
    void setFillRule(Qt::FillRule rule);
    void setFillGradient(const QColor &color1, const QColor &color2);
    void setPenWidth(int width);
    void setPenColor(const QColor &color);
    void setRotationAngle(int degrees);

protected:
    void paintEvent(QPaintEvent *event) override;

我们声明了几个用于更新 RenderArea 小部件相关画家路径的公共槽。此外,我们重新实现了 QWidget::minimumSizeHint() 和 QWidget::sizeHint() 函数,以使 RenderArea 小部件在我们的应用程序中具有合理的尺寸,并重新实现了 QWidget::paintEvent() 事件处理器以绘制其画家路径。

private:
    QPainterPath path;
    QColor fillColor1;
    QColor fillColor2;
    int penWidth;
    QColor penColor;
    int rotationAngle;
};

RenderArea 类的每个实例都有一个 QPainterPath、几个填充颜色、笔宽、笔颜色和一个旋转角度。

RenderArea 类实现

构造函数接收一个 QPainterPath 作为参数(除了可选的 QWidget 父对象)

RenderArea::RenderArea(const QPainterPath &path, QWidget *parent)
    : QWidget(parent), path(path)
{
    penWidth = 1;
    rotationAngle = 0;
    setBackgroundRole(QPalette::Base);
}

在构造函数中,我们使用 QPainterPath 参数初始化 RenderArea 小部件,以及初始化笔宽和旋转角度。我们还设置了小部件的背景角色;通常情况下,QPalette::Base 是白色的。

QSize RenderArea::minimumSizeHint() const
{
    return QSize(50, 50);
}

QSize RenderArea::sizeHint() const
{
    return QSize(100, 100);
}

然后,我们重新实现了 QWidget::minimumSizeHint() 和 QWidget::sizeHint() 函数,使 RenderArea 小部件在我们的应用程序中有合理的尺寸。

void RenderArea::setFillRule(Qt::FillRule rule)
{
    path.setFillRule(rule);
    update();
}

void RenderArea::setFillGradient(const QColor &color1, const QColor &color2)
{
    fillColor1 = color1;
    fillColor2 = color2;
    update();
}

void RenderArea::setPenWidth(int width)
{
    penWidth = width;
    update();
}

void RenderArea::setPenColor(const QColor &color)
{
    penColor = color;
    update();
}

void RenderArea::setRotationAngle(int degrees)
{
    rotationAngle = degrees;
    update();
}

各种公共槽通过设置相关属性并调用 QWidget::update() 函数来更新 RenderArea 小部件的画家路径,强制小部件以新的渲染偏好进行重绘。

QWidget::update() 槽不会导致立即重绘;而不是,它在 Qt 返回主事件循环时安排一个绘制事件进行处理。

void RenderArea::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

绘制事件是请求重绘小部件的所有或部分。paintEvent() 函数是一个事件处理器,可以被重新实现以接收小部件的重绘事件。我们重新实现事件处理器以绘制 RenderArea 小部件的画家路径。

首先,我们为 RenderArea 实例创建一个 QPainter,并设置绘画器的渲染提示。使用 QPainter::RenderHints 来指定对 QPainter 的标志,这些标志可能会被任何特定引擎尊重或忽略。 QPainter::Antialiasing 表示如果可能,引擎应该对原型的边缘进行抗锯齿处理,即围绕原始像素放置额外的像素以平滑边缘。

    painter.scale(width() / 100.0, height() / 100.0);
    painter.translate(50.0, 50.0);
    painter.rotate(-rotationAngle);
    painter.translate(-50.0, -50.0);

然后,我们缩放 QPainter 的坐标系统,以确保将绘画路径渲染在正确的尺寸,即当应用程序调整大小时,它与 RenderArea 小部件一起增长。当构造各种画家路径时,它们都在一个宽度为 100 像素的正方形内渲染,等同于 RenderArea::sizeHint()。使用 QPainter::scale() 函数将坐标系统缩放为 RenderArea 小部件当前宽度和高度的 1/100。

现在,当我们确信画家路径具有正确的尺寸时,我们可以将坐标系统平移,使画家路径围绕 RenderArea 小部件的中心旋转。执行旋转后,我们必须记住再次将坐标系统平移回来。

    painter.setPen(QPen(penColor, penWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    QLinearGradient gradient(0, 0, 0, 100);
    gradient.setColorAt(0.0, fillColor1);
    gradient.setColorAt(1.0, fillColor2);
    painter.setBrush(gradient);
    painter.drawPath(path);
}

然后,我们使用实例的渲染首选项设置 QPainter 的笔。我们创建了一个 QLinearGradient 并设置其颜色,与 RenderArea 小部件的填充颜色相对应。最后,我们设置 QPainter 的画笔(渐变自动转换为 QBrush),然后使用 QPainter::drawPath() 函数绘制 RenderArea 小部件的画家路径。

示例项目 @ code.qt.io

© 2024 Qt 公司有限公司。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可的。Qt 和相应的标志是芬兰和/或其他国家的 Qt 公司的商标。所有其他商标均为各自所有者的财产。