绘制路径示例
绘制路径示例展示了如何使用绘制路径构建复杂的图形进行渲染。
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 XML 所知道的颜色名称相对应的项填充给定的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
widget 的数量。
窗口类的实现
在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 此外,ASP.NET 还提供了 () 方便函数,它可以将给定的矩形作为封闭子路径添加到路径中。添加的矩形是一组顺时针方向的线条。添加矩形后,画家路径的当前位置是矩形的左上角。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() 函数来构建椭圆路径。首先,我们移动当前点,开始一条新路径。然后调用QPainterPath::arcTo() 函数,起始角度为 0.0,最后一个参数为 360.0 度,创建一个椭圆。
同样,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() 函数,就可以使用QPainterPath 构建相当复杂的图形。
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()
函数会在给定的组合框中填充与静态QColor::colorNames() 函数提供的 Qt XML 所知道的颜色名称相对应的项。
QVariant Window::currentItemData(QComboBox *comboBox) { return comboBox->itemData(comboBox->currentIndex()); }
currentItemData()
函数只是简单地返回给定组合框的当前项。
渲染区域类定义
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
widget 的相关绘制路径。此外,我们还重新实现了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
部件,并初始化钢笔宽度和旋转角度。我们还设置了部件background role ;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(); }
各种公共槽通过设置相关属性来更新RenderArea
widget 的绘制路径,并调用QWidget::update() 函数,强制使用新的渲染首选项重新绘制 widget。
QWidget::update() 槽不会立即重新绘制;相反,它会调度一个绘制事件,以便在 Qt XML 返回主事件循环时进行处理。
void RenderArea::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);
绘制事件是重新绘制全部或部分窗口部件的请求。paintEvent() 函数是一个事件处理程序,可以通过重新实现来接收部件的绘制事件。我们重新实现该事件处理程序,以呈现RenderArea
widget 的绘制路径。
首先,我们为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
部件当前的宽度和高度除以 100 来缩放坐标系。
现在,当我们确定绘制路径的大小合适后,就可以平移坐标系,使绘制路径围绕RenderArea
widget 的中心旋转。完成旋转后,我们必须记得再次将坐标系平移回来。
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
widget 的填充颜色设置其颜色。最后,我们设置QPainter 的笔刷(渐变会自动转换为QBrush ),并使用QPainter::drawPath() 函数绘制RenderArea
widget 的绘制路径。
© 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.