3

I'm trying to add support for zooming in and out inside a Canvas widget, containing both text element (created using create_text) and non-text elements, such as rectangles (created with create_rectangle), etc.

So far, I made the following MRE using both part of this answer and this one:

import tkinter as tk
from tkinter.font import Font

root = tk.Tk()

canvas = tk.Canvas(root, width=400, height=400)
canvas.pack()

font = Font(family="Arial", size=10)
fontsize = 10

# Add elements to canvas
rectangle = canvas.create_rectangle(100, 100, 300, 200, fill='red')
oval = canvas.create_oval(150, 150, 250, 250, fill='blue')
text = canvas.create_text(200, 175, text="Hello", font=font)

def do_zoom(event):
    global fontsize

    x = canvas.canvasx(event.x)
    y = canvas.canvasy(event.y)
    factor = 1.001 ** event.delta

    if (event.delta > 0):
        fontsize *= 1.1
        font.configure(size=int(fontsize))
    elif (event.delta < 0):
        fontsize *= 0.9
        font.configure(size=int(fontsize))
    canvas.scale("all", x, y, factor, factor)


canvas.bind("<MouseWheel>", do_zoom)
canvas.bind('<ButtonPress-1>', lambda event: canvas.scan_mark(event.x, event.y))
canvas.bind("<B1-Motion>", lambda event: canvas.scan_dragto(event.x, event.y, gain=1))

root.mainloop()

This seems to work, but has one or two issues:

  • Once the font size get to 0 or 0.0??? in floats (happen when zooming out), the font size doesn't match with the actual visual size of the font, as it seems to be fixed in a higher size (can be seen on this gif here)
  • When zooming in and out fast enough, repeatedly, a discrepancy can be seen on the font size on previous mousewheel scrolling, and the next one (can be seen by printing the font size).

In short, I'm wondering if there is a way, either to fix the above (and the reasons for them happening, aside from my own conjecture) or if there is a better way to handle this.

Nordine Lotfi
  • 463
  • 2
  • 5
  • 20

2 Answers2

4

Positive numbers are treated as points, negative numbers are treated as pixels. You will need to write code to account for that.

The official tcl/tk documentation says this about the size option:

”If the size argument is a positive number, it is interpreted as a size in points. If size is a negative number, its absolute value is interpreted as a size in pixels. If a font cannot be displayed at the specified size, a nearby size will be chosen. If size is unspecified or zero, a platform-dependent default size will be chosen.”

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Interesting :o Thank you for this information! Do you mind recommending or showing some example code to get around that? I tried a if condition to prevent it from getting lower than 1 (for the font size) but it didn't work as expected. Same for using min/max. – Nordine Lotfi Apr 11 '23 at 01:56
  • 1
    It's not the root cause. See my answer. – relent95 Apr 11 '23 at 04:09
  • 1
    @NordineLotfi `if int(fontsize) < 1: canvas.itemconfig(text, state='hidden')` should work, doesn't it? – Thingamabobs Apr 11 '23 at 04:10
  • It would work, but then I'd have to do this on each text element on the canvas. On the MRE on my post, I'm configuring the font size for every element instead of one at a time :/ @Thingamabobs – Nordine Lotfi Apr 11 '23 at 11:53
2

That's because the zoom factor for the coordinates of the canvas items and the zoom factor for the font size are different in your implementation. Keep in mind the XSCALE and YSCALE parameters of the scale command of the canvas accumulate for repeated calls. Because the original coordinates of the items are transformed with roundoff errors, I don't recommend using the scale() to implement zooming. (See the implementation of the polygon, for example.)

Anyway, if you don't mind the roundoff errors, do like this.

...
factor = 1.0
def do_zoom(event):
    global factor

    x = canvas.canvasx(event.x)
    y = canvas.canvasy(event.y)

    if (event.delta > 0):
        ratio = 1.1
    elif (event.delta < 0):
        ratio = 0.9
    factor *= ratio

    font.configure(size=int(fontsize*factor))
    canvas.scale("all", x, y, ratio, ratio)
...
relent95
  • 3,703
  • 1
  • 14
  • 17
  • There's a small error in `ratio *= 0.9`, with that fixed your answer works pretty well. – Derek Apr 11 '23 at 05:41
  • Right! Corrected a typo. – relent95 Apr 11 '23 at 05:56
  • This is good. Does that mean there isn't any way to handle the rounding error? (I now understand the second problem I described in my post is related to that, thanks to you mentioning it) – Nordine Lotfi Apr 11 '23 at 11:40
  • 1
    To avoid the roundoff errors, you need to define a model(document) containing information such as an item type, coordinates, etc. Then, recreate the canvas items when the zoom factor changes. (It's called rendering a view from the model.) – relent95 Apr 12 '23 at 03:56