2

I am trying to customise the title bar in Tkinter.

Right now it looks like this: Right now it looks like this

I would like to change it to look like this: would like to change it to look like this

I've managed to make this: managed to make this

With the following code:

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

My code structure is based on this Switch between two frames in tkinter

I'd like to be able to add a minimise button. I tried creating a button similar to the close button with app.iconify() as the command, but that won't work alongside overrideredirect(True).

It'd also be good if it showed up on the task bar.

Also, the movement has a big problem in that whenever you try and move the window it moves the window so that its top left corner is positioned where your cursor is. This is extremely annoying and isn't typical behaviour for Windows.

If anyone knows how to fix these problems it would be greatly appreciated.

EDIT: I have now managed to make a custom title bar which I can use to drag the window around seamlessly. I have also made the app show up in the taskbar, as well as adding a minimise button to the title bar. However, I have not been able to get the minimise button to actually work.

J P
  • 541
  • 1
  • 10
  • 20

1 Answers1

0

The title bar and all other window decorations (maximize, minimize and restore buttons and any edges to the window) are owned and managed by the window manager. Tk has to ask the window manager to do anything with them which is why setting the application title is a wm_title() call. By setting the overrideredirect flag you have requested the window manage to stop decorating your window entirely and you are now faking a titlebar in the client area of your window using a frame.

My advice would be to stop fighting the system. Thats what window manager themes are for and Windows doesn't let you mess with those really. If you really do still want to go this route you can create the windows frame decorations as Ttk theming elements as they are part of the current styling collection and can be created using the vsapi ttk element engine. Some values from the Visual Styles 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           |

I've used these in python like this to get a closebutton element you can then include in a ttk widget style.

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
}''')

However, I suspect you probably still want to avoid the themed nature of these buttons so you will more likely want to just use a png image and use a canvas instead of a frame. Then you can use a tag binding to pick up events on the pseudo-titlebar buttons easily.

Example using ttk theme elements

#!/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()

You can't get minimize to work as that is a denied operation for a overrideredirect window. I think some meddling with the windows styles might allow creating a toplevel window without a titlebar if you need to support that. I'v included an example that lets your overrideredirect window appear in the taskbar but it won't respond to input from that as the point of this state is to not be managed by the window manager.

patthoyts
  • 32,320
  • 3
  • 62
  • 93
  • Thanks for your reply. I have actually managed to make a title bar which I can drag from, and I have created minimise and close buttons. The close button works with .destroy, but I am struggling with binding a command to the minimise button as I can't find anything that works. Also, the app doesn't minimise when you click on it in the task bar. Any ideas how to fix these problems? I suppose I should edit your code to add WINDOW 15 but I don't really understand your code and wouldn't know how to bind it to my current minimise button. – J P Apr 03 '18 at 18:29
  • Also I'm using Tk if that makes a difference. – J P Apr 03 '18 at 18:35