Rotated QGraphicsRectItem moves randomly when dragged using mouse - python

I have a movable QGraphicsRectItem which is rotated to 90 degrees and set to a scene. When I drag the item, it moves randomly and eventually disappear.
However, when I set the rotation to 0, the item moves flawlessly.
Here is my minimal reproducible example.
class main_window(QWidget):
def __init__(self):
super().__init__()
self.rect = Rectangle(100, 100, 100, 100)
self.rect.setRotation(90)
self.view = QGraphicsView(self)
self.scene = QGraphicsScene(self.view)
self.scene.addItem(self.rect)
self.view.setSceneRect(0, 0, 500,500)
self.view.setScene(self.scene)
self.slider = QSlider(QtCore.Qt.Horizontal)
self.slider.setMinimum(0)
self.slider.setMaximum(90)
vbox = QVBoxLayout(self)
vbox.addWidget(self.view)
vbox.addWidget(self.slider)
self.setLayout(vbox)
self.slider.valueChanged.connect(self.rotate)
def rotate(self, value):
self.angle = int(value)
self.rect.setRotation(self.angle)
class Rectangle(QGraphicsRectItem):
def __init__(self, *args):
super().__init__(*args)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsScenePositionChanges, True)
self.setPen(QPen(QBrush(QtGui.QColor('red')), 5))
self.selected_edge = None
self.first_pos = None
self.click_rect = None
def mousePressEvent(self, event):
self.first_pos = event.pos()
self.rect_shape = self.rect()
self.click_rect = self.rect_shape
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
# Calculate how much the mouse has moved since the click.
self.pos = event.pos()
x_diff = self.pos.x() - self.first_pos.x()
y_diff = self.pos.y() - self.first_pos.y()
# Start with the rectangle as it was when clicked.
self.rect_shape = QtCore.QRectF(self.click_rect)
self.rect_shape.translate(x_diff, y_diff)
self.setRect(self.rect_shape)
self.setTransformOriginPoint(self.rect_shape.center())
(I included a slider at the bottom of the main window to conveniently rotate the item)
Why does this happen?

The issue is caused by various aspects:
setting a QRect at given coordinates while keeping the item at the same default position (0, 0);
changing the rectangle as consequence of a mouse move event;
changing the transformation origin point after that;
the mapping of the mouse coordinates between integer based point (on the screen) and floating (on the scene);
the transformation (rotation);
implementing the item movement without considering the above (while QGraphicsItem already provides it with the ItemIsMovable flag);
Note that while rotation might seem a simple operation, it is achieved by using a combination of two transformations: shearing and scaling; this means that the transformation applies very complex computations that depend on the floating point precision.
This becomes an issue when dealing with integer to floating conversion: the same mouse (integer based) coordinate can be mapped at a very different point depending on the transformation, and the "higher" the transformation is applied, the bigger the difference can be. As a result, the mapped mouse position can be very different, the rectangle is translated to a "wrong" point, and the transformation origin point moves the rectangle "away" by an increasing ratio.
The solution is to completely change the way the rectangle is positioned and actually simplify the reference: the rectangle is always centered at the item position, so that we can keep the transformation origin point at the default (0, 0 in item coordinates).
The only inconvenience with this approach is that the item's pos() will not be on its top left corner anymore, but that is not a real issue: when the item is rotated, its top left corner would not be at that position anyway.
If you need to know the actual position of the item, you can then translate the rectangle based on the item scene position.
If you want to position the rectangle based on its top left corner, you have to map the position from the scene and compute the delta of the reference point (the actual top left corner).
I took the liberty of taking your previous question, which implemented the resizing, and improving it also to better show how the solution works.
class Selection(QtWidgets.QGraphicsRectItem):
Left, Top, Right, Bottom = 1, 2, 4, 8
def __init__(self, *args):
rect = QtCore.QRectF(*args)
pos = rect.center()
# move the center of the rectangle to 0, 0
rect.translate(-rect.center())
super().__init__(rect)
self.setPos(pos)
self.setPen(QtGui.QPen(QtCore.Qt.red, 5))
self.setFlags(
self.ItemIsMovable |
self.ItemIsSelectable |
self.ItemSendsGeometryChanges
)
def mapRect(self):
return QtCore.QRectF(
self.mapToScene(self.rect().topLeft()),
self.rect().size()
)
def setRectPosition(self, pos):
localPos = self.mapFromScene(pos)
delta = self.rect().topLeft() - localPos
self.setPos(self.pos() + delta)
def itemChange(self, change, value):
if change in (self.ItemPositionHasChanged, self.ItemRotationHasChanged):
print(self.mapRect())
return super().itemChange(change, value)
def mousePressEvent(self, event):
super().mousePressEvent(event)
pos = event.pos()
rect = self.rect()
margin = self.pen().width() / 2
self.anchor = 0
if pos.x() <= rect.x() + margin:
self.anchor |= self.Left
elif pos.x() >= rect.right() - margin:
self.anchor |= self.Right
if pos.y() <= rect.y() + margin:
self.anchor |= self.Top
elif pos.y() >= rect.bottom() - margin:
self.anchor |= self.Bottom
if self.anchor:
self.clickAngle = QtCore.QLineF(QtCore.QPointF(), pos).angle()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if not self.anchor:
super().mouseMoveEvent(event)
return
rect = self.rect()
pos = event.pos()
if self.anchor == self.Left:
rect.setLeft(pos.x())
elif self.anchor == self.Right:
rect.setRight(pos.x())
elif self.anchor == self.Top:
rect.setTop(pos.y())
elif self.anchor == self.Bottom:
rect.setBottom(pos.y())
else:
# clicked on a corner, let's rotate
angle = QtCore.QLineF(QtCore.QPointF(), pos).angle()
rotation = max(0, min(90, self.rotation() + self.clickAngle - angle))
self.setRotation(rotation)
return
pos = self.mapToScene(rect.center())
self.setPos(pos)
rect.moveCenter(QtCore.QPointF())
self.setRect(rect)

Related

QGraphicsPixmapItem zooming in/out

My application is for drawing shapes (polygons, points ..etc), I used QGrapichView and QGrapichScene and I added two different pixmap items (one for drawing, and the other one is 'legend' that will show the user how much the distance is), So far so good.
I implemented the zoom functionality and it is working fun. Still, my problem is: that when I zoom in/out the whole scene (the canvas QGraphicsPixmapItem and the legend QGraphicsPixmapItem) is affected and that is expected because I am re-scaling the QGrapichView while zooming (using the mouse wheel).
What I want is: I need only the canvas item to be zoomed in/out not the whole scene so that the legend will always be visible to the user.
Here is a snippet of the code that am using in zooming from this answer:
class PixmapScene(QGraphicsScene):
pass
class Canvas(QGraphicsView):
def __init__(self, scene):
super().__init__(scene)
self.scene = scene
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
background_color = QColor("#443b36")
self.pixmap_item: QGraphicsItem = self.scene.addPixmap(QPixmap(780, 580))
self.pixmap_item.setTransformationMode(Qt.FastTransformation)
self.pixmap_item.pixmap().fill(background_color)
self.legend = QPixmap(780, 580)
self.legend.fill(QColor("#00ffffff"))
p = QtGui.QPainter(self.legend)
p.setPen(QPen(QColor("#0000FF"), 4))
p.drawLine(35 * 5.1, self.legend.height() - 60, 35 * 8.9, self.legend.height() - 60)
p.setPen(QColor("#c9c9c9"))
p.setFont(QFont('Century Gothic', 14))
p.drawText(35 * 5.5, self.legend.height() - 35, f'this text is from the other pixmap (legend)')
p.end()
self.scene.addPixmap(self.legend)
self.zoom_times = 0
def wheelEvent(self, event):
zoom_in_factor = 1.25
zoom_out_factor = 1 / zoom_in_factor
# Save the scene pos
old_pos = self.mapToScene(event.pos())
# Zoom
if event.angleDelta().y() > 0:
if self.zoom_times == 6:
return
zoom_factor = zoom_in_factor
self.zoom_times += 1
else:
if self.zoom_times == 0:
return
zoom_factor = zoom_out_factor
self.zoom_times -= 1
# here we are scaling the whole scene, what I want is zooming with keeping legend as it is
self.scale(zoom_factor, zoom_factor)
# Get the new position
new_pos = self.mapToScene(event.pos())
# Move scene to old position
delta = new_pos - old_pos
self.translate(delta.x(), delta.y())

Keeping track of each point of each polygon on a canvas with tkinter

I'm making and UI for a Tangram (puzzle with different polygons) using tkinter in Python and I'd like to keep track of the coordinates of each point of each polygon when they move around my canvas.
To do so, I created this class:
class Polygon:
def __init__(self, coords, color, canvas):
self.coords = coords
self.color = color
self.move = False
canvas.bind('<Button-1>', self.start_movement)
canvas.bind('<Motion>', self.movement)
canvas.bind('<ButtonRelease-1>', self.stopMovement)
canvas.bind('<Button-3>', self.rotate)
canvas.create_polygon(self.coords, fill=self.color)
Each polygon is created this way :
medium_triangle = Polygon((0,0, 100*math.sqrt(2),0, 0,100*math.sqrt(2)),
'red', drawing_place)
small_triangle_1 = Polygon((0,0 ,100,0, 0,100), 'purple', drawing_place)
[...]
big_triangle_2 = Polygon((0,0, 200,0, 0,200), 'green', drawing_place)
My main problem is that it looks like I can only modify the coords attribute of the last Polygon created.
I use the mouse to drag the pieces on my canvas and I use these methods to make my Polygons move:
def start_movement(self, event):
self.move = True
# Translate mouse coordinates to canvas coordinate
self.initi_x = drawing_place.canvasx(event.x)
self.initi_y = drawing_place.canvasy(event.y)
self.movingimage = drawing_place.find_closest(self.initi_x, self.initi_y,
halo=1) # get canvas object
# ID of where mouse
# pointer is.
def movement(self, event):
if self.move:
end_x = drawing_place.canvasx(event.x) # Translate mouse x screen
# coordinate to canvas coordinate.
end_y = drawing_place.canvasy(event.y) # Translate mouse y screen
# coordinate to canvas coordinate.
deltax = end_x - self.initi_x # Find the difference
deltay = end_y - self.initi_y # Find the difference
self.newPosition(deltax, deltay)
self.initi_x = end_x # Update previous current with new location
self.initi_y = end_y
drawing_place.move(self.movingimage, deltax, deltay) # Move object
def stopMovement(self, event):
self.move = False
affichage(self)
I manage to add to my initial coordinates, the displacement which was carried out thanks to my new_position method :
def newPosition(self, deltax, deltay):
coord = self.coords # Retrieve object points coordinates
old_coord = list(coord) # Tuple to List
c = [] # New coords
i = 0 # Cursor on old_coord
for coordinates in old_coord:
# check if index of coordinates in range of i and len(old_coord)
# in old_coord is pair (x coord).
if (old_coord.index(coordinates, i, len(old_coord)) % 2) == 0:
c.append(coordinates + deltax)
else: # index's impair => y-coord
c.append(coordinates + deltay)
i += 1
coord2 = tuple(c) # List to Tuple
self.set_coords(coord2)
def set_coords(self, coords):
self.coords = coords
But as you can see right here in my console
just after medium_triangle declaration:
(0, 0, 141.4213562373095, 0, 0, 141.4213562373095)
(0, 0, 200, 0, 0, 200)
(0, 0, 200, 0, 0, 200)
(0, 0, 200, 0, 0, 200)
(0, 0, 200, 0, 0, 200)
(297.0, 61.0, 497.0, 61.0, 297.0, 261.0)
(551.0, 166.0, 751.0, 166.0, 551.0, 366.0)
(951.0, 250.0, 1151.0, 250.0, 951.0, 450.0)
During the declarations of my polygons, I seem to be able to print their coordinates with medium_triangle.coords but after, when I click on my canvas, it directly displays the coordinates of the last one declared. And when I move another piece on my canvas it just adds to the same Polygon.
I'm not quite comfortable with classes and methods etc but I thought I understood that each of my polygons was a different instance of my class but despite this it looks like I can only access one instance of the Polygon.
I hope my problem is clear, have I really created different polygons and if yes why can't I modify them separately?
canvas.bind('<Button-1>', self.start_movement)
canvas.bind('<Motion>', self.movement)
canvas.bind('<ButtonRelease-1>', self.stopMovement)
canvas.bind('<Button-3>', self.rotate)
Your canvas (of type Canvas, or so I assume) can only be bound to one action per key. Try this code:
canvas.bind('<Button-1>', self.start_movement)
canvas.bind('<Button-1>', lambda e: print("ok"))
...and you'll see that it won't call start_movement() anymore, because the lambda will be called instead.
Here, the only functions bound to the canvas are the ones you last called: so the ones in the initialization of the last Polygon you created. By binding new methods, you erased the previous bindings to the same keys.
You should not use canvas.bind(...) inside Polygon class. Use canvas.tag_bind(...) instead:
class Polygon:
def __init__(self, coords, color, canvas):
self.coords = coords
self.color = color
self.move = False
self.canvas = canvas
self.id = canvas.create_polygon(self.coords, fill=self.color)
canvas.tag_bind(self.id, '<Button-1>', self.start_movement)
canvas.tag_bind(self.id, '<Motion>', self.movement)
canvas.tag_bind(self.id, '<ButtonRelease-1>', self.stopMovement)
canvas.tag_bind(self.id, '<Button-3>', self.rotate)
Note that I have saved the passed canvas to an instance variable self.canvas. You should replace all drawing_place by self.canvas inside other class methods.
Also you don't need to call the following line inside start_movement():
self.movingimage = drawing_place.find_closest(self.initi_x, self.initi_y,
halo=1) # get canvas object
# ID of where mouse
# pointer is.
As self.id can be used instead of the result of find_closest() inside movement():
def movement(self, event):
if self.move:
end_x = self.canvas.canvasx(event.x) # Translate mouse x screen
# coordinate to canvas coordinate.
end_y = self.canvas.canvasy(event.y) # Translate mouse y screen
# coordinate to canvas coordinate.
deltax = end_x - self.initi_x # Find the difference
deltay = end_y - self.initi_y # Find the difference
self.newPosition(deltax, deltay)
self.initi_x = end_x # Update previous current with new location
self.initi_y = end_y
self.canvas.move(self.id, deltax, deltay) # Move object

How to make an image move through a pyglet window?

I am trying to make an animation using pyglet. So first I tried a simple animation, moving the image in a strait line. Idealy I would like it to bounce around from left to right.
Here is my code:
import pyglet
def center_image(image):
"""Sets an image's anchor point to its center"""
image.anchor_x = image.width // 2
image.anchor_y = image.height // 2
# Make window.
window = pyglet.window.Window(width=640, height=480)
# Load image.
pyglet.resource.path = ['images']
pyglet.resource.reindex()
heart_img = pyglet.resource.image('red-heart.png')
center_image(heart_img)
# Make animation sprite.
heart_grid = pyglet.image.ImageGrid(heart_img, rows=1, columns=5)
heart_ani = pyglet.image.Animation.from_image_sequence(heart_grid, duration=0.1)
heart_sprite = pyglet.sprite.Sprite(heart_ani, x=100, y=300)
heart_sprite.update(scale=0.05)
#window.event
def on_draw():
window.clear()
heart_sprite.draw()
if __name__ == '__main__':
pyglet.app.run()
This code produces this:
How can I make the whole heart move through the window?
The desired trajectory of the heart would be something like this:
Where the box is the frame, the arches are the trajectory and O is a sprite. So the heart would bounce of the first letter of each word and then bounce of the sprite.
So the main issue is that Animation assumes a series of images within a large image. It's called sprite animations and it's essentially just a series strip (usually in a row or a grid pattern) of the movements you want. It's useful for animating walking, attacking and other similar game mechanics.
But to move an object around the canvas, you would need to manipulate the vertices or the image location manually in some way. Your own solution works on the principle of checking if X is greater or less than min and max restrictions. And I would just like to add ontop of that to show some techniques to make it easier and faster to work with the movements and directions. Below I've worked with bitwise operations to determain the direction of movement and this makes the heart bounce around the parent (window) constraints of width and height.
I've also taken the liberty to make the whole project more object oriented by inheriting the pyglet Window class into one object/class as well as make heart it's own class to easier separate what is called when and on what object.
from pyglet import *
from pyglet.gl import *
key = pyglet.window.key
# Indented oddly on purpose to show the pattern:
UP = 0b0001
DOWN = 0b0010
LEFT = 0b0100
RIGHT = 0b1000
class heart(pyglet.sprite.Sprite):
def __init__(self, parent, image='heart.png', x=0, y=0):
self.texture = pyglet.image.load(image)
pyglet.sprite.Sprite.__init__(self, self.texture, x=x, y=y)
self.parent = parent
self.direction = UP | RIGHT # Starting direction
def update(self):
# We can use the pattern above with bitwise operations.
# That way, one direction can be merged with another without collision.
if self.direction & UP:
self.y += 1
if self.direction & DOWN:
self.y -= 1
if self.direction & LEFT:
self.x -= 1
if self.direction & RIGHT:
self.x += 1
if self.x+self.width > self.parent.width:
self.direction = self.direction ^ RIGHT # Remove the RIGHT indicator
self.direction = self.direction ^ LEFT # Start moving to the LEFT
if self.y+self.height > self.parent.height:
self.direction = self.direction ^ UP # Remove the UP indicator
self.direction = self.direction ^ DOWN # Start moving DOWN
if self.y < 0:
self.direction = self.direction ^ DOWN
self.direction = self.direction ^ UP
if self.x < 0:
self.direction = self.direction ^ LEFT
self.direction = self.direction ^ RIGHT
def render(self):
self.draw()
# This class just sets up the window,
# self.heart <-- The important bit
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.x, self.y = 0, 0
self.heart = heart(self, x=100, y=100)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
def render(self):
self.clear()
self.heart.update()
self.heart.render()
## Add stuff you want to render here.
## Preferably in the form of a batch.
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
if __name__ == '__main__':
x = main()
x.run()
The basic principle is the same, manipulating sprite.x to move it laterally, and sprite.y vertically. There's more optimizations to be done, for instance, updates should be scaled according to last render. This is done to avoid glitches if your graphics card can't keep up. It can get quite complicate quite fast, so I'll leave you with an example of how to calculate those movements.
Further more, you probably want to render a batch, not the sprite directly. Which would speed up rendering processes quite a lot for larger projects.
If you're unfamiliar with bitwise operations, a short description would be that it operates on a bit/binary level (4 == 0100 as an example), and doing XOR operations on the values of UP, DOWN, LEFT and RIGHT. We can add/remove directions by merging 0100 and 0001 resulting in 0101 as an example. We can then do binary AND (not like the traditional and operator) to determinate if a value contains a 1 on the third position (0100) by doing self.direction & 0100 which will result in 1 if it's True. It's a handy quick way of checking "states" if you will.
My solution uses the midpoint between two fixed Sprites to determine whether the moving Sprite should go up or down. For this I made all letters individual Sprites, one png for each letter.
Hopefully this image will explain the code below a bit better.
#!/usr/bin/env python
import pyglet
CURR_BOUNCE = 0
MIDPOINTS = []
ENDPOINTS = []
def calculate_midpoint(s1, s2):
""" Calculate the midpoint between two sprites on the x axis. """
return (s1.x + s2.x) // 2
def should_move_down():
""" Decides if the Sprite is going up or down. """
global CURR_BOUNCE
# If the Sprite completed all bounces the app closes (not the best solution).
if max(len(MIDPOINTS), len(ENDPOINTS), CURR_BOUNCE) == CURR_BOUNCE:
raise SystemExit
# Move down if the Sprite is between the midpoint and the end (landing) point.
if MIDPOINTS[CURR_BOUNCE] <= heart_sprite.x <= ENDPOINTS[CURR_BOUNCE]:
return True
# If the Sprite has passed both the mid and end point then it can move on to the next bounce.
if max(MIDPOINTS[CURR_BOUNCE], heart_sprite.x, ENDPOINTS[CURR_BOUNCE]) == heart_sprite.x:
CURR_BOUNCE += 1
# Default behaviour is to keep going up.
return False
def update(dt):
"""
Move Sprite by number of pixels in each tick.
The Sprite always moves to the right on the x-axis.
The default movement on the y-axis is up.
"""
heart_sprite.x += dt * heart_sprite.dx
if should_move_down():
# To go down simply make the movement on the y-axis negative.
heart_sprite.y -= dt * heart_sprite.dy
else:
heart_sprite.y += dt * heart_sprite.dy
def center_image(image):
""" Sets an image's anchor point to its centre """
image.anchor_x = image.width // 2
image.anchor_y = image.height // 2
# Make window.
window = pyglet.window.Window(width=640, height=480)
# Set image path.
pyglet.resource.path = ['images']
pyglet.resource.reindex()
# Load images.
heart_img = pyglet.resource.image('red-heart.png')
cupcake_img = pyglet.resource.image('cupcake.png')
s_img = pyglet.resource.image('S.png')
# Add all letters here ...
t_img = pyglet.resource.image('t.png')
# Center images.
center_image(heart_img)
center_image(cupcake_img)
center_image(s_img)
# Centre all letters here ...
center_image(t_img)
# Make sprites.
half_window_height = window.height // 2
heart_sprite = pyglet.sprite.Sprite(img=heart_img, x=100, y=300)
# Set Sprite's speed.
heart_sprite.dx = 200
heart_sprite.dy = 90
cupcake_sprite = pyglet.sprite.Sprite(img=cupcake_img, x=550, y=half_window_height)
s_sprite = pyglet.sprite.Sprite(img=s_img, x=100, y=half_window_height)
# Make all letters into Sprites and adjust the x-axis coordinates...
t_sprite = pyglet.sprite.Sprite(img=t_img, x=310, y=half_window_height)
# Calculate midpoints.
# Here the midpoint between the 'bouncing point' and the 'landing point' is calculated.
# This is done for all bounces the Sprite makes.
MIDPOINTS.append(calculate_midpoint(s_sprite, t_sprite))
MIDPOINTS.append(calculate_midpoint(t_sprite, cupcake_sprite))
# The 'landing point' must be saved to be able to determine when one bounce has finished
# and move on to the next.
ENDPOINTS.append(t_sprite.x)
ENDPOINTS.append(cupcake_sprite.x)
# Rescale sprites.
heart_sprite.update(scale=0.05)
cupcake_sprite.update(scale=0.1)
s_sprite.update(scale=0.3)
# Resize all letter Sprites here ...
t_sprite.update(scale=0.3)
#window.event
def on_draw():
window.clear()
cupcake_sprite.draw()
heart_sprite.draw()
s_sprite.draw()
# Draw all letter Sprites here ...
t_sprite.draw()
#window.event
def on_mouse_press(x, y, button, modifiers):
"""
I only put the schedule_interval inside a mouse_press event so that I can control
when the animation begins by clicking on it. Otherwise the last line in this method
can be placed directly above the 'pyglet.app.run()' line. This would run the
animation automatically when the app starts.
"""
# Call update 60 times a second
pyglet.clock.schedule_interval(update, 1/60.)
if __name__ == '__main__':
pyglet.app.run()

get real size of QPixmap in Qlabel

Is there some simple way in PyQt5 to get real dimensions of the pixmap displayed in QLabel? I am trying to select part of the image with rubber band. But I can't find a way to limit the rubberband only to pixmap. The QLabel.pixmap().rect() returns dimensions of the whole QLabel not only the pixmap. The problem arises when the pixmap is scaled and there are stripes on the sides of the picture.
The Example image
Example image 2
I posted are quite self explanatory. I don't want the rubberband to be able to move out of the picture to the white stripes.
class ResizableRubberBand(QWidget):
def __init__(self, parent=None):
super(ResizableRubberBand, self).__init__(parent)
self.aspect_ratio = None
self.setWindowFlags(Qt.SubWindow)
self.layout = QHBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.grip1 = QSizeGrip(self)
self.grip2 = QSizeGrip(self)
self.layout.addWidget(self.grip1, 0, Qt.AlignLeft | Qt.AlignTop)
self.layout.addWidget(self.grip2, 0, Qt.AlignRight | Qt.AlignBottom)
self.rubberband = QRubberBand(QRubberBand.Rectangle, self)
self.rubberband.setStyle(QStyleFactory.create("Fusion"))
self.rubberband.move(0, 0)
self.rubberband.show()
self.show()
class ResizablePixmap(QLabel):
def __init__(self, bytes_image):
QLabel.__init__(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
self.setStyleSheet("background-color:#ffffff;")
self.update_pixmap(bytes_image)
def resizeEvent(self, event):
if event:
x = event.size().width()
y = event.size().height()
else:
x = self.width()
y = self.height()
self.current_pixmap = self._bytes2pixmap(self.bytes_image_edit)
self.setPixmap(self.current_pixmap.scaled(x, y, Qt.KeepAspectRatio))
self.resize(x, y)
def update_pixmap(self, bytes_image):
self.bytes_image_edit = bytes_image
self.current_pixmap = self._bytes2pixmap(bytes_image)
self.setPixmap(self.current_pixmap)
self.resizeEvent(None)
#staticmethod
def _bytes2pixmap(raw_image):
image = QImage()
image.loadFromData(raw_image)
return QPixmap(image)
#staticmethod
def _pixmap2bytes(pixmap):
byte_array = QByteArray()
buffer = QBuffer(byte_array)
buffer.open(QIODevice.WriteOnly)
pixmap.save(buffer, 'PNG')
return byte_array.data()
#property
def image_dims(self):
return self.width(), self.height()
def force_resize(self, qsize):
self.resizeEvent(QResizeEvent(qsize, qsize))
class SelectablePixmap(ResizablePixmap):
def __init__(self, bytes_image):
super().__init__(bytes_image)
self.currentQRubberBand = None
self.move_rubber_band = False
self.rubber_band_offset = None
def cancel_selection(self):
self.currentQRubberBand.hide()
self.currentQRubberBand.deleteLater()
self.currentQRubberBand = None
self.selectionActive.emit(False)
def mousePressEvent(self, eventQMouseEvent):
if not self.currentQRubberBand:
self.currentQRubberBand = ResizableRubberBand(self)
self.selectionActive.emit(True)
if self.currentQRubberBand.geometry().contains(eventQMouseEvent.pos()):
self.move_rubber_band = True
self.rubber_band_offset = (eventQMouseEvent.pos() -
self.currentQRubberBand.pos())
else:
self.originQPoint = eventQMouseEvent.pos()
if self.pixmap().rect().contains(self.originQPoint):
self.currentQRubberBand.setGeometry(QRect(self.originQPoint,
QSize()))
self.currentQRubberBand.show()
def mouseMoveEvent(self, eventQMouseEvent):
if self.move_rubber_band:
pos = eventQMouseEvent.pos() - self.rubber_band_offset
if self.pixmap().rect().contains(pos):
self.currentQRubberBand.move(pos)
else:
rect = QRect(self.originQPoint, eventQMouseEvent.pos())
self.currentQRubberBand.setGeometry(rect.normalized())
def mouseReleaseEvent(self, eventQMouseEvent):
if self.move_rubber_band:
self.move_rubber_band = False
The "easy" answer to your question is that you can get the actual geometry of the QPixmap by moving its QRect. Since you're using center alignment, that's very simple:
pixmap_rect = self.pixmap.rect()
pixmap_rect.moveCenter(self.rect().center())
Unfortunately you can't just use that rectangle with your implementation, mostly because you are not really using a QRubberBand.
The concept of a child rubberband, using size grips for resizing, is clever, but has a lot of limitations.
While QSizeGrips make resizing easier, their behavior can't be easily "restricted": you'll probably end up trying to reimplement resize and resizeEvent (risking recursions), maybe with some tricky and convoluted mouse event checking. Also, you'll never be able to resize that "virtual" rubberband to a size smaller to the sum of the QSizeGrips' sizes, nor to a "negative" selection.
Also, in your code you never resize the actual QRubberBand geometry (but that can be done within the ResizableRubberBand.resizeEvent()).
Finally, even if you haven't implemented the selection resizing after an image resizing, you would have a lot of issues if you did (mostly because of the aforementioned minimum size restrainings).
I think that a better solution is to use a simple QRubberBand and implement its interaction directly from the widget that uses it. This lets you have finer control over it, also allowing complete resize features (not only top left and bottom right corners).
I slightly modified your base class code, as you should avoid any resizing within a resizeEvent() (even if it didn't do anything in your case, since the size argument of resize() was the same) and did unnecessary calls to _bytes2pixmap.
class ResizablePixmap(QLabel):
def __init__(self, bytes_image):
QLabel.__init__(self)
self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.setAlignment(Qt.AlignCenter)
self.setStyleSheet("background-color: #ffffff;")
self.update_pixmap(bytes_image)
def update_pixmap(self, bytes_image):
self.bytes_image_edit = bytes_image
self.current_pixmap = self._bytes2pixmap(bytes_image)
def scale(self, fromResize=False):
# use a single central method for scaling; there's no need to call it upon
# creation and also resize() won't work anyway in a layout
self.setPixmap(self.current_pixmap.scaled(self.width(), self.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation))
def resizeEvent(self, event):
super(ResizablePixmap, self).resizeEvent(event)
self.scale(True)
#staticmethod
def _bytes2pixmap(raw_image):
image = QImage()
image.loadFromData(raw_image)
return QPixmap(image)
class SelectablePixmap(ResizablePixmap):
selectionActive = pyqtSignal(bool)
def __init__(self, bytes_image):
super().__init__(bytes_image)
# activate mouse tracking to change cursor on rubberband hover
self.setMouseTracking(True)
self.currentQRubberBand = None
self.rubber_band_offset = None
self.moveDirection = 0
def create_selection(self, pos):
if self.currentQRubberBand:
self.cancel_selection()
self.currentQRubberBand = QRubberBand(QRubberBand.Rectangle, self)
self.currentQRubberBand.setStyle(QStyleFactory.create("Fusion"))
self.currentQRubberBand.setGeometry(pos.x(), pos.y(), 1, 1)
self.currentQRubberBand.show()
self.originQPoint = pos
self.currentQRubberBand.installEventFilter(self)
def cancel_selection(self):
self.currentQRubberBand.hide()
self.currentQRubberBand.deleteLater()
self.currentQRubberBand = None
self.originQPoint = None
self.selectionActive.emit(False)
def scale(self, fromResize=False):
if fromResize and self.currentQRubberBand:
# keep data for rubber resizing, before scaling
oldPixmapRect = self.pixmap().rect()
oldOrigin = self.currentQRubberBand.pos() - self.pixmapRect.topLeft()
super(SelectablePixmap, self).scale()
# assuming that you always align the image in the center, get the current
# pixmap rect and move the rectangle center to the current geometry
self.pixmapRect = self.pixmap().rect()
self.pixmapRect.moveCenter(self.rect().center())
if fromResize and self.currentQRubberBand:
# find the new size ratio based on the previous
xRatio = self.pixmapRect.width() / oldPixmapRect.width()
yRatio = self.pixmapRect.height() / oldPixmapRect.height()
# create a new geometry using 0-rounding for improved accuracy
self.currentQRubberBand.setGeometry(
round(oldOrigin.x() * xRatio, 0) + self.pixmapRect.x(),
round(oldOrigin.y() * yRatio + self.pixmapRect.y(), 0),
round(self.currentQRubberBand.width() * xRatio, 0),
round(self.currentQRubberBand.height() * yRatio, 0))
def updateMargins(self):
# whenever the rubber rectangle geometry changes, create virtual
# rectangles for corners and sides to ease up mouse event checking
rect = self.currentQRubberBand.geometry()
self.rubberTopLeft = QRect(rect.topLeft(), QSize(8, 8))
self.rubberTopRight = QRect(rect.topRight(), QSize(-8, 8)).normalized()
self.rubberBottomRight = QRect(rect.bottomRight(), QSize(-8, -8)).normalized()
self.rubberBottomLeft = QRect(rect.bottomLeft(), QSize(8, -8)).normalized()
self.rubberLeft = QRect(self.rubberTopLeft.bottomLeft(), self.rubberBottomLeft.topRight())
self.rubberTop = QRect(self.rubberTopLeft.topRight(), self.rubberTopRight.bottomLeft())
self.rubberRight = QRect(self.rubberTopRight.bottomLeft(), self.rubberBottomRight.topRight())
self.rubberBottom = QRect(self.rubberBottomLeft.topRight(), self.rubberBottomRight.bottomLeft())
self.rubberInnerRect = QRect(self.rubberTop.bottomLeft(), self.rubberBottom.topRight())
def eventFilter(self, source, event):
if event.type() in (QEvent.Resize, QEvent.Move):
self.updateMargins()
return super(SelectablePixmap, self).eventFilter(source, event)
def mousePressEvent(self, event):
pos = event.pos()
if not self.currentQRubberBand or not pos in self.currentQRubberBand.geometry():
if pos not in self.pixmapRect:
self.originQPoint = None
return
self.create_selection(pos)
elif pos in self.rubberTopLeft:
self.originQPoint = self.currentQRubberBand.geometry().bottomRight()
elif pos in self.rubberTopRight:
self.originQPoint = self.currentQRubberBand.geometry().bottomLeft()
elif pos in self.rubberBottomRight:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
elif pos in self.rubberBottomLeft:
self.originQPoint = self.currentQRubberBand.geometry().topRight()
elif pos in self.rubberTop:
self.originQPoint = self.currentQRubberBand.geometry().bottomLeft()
self.moveDirection = Qt.Vertical
elif pos in self.rubberBottom:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
self.moveDirection = Qt.Vertical
elif pos in self.rubberLeft:
self.originQPoint = self.currentQRubberBand.geometry().topRight()
self.moveDirection = Qt.Horizontal
elif pos in self.rubberRight:
self.originQPoint = self.currentQRubberBand.geometry().topLeft()
self.moveDirection = Qt.Horizontal
else:
self.rubber_band_offset = pos - self.currentQRubberBand.pos()
def mouseMoveEvent(self, event):
pos = event.pos()
if event.buttons() == Qt.NoButton and self.currentQRubberBand:
if pos in self.rubberTopLeft or pos in self.rubberBottomRight:
self.setCursor(Qt.SizeFDiagCursor)
elif pos in self.rubberTopRight or pos in self.rubberBottomLeft:
self.setCursor(Qt.SizeBDiagCursor)
elif pos in self.rubberLeft or pos in self.rubberRight:
self.setCursor(Qt.SizeHorCursor)
elif pos in self.rubberTop or pos in self.rubberBottom:
self.setCursor(Qt.SizeVerCursor)
elif pos in self.rubberInnerRect:
self.setCursor(Qt.SizeAllCursor)
else:
self.unsetCursor()
elif event.buttons():
if self.rubber_band_offset:
target = pos - self.rubber_band_offset
rect = QRect(target, self.currentQRubberBand.size())
# limit positioning of the selection to the image rectangle
if rect.x() < self.pixmapRect.x():
rect.moveLeft(self.pixmapRect.x())
elif rect.right() > self.pixmapRect.right():
rect.moveRight(self.pixmapRect.right())
if rect.y() < self.pixmapRect.y():
rect.moveTop(self.pixmapRect.y())
elif rect.bottom() > self.pixmapRect.bottom():
rect.moveBottom(self.pixmapRect.bottom())
self.currentQRubberBand.setGeometry(rect)
elif self.originQPoint:
if self.moveDirection == Qt.Vertical:
# keep the X fixed to the current right, so that only the
# vertical position is changed
pos.setX(self.currentQRubberBand.geometry().right())
else:
# limit the X to the pixmapRect extent
if pos.x() < self.pixmapRect.x():
pos.setX(self.pixmapRect.x())
elif pos.x() > self.pixmapRect.right():
pos.setX(self.pixmapRect.right())
if self.moveDirection == Qt.Horizontal:
# same as before, but for the Y position
pos.setY(self.currentQRubberBand.geometry().bottom())
else:
# limit the Y to the pixmapRect extent
if pos.y() < self.pixmapRect.y():
pos.setY(self.pixmapRect.y())
elif pos.y() > self.pixmapRect.bottom():
pos.setY(self.pixmapRect.bottom())
rect = QRect(self.originQPoint, pos)
self.currentQRubberBand.setGeometry(rect.normalized())
def mouseReleaseEvent(self, event):
self.rubber_band_offset = None
self.originQPoint = None
self.moveDirection = 0
You could store width and height of the image (before you create the pixmap from bytes) into global variable and then use getter to access it from outside of class.

Dragging and Copying Images in wxPython

I am attempting to modify the DragImage demo from the wxPython examples so that instead of just moving the image around, dragging an image produces a new copy which is draggable, while the original "source" image remains. A decent analogy is that the original images are like a set of widgets to choose from; clicking and dragging on any one of those produces a widget for you to place wherever (and this can be done multiple times) while the source widget remains.
import os
import glob
import wx
import wx.lib.scrolledpanel as scrolled
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent)
frm_pnl = MainPanel(self)
self.Show()
class DragShape:
def __init__(self, bmp):
self.bmp = bmp
self.pos = (0,0)
self.shown = True
self.text = None
self.fullscreen = False
def HitTest(self, pt):
rect = self.GetRect()
return rect.InsideXY(pt.x, pt.y)
def GetRect(self):
#return wx.Rect(self.pos[0], self.pos[1], self.bmp.GetWidth(), self.bmp.GetHeight())
return wx.Rect(self.pos[0], self.pos[1], self.bmp.GetWidth()/2, self.bmp.GetHeight()/2)
def Draw(self, dc, op = wx.COPY):
if self.bmp.Ok():
memDC = wx.MemoryDC()
memDC.SelectObject(self.bmp)
#dc.Blit(self.pos[0], self.pos[1],
# self.bmp.GetWidth(), self.bmp.GetHeight(),
# memDC, 0, 0, op, True)
dc.Blit(self.pos[0], self.pos[1],
self.bmp.GetWidth()/2, self.bmp.GetHeight()/2,
memDC, 0, 0, op, True)
return True
else:
return False
class MainPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1, size = (900, 700))
self.shapes = []
#panel for mechanisms
mechPnl = MechanismPanel(self)
mechSzr = wx.BoxSizer(wx.HORIZONTAL)
mechSzr.Add(mechPnl, 1)
selfSizer = wx.BoxSizer(wx.VERTICAL)
selfSizer.Add(mechSzr, 0, wx.EXPAND)
selfSizer.Layout()
self.SetSizer(selfSizer)
self.dragImage = None
self.dragShape = None
self.hiliteShape = None
self.SetCursor(wx.StockCursor(wx.CURSOR_ARROW))
#self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_PAINT, self.OnPaint)
mechPnl.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
mechPnl.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
mechPnl.Bind(wx.EVT_MOTION, self.OnMotion)
mechPnl.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
# The mouse is moving
def OnMotion(self, evt):
print "On motion!"
# Ignore mouse movement if we're not dragging.
if not self.dragShape or not evt.Dragging() or not evt.LeftIsDown():
return
# if we have a shape, but haven't started dragging yet
if self.dragShape and not self.dragImage:
# only start the drag after having moved a couple pixels
tolerance = 2
pt = evt.GetPosition()
dx = abs(pt.x - self.dragStartPos.x)
dy = abs(pt.y - self.dragStartPos.y)
if dx <= tolerance and dy <= tolerance:
return
# refresh the area of the window where the shape was so it
# will get erased.
self.dragShape.shown = False
self.RefreshRect(self.dragShape.GetRect(), True)
self.Update()
if self.dragShape.text:
self.dragImage = wx.DragString(self.dragShape.text,
wx.StockCursor(wx.CURSOR_HAND))
else:
self.dragImage = wx.DragImage(self.dragShape.bmp,
wx.StockCursor(wx.CURSOR_HAND))
hotspot = self.dragStartPos - self.dragShape.pos
self.dragImage.BeginDrag(hotspot, self, self.dragShape.fullscreen)
self.dragImage.Move(pt)
self.dragImage.Show()
# if we have shape and image then move it, posibly highlighting another shape.
elif self.dragShape and self.dragImage:
onShape = self.FindShape(evt.GetPosition())
unhiliteOld = False
hiliteNew = False
# figure out what to hilite and what to unhilite
if self.hiliteShape:
if onShape is None or self.hiliteShape is not onShape:
unhiliteOld = True
if onShape and onShape is not self.hiliteShape and onShape.shown:
hiliteNew = True
# if needed, hide the drag image so we can update the window
if unhiliteOld or hiliteNew:
self.dragImage.Hide()
if unhiliteOld:
dc = wx.ClientDC(self)
self.hiliteShape.Draw(dc)
self.hiliteShape = None
if hiliteNew:
dc = wx.ClientDC(self)
self.hiliteShape = onShape
self.hiliteShape.Draw(dc, wx.INVERT)
# now move it and show it again if needed
self.dragImage.Move(evt.GetPosition())
if unhiliteOld or hiliteNew:
self.dragImage.Show()
# Left mouse button up.
def OnLeftUp(self, evt):
print "On left up!"
if not self.dragImage or not self.dragShape:
self.dragImage = None
self.dragShape = None
return
# Hide the image, end dragging, and nuke out the drag image.
self.dragImage.Hide()
self.dragImage.EndDrag()
self.dragImage = None
if self.hiliteShape:
self.RefreshRect(self.hiliteShape.GetRect())
self.hiliteShape = None
# reposition and draw the shape
# Note by jmg 11/28/03
# Here's the original:
#
# self.dragShape.pos = self.dragShape.pos + evt.GetPosition() - self.dragStartPos
#
# So if there are any problems associated with this, use that as
# a starting place in your investigation. I've tried to simulate the
# wx.Point __add__ method here -- it won't work for tuples as we
# have now from the various methods
#
# There must be a better way to do this :-)
#
self.dragShape.pos = (
self.dragShape.pos[0] + evt.GetPosition()[0] - self.dragStartPos[0],
self.dragShape.pos[1] + evt.GetPosition()[1] - self.dragStartPos[1]
)
self.dragShape.shown = True
self.RefreshRect(self.dragShape.GetRect())
self.dragShape = None
# Fired whenever a paint event occurs
def OnPaint(self, evt):
print "On paint!"
dc = wx.PaintDC(self)
self.PrepareDC(dc)
self.DrawShapes(dc)
# Left mouse button is down.
def OnLeftDown(self, evt):
print "On left down!"
# Did the mouse go down on one of our shapes?
shape = self.FindShape(evt.GetPosition())
# If a shape was 'hit', then set that as the shape we're going to
# drag around. Get our start position. Dragging has not yet started.
# That will happen once the mouse moves, OR the mouse is released.
if shape:
self.dragShape = shape
self.dragStartPos = evt.GetPosition()
# Go through our list of shapes and draw them in whatever place they are.
def DrawShapes(self, dc):
for shape in self.shapes:
if shape.shown:
shape.Draw(dc)
# This is actually a sophisticated 'hit test', but in this
# case we're also determining which shape, if any, was 'hit'.
def FindShape(self, pt):
for shape in self.shapes:
if shape.HitTest(pt):
return shape
return None
# Clears the background, then redraws it. If the DC is passed, then
# we only do so in the area so designated. Otherwise, it's the whole thing.
def OnEraseBackground(self, evt):
dc = evt.GetDC()
if not dc:
dc = wx.ClientDC(self)
rect = self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
self.TileBackground(dc)
# tile the background bitmap
def TileBackground(self, dc):
sz = self.GetClientSize()
w = self.bg_bmp.GetWidth()
h = self.bg_bmp.GetHeight()
x = 0
while x < sz.width:
y = 0
while y < sz.height:
dc.DrawBitmap(self.bg_bmp, x, y)
y = y + h
x = x + w
# We're not doing anything here, but you might have reason to.
# for example, if you were dragging something, you might elect to
# 'drop it' when the cursor left the window.
def OnLeaveWindow(self, evt):
pass
class MechanismPanel(scrolled.ScrolledPanel):
def __init__(self, parent):
scrolled.ScrolledPanel.__init__(self, parent, -1, size = (400, 140))
self.SetBackgroundColour((211, 211, 211))
mechPnlSzr = wx.BoxSizer(wx.HORIZONTAL)
os.chdir("./figures")
position = 50
for file in glob.glob("icon*.png"):
print file
imgIcon = wx.Image(file, wx.BITMAP_TYPE_PNG).ConvertToBitmap()
staticBitmap = wx.StaticBitmap(self, -1, imgIcon, (position, 50), (50, 50))
shape = DragShape(staticBitmap.GetBitmap())
shape.pos = (position, 50)
position = position + 100
shape.fullscreen = True
parent.shapes.append(shape)
mechPnlSzr.Add(staticBitmap, 0, wx.FIXED, border = 20)
self.SetSizer(mechPnlSzr)
self.SetAutoLayout(1)
self.SetupScrolling()#scroll_y = False)
app = wx.App(False)
frame = MainWindow(None, "Trading Client")
app.MainLoop()

Categories