I'm working on a dragable tkinter canvas class that knows its position and scale, for that I would like to use a class that inherits from tkinter.canvas
. However, the inheriting class behaves differently to when I use the tkinter.canvas
class. The problem isn't that the size when initializing my own class isn't correct. I want to know why, and how to solve the difference... A known AI tool rewrites the program but cannot explain why the inheriting behavior differs from the standard class.
I'm on win11 23H2 x64 system with Python 3.10.7. Just change the True
in the __init__
of class App
to False
and it behaves differently, at least on my machine.
import tkinter as tk
class App(tk.Tk):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if True: # change True to False to see the difference
# using the ebenden class
self.canvas = OwnCanvas(self, width=400, height=400)
self.canvas.pack(side="top", fill="both", expand=True)
else:
# using the tkinter class
self.canvas = tk.Canvas(self, width=400, height=400)
self.canvas.pack(side="top", fill="both", expand=True)
for y in range(-10, 10):
for x in range(-10, 10):
self.canvas.create_oval(x-5, y-5, x+5, y+5)
class OwnCanvas(tk.Canvas):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.status_bar = tk.Frame(self)
self.ulc_position = [.0, .0] # upper left corner position
self.position_label = tk.Label(self.status_bar, text="Position: (0, 0)")
self.c_scale = 1.0 # canvas scale
self.scale_label = tk.Label(self.status_bar, text="Scale: 1.0")
self.bind("<Button-1>", self.on_b1_click)
self.bind("<B1-Motion>", self.on_b1_motion)
self.bind("<MouseWheel>", self.on_mousewheel)
self.lastX, self.lastY = 0, 0
def pack(self, *args, **kwargs):
super().pack(*args, **kwargs)
self.status_bar.pack(side="bottom", fill="x")
self.position_label.pack(side="left")
self.scale_label.pack(side="left")
def on_b1_click(self, event):
self.lastX, self.lastY = event.x, event.y
def on_b1_motion(self, event):
dx, dy = event.x-self.lastX, event.y-self.lastY
self.move(tk.ALL, dx, dy)
self.lastX, self.lastY = event.x, event.y
self.ulc_position[0] += dx
self.ulc_position[1] += dy
self.position_label.config(text=f"Position: ({self.ulc_position[0]}, {self.ulc_position[1]})")
def on_mousewheel(self, event):
if event.delta > 0:
self.scale(tk.ALL, 0, 0, 1.1, 1.1)
self.c_scale *= 1.1
else:
self.scale(tk.ALL, 0, 0, 0.9, 0.9)
self.c_scale *= 0.9
self.scale_label.config(text=f"Scale: {self.c_scale:.2f}")
def main():
app = App()
app.mainloop()
if __name__ == "__main__":
main()
As explained in the comments you can not compare the initialisation of a pure Canvas
with basically creating combination of widgets at the same level.
standard tk.Canvas doesn't have Frame and other element inside and it doesn't have to calculate size using size of Frame. And if you want to put widget on Canvas then you should use canvas.create_window(widget) instead of using widget.pack(). And if you want to use pack() then maybe you should start with Frame and put Canvas in this Frame and other frames also but in this Frame instead of puttting in Canvas. – furas
If the size you pass in the initialization is soly for the canvas and the overall widget OwnCanvas
can be a little bigger you could just wrap the contents in a container
frame. otherwise you could set the size of status_bar
and subtract it from the geometry passed to your class when initializing the Canvas
.
Here is an example of the container implementation without calculating the size to respect the size of the status_bar
:
import tkinter as tk
class App(tk.Tk):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# pass master with keyword to create container
self.canvas = OwnCanvas(master=self, width=400, height=400)
self.canvas.pack(side="top", fill="both", expand=True)
for y in range(-10, 10):
for x in range(-10, 10):
self.canvas.create_oval(x - 5, y - 5, x + 5, y + 5)
class OwnCanvas(tk.Canvas):
def __init__(self, master, *args, **kwargs) -> None:
self.container = tk.Frame(master) # create container frame
# assign the canvas to the container
super().__init__(master=self.container, *args, **kwargs)
self.status_bar = tk.Frame(self.container) # also assign status to container
self.ulc_position = [.0, .0] # upper left corner position
self.position_label = tk.Label(self.status_bar, text="Position: (0, 0)")
self.c_scale = 1.0 # canvas scale
self.scale_label = tk.Label(self.status_bar, text="Scale: 1.0")
self.bind("<Button-1>", self.on_b1_click)
self.bind("<B1-Motion>", self.on_b1_motion)
self.bind("<MouseWheel>", self.on_mousewheel)
self.lastX, self.lastY = 0, 0
def pack(self, *args, **kwargs):
self.container.pack() # don't forgeet to pack the container
super().pack(*args, **kwargs)
self.status_bar.pack(side="bottom", fill="x")
self.position_label.pack(side="left")
self.scale_label.pack(side="left")
def on_b1_click(self, event):
self.lastX, self.lastY = event.x, event.y
def on_b1_motion(self, event):
dx, dy = event.x - self.lastX, event.y - self.lastY
self.move(tk.ALL, dx, dy)
self.lastX, self.lastY = event.x, event.y
self.ulc_position[0] += dx
self.ulc_position[1] += dy
self.position_label.config(text=f"Position: ({self.ulc_position[0]}, {self.ulc_position[1]})")
def on_mousewheel(self, event):
if event.delta > 0:
self.scale(tk.ALL, 0, 0, 1.1, 1.1)
self.c_scale *= 1.1
else:
self.scale(tk.ALL, 0, 0, 0.9, 0.9)
self.c_scale *= 0.9
self.scale_label.config(text=f"Scale: {self.c_scale:.2f}")
def main():
app = App()
app.mainloop()
if __name__ == "__main__":
main()