摘要:
在 QGraphicsView 框架下,多个形状组合为 QPainterPath 时常出现填充不完整、内部区域点击不响应的问题。本文通过调整填充规则(奇偶规则/环绕规则)与各子形状的绘制方向(顺/逆时针),使两者匹配,从而解决上述问题;对于方向固定的弧线,采用负角度绘制或 toReversed() 反向路径加以解决,并给出验证实验。
关键词:
QPainterPath;QGraphicsView;填充规则;奇偶规则;环绕规则;顺逆时针;WindingFill;OddEvenFill;toReversed;路径方向
版本:Qt 5.14.2
问题描述:
在QGraphicsView框架下,组合多种形状的QPainterPath,
- 在填充颜色时,没有完全填充;
- 鼠标点击某些内部区域时,没有响应鼠标事件;
- 对于某些没有闭合的曲线,上述问题同样存在。
解决思路:
调整QPainterPath填充规则(fillRule),以及各个子形状或者路径的绘制方向(顺时针或逆时针)
Qt提供两种填充规则:奇偶规则(Qt::OddEvenFill)、环绕规则(Qt::WindingFill),默认为奇偶规则
- 奇偶规则:使用这个规则时,我们通过以下方法判断一个点是否在形状内部:从该点画一条水平线到形状外部,然后数交点的数量。如果交点数是奇数,说明该点在形状内部。
- 环绕规则:根据这个规则,我们通过以下方法判断一个点是否在形状内部:从该点画一条水平线到形状外部的位置,确定线每个交点的方向是向上还是向下。通过将每个交点方向相加来确定环绕数。如果这个数不为零,说明该点在形状内部。在大多数情况下,这种填充模式也可以看作是闭合形状的交集。


数据准备:
点击折叠或展开代码
void Form::initGraphics() { m_scene = new QGraphicsScene(this); ui->graphicsView->setScene(m_scene); // 绘制底层直线,测试填充效果 QGraphicsLineItem *line = new QGraphicsLineItem; line->setLine(-1000, 50, 1000, 50); line->setZValue(-100); m_scene->addItem(line); QGraphicsLineItem *line2 = new QGraphicsLineItem; line2->setLine(-1000, 180, 1000, 180); line2->setZValue(-100); m_scene->addItem(line2); } QGraphicsPathItem *Form::createPathItem(const QPainterPath &path, const QPointF &pos, const QString &label) { QGraphicsPathItem *item = new QGraphicsPathItem; item->setPath(path); item->setPos(pos); item->setFlag(QGraphicsItem::ItemIsMovable, true); item->setBrush(Qt::green); m_scene->addItem(item); QGraphicsSimpleTextItem *text = new QGraphicsSimpleTextItem; text->setText(label); text->setParentItem(item); text->setPos(0, -20); return item; }
代码说明:
initGraphics()中,创建画布QGraphicsScene,准备两条水平线;createPathItem方法中,创建图形,添加形状或者路径,设置位置和填充颜色,设置文字标签显示方便区分;
示例代码:
点击折叠或展开代码
// 创建正方形 QPolygonF Form::makeSquarePolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(100, 0) << QPointF(100, 100) << QPointF(0, 100); else p << QPointF(0, 0) << QPointF(0, 100) << QPointF(100, 100) << QPointF(100, 0); p << p.first(); return p; } // 创建三角形 QPolygonF Form::makeTrianglePolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(100, 0) << QPointF(50, 100); else p << QPointF(0, 0) << QPointF(50, 100) << QPointF(100, 0); p << p.first(); return p; } // 分别创建顺、逆时针的正方形和三角形,横向排列 void Form::squareCwTriangleCw() { QPainterPath path; path.addPolygon(makeSquarePolygon(true)); path.addPolygon(makeTrianglePolygon(true)); path.setFillRule(Qt::OddEvenFill); m_item1 = createPathItem(path, QPointF(0, 0), "SqCW_TrCW"); } void Form::squareCwTriangleCcw() { QPainterPath path; path.addPolygon(makeSquarePolygon(true)); path.addPolygon(makeTrianglePolygon(false)); path.setFillRule(Qt::OddEvenFill); m_item2 = createPathItem(path, QPointF(130, 0), "SqCW_TrCCW"); } void Form::squareCcwTriangleCw() { QPainterPath path; path.addPolygon(makeSquarePolygon(false)); path.addPolygon(makeTrianglePolygon(true)); path.setFillRule(Qt::OddEvenFill); m_item3 = createPathItem(path, QPointF(260, 0), "SqCCW_TrCW"); } void Form::squareCcwTriangleCcw() { QPainterPath path; path.addPolygon(makeSquarePolygon(false)); path.addPolygon(makeTrianglePolygon(false)); path.setFillRule(Qt::OddEvenFill); m_item4 = createPathItem(path, QPointF(390, 0), "SqCCW_TrCCW"); } // 变换填充规则并刷新 void Form::applyFillRule(Qt::FillRule rule) { QList list; list << m_item1 << m_item2 << m_item3 << m_item4; for (QGraphicsPathItem *item : qAsConst(list)) { QPainterPath path = item->path(); path.setFillRule(rule); item->setPath(path); } m_scene->update(); } // 用两个按钮来切换填充规则 void Form::onOddEvenClicked() { applyFillRule(Qt::OddEvenFill); } void Form::onWindingClicked() { applyFillRule(Qt::WindingFill); } // 测试代码 Form::Form(QWidget *parent) : QWidget(parent), ui(new Ui::Form) { ui->setupUi(this); // 创建画布及水平线 initGraphics(); // 创建顺逆时针的正方形和三角形 squareCwTriangleCw(); squareCwTriangleCcw(); squareCcwTriangleCw(); squareCcwTriangleCcw(); // 连接按钮信号,切换填充规则 connect(ui->pushButton_oddeven, &QPushButton::clicked, this, &Form::onOddEvenClicked); connect(ui->pushButton_winding, &QPushButton::clicked, this, &Form::onWindingClicked); }
代码说明:
makeSquarePolygon创建正方形,makeTrianglePolygon创建三角形;squareXXTriangleXX方法创建顺逆时针的正方形和三角形;注意,在框架中,向下为Y轴正方向;applyFillRule修改填充规则,通过两个按钮来改变;
效果演示:
奇偶规则:
环绕规则:

其他问题:
面对其他不规则图形,绘制方向是固定的,比如Qt中的弧线,默认从3点开始逆时针绘制,如何解决?
思路:
- 使用负数角度,可以顺时针绘制;
- 使用
QPainterPath的toReversed方法,逆向路径绘制;
示例代码:
点击折叠或展开代码
// 创建矩形 QPolygonF Form::makeRectPolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(200, 0) << QPointF(200, 100) << QPointF(0, 100); else p << QPointF(0, 0) << QPointF(0, 100) << QPointF(200, 100) << QPointF(200, 0); p << p.first(); return p; } // 创建弧线 QPainterPath Form::makeArcPath(bool reversed) { QPainterPath arc; arc.moveTo(200, 50); arc.arcTo(QRectF(0, 0, 200, 100), 0, 270); if (reversed) arc = arc.toReversed(); return arc; } // 创建顺逆时针的矩形和弧线 void Form::rectCwArc() { QPainterPath path; path.addPolygon(makeRectPolygon(true)); path.addPath(makeArcPath(false)); path.setFillRule(Qt::OddEvenFill); m_item5 = createPathItem(path, QPointF(0, 130), "RectCW_Arc"); } void Form::rectCwArcReverse() { QPainterPath path; path.addPolygon(makeRectPolygon(true)); path.addPath(makeArcPath(true)); path.setFillRule(Qt::OddEvenFill); m_item6 = createPathItem(path, QPointF(230, 130), "RectCW_ArcReverse"); } void Form::rectCcwArc() { QPainterPath path; path.addPolygon(makeRectPolygon(false)); path.addPath(makeArcPath(false)); path.setFillRule(Qt::OddEvenFill); m_item7 = createPathItem(path, QPointF(460, 130), "RectCCW_Arc"); } void Form::rectCcwArcReverse() { QPainterPath path; path.addPolygon(makeRectPolygon(false)); path.addPath(makeArcPath(true)); path.setFillRule(Qt::OddEvenFill); m_item8 = createPathItem(path, QPointF(690, 130), "RectCCW_ArcReverse"); } void Form::applyFillRule(Qt::FillRule rule) { QList list; list << m_item1 << m_item2 << m_item3 << m_item4 << m_item5 << m_item6 << m_item7 << m_item8; for (QGraphicsPathItem *item : qAsConst(list)) { QPainterPath path = item->path(); path.setFillRule(rule); item->setPath(path); } m_scene->update(); }
代码说明:
makeRectPolygon方法创建矩形,makeArcPath方法创建一个四分之三圆的弧线;rectXXXArcXXX方法分别绘制顺逆时针的矩形、正反向绘制的弧线;
效果演示
奇偶规则:

环绕规则:

附录一:参考文献
附录二:完整代码
- form.h
点击折叠或展开代码
#ifndef FORM_H #define FORM_H #include #include #include namespace Ui { class Form; } class Form : public QWidget { Q_OBJECT public: explicit Form(QWidget *parent = nullptr); ~Form(); private: Ui::Form *ui; QGraphicsScene *m_scene = nullptr; QGraphicsPathItem *m_item1 = nullptr; QGraphicsPathItem *m_item2 = nullptr; QGraphicsPathItem *m_item3 = nullptr; QGraphicsPathItem *m_item4 = nullptr; QGraphicsPathItem *m_item5 = nullptr; QGraphicsPathItem *m_item6 = nullptr; QGraphicsPathItem *m_item7 = nullptr; QGraphicsPathItem *m_item8 = nullptr; void initGraphics(); QGraphicsPathItem *createPathItem(const QPainterPath &path, const QPointF &pos, const QString &label); QPolygonF makeSquarePolygon(bool clockwise); QPolygonF makeRectPolygon(bool clockwise); QPolygonF makeTrianglePolygon(bool clockwise); QPainterPath makeArcPath(bool reversed); void squareCwTriangleCw(); void squareCwTriangleCcw(); void squareCcwTriangleCw(); void squareCcwTriangleCcw(); void rectCwArc(); void rectCwArcReverse(); void rectCcwArc(); void rectCcwArcReverse(); void applyFillRule(Qt::FillRule rule); void onOddEvenClicked(); void onWindingClicked(); }; #endif // FORM_H
- form.cpp
点击折叠或展开代码
#include "form.h" #include "ui_form.h" Form::Form(QWidget *parent) : QWidget(parent), ui(new Ui::Form) { ui->setupUi(this); initGraphics(); squareCwTriangleCw(); squareCwTriangleCcw(); squareCcwTriangleCw(); squareCcwTriangleCcw(); rectCwArc(); rectCwArcReverse(); rectCcwArc(); rectCcwArcReverse(); connect(ui->pushButton_oddeven, &QPushButton::clicked, this, &Form::onOddEvenClicked); connect(ui->pushButton_winding, &QPushButton::clicked, this, &Form::onWindingClicked); } Form::~Form() { delete ui; } void Form::initGraphics() { m_scene = new QGraphicsScene(this); ui->graphicsView->setScene(m_scene); // 绘制底层直线,测试填充效果 QGraphicsLineItem *line = new QGraphicsLineItem; line->setLine(-1000, 50, 1000, 50); line->setZValue(-100); m_scene->addItem(line); QGraphicsLineItem *line2 = new QGraphicsLineItem; line2->setLine(-1000, 180, 1000, 180); line2->setZValue(-100); m_scene->addItem(line2); } QGraphicsPathItem *Form::createPathItem(const QPainterPath &path, const QPointF &pos, const QString &label) { QGraphicsPathItem *item = new QGraphicsPathItem; item->setPath(path); item->setPos(pos); item->setFlag(QGraphicsItem::ItemIsMovable, true); item->setBrush(Qt::green); m_scene->addItem(item); QGraphicsSimpleTextItem *text = new QGraphicsSimpleTextItem; text->setText(label); text->setParentItem(item); text->setPos(0, -20); return item; } QPolygonF Form::makeSquarePolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(100, 0) << QPointF(100, 100) << QPointF(0, 100); else p << QPointF(0, 0) << QPointF(0, 100) << QPointF(100, 100) << QPointF(100, 0); p << p.first(); return p; } QPolygonF Form::makeRectPolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(200, 0) << QPointF(200, 100) << QPointF(0, 100); else p << QPointF(0, 0) << QPointF(0, 100) << QPointF(200, 100) << QPointF(200, 0); p << p.first(); return p; } QPolygonF Form::makeTrianglePolygon(bool clockwise) { QPolygonF p; if (clockwise) p << QPointF(0, 0) << QPointF(100, 0) << QPointF(50, 100); else p << QPointF(0, 0) << QPointF(50, 100) << QPointF(100, 0); p << p.first(); return p; } QPainterPath Form::makeArcPath(bool reversed) { QPainterPath arc; arc.moveTo(200, 50); arc.arcTo(QRectF(0, 0, 200, 100), 0, 270); if (reversed) arc = arc.toReversed(); return arc; } void Form::squareCwTriangleCw() { QPainterPath path; path.addPolygon(makeSquarePolygon(true)); path.addPolygon(makeTrianglePolygon(true)); path.setFillRule(Qt::OddEvenFill); m_item1 = createPathItem(path, QPointF(0, 0), "SqCW_TrCW"); } void Form::squareCwTriangleCcw() { QPainterPath path; path.addPolygon(makeSquarePolygon(true)); path.addPolygon(makeTrianglePolygon(false)); path.setFillRule(Qt::OddEvenFill); m_item2 = createPathItem(path, QPointF(130, 0), "SqCW_TrCCW"); } void Form::squareCcwTriangleCw() { QPainterPath path; path.addPolygon(makeSquarePolygon(false)); path.addPolygon(makeTrianglePolygon(true)); path.setFillRule(Qt::OddEvenFill); m_item3 = createPathItem(path, QPointF(260, 0), "SqCCW_TrCW"); } void Form::squareCcwTriangleCcw() { QPainterPath path; path.addPolygon(makeSquarePolygon(false)); path.addPolygon(makeTrianglePolygon(false)); path.setFillRule(Qt::OddEvenFill); m_item4 = createPathItem(path, QPointF(390, 0), "SqCCW_TrCCW"); } void Form::rectCwArc() { QPainterPath path; path.addPolygon(makeRectPolygon(true)); path.addPath(makeArcPath(false)); path.setFillRule(Qt::OddEvenFill); m_item5 = createPathItem(path, QPointF(0, 130), "RectCW_Arc"); } void Form::rectCwArcReverse() { QPainterPath path; path.addPolygon(makeRectPolygon(true)); path.addPath(makeArcPath(true)); path.setFillRule(Qt::OddEvenFill); m_item6 = createPathItem(path, QPointF(230, 130), "RectCW_ArcReverse"); } void Form::rectCcwArc() { QPainterPath path; path.addPolygon(makeRectPolygon(false)); path.addPath(makeArcPath(false)); path.setFillRule(Qt::OddEvenFill); m_item7 = createPathItem(path, QPointF(460, 130), "RectCCW_Arc"); } void Form::rectCcwArcReverse() { QPainterPath path; path.addPolygon(makeRectPolygon(false)); path.addPath(makeArcPath(true)); path.setFillRule(Qt::OddEvenFill); m_item8 = createPathItem(path, QPointF(690, 130), "RectCCW_ArcReverse"); } void Form::applyFillRule(Qt::FillRule rule) { QList list; list << m_item1 << m_item2 << m_item3 << m_item4 << m_item5 << m_item6 << m_item7 << m_item8; for (QGraphicsPathItem *item : qAsConst(list)) { QPainterPath path = item->path(); path.setFillRule(rule); item->setPath(path); } m_scene->update(); } void Form::onOddEvenClicked() { applyFillRule(Qt::OddEvenFill); } void Form::onWindingClicked() { applyFillRule(Qt::WindingFill); }