Getting mouse position relative to image limits - python

I display an image within a custom QLabel and get clicks on this label. I'm interested in clicks within the image only, and in a position expressed by a number between 0 and 1, 0 being the leftmost or topmost pixel and 1 the rightmost or bottommost pixel, regardless of the image actual size.
I can't get the image rectangle to compute the position. When I call self.pixmap.rect(), width and height are the original image dimensions, not the dimensions of the image which was scaled to fit into the label.
What am I doing doing wrong?
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QGridLayout
from PyQt5.QtGui import QPixmap
class Window(QWidget):
def __init__(self):
super().__init__()
image_path = '092.jpg'
image_area = Image_Area(QPixmap(image_path))
image_area.left_click.connect(self.image_clicked)
layout = QGridLayout()
layout.addWidget(image_area, 0, 0)
self.setLayout(layout)
# Left click on image
def image_clicked(self, x, y):
print(f'{x:.3f},{y:.3f}')
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self.pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self.pixmap != None:
width, height = int(self.width()), int(self.height())
scaled_pixmap = self.pixmap.scaled(width, height, Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = e.x(), e.y()
if self.pixmap:
# Get pixmap rectangle
r = self.pixmap.rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0+r.width(), y0+r.height()
# Check we clicked on the pixmap
if x >= x0 and x < x1 and y >= y0 and y < y1:
# emit position relative to pixmap bottom-left corner
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)
app = QApplication([])
window = Window()
window.show()
app.exec()
Update: After the answer was provided, I realized the computation of the click position relative to the image origin was wrong, here is an accurate version for the persons interested:
def mousePressEvent(self, e):
# Mouse position is in label coordinates
x, y, = e.x(), e.y()
if self.pixmap():
# pixmap is not a widget, we don't have its location,
# so we assume the pixmap is centered in the label and
# compute the location with respect to label
label_size = self.size()
pixmap_size = self.pixmap().size()
width = pixmap_size.width()
height = pixmap_size.height()
x0 = int((label_size.width() - width) / 2)
y0 = int((label_size.height() - height) / 2)
# Check we clicked on the pixmap
if (x >= x0 and x < (x0 + width) and
y >= y0 and y < (y0 + height)):
# emit position relative to pixmap top-left corner
x_relative = (x - x0) / width
y_relative = (y - y0) / height
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)

You have to use the QPixmap established in the QLabel that can be obtained through the pixmap() method. The problem is that you are obfuscating the access to that method since you have an attribute with a similar name. So the solution is to use pixmap() and rename the attribute pixmap.
class Image_Area(QLabel):
left_click = pyqtSignal(float, float)
def __init__(self, pixmap):
super().__init__()
self._pixmap = pixmap
self.setPixmap(pixmap)
self.setScaledContents(False)
self.setMinimumSize(100, 100)
def resizeEvent(self, e):
if self._pixmap is not None:
scaled_pixmap = self._pixmap.scaled(self.size(), Qt.KeepAspectRatio)
self.setPixmap(scaled_pixmap)
return super().resizeEvent(e)
def mousePressEvent(self, e):
x, y, = (
e.x(),
e.y(),
)
if self.pixmap:
r = self.pixmap().rect()
x0, y0 = r.x(), r.y()
x1, y1 = x0 + r.width(), y0 + r.height()
if x >= x0 and x < x1 and y >= y0 and y < y1:
x_relative = (x - x0) / (x1 - x0)
y_relative = (y - y0) / (y1 - y0)
self.left_click.emit(x_relative, y_relative)
super().mousePressEvent(e)

Related

Exclude results in find_closest (Tkinter, Python)

I am writing a script to store movements over a hexgrid using Tkinter. As part of this I want to use a mouse-click on a Tkinter canvas to first identify the click location, and then draw a line between this point and the location previously clicked.
Generally this works, except that after I've drawn a line, it become an object that qualifies for future calls off the find_closest method. This means I can still draw lines between points, but selecting the underlying Hex in the Hexgrid over times becomes nearly impossible. I was wondering if someone could help me find a solution to exclude particular objects (lines) from the find_closest method.
edit: I hope this code example is minimal enough.
import tkinter
from tkinter import *
from math import radians, cos, sin, sqrt
class App:
def __init__(self, parent):
self.parent = parent
self.c1 = Canvas(self.parent, width=int(1.5*340), height=int(1.5*270), bg='white')
self.c1.grid(column=0, row=0, sticky='nsew')
self.clickcount = 0
self.clicks = [(0,0)]
self.startx = int(20*1.5)
self.starty = int(20*1.5)
self.radius = int(20*1.5) # length of a side
self.hexagons = []
self.columns = 10
self.initGrid(self.startx, self.starty, self.radius, self.columns)
self.c1.bind("<Button-1>", self.click)
def initGrid(self, x, y, radius, cols):
"""
2d grid of hexagons
"""
radius = radius
column = 0
for j in range(cols):
startx = x
starty = y
for i in range(6):
breadth = column * (1.5 * radius)
if column % 2 == 0:
offset = 0
else:
offset = radius * sqrt(3) / 2
self.draw(startx + breadth, starty + offset, radius)
starty = starty + 2 * (radius * sqrt(3) / 2)
column = column + 1
def draw(self, x, y, radius):
start_x = x
start_y = y
angle = 60
coords = []
for i in range(6):
end_x = start_x + radius * cos(radians(angle * i))
end_y = start_y + radius * sin(radians(angle * i))
coords.append([start_x, start_y])
start_x = end_x
start_y = end_y
hex = self.c1.create_polygon(coords[0][0], coords[0][1], coords[1][0], coords[1][1], coords[2][0],
coords[2][1], coords[3][0], coords[3][1], coords[4][0], coords[4][1],
coords[5][0], coords[5][1], fill='black')
self.hexagons.append(hex)
def click(self, evt):
self.clickcount = self.clickcount + 1
x, y = evt.x, evt.y
tuple_alfa = (evt.x, evt.y)
self.clicks.append(tuple_alfa)
if self.clickcount >= 2:
start = self.clicks[self.clickcount - 1]
startx = start[0]
starty = start[1]
self.c1.create_line(evt.x, evt.y, startx, starty, fill='white')
clicked = self.c1.find_closest(x, y)[0]
print(clicked)
root = tkinter.Tk()
App(root)
root.mainloop()

Make graphics item move around another item instead of passing through

I have a graphics scene with QGraphicsEllipseitem circles that are movable. I am trying to have the one I am dragging move around the other circle instead of allowing them to overlap aka collide. So far I was able to stop the collision but its not moving around smoothly it snaps to a corner. I dont know how to fix it.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import math
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
self.scene().views()[0].parent().movearound()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.gscene = QGraphicsScene(0, 0, 1000, 1000)
gview = QGraphicsView(self.gscene)
self.setCentralWidget(gview)
self.circle1 = Circleitem (123, brush=QColor(255,255,0))
self.circle2 =Circleitem(80, brush=QColor(0,255,0))
self.gscene.addItem(self.circle1)
self.gscene.addItem(self.circle2)
self.circle1.setPos(500, 500)
self.circle2.setPos(300, 300)
self.show()
def movearound(self):
if self.gscene.selectedItems()[0] == self.circle1:
moveditem = self.circle1
stillitem = self.circle2
else:
moveditem = self.circle2
stillitem = self.circle1
if len(self.gscene.collidingItems(moveditem)) != 0:
xdist = moveditem.x() - stillitem.x()
ydist = moveditem.y() - stillitem.y()
totaldist = moveditem.rect().width()/2 + stillitem.rect().width()/2
totaldist *= math.sqrt(1 + pow(math.pi, 2)/10)/2
if ( abs(xdist) < totaldist or abs(ydist) < totaldist ):
if xdist > 0:
x = stillitem.x() + totaldist
else:
x = stillitem.x() - totaldist
if ydist > 0:
y = stillitem.y() + totaldist
else:
y = stillitem.y() - totaldist
moveditem.setPos(x, y)
app = QApplication([])
win = MainWindow()
app.exec()
It is simpler to keep the logic in Circleitem.mouseMoveEvent and use QLineF to find the distance and new position.
class Circleitem(QGraphicsEllipseItem):
def __init__(self, size, brush):
super().__init__()
radius = size / -2
self.setRect(radius, radius, size, size)
self.setBrush(brush)
self.setFlag(self.ItemIsMovable)
self.setFlag(self.ItemIsSelectable)
def paint(self, painter, option, a):
option.state = QStyle.State_None
return super(Circleitem, self).paint(painter,option)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
colliding = self.collidingItems()
if colliding:
item = colliding[0] # Add offset if points are equal so length > 0
line = QLineF(item.pos(), self.pos() + QPoint(self.pos() == item.pos(), 0))
min_distance = (self.rect().width() + item.rect().width()) / 2
if line.length() < min_distance:
line.setLength(min_distance)
self.setPos(line.p2())

Creating Custom Time Picker Widget

I need to create a widget that is used to pick a time. QTimeEdit widget doesn't seem intuitive or a good design. So I decided to create a time picker similar to the time picker in smartphones.
I managed to create the clock and click that makes the pointer (something similar to the pointer in the image) move to the currently clicked position (note: it's not perfect, it still looks bad). I would like to have help with making the inner clock
Here is my code:
from PyQt5 import QtWidgets, QtGui, QtCore
import math, sys
class ClockWidget(QtWidgets.QWidget): # I want to be able to reuse this class for other programs also, so please don't hard code values of the list, start and end
def __init__(self, start, end, lst=[], *args, **kwargs):
super(ClockWidget, self).__init__(*args, **kwargs)
self.lst = lst
if not self.lst:
self.lst = [*range(start, end)]
self.index_start = 0 # tune this to move the letters in the circle
self.pointer_angles_multiplier = 9 # just setting the default values
self.current = None
self.rects = []
#property
def index_start(self):
return self._index_start
#index_start.setter
def index_start(self, index):
self._index_start = index
def paintEvent(self, event):
self.rects = []
painter = QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setColor(QtCore.Qt.red)
pen.setWidth(2)
painter.setPen(pen)
x, y = self.rect().x(), self.rect().y()
width, height = self.rect().width(), self.rect().height()
painter.drawEllipse(x, y, x + width, x + height)
s, t, equal_angles, radius = self.angle_calc()
radius -= 30
pen.setColor(QtCore.Qt.green)
pen.setWidth(2)
painter.setPen(pen)
""" pointer angle helps in determining to which position the pointer should be drawn"""
self.pointer_x, self.pointer_y = s + ((radius-30) * math.cos(self.pointer_angles_multiplier * equal_angles)), t \
+ ((radius-30) * math.sin(self.pointer_angles_multiplier * equal_angles))
""" The pendulum like pointer """
painter.drawLine(QtCore.QPointF(s, t), QtCore.QPointF(self.pointer_x, self.pointer_y))
painter.drawEllipse(QtCore.QRectF(QtCore.QPointF(self.pointer_x - 20, self.pointer_y - 40),
QtCore.QPointF(self.pointer_x + 30, self.pointer_y + 10)))
pen.setColor(QtCore.Qt.blue)
pen.setWidth(3)
font = self.font()
font.setPointSize(14)
painter.setFont(font)
painter.setPen(pen)
""" Drawing the number around the circle formula y = t + radius * cos(a)
y = s + radius * sin(a) where angle is in radians (s, t) are the mid point of the circle """
for index, char in enumerate(self.lst, start=self.index_start):
angle = equal_angles * index
y = t + radius * math.sin(angle)
x = s + radius * math.cos(angle)
# print(f"Add: {add_x}, index: {index}; char: {char}")
rect = QtCore.QRectF(x - 30, y - 40, x + 60, y) # clickable point
self.rects.append([index, char, rect]) # appends index, letter, rect
painter.setPen(QtCore.Qt.blue)
painter.drawRect(rect) # helps in visualizing the points where the click can received
print(f"Rect: {rect}; char: {char}")
painter.setPen(QtCore.Qt.red)
points = QtCore.QPointF(x, y)
painter.drawText(points, str(char))
def mousePressEvent(self, event):
for x in self.rects:
index, char, rect = x
if event.button() & QtCore.Qt.LeftButton and rect.contains(event.pos()):
self.pointer_angles_multiplier = index
self.current = char
self.update()
break
def angle_calc(self):
"""
This will simply return (midpoints of circle, divides a circle into the len(list) and return the
angle in radians, radius)
"""
return ((self.rect().width() - self.rect().x()) / 2, (self.rect().height() - self.rect().y()) / 2,
(360 / len(self.lst)) * (math.pi / 180), (self.rect().width() / 2))
def resizeEvent(self, event: QtGui.QResizeEvent):
"""This is supposed to maintain a Square aspect ratio on widget resizing but doesn't work
correctly as you will see when executing"""
if event.size().width() > event.size().height():
self.resize(event.size().height(), event.size().width())
else:
self.resize(event.size().width(), event.size().width())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
message = ClockWidget(1, 13)
message.index_start = 10
message.show()
sys.exit(app.exec())
The Output:
The blue rectangles represent the clickable region. I would be glad if you could also, make the pointer move to the closest number when clicked inside the clock (Not just move the pointer when the clicked inside the blue region)
There is one more problem in my code, that is the numbers are not evenly spaced from the outer circle. (like the number 12 is closer to the outer circle than the number 6)
Disclaimer: I will not explain the cause of the error but the code I provide I think should give a clear explanation of the errors.
The logic is to calculate the position of the centers of each small circle, and use the exinscribed rectangle to take it as a base to draw the text and check if the point where you click is close to the texts.
from functools import cached_property
import math
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ClockWidget(QtWidgets.QWidget):
L = 12
r = 40.0
DELTA_ANGLE = 2 * math.pi / L
current_index = 9
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
R = min(self.rect().width(), self.rect().height()) / 2
margin = 4
Rect = QtCore.QRectF(0, 0, 2 * R - margin, 2 * R - margin)
Rect.moveCenter(self.rect().center())
painter.setBrush(QtGui.QColor("gray"))
painter.drawEllipse(Rect)
rect = QtCore.QRectF(0, 0, self.r, self.r)
if 0 <= self.current_index < 12:
c = self.center_by_index(self.current_index)
rect.moveCenter(c)
pen = QtGui.QPen(QtGui.QColor("red"))
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(c, self.rect().center())
painter.setBrush(QtGui.QColor("red"))
painter.drawEllipse(rect)
for i in range(self.L):
j = (i + 2) % self.L + 1
c = self.center_by_index(i)
rect.moveCenter(c)
painter.setPen(QtGui.QColor("white"))
painter.drawText(rect, QtCore.Qt.AlignCenter, str(j))
def center_by_index(self, index):
R = min(self.rect().width(), self.rect().height()) / 2
angle = self.DELTA_ANGLE * index
center = self.rect().center()
return center + (R - self.r) * QtCore.QPointF(math.cos(angle), math.sin(angle))
def index_by_click(self, pos):
for i in range(self.L):
c = self.center_by_index(i)
delta = QtGui.QVector2D(pos).distanceToPoint(QtGui.QVector2D(c))
if delta < self.r:
return i
return -1
def mousePressEvent(self, event):
i = self.index_by_click(event.pos())
if i >= 0:
self.current_index = i
self.update()
#property
def hour(self):
return (self.current_index + 2) % self.L + 1
def minumumSizeHint(self):
return QtCore.QSize(100, 100)
def main():
app = QtWidgets.QApplication(sys.argv)
view = ClockWidget()
view.resize(400, 400)
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

VTK: problem about coordinate convertion: display vs world

I am using VTK in my project, and I meet a problem about the coordinate convertion: from display coordinate vs world coordiante.
First, I show a cone and set the window size to (500, 500). Then, I calcualte the world point using the (250, 250) display point. The display point (250, 250) is located in the cone.
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
if picker.GetActor():
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
self.position = position
I show this point as a red point.
Then, I rotate the screen, and I want to convert the world point as the display point. In my thought, the red point would move along with the cone.
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToWorld()
point = self.position
coordinate.SetValue(point[0], point[1], point[2])
displayCoor = coordinate.GetComputedDisplayValue(self.ren)
However, the red point is seperated from the cone. Is there something wrong with the coordinate convertion?
The whole code is:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys, os
import numpy as np
import vtk, qimage2ndarray
class vtkLabel(QLabel):
def __init__(self):
super(vtkLabel, self).__init__()
self.setFixedSize(500, 500)
cone = vtk.vtkConeSource()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(cone.GetOutputPort())
actor = vtk.vtkActor()
actor.SetMapper(mapper)
ren = vtk.vtkRenderer()
ren.AddActor(actor)
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetOffScreenRendering(1)
imageFilter = vtk.vtkWindowToImageFilter()
imageFilter.SetInput(renWin)
self.ren = ren
self.renWin = renWin
self.imageFilter = imageFilter
def mousePressEvent(self, QMouseEvent):
super().mousePressEvent(QMouseEvent)
pos = QMouseEvent.pos()
x = pos.x()
y = pos.y()
self.lastPos = [x, y]
def mouseReleaseEvent(self, QMouseEvent):
super().mouseReleaseEvent(QMouseEvent)
self.lastPos = None
def mouseMoveEvent(self, QMouseEvent):
super().mouseMoveEvent(QMouseEvent)
pos = QMouseEvent.pos()
x = pos.x()
y = pos.y()
if self.lastPos != None:
if QMouseEvent.buttons() == Qt.RightButton:
dy = self.lastPos[1] - y
center = self.ren.GetCenter()
dyf = dy/center[1]
import math
val = math.pow(1.1, dyf)
self.ren.GetActiveCamera().Dolly(val)
if QMouseEvent.buttons() == Qt.LeftButton:
dx = x - self.lastPos[0]
dy = self.lastPos[1] - y
size = self.renWin.GetSize()
delta_elevation = -20.0 / size[1]
delta_azimuth = -20.0 / size[0]
rxf = dx * delta_azimuth
ryf = dy * delta_elevation
camera = self.ren.GetActiveCamera()
camera.Azimuth(rxf)
camera.Elevation(ryf)
camera.OrthogonalizeViewUp()
self.ren.ResetCameraClippingRange()
self.ren.UpdateLightsGeometryToFollowCamera()
self.ren.Render()
self.calculationForDisplay()
def resizeEvent(self, QMouseEvent):
super().resizeEvent(QMouseEvent)
self.renWin.SetSize(self.width(), self.height())
self.calculationForDisplay()
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
if picker.GetActor():
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
self.position = position
print('the point is one of the cone point')
else:
raise RuntimeError('the point is not one of the cone point')
def calculationForDisplay(self):
self.renWin.Render()
self.imageFilter.Modified()
self.imageFilter.Update()
displayImg = self.imageFilter.GetOutput()
dims = displayImg.GetDimensions()
from vtk.util.numpy_support import vtk_to_numpy
numImg = vtk_to_numpy(displayImg.GetPointData().GetScalars())
numImg = numImg.reshape(dims[1], dims[0], 3)
numImg = numImg.transpose(0, 1, 2)
numImg = np.flipud(numImg)
displayQImg = qimage2ndarray.array2qimage(numImg)
pixmap = QPixmap.fromImage(displayQImg)
self.pixmap = pixmap
self.update()
def paintEvent(self, QPaintEvent):
super(vtkLabel, self).paintEvent(QPaintEvent)
painter = QPainter(self)
width = self.width()
height = self.height()
painter.drawPixmap(QPoint(0, 0), self.pixmap, QRect(0, 0, width, height))
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToWorld()
point = self.position
coordinate.SetValue(point[0], point[1], point[2])
displayCoor = coordinate.GetComputedDisplayValue(self.ren)
x = displayCoor[0]
y = displayCoor[1]
y = self.height() - y
painter.setPen(QPen(QColor(255, 0, 0), 5))
painter.drawPoint(x, y)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setFixedSize(500, 500)
self.imageLabel = vtkLabel()
self.setCentralWidget(self.imageLabel)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Update:
Finally, I know what's wrong with my code.
In the resizeEvent, I obtain the world point by:
coordinate = vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(250, 250, 0)
position = coordinate.GetComputedWorldValue(self.ren)
Actually, the z should not be 0. We should use the following code to obtain the point:
picker = vtk.vtkPropPicker()
picker.Pick(250, 250, 0, self.ren)
pos = picker.GetPickPosition()
Then, everything is OK.

Canvas absolute and relative coordinates, delta pixels retrieval

In this test script I draw a square I may zoom in by using the mouse wheel.
If I right click inside a cell I get the right cell coordinates (not x and y, but column and row): this is exactly what I expect it to write to the console in the background.
If I, instead, move the canvas by pressing the mouse left button and dragging it somewhere else, the coordinates are not right any more.
Where do I get the delta x and delta y (or offsets) to give back the right info?
FYI:
1) get_pos() is the method that does the check and produces the result.
2) the following code has been tested on Ubuntu 16.10 (with the latest updates) running Python 3.5.2.
import tkinter as tk
import tkinter.ttk as ttk
class GriddedMazeCanvas(tk.Canvas):
def almost_centered(self, cols, rows):
width = int(self['width'])
height = int(self['height'])
cell_dim = self.settings['cell_dim']
rows = rows % height
cols = cols % width
w = cols * cell_dim
h = rows * cell_dim
if self.zoom < 0:
raise ValueError('zoom is negative:', self.zoom)
zoom = self.zoom
if self.drawn() and 1 != zoom:
w *= zoom
h *= zoom
h_shift = (width - w) // 2
v_shift = (height - h) // 2
return [h_shift, v_shift,
h_shift + w, v_shift + h]
def __init__(self, *args, **kwargs):
if 'settings' not in kwargs:
raise ValueError("'settings' not passed.")
settings = kwargs['settings']
del kwargs['settings']
super().__init__(*args, **kwargs)
self.config(highlightthickness=0)
self.settings = settings
self.bind_events()
def draw_maze(self, cols, rows):
self.cols = cols
self.rows = rows
if self.not_drawn():
self.cells = {}
self.cell_dim = self.settings['cell_dim']
self.border_thickness = self.settings['border_thickness']
self.zoom = 1
self.delete(tk.ALL)
maze, coords = self._draw_maze(cols, rows, fix=False)
lines = self._draw_grid(coords)
return maze, lines
def _draw_maze(self, cols, rows, fix=True):
data = self.settings
to_max = data['to_max']
border_thickness = data['border_thickness']
poligon_color = data['poligon_color']
poligon_border_color = data['poligon_border_color']
coords = self.almost_centered(cols, rows)
if fix:
# Fix for the disappearing NW borders
if to_max == cols:
coords[0] += 1
if to_max == rows:
coords[1] += 1
maze = self.create_rectangle(*coords,
fill=poligon_color,
outline=poligon_border_color,
width=border_thickness,
tag='maze')
return maze, coords
def _draw_grid(self, coords):
data = self.settings
poligon_border_color = data['poligon_border_color']
cell_dim = data['cell_dim']
if coords is None:
if self.not_drawn():
raise ValueError('The maze is still uninitialized.')
x1, y1, x2, y2 = self.almost_centered(self.cols, self.rows)
else:
x1, y1, x2, y2 = coords
zoom = self.zoom
if self.drawn() and 1 != zoom:
if self.zoom < 1:
self.zoom = zoom = 1
print('no zooming below 1.')
else:
cell_dim *= zoom
lines = []
for i, x in enumerate(range(x1, x2, cell_dim)):
line = self.create_line(x, y1, x, y2,
fill=poligon_border_color,
tags=('grid', 'grid_hl_{}'.format(i)))
lines.append(line)
for i, y in enumerate(range(y1, y2, cell_dim)):
line = self.create_line(x1, y, x2, y,
fill=poligon_border_color,
tags=('grid', 'grid_vl_{}'.format(i)))
lines.append(line)
return lines
def drawn(self):
return hasattr(self, 'cells')
def not_drawn(self):
return not self.drawn()
def bind_events(self):
self.bind('<Button-4>', self.onZoomIn)
self.bind('<Button-5>', self.onZoomOut)
self.bind('<ButtonPress-1>', self.onScrollStart)
self.bind('<B1-Motion>', self.onScrollMove)
self.tag_bind('maze', '<ButtonPress-3>', self.onMouseRight)
def onScrollStart(self, event):
print(event.x, event.y, self.canvasx(event.x), self.canvasy(event.y))
self.scan_mark(event.x, event.y)
def onMouseRight(self, event):
col, row = self.get_pos(event)
print('zoom:', self.zoom, ' col, row:', col, row)
def onScrollMove(self, event):
delta = event.x, event.y
self.scan_dragto(*delta, gain=1)
def onZoomIn(self, event):
if self.not_drawn():
return
max_zoom = 9
self.zoom += 1
if self.zoom > max_zoom:
print("Can't go beyond", max_zoom)
self.zoom = max_zoom
return
print('Zooming in.', event.num, event.x, event.y, self.zoom)
self.draw_maze(self.cols, self.rows)
def onZoomOut(self, event):
if self.not_drawn():
return
self.zoom -= 1
if self.zoom < 1:
print("Can't go below one.")
self.zoom = 1
return
print('Zooming out.', event.num, event.x, event.y, self.zoom)
self.draw_maze(self.cols, self.rows)
def get_pos(self, event):
x, y = event.x, event.y
cols, rows = self.cols, self.rows
cell_dim, zoom = self.cell_dim, self.zoom
x1, y1, x2, y2 = self.almost_centered(cols, rows)
print('x1, y1, x2, y2:', x1, y1, x2, y2,
' bbox:', self.bbox('maze'))
if not (x1 <= x <= x2 and y1 <= y <= y2):
print('Here we are out of bounds.')
return None, None
scale = zoom * cell_dim
col = (x - x1) // scale
row = (y - y1) // scale
return col, row
class CanvasButton(ttk.Button):
def freeze_origin(self):
if not hasattr(self, 'origin'):
canvas = self.canvas
self.origin = canvas.xview()[0], canvas.yview()[0]
def reset(self):
canvas = self.canvas
x, y = self.origin
canvas.yview_moveto(x)
canvas.xview_moveto(y)
def __init__(self, *args, **kwargs):
if 'canvas' not in kwargs:
raise ValueError("'canvas' not passed.")
canvas = kwargs['canvas']
del kwargs['canvas']
super().__init__(*args, **kwargs)
self.config(command=self.reset)
self.canvas = canvas
root = tk.Tk()
settings = {'cell_dim': 3,
'to_max': 200,
'border_thickness': 1,
'poligon_color': '#F7F37E',
'poligon_border_color': '#AC5D33'}
frame = ttk.Frame(root)
canvas = GriddedMazeCanvas(frame,
settings=settings,
width=640,
height=480)
button = CanvasButton(frame, text='Reset', canvas=canvas)
button.freeze_origin()
canvas.draw_maze(20, 10)
canvas.grid(row=0, column=0, sticky=tk.NSEW)
button.grid(row=1, column=0, sticky=tk.EW)
frame.rowconfigure(0, weight=1)
frame.grid()
root.mainloop()
Looking at a previously answered question, I learned how to find the delta x and delta y I was looking for.
So, the solution is:
def get_pos(self, event):
x, y = event.x, event.y
cols, rows = self.cols, self.rows
cell_dim, zoom = self.cell_dim, self.zoom
x1, y1, x2, y2 = self.almost_centered(cols, rows)
# the following line stores deltax and deltay into x0, y0
x0, y0 = int(self.canvasx(0)), int(self.canvasy(0))
# then it is trivial to compute the solution
xa, ya = x1 - x0, y1 - y0
xb, yb = x2 - x0, y2 - y0
if not (xa <= x <= xb and ya <= y <= yb):
print('Here we are out of bounds.')
return None, None
scale = zoom * cell_dim
col = (x - xa) // scale
row = (y - ya) // scale
return col, row

Categories

Resources