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


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

关键词:
    QPainterPathQGraphicsView;填充规则;奇偶规则;环绕规则;顺逆时针;WindingFillOddEvenFilltoReversed;路径方向

版本: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<QGraphicsPathItem *> 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. 使用QPainterPathtoReversed方法,逆向路径绘制;

示例代码:

点击折叠或展开代码
// 创建矩形
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<QGraphicsPathItem *> 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 <QWidget>
#include <QtWidgets>
#include <QGraphicsItem>

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<QGraphicsPathItem *> 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);
}

posted @ 2026-08-10 14:22  薄暮知秋  阅读(20)  评论(0)    收藏  举报
/*https://www.cnblogs.com/lingr7/p/15651906.html*/ /*自动显示目录导航*/