Python 在Tkinter中创建自定义标题栏时遇到问题

Python 在Tkinter中创建自定义标题栏时遇到问题,python,windows,tkinter,Python,Windows,Tkinter,我试图在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 = ap

我试图在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()
def move_窗口(事件):
app.geometry('+{0}+{1}'。格式(event.x_root,event.y_root))
如果名称=“\uuuuu main\uuuuuuuu”:
app=SampleApp()
app.overrideredirect(真)
screen_width=app.winfo_screenwidth()
屏幕高度=app.winfo屏幕高度()
x_坐标=(屏幕宽度/2)-(1050/2)
y坐标=(屏幕高度/2)-(620/2)
app.geometry(“{}x{}+{}+{}.”格式(1050650,int(x_坐标),int(y_坐标)))
标题栏=框架(应用程序,背景='#090909',浮雕='凸起',bd=0,高度=20,宽度=1050)
关闭按钮=按钮(标题栏,text='X',command=app.destroy,width=5,bg=“#090909”,fg=“#888”,bd=0)
标题栏位置(x=0,y=0)
关闭按钮。放置(依赖=0,relx=1,x=0,y=0,锚定=NE)
标题栏绑定(“”,移动窗口)
app.mainloop()
我的代码结构基于此

我想能够添加一个最小化按钮。我尝试创建一个类似于关闭按钮的按钮,命令为
app.iconify()
,但与
overrideredirect(True)
一起无法使用

如果它出现在任务栏上也会很好

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

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


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

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

我的建议是停止与体制对抗。这就是Windows管理器主题的用途,Windows不会让您真正地弄乱这些主题。如果您真的还想这样做,可以将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中使用了这些元素来获得closebutton元素,然后可以将其包含在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蟒蛇3
"""
问:在Tkinter中创建自定义标题栏有困难
https://stackoverflow.com/q/49621671/291641
"""
将tkinter作为tk导入
将tkinter.ttk导入为ttk
从ctypes导入Windell
GWL_EXSTYLE=-20
WS_EX_APPWINDOW=0x00040000
WS_EX_TOOLWINDOW=0x00000080
def set_应用程序窗口(根):
“”“更改窗口标志以允许重写直接窗口。”
显示在任务栏上。
(见https://stackoverflow.com/a/30819099/291641)
"""
hwnd=windl.user32.GetParent(root.winfo_id())
style=windl.user32.GetWindowLongPtrW(hwnd,GWL_EXSTYLE)
style=style&~WS\u EX\u工具窗口
style=style | WS_EX_APPWINDOW
res=windell.user32.SetWindowLongPtrW(hwnd,GWL_EXSTYLE,style)
#重新断言新的窗口样式
root.wm_-draw()
root.after(10,lambda:root.wm_deiconify())
def创建按钮元素(根、名称、id):
“”“从Windows主题创建一些自定义按钮元素。”。
由于python包装中存在解析错误,请直接调用Tk。”“”
eval(''ttk::style元素创建{0}vsapi窗口{1}{{
{{按下!选中}}3
{{active!selected}}2
{{按下选定按钮}}6
{{active selected}}5
{{selected}}4
{{}} 1
}}-syssize{{SM_CXVSCROLL SM_CYVSCROLL}}}''。格式(名称、id))
类标题框(ttk.Widget):
“”“基于帧的类,其一端有按钮元素
模拟windowmanager提供的标题栏。
将处理按钮单击事件并生成虚拟事件
如果单击发生在其中一个按钮元素上。”“”
def _初始功率(自功率、主功率,**kw):
self.point=无
kw['style']='Title.Frame'
千瓦['class']='TitleFrame'
Widget.\uuuu初始化(self,master,'ttk::frame',kw)
@静力学方法
def寄存器(根):
“”“注册标题栏框架的自定义窗口样式。”。
必须在应用程序启动时调用一次。”“”
style=ttk.style()
创建按钮元素(根,“关闭”,18)
创建按钮元素(根,“最小化”,15)
创建按钮元素(根,“最大化”,17)
创建按钮元素(根,“还原”,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),backgro
#!/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()