Tkinter - 浮动窗口 - 调整大小

5
this问题的启发,我想为我的根窗口编写自己的调整大小函数。但我注意到我的代码存在一些性能问题。如果你快速调整大小,你会发现窗口不能像我希望的那样迅速找到它的高度,它会“卡顿”。(更像是摆动) 有人知道这是为什么吗?我最好的猜测是tkinter事件处理程序太慢了,或者我做的数学运算不是最快的方式。
我尝试在不同位置和多次使用update_idletasks()。我尝试的另一种方法是使用after方法,但效果更差。
以下是示例代码:
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()

完整示例

更新

看起来性能问题与微软有关,这是一个众所周知的问题,让大多数微软开发人员感到疯狂。

更新2

由于这个问题似乎与MS-Windows有关,我尝试找到一个MS特定的解决方案并进行了大量研究。我试图拦截消息,如wm_painwm_nccalcsize等等。在某个地方,我认为已经有一个大小框,因此利用它是有意义的。但是,这种解决方案还存在另一个问题。

顶部有一条细白线。我花了很长时间才找到答案,它只是sizebox本身。不幸的是,我还没有找到通过win32 apiDwmapi配置 sizebox的方法。


简而言之

这个问题的最好答案是带有蓝色和绿色标签的平滑调整大小事件。但如果您找到一种方法来消除细白线并仍然具有调整大小能力,仅缩小窗口矩形到客户端矩形不起作用或您只有1像素可以调整大小也是一种解决方案。

更新后的代码如下:

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()

系统信息:

Windows 10 Home;x64-基础,Intel(R) Core(TM) i3-2120 @ 3.30GHz,3300 MHz,2个核心

使用:

Python 3.7.2 和 tkinter 8.6


1
实际上,你的代码非常不错,在我的系统上运行得非常好。我没有遇到任何卡顿。 - OneMadGypsy
1
无法复制:我在一台过时的系统上体验到了非常流畅的调整大小,没有任何卡顿。osx10.12.5 MacBook Air 2核i5 2011年4GB RAM,2个浏览器,一个大型PDF,系统监视器,Textedit打开和两个屏幕使用Jupyter笔记本中的python 3.8 - 您的代码存在错误:在双监视器上,通过边界调整大小会使y朝两个方向扩展。 - Reblochon Masque
@ReblochonMasque,正如您所说,这似乎是一个仅限Windows的问题,并且是一个众所周知的问题。我将使用这些信息更新此问题。 - Thingamabobs
@Thingamabobs 在TL;DR中,这段代码使用了Windows内置的 resize。因此,您可以删除绑定。另外,我可以在Windows 11上重现卡顿问题。顺便说一下,当我使用Windows 10时,我也曾遇到过这个问题,但我已经习惯了这种卡顿。 - TheLizzard
@TheLizzard 是的,在第二段代码中,sizebox是激活的。我没有删除绑定以进行比较。我试图习惯它,但每次调整大小时,我都觉得代码很糟糕。我目前所做的是减小顶部的sizebox,这样就没有调整大小选项和白色条纹了。在另一个项目中,我将白色条纹的颜色更改为自定义标题栏的颜色,这似乎是在Windows下尝试浮动窗口时最接近的方法。但我想再次问问社区,是否有人发现或知道更好的方法。 - Thingamabobs
3个回答

0

我能够通过在您的 OnMotion 方法开头添加 update() 函数来解决问题。


1
请在添加update()函数的具体位置上再增加一些细节说明。 - David Collins
它对我没有解决之道,如果我稍微尝试一下,就会出现“RecursionError: maximum recursion depth exceeded while calling a Python object”错误。 - Thingamabobs

0
我刚刚注意到Windows本身还没有解决这个问题。如果你拿一个普通的目录/文件夹并调整它的大小,你会看到客户区域中与我上面示例中相同的闪烁。唯一的区别似乎是他们没有被擦除背景的问题。所以对于Windows 10和11来说,这个问题现在看来已经解决了。

-1

我快速添加了一些小部件和事件,以便每个人都可以注意到性能问题,如果在OnMotion方法开头的注释行(self.update())被停用。我已经尝试过这段代码而没有出现任何错误。希望这解决了您的问题。

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()

1
我仍然遇到这些问题。尝试使用锚点南/西并扩展窗口。此外,在事件中摇动鼠标几秒钟后,您仍将收到错误。 - Thingamabobs
这比OP拥有的更糟糕,因为它会崩溃。-1. - CristiFati

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接