In my program I'm trying to map the coordinates of a mousepress back to the coordinate dimensions of an image. I'm using PyQt4 in Python. The program below demonstrates the problem. I have a widget that makes a few image transformations. After those image transformations the image is shown in the center of the widget, while maintaining the original aspect ratio of the image. Since the image is scaled and translated, a coordinate of a MouseEvent must be remapped to the coordinate system of the Image.
The program below has a class "ScalingWidget", that should be able to do these transformations and should also be able to remap the coordinate in a mouseReleaseEvent back to the coordinate system of the image. This works perfectly as expected when I show the widget outside a Layout and mainwindow, but it gets evil when I embed the widget in a bigger gui. Then the coordinates after mapping them back to the Image coordinates suddenly displays an offset.
The minimal program below can be started with and without the bug by specifing flag -b when starting the program. The option -n can put the instance of ScalingWidget deep and deeper inside a "gui", and the deeper it is embedded in the layouts the more strong the bug will be visible. The stupid thing is, although drawing indicates that the transformations are correct, the mapped coordinates (printed in the window title and console) indicate that remapping them back to the image coordinates is screwed up when the -b flag is present.
So my question is: What am I doing wrong with remapping the mouse coordinates back to the image dimensions when my ScalingWidget is embedded in a layout?
I don't expect the remapping to be pixel perfect, but just as accurate as the end user can position the mouse. There are two points x=20, y=20 and at x=380 and y=380 these can be used as reference point.
Any help is most welcome!
#!/usr/bin/env python
from PyQt4 import QtGui
from PyQt4 import QtCore
import sys
import argparse
class ScalingWidget (QtGui.QWidget):
''' Displays a pixmap optimally in the center of the widget, in such way
the pixmap is shown in the middle
'''
white = QtGui.QColor(255,255,255)
black = QtGui.QColor( 0, 0, 0)
arcrect = QtCore.QRect(-10, -10, 20, 20)
def __init__(self):
super(ScalingWidget, self).__init__()
self.pixmap = QtGui.QPixmap(400, 400)
painter = QtGui.QPainter(self.pixmap)
painter.fillRect(self.pixmap.rect(), self.white)
self.point1 = QtCore.QPoint(20, 20)
self.point2 = QtCore.QPoint(380, 380)
painter.setPen(self.black)
painter.drawRect(QtCore.QRect(self.point1, self.point2))
painter.end()
self.matrix = None
def sizeHint(self):
return QtCore.QSize(500,400)
##
# Applies the default transformations
#
def _default_img_transform(self, painter):
#size of widget
winheight = float(self.height())
winwidth = float(self.width())
#size of pixmap
scrwidth = float(self.pixmap.width())
scrheight = float(self.pixmap.height())
assert(painter.transform().isIdentity())
if scrheight <= 0 or scrwidth <= 0:
raise RuntimeError(repr(self) + "Unable to determine Screensize")
widthr = winwidth / scrwidth
heightr = winheight / scrheight
if widthr > heightr:
translate = (winwidth - heightr * scrwidth) /2
painter.translate(translate, 0)
painter.scale(heightr, heightr)
else:
translate = (winheight - widthr * scrheight) / 2
painter.translate(0, translate)
painter.scale(widthr, widthr)
# now store the matrix used to map the mouse coordinates back to the
# coordinates of the pixmap
self.matrix = painter.deviceTransform()
def paintEvent(self, e):
painter = QtGui.QPainter(self)
painter.setClipRegion(e.region())
# fill the background of the entire widget.
painter.fillRect(self.rect(), QtGui.QColor(0,0,0))
# transform to place the image nicely in the center of the widget.
self._default_img_transform(painter)
painter.drawPixmap(self.pixmap.rect(), self.pixmap, self.pixmap.rect())
pen = QtGui.QPen(QtGui.QColor(255,0,0))
# Just draw on the points used to make the black rectangle of the pix map
# drawing is not affected, be remapping those coordinates with the "same"
# matrix is.
pen.setWidth(4)
painter.setPen(pen)
painter.save()
painter.translate(self.point1)
painter.drawPoint(0,0)
painter.restore()
painter.save()
painter.translate(self.point2)
painter.drawPoint(0,0)
painter.restore()
painter.end()
def mouseReleaseEvent(self, event):
x, y = float(event.x()), float(event.y())
inverted, invsucces = self.matrix.inverted()
assert(invsucces)
xmapped, ymapped = inverted.map(x,y)
print x, y
print xmapped, ymapped
self.setWindowTitle("mouse x,y = {}, {}, mapped x, y = {},{} "
.format(x, y, xmapped, ymapped)
)
def start_bug():
''' Displays the mouse press mapping bug.
This is a bit contrived, but in the real world
a widget is embedded in deeper in a gui
than a single widget, besides the problem
grows with the depth of embedding.
'''
app = QtGui.QApplication(sys.argv)
win = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
win.setLayout(layout)
widget = None
for i in range(0, args.increase_bug):
if i < args.increase_bug-1:
widget = QtGui.QWidget()
layout.addWidget(widget)
layout= QtGui.QVBoxLayout()
widget.setLayout(layout)
else:
layout.addWidget(ScalingWidget())
win.show()
sys.exit(app.exec_())
def start_no_bug():
''' Does not show the mapping bug, the mouse event.x() and .y() map nicely back to
the coordinate system of the pixmap
'''
app = QtGui.QApplication(sys.argv)
win = ScalingWidget()
win.show()
sys.exit(app.exec_())
# parsing arguments
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-b', '--display-bug', action='store_true',
help="Toggle this option to get the bugged version"
)
parser.add_argument('-n', '--increase-bug', type=int, default=1,
help="Increase the bug by n times."
)
if __name__ == "__main__":
args = parser.parse_args()
if args.display_bug:
start_bug()
else:
start_no_bug()
The basic idea of the _default_image_transform is correct. The error is in the end of the function.
def _default_img_transform(self, painter):
#size of widget
winheight = float(self.height())
winwidth = float(self.width())
#size of pixmap
scrwidth = float(self.pixmap.width())
scrheight = float(self.pixmap.height())
assert(painter.transform().isIdentity())
if scrheight <= 0 or scrwidth <= 0:
raise RuntimeError(repr(self) + "Unable to determine Screensize")
widthr = winwidth / scrwidth
heightr = winheight / scrheight
if widthr > heightr:
translate = (winwidth - heightr * scrwidth) /2
painter.translate(translate, 0)
painter.scale(heightr, heightr)
else:
translate = (winheight - widthr * scrheight) / 2
painter.translate(0, translate)
painter.scale(widthr, widthr)
# now store the matrix used to map the mouse coordinates back to the
# coordinates of the pixmap
self.matrix = painter.deviceTransform() ## <-- error is here
The last line of the function _default_image_transform
should be:
self.matrix = painter.transform()
According to the documentation one should only call the QPainter.deviceTransform()
when you are working with QT::HANDLE which is an platform dependent handle. Since I wasn't working with a platform dependent handle I shouldn't have called it. It works out when I show the widget, but not when it is embedded in a layout. Then the deviceTransform matrix is different from the normal QPainter.transform() matrix. See also http://doc.qt.io/qt-4.8/qpainter.html#deviceTransform