QCustomPlot Discussion and Comments

Square ZoomingReturn to overview

hi,
I'm trying to add a new feature zoom by selecting a desired area by holding left button of mouse and drag mouse to draw a square or rectangle then zoom on area that the rectangle or square has drawn, so I want to help me to add this important feature to this excellent library. my primary idea is to work on mouse release and mouse press events.
I want to ask every one (specially dear DerManu)to say their idea about this and how to do this in optimize and perfect way.

best regards,
alireza aramoun.

Alternative zooming modes, including rectangle selection is planned for a version after 1.2, possibly 1.3. If it requires alteration of existing public interface, it will have to wait for version 2.0. The best way to implement it in current versions is to use the QCPItemRect and the mouse signals that QCustomPlot emits. But being an ad-hoc solution to the problem, this isn't the way it should be done in a permanent solution in the QCP code.

My current plan outline to implement it is as follows:
- QCPAxisRect should have an enum property that defines the mechanism of zooming/range dragging.
- Depending on that, the QCPAxisRect reacts differently to the QCPLayoutElement::mousePress/Move/ReleaseEvents that it receives.
- An elegant mechanism must be found to overlay all objects with the selection rectangle (without messing with the item lists, because that could have negative impact on user code when suddenly the number of items change when a selection rect becomes visible). The easiest way could be to make QCP query all axis rects whether they need to draw a selection rectangle, at the end of the QCustomPlot::draw method. A more general version could be that QCP queries all layout elements to ask whether they want to draw any kind of overlay. This would make the feature future-proof, when for example other layout elements need to draw some sort of mouse interaction indicator (e.g. a color gradient for colormaps might want to show color-range selection in some special way).

As you can see, implementing that feature requires some insight into the mechanics of QCP and alot of tricky design decisions, to make it useful for permanent integration into the QCP code. Ad-hoc solutions wouldn't be beneficial to the project in the long run, that's why I'm very picky about this stuff :).

thank you very much for your support.

Hi there,

I started using new to QCustomPlot last week-end. I think it's a great library, I was able to customize some stuff in a few minutes, unlike other similar libraries I tried for a while.

One of the first feature I needed for my use-case was the square zoom. I'm looking forward to using the official feature, for the moment I'd like to share with you what I did.

Note: For simplicity, I consider a QMainWindow containing all the stuff.

I used a QRubberBand, as suggested here: http://www.qtcentre.org/threads/51544-QRubberband-and-QcustomPlot.

My QMainWindow has a QRubberBand* (rubberBand) and a QPoint (rubberOrigin - the current "origin" of it). Unlike the link above, I directly created the QRubberBand in the QMainWindow's ctor:

rubberBand = new QRubberBand(QRubberBand::Rectangle, customPlot);

Then I wrote the code for mouse events:

void MyWindow::mousePress( QMouseEvent *mevent )
{
	rubberOrigin = mevent->pos();
	
	if(mevent->button() == Qt::RightButton)
        {
		rubberBand->setGeometry(QRect(rubberOrigin, QSize()));
		rubberBand->show();
	}
...
}

void MyWindow::mouseMove( QMouseEvent* mevent )
{
	rubberBand->setGeometry(QRect(rubberOrigin, mevent->pos()).normalized());
	...
}

The main job is done in the mouseRelease handler: first I convert the coordinates of the rubber band (in pixel) to the local coordinates of each axes, then I change the axes' ranges. In my example I have just an X axis (e.g. a time axis) and several Y axes (one for each graph of my plot):

void MyWindow::mouseRelease( QMouseEvent* mevent )
{
	if (rubberBand->isVisible())
	{
		auto zoomRect = rubberBand->geometry();
		int xp1, yp1, xp2, yp2;
		zoomRect.getCoords(&xp1, &yp1, &xp2, &yp2);
		auto x1 = ui.customPlot->xAxis->pixelToCoord(xp1);
		auto x2 = ui.customPlot->xAxis->pixelToCoord(xp2);
		// I have a common x axis
		ui.customPlot->xAxis->setRange(x1, x2);
		
		auto numOfY = ui.customPlot->axisRect(0)->axisCount(QCPAxis::atLeft);
		for(auto i=0; i<numOfY; ++i)
		{
			auto currentY = ui.customPlot->axisRect(0)->axis(QCPAxis::atLeft, i);
			currentY->setRange(currentY->pixelToCoord(yp1), currentY->pixelToCoord(yp2));
		}
		
		rubberBand->hide();
		ui.customPlot->replot();
	}
}

To generalize this example for several X axes I think you can wrote the same loop I used for the Y axes.

An easy way to implement zoom-in/zoom-out: use a stack. Push the range values (or just the rect and then recalculate each range) on zoom-in, pop them on zoom-out.

Hope can be useful,

Marco

thanks for your answer,
what do you think about code below that strike to my mind?
does this code need some optimization or is it true to do this for rectangle zooming??
I've used rectangle element instead of QRubberBand and I've used signal slot mechanism instead of reimplementing mouse-events, and wrote something like that:

first connecting signals and slots to together:

   connect(ui->widget, SIGNAL(mousePress(QMouseEvent*)), this, SLOT(slt_pressQCP(QMouseEvent*)));
   connect(ui->widget, SIGNAL(mouseRelease(QMouseEvent*)), this, SLOT(slt_releaseQCP(QMouseEvent*)));
   connect(ui->widget, SIGNAL(mouseMove(QMouseEvent*)), this, SLOT(slt_moveQCP(QMouseEvent*)));

then slot's codes, rectZoom is QCPItemRect:

void MainWindow::slt_pressQCP(QMouseEvent *mouse)
{
    double X = ui->widget->xAxis->pixelToCoord(mouse->x());
    double Y = ui->widget->yAxis->pixelToCoord(mouse->y());
    rectZoom->topLeft->setCoords(X, Y);
    topleftRect.setX(X);
    topleftRect.setY(Y);
    rectZoom->setVisible(true);
//    rectZoom->bottomRight->setCoords(1, 1);
    isQCPPressed = true;
//    ui->widget->replot();
    bottomRightRect.setX(0);
    bottomRightRect.setY(0);
}

void MainWindow::slt_releaseQCP(QMouseEvent *)
{
    if( (bottomRightRect.x() == 0) || (bottomRightRect.y() == 0))
    {
        rectZoom->setVisible(false);
        return;
    }

    ui->widget->xAxis->setRange(topleftRect.x(), bottomRightRect.x());
    ui->widget->yAxis->setRange(topleftRect.y(), bottomRightRect.y());
    rectZoom->topLeft->setCoords(0,0);
    rectZoom->bottomRight->setCoords(0,0);
    isQCPPressed = false;

    rectZoom->setVisible(false);
    ui->widget->replot();
}

void MainWindow::slt_moveQCP(QMouseEvent *mouse)
{
    if(isQCPPressed)
    {
        double X = ui->widget->xAxis->pixelToCoord(mouse->x());
        double Y = ui->widget->yAxis->pixelToCoord(mouse->y());
        rectZoom->bottomRight->setCoords(X, Y);
        ui->widget->replot();
        bottomRightRect.setX(X);
        bottomRightRect.setY(Y);
    }
}

Hi,
I started working on the interactions on my side too.. And I added a rectangle zoom. I was waiting for my whole code to be fully mature to send a merge request.
If you want to test it, clone it from gitorious :
https://gitorious.org/qcustomplot/wawanbretons-qcustomplot

Then you can activate the rect zoom with the standard setInteractions method, I added a value on the enumeration.
DerManu, I will send you a private mail to argue on my changes and discuss with you if they are good enough to be integrated.

I've implemented square zooming by subclassing QCustomPlot (thank to previous posts).
I think this is a little better than put zooming code to MainWindow class.

CustomPlotZoom.h

#pragma once

#include <QPoint>
#include "../3rd_party/qcustomplot/qcustomplot.h"

class QRubberBand;
class QMouseEvent;
class QWidget;

class CustomPlotZoom : public QCustomPlot
{
    Q_OBJECT

public:
    CustomPlotZoom(QWidget * parent = 0);
    virtual ~CustomPlotZoom();

    void setZoomMode(bool mode);

private slots:
    void mousePressEvent(QMouseEvent * event) override;
    void mouseMoveEvent(QMouseEvent * event) override;
    void mouseReleaseEvent(QMouseEvent * event) override;

private:
    bool mZoomMode;
    QRubberBand * mRubberBand;
    QPoint mOrigin;
}

CustomPlotZoom.cpp

#include <QRubberBand>
#include "CustomPlotZoom.h"

CustomPlotZoom::CustomPlotZoom(QWidget * parent)
    : QCustomPlot(parent)
    , mZoomMode(false)
    , mRubberBand(new QRubberBand(QRubberBand::Rectangle, this))
{}

CustomPlotZoom::~CustomPlotZoom()
{
    delete mRubberBand;
}

void CustomPlotZoom::setZoomMode(bool mode)
{
    mZoomMode = mode;
}

void CustomPlotZoom::mousePressEvent(QMouseEvent * event)
{
    if (mZoomMode)
    {
        if (event->button() == Qt::RightButton)
        {
            mOrigin = event->pos();
            mRubberBand->setGeometry(QRect(mOrigin, QSize()));
            mRubberBand->show();
        }
    }
    QCustomPlot::mousePressEvent(event);
}

void CustomPlotZoom::mouseMoveEvent(QMouseEvent * event)
{
    if (mRubberBand->isVisible())
    {
        mRubberBand->setGeometry(QRect(mOrigin, event->pos()).normalized());
    }
    QCustomPlot::mouseMoveEvent(event);
}

void CustomPlotZoom::mouseReleaseEvent(QMouseEvent * event)
{
    if (mRubberBand->isVisible())
    {
        const QRect & zoomRect = mRubberBand->geometry();
        int xp1, yp1, xp2, yp2;
        zoomRect.getCoords(&xp1, &yp1, &xp2, &yp2);
        auto x1 = xAxis->pixelToCoord(xp1);
        auto x2 = xAxis->pixelToCoord(xp2);
        auto y1 = yAxis->pixelToCoord(yp1);
        auto y2 = yAxis->pixelToCoord(yp2);

        xAxis->setRange(x1, x2);
        yAxis->setRange(y1, y2);

        mRubberBand->hide();
        replot();
    }
    QCustomPlot::mouseReleaseEvent(event);
}

this is a nice work, Serg.
but something is not clear for me that why does n't use rectangle element instead of QRubberBand ?!!

Really a great work, Serg!

The code works fine.

@aramoun: If you have an implementation with a rectangle some time in the future, please let us know.

#ifndef _MY_WIDGET_H
#define _MY_WIDGET_H

#include <qapplication.h>
#include <qpushbutton.h>
#include <qpainter.h>
#include <qpixmap.h>

class MyWidget : public QWidget
{
Q_OBJECT

public:
MyWidget(QWidget* parent = 0, const char* name = 0);
~MyWidget();

// to render the image onto the widget
void paintImage();

/* paint handler */
void paintEvent(QPaintEvent* e);

/* mouse event handler */
void mousePressEvent(QMouseEvent* e);

// TODO: Add the mouse move and mouse release event handlers

public slots:
// to save the image into a bitmap file
void saveImage();

private:
QPainter* painter;
QPixmap* image;

QPushButton* saveButton;

// for storing the size of the margin to the top and the left edge of the window
int margin_x, margin_y;

// for storing the mouse press position
int prev_x, prev_y;
};

#endif
this is my widget.h file

#include "my_widget.h"
#include <qpen.h>
#include <qbrush.h>
#include <iostream>

using namespace std;

/************************************/
// Constructor & Destructor
/************************************/

MyWidget::MyWidget(QWidget* parent, const char* name) : QWidget(parent, name) {

// create the QPainter for the widget
painter = new QPainter(this);

// create a pixmap image
image = new QPixmap(400, 300);
// fill the image with white color
image->fill(Qt::white);

// set the margin size
margin_x = 50;
margin_y = 50;

// create the push button to save the image into a bitmap file
saveButton = new QPushButton("Save Image", this);
QObject::connect(saveButton, SIGNAL(clicked()), this, SLOT(saveImage()));
saveButton->setGeometry(150, 370, 200, 30);

// set the size of the window
resize(500, 450);
}

MyWidget::~MyWidget(){

delete painter;
delete image;
delete saveButton;
}

/************************************/
// Mouse Event Handlers
/************************************/

// Mouse Press Event Handler
void MyWidget::mousePressEvent(QMouseEvent * e) {

// check if the button that caused the event the right button of the mouse
if ( e->button() == Qt::RightButton ) {

// copy the existing pixmap image back to the widget
paintImage();

// create a QPen object with black line color, width size of 5 and solid line style
QPen pen(Qt::black, 5, Qt::SolidLine);

// create a QBrush object with blue fill color, and solid pattern fill style
QBrush brush(Qt::blue, Qt::SolidPattern);

// close the active painter
if ( painter->isActive() )
painter->end();

// begins painting the widget
painter->begin(this);
painter->setPen(pen); // set the pen of the painter
painter->setBrush(brush); // set the brush of the painter
painter->setClipRect(margin_x, margin_y, 400, 300); // enable drawing with a rectangular area only
painter->drawRect(e->x(), e->y(), 80, 120); // draw a rectangle of size 80 by 120
painter->end();

// begins painting the pixmap image
painter->begin(image);
painter->setPen(pen); // set the pen of the painter
painter->setBrush(brush); // set the brush of the painter
painter->drawRect(e->x() - margin_x, e->y() - margin_y, 80, 120); // draw a rectangle of size 80 by 120
painter->end();

} else if ( e->button() == Qt::LeftButton ) {
// TODO: implement the actions for mouse left button press

}
}

// Mouse Move Event Handler
// TODO: implement the mouse move event handler

// Mouse Release Event Handler
// TODO: implement the mouse release event handler


/************************************/
// Paint Event Handler
/************************************/
void MyWidget::paintEvent(QPaintEvent *) {

// copy the existing pixmap image back to the widget
paintImage();

}

/************************************/
// Member Functions
/************************************/

// paintImage() function is to copy the existing pixmap image back to the widget
void MyWidget::paintImage() {

// close the active painter
if ( painter->isActive() ) {
painter->end();
}
// begin drawing on the widget
painter->begin(this);

// draw the pixmap from the image QPixmap object to the widget
if ( !image->isNull() ) {
painter->drawPixmap(margin_x, margin_y, (*image));
}

painter->end();
}

// paintImage() function is to save the pixmap image into a bitmap image file, named "temp.bmp"
void MyWidget::saveImage() {

image->save("temp.bmp", "BMP");
}
this is my widget.cpp

I am really new to programming in c++ and Qt
I have to modify the my_widget.h and my_widget.cpp such that the user can draw a rectangle in different sizes by mouse drag. Hence, when a user clicks and drags the left mouse button, a rectangle will be drawn until the user releases the button. The size of the rectangle will depend on how far the user drags the mouse.

I really don't understand how to start. can some1 help me with this ?

Nice work, Serg, thanks! To be able to rescale the axes (zoom out to the full data range) by using a single rightclick, which some might find useful, I added the <cmath> header (for the abs function) and replaced Serg's code CustomPlotZoom.cpp in line 55-56 with:

if(std::abs(xp1-xp2)<=1)
{
   rescaleAxes();
}
else{
   xAxis->setRange(x1, x2);
   yAxis->setRange(y1, y2);
}

Note, that it basically just detects if the RubberBand is only one pixel in size, which seems to be the minimum size once it's created. There might however be more sophisticated approaches.

Best regards,

Joe

In Serg example, if you use QRectF with double variables instead of QRect and int variables, you can make more precise zooming.

we can use zooming event only for mouse event if i want to use zooming for touch screen how can i do?