Zooming into Pixmap about mouse pointer
-
Further to an earlier question. When I roll the mouse wheel to zoom into the pixmap, I wish to zoom into it so that the zoom is centred on the mouse position (or the position within the scaled Pixmap that's closest to the mouse pointer).
Currently the zoom is all relative to the origin (top left).
My code sets the basic scaling in resizeEvent() and the zoom factor in wheelEvent():
void DSSImageWidget::resizeEvent(QResizeEvent* e) { QSize sz = e->size(); qreal hScale = (qreal)sz.width() / (m_pixmap.width() + 4); qreal vScale = (qreal)sz.height() / (m_pixmap.height() + 4); m_scale = std::min(hScale, vScale); update(); Inherited::resizeEvent(e); } void DSSImageWidget::wheelEvent(QWheelEvent* e) { qreal degrees = -e->angleDelta().y() / 8.0; qreal steps = degrees / 60.0; qreal factor = m_factor * std::pow(1.125, steps); m_factor = std::clamp(factor, 1.0, 5.0); update(); }
Which is fine but how do I adjust my paintEvent code to do that? I'm sort of guessing that I need to save the mouse position in wheelEvent and do some tricksy stuff in paintEvent(), but I'm very unclear what's needed.
My paint code (stolen from the affine sample) currently looks like:
void DSSImageWidget::paintEvent(QPaintEvent* event) { QPainter painter; painter.begin(this); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); QPointF center(m_pixmap.width() / qreal(2), m_pixmap.height() / qreal(2)); //painter.translate(center); painter.scale(m_factor*m_scale, m_factor*m_scale); //painter.translate(-center); painter.drawPixmap(QPointF(0, 0), m_pixmap); painter.setPen(QPen(QColor(255, 0, 0, alpha), 0.25, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin)); painter.setBrush(Qt::NoBrush); painter.drawRect(QRectF(0, 0, m_pixmap.width(), m_pixmap.height()).adjusted(-2, -2, 2, 2)); painter.end(); }
Thanks
David -
I finally beat this into submission, here are the critical parts of the code:
in the header file:
typedef QWidget Inherited; private: bool initialised; qreal m_scale, m_zoom; QPointF m_origin; QPixmap & m_pixmap; QPointF m_pointInPixmap; inline bool mouseOverImage(QPointF loc) { qreal x = loc.x(), y = loc.y(), ox = m_origin.x(), oy = m_origin.y(); return ( (x >= ox) && (x <= ox + (m_pixmap.width() * m_scale)) && (y >= oy) && (y <= oy + (m_pixmap.height() * m_scale))); };
And the main C++ code:
DSSImageWidget::DSSImageWidget(QPixmap& p, QWidget* parent) : QWidget(parent), initialised(false), m_scale(1.0), m_zoom(1.0), m_origin(0.0, 0.0), m_pixmap(p), m_pointInPixmap((m_pixmap.width() / 2), (m_pixmap.height() / 2)) { setAttribute(Qt::WA_MouseTracking); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } void DSSImageWidget::resizeEvent(QResizeEvent* e) { QSize sz = e->size(); qreal pixWidth = m_pixmap.width(); qreal pixHeight = m_pixmap.height(); qreal hScale = (qreal)sz.width() / pixWidth; qreal vScale = (qreal)sz.height() / pixHeight; m_scale = std::min(hScale, vScale); qreal xoffset = 0.0, yoffset = 0.0; if ((pixWidth * m_scale) < sz.width()) { xoffset = (sz.width() - (pixWidth * m_scale)) / 2.0; } if ((pixHeight * m_scale) < sz.height()) { yoffset = (sz.height() - (pixHeight * m_scale)) / 2.0; } m_origin = QPointF(xoffset, yoffset); update(); Inherited::resizeEvent(e); } void DSSImageWidget::paintEvent(QPaintEvent* event) { QPainter painter; qDebug() << "pointInPixmap: " << m_pointInPixmap.x() << m_pointInPixmap.y(); // // Now calcualate the rectangle we're interested in // qreal width = m_pixmap.width(); qreal height = m_pixmap.height(); qreal x = m_pointInPixmap.x(); qreal y = m_pointInPixmap.y(); QRectF sourceRect( x - (x / m_zoom), y - (y / m_zoom), width / m_zoom, height / m_zoom ); qDebug() << "sourceRect: " << sourceRect.x() << sourceRect.y() << sourceRect.width() << sourceRect.height(); // // Now calculate the rectangle that is the intersection of this rectangle and the pixmap's rectangle. // sourceRect &= m_pixmap.rect(); painter.begin(this); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.translate(m_origin); painter.scale(m_zoom*m_scale, m_zoom*m_scale); painter.translate(-m_origin); //painter.drawPixmap(QPointF(0.0, 0.0), m_pixmap, sourceRect); painter.drawPixmap(m_origin, m_pixmap, sourceRect); painter.end(); } #if QT_CONFIG(wheelevent) void DSSImageWidget::wheelEvent(QWheelEvent* e) { qreal degrees = -e->angleDelta().y() / 8.0; // // If zooming in and zoom factor is currently 1.0 // then remember mouse location // if ((degrees > 0) && (m_zoom == 1.0)) { QPointF mouseLocation = e->position(); if (mouseOverImage(mouseLocation)) { m_pointInPixmap = QPointF((mouseLocation-m_origin) / m_scale); } else { m_pointInPixmap = QPointF((m_pixmap.width() / 2), (m_pixmap.height() / 2)); } } qreal steps = degrees / 60.0; qreal factor = m_zoom * std::pow(1.125, steps); m_zoom = std::clamp(factor, 1.0, 5.0); if (degrees < 0 && m_zoom == 1.0) { m_pointInPixmap = QPointF((m_pixmap.width() / 2), (m_pixmap.height() / 2)); } update(); Inherited::wheelEvent(e); } #endif
Hope this helps someone else. David
-
Is there anyone who can help with this - I'm sure that to some of you this may seem like a stupid beginner's question, but I'm rather struggling to understand how to scale a pixmap to a window (that part works) and then zoom in with the part of the pixmap under the mouse pointer remaining in place.
Thanks
David -
There are multiple ways of attacking this problem, but the best approach will be determined by the size of the pixmap. If it is large, like those associated with maps, then the best approach is to use something like wavelet compression or tile the map. For small pixmaps the easiest method is to scale the pixmap and then use a rectangle to copy only that portion of the pixmap to the widget. The other way is to select the area to be drawn and scale it.
-
I don't think you've understood my intent. If I position the mouse over a star in the image (as these are astrophotographic images), and rotate the mouse wheel I want the star to remain under the mouse pointer and image to be zoomed around it.
The code I have works to the extent that it zooms the image but only about the top left corner which is the bit that doesn't move when the pixmap is zoomed.
-
I understand and what I stated will do that, it just did not stat the iterations required. At each mouse wheel move event, you must find the location of the mouse. Generally that is a scale reduction or increase for each direction of the mouse wheel rotation. Thus you have what you require. Increment the scale for the increase/decrease and scale the pixmap. Now you must create a rectangle that is centered on the current mouse location which you will use the select a portion of the scaled pixmap to draw.
I just saw your statement about the top left corner is stationary and that means that the rectangle you are using is not centered on the mouse location. Just move the center of that rectangle to the mouse location. Try that.
-
I think I've got a conceptual gap here (mine). The rectangle I've got to play with is the Widget's rectangle. I already have the mouse position (that's recorded as the zoom is started).
So where does this rectangle centred on the mouse come from (yes, OK I create that programmatically - I assume the same size as my widget), how do I map it back into the original drawing rectangle of the widget.
Thanks
David -
@Perdrix Remember that Qt uses a coordinate system that has the top left corner as 0,0 with the y-axis increasing downward and the x-axis increasing to the right.
You are using painter.drawPixmap(QPointF(0, 0), m_pixmap); to draw the pixmap which means that the scaled pixmap is drawn starting at the top left, try drawing not from 0,0 but from -center x, -center y. That should shift the pixmap so that center point is now centered.
-
I changed the code to read:
QPainter painter; painter.begin(this); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); //QPointF whereScaled = m_where / (m_zoom * m_scale); //qDebug() << "m_where:" << m_where.x() << m_where.y(); //qDebug() << whereScaled.x() << " " << whereScaled.y(); //painter.translate(-m_where); painter.scale(m_zoom*m_scale, m_zoom*m_scale); //painter.translate(m_where); painter.drawPixmap(QPointF(-m_where.x(), -m_where.y()), m_pixmap); painter.setPen(QPen(QColor(255, 0, 0, alpha), 0.25, Qt::SolidLine, Qt::FlatCap, Qt::BevelJoin)); painter.setBrush(Qt::NoBrush); painter.drawRect(QRectF(0, 0, m_pixmap.width(), m_pixmap.height()).adjusted(-2, -2, 2, 2)); painter.end();
But that didn't work :(
-
The problem, as I see it, is that draw pixmap method that you are using will draw the pixmap at the given point. Look at the method drawPixmap(target rectangle, pixmap, source rectangle). In your case the target rectangle is the widget rectangle and the source rectangle is that portion of the astrophotographic image that is centered on the star. The mouse location is relative to the widget, but the source rectangle must be centered on the star location in the image and not the mouse location on the widget. That means that some calculations starting with the dimensions of the image (pixmap) must be used to map a point on the screen to a point on the image, the star location. That mapping is what you are missing.
How do you calculate the mapping? You must reverse all of the transformations that have been made to the image as it is currently displayed to reach a starting point or define a starting point, e.g., require that the star be selected when the image is first displayed. The question is do you want the zooming the work for any case or do you want the zooming to work only from a specified starting point--the latter is the simple case.
However there is another complicating factor the aspect ratio mismatch. In all likelihood the aspect ratios of the rectangle of the widget and the image will no match, so to solve that problem use the drawPixmap method that uses point target, pixmap and source rectangle. I am not sure if any scaling is involved with this method.
I have probably given you more information about the problem than you wanted to know, but I hope you find it helpful.
I would suggest that you start with a very simple example that you can expand upon. If the images you provided display the complete image then the mapping is a simple ratio of the mouse location on the widget to the dimensions of image to find the star location. Try that.
-
I finally beat this into submission, here are the critical parts of the code:
in the header file:
typedef QWidget Inherited; private: bool initialised; qreal m_scale, m_zoom; QPointF m_origin; QPixmap & m_pixmap; QPointF m_pointInPixmap; inline bool mouseOverImage(QPointF loc) { qreal x = loc.x(), y = loc.y(), ox = m_origin.x(), oy = m_origin.y(); return ( (x >= ox) && (x <= ox + (m_pixmap.width() * m_scale)) && (y >= oy) && (y <= oy + (m_pixmap.height() * m_scale))); };
And the main C++ code:
DSSImageWidget::DSSImageWidget(QPixmap& p, QWidget* parent) : QWidget(parent), initialised(false), m_scale(1.0), m_zoom(1.0), m_origin(0.0, 0.0), m_pixmap(p), m_pointInPixmap((m_pixmap.width() / 2), (m_pixmap.height() / 2)) { setAttribute(Qt::WA_MouseTracking); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } void DSSImageWidget::resizeEvent(QResizeEvent* e) { QSize sz = e->size(); qreal pixWidth = m_pixmap.width(); qreal pixHeight = m_pixmap.height(); qreal hScale = (qreal)sz.width() / pixWidth; qreal vScale = (qreal)sz.height() / pixHeight; m_scale = std::min(hScale, vScale); qreal xoffset = 0.0, yoffset = 0.0; if ((pixWidth * m_scale) < sz.width()) { xoffset = (sz.width() - (pixWidth * m_scale)) / 2.0; } if ((pixHeight * m_scale) < sz.height()) { yoffset = (sz.height() - (pixHeight * m_scale)) / 2.0; } m_origin = QPointF(xoffset, yoffset); update(); Inherited::resizeEvent(e); } void DSSImageWidget::paintEvent(QPaintEvent* event) { QPainter painter; qDebug() << "pointInPixmap: " << m_pointInPixmap.x() << m_pointInPixmap.y(); // // Now calcualate the rectangle we're interested in // qreal width = m_pixmap.width(); qreal height = m_pixmap.height(); qreal x = m_pointInPixmap.x(); qreal y = m_pointInPixmap.y(); QRectF sourceRect( x - (x / m_zoom), y - (y / m_zoom), width / m_zoom, height / m_zoom ); qDebug() << "sourceRect: " << sourceRect.x() << sourceRect.y() << sourceRect.width() << sourceRect.height(); // // Now calculate the rectangle that is the intersection of this rectangle and the pixmap's rectangle. // sourceRect &= m_pixmap.rect(); painter.begin(this); painter.setRenderHint(QPainter::Antialiasing); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.translate(m_origin); painter.scale(m_zoom*m_scale, m_zoom*m_scale); painter.translate(-m_origin); //painter.drawPixmap(QPointF(0.0, 0.0), m_pixmap, sourceRect); painter.drawPixmap(m_origin, m_pixmap, sourceRect); painter.end(); } #if QT_CONFIG(wheelevent) void DSSImageWidget::wheelEvent(QWheelEvent* e) { qreal degrees = -e->angleDelta().y() / 8.0; // // If zooming in and zoom factor is currently 1.0 // then remember mouse location // if ((degrees > 0) && (m_zoom == 1.0)) { QPointF mouseLocation = e->position(); if (mouseOverImage(mouseLocation)) { m_pointInPixmap = QPointF((mouseLocation-m_origin) / m_scale); } else { m_pointInPixmap = QPointF((m_pixmap.width() / 2), (m_pixmap.height() / 2)); } } qreal steps = degrees / 60.0; qreal factor = m_zoom * std::pow(1.125, steps); m_zoom = std::clamp(factor, 1.0, 5.0); if (degrees < 0 && m_zoom == 1.0) { m_pointInPixmap = QPointF((m_pixmap.width() / 2), (m_pixmap.height() / 2)); } update(); Inherited::wheelEvent(e); } #endif
Hope this helps someone else. David