• Qt 图形填充规则实战:OddEven vs Winding,顺逆时针如何影响填充与点击检测



    摘要:
        在 QGraphicsView 框架下,多个形状组合为 QPainterPath 时常出现填充不完整、内部区域点击不响应的问题。本文通过调整填充规则(奇偶规则/环绕规则)与各子形状的绘制方向(顺/逆时针),使两者匹配,从而解决上述问题;对于方向固定的弧线,采用负角度绘制或 toReversed() 反向路径加以解决,并给出验证实验。

    关键词:
        QPainterPath;QGraphicsView;填充规则;奇偶规则;环绕规则;顺逆时针;WindingFill;OddEvenFill;toReversed;路径方向

    版本:Qt 5.14.2

    问题描述:

        在QGraphicsView框架下,组合多种形状的QPainterPath,

    • 在填充颜色时,没有完全填充;
    • 鼠标点击某些内部区域时,没有响应鼠标事件;
    • 对于某些没有闭合的曲线,上述问题同样存在。

    解决思路:

        调整QPainterPath填充规则(fillRule),以及各个子形状或者路径的绘制方向(顺时针或逆时针)
        Qt提供两种填充规则:奇偶规则(Qt::OddEvenFill)、环绕规则(Qt::WindingFill),默认为奇偶规则

    • 奇偶规则:使用这个规则时,我们通过以下方法判断一个点是否在形状内部:从该点画一条水平线到形状外部,然后数交点的数量。如果交点数是奇数,说明该点在形状内部。
    • 环绕规则:根据这个规则,我们通过以下方法判断一个点是否在形状内部:从该点画一条水平线到形状外部的位置,确定线每个交点的方向是向上还是向下。通过将每个交点方向相加来确定环绕数。如果这个数不为零,说明该点在形状内部。在大多数情况下,这种填充模式也可以看作是闭合形状的交集。

    image

    image

    数据准备:

    点击折叠或展开代码
    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;
    }

    代码说明:

    1. initGraphics()中,创建画布QGraphicsScene,准备两条水平线;
    2. 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);
    }

    代码说明:

    1. makeSquarePolygon创建正方形,makeTrianglePolygon创建三角形;
    2. squareXXTriangleXX方法创建顺逆时针的正方形和三角形;注意,在框架中,向下为Y轴正方向;
    3. applyFillRule修改填充规则,通过两个按钮来改变;

    效果演示:

    奇偶规则:image

    环绕规则:
    image

    其他问题:

        面对其他不规则图形,绘制方向是固定的,比如Qt中的弧线,默认从3点开始逆时针绘制,如何解决?
    思路:

    1. 使用负数角度,可以顺时针绘制;
    2. 使用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();
    }

    代码说明:

    1. makeRectPolygon方法创建矩形,makeArcPath方法创建一个四分之三圆的弧线;
    2. rectXXXArcXXX方法分别绘制顺逆时针的矩形、正反向绘制的弧线;

    效果演示

    奇偶规则:
    image
    环绕规则:
    image

    附录一:参考文献

    附录二:完整代码

    • 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);
    }
  • 相关阅读:
    MySQL数据库#6
    【板栗糖GIS】arcmap—如何在没有脚本的情况下分割字段
    iOS 提高Xcode运行速度
    计算机二级(Python)真题讲解每日一题:《字典字符查找》
    YOLOv5从训练到移植
    C. Swap Game #832 div2
    Arduino ESP32/ESP8266 +ST7735 1.8“tft中秋小时钟
    NB!各种免费API接口分享,热门、有意思的都有~
    矩阵初等变换整理
    博客性能优化笔记 | 99分
  • 原文地址:https://www.cnblogs.com/wsry/p/22363584