MarKS
MarKS

Reputation: 504

Drawing QGraphicsLineItem on a QTableView

I have an application where i need to draw lines on a QTableView showing the range of frames. I have a QGraphicsView and QGraphicsScene which holds a QTableView As shown below:

enter image description here

Criteria

I have hereby created a MVCE

Mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

MainWindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "timelineview.h"

MainWindow::MainWindow(QWidget* parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    TimelineView* graphicsView = new TimelineView(this);
    setCentralWidget(graphicsView);
}

MainWindow::~MainWindow()
{
    delete ui;
}

TimelineView.cpp

// qt
#include <QHeaderView>

// local
#include "timelineview.h"

TimelineView::TimelineView(QWidget* parent) :
    QGraphicsView(parent),
    m_scene{new TimelineScene}
{
    setScene(m_scene);

    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);

    m_table = new QTableView;

    // settings for table view
    m_table->setSelectionMode(QAbstractItemView::NoSelection);
    m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
    m_table->verticalHeader()->hide();
    m_table->horizontalHeader()->setHighlightSections(false);

    m_tableModel = new QStandardItemModel(10, 100, m_table);
    m_table->setModel(m_tableModel);

    m_scene->addWidget(m_table);

    setMouseTracking(true);
}


void TimelineView::resizeEvent(QResizeEvent* event)
{
    m_scene->setSceneRect(0, 0, width(), height());
    m_table->setGeometry(m_scene->sceneRect().toRect());
    fitInView(m_scene->sceneRect(), Qt::KeepAspectRatioByExpanding);
    QGraphicsView::resizeEvent(event);
}

TimelineView.h

#ifndef TIMELINEVIEW_H
#define TIMELINEVIEW_H

// qt
#include <QGraphicsView>
#include <QTableView>
#include <QStandardItemModel>

// local
#include "timelinescene.h"

class TimelineView : public QGraphicsView
{
    public:

        explicit TimelineView(QWidget* parent = nullptr);

    protected:

        virtual void resizeEvent(QResizeEvent* event) override;

    private:

        QTableView* m_table;
        QStandardItemModel* m_tableModel;
        TimelineScene* m_scene;
};

#endif // TIMELINEVIEW_H

TimelineScene.cpp

#include "timelinescene.h"

TimelineScene::TimelineScene(QObject* parent) :
    QGraphicsScene(parent)
    , m_lineItem{nullptr}
    , m_isPressed{false}
{

}



void TimelineScene::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
    if(event->button() == Qt::LeftButton)
    {
        m_origPoint = event->scenePos();
        m_isPressed = true;
    }

    QGraphicsScene::mousePressEvent(event);
}



void TimelineScene::mouseMoveEvent(QGraphicsSceneMouseEvent* event)
{
    if(m_isPressed)
    {
        if(m_lineItem == nullptr)
        {
            // create a pen for the line to be drawn
            QPen pen;
            pen.setStyle(Qt::SolidLine);
            pen.setBrush(QColor(255, 102, 0));
            pen.setWidth(8);

            m_lineItem = new QGraphicsLineItem(m_origPoint.x(), m_origPoint.y(), event->scenePos().x(), event->scenePos().y());

            // set the pen
            m_lineItem->setPen(pen);

            // add the item to the scene
            addItem(m_lineItem);
        }

        m_lineItem->setLine(m_origPoint.x(), m_origPoint.y(), event->scenePos().x(), m_origPoint.y());

        update();
    }

    QGraphicsScene::mouseMoveEvent(event);
}



void TimelineScene::mouseReleaseEvent(QGraphicsSceneMouseEvent* event)
{
    m_lineItem = nullptr;
    m_isPressed = false;

    QGraphicsScene::mouseReleaseEvent(event);
}

TimelineScene.h

#ifndef TIMELINESCENE_H
#define TIMELINESCENE_H

// qt
#include <QGraphicsScene>
#include <QObject>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsLineItem>
#include <QPointF>



class TimelineScene : public QGraphicsScene
{
    public:

        explicit TimelineScene(QObject* parent = nullptr);

    protected:

        virtual void mousePressEvent(QGraphicsSceneMouseEvent* event) override;
        virtual void mouseMoveEvent(QGraphicsSceneMouseEvent* event) override;
        virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) override;

    private:

        QGraphicsLineItem* m_lineItem;
        QPointF m_origPoint;
        bool m_isPressed;
};

#endif // TIMELINESCENE_H

Problems:

I know i could handle all of these issues if i could find a way to retrieve info about the underlying QTableView but not sure how.

Upvotes: 1

Views: 960

Answers (2)

sendevent
sendevent

Reputation: 222

As already discussed, using a delegate is the right way to do it.

In case it's impossible for some reason, I strongly recommend you not mix the model view with the graphics scene in such way.

In case it's absolutely impossible to use delegates, I'd think about drawing the lines on QTableView — it has API for accessing model indices through pixel coordinates, it does know its scrolling position and it does have handlers for all the necessary user input. So all that you have to do is to store the collection of QLine in a class derived from the QTableView and draw the lines which fit the current viewport via QPainter. I'm afraid that even if the "overlay" with QGraphicsScene looks like the easiest solution now, in the nuts it's tons of issues related to the user input handling/routing. And it's definitely not the Qt-way.

UPD: a delegate-based sample:

The idea is quite simple:

  • the whole line is a collection of segments — each cell is a segment;
  • for each cell, map the line start and end x-coordinate to the range [0.0;1.0];
  • store mapped coordinates in the model itself.

Then you can draw your custom delegate as follows:

void LineDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QStyledItemDelegate::paint(painter, option, index);

    const QVariant &lineData = index.data(Line::DataRole);
    if (lineData.isValid() && lineData.canConvert<Line>()) {
        const Line &line = lineData.value<Line>();
        const QLineF &lineF = line.toQLine(option.rect);
        painter->save();

        painter->setPen(m_linePen);
        painter->drawLine(lineF);

        painter->restore();
    }
}

The mouse handling is a bit more tricky:

bool LineDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
                               const QModelIndex &index)
{
    switch (event->type()) {
    case QEvent::MouseButtonPress:
        handleMousePress(static_cast<QMouseEvent *>(event), model, option, index);
        break;
    case QEvent::MouseMove:
        handleMouseMove(static_cast<QMouseEvent *>(event), model, option, index);
        break;
    case QEvent::MouseButtonRelease:
        handleMouseRelease(static_cast<QMouseEvent *>(event), model, option, index);
        break;
    default:
        break;
    }

    return QStyledItemDelegate::editorEvent(event, model, option, index);
}

void LineDelegate::handleMousePress(QMouseEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
                                    const QModelIndex &index)
{
    Q_UNUSED(event);
    Q_UNUSED(model);

    if (index.isValid()) {
        m_startIndex = index;
        m_startPoint = { event->x() - option.rect.x(), option.rect.center().y() };
    }
}

void LineDelegate::handleMouseMove(QMouseEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
                                   const QModelIndex &index)
{
    const QPoint endPoint = { event->x() - option.rect.x(), option.rect.center().y() };
    if (m_startIndex != index) {
        m_startIndex = index;
        m_startPoint = endPoint;
    }

    const Line &line = Line::fromQLine({ m_startPoint, endPoint }, option.rect);

    model->setData(index, QVariant::fromValue(line), Line::DataRole);
}

void LineDelegate::handleMouseRelease(QMouseEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option,
                                      const QModelIndex &index)
{
    Q_UNUSED(event);
    Q_UNUSED(model);
    Q_UNUSED(option);
    Q_UNUSED(index);

    m_startIndex = QModelIndex();
    m_startPoint = { -1, -1 };
}

Here is how it looks like:

screencast

The complete code of the sample above is available on github

Please note — that's just a brief prototype. There is no support for things like right-to-left mouse movement, nor limitation to a current row only, etc. But that should be enough for you to start, I hope.

Upvotes: 2

ypnos
ypnos

Reputation: 52347

Apart from our discussion about delegates in the comments, to answer your two questions:

1. Finding model index from scene coordinates

2. Restricting item movement

You can override QGraphicsItem::itemChange() to manipulate any movement of an item even while dragged by the user. It looks like this:

QVariant QGraphicsItem::itemChange(GraphicsItemChange change,
                                              const QVariant &value)
{
    if (change == ItemPositionChange) {
        // restr.Position() returns a QPointF limited by the allowed are
        return restrictPosition(value.toPointF());
    }
    …
}

Upvotes: 0

Related Questions