【问题标题】:Trouble making a custom title bar in Tkinter在 Tkinter 中制作自定义标题栏时遇到问题
【发布时间】:2018-09-12 06:48:50
【问题描述】:

我正在尝试自定义 Tkinter 中的标题栏。

现在它看起来像这样:

我想把它改成这样:

我设法做到了:

使用以下代码:

def move_window(event):
    app.geometry('+{0}+{1}'.format(event.x_root, event.y_root))

if __name__ == "__main__":
    app = SampleApp()
    app.overrideredirect(True)
    screen_width = app.winfo_screenwidth()
    screen_height = app.winfo_screenheight()
    x_coordinate = (screen_width/2) - (1050/2)
    y_coordinate = (screen_height/2) - (620/2)
    app.geometry("{}x{}+{}+{}".format(1050, 650, int(x_coordinate), int(y_coordinate)))
    title_bar = Frame(app, bg='#090909', relief='raised', bd=0, height=20, width=1050)
    close_button = Button(title_bar, text='X', command=app.destroy, width=5, bg="#090909", fg="#888", bd=0)
    title_bar.place(x=0, y=0)
    close_button.place(rely=0, relx=1, x=0, y=0, anchor=NE)
    title_bar.bind('<B1-Motion>', move_window)
    app.mainloop()

我的代码结构是基于这个Switch between two frames in tkinter

我希望能够添加一个最小化按钮。我尝试使用app.iconify() 作为命令创建一个类似于关闭按钮的按钮,但这不适用于overrideredirect(True)

如果出现在任务栏上也不错。

此外,移动还有一个大问题,即每当您尝试移动窗口时,它都会移动窗口,使其左上角位于光标所在的位置。这非常烦人,而且不是 Windows 的典型行为。

如果有人知道如何解决这些问题,将不胜感激。

编辑:我现在设法制作了一个自定义标题栏,我可以用它来无缝拖动窗口。我还让应用程序显示在任务栏中,并在标题栏中添加了一个最小化按钮。但是,我无法让最小化按钮真正起作用。

【问题讨论】:

    标签: python windows tkinter


    【解决方案1】:

    标题栏和所有其他窗口装饰(最大化、最小化和恢复按钮以及窗口的任何边缘)都由窗口管理器拥有和管理。 Tk 必须要求窗口管理器对它们做任何事情,这就是为什么设置应用程序标题是 wm_title() 调用。通过设置 overrideredirect 标志,您已请求窗口设法完全停止装饰您的窗口,并且您现在正在使用框架在窗口的客户区域中伪造标题栏。

    我的建议是停止与系统作斗争。这就是窗口管理器主题的用途,Windows 不会让你真正搞砸这些。如果你真的仍然想走这条路,你可以将窗框装饰创建为 Ttk 主题元素,因为它们是当前样式集合的一部分,并且可以使用 vsapi ttk 元素引擎创建。视觉样式 API 中的一些值:

    | Function    | Class   | ID               | Numerical ID |
    +-------------+---------+------------------+--------------+
    | Minimize    | WINDOW  | WP_MINBUTTON     | 15           |
    | Maximize    | WINDOW  | WP_MAXBUTTON     | 17           |
    | Close       | WINDOW  | WP_CLOSEBUTTON   | 18           |
    | Restore     | WINDOW  | WP_RESTOREBUTTON | 21           |
    

    我已经像这样在 python 中使用这些来获得一个关闭按钮元素,然后您可以将其包含在 ttk 小部件样式中。

    style = ttk.Style()
    # There seems to be some argument parsing bug in tkinter.ttk so cheat and eval
    # the raw Tcl code to add the vsapi element for a pin.
    root.eval('''ttk::style element create closebutton vsapi WINDOW 18 {
        {pressed !selected} 3
        {active !selected} 2
        {pressed selected} 6
        {active selected} 5
        {selected} 4
        {} 1
    }''')
    

    但是,我怀疑您可能仍想避免这些按钮的主题性质,因此您更可能只想使用 png 图像并使用画布而不是框架。然后,您可以使用标签绑定轻松获取伪标题栏按钮上的事件。

    使用 ttk 主题元素的示例

    #!/usr/bin/env python3
    """
    Q: Trouble making a custom title bar in Tkinter
    
    https://stackoverflow.com/q/49621671/291641
    """
    
    import tkinter as tk
    import tkinter.ttk as ttk
    from ctypes import windll
    
    GWL_EXSTYLE=-20
    WS_EX_APPWINDOW=0x00040000
    WS_EX_TOOLWINDOW=0x00000080
    
    def set_appwindow(root):
        """Change the window flags to allow an overrideredirect window to be
        shown on the taskbar.
        (See https://stackoverflow.com/a/30819099/291641)
        """
        hwnd = windll.user32.GetParent(root.winfo_id())
        style = windll.user32.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)
        style = style & ~WS_EX_TOOLWINDOW
        style = style | WS_EX_APPWINDOW
        res = windll.user32.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style)
        # re-assert the new window style
        root.wm_withdraw()
        root.after(10, lambda: root.wm_deiconify())
    
    def create_button_element(root, name, id):
        """Create some custom button elements from the Windows theme.
        Due to a parsing bug in the python wrapper, call Tk directly."""
        root.eval('''ttk::style element create {0} vsapi WINDOW {1} {{
            {{pressed !selected}} 3
            {{active !selected}} 2
            {{pressed selected}} 6
            {{active selected}} 5
            {{selected}} 4
            {{}} 1
        }} -syssize {{SM_CXVSCROLL SM_CYVSCROLL}}'''.format(name,id))
    
    class TitleFrame(ttk.Widget):
        """Frame based class that has button elements at one end to
        simulate a windowmanager provided title bar.
        The button click event is handled and generates virtual events
        if the click occurs over one of the button elements."""
        def __init__(self, master, **kw):
            self.point = None
            kw['style'] = 'Title.Frame'
            kw['class'] = 'TitleFrame'
            ttk.Widget.__init__(self, master, 'ttk::frame', kw)
        @staticmethod
        def register(root):
            """Register the custom window style for a titlebar frame.
            Must be called once at application startup."""
            style = ttk.Style()
            create_button_element(root, 'close', 18)
            create_button_element(root, 'minimize', 15)
            create_button_element(root, 'maximize', 17)
            create_button_element(root, 'restore', 21)
            style.layout('Title.Frame', [
                ('Title.Frame.border', {'sticky': 'nswe', 'children': [
                    ('Title.Frame.padding', {'sticky': 'nswe', 'children': [
                        ('Title.Frame.close', {'side': 'right', 'sticky': ''}),
                        ('Title.Frame.maximize', {'side': 'right', 'sticky': ''}),
                        ('Title.Frame.minimize', {'side': 'right', 'sticky': ''})
                    ]})
                ]})
            ])
            style.configure('Title.Frame', padding=(1,1,1,1), background='#090909')
            style.map('Title.Frame', **style.map('TEntry'))
            root.bind_class('TitleFrame', '<ButtonPress-1>', TitleFrame.on_press)
            root.bind_class('TitleFrame', '<B1-Motion>', TitleFrame.on_motion)
            root.bind_class('TitleFrame', '<ButtonRelease-1>', TitleFrame.on_release)
        @staticmethod
        def on_press(event):
            event.widget.point = (event.x_root,event.y_root)
            element = event.widget.identify(event.x,event.y)
            if element == 'close':
                event.widget.event_generate('<<TitleFrameClose>>')
            elif element == 'minimize':
                event.widget.event_generate('<<TitleFrameMinimize>>')
            elif element == 'restore':
                event.widget.event_generate('<<TitleFrameRestore>>')
        @staticmethod
        def on_motion(event):
            """Use the relative distance since the last motion or buttonpress event
            to move the application window (this widgets toplevel)"""
            if event.widget.point:
                app = event.widget.winfo_toplevel()
                dx = event.x_root - event.widget.point[0]
                dy = event.y_root - event.widget.point[1]
                x = app.winfo_rootx() + dx
                y = app.winfo_rooty() + dy
                app.wm_geometry('+{0}+{1}'.format(x,y))
                event.widget.point=(event.x_root,event.y_root)
        @staticmethod
        def on_release(event):
            event.widget.point = None
    
    
    class SampleApp(tk.Tk):
        """Example basic application class"""
        def __init__(self, *args, **kwargs):
            tk.Tk.__init__(self, *args, **kwargs)
            self.wm_geometry('320x240')
    
    def main():
        app = SampleApp()
        TitleFrame.register(app)
        app.overrideredirect(True)
        screen_width = app.winfo_screenwidth()
        screen_height = app.winfo_screenheight()
        x_coordinate = (screen_width/2) - (1050/2)
        y_coordinate = (screen_height/2) - (620/2)
        app.geometry("{}x{}+{}+{}".format(1050, 650, int(x_coordinate), int(y_coordinate)))
        title_bar = TitleFrame(app, height=20, width=1050)
        title_bar.place(x=0, y=0)
        app.bind('<<TitleFrameClose>>', lambda ev: app.destroy())
        app.bind('<<TitleFrameMinimize>>', lambda ev: app.wm_iconify())
        app.bind('<Key-Escape>', lambda ev: app.destroy())
        app.after(10, lambda: set_appwindow(app))
        app.mainloop()
    
    if __name__ == "__main__":
        main()
    

    您无法使最小化工作,因为这是对覆盖重定向窗口的拒绝操作。如果你需要支持的话,我认为一些对 windows 样式的干预可能允许创建一个没有标题栏的顶层窗口。我提供了一个示例,它让您的覆盖重定向窗口出现在任务栏中,但它不会响应来自该状态的输入,因为此状态的点不由窗口管理器管理。

    【讨论】:

    • 感谢您的回复。实际上,我已经设法制作了一个可以从中拖动的标题栏,并且我创建了最小化和关闭按钮。关闭按钮适用于.destroy,但我正在努力将命令绑定到最小化按钮,因为我找不到任何有用的东西。此外,当您在任务栏中单击该应用程序时,它不会最小化。任何想法如何解决这些问题?我想我应该编辑您的代码以添加 WINDOW 15,但我不太了解您的代码,也不知道如何将其绑定到我当前的最小化按钮。
    • 如果有影响的话,我也在使用 Tk。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-01
    • 2021-12-19
    相关资源
    最近更新 更多