-1

I have a QwtPlot view containing many QwtPlotCurve and I want to highlight/mignify (currently simply trying to change the color) of the closest point to the mouse position (because I'll display some info about this point of measurement when user will press the mouse button, and I'd like him to know what point is the current "target").

So I use a QwtPlotPicker to get mouse position and then I setup an extra QwtPlotCurve curve with this single point ("target") to be drawn with a different color on top of the others.

It works, but the only way I could make this work is by calling QwtPlot::replot() which is heavy to be called every time the mouse is being moved (as I may have many thousands of point being plotted).

I'd like to only repaint the area where previously highlighted point was (to restore default display) and then only repaint the area where newly highlighted point is. But when I do, this (call repaint(QRect) rather than replot()), nothing happens (no point is highlighted), however, if I deactivate the window, I see the point gets highlighted, so it looks like repaint does some piece of job but not enough for the end user to see it...

Note that I disabled Qwt backing store features.

Here is my MCVE:

widget.h:

#include <QDialog>

class QLabel;
class QwtPlotCurve;
class QwtPlot;
class Dialog : public QDialog
{
    Q_OBJECT
public:
    Dialog();

public slots:
    void onHovered( const QPointF& pt );

private:
    std::vector<QwtPlotCurve*> curves;
    QwtPlotCurve* highlight;
    std::tuple<QwtPlotCurve*,int,QRect> highlighted;
    QLabel* closestLabel;
    QwtPlot* plot;
};

widget.cpp:

#include "widget.h"

#include <QVBoxLayout>
#include <QLabel>

#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_picker.h>
#include <qwt_plot_canvas.h>
#include <qwt_picker_machine.h>

#include <sstream>

Dialog::Dialog()
{
    setLayout( new QVBoxLayout() );

    plot = new QwtPlot(this);

    layout()->addWidget( plot );
    layout()->addWidget( closestLabel = new QLabel( this ) );

    for ( int i = 0; i != 5; ++i )
    {
        QwtPlotCurve* curve = new QwtPlotCurve();

        QVector<double> x, y;
        for ( int i = 0; i != 10; ++i )
        {
            x.push_back( std::rand() );
            y.push_back( std::rand() );
        }

        curve->setSamples( x, y );

        curve->setStyle( QwtPlotCurve::Dots );
        curve->setPen( Qt::black, 5 );
        curve->attach(plot);

        curves.push_back( curve );
    }

    highlight = new QwtPlotCurve();
    highlight->setSamples( {}, {} );
    highlight->setStyle( QwtPlotCurve::Dots );
    highlight->setPen( Qt::red, 5 );
    highlight->attach(plot);

    QwtPlotCanvas* canvas = dynamic_cast<QwtPlotCanvas*>( plot->canvas() );
    if ( canvas )
        canvas->setPaintAttribute( QwtPlotCanvas::BackingStore, false );

    plot->replot();

    QwtPlotPicker* picker = new QwtPlotPicker( plot->canvas() );
    picker->setStateMachine(new QwtPickerTrackerMachine());
    connect(picker, SIGNAL(moved(const QPointF&)), this, SLOT(onHovered(const QPointF&)));
}

// inspired from QwtPlotCurve::closestPoint
int closestPoint( QwtPlotCurve& curve, const QPoint &pos, double *dist )
{
    const size_t numSamples = curve.dataSize();

    if ( curve.plot() == NULL || numSamples <= 0 )
        return -1;

    const QwtSeriesData<QPointF> *series = curve.data();

    const QwtScaleMap xMap = curve.plot()->canvasMap( curve.xAxis() );
    const QwtScaleMap yMap = curve.plot()->canvasMap( curve.yAxis() );

    const double xPos = xMap.transform( pos.x() );
    const double yPos = yMap.transform( pos.y() );

    int index = -1;
    double dmin = DBL_MAX;

    for ( uint i = 0; i < numSamples; i++ )
    {
        const QPointF sample = series->sample( i );

        const double cx = xMap.transform( sample.x() ) - xPos;
        const double cy = yMap.transform( sample.y() ) - yPos;

        const double dist = sqrt( pow(cx,2) + pow(cy,2) );
        if ( dist < dmin )
        {
            index = i;
            dmin = dist;
        }
    }
    if ( dist )
        *dist = dmin;

    return index;
}

void Dialog::onHovered( const QPointF& pt )
{
    // mouse moved!

    QwtPlotCurve* closest = NULL;
    int closestIndex = -1;
    double minDist = DBL_MAX;
    for ( auto curve : curves )
    {
        double dist;
        int index = closestPoint( *curve, pt.toPoint(), &dist );
        if ( dist < minDist )
        {
            minDist = dist;
            closestIndex = index;
            closest = curve;
        }
    }

    if ( !closest )
        return;

    std::stringstream str;
    QPointF closestPoint = closest->sample(closestIndex);
    str << "Closest point is " << closestPoint.rx() << "," << closestPoint.ry();
    closestLabel->setText( str.str().c_str() );

    if ( std::get<0>( highlighted ) == closest &&
         std::get<1>( highlighted ) == closestIndex )
    {
        // highlighted point is unchanged
        return;
    }
    else
    {
        // highlighted point changed

        const QwtScaleMap xMap = plot->canvasMap( QwtPlot::xBottom );
        const QwtScaleMap yMap = plot->canvasMap( QwtPlot::yLeft );

        const int rectSize = highlight->pen().width() * 2;
        const int x = xMap.transform( closestPoint.rx() );
        const int y = xMap.transform( closestPoint.ry() );
        const QRect cr = plot->canvas()->contentsRect();

        highlight->setSamples( { closestPoint.rx() }, { closestPoint.ry() } );

        QRect smallCR( x - rectSize/2, y - rectSize/2, rectSize, rectSize );

        std::tuple<QwtPlotCurve*,int,QRect> newHighlighted{ closest, closestIndex, smallCR };

        QwtPlotCanvas* canvas = dynamic_cast<QwtPlotCanvas*>( plot->canvas() );
        if ( canvas )
        {
            if ( std::get<2>( highlighted ) != QRect() )
            {
                // repaint previously highlighted area:
                canvas->repaint( std::get<2>( highlighted ) );
            }
            // repaint newly highlighted area:
            canvas->repaint( std::get<2>( newHighlighted ) );

            // if you replace lines above by this one, it works!
            //canvas->replot();
        }

        highlighted = newHighlighted;
    }
}

main.cpp:

#include <QApplication>
#include "widget.h"
int main( int argc, char* argv[] )
{
    QApplication app( argc, argv );
    Dialog dlg;

    dlg.show();

    return app.exec();
}

Edit:

If I replace highlight = new QwtPlotCurve(); by highlight = new MyCurve(); with MyCurve defined as:

class MyCurve : public QwtPlotCurve
{
public:
    void drawSeries( QPainter *painter,
    const QwtScaleMap &xMap, const QwtScaleMap &yMap,
    const QRectF &canvasRect, int from, int to ) const override
    {
        static int i = 0;
        if ( dataSize() != 0 )
            std::cout << "PAINTING " << i++ << std::endl;

        QwtPlotCurve::drawSeries( painter, xMap, yMap, canvasRect, from, to );
    }
};

Then I see that the console show a new "PAINTING" when each canvas->repaint are called, howevere the red point does not become visible. Now if I move another window over mine (or press Alt), a new "PAINTING" is reported and this time the closest point becomes red. So as I mentioned, the method looks good but not enough to have the view be repainted as expected...

jpo38
  • 20,821
  • 10
  • 70
  • 151

1 Answers1

1

You should use QwtPlotDirectPainter, it is designed to do exactly what you want:

QwtPlotDirectPainter offers an API to paint subsets ( f.e all additions points ) without erasing/repainting the plot canvas.

You can se it being used in the "event_filter" example of Qwt:

// Hightlight the selected point
void CanvasPicker::showCursor( bool showIt )
{
    if ( !d_selectedCurve )
        return;

    QwtSymbol *symbol = const_cast<QwtSymbol *>( d_selectedCurve->symbol() );

    const QBrush brush = symbol->brush();
    if ( showIt )
        symbol->setBrush( symbol->brush().color().dark( 180 ) );

    QwtPlotDirectPainter directPainter;
    directPainter.drawSeries( d_selectedCurve, d_selectedPoint, d_selectedPoint );

    if ( showIt )
        symbol->setBrush( brush ); // reset brush
}

Depending on the showIt parameter, this function will either draw the point as "selected" or redraw it in its original/unselected style.

You can see how it is used in the select() function:

void CanvasPicker::select( const QPoint &pos )
{
    [...]

    showCursor( false ); // Mark the previously selected point as deselected
    d_selectedCurve = NULL;
    d_selectedPoint = -1;

    if ( curve && dist < 10 ) // 10 pixels tolerance
    {
        d_selectedCurve = curve;
        d_selectedPoint = index;
        showCursor( true ); // Mark the new point as selected.
    }
}

In you case, I believe you could directly use the CanvasPicker class and just do some fine tuning like calling select() on QEvent::MouseMove instead of QEvent::MouseButtonPress.

Benjamin T
  • 8,120
  • 20
  • 37
  • This looks interesting. For a +50 reputation, could you please provide an answer with my MCVE updated and working to prove this fully works? – jpo38 Jan 11 '19 at 17:51
  • Hi Benjamin, I tried to use `QwtPlotDirectPainter` within my example. I could easily change a point color from black to red when the mouse is close to it using `QwtPlotDirectPainter` and not calling `replot`. Fine. But I don't see how to change the point color back from red to black when the mouse becomes closer to a different point without replotting the whole `QwtPlot`, or at least the whole `QwtPlotCurve`. I feel like this would imply deep change in my code while it apparently almost work: as I said "if I deactivate the window, I see the point gets highlighted..." – jpo38 Jan 14 '19 at 13:53
  • @jpo38 I have added some details in my answer about how you can "unselect" a point. I will not bother update your MCVE as my answer already has to its own MCVE (i.e. the event_filter" example from Qwt). – Benjamin T Jan 14 '19 at 14:18
  • The problem is that this will only hide/show a given plot of a given curve. But if the plot area has many curves being displayed, and some of them having some drawing being "behind" the selected item being shown/hidden, I doubt they will get replotted when this the selected item will be painted/unpainted. That's why I think the right way to do this is to ask to invalidate a given QRect. Else, I would need to find all piece of series of all curves around the modified one that would need to be repainted.... – jpo38 Jan 14 '19 at 19:24
  • I just tested, changed the position of curves in event_filter example. I now have magenta curve that overlaps blue curve. And I confirm that if I select/unselect a magenta point, the blue curve overlapping items does not get repainted as expected... – jpo38 Jan 14 '19 at 19:54
  • @jpo38 I just had some time to play with the "event_filter" example. Indeed, the parts of the curves that are drawn with the `QwtPlotDirectPainter` are drawn on top of all other curves. This means that the curve ordering is not respected, until the next paint event. Sadly the API of Qwt does not seem to offer any partial repaint features. The `repaint(QRect)` function comes from `QWidget` and might not work properly for QwtPlot (bug?). Maybe `update()` can help, it is the same as calling `repaint()`, but with a performance benefit as multiple call to `update()` can lead to a single repaint. – Benjamin T Jan 17 '19 at 13:38
  • I tried both `update()` and `repaint()` and I confirm they do not behave as expected with Qwt. You are right, the right answer to my question could have been ` the API of Qwt does not seem to offer any partial repaint features`... – jpo38 Jan 17 '19 at 13:51