Tkinter – Floating Window – Resize

Question:

Inspired by this question, I would like to write my own resizing function for my root window.
But I just noticed that my code shows some performance issues. If you resize it quickly you can see that the window doesn’t finds its height prompt as I wish, it "stutters". (It’s more like a swinging)

Does someone know why this happens? My best guess is that tkinter event handler is too slow for it, or the math I did isn’t the quickest way.

I did try update_idletasks() on different locations and also several times. Another way that I have tried was to use the after method but it made it worse.

Here is an example code:

import tkinter as tk


class FloatingWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.overrideredirect(True)
        self.center()

        self.label = tk.Label(self, text="Grab the upper-right corner to resize")
        self.label.pack(side="top", fill="both", expand=True)

        self.grip2 = tk.Label(self,bg='blue')
        self.grip2.place(relx=1.0, rely=0, anchor="ne")
        self.grip2.bind("<B1-Motion>",self.OnMotion)

    def OnMotion(self, event):
        abs_x = self.winfo_pointerx() - self.winfo_rootx()
        abs_y = self.winfo_pointery() - self.winfo_rooty()
        if abs_x >0:
            x = self.winfo_rootx()
            y = self.winfo_rooty()+abs_y
            height = self.winfo_height()-abs_y
            if height >0:
                self.geometry("%dx%d+%d+%d" % (abs_x,height,
                                               x,y))
            
        
    def center(self):
        width = 300
        height = 300
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        x_coordinate = (screen_width/2) - (width/2)
        y_coordinate = (screen_height/2) - (height/2)

        self.geometry("%dx%d+%d+%d" % (width, height,
                                       x_coordinate, y_coordinate))

app=FloatingWindow()
app.mainloop()

full example

Update

It appears that the performance issue is Microsoft related and a well known issue which drives most MS-Developer crazy.

Update 2

Since this issue seems MS-Windows related, I tried to find a MS specific solution and did a lot of research. I’ve tried to intercept messages like wm_pain, wm_nccalcsize and many more.
Somewhere on the way I thought, there is already an sizebox so it makes sense to make use of it. But it appears another issue with this solution.

A thin white stripe on the top edge. I took my quite a while till I found the answer its just the sizebox itself. Unfortunately, I haven’t found a way to configure the sizebox via the win32 api or the Dwmapi.


TL;DR

The answer to this question is preferably a smooth resizing event with the blue and green Labels. But if you find a way to erase the thin white line and still have resizing ability, (just shrinking the window rect to the client rect does not work or you have just 1 pixel to resize) would be a solution too.

The updated code looks like this:

import tkinter as tk
import win32gui
import win32api
import win32con


class FloatingWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        #self.overrideredirect(True)
        self.hWnd = int(self.wm_frame(), 16)

        self.label = tk.Label(self, text="Grab one of the blue")
        self.label.pack(side="top", fill="both", expand=True)
        
        blues = {'se' : (1,1),'ne' : (1,0),'nw' : (0,0),'sw' : (0,1)}
        grens = {'e' : (1,0.5), 'n' : (0.5,0), 'w' : (0,0.5), 's' : (0.5,1)}

        for k,v in blues.items():
            ref = tk.Label(self, bg='blue')
            ref.place(relx=v[0],rely=v[1],anchor=k)
            ref.bind("<B1-Motion>", lambda e, mode=k:self.OnMotion(e,mode))

        for k,v in grens.items():
            ref = tk.Label(self, bg='green')
            ref.place(relx=v[0],rely=v[1],anchor=k)
            ref.bind("<B1-Motion>", lambda e, mode=k:self.OnMotion(e,mode))

        self.bind('<ButtonPress-1>', self.start_drag)
        self.bind('<ButtonRelease-1>', self.stop_drag)
        return        

    def stop_drag(self,event):
        self.start_abs_x = None
        self.start_abs_y = None
        self.start_width = None
        self.start_height= None
        self.start_x = None
        self.start_y = None
    def start_drag(self,event):
        self.update_idletasks()
        self.start_abs_x = self.winfo_pointerx() - self.winfo_rootx()
        self.start_abs_y = self.winfo_pointery() - self.winfo_rooty()
        self.start_width = self.winfo_width()
        self.start_height= self.winfo_height()
        self.start_x = self.winfo_x()
        self.start_y = self.winfo_y()
        
    def OnMotion(self, event, mode):
        self.update_idletasks()
        abs_x = self.winfo_pointerx() - self.winfo_rootx()
        abs_y = self.winfo_pointery() - self.winfo_rooty()
        width = self.winfo_width()
        height= self.winfo_height()
        x = self.winfo_x()
        y = self.winfo_y()
        x_motion = self.start_abs_x - abs_x
        y_motion = self.start_abs_y - abs_y

        self.calc_x = x;self.calc_y=y;self.calc_w=width;
        self.calc_h=self.start_height
        if 'e' in mode:
            self.calc_w = self.start_width-x_motion
        if 's' in mode:
            self.calc_h -= y_motion
        
        if 'n' in mode:
            self.calc_y = y-y_motion
            self.calc_h = height+y_motion
        if 'w' in mode:
            self.calc_w = width+x_motion
            self.calc_x = x-x_motion

        self.geometry("%dx%d+%d+%d" % (self.calc_w,self.calc_h,
                                       self.calc_x,self.calc_y))

    def center(self):
        width = 300
        height = 300
        screen_width = self.winfo_screenwidth()
        screen_height = self.winfo_screenheight()
        x_coordinate = (screen_width/2) - (width/2)
        y_coordinate = (screen_height/2) - (height/2)

        self.geometry("%dx%d+%d+%d" % (width, height,
                                       x_coordinate, y_coordinate))

app=FloatingWindow()
app.update_idletasks()
hwnd = win32gui.GetParent(app.hWnd)
style= win32api.GetWindowLong(hwnd, win32con.GWL_STYLE)
style&= ~win32con.WS_CAPTION
#style&= ~win32con.WS_SIZEBOX
valid= win32api.SetWindowLong(hwnd, win32con.GWL_STYLE, style)

app.mainloop()

System Information:

Windows 10 Home; x64-base,Intel(R) Core(TM) i3-2120 @ 3.30GHz, 3300
MHz, 2Cores

with

Python 3.7.2 and tkinter 8.6

Asked By: Thingamabobs

||

Answers:

I was able to solve the problem by adding the update() function in the beginning of your OnMotion method

Answered By: Zak

I have added quickly some widgets, and some events in order that everyone can notice the performance issues, if the commented line (in the beginning of the OnMotion method) is deactivated self.update(). I have played with the code without getting any error. Hope this solve your issue.

import tkinter as tk

class FloatingWindow(tk.Tk):
    def __init__(self):
        super().__init__()
        self.overrideredirect(True)
        self.geometry("800x400+300+100")
        self.minsize(200, 200)
        self.config(bg="green")
        self.grid_columnconfigure(0, weight=3)
        self.grid_rowconfigure(1, weight=3)

        self.menu()
        self.textbox()        

        self.grip_se = tk.Label(self,bg='blue')
        self.grip_se.place(relx=1.0, rely=1.0, anchor="se")
        self.grip_se.bind("<B1-Motion>",lambda e, mode='se':self.OnMotion(e,mode))

        self.grip_e = tk.Label(self,bg='green')
        self.grip_e.place(relx=1.0, rely=0.5, anchor="e")
        self.grip_e.bind("<B1-Motion>",lambda e, mode='e':self.OnMotion(e,mode))
        
        self.grip_ne = tk.Label(self,bg='blue')
        self.grip_ne.place(relx=1.0, rely=0, anchor="ne")
        self.grip_ne.bind("<B1-Motion>",lambda e, mode='ne':self.OnMotion(e,mode))

        self.grip_n = tk.Label(self,bg='green')
        self.grip_n.place(relx=0.5, rely=0, anchor="n")
        self.grip_n.bind("<B1-Motion>",lambda e, mode='n':self.OnMotion(e,mode))

        self.grip_nw = tk.Label(self,bg='blue')
        self.grip_nw.place(relx=0, rely=0, anchor="nw")
        self.grip_nw.bind("<B1-Motion>",lambda e, mode='nw':self.OnMotion(e,mode))

        self.grip_w = tk.Label(self,bg='green')
        self.grip_w.place(relx=0, rely=0.5, anchor="w")
        self.grip_w.bind("<B1-Motion>",lambda e, mode='w':self.OnMotion(e,mode))

        self.grip_sw = tk.Label(self,bg='blue')
        self.grip_sw.place(relx=0, rely=1, anchor="sw")
        self.grip_sw.bind("<B1-Motion>",lambda e, mode='sw':self.OnMotion(e,mode))

        self.grip_s = tk.Label(self,bg='green')
        self.grip_s.place(relx=0.5, rely=1, anchor="s")
        self.grip_s.bind("<B1-Motion>",lambda e, mode='s':self.OnMotion(e,mode))

    def menu(self):
        self.frame = tk.Frame(self, height=25, bg='black')
        self.frame.grid(row=0, column=0, sticky="new")
        color = ['#FEF3B3','#FFF9DC', "#341C09"]

        for i in range(3):
            self.button = tk.Button(self.frame, text="Can you see", font=('calibri',12), bg=color[i-1], fg="red", relief="flat", bd=0)
            self.button.pack(side="left", fill="both", padx=3)
            self.lbl_space  = tk.Label(self.frame ,text="",bd=0,bg="black")
            self.lbl_space.pack(side="left", padx=5)

            self.button = tk.Button(self.frame, text="Can you see", font=('calibri',12), bg=color[i-1], fg="red", relief="flat", bd=0)
            self.button.pack(side="right", fill="both", padx=3)

            
    def textbox(self):
        self.frame2 = tk.Frame(self, bg='white')
        self.frame2.grid(row=1, column=0, sticky="wens")
        self.text_editor = tk.Text(self.frame2, wrap='word', font='calibri 12',undo = True, relief=tk.FLAT,bg="white")
        self.yscrollbar = tk.Scrollbar(self.frame2, command=self.text_editor.yview)
        self.yscrollbar.grid(row=0, column=1, sticky="ns")#ns

        self.text_editor.config(yscrollcommand=self.yscrollbar.set)
        self.text_editor.grid(row=0, column=0, sticky="wens", padx=3)

        self.frame2.grid_columnconfigure(0, weight=3)
        self.frame2.grid_rowconfigure(0, weight=3)
        self.text_editor.insert("1.0", 'Bed sincerity yet therefore forfeited his certainty neglected questions. Pursuit chamber as elderly amongst on. Distant however warrant farther to of. My justice wishing prudent waiting in be. Comparison age not pianoforte increasing delightful now. Insipidity sufficient dispatched any reasonably led ask. Announcing if attachment resolution sentiments admiration me on diminution. ')

        # insert a widget inside the text box
        options = ["choice 1","choice 2"]
        clicked = tk.StringVar()
        clicked.set(options[0])
        self.drop = tk.OptionMenu(self.text_editor, clicked, *options)
        self.text_editor.window_create("1.0", window=self.drop)
        self.drop.config(bg="#474747", relief='flat', font=('calibri',11, 'bold'))

    def OnMotion(self, event, mode):
        self.update() # <==== if you deactivate this line you can see the performance issues
        
        abs_x = self.winfo_pointerx() - self.winfo_rootx()
        abs_y = self.winfo_pointery() - self.winfo_rooty()
        width = self.winfo_width()
        height= self.winfo_height()
        x = self.winfo_rootx()
        y = self.winfo_rooty()
        
        if mode == 'se' and abs_x >0 and abs_y >0:
                self.geometry("%sx%s" % (abs_x,abs_y)
                              )
                
        if mode == 'e':
            self.geometry("%sx%s" % (abs_x,height)
                          )
        if mode == 'ne' and abs_x >0:
                y = y+abs_y
                height = height-abs_y
                if height >0:
                    self.geometry("%dx%d+%d+%d" % (abs_x,height,
                                                   x,y))
        if mode == 'n':
            height=height-abs_y
            y = y+abs_y
            if height >0 and width >0:
                self.geometry("%dx%d+%d+%d" % (width,height,
                                               x,y))
            
        if mode == 'nw':
            width = width-abs_x
            height=height-abs_y
            x = x+abs_x
            y = y+abs_y
            if height >0 and width >0:
                self.geometry("%dx%d+%d+%d" % (width,height,
                                               x,y))
        if mode == 'w':
            width = width-abs_x
            x = x+abs_x
            if height >0 and width >0:
                self.geometry("%dx%d+%d+%d" % (width,height,
                                               x,y))
        if mode == 'sw':
            width = width-abs_x
            height=height-(height-abs_y)
            x = x+abs_x
            if height >0 and width >0:
                self.geometry("%dx%d+%d+%d" % (width,height,
                                               x,y))
        if mode == 's':
            height=height-(height-abs_y)
            if height >0 and width >0:
                self.geometry("%dx%d+%d+%d" % (width,height,
                                               x,y))
            


app=FloatingWindow()
app.mainloop()
Answered By: Zak

I just noticed that Windows itself hadn’t figured it out yet. If you take an ordinary directory / folder and resize it you will see the same flickering in the client area as in my example above. The only difference seems to be that they haven’t an issue with erased background. So for Windows 10 and 11 the case seems closed, for now.

Answered By: Thingamabobs
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.