Zoom on mouse position QGraphicsView - python

I am looking to find how to zoom in a QGraphicsView but on the cursor position. Currently I am able to zoom but the position it is zooming onto is not consistent.
def wheelEvent(self, event):
'''Wheel event to zoom
'''
# Run default event
QtWidgets.QGraphicsView.wheelEvent(self, event)
# Define zoom factor
factor = 1.1
if event.delta() < 0:
factor = 0.9
self.scale(factor, factor)
I have seen the use of self.mapToScene() but have been unsuccessful

A possible solution is to focus on the point where the mouse is, scale and recalculate the point where the new center should be:
def wheelEvent(self, event):
factor = 1.1
if event.delta() < 0:
factor = 0.9
view_pos = event.pos()
scene_pos = self.mapToScene(view_pos)
self.centerOn(scene_pos)
self.scale(factor, factor)
delta = self.mapToScene(view_pos) - self.mapToScene(self.viewport().rect().center())
self.centerOn(scene_pos - delta)

Related

Collision detection between circle and rectangle

I wrote some code to show a circle and a rectangle randomly on the screen with PyQt6. and I want to detect if these two objects have a collision then I make them red otherwise I make them green.
But how should I detect whether there is a collision or not?
here is my code
from random import randint
from sys import argv
from PyQt6.QtCore import QRect, QTimer, Qt, QMimeData
from PyQt6.QtGui import QColor, QKeyEvent, QMouseEvent, QPainter, QPen, QPaintEvent, QBrush, QDrag
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QMainWindow, QPushButton
class Window(QMainWindow):
def __init__(self) -> None:
super().__init__()
screenWidth = 1920
screenHeight = 1080
self.isRunning = True
self.windowWidth = 1200
self.windowHeight = 800
self.clockCounterVariable = 0
self.milSec = 0
self.seconds = 0
self.minutes = 0
self.hours = 0
self.setWindowTitle("Smart rockets")
self.setGeometry((screenWidth - self.windowWidth) // 2, (screenHeight - self.windowHeight) // 2, self.windowWidth, self.windowHeight)
self.setLayout(QVBoxLayout())
self.setStyleSheet("background-color:rgb(20, 20, 20);font-size:20px;")
self.clock = QTimer(self)
self.clock.timeout.connect(self.clockCounter)
self.clock.start(10)
button = QPushButton("Refresh", self)
button.setGeometry(20,self.windowHeight - 60,self.windowWidth - 40,40)
button.setStyleSheet("background-color:rgb(80, 80, 80);font-size:20px;")
button.setCheckable(True)
button.clicked.connect(self.refreshRectAndCircle)
rectangleWidth = randint(50, 500)
rectangleHeight = randint(50, 500)
self.rectangle = QRect(randint(0, self.windowWidth - rectangleWidth), randint(0, self.windowHeight - rectangleHeight - 80), rectangleWidth, rectangleHeight)
circleRadius = randint(50, 200)
self.circle = QRect(randint(0, self.windowWidth - circleRadius), randint(0, self.windowHeight - circleRadius - 80), circleRadius, circleRadius)
self.show()
def dragEnterEvent(self, event) -> super:
event.accept()
def keyPressEvent(self, event: QKeyEvent) -> super:
key = QKeyEvent.key(event)
if key == 112 or key == 80: # P/p
if self.isRunning:
self.clock.stop()
print("pause process")
self.isRunning = False
else:
print("continue process")
self.isRunning = True
self.clock.start(10)
elif (key == 115) or (key == 83): # S/s
self.closeWindow()
return super().keyPressEvent(event)
def mousePressEvent(self, event: QMouseEvent) -> super:
if event.buttons() == Qt.MouseButton.LeftButton:
if self.isRunning:
self.clock.stop()
print("pause process")
self.isRunning = False
else:
print("continue process")
self.isRunning = True
self.clock.start(10)
return super().mousePressEvent(event)
def clockCounter(self) -> None:
self.clockCounterVariable += 1
self.update()
def paintEvent(self, a0: QPaintEvent) -> super:
painter = QPainter()
self.milSec = self.clockCounterVariable
self.seconds, self.milSec = divmod(self.milSec, 100)
self.minutes, self.seconds = divmod(self.seconds, 60)
self.hours, self.minutes = divmod(self.minutes, 60)
painter.begin(self)
painter.setPen(QPen(QColor(255, 128, 20), 1, Qt.PenStyle.SolidLine))
painter.drawText(QRect(35, 30, 400, 30), Qt.AlignmentFlag.AlignLeft, "{:02d} : {:02d} : {:02d} : {:02d}".format(self.hours, self.minutes, self.seconds, self.milSec))
if self.collided():
painter.setPen(QPen(QColor(255, 20, 20), 0, Qt.PenStyle.SolidLine))
painter.setBrush(QBrush(QColor(128, 20, 20), Qt.BrushStyle.SolidPattern))
else:
painter.setPen(QPen(QColor(20, 255, 20), 0, Qt.PenStyle.SolidLine))
painter.setBrush(QBrush(QColor(20, 128, 20), Qt.BrushStyle.SolidPattern))
painter.drawRect(self.rectangle)
painter.drawEllipse(self.circle)
painter.end()
return super().paintEvent(a0)
def refreshRectAndCircle(self) -> None:
rectangleWidth = randint(50, 500)
rectangleHeight = randint(50, 500)
self.rectangle = QRect(randint(0, self.windowWidth - rectangleWidth), randint(0, self.windowHeight - rectangleHeight - 80), rectangleWidth, rectangleHeight)
circleRadius = randint(50, 200)
self.circle = QRect(randint(0, self.windowWidth - circleRadius), randint(0, self.windowHeight - circleRadius - 80), circleRadius, circleRadius)
self.update()
def collided(self) -> bool:
# return True if collided and return False if not collided
circle = self.circle
rect = self.rectangle
if __name__ == "__main__":
App = QApplication(argv)
window = Window()
App.exec()
how should I detect whether there is a collision between the circle and the rectangle or not?
While you can achieve this with math functions, luckily Qt provides some useful functions that can make this much easier.
You can achieve this with three steps - or even just one (see the last section).
Check the center of the circle
If the center of the circle is within the boundaries of the rectangle, you can always assume that they collide. You're using a QRect, which is a rectangle that is always aligned to the axis, making things much easier.
Mathematically speaking you just need to ensure that the X of the center is between the smallest and biggest X of the left and right vertical lines of the rectangle, then the same for the Y.
Qt allows us to check if QRect.contains() the QRect.center() of the circle.
def collided(self) -> bool:
center = self.circle.center()
if self.rectangle.contains(center):
return True
Check the vertexes of the rectangle
If the length between the center of the circle and any of the vertexes of the rectangle is smaller than the radius, you can be sure that they are within the circle area.
Using the basic Pythagorean equation, you can know the hypotenuse created between the center and each of the vertexes of the rectangle, and if the hypotenuse is smaller than the radius, it means that they are within the circle.
With Qt we can use QLineF with the center and the vertexes (topLeft(), topRight(), bottomRight() and bottomLeft()), whenever any of the lengths is smaller than the radius, it means that the vertex is within the circle. Using QPolygonF we can easily iterate through all vertexes in a for loop.
# ...
center = QPointF(center)
radius = self.circle.width() / 2
corners = QPolygonF(QRectF(self.rectangle))[:4]
for corner in corners:
if QLineF(center, corner).length() < radius:
return True
Check the closest side of the rectangle
It is possible that the circle only collides with a side of the rectangle: the center of the circle is outside of the rectangle, and none of the vertexes are within the circle.
Consider this case:
In this situations, the collision always happens whenever the perpendicular line of the closest side of the rectangle is smaller than the radius:
Using math, we'll need to get the line perpendicular to the closest side, going toward the center of the circle, computing the angle between the side and the lines connecting the center with each vertex (shown in orange above), then with the help of some trigonometry, get the cathetus of one of the triangles (shown in red): if the length of that line is smaller than the radius, the shapes collide.
Luckily again, Qt can help us. We can get the two closest points using the lines created in the section "Check the vertexes of the rectangle" above, get the side of those points and compute a perpendicular angle that will be used to create a "diameter": starting from the center, we create two lines with opposite angles and the radius with the fromPolar(), then create the actual diameter with the external points of those lines. Finally, we check if that diameter intersects() with the side.
And this is the final function:
def collided(self) -> bool:
center = self.circle.center()
if self.rectangle.contains(center):
return True
# use floating point based coordinates
center = QPointF(center)
radius = self.circle.width() / 2
corners = QPolygonF(QRectF(self.rectangle))[:4]
lines = []
for corner in corners:
line = QLineF(center, corner)
if line.length() < radius:
return True
lines.append(line)
# sort lines by their lengths
lines.sort(key=lambda l: l.length())
# create the side of the closest points
segment = QLineF(lines[0].p2(), lines[1].p2())
# the perpendicular angle, intersecting with the center of the circle
perpAngle = (segment.angle() + 90) % 360
# the ends of the "diameter" per pendicular to the side
d1 = QLineF.fromPolar(radius, perpAngle).translated(center)
d2 = QLineF.fromPolar(radius, perpAngle + 180).translated(center)
# the actual diameter line
diameterLine = QLineF(d1.p2(), d2.p2())
# get the intersection type
intersection = diameterLine.intersects(segment, QPointF())
return intersection == QLineF.BoundedIntersection
Further considerations
when dealing with geometric shapes, you should consider using QPainterPath which actually makes the above extremely simpler:
def collided(self) -> bool:
circlePath = QPainterPath()
circlePath.addEllipse(QRectF(self.circle))
return circlePath.intersects(QRectF(self.rectangle))
Qt has a powerful (yet complex) Graphics View Framework that makes graphics and user interaction much more intuitive and effective; while the QPainter API is certainly easier for simpler cases, it may result in cumbersome (and difficult to debug) code as soon as your program requirements grow in complexity;
QMainWindow has its own, private and inaccessible layout manager, you cannot call setLayout() on it; use setCentralWidget() and set a layout to that widget eventually;
never use generic stylesheet properties for parent widgets (as you did for the main window) because it may result in awkward drawing of complex widgets like scroll areas; always use selector types for windows and containers instead;
unless you actually need to paint on the QMainWindow contents (which is a rare occurrence), you should always implement the paintEvent() on its central widget instead; otherwise, if you don't need QMainWindow features (menubar, statusbar, dock widgets and toolbars), just use a QWidget;
QTimer is not reliable for precise time measurement: if any function called while it's running requires more time than the timeout interval, the connected function will always be called afterwards; use QElapsedTimer instead;
in paintEvent() just use painter = QPainter(self), remove painter.begin(self) (it's implicit using the above) and painter.end() (unnecessary, since it's automatically destroyed when the function returns);
don't create unnecessary instance attributes (self.milSec, self.seconds, etc) that will be almost certainly overwritten sooner or later, and that you're not using elsewhere; the paint event must always return as soon as possible and must be always optimized as much as possible;

Zoom in the current mouse position of a QGraphicsView

I know this is possible by using the scale method of the QGraphicsView and setting the anchor using setTransformationAnchor(QGraphicsView.AnchorUnderMouse). However, doing so would result to the image quality being compromised when zooming.
So what I did instead is to scale the pixmap, and set it as the QGraphicsPixmapItem of the scene. To zoom in on the current mouse position, I use the mapToScene method and position of the wheel event, but this is still not as smooth as the "traditional" implementation.
Code:
class Zoom_View(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.pix_map = QPixmap("path/to/file")
self.pix_map_item = self.scene.addPixmap(self.pix_map)
self.global_factor = 1
def scale_image(self, factor):
_pixmap = self.pix_map.scaledToHeight(int(factor*self.viewport().geometry().height()), Qt.SmoothTransformation)
self.pix_map_item.setPixmap(_pixmap)
self.scene.setSceneRect(QRectF(_pixmap.rect()))
def wheelEvent(self, event):
factor = 1.5
if QApplication.keyboardModifiers() == Qt.ControlModifier:
view_pos = event.pos()
scene_pos = self.mapToScene(view_pos)
self.centerOn(scene_pos)
if event.angleDelta().y() > 0 and self.global_factor < 20:
self.global_factor *= factor
self.scaleImage(self.global_factor)
elif event.angleDelta().y() < 0 and self.global_factor > 0.2:
self.global_factor /= factor
self.scaleImage(self.global_factor)
else:
return super().wheelEvent(event)

Python Matplotlib: reduce render time for interactive plot

I've got the following code that produces a plot that can interactively be modified. Clicking / holding the left mouse button sets the marker position, Holding the right button and moving the mouse moves the plotted data in direction x and using the mouse wheel zooms in/out. Additionally, resizing the window calls figure.tight_layout() so that the size of the axes is adapted to the window size.
# coding=utf-8
from __future__ import division
from Tkinter import *
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from numpy import arange, sin, pi
matplotlib.use('TkAgg')
class PlotFrame(Frame):
def __init__(self, master, **ops):
Frame.__init__(self, master, **ops)
self.figure = Figure()
self.axes_main = self.figure.add_subplot(111)
for i in range(10):
t = arange(0, 300, 0.01)
s = sin(0.02 * pi * (t + 10 * i))
self.axes_main.plot(t, s)
self.plot = FigureCanvasTkAgg(self.figure, master=self)
self.plot.show()
self.plot.get_tk_widget().pack(fill=BOTH, expand=1)
self.dragging = False
self.dragging_button = None
self.mouse_pos = [0, 0]
self.marker = self.figure.axes[0].plot((0, 0), (-1, 1), 'black', linewidth=3)[0]
self.plot.mpl_connect('button_press_event', self.on_button_press)
self.plot.mpl_connect('button_release_event', self.on_button_release)
self.plot.mpl_connect('motion_notify_event', self.on_mouse_move)
self.plot.mpl_connect('scroll_event', self.on_mouse_scroll)
self.plot.mpl_connect("resize_event", self.on_resize)
def on_resize(self, _):
self.figure.tight_layout()
def axes_size(self):
pos = self.axes_main.get_position()
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
axis_size = [(pos.x1 - pos.x0) * width, (pos.y1 - pos.y0) * height]
return axis_size
def on_button_press(self, event):
# right mouse button clicked
if not self.dragging and event.button in (1, 3):
self.dragging = True
self.dragging_button = event.button
self.mouse_pos = [event.x, event.y]
# left mouse button clicked
if event.button == 1 and event.xdata is not None:
self.move_marker(event.xdata)
def on_button_release(self, event):
if self.dragging and self.dragging_button == event.button:
self.dragging = False
def on_mouse_move(self, event):
if self.dragging and self.dragging_button == 3:
dx = event.x - self.mouse_pos[0]
self.mouse_pos = [event.x, event.y]
x_min, x_max = self.figure.axes[0].get_xlim()
x_range = x_max - x_min
x_factor = x_range / self.axes_size()[0]
self.figure.axes[0].set_xlim([x_min - dx * x_factor, x_max - dx * x_factor])
self.plot.draw()
elif self.dragging and self.dragging_button == 1:
self.move_marker(event.xdata)
def on_mouse_scroll(self, event):
if event.xdata is None:
return
zoom_direction = -1 if event.button == 'up' else 1
zoom_factor = 1 + .4 * zoom_direction
x_min, x_max = self.figure.axes[0].get_xlim()
min = event.xdata + (x_min - event.xdata) * zoom_factor
max = event.xdata + (x_max - event.xdata) * zoom_factor
self.figure.axes[0].set_xlim([min, max])
self.plot.draw()
def move_marker(self, x_position):
y_min, y_max = self.figure.axes[0].get_ylim()
self.marker.set_data((x_position, x_position), (y_min, y_max))
self.plot.draw()
if __name__ == '__main__':
gui = Tk()
vf = PlotFrame(gui)
vf.pack(fill=BOTH, expand=1)
gui.mainloop()
The implementation works fine, but rendering is really slow when displaying a lot of lines. How can I make rendering faster? As you can see in the implementation above, the whole plot is drawn completely every time anything changes which shouldn't be necessary. My thoughts on this:
Resizing the window: draw everything
Zooming: draw everything
Moving the marker: just redraw the marker (one line) instead of drawing everything
Moving the plot in x direction: move the pixels currently displayed in the plot left/right and only draw pixels that are moved into the visible area
Drawing everything when resizing/zooming is fine for me, but I really need faster drawing of the latter two modifications. I already looked into matplotlib's animations, but as far as I understood, they won't help in my case. Any help is greatly appreciated, thanks!
The solution seems to be to cache elements that get redrawn as you said:
One major thing that gets redrawn is the background:
# cache the background
background = fig.canvas.copy_from_bbox(ax.bbox)
After caching restore it using restore region then just re-draw the points/line at every call you need
# restore background
fig.canvas.restore_region(background)
# redraw just the points
ax.draw_artist(points)
# fill in the axes rectangle
fig.canvas.blit(ax.bbox)
To optimize drawing blitting can be used. With it only given artists (those that were changed) will be rendered instead of the whole figure.
Motplotlib uses that technique internally in the animation module. You can use Animation class in it as a reference to implement the same behaviour in your code. Look at the _blit_draw() and several related functions after it in the sources.

"tkinter.TclError: invalid command name" error after calling root.destroy()

I am in the process of learning tkinter on Python 3.X. I am writing a simple program which will get one or more balls (tkinter ovals) bouncing round a rectangular court (tkinter root window with a canvas and rectangle drawn on it).
I want to be able to terminate the program cleanly by pressing the q key, and have managed to bind the key to the root and fire the callback function when a key is pressed, which then calls root.destroy().
However, I'm still getting errors of the form _tkinter.TclError: invalid command name ".140625086752360" when I do so. This is driving me crazy. What am I doing wrong?
from tkinter import *
import time
import numpy
class Ball:
def bates():
"""
Generator for the sequential index number used in order to
identify the various balls.
"""
k = 0
while True:
yield k
k += 1
index = bates()
def __init__(self, parent, x, y, v=0.0, angle=0.0, accel=0.0, radius=10, border=2):
self.parent = parent # The parent Canvas widget
self.index = next(Ball.index) # Fortunately, I have all my feathers individually numbered, for just such an eventuality
self.x = x # X-coordinate (-1.0 .. 1.0)
self.y = y # Y-coordinate (-1.0 .. 1.0)
self.radius = radius # Radius (0.0 .. 1.0)
self.v = v # Velocity
self.theta = angle # Angle
self.accel = accel # Acceleration per tick
self.border = border # Border thickness (integer)
self.widget = self.parent.canvas.create_oval(
self.px() - self.pr(), self.py() - self.pr(),
self.px() + self.pr(), self.py() + self.pr(),
fill = "red", width=self.border, outline="black")
def __repr__(self):
return "[{}] x={:.4f} y={:.4f} v={:.4f} a={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
self.index, self.x, self.y, self.v, self.theta,
self.radius, self.border, self.px(), self.py(), self.pr())
def pr(self):
"""
Converts a radius from the range 0.0 .. 1.0 to window coordinates
based on the width and height of the window
"""
assert self.radius > 0.0 and self.radius <= 1.0
return int(min(self.parent.height, self.parent.width)*self.radius/2.0)
def px(self):
"""
Converts an X-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its width
"""
assert self.x >= -1.0 and self.x <= 1.0
return int((1.0 + self.x) * self.parent.width / 2.0 + self.parent.border)
def py(self):
"""
Converts a Y-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its height
"""
assert self.y >= -1.0 and self.y <= 1.0
return int((1.0 - self.y) * self.parent.height / 2.0 + self.parent.border)
def Move(self, x, y):
"""
Moves ball to absolute position (x, y) where x and y are both -1.0 .. 1.0
"""
oldx = self.px()
oldy = self.py()
self.x = x
self.y = y
deltax = self.px() - oldx
deltay = self.py() - oldy
if oldx != 0 or oldy != 0:
self.parent.canvas.move(self.widget, deltax, deltay)
def HandleWallCollision(self):
"""
Detects if a ball collides with the wall of the rectangular
Court.
"""
pass
class Court:
"""
A 2D rectangular enclosure containing a centred, rectagular
grid of balls (instances of the Ball class).
"""
def __init__(self,
width=1000, # Width of the canvas in pixels
height=750, # Height of the canvas in pixels
border=5, # Width of the border around the canvas in pixels
rows=1, # Number of rows of balls
cols=1, # Number of columns of balls
radius=0.05, # Ball radius
ballborder=1, # Width of the border around the balls in pixels
cycles=1000, # Number of animation cycles
tick=0.01): # Animation tick length (sec)
self.root = Tk()
self.height = height
self.width = width
self.border = border
self.cycles = cycles
self.tick = tick
self.canvas = Canvas(self.root, width=width+2*border, height=height+2*border)
self.rectangle = self.canvas.create_rectangle(border, border, width+border, height+border, outline="black", fill="white", width=border)
self.root.bind('<Key>', self.key)
self.CreateGrid(rows, cols, radius, ballborder)
self.canvas.pack()
self.afterid = self.root.after(0, self.Animate)
self.root.mainloop()
def __repr__(self):
s = "width={} height={} border={} balls={}\n".format(self.width,
self.height,
self.border,
len(self.balls))
for b in self.balls:
s += "> {}\n".format(b)
return s
def key(self, event):
print("Got key '{}'".format(event.char))
if event.char == 'q':
print("Bye!")
self.root.after_cancel(self.afterid)
self.root.destroy()
def CreateGrid(self, rows, cols, radius, border):
"""
Creates a rectangular rows x cols grid of balls of
the specified radius and border thickness
"""
self.balls = []
for r in range(1, rows+1):
y = 1.0-2.0*r/(rows+1)
for c in range(1, cols+1):
x = 2.0*c/(cols+1) - 1.0
self.balls.append(Ball(self, x, y, 0.001,
numpy.pi/6.0, 0.0, radius, border))
def Animate(self):
"""
Animates the movement of the various balls
"""
for c in range(self.cycles):
for b in self.balls:
b.v += b.accel
b.Move(b.x + b.v * numpy.cos(b.theta),
b.y + b.v * numpy.sin(b.theta))
self.canvas.update()
time.sleep(self.tick)
self.root.destroy()
I've included the full listing for completeness, but I'm fairly sure that the problem lies in the Court class. I presume it's some sort of callback or similar firing but I seem to be beating my head against a wall trying to fix it.
You have effectively got two mainloops. In your Court.__init__ method you use after to start the Animate method and then start the Tk mainloop which will process events until you destroy the main Tk window.
However the Animate method basically replicates this mainloop by calling update to process events then time.sleep to waste some time and repeating this. When you handle the keypress and terminate your window, the Animate method is still running and attempts to update the canvas which no longer exists.
The correct way to handle this is to rewrite the Animate method to perform a single round of moving the balls and then schedule another call of Animate using after and provide the necessary delay as the after parameter. This way the event system will call your animation function at the correct intervals while still processing all other window system events promptly.

Tkinter drag and drop

I'm working on a drag and drop function that will allow me to move items around on a canvas.I have it working (Kind of) but I move only slightly but the line shoots across the screen (And eventually off the visible part of the canvas so I cannot get to it. I'm not sure where to go from here. Below is the drag and drop code I've created so far:
def onPressToMove(self, event): #get initial location of object to be moved
winX = event.x - self.workspace.canvasx(0)
winY = event.y - self.workspace.canvasy(0)
self.dragInfo["Widget"] = self.workspace.find_closest(event.x, event.y, halo = 5)[0]
self.dragInfo["xCoord"] = winX
self.dragInfo["yCoord"] = winY
def onReleaseToMove(self, event): #reset data on release
self.dragInfo["Widget"] = None
self.dragInfo["xCoord"] = 0
self.dragInfo["yCoord"] = 0
def onMovement(self, event):
winX = event.x - self.workspace.canvasx(0)
winY = event.y - self.workspace.canvasy(0)
newX = winX - self.dragInfo["xCoord"]
newY = winY - self.dragInfo["yCoord"]
self.workspace.move(self.dragInfo["Widget"], newX, newY)
dragInfo is a dictionary I'm using to store the data. Originally I thought that translating the canvas coordinates to window coordinates would help, but it acts the same as without that stuff.
This answer to the question "board drawing code to move an oval" shows how to drag an object on a canvas.
In your case, you're not resetting the base of the delta as you move the object. If the mouse moves one pixel to the right, you use move to move the mouse one pixel to the right.
Now, let's say you move it one more pixel to the right. This time, your calculation says the delta is 2 from the starting point even though you only actually moved the mouse one more pixel). Next time you move one pixel, you're calculating a delta of 3, and so on.
The solution is simple: reset dragInfo["xCoord"] and dragInfo["yCoord"] while it is moving, since you only want to compute the delta to its previous position, not the original starting position.
def onPressToMove(self, event): #get initial location of object to be moved
winX = event.x - self.canvas.canvasx(0)
winY = event.y - self.canvas.canvasy(0)
self.dragInfo["Widget"] = self.canvas.find_closest(event.x, event.y, halo = 5)[0]
# reset the starting point for the next move
self.dragInfo["xCoord"] = winX
self.dragInfo["yCoord"] = winY

Categories

Resources