0

I'm working on an interactive simulator for physics. I want each physical object and the GUI to have their own classes for drawing, and I want to be able to resize the window. The canvas is currently contained in the GUI. How do I move all objects on window resize? All of them are tied to the window center.

My current setup is a frame that holds

  • a canvas
  • a frame that holds
    • two frames that each hold a couple sliders.

I've left in only one slider to show the general idea without too much clutter. Likewise, I only left in one object.

I used the solution offered here, slightly adjusted by me to move objects instead of rescaling them. How to get tkinter canvas to dynamically resize to window width?

I printed the supposed offsets (differences between new and old size, split in half and rounded), but they are constantly negative, regardless of whether I'm expanding or shrinking the window in either direction. Using curr_w = self.winfo_width(); makes the differences 0.

from tkinter import *
import time
import math

def splitterCoords(canv_w, canv_h):
    splitterX = round(canv_w/2);
    splitterY = round(canv_h/2);
    splitterLen = round(round(canv_h/5)*0.5*math.sqrt(2));
    return [splitterX + splitterLen, splitterY - splitterLen, splitterX - splitterLen, splitterY + splitterLen];

class ResizingCanvas(Canvas):
    def __init__(self,parent,**kwargs):
        Canvas.__init__(self,parent,**kwargs)
        self.bind("<Configure>", self.on_resize)
    def on_resize(self,event):
        curr_w = self.winfo_reqwidth();
        curr_h = self.winfo_reqheight();
        new_w = event.width;
        new_h = event.height;
        self.config(width = new_w, height = new_h)
        self.move("item",round((new_w-curr_w)/2),round((new_h-curr_h)/2))   

class GUI:
    def __init__(self,master):
        frame = Frame(master)
        frame.pack(fill = BOTH, expand = 1)
        bg = ResizingCanvas(frame, width = 1200, height = 600, background = "#F0F0F0", borderwidth = 3, relief = SUNKEN)
        bg.pack(fill = BOTH, expand = 1)
        sliders = Frame(frame)
        sliders.pack(fill = X, expand = 1)
        distSliders = Frame(sliders)
        distSliders.pack(fill = X, expand = 1, side = LEFT)
        distMirror1Obj = Scale(distSliders, orient=HORIZONTAL)
        distMirror1Obj.pack(fill = X, expand = 1)

class Splitter:
    def __init__(self, canvas, color):
        self.canvas = canvas
        canv_w = canvas.winfo_width()
        canv_h = canvas.winfo_height()
        self.id = canvas.create_line(splitterCoords(canv_w,canv_h), fill = color, tags = "item")
    def draw(self):
        pass        

root = Tk()
gui_r = GUI(root)
root.update()
splitter = Splitter(root.children['!frame'].children['!resizingcanvas'], "black")

while 1:
    root.update_idletasks()
    root.update()
    splitter.draw()
    time.sleep(0.01)

What currently happens on running the .py script: the window appears, then smoothly expands until the canvas becomes 1920x1080 (my screen resolution). The slider (or sliders in the full code) does not appear. If I then shrink the window in any way, the created item is moved strictly up and left.

I suspect if I stop the window from resizing, I'd be able to store the center value and update it after moving the object, but I don't know how to do that.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
bqback
  • 55
  • 9
  • your main problem is `while 1` - if you use standard `root.mainloop()` then you will see slider. If you want to smoothly resize window then better use `root.after(time_ms, function_name)` to repeate function which will not block `root.mainloop()` – furas Jul 12 '19 at 22:07
  • second problem is `self.config(width = new_w, height = new_h)` which uses size of `canvas+border` so finally canvas fill all window/screen and there is no place for Frame with slider. Frame is somewhrere below botton border of your screen. – furas Jul 12 '19 at 22:10
  • Are you aware that you can use the string `"all"` when you call the various canvas methods such as `move` and `itemconfigure` to affect all canvas items at once? – Bryan Oakley Jul 12 '19 at 23:43
  • It's unclear if you want the smooth expansion to fill the screen to happen. What @furas said about using `mainloop()` is true, _everything_ that happens in a tkinter application must occur while that is running — so the ``while loop at the end is a no-no. I suggest you simplify your question (and related code) to narrow it down to the primary thing you want to know how to do. – martineau Jul 12 '19 at 23:46
  • @furas I don't want the window to expand on launch at all. I want it to stay at specified width and height, and to react to expansion/shrinking. – bqback Jul 13 '19 at 14:38
  • @furas Will I still be able to use animation? I've looked up a few tutorials on animation with TkInter, and some used this instead of `.mainloop()` – bqback Jul 13 '19 at 14:40
  • to make animation you can use `root.after(time_ms, function)` and it will not block mainloop. In `function` you will move elements and run again `root.after(time_ms, function)` – furas Jul 13 '19 at 14:42
  • here you can see few animations with `after()`: [python-examples/tkinter/canvas](https://github.com/furas/python-examples/tree/master/tkinter/__canvas__). See bouncing-ball, move-around-canvas, solar-system. – furas Jul 13 '19 at 14:47
  • @furas Sorry for not responding for so long. I forgot to mention that the sliders appeared even with while 1, so the expansion I added makes the sliders disappear. I'll just go with a non-resizable window for now. – bqback Jul 16 '19 at 18:21

1 Answers1

0

To get current size of canvas you need

curr_w = int( self['width'] )
curr_h = int( self['height'] )

or

curr_w = int( self.cget('width') )
curr_h = int( self.cget('height') )

instead of self.winfo_reqwidth(), self.winfo_reqheight()

It seems that event gives canvas's size with border or relief so you have to substract some value. On my computer it is 8 - I test different values till I get the same values for curr_w/curr_h and new_w,new_h at start.

new_w = event.width  - 8  # border size
new_h = event.height - 8  # border size 

This gives me line always in center.

diff_x = new_w - curr_w
diff_y = new_h - curr_h

self.move("item", diff_x/2, diff_y/2) 

In first version I rounded values to integers but then object was not ideally in center after few resizes. But Canvas can use float values and then object is always in the center.

It still has some problem with bottom Frame which sometimes hides below window border.

Code:

from tkinter import *
import time
import math

def splitterCoords(canv_w, canv_h):
    x = canv_w/2
    y = canv_h/2
    length = (canv_h/5)*0.5*math.sqrt(2)
    return (x + length, y-length, x-length, y+length)

class ResizingCanvas(Canvas):

    def __init__(self,parent,**kwargs):
        Canvas.__init__(self,parent,**kwargs)
        self.bind("<Configure>", self.on_resize)

    def on_resize(self, event):
        curr_w = int(self['width'])  # self.winfo_reqwidth()
        curr_h = int(self['height']) # self.winfo_reqheight()
        new_w = event.width  - 8  # border size
        new_h = event.height - 8  # border size 

        self.config(width=new_w, height=new_h)

        diff_x = new_w - curr_w
        diff_y  = new_h - curr_h
        self.move("item", diff_x/2, diff_y/2)   

        #------------------
        win_w = self.winfo_reqwidth()
        win_h = self.winfo_reqheight()
        print('win:', win_w, win_h, '| old:', curr_w, curr_h, "| new:", new_w, new_h)

class GUI:
    def __init__(self,master):
        frame = Frame(master)
        frame.pack(fill = BOTH, expand = 1)
        bg = ResizingCanvas(frame, width = 1200, height = 600, background = "#F0F0F0", borderwidth = 3, relief = SUNKEN)
        bg.pack(fill = BOTH, expand = 1)
        sliders = Frame(frame)
        sliders.pack(fill = X, expand = 1)
        distSliders = Frame(sliders)
        distSliders.pack(fill = X, expand = 1, side = LEFT)
        distMirror1Obj = Scale(distSliders, orient=HORIZONTAL)
        distMirror1Obj.pack(fill = X, expand = 1)

class Splitter:
    def __init__(self, canvas, color):
        self.canvas = canvas
        canv_w = canvas.winfo_width()
        canv_h = canvas.winfo_height()
        self.id = canvas.create_line(splitterCoords(canv_w,canv_h), fill = color, tags = "item")
    def draw(self):
        pass        

root = Tk()
gui_r = GUI(root)
root.update()
splitter = Splitter(root.children['!frame'].children['!resizingcanvas'], "black")
root.mainloop()

I use root.mainloop() and I can see all widgets.

furas
  • 134,197
  • 12
  • 106
  • 148
  • 1
    _"To get current size of canvas you need `int( self['width'])`"_ - that is a false statement. The `width` attribute returns the value of the `width` configuration option. It does not necessarily return the current width. `self.winfo_width()` will always return the current width of the window. – Bryan Oakley Jul 13 '19 at 00:50