Search code examples
qtplotqt5qtchartsqchart

Implement selection on QChartView


I want to make an implementation of chart selection based on QChart and QChartView. The family of the classes have a big advantage - easy use of openGL and animations, for example:

QLineSeries *series = new QLineSeries();
series->setUseOpenGL(true); // <==

QChart *chart = new QChart();
chart->addSeries(series);
chart->setAnimationOptions(QChart::AllAnimations); // <==

QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);    

The QChartView class provides the useful zoom feature - QChartView::setRubberBand():

chartView->setRubberBand(QChartView::RectangleRubberBand);

enter image description here

The main problem is that the rubber band can be used only for zoom, but I need to implement it for horizontal selection without zoom, as the feature usually implemented in audio editors: enter image description here

Now, when I have inherit QChartView, I can disable zoom after selection:

class ChartView : public QChartView
...
bool m_drawRubberBand;
QRubberBand m_rubberBand;
...

ChartView::ChartView(QChart *chart, QWidget *parent) 
    : QChartView(chart, parent)
{
    setRubberBand(QChartView::HorizontalRubberBand);
}
...

// Just copy-paste from the Qt 5 sources - file \Src\qtcharts\src\charts\qchartview.cpp:
/*!
If the rubber band rectangle is displayed in the press event specified by
\a event, the event data is used to update the rubber band geometry.
Otherwise, the default QGraphicsView::mouseMoveEvent() implementation is called.
*/
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
    if (m_drawRubberBand && m_rubberBand.isVisible()) 
    {
        QRect rect = chart()->plotArea().toRect();
        int width = event->pos().x() - m_rubberBandOrigin.x();
        int height = event->pos().y() - m_rubberBandOrigin.y();
        if (!rubberBand().testFlag(VerticalRubberBand)) 
        {
            m_rubberBandOrigin.setY(rect.top());
            height = rect.height();
        }
        if (!rubberBand().testFlag(HorizontalRubberBand))
        {
            m_rubberBandOrigin.setX(rect.left());
            width = rect.width();
        }
        m_rubberBand.setGeometry(QRect(m_rubberBandOrigin.x(), m_rubberBandOrigin.y(), width, height).normalized());
    }
    else 
    {
        QGraphicsView::mouseMoveEvent(event);
    }
}

Then I can just don't implement the zoom action on the mouse key release event:

void ChartView::mouseReleaseEvent(QMouseEvent *event)
{
    if (m_rubberBand.isVisible()) 
    {
        if (event->button() == Qt::LeftButton)
        {
            m_drawRubberBand = false;    
            do_nothing(); // <==
        }

    }
}

So, my questions now:

  1. How borders of the the visual rubber band can be mapped to real chart's coordinates. I.e., how can the selection be mapped into a line series on the chart? Now I receive same wrong coordinates:

      void MyView::resizeEvent(QResizeEvent *event) 
      {
          QChartView::resizeEvent(event);
    
          QRect rct(QPoint(10, 10), QPoint(20, 20));
          qDebug() << mapToScene(rct); <==
      }
    

Output:

QPolygonF(QPointF(10,10)QPointF(21,10)QPointF(21,21)QPointF(10,21))
QPolygonF(QPointF(10,10)QPointF(21,10)QPointF(21,21)QPointF(10,21))
QPolygonF(QPointF(10,10)QPointF(21,10)QPointF(21,21)QPointF(10,21))
QPolygonF(QPointF(10,10)QPointF(21,10)QPointF(21,21)QPointF(10,21))
...
  1. How can an existing rubber selection be proportionally resized together with the view?

Edit: May be it is a useful keyword - QGraphicsScene::setSelectionArea().

The Qt 5 chip example which provides nice rubber band selection, but the example based on QGraphicsView, not on QChartView.


Solution

  • The question is resolved thanks to the reply to this answer: Get mouse coordinates in QChartView's axis system

    The key moment: it was necessary to invoke QChart::mapToValue() for a correct coordinates transform:

    QPointF ChartView::point_to_chart(const QPoint &pnt)
    {
        QPointF scene_point = mapToScene(pnt);
        QPointF chart_point = chart()->mapToValue(scene_point);
    
        return chart_point;
    }
    

    And the inverse transformation:

    QPoint ChartView::chart_to_view_point(QPointF char_coord)
    {
        QPointF scene_point = chart()->mapToPosition(char_coord);
        QPoint view_point = mapFromScene(scene_point);
    
        return view_point;
    }
    

    That's how I have implemented resize of the rubber band on the resizeEvent.
    Firstly, I save the current rubber band on mouse release event:

    void ChartView::mouseReleaseEvent(QMouseEvent *event)
    {
        if (m_rubberBand.isVisible()) 
        {
            update_rubber_band(event);            
            m_drawRubberBand = false;
            save_current_rubber_band(); <==            
        }
    }
    

    Where:

    void ChartView::update_rubber_band(QMouseEvent * event)
    {
        QRect rect = chart()->plotArea().toRect();
        int width = event->pos().x() - m_rubberBandOrigin.x();
        int height = event->pos().y() - m_rubberBandOrigin.y();
        if (!rubberBand().testFlag(VerticalRubberBand))
        {
            m_rubberBandOrigin.setY(rect.top());
            height = rect.height();
        }
        if (!rubberBand().testFlag(HorizontalRubberBand))
        {
            m_rubberBandOrigin.setX(rect.left());
            width = rect.width();
        }
        m_rubberBand.setGeometry(QRect(m_rubberBandOrigin.x(), m_rubberBandOrigin.y(), width, height).normalized());
    }
    

    And:

    void ChartView::save_current_rubber_band()
    {
        QRect rect = m_rubberBand.geometry();
    
        QPointF chart_top_left = point_to_chart(rect.topLeft());
        m_chartRectF.setTopLeft(chart_top_left);
    
        QPointF chart_bottom_right = point_to_chart(rect.bottomRight());
        m_chartRectF.setBottomRight(chart_bottom_right);
    }
    

    And how I repaint the rubber on the resize event:

    void ChartView::resizeEvent(QResizeEvent *event)
    {
        QChartView::resizeEvent(event);
    
        if (m_rubberBand.isVisible())
        {
            restore_rubber_band();
        }
    
        apply_nice_numbers();
    }
    

    Where:

    void ChartView::restore_rubber_band()
    {
        QPoint view_top_left = chart_to_view_point(m_chartRectF.topLeft());
        QPoint view_bottom_right = chart_to_view_point(m_chartRectF.bottomRight());
    
        m_rubberBandOrigin = view_top_left;
        m_rubberBand.setGeometry(QRect(view_top_left, view_bottom_right));
    }
    

    And don't forget about the "nice numbers":

    void ChartView::apply_nice_numbers()
    {
        QList<QAbstractAxis*> axes_list = chart()->axes();
        for each(QAbstractAxis* abstract_axis in axes_list)
        {
            QValueAxis* value_axis = qobject_cast<QValueAxis*>(abstract_axis);
            if (value_axis)
            {
                value_axis->applyNiceNumbers();
            }
        }
    }
    

    This logic in action. Before resize: enter image description here

    After resize: enter image description here