4

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

Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
  • 1
    Actually, your code is pretty good and this works quite well on my system. I didn't experience any stuttering. – OneMadGypsy Sep 26 '20 at 00:56
  • 1
    No can reproduce: I experienced a very smooth resizing, without any sort of stutter on an obsolete system. `osx10.12.5 macbook air 2 cores i5 2011 with 4GB RAM, 2 browsers, one large pdf, system monitor, textedit opened, and two screens` using `python 3.8` via `Jupyter notebook` - your code has a bug: on a dual monitor, resizing over the border makes the `y's` expand in both directions. – Reblochon Masque Nov 08 '20 at 02:42
  • @ReblochonMasque As you stated it appears to be an *windows only* problem and is a [well known issue](https://stackoverflow.com/q/53000291/13629335). I will update this Question with these information. – Thingamabobs Oct 24 '21 at 09:07
  • @Thingamabobs In the TL;DR, the code uses Window's built-in resize. Therefore, you can remove the bindings. Also I can reproduce the stuttering on Windows 11. Btw I had that problem when I was on Windows 10, but I got used to the stuttering. – TheLizzard Sep 07 '22 at 15:21
  • @TheLizzard Yes, in the second code the sizebox is active. I didn't removed the bindings for comparison. I tried to get used to it, but every time I resized, I thought the code is garbage. What I currently do is to reduce the sizebox of the top so there is no resize option and no white stripe. In a different project I have changed the color of the white stripe in the color of the custom titlebar and this seems the closest one can get by trying a *floating window* under Windows. But I wanted to ask the community (once again) if someone finds or knows a better way. – Thingamabobs Sep 07 '22 at 16:04

3 Answers3

0

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

Zak
  • 27
  • 6
  • 1
    Please add a little more detail on just how, and specifically where, you add the `update()` function – David Collins Nov 01 '20 at 05:25
  • It dosent solves it for me and if I play a little bit around I get an `RecursionError: maximum recursion depth exceeded while calling a Python object` – Thingamabobs Nov 01 '20 at 06:19
0

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.

Thingamabobs
  • 7,274
  • 5
  • 21
  • 54
-1

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()
Zak
  • 27
  • 6
  • 1
    I still experience these issues. Try the anchor south/west and expand the window. Also by shaking the mouse in the event after a few seconds you will still get the error. – Thingamabobs Nov 04 '20 at 16:38
  • This is worse than what *OP* has, as it crashes. -1. – CristiFati Sep 11 '22 at 00:56