Tkinter: changing canvas pixel size - python

Here is what I want to do:
I am designing an application that allows, among other things, to view DICOM images. What I will call "image" is actually a set of files that contain 2D arrays corresponding to slices. I am using Tkinter to provide an UI. My application only requires to display binary images.
In order to display the slices, I use tk.Canvas which allows very fast display of images. Indeed, I need the most optimised displaying device since I want to the user to be able to travel across slices using the mouse wheel.
The problem is: when displaying a slice, the canvas is always allocating the same dimensions to pixels and therefore, images with lower resolution appear very small. What I want to do is to prevent the user from killing his/her eyes by resizing the canvas.
I thought of course of using PIL.Image().resize() on the image that is then converted to PIL.ImageTk() but this causes two problems:
The greater the resizing, the more time is needed to perform the process and therefore the less the viewer is optimised
This resizing action actually modifies the number of pixels, which loses the original resolution. I do not want this to happen since I require to retrieve mouse position as it hovers over the canvas, in terms of pixels in the original resolution
The solution in my opinion would therefore be to modify the pixel size of the canvas. If it is possible to define it from the start, then resizing would not be necessary and there would be no optimisation problem.
But I have not been able to find a way to modify this. Would anyone have an idea?
I am providing only the frame and the imager of my project if it can help:
Frame:
import PIL.Image
import PIL.ImageTk
import numpy as np
from gui.statusbar import *
from tkinter.messagebox import showinfo
class DicomViewerFrame(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.image_ax = None
self.image_sag = None
self.photo_ax = None
self.photo_sag = None
self.canvas_axial = None
self.canvas_sagittal = None
self.imager = None
self.arr_axial = None
self.arr_sagittal = None
self.index_axial = None
self.index_sagittal = None
self.status_axial = None
self.status_sagittal = None
self.roi_definition = True
self.upper = 0
self.lower = 0
self.selection_axial = None
self.selection_sagittal = None
self.start_x = self.start_y = self.start_z = 0
self.end_x = self.end_y = self.end_z = 0
self.roi = None
self.offset = 700
self.init_viewer_frame()
def init_viewer_frame(self):
# Image canvas
self.canvas_axial = Canvas(self, bd=0, highlightthickness=0)
self.canvas_axial.grid(row=0, column=0, sticky="nw")
self.canvas_axial.bind("<MouseWheel>", self.scroll_axial_images)
self.canvas_axial.config(width=self.offset)
if self.roi_definition:
self.canvas_axial.bind("<B1-Motion>", self.on_move_press_axial)
self.canvas_axial.bind("<ButtonPress-1>", self.on_button_press_axial)
self.canvas_axial.bind("<ButtonRelease-1>", self.on_button_release)
self.canvas_sagittal = Canvas(self, bd=0, highlightthickness=0)
self.canvas_sagittal.grid(row=0, column=1, sticky="nw")
self.canvas_sagittal.bind("<MouseWheel>", self.scroll_sagittal_images)
if self.roi_definition:
self.canvas_sagittal.bind("<B1-Motion>", self.on_move_press_sagittal)
self.canvas_sagittal.bind("<ButtonPress-1>", self.on_button_press_sagittal)
self.canvas_sagittal.bind("<ButtonRelease-1>", self.on_button_release)
# Status bar
self.status_axial = StatusBar(self)
self.status_axial.grid(row=3, column=0, sticky="w")
self.status_sagittal = StatusBar(self)
self.status_sagittal.grid(row=3, column=1, sticky="w")
self.canvas_axial.bind('<Motion>', self.motion_axial)
self.canvas_sagittal.bind('<Motion>', self.motion_sagittal)
def on_button_press_axial(self, event):
self.canvas_axial.delete(self.selection_axial)
self.canvas_sagittal.delete(self.selection_sagittal)
# save mouse drag start position
self.start_x = event.x
self.start_y = event.y
self.selection_axial = self.canvas_axial.create_rectangle(self.end_x, self.end_y, 0, 0, outline="green")
self.selection_sagittal = self.canvas_sagittal.create_rectangle(0, self.start_x, self.arr_sagittal.shape[1],
self.end_x, outline="green")
def on_button_press_sagittal(self, event):
self.canvas_sagittal.delete(self.selection_sagittal)
# save mouse drag start position
self.start_z = event.x
self.selection_sagittal = self.canvas_sagittal.create_rectangle(self.start_z, self.start_x, 0,
self.end_x, outline="green")
def on_move_press_axial(self, event):
curX, curY = (event.x, event.y)
self.end_x = curX
self.end_y = curY
self.motion_axial(event)
# expand rectangle as you drag the mouse
self.canvas_axial.coords(self.selection_axial, self.start_x, self.start_y, curX, curY)
self.canvas_sagittal.coords(self.selection_sagittal, 0, self.start_x, self.arr_sagittal.shape[1], curX)
def on_move_press_sagittal(self, event):
curZ = event.x
self.end_z = curZ
self.motion_sagittal(event)
# expand rectangle as you drag the mouse
self.canvas_sagittal.coords(self.selection_sagittal, self.start_z, self.start_x, curZ, self.end_x)
def on_button_release(self, event):
roi_axial = self.canvas_axial.bbox(self.selection_axial)
roi_sagittal = self.canvas_sagittal.bbox(self.selection_sagittal)
self.roi = ((roi_axial[0], roi_axial[1], roi_sagittal[0]), (roi_axial[2], roi_axial[3], roi_sagittal[2]))
def show_image(self, array_axial, index_axial, array_sagittal, index_sagittal):
self.upper = int(self.parent.pcd_preparer.get_current_upper().get())
self.lower = int(self.parent.pcd_preparer.get_current_lower().get())
if array_axial is None:
return
if array_sagittal is None:
return
# Convert numpy array into a PhotoImage and add it to canvas
self.image_ax = PIL.Image.fromarray(array_axial)
self.photo_ax = PIL.ImageTk.PhotoImage(self.image_ax)
self.image_sag = PIL.Image.fromarray(array_sagittal)
self.photo_sag = PIL.ImageTk.PhotoImage(self.image_sag)
self.canvas_axial.delete("IMG")
self.canvas_axial.create_image(0, 0, image=self.photo_ax, anchor=NW, tags="IMG")
self.canvas_axial.create_text(40, 10, fill="green", text="Slice " + str(index_axial), font=10)
self.canvas_axial.create_text(40, 40, fill="green", text="Axial", font=10)
self.canvas_sagittal.delete("IMG")
self.canvas_sagittal.create_image(0, 0, image=self.photo_sag, anchor=NW, tags="IMG")
self.canvas_sagittal.create_text(40, 10, fill="green", text="x = " + str(index_sagittal), font=10)
self.canvas_sagittal.create_text(40, 40, fill="green", text="Sagittal", font=10)
width_ax = self.image_ax.width
height_ax = self.image_ax.height
width_sag = self.image_sag.width
height_sag = self.image_sag.height
self.canvas_axial.configure(width=width_ax, height=height_ax)
self.canvas_sagittal.configure(width=width_sag, height=height_sag)
# We need to at least fit the entire image, but don't shrink if we don't have to
width_ax = max(self.parent.winfo_width(), width_ax)
height_ax = max(self.parent.winfo_height(), height_ax + StatusBar.height)
width_sag = max(self.parent.winfo_width(), width_sag)
height_sag = max(self.parent.winfo_height(), height_sag + StatusBar.height)
# Resize root window and prevent resizing smaller than the image
newsize = "{}x{}".format(width_ax + width_sag, height_ax + StatusBar.height)
self.parent.geometry(newsize)
# self.parent.minsize(width_ax + width_sag, height_ax + height_sag)
if self.selection_axial is not None:
self.selection_axial = self.canvas_axial.create_rectangle(self.start_x, self.start_y, self.end_x,
self.end_y, outline="green")
if self.selection_sagittal is not None:
self.selection_sagittal = self.canvas_sagittal.create_rectangle(self.start_z, self.start_x, self.end_z,
self.end_x, outline="green")
def scroll_sagittal_images(self, e):
self.imager.index_sagittal += int(e.delta / 120)
self.arr_sagittal, self.index_sagittal = self.imager.get_current_sagittal_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def scroll_axial_images(self, e):
self.imager.index_axial += int(e.delta / 120)
self.arr_axial, self.index_axial = self.imager.get_current_axial_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def change_range(self):
self.arr_axial, self.index_axial = self.imager.get_current_axial_image(self.upper, self.lower)
self.arr_sagittal, self.index_sagittal = self.imager.get_current_sagittal_image(self.upper, self.lower)
self.show_image(self.arr_axial, self.index_axial, self.arr_sagittal, self.index_sagittal)
def set_imager(self, im):
self.imager = im
def motion_axial(self, event):
x, y = event.x, event.y
self.status_axial.set('x = {}, y = {}'.format(x, y))
def motion_sagittal(self, event):
z, y = event.x, event.y
self.status_sagittal.set('y = {}, z = {}'.format(y, z))
Imager:
import numpy as np
class DicomImager:
def __init__(self, datasets):
self.values = None
self.datasets = datasets
self._index_axial = 0
self._index_sagittal = 0
self._window_width = 1
self._window_center = 0
self.size = (int(datasets[0].Rows), int(datasets[0].Columns), len(datasets))
self.spacings = (float(datasets[0].PixelSpacing[0]),
float(datasets[0].PixelSpacing[1]),
float(datasets[0].SliceThickness))
self.axes = (np.arange(0.0, (self.size[0] + 1) * self.spacings[0], self.spacings[0]),
np.arange(0.0, (self.size[2] + 1) * self.spacings[2], self.spacings[2]),
np.arange(0.0, (self.size[1] + 1) * self.spacings[1], self.spacings[1]))
# Load pixel data
self.values = np.zeros(self.size, dtype='int32')
for i, d in enumerate(datasets):
# Also performs rescaling. 'unsafe' since it converts from float64 to int32
np.copyto(self.values[:, :, i], d.pixel_array, 'unsafe')
self.max_value = np.amax(self.values)
self.min_value = np.amin(self.values)
#property
def index_sagittal(self):
return self._index_sagittal
#index_sagittal.setter
def index_sagittal(self, value):
while value < 0:
value += self.size[0]
self._index_sagittal = value % self.size[0]
#property
def index_axial(self):
return self._index_axial
#index_axial.setter
def index_axial(self, value):
while value < 0:
value += self.size[2]
self._index_axial = value % self.size[2]
#property
def window_width(self):
return self._window_width
#window_width.setter
def window_width(self, value):
self._window_width = max(value, 1)
#property
def window_center(self):
return self._window_center
#window_center.setter
def window_center(self, value):
self._window_center = value
def get_sagittal_image(self, index, upper, lower):
# int32 true values (HU or brightness units)
img = self.values[index, :, :]
res1 = np.zeros(img.shape)
res1[img < upper] = 1
res1[img < lower] = 0
# Cast to RGB image so that Tkinter can handle it
res = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
res[:, :, 0] = res[:, :, 1] = res[:, :, 2] = res1 * 255
return res
def get_axial_image(self, index, upper, lower):
# int32 true values (HU or brightness units)
img = self.values[:, :, index]
res1 = np.zeros(img.shape)
res1[img < upper] = 1
res1[img < lower] = 0
# Cast to RGB image so that Tkinter can handle it
res = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
res[:, :, 0] = res[:, :, 1] = res[:, :, 2] = res1 * 255
return res
def get_current_sagittal_image(self, upper, lower):
return self.get_sagittal_image(self._index_sagittal, upper, lower), self._index_sagittal
def get_current_axial_image(self, upper, lower):
return self.get_axial_image(self._index_axial, upper, lower), self._index_axial

The solution in my opinion would therefore be to modify the pixel size of the canvas... But I have not been able to find a way to modify this. Would anyone have an idea?
There is no way to modify the size of a pixel in the canvas. Your only option is to resize the image.

Related

Saving a 16x16 image from a tkinter canvas to a png in python

When I am trying to save an image from a tkinter canvas onto an external file, it doesn't keep the 16x16 resolution I would like which makes it blurry.
I was expecting no blur and it to keep the 16x16 resolution. I tried two methods of saving the file
The first method I tried is this:
self.sprite = tk.Canvas(self.root, width=16, height=16)
def get_clicked_pos(self, event):
y, x = event.y, event.x
gap = 288//16
row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
col = x//gap
self.spriteGrid[row][col].tile = self.colour
self.spriteDraw(gap) # draws on the large canvas
self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")
def save_as_png1(self):
ps = self.sprite.postscript(width=16, height=16)
image = PIL.Image.open(io.BytesIO(ps.encode('utf-8')))
image.save('sprite.png')
In this method I made two canvases. One that is large and visible but a 16x16 grid so you can draw and one that is the correct size and not visible. When you draw on the large canvas it mimics it on the small canvas and when you save it turns the correct size canvas into a postscript and makes it a sprite. However, it doesn't work and creates this:
enter image description here method 1
The second method I tried is:
self.image1 = PIL.Image.new("RGB", (16, 16), "white")
self.draw = PIL.ImageDraw.Draw(self.image1)
def get_clicked_pos(self, event):
y, x = event.y, event.x
gap = 288//16
row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
col = x//gap
self.spriteGrid[row][col].tile = self.colour
self.spriteDraw(gap)
self.draw.rectangle([(col, row), (col+1, row+1)], fill=self.colour)
self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")
def save_as_png2(self):
self.image1.save("alternate.png")
In this method, it copies what you draw onto a PIL image which then gets saved when you call save_as_image2. This provides similar results but in a 16x16 file
enter image description here method 2
The whole code is:
import tkinter as tk
from tkinter.colorchooser import askcolor
import PIL.Image
import PIL.ImageDraw
import io
class DrawWindow():
def __init__(self):
self.root = tk.Tk()
self.colour = "black"
self.menubar = tk.Menu(self.root)
self.optionsmenu = tk.Menu(self.menubar, tearoff=0)
self.optionsmenu.add_command(label="Save v1", command=self.save_as_png1)
self.optionsmenu.add_command(label="Save v2", command=self.save_as_png2)
self.menubar.add_cascade(menu=self.optionsmenu, label="Options")
self.root.config(menu=self.menubar)
self.sprFrame = tk.Frame(self.root)
self.sprFrame.pack()
self.spriteCanvas = tk.Canvas(self.sprFrame, height=288, width=288, bg="white")
self.spriteCanvas.pack(pady=5, padx=5)
self.pickColour = tk.Button(self.sprFrame, text="Pick colour", command=self.changeColour)
self.pickColour.pack(pady=5)
self.spriteGrid = []
for i in range(16):
self.spriteGrid.append([])
for j in range(16):
spot = Spot(i, j, 1, 16)
self.spriteGrid[i].append(spot)
self.sprite = tk.Canvas(self.root, width=16, height=16)
self.image1 = PIL.Image.new("RGB", (16, 16), "white")
self.draw = PIL.ImageDraw.Draw(self.image1)
self.spriteCanvas.bind("<Button-1>", self.get_clicked_pos)
self.root.mainloop()
def get_clicked_pos(self, event):
y, x = event.y, event.x
gap = 288//16
row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
col = x//gap
self.spriteGrid[row][col].tile = self.colour
self.spriteDraw(gap)
self.draw.rectangle([(col, row), (col+1, row+1)], fill=self.colour)
self.sprite.create_rectangle(col, row, col+1, row+1, fill=self.colour, outline="")
def changeColour(self):
self.colour = askcolor(title="Sprite Colour")[1]
def save_as_png1(self):
ps = self.sprite.postscript(width=16, height=16)
image = PIL.Image.open(io.BytesIO(ps.encode('utf-8')))
image.save('sprite.png')
def save_as_png2(self):
self.image1.save("alternate.png")
def spriteDraw(self, gap):
for i in self.spriteGrid:
for j in i:
if not(j.tile==None):
self.spriteCanvas.create_rectangle(j.y*gap, j.x*gap, j.y*gap+gap, j.x*gap+gap, fill=j.tile)
class Spot:
def __init__(self, row, col, width, total_rows):
self.row = row
self.col = col
self.x = row * width
self.y = col * width
self.tile = None
self.width = width
self.total_rows = total_rows
DrawWindow()
I figured it out, method two works but I was using self.draw.rectangle() when I should've been using self.image1.putpixel().
So for anyone wanting the finished code it is:
import tkinter as tk
from tkinter.colorchooser import askcolor
import PIL.Image
import PIL.ImageDraw
class DrawWindow():
def __init__(self):
self.root = tk.Tk()
self.colour = ((0, 0, 0), "black")
self.menubar = tk.Menu(self.root)
self.optionsmenu = tk.Menu(self.menubar, tearoff=0)
self.optionsmenu.add_command(label="Save image", command=self.save_as_png)
self.menubar.add_cascade(menu=self.optionsmenu, label="Options")
self.root.config(menu=self.menubar)
self.sprFrame = tk.Frame(self.root)
self.sprFrame.pack()
self.spriteCanvas = tk.Canvas(self.sprFrame, height=288, width=288, bg="white")
self.spriteCanvas.pack(pady=5, padx=5)
self.pickColour = tk.Button(self.sprFrame, text="Pick colour", command=self.changeColour)
self.pickColour.pack(pady=5)
self.spriteGrid = []
for i in range(16):
self.spriteGrid.append([])
for j in range(16):
spot = Spot(i, j, 1, 16)
self.spriteGrid[i].append(spot)
self.image1 = PIL.Image.new("RGB", (16, 16), "white")
self.spriteCanvas.bind("<Button-1>", self.get_clicked_pos)
self.root.mainloop()
def get_clicked_pos(self, event):
y, x = event.y, event.x
gap = 288//16
row = y//gap # row and column are the wrong way but i found it easier to keep it that way instead of changing it all
col = x//gap
self.spriteGrid[row][col].tile = self.colour[1]
self.spriteDraw(gap)
self.image1.putpixel((col, row), self.colour[0])
def changeColour(self):
self.colour = askcolor(title="Sprite Colour")
print(self.colour)
def save_as_png(self):
self.image1.save("sprite.png")
def spriteDraw(self, gap):
for i in self.spriteGrid:
for j in i:
if not(j.tile==None):
self.spriteCanvas.create_rectangle(j.y*gap, j.x*gap, j.y*gap+gap, j.x*gap+gap, fill=j.tile)
class Spot:
def __init__(self, row, col, width, total_rows):
self.row = row
self.col = col
self.x = row * width
self.y = col * width
self.tile = None
self.width = width
self.total_rows = total_rows
DrawWindow()

How to put a button image borderless/tansparent

I'm trying to put my button borderless so we can we see the progressbar on the background
I tried all the forum but coundt put the border of the button round or make something like the second picture
this is the part of the code for the button :
bg3 = Image.open("blanc.png")
resized_bg03 = bg3.resize((20, 20),Image.ANTIALIAS)
new_bg03 = ImageTk.PhotoImage(resized_bg03)
s = Button(Fenetre,image=new_bg03,borderwidth=0,highlightthickness=0)
s.place(x=600, y=10,height = 20 , width = 20)
I would like to have something more like this with a circle and no border :
can someone help me :)
Try this:
import tkinter as tk
# Kindly plagiarised from: https://stackoverflow.com/a/17985217/11106801
def _create_circle(self, x, y, r, **kwargs):
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs)
tk.Canvas.create_circle = _create_circle
# Please suggest a better name for the class
class ProgressBar(tk.Canvas):
def __init__(self, height=20, radius=6, width=400, circle_colour="black",
colour="red", bar_width=6, bd=0, highlightthickness=0,
anti_bar_colour="white", **kwargs):
super().__init__(height=height, width=width, bd=bd,
highlightthickness=highlightthickness, **kwargs)
self.radius = radius
self.height = height
self.width = width
self.rectangle = None
self.circle_colour = circle_colour
self.colour = colour
self.bar_width = bar_width
self.circle = None
self.button_1_down = False
self.create_anti_bar(anti_bar_colour)
self.progress = 0
super().bind("<Enter>", self.show_circle)
super().bind("<Leave>", self.hide_circle)
super().bind("<Button-1>", self.mouse_click)
super().bind("<ButtonRelease-1>", self.mouse_release)
super().bind("<B1-Motion>", self.mouse_motion)
def create_anti_bar(self, colour):
start_y = (self.height - self.bar_width)/2
end_y = self.height - start_y
start_x = self.radius
end_x = self.width - self.radius
# Change it to >= if you don't what the bar to appear when the
# progress is at 0
if start_x > end_x:
return None
super().create_rectangle(start_x, start_y, end_x, end_y,fill=colour,
outline=colour)
def mouse_click(self, event):
self.button_1_down = True
self.progress = (event.x - self.radius)/(self.width - 2*self.radius)
def mouse_release(self, event=None):
self.button_1_down = False
def mouse_motion(self, event):
if self.button_1_down:
self.mouse_click(event)
def hide_circle(self, event=None):
if self.circle is not None:
super().delete(self.circle)
self.circle = None
def show_circle(self, event=None):
# Try removing the circle if we can
self.hide_circle()
x = (self.width - 2*self.radius)*self._progress + self.radius
self.circle = super().create_circle(x, self.height//2, self.radius,
fill=self.circle_colour,
outline=self.circle_colour)
def update_bar(self):
# Try removing the progress bar
if self.rectangle is not None:
super().delete(self.rectangle)
start_y = (self.height - self.bar_width)/2
end_y = self.height - start_y
start_x = self.radius
end_x = (self.width - 2*self.radius)*self._progress + self.radius
# Change it to >= if you don't what the bar to appear when the
# progress is at 0
if start_x > end_x:
return None
self.rectangle = super().create_rectangle(start_x, start_y, end_x,
end_y, fill=self.colour,
outline=self.colour)
#property
def progress(self):
return self._progress
#progress.setter
def progress(self, new_value):
# Check if the new_value is in the correct range
if new_value < 0:
new_value = 0
elif new_value > 1:
new_value = 1
# Update self._progress
self._progress = new_value
# Update the progress bar
self.update_bar()
# If the circle was shown update it
if self.circle is not None:
self.show_circle()
if __name__ == "__main__":
root = tk.Tk()
pbar = ProgressBar()
pbar.pack()
pbar.progress = 0
# keep incrementing the progress until the end then stop
def increment_progress():
pbar.progress += 0.001
if pbar.progress >= 1:
return None
pbar.after(10, increment_progress)
increment_progress()
root.mainloop()
Tell me if you don't get what any of the methods do. It is too much code to properly annotate

Tkinter: How to get correct bounding box from a zoomed image?

I am writing a program in python tkinter that allows you to select certain areas in the image. This image can be zoomed and dragged inside a Canvas. But the problem is that I can't get correct coordinates of the selections and extract thumbnails from original images using the small one.
Here is my code and a screenshot:
from tkinter import *
from tkinter import ttk
from tkinter import messagebox as mb
import glob
import os
from PIL import Image, ImageTk
import cv2
from src.lsh_utils import get_lsh, query_lsh
from src.io_utils import get_config
COLORS = ['red', 'blue', 'olive', 'teal', 'cyan', 'green', 'black']
CONFIG = get_config('conf.json')
class AutoScrollbar(ttk.Scrollbar):
''' A scrollbar that hides itself if it not needed.
Works only if you use the grid geometry manager '''
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
ttk.Scrollbar.set(self, lo, hi)
def pack(self, **kw):
raise TclError('Cannot use pack with this widget')
def place(self, **kw):
raise TclError('Cannot use place with this widget')
class Zoom_Advanced(ttk.Frame):
''' Advanced zoom of the image '''
def __init__(self, mainframe, path):
''' Initialize the main Frame '''
ttk.Frame.__init__(self, master=mainframe)
self.master.title('Zoom with mouse wheel')
# Vertical and horizontal scrollbars for canvas
vbar = AutoScrollbar(self.master, orient='vertical')
hbar = AutoScrollbar(self.master, orient='horizontal')
vbar.grid(row=0, column=1, sticky='ns', rowspan=4)
hbar.grid(row=1, column=0, sticky='we')
# Create canvas and put image on it
self.canvas = Canvas(self.master, highlightthickness=0,
xscrollcommand=hbar.set, yscrollcommand=vbar.set)
self.canvas.grid(row=0, column=0, sticky='nswe')
self.canvas.update() # wait till canvas is created
vbar.configure(command=self.scroll_y) # bind scrollbars to the canvas
hbar.configure(command=self.scroll_x)
# Make the canvas expandable
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
# Bind events to the Canvas
self.canvas.bind('<Configure>', self.show_image) # canvas is resized
self.canvas.bind('<Button-3>', self.move_from)
self.canvas.bind("<Button-1>", self.mouseClick)
self.canvas.bind('<B3-Motion>', self.move_to)
self.canvas.bind("<Motion>", self.mouseMove)
self.canvas.bind('<MouseWheel>', self.wheel) # with Windows and MacOS, but not Linux
self.canvas.bind('<Button-5>', self.wheel) # only with Linux, wheel scroll down
self.canvas.bind('<Button-4>', self.wheel) # only with Linux, wheel scroll up
self.master.bind("<Escape>", self.cancelBBox) # press <Espace> to cancel current bbox
self.master.bind("s", self.cancelBBox)
self.master.bind("a", self.prevImage) # press 'a' to go backforward
self.master.bind("d", self.nextImage) # press 'd' to go forward
self.side_container = Frame(self.master)
self.side_container.grid_rowconfigure(0, weight=1)
self.side_container.grid_columnconfigure(0, weight=1)
self.side_container.grid(row=0, column=2, sticky='nse')
self.imscale = 1.0 # scale for the canvaas image
self.delta = 1.3 # zoom magnitude
self.lb1 = Label(self.side_container, text='Bounding boxes:')
self.lb1.grid(row=0, column=0, sticky='nwe')
self.listbox = Listbox(self.side_container)
self.listbox.grid(row=1, column=0, sticky='nwe')
self.btnDel = Button(self.side_container, text='Delete', command=self.delBBox)
self.btnDel.grid(row=2, column=0, sticky='nwe')
self.btnClear = Button(self.side_container, text='ClearAll', command=self.clearBBox)
self.btnClear.grid(row=3, column=0, sticky='nwe')
# control panel for image navigation
self.ctrPanel = Frame(self.side_container)
self.ctrPanel.grid(row=4, column=0, columnspan=2, sticky='we')
self.prevBtn = Button(self.ctrPanel, text='<< Prev', width=10, command=self.prevImage)
self.prevBtn.pack(side=LEFT, padx=5, pady=3)
self.nextBtn = Button(self.ctrPanel, text='Next >>', width=10, command=self.nextImage)
self.nextBtn.pack(side=LEFT, padx=5, pady=3)
self.progLabel = Label(self.ctrPanel, text="Progress: / ")
self.progLabel.pack(side=LEFT, padx=5)
self.tmpLabel = Label(self.ctrPanel, text="Go to Image No.")
self.tmpLabel.pack(side=LEFT, padx=5)
self.idxEntry = Entry(self.ctrPanel, width=5)
self.idxEntry.pack(side=LEFT)
self.goBtn = Button(self.ctrPanel, text='Go', command=self.gotoImage)
self.goBtn.pack(side=LEFT)
self.runBtn = Button(self.ctrPanel, text='Run', command=self.run)
self.runBtn.pack(side=LEFT, padx=5, pady=3)
self.imageDir = ''
self.imageList = []
self.egDir = ''
self.egList = []
self.outDir = ''
self.cur = 0
self.total = 0
self.category = 0
self.imagename = ''
self.labelfilename = ''
self.tkimg = None
self.cla_can_temp = []
self.detection_images_path = CONFIG.get('DETECTIONS_PATH', 'data\\detections')
self.orig_images_path = CONFIG.get('IMAGE_DIR')
self.viewer = None
self._new_window = None
# initialize mouse state
self.STATE = {}
self.STATE['click'] = 0
self.STATE['x'], self.STATE['y'] = 0, 0
# reference to bbox
self.bboxIdList = []
self.bboxId = None
self.bboxList = []
self.hl = None
self.vl = None
self.disp = Label(self.ctrPanel, text='')
self.disp.pack(side=RIGHT)
self.loadDir()
# self.show_image()
def scroll_y(self, *args, **kwargs):
''' Scroll canvas vertically and redraw the image '''
self.canvas.yview(*args, **kwargs) # scroll vertically
self.show_image() # redraw the image
def scroll_x(self, *args, **kwargs):
''' Scroll canvas horizontally and redraw the image '''
self.canvas.xview(*args, **kwargs) # scroll horizontally
self.show_image() # redraw the image
def move_from(self, event):
''' Remember previous coordinates for scrolling with the mouse '''
self.canvas.scan_mark(event.x, event.y)
def move_to(self, event):
''' Drag (move) canvas to the new position '''
self.canvas.scan_dragto(event.x, event.y, gain=1)
self.show_image() # redraw the image
def wheel(self, event):
''' Zoom with mouse wheel '''
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
bbox = self.canvas.bbox(self.container) # get image area
if bbox[0] < x < bbox[2] and bbox[1] < y < bbox[3]: pass # Ok! Inside the image
else: return # zoom only inside image area
scale = 1.0
# Respond to Linux (event.num) or Windows (event.delta) wheel event
if event.num == 5 or event.delta == -120: # scroll down
i = min(self.width, self.height)
if int(i * self.imscale) < 30: return # image is less than 30 pixels
self.imscale /= self.delta
scale /= self.delta
if event.num == 4 or event.delta == 120: # scroll up
i = min(self.canvas.winfo_width(), self.canvas.winfo_height())
if i < self.imscale: return # 1 pixel is bigger than the visible area
self.imscale *= self.delta
scale *= self.delta
self.canvas.scale('all', x, y, scale, scale) # rescale all canvas objects
self.show_image()
def show_image(self, event=None):
''' Show image on the Canvas '''
bbox1 = self.canvas.bbox(self.container) # get image area
# Remove 1 pixel shift at the sides of the bbox1
bbox1 = (bbox1[0] + 1, bbox1[1] + 1, bbox1[2] - 1, bbox1[3] - 1)
bbox2 = (self.canvas.canvasx(0), # get visible area of the canvas
self.canvas.canvasy(0),
self.canvas.canvasx(self.canvas.winfo_width()),
self.canvas.canvasy(self.canvas.winfo_height()))
bbox = [min(bbox1[0], bbox2[0]), min(bbox1[1], bbox2[1]), # get scroll region box
max(bbox1[2], bbox2[2]), max(bbox1[3], bbox2[3])]
print(bbox)
if bbox[0] == bbox2[0] and bbox[2] == bbox2[2]: # whole image in the visible area
bbox[0] = bbox1[0]
bbox[2] = bbox1[2]
if bbox[1] == bbox2[1] and bbox[3] == bbox2[3]: # whole image in the visible area
bbox[1] = bbox1[1]
bbox[3] = bbox1[3]
print(bbox2, bbox1)
self.canvas.configure(scrollregion=bbox) # set scroll region
x1 = max(bbox2[0] - bbox1[0], 0) # get coordinates (x1,y1,x2,y2) of the image tile
y1 = max(bbox2[1] - bbox1[1], 0)
x2 = min(bbox2[2], bbox1[2]) - bbox1[0]
y2 = min(bbox2[3], bbox1[3]) - bbox1[1]
print(x1, y1, x2, y2)
if int(x2 - x1) > 0 and int(y2 - y1) > 0: # show image if it in the visible area
x = min(int(x2 / self.imscale), self.width) # sometimes it is larger on 1 pixel...
y = min(int(y2 / self.imscale), self.height) # ...and sometimes not
print(x, y)
image = self.image.crop((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
self.imagetk = ImageTk.PhotoImage(image.resize((int(x2 - x1), int(y2 - y1))))
imageid = self.canvas.create_image(max(bbox2[0], bbox1[0]), max(bbox2[1], bbox1[1]),
anchor='nw', image=self.imagetk)
self.canvas.lower(imageid) # set image into background
self.canvas.imagetk = self.imagetk # keep an extra reference to prevent garbage-collection
def run(self):
labels = self._compare_labels()
lsh = get_lsh(CONFIG.get('LSH_DATASET'))
if not labels:
return
for file, bboxes in labels.items():
im = cv2.imread(file)
h, w, _ = im.shape
print(w, h)
rois = []
for box in bboxes:
x1 = int(int(box[0])/600*w)
y1 = int(int(box[1])/800*h)
x2 = int(int(box[2])/600*w)
y2 = int(int(box[3])/800*h)
roi = im[y1:y2, x1:x2].copy()
print(file, [n[0][1] for n in query_lsh(lsh, roi)])
# cv2.imshow(str(box), roi)
# cv2.waitKey()
# def _update_labels_boxes(self, w, h):
# for i, bbox in enumerate(self.listbox):
# x1 = int(int(bbox[0]) / 768 * w)
# y1 = int(int(bbox[1]) / 768 * h)
# x2 = int(int(bbox[2]) / 768 * w)
# y2 = int(int(bbox[3]) / 768 * h)
#
def _compare_labels(self):
label_dict = dict()
if not self.imageDir:
return None
for label_file in glob.glob(os.path.join(self.imageDir, '*.txt')):
with open(label_file, 'r') as f:
filename = ''.join([os.path.splitext(label_file)[0], '.jpg'])
label_dict[filename] = [tuple(l.split()) for l in f.readlines()]
return label_dict
def loadDir(self, dbg=False):
# get image list
self.imageDir = self.orig_images_path
# print self.imageDir
# print self.category
self.imageList = glob.glob(os.path.join(self.imageDir, '*.JPG'))
print(self.imageList)
if len(self.imageList) == 0:
print('No .JPG images found in the specified dir!')
return
# default to the 1st image in the collection
self.cur = 1
self.total = len(self.imageList)
# set up output dir
self.outDir = self.imageDir
if not os.path.exists(self.outDir):
os.mkdir(self.outDir)
self.loadImage()
print('%d images loaded from %s' % (self.total, self.orig_images_path))
def new_window(self, path):
self.newWindow = Toplevel(self.master)
# frame = Frame(self.newWindow)
self.new_panel = Canvas(self.newWindow, cursor='tcross')
self.new_img = ImageTk.PhotoImage(Image.open(path).resize((600, 800)))
self.new_panel.pack()
self.new_panel.config(width=max(self.new_img.width(), 400), height=max(self.new_img.height(), 400))
self.new_panel.create_image(0, 0, image=self.new_img, anchor=NW)
return self.new_panel
def loadImage(self):
# load image
imagepath = self.imageList[self.cur - 1]
self.img = Image.open(imagepath).resize((600, 800))
detect_image_path = os.path.join(self.detection_images_path, 'detect_'+os.path.basename(imagepath))
if not os.path.exists(detect_image_path):
mb.showerror(f'image {detect_image_path} doesn\' exists')
self.nextImage(save=False)
self.new_window(detect_image_path)
# cv2.imshow(f'{detect_image_path}', detect_image_path)
self.image = Image.open(path) # open image
self.width, self.height = self.image.size
# Put image into container rectangle and use it to set proper coordinates to the image
self.container = self.canvas.create_rectangle(0, 0, self.width, self.height, width=0)
self.show_image()
# load labels
self.clearBBox()
self.imagename = os.path.split(imagepath)[-1].split('.')[0]
labelname = self.imagename + '.txt'
self.labelfilename = os.path.join(self.outDir, labelname)
if os.path.exists(self.labelfilename):
with open(self.labelfilename) as f:
for (i, line) in enumerate(f):
# tmp = [int(t.strip()) for t in line.split()]
tmp = line.split()
self.bboxList.append(tuple(tmp))
tmpId = self.canvas.create_rectangle(int(tmp[0]), int(tmp[1]),
int(tmp[2]), int(tmp[3]),
width=2,
outline=COLORS[(len(self.bboxList) - 1) % len(COLORS)])
# print tmpId
self.bboxIdList.append(tmpId)
self.listbox.insert(END, '(%d, %d) -> (%d, %d)' % (int(tmp[0]), int(tmp[1]),
int(tmp[2]), int(tmp[3])))
self.listbox.itemconfig(len(self.bboxIdList) - 1,
fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)])
def saveImage(self):
if self.newWindow:
self.newWindow.destroy()
with open(self.labelfilename, 'w') as f:
for bbox in self.bboxList:
f.write(' '.join(map(str, bbox)) + '\n')
print('Image No. %d saved' % (self.cur))
def mouseClick(self, event):
new_x, new_y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
if self.STATE['click'] == 0:
self.STATE['x'], self.STATE['y'] = new_x, new_y
else:
x1, x2 = min(self.STATE['x'], new_x), max(self.STATE['x'], new_x)
y1, y2 = min(self.STATE['y'], new_y), max(self.STATE['y'], new_y)
x, y = int(x2 / self.imscale), int(y2 / self.imscale)
print(self.imscale, (int(x1 / self.imscale), int(y1 / self.imscale), x, y))
im = self.image.crop((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
im.show()
self.bboxList.append((int(x1 / self.imscale), int(y1 / self.imscale), x, y))
self.bboxIdList.append(self.bboxId)
self.bboxId = None
self.listbox.insert(END, '(%d, %d) -> (%d, %d)' % (x1, y1, x2, y2))
self.listbox.itemconfig(len(self.bboxIdList) - 1, fg=COLORS[(len(self.bboxIdList) - 1) % len(COLORS)])
self.STATE['click'] = 1 - self.STATE['click']
def mouseMove(self, event):
new_x, new_y = int(self.canvas.canvasx(event.x)), int(self.canvas.canvasy(event.y))
self.disp.config(text='x: %d, y: %d' % (new_x, new_y))
if self.canvas:
if self.hl:
self.canvas.delete(self.hl)
self.hl = self.canvas.create_line(0, new_y, self.canvas['width'], new_y, width=2)
if self.vl:
self.canvas.delete(self.vl)
self.vl = self.canvas.create_line(new_x, 0, new_x, self.canvas['height'], width=2)
if 1 == self.STATE['click']:
if self.bboxId:
self.canvas.delete(self.bboxId)
self.bboxId = self.canvas.create_rectangle(self.STATE['x'], self.STATE['y'],
new_x, new_y,
width=2,
outline=COLORS[len(self.bboxList) % len(COLORS)])
def cancelBBox(self, event):
if 1 == self.STATE['click']:
if self.bboxId:
self.canvas.delete(self.bboxId)
self.bboxId = None
self.STATE['click'] = 0
def delBBox(self):
sel = self.listbox.curselection()
if len(sel) != 1:
return
idx = int(sel[0])
self.canvas.delete(self.bboxIdList[idx])
self.bboxIdList.pop(idx)
self.bboxList.pop(idx)
self.listbox.delete(idx)
def clearBBox(self):
for idx in range(len(self.bboxIdList)):
self.canvas.delete(self.bboxIdList[idx])
self.listbox.delete(0, len(self.bboxList))
self.bboxIdList = []
self.bboxList = []
def prevImage(self, event=None):
self.saveImage()
if self.cur > 1:
self.cur -= 1
self.loadImage()
def nextImage(self, event=None, save=True):
if save:
self.saveImage()
if self.cur < self.total:
self.cur += 1
self.loadImage()
def gotoImage(self):
idx = int(self.idxEntry.get())
if 1 <= idx <= self.total:
self.saveImage()
self.cur = idx
self.loadImage()
path = 'data\\detections\\detect_1.jpg' # place path to your image here
root = Tk()
root.geometry('1280x720')
app = Zoom_Advanced(root, path=path)
root.mainloop()
I think the solution is in def MouseClick and in show_image. My thoughts is to use the same methods of extracting a tile (or a bbox) from original image as in show_image.
I tried to fo that but I didn't get any results. I don't understand how can I do that so I'm searching for the help here.
I did that with this code, where self.container is a rectangle around image with its width and height and self.imscale is scale of image.
...
new_x, new_y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
if self.STATE['click'] == 0:
self.STATE['x'], self.STATE['y'] = new_x, new_y
else:
x1, x2 = min(self.STATE['x'], new_x), max(self.STATE['x'], new_x)
y1, y2 = min(self.STATE['y'], new_y), max(self.STATE['y'], new_y)
self.canvas.create_text((x1 + x2) // 2, (y1 + y2) // 2, text=self.bbox_num)
bbox = self.canvas.bbox(self.container)
x1 = int((x1 - bbox[0]) / self.imscale)
y1 = int((y1 - bbox[1]) / self.imscale)
x2 = int((x2 - bbox[0]) / self.imscale)
y2 = int((y2 - bbox[1]) / self.imscale)

qtpropertyanimation only change the scale

I used animator class to generate ellipseobject and also add animation to them, the goal is only change the scale (from double size to normal size) of the dot, but not do the translate. Right now what I am facing is the dot does shrink, but it moves from the location where (x y value are double with its original x y value) to its original x y location.
Here is the animator class
class Animator:
def __init__(self, animation_config, num_rows, num_cols, background_colour, parent, user_idx = 0, resolution_width = 1920,
resolution_height = 1200, log_file_path = "coordinates.txt"):
with open(log_file_path, "w") as log_file:
log_file.write("")
circle = animation_config["circle"]
self.__cur_circle = None
self.__cur_colour = None
self.__animated_colour = QColor(*circle["colour"])
self.__log_file_path = log_file_path
# Initialize the circle table
diamond_square = DiamondSquare()
self.__user_idx = user_idx % 64
shift_xn = diamond_square.dx[user_idx]
shift_yn = diamond_square.dy[user_idx]
intv_x = (resolution_width - 20) / 10
intv_y = (resolution_height - 20) / 10
print(shift_xn,shift_yn)
self.__circle_table = []
y = 0
for i in range (11):
circles = []
x = 0
for j in range (11):
cir_x = (j * intv_x) + shift_xn
cir_y = (i * intv_y) + shift_yn
print(cir_x,cir_y)
if cir_x <= resolution_width and cir_y <= resolution_height:
circles.append(EllipseObject(parent,cir_x,cir_y, x, y, intv_x, intv_y,
hidden_colour = background_colour,
display_colour=self.__animated_colour))
x += 1
y += 1
self.__circle_table.append(circles)
# Initalize the first animation
self.__first = QPropertyAnimation()
self.__first.setPropertyName(b"scale")
self.__first.setDuration(animation_config["animation_duration"])
self.__first.setStartValue(2)
self.__first.setEndValue(1)
and here is my EllipseObject class
class EllipseObject(QGraphicsWidget):
def __init__(
self,
parent,
x = 0,
y = 0,
ind_x = 0,
ind_y = 0,
intv_x = 0,
intv_y = 0,
width = 20,
height = 20,
hidden = True,
hidden_colour = QColor(Qt.white),
display_colour = QColor(Qt.black)
):
super().__init__(parent)
self.__x = x
self.__y = y
self.__ind_x = ind_x
self.__ind_y = ind_y
self.__intv_x = intv_x
self.__intv_y = intv_y
self.__height = height
self.__width = width
self.hidden = hidden
self.hidden_colour = hidden_colour
self.display_colour = display_colour
def paint(self, painter, option, widget = None):
colour = QColor(Qt.white) if self.hidden else self.display_colour
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(colour)
painter.setBrush(QBrush(colour))
# painter.drawEllipse(self.__x, self.__y, self.__width, self.__height)
painter.drawEllipse(self.boundingRect())
# point = self.mapToScene(self.boundingRect().center())
# print("Draw this ",point.x(), point.y())
def get_coordinates(self):
point = self.mapToScene(self.boundingRect().center())
return (point.x(), point.y())
def boundingRect(self):
return QRectF(self.__x,
self.__y,
self.__height, self.__width)
What you are probably observing is that it is rising with respect to the upper left corner and will give you the appearance that it is moving. The QGraphicsItem has a property called transformOriginPoint with respect to which transformations, such as rotation and scaling, are performed and which is found at point (0, 0). In your case your code has several errors, you must set the position with setPos() and not through boundingRect(), so that the scaling is with respect to the center of the item establishes that the boundingRect() is symmetric.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class EllipseObject(QGraphicsWidget):
def __init__(
self,
parent=None,
x = 0,
y = 0,
ind_x = 0,
ind_y = 0,
intv_x = 0,
intv_y = 0,
width = 20,
height = 20,
hidden = True,
hidden_colour = QColor(Qt.white),
display_colour = QColor(Qt.black)
):
super().__init__(parent)
self.__x = x
self.__y = y
self.__ind_x = ind_x
self.__ind_y = ind_y
self.__intv_x = intv_x
self.__intv_y = intv_y
self.__height = height
self.__width = width
self.hidden = hidden
self.hidden_colour = hidden_colour
self.display_colour = display_colour
self.setPos(self.__x, self.__y)
def paint(self, painter, option, widget = None):
colour = QColor(Qt.white) if self.hidden else self.display_colour
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(colour)
painter.setBrush(QBrush(colour))
# painter.drawEllipse(self.__x, self.__y, self.__width, self.__height)
painter.drawEllipse(self.boundingRect())
def get_coordinates(self):
return (self.pos().x(), self.pos().y())
def boundingRect(self):
return QRectF(-0.5*self.__width,
-0.5*self.__height,
self.__height, self.__width)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = QGraphicsView()
scene = QGraphicsScene(w)
w.setScene(scene)
it = EllipseObject(x=100, y=100, hidden=False)
scene.addItem(it)
animation = QPropertyAnimation(it, b"scale")
animation.setDuration(2000)
animation.setStartValue(2)
animation.setEndValue(1)
animation.start()
w.show()
sys.exit(app.exec_())
Note:
Instead of using flag called hidden, use setVisible(boolean), show() or hide() directly. Qt already has many methods implemented, the wheel does not reinvent.

How may I reset the view of a canvas in tkinter?

When I draw a rectangle in the canvas I know its coordinates, absolute (canvasx(), canvasy()) and relative (the ones that I have passed).
Then, I may be able to move the canvas using the canvas scan_mark() and scan_dragto() methods.
How may I return to the original position (before one or more of these scan_mark()/scan_dragto() calls) in order to re-center the rectangle of which the user might even have lost the position?
Is there any reset view command/method? How may I keep track of the change that has occurred?
The Tkinter documentation I've seen doesn't seem to mention this, but the underlying Tk Canvas xview/yview methods can be called with no parameter to get the current scroll position (actually, as a tuple of two elements, the second not being of any use to you). So, the following should work (not tested):
Save position:
origX = yourcanvas.xview()[0]
origY = yourcanvas.yview()[0]
Restore position:
yourcanvas.xview_moveto(origX)
yourcanvas.yview_moveto(origY)
As in the previous comment #jasonharper, his suggestion worked. For others who might have the same problem I attach a working example even though it has not been cleaned so well to be proud of, it may give you way to see how zoom in and out, view drag and reset might work.
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 // 2 + 1
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 = 0
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
if self.drawn() and 0 != self.zoom:
if self.zoom < 0:
self.zoom = 0
print('no zooming at negative values.')
else:
zoom = self.zoom // 2 + 1
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, col, row:', self.zoom, 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 = 16
self.zoom += 2
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 -= 2
if self.zoom < 0:
print("Can't go below zero.")
self.zoom = 0
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)
if not (x1 <= x <= x2 and y1 <= y <= y2):
print('Here we are out of bounds.')
return None, None
scale = (zoom // 2 + 1) * 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 not 'canvas' 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()
Tested with Python 3.4 on latest Ubuntu 16.10
(Events: MouseLeft, Mouseright, MouseWheel, ButtonPress)
HTH

Categories