3

So, I was recently checking out VsCode, and I noticed an interesting feature. Although there was a taskbar icon, there was no titlebar; instead, VsCode implements its own. I looked at some other programs from Microsoft, and they do the same thing. I think this is a very cool feature.

I make a lot of productivity apps with Tkinter*, so I looked at how to do this in my apps. Unfortunately, the standard way to get rid of the titlebar in Tkinter is to disable the Window Manager (using overridedirect(1)). This also gets rid of the taskbar icon, which I want to keep.

In other words, what I am trying to get is titlebar while still keeping this: taskbar.

* For reference I am using Python 3.8 and TkInter 8.6.

j_4321
  • 15,431
  • 3
  • 34
  • 61

3 Answers3

3

You can create your own buttons titlebar using frames. Here take a look at this. I also worked on a tkinter based app and created this along with using what @Hruthik Reddy has given.

I make a lot of productivity apps with Tkinter*, so I looked at how to do this in my apps.

Assuming that you may have used classes at some point in those apps, I have created this inherited Tk subclass, and explained in comments:

import tkinter as tk
import tkinter.ttk as ttk
from ctypes import windll


class TestApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        # set overrideredirect to True to remove the windows default decorators
        self.overrideredirect(True)

        self.geometry('700x500+10+10')  # you may or may not want to initialize the geometry of the window
        self.minsize(193, 109)

        # (x, y) coordinates from top left corner of the window
        self.x = None
        self.y = None

        # Create a frame that will contain the title label of the window
        self.frame = tk.Frame(self, bg='gray38')
        self.frame.pack(side=tk.TOP, fill=tk.X)

        # Label `name` for the window
        # Since buttons are on the right side and the name of the window is on the left side, the label will be packed towards LEFT side
        self.name = tk.Label(self.frame, text='Simple Text Box', font='Consolas 11',
                             bg=self.frame.cget('background'), fg='white')
        self.name.pack(side=tk.LEFT, fill=tk.X, anchor=tk.CENTER)

        # Pack the close button to the right-most side
        self.close = tk.Button(self.frame, text='✕', bd=0, width=3, font='Consolas 13',
                               command=self.destroy, bg=self.frame.cget('background'))
        self.close.pack(side=tk.RIGHT)

        # Pack the maximize button second from the right
        # The unicode string as the value of the keyword `text` below, is taken from the internet, it contains the maximize icon as unicode character
        self.maximize = tk.Button(self.frame, text=u"\U0001F5D6", bd=0, width=3, font='Consolas',
                                  command=self.maximize_win, bg=self.frame.cget('background'))
        self.maximize.pack(side=tk.RIGHT)

        # Pack the minimize button third from the right
        self.minimize = tk.Button(self.frame, text='—', bd=0, width=3, font='Consolas 13',
                                  command=self.minimize_win, bg=self.frame.cget('background'))
        self.minimize.pack(side=tk.RIGHT)

        # -------------------
        # NOW YOU CAN PUT WHATEVER WIDGETS YOU WANT AFTER THIS BUT FOR THIS EXAMPLE I
        # HAVE TAKEN A TEXTBOX WITH HORIZONTAL AND VERTICAL SCROLLBARS AND A SIZEGRIP
        # -------------------

        # The frame below contains the vertical scrollbar and the sizegrip (sizegrip helps in resizing the window
        self.scroll_frame = tk.Frame(self)
        v_scroll = tk.Scrollbar(self.scroll_frame, orient=tk.VERTICAL)
        h_scroll = tk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.grip = ttk.Sizegrip(self.scroll_frame)

        # I am directly putting the textbox in the window, you may add frames and other stuff
        self.text = tk.Text(self, wrap=tk.NONE, yscrollcommand=v_scroll.set, xscrollcommand=h_scroll.set,
                            font='Consolas 14', width=1, height=1)

        # set the scrollbar for y and x views of the textbox respectively
        v_scroll.config(command=self.text.yview)
        h_scroll.config(command=self.text.xview)

        # Packing scrollbar frame, the scrollbars and the grip according to the arrangement I want
        self.scroll_frame.pack(side=tk.RIGHT, fill=tk.Y)
        v_scroll.pack(side=tk.TOP, fill=tk.Y, expand=tk.Y)
        self.grip.pack(side=tk.BOTTOM)
        self.text.pack(side=tk.TOP, expand=tk.TRUE, fill=tk.BOTH)
        h_scroll.pack(side=tk.BOTTOM, fill=tk.X)
        
        self.grip.bind("<B1-Motion>", self.onmotion)
        # Bind the motion of mouse after mouse click to the onmotion function for window resizing

        self.call('encoding', 'system', 'utf-8')

        # Binding `<Enter>` and `<Leave>` mouse event to their respective functions
        # `<Enter>` event is called when the mouse pointer enters any widget
        # `<Leave>` event is called when the mouse pointer leaves any widget
        # Here when the mouse pointer enters or leaves the buttons their color will change
        self.close.bind('<Enter>', lambda _: self.close.config(bg='red'))
        self.close.bind('<Leave>', lambda _: self.close.config(bg=self.frame.cget('background')))
        self.minimize.bind('<Enter>', lambda _: self.minimize.config(bg='gray58'))
        self.minimize.bind('<Leave>', lambda _: self.minimize.config(bg=self.frame.cget('background')))
        self.maximize.bind('<Enter>', lambda _: self.maximize.config(bg='gray58'))
        self.maximize.bind('<Leave>', lambda _: self.maximize.config(bg=self.frame.cget('background')))

        # Now you may want to move your window (obviously), so the respective events are bound to the functions
        self.frame.bind("<ButtonPress-1>", self.start_move)
        self.frame.bind("<ButtonRelease-1>", self.stop_move)
        self.frame.bind("<B1-Motion>", self.do_move)
        self.frame.bind('<Double-1>', self.maximize_win)
        self.name.bind("<ButtonPress-1>", self.start_move)
        self.name.bind("<ButtonRelease-1>", self.stop_move)
        self.name.bind("<B1-Motion>", self.do_move)
        self.name.bind('<Double-1>', self.maximize_win)

    def start_move(self, event):
        """ change the (x, y) coordinate on mousebutton press and hold motion """
        self.x = event.x
        self.y = event.y

    def stop_move(self, event):
        """ when mouse button is released set the (x, y) coordinates to None """
        self.x = None
        self.y = None

    def do_move(self, event):
        """ function to move the window """
        self.wm_state('normal')  # if window is maximized, set it to normal (or resizable)
        self.maximize.config(text=u"\U0001F5D6")  # set the maximize button text to the square character of maximizing window
        deltax = event.x - self.x
        deltay = event.y - self.y
        x = self.winfo_x() + deltax
        y = self.winfo_y() + deltay
        self.geometry(f"+{x}+{y}")

    def onmotion(self, event):
        """ function to change window size """
        self.wm_state('normal')
        self.maximize.config(text=u"\U0001F5D6")
        x1 = self.winfo_pointerx()
        y1 = self.winfo_pointery()
        x0 = self.winfo_rootx()
        y0 = self.winfo_rooty()
        self.geometry("%sx%s" % ((x1-x0), (y1-y0)))
        return

    def minimize_win(self, event=None):
        """ function to iconify or minimize window as an icon """
        self.overrideredirect(False)
        self.wm_iconify()
        self.bind('<FocusIn>', self.on_deiconify)

    def maximize_win(self, event=None):
        """ function to maximize window or make it normal (exit maximize) """
        if self.maximize.cget('text') == u"\U0001F5D7":
            self.wm_state('normal')
            self.maximize.config(text=u"\U0001F5D6")
            return
        self.wm_state('zoomed')
        self.maximize.config(text=u"\U0001F5D7")

    def on_deiconify(self, event):
        """ function to deiconify or window """
        self.overrideredirect(True)
        set_appwindow(root=self)


def set_appwindow(root):
    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())


if __name__ == '__main__':
    GWL_EXSTYLE = -20
    WS_EX_APPWINDOW = 0x00040000
    WS_EX_TOOLWINDOW = 0x00000080

    app = TestApp()
    # print(app.tk.call('tk', 'windowingsystem'))
    # # Here root.tk.call('tk', 'windowingsystem') calls tk windowingsystem in Tcl, and that returns 'win32',
    # # 'aqua' or 'x11' as documented in tk
    app.after(10, lambda: set_appwindow(root=app))
    app.text.insert(1.0, 'Drag the window using the title or the empty area to the right of the\ntitle.'
                         ' Try maximizing / minimizing.\n\n-- YOU MAY HAVE A PROBLEM WITH RESIZING --\n'
                         '-- ALSO IF YOU REMOVE `height` AND `width` KEYWORDS FROM THE TEXTBOX DECLARATION'
                         ' AND FONT SIZE IS TOO BIG THE SCROLLBAR MAY DISAPPEAR --\nSO KEEP THOSE KEYWORDS THERE!')
    app.mainloop()

When window is maximized you may not be able to see the taskbar. But you can still resize it using sizegrip. I don't yet know how to make window resizing possible from window borders with overrideredirect but Sizegrip works just fine.

Now regarding the set_appwindow function, this is what MSDN says:

The Shell creates a button on the taskbar whenever an application creates a window that isn't owned. To ensure that the window button is placed on the taskbar, create an unowned window with the WS_EX_APPWINDOW extended style. To prevent the window button from being placed on the taskbar, create the unowned window with the WS_EX_TOOLWINDOW extended style. As an alternative, you can create a hidden window and make this hidden window the owner of your visible window.

Complete reference here

This may seem like a very long answer but I hope it covers all what you need and helps you.

prerakl123
  • 121
  • 1
  • 11
1

Check out the following code it worked for me :-

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):
    hwnd = windll.user32.GetParent(root.winfo_id())
    style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
    style = style & ~WS_EX_TOOLWINDOW
    style = style | WS_EX_APPWINDOW
    res = windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, style)
    root.wm_withdraw()
    root.after(10, lambda: root.wm_deiconify())

def main():
    root = tk.Tk()
    root.wm_title("AppWindow Test")
    button = ttk.Button(root, text='Exit', command=lambda: root.destroy())
    button.place(x=10,y=10)
    root.overrideredirect(True)
    root.after(10, lambda: set_appwindow(root))
    root.mainloop()

if __name__ == '__main__':
    main()
1

In the function on_deiconify() we need to unbind the previous event <FocusIn> to avoid from blitting of the window we write like this

def on_deiconify(self, event):
        """ function to deiconify or window """
        self.overrideredirect(True)
        set_appwindow(root=self)
        self.unbind("<FocusIn>")
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103