如何使用箭头将 QTableWidget 单元格的 QRadioButton 与另一个 QTableWidget 单元格连接起来?

How to connect a QRadioButton of a QTableWidget cell with another QTableWidget cell using an arrow?

我有一个 QListWidget 和一个 QGraphicsView 都被子类化以覆盖它们的一些成员。我准备了一个最小的可验证示例来展示我遇到的问题 here

QListWidget 我可以拖放特定字段(由 QTableWidget 表示)并将它们放到 QGraphicsView 中,为了做到这一点,我使用了 QGraphicsProxyWidget方法如下所示。

问题

现在,如何将一个 QTableWidget 的单元格内的 2 QRadioButton 连接到另一个 QTableWidget 的另一个单元格?

重要的是要提到绿色 QGraphicsRectItem 它用于在 QTableWidget 周围移动以及调整其尺寸。

下面是我目前能得到的结果:

低于我一直努力实现的预期结果:

下面是最重要的代码部分:

scene.h

#ifndef SCENE_H
#define SCENE_H

#include <QGraphicsScene>

class Scene : public QGraphicsScene
{
public:
    Scene(QObject *parent = nullptr);

protected:
  void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
  void dragMoveEvent(QGraphicsSceneDragDropEvent *event);
  void dropEvent(QGraphicsSceneDragDropEvent *event);
};

#endif // SCENE_H

scene.cpp

#include "arrow.h"

#include <QGraphicsSceneDragDropEvent>
#include <QMimeData>
#include <QTableWidget>
#include <QGraphicsProxyWidget>
#include <QVBoxLayout>
#include <QMetaEnum>
#include <QEvent>
#include <QSizeGrip>
#include <QRadioButton>


Scene::Scene(QObject *parent)
{
    setBackgroundBrush(Qt::lightGray);

}

void Scene::dragEnterEvent(QGraphicsSceneDragDropEvent *event) {
  if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
    event->setAccepted(true);
}

void Scene::dragMoveEvent(QGraphicsSceneDragDropEvent *event) {
  if (event->mimeData()->hasFormat("application/x-qabstractitemmodeldatalist"))
    event->setAccepted(true);
}

void Scene::dropEvent(QGraphicsSceneDragDropEvent *event) {
    QByteArray encoded =
      event->mimeData()->data("application/x-qabstractitemmodeldatalist");
    QDataStream stream(&encoded, QIODevice::ReadOnly);

    QStringList rosTables;
    QString newString;

    while (!stream.atEnd()) {
    int row, col;
    QMap<int, QVariant> roleDataMap;
    stream >> row >> col >> roleDataMap;
    rosTables << roleDataMap[Qt::DisplayRole].toString();
    }
    for (const QString &tableType : rosTables) {
        if (tableType == "Images") {
            QPoint initPos(0, 0);
            auto *wgt = new CustomTableWidget;
            auto *proxyControl = addRect(0, 0, 0, 0, QPen(Qt::black),
                                         QBrush(Qt::darkGreen));
            auto *sizeGrip = new QSizeGrip(wgt);
            auto *layout = new QHBoxLayout(wgt);

            layout->setContentsMargins(0, 0, 0, 0);
            layout->addWidget(sizeGrip, 0, Qt::AlignRight | Qt::AlignBottom);

            connect(wgt, &CustomTableWidget::sizeChanged, [wgt, proxyControl](){
                proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
            });

            wgt->setColumnCount(4);
            wgt->setRowCount(4);

            for (int ridx = 0; ridx < wgt->rowCount(); ridx++) {
                for (int cidx = 0; cidx < wgt->columnCount(); cidx++) {
                    QRadioButton *radio1, *radio2;
                    auto* item = new QTableWidgetItem();
                    item->setText(QString("%1").arg(ridx));
                    wgt->setItem(ridx,cidx,item);
                    radio1 = new QRadioButton;
                    radio2 = new QRadioButton;
                    wgt->setCellWidget(cidx, 0, radio1);
                    wgt->setCellWidget(cidx, 3, radio2);
                    Arrow *arrow = new Arrow; 
                }
            }

            auto *const proxy = addWidget(wgt);

            proxy->setPos(initPos.x(), initPos.y()
                          + proxyControl->rect().height());
            proxy->setParentItem(proxyControl);

            proxyControl->setPos(initPos.x(), initPos.y());
            proxyControl->setFlag(QGraphicsItem::ItemIsMovable, true);
            proxyControl->setFlag(QGraphicsItem::ItemIsSelectable, true);
            proxyControl->setRect(wgt->geometry().adjusted(-10, -10, 10, 10));
        }
    }
}

diagramitem.h

#ifndef DIAGRAMITEM_H
#define DIAGRAMITEM_H

#include <QGraphicsPolygonItem>

class Arrow;
class DiagramItem : public QGraphicsPolygonItem
{
public:

    DiagramItem(QMenu *contextMenu, QGraphicsItem *parent = Q_NULLPTR);

    void removeArrow(Arrow *arrow);
    void removeArrows();
    void addArrow(Arrow *arrow);
    QPixmap image() const;
protected:
    void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
private:
    QPolygonF myPolygon;
    QList<Arrow*> arrows;
    QMenu *myContextMenu;
};

#endif // DIAGRAMITEM_H

diagramitem.cpp

#include "diagramitem.h"
#include "arrow.h"
#include <QPainter>
#include <QGraphicsScene>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>


DiagramItem::DiagramItem(QMenu *contextMenu, QGraphicsItem *parent) : QGraphicsPolygonItem(parent)
{
    myContextMenu = contextMenu;
    setPolygon(myPolygon);
    setFlag(QGraphicsItem::ItemIsMovable, true);
    setFlag(QGraphicsItem::ItemIsSelectable, true);
    setFlag(QGraphicsItem::ItemSendsGeometryChanges, true);

}

void DiagramItem::removeArrow(Arrow *arrow)
{
    int index = arrows.indexOf(arrow);

    if (index != -1)
        arrows.removeAt(index);
}

void DiagramItem::removeArrows()
{
    foreach (Arrow *arrow, arrows) {
        arrow->startItem()->removeArrow(arrow);
        arrow->endItem()->removeArrow(arrow);
        scene()->removeItem(arrow);
        delete arrow;
    }
}

void DiagramItem::addArrow(Arrow *arrow)
{
    arrows.append(arrow);
}

void DiagramItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
    scene()->clearSelection();
    setSelected(true);
    myContextMenu->exec(event->screenPos());
}

QVariant DiagramItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
    if (change == QGraphicsItem::ItemPositionChange) {
        foreach (Arrow *arrow, arrows) {
            arrow->updatePosition();
        }
    }
    return value;
}

arrow.h

#ifndef ARROW_H
#define ARROW_H

#include <QGraphicsLineItem>
#include "diagramitem.h"
class Arrow : public QGraphicsLineItem
{
public:

    enum { Type = UserType + 4 };
    Arrow(DiagramItem *startItem, DiagramItem *endItem,
      QGraphicsItem *parent = nullptr);

    DiagramItem *startItem() const { return myStartItem; }
    DiagramItem *endItem() const { return myEndItem; }
    QPainterPath shape() const override;

    void setColor(const QColor &color) {
        myColor = color;
    }

    int type() const override { return Type; }
    void updatePosition();

protected:
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override;

private:
    QColor myColor;
    DiagramItem *myStartItem;
    DiagramItem *myEndItem;
    QPolygonF arrowHead;
};
#endif // ARROW_H

arrow.cpp

#include "arrow.h"
#include <QPen>
#include <QPainter>
#include "qmath.h"

Arrow::Arrow(DiagramItem *startItem, DiagramItem *endItem, QGraphicsItem *parent) : QGraphicsLineItem(parent)
{
    myStartItem = startItem;
    myEndItem = endItem;
    myColor = Qt::GlobalColor::black;
    setPen(QPen(myColor, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    setFlag(QGraphicsItem::ItemIsSelectable, true);
}

QPainterPath Arrow::shape() const
{
    QPainterPath path = QGraphicsLineItem::shape();
    path.addPolygon(arrowHead);
    return path;
}

void Arrow::updatePosition()
{
    QLineF line(mapFromItem(myStartItem, 0, 0), mapFromItem(myEndItem, 0, 0));
    setLine(line);
}

void Arrow::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(option)
    Q_UNUSED(widget)
    if (myStartItem->collidesWithItem(myEndItem))
        return;

    QPen myPen = pen();
    myPen.setColor(myColor);
    qreal arrowSize = 20;
    painter->setPen(myPen);
    painter->setBrush(myColor);

    QLineF centerLine(myStartItem->pos(), myEndItem->pos());
    QPolygonF endPolygon = myEndItem->polygon();
    QPointF p1 = endPolygon.first() + myEndItem->pos();
    QPointF p2;
    QPointF intersectPoint;
    QLineF polyLine;
    for (int i = 1; i < endPolygon.count(); ++i) {
        p2 = endPolygon.at(i) + myEndItem->pos();
        polyLine = QLineF(p1, p2);
        QLineF::IntersectType intersectType =
            polyLine.intersect(centerLine, &intersectPoint);
        if (intersectType == QLineF::BoundedIntersection)
            break;
        p1 = p2;
    }

    setLine(QLineF(intersectPoint, myStartItem->pos()));

    double angle = std::atan2(-line().dy(), line().dx());

    QPointF arrowP1 = line().p1() + QPointF(sin(angle + M_PI / 3) * arrowSize,
                                    cos(angle + M_PI / 3) * arrowSize);
    QPointF arrowP2 = line().p1() + QPointF(sin(angle + M_PI - M_PI / 3) * arrowSize,
                                    cos(angle + M_PI - M_PI / 3) * arrowSize);

    arrowHead.clear();
    arrowHead << line().p1() << arrowP1 << arrowP2;

    painter->drawLine(line());
    painter->drawPolygon(arrowHead);
    if (isSelected()) {
        painter->setPen(QPen(myColor, 1, Qt::DashLine));
        QLineF myLine = line();
        myLine.translate(0, 4.0);
        painter->drawLine(myLine);
        myLine.translate(0,-8.0);
        painter->drawLine(myLine);
    }
}

到目前为止我为解决问题做了什么:

1) 我遇到了 this post 这对于理解关于如何做到这一点的最初想法很有用,但它并没有真正提供一种方法,或者关于如何最好地进行的实施想法

2) 我研究了官方文档,在问这个问题之前,我浏览了整个 Diagram Scene 示例并理解了如何创建 Arrow目的。关于那方面的文档非常好,让我明白了图形行项目是如何形成的。 但是我无法(回到我的示例)如何“意识到”QRadioButton 我正试图将其中心用作箭头广告的起点,因此,我如何“意识到”目的地 QRadioButton 在另一个单元格中,它必须在那里连接?

下面具体说说我的意思:

所以基本上 QRadioButton 的起点改变颜色(或样式),到达点也改变颜色。

3) 我认为 Arrow 对象必须在子类 QGraphicsScene 中创建,因为它已经处理了 mouse events

4) 尽管到目前为止我已经尝试过,但我找不到任何其他有用的帮助。虽然我还在研究如何做到这一点。

如果有人遇到过同样的情况,请提供有关如何更好地继续解决此问题并找到解决此问题的方法的指导。

解决方案

勾选开始和结束单选按钮后,您需要创建箭头,将这些按钮作为开始和结束节点,例如:

void Backend::onInputRadioButton(bool checked)
{
    m_endNode = checked ? static_cast<QRadioButton *>(sender()) : nullptr;

    if (m_startNode && m_endNode)
        m_scene->addItem(new ArrowItem(m_startNode, m_endNode));
}

然后你需要连接 top-most 图形项的信号,这些图形项包含表格,与 ArrowItemupdatePosition 插槽连接,例如:

connect(m_startItem->property("item").value<MovableItem *>(),
        &MovableItem::itemMoved, this, &ArrowItem::updatePosition);
connect(m_endItem->property("item").value<MovableItem *>(),
        &MovableItem::itemMoved, this, &ArrowItem::updatePosition);

注意:我正在使用 属性 来保存对容器项的引用。

最后,您需要更新箭头线,例如:

void ArrowItem::updatePosition()
{
    QPointF offset(7, 15);
    QPointF p1 = m_startItem->property("item").value<MovableItem *>()->pos()
                 + m_startItem->parentWidget()->mapToParent(m_startItem->pos())
                 + offset;
    QPointF p2 = m_endItem->property("item").value<MovableItem *>()->pos()
                 + m_endItem->parentWidget()->mapToParent(m_endItem->pos())
                 + offset;

    setLine(QLineF(p1, p2));
}

例子

我敢于对您的代码提出改进建议。您可以在 GitHub.

上找到我为您编写的完整示例

结果

提供的示例产生以下结果:

注意:箭头缺失。再次检查 Diagram Scene Example 以了解如何绘制它们。