Siddharth Dushantha
Siddharth Dushantha

Reputation: 1491

How can I change the title bar on a Tkinter window to have the "modern" look on macOS?

By default when we create a simple window on macOS using tkinter it looks like the window on the left. I'm trying to create a window that looks like the one on the right.enter image description here

I've tried Googling this but I can't seem to find a solution. I assume it is because I am not using the correct key terms as "modern title bar" is most likely wrong.

If doing this is not possible using tkinter, are there any recommendations for other GUI libraries that supports this "modern" look?

Upvotes: 3

Views: 823

Answers (1)

Charlie
Charlie

Reputation: 27

Here is some code to create a window similar to the window you want. win is where your gui will be. It is a Canvas

from tkinter import *
## if installed: from tkmacosx import *
from tkinter.ttk import Sizegrip
import time

Title="finish mac titlebar"

root=Tk()
root.overrideredirect(1)
## if needed for your mac: root.overrideredirect(0)
root.geometry("300x150")
root.update_idletasks()
root.title(" "+Title+" ")
root.resizable(True, True)
root.maxsize(root.winfo_screenwidth(), root.winfo_screenheight())
root.attributes("-transparent", True)

dummy=Tk()
dummy.geometry("1x1")
dummy.resizable(0,0)
dummy.attributes("-alpha", False)
dummy.title(Title)

def unminimize_me(e):
    root.deiconify()

dummy.bind("<FocusIn>", unminimize_me)

gray="#221F1E"
black="#111010"

root.config(bg=black)

title=Frame(root, bg=gray)
title.pack(fill=X, side=TOP)

win=Canvas(root, width=1, height=1, bg=black, highlightthickness=0)
win.pack(side=TOP, expand=True, fill=BOTH)

bottom=Frame(root, bg=black)
bottom.pack(side=BOTTOM, fill=X)

bottomright=Canvas(bottom,  width=8, height=8, highlightthickness=0, bg="systemTransparent")
bottomright.pack(side=RIGHT)
bottomright.create_oval(-9,-9, 7,7, fill=black, outline=black)

bottomleft=Canvas(bottom,  width=8, height=8, highlightthickness=0, bg="systemTransparent")
bottomleft.pack(side=LEFT)
bottomleft.create_oval(1,-9, 17,7, fill=black, outline=black)

left=Canvas(title, width=8, height=8, highlightthickness=0, bg="systemTransparent")
left.place(x=0, y=0)
left.create_oval(0,0, 16,16, fill=gray, outline=gray)
right=Canvas(title, width=8, height=8, highlightthickness=0, bg="systemTransparent")
right.pack(side=RIGHT, anchor=NE)
right.create_oval(7,1, -9,17, fill=gray, outline=gray)

def get_pos(e):
    xwin=root.winfo_x()
    ywin=root.winfo_y()
    startx=e.x_root
    starty=e.y_root

    ywin=ywin-starty
    xwin=xwin-startx

    def move_window(e):
        if e.x_root+xwin > -1 and e.y_root+ywin >-1:
            root.geometry("+{0}+{1}".format(e.x_root+xwin, e.y_root+ywin))
    startx=e.x_root
    starty=e.y_root

    title.bind("<Button1-Motion>", move_window)
title.bind("<Button-1>", get_pos)

def onx(e):
    close.create_text(8,8, text="x", fill="dark red")

def offx(e):
    close.delete(ALL)
    close.create_oval(1,1, 15,15, outline="dark red", fill="red")

def x(e):
    dummy.unbind("<FocusIn>")
    root.destroy()
    dummy.destroy()

close=Canvas(title, bg=gray, height=16, width=16, highlightthickness=0)
close.create_oval(1,1, 15,15, outline="dark red", fill="red")
close.pack(side=LEFT, pady=3, padx=2)
close.bind("<Enter>", onx)
close.bind("<Leave>", offx)
close.bind("<Button-1>", x)

def onmin(e):
    minimize.create_text(8,8, text="-", fill="goldenrod")

def offmin(e):
    minimize.delete(ALL)
    minimize.create_oval(1,1, 15,15, outline="goldenrod", fill="yellow")

def minimize_me(e):
    root.update_idletasks()
    root.state("withdrawn")

minimize=Canvas(title, bg=gray, height=16, width=16, highlightthickness=0)
minimize.create_oval(1,1, 15,15, outline="goldenrod", fill="yellow")
minimize.pack(side=LEFT, pady=3, padx=2)
minimize.bind("<Enter>", onmin)
minimize.bind("<Leave>", offmin)
minimize.bind("<Button-1>", minimize_me)

window = 0
run=True
def maximize_me_for_command():
    run=True
    def loop():
        global window, run
        if run == True:
            if window < 2000:
                window += 132
                root.geometry(f"{window}x{window}")
            else:
                window = 0
                run=False
            root.after(50, loop)
    loop()
    root.geometry("+0+0")

def titleleft():
    root.geometry(f"400x1500+0+0")

def titleright():
    root.geometry(f"400x1500+{root.winfo_screenwidth()-400}+0")

maxmenu=Menu(root)
maxmenu.add_cascade(label="▬  Enter Full Screen", command=maximize_me_for_command)
maxmenu.add_cascade(label="⇤ Title Window to Left of Screen", command=titleleft)
maxmenu.add_cascade(label="⇥ Title Window to Right of Screen", command=titleright)

go=False

def show():
    global go
    if go==True:
        maxmenu.tk_popup(root.winfo_x()+55, root.winfo_y()+32)
        go=False

def onmax(e):
    global go
    maximize.create_rectangle(4,4, 12,12, width=0, fill="darkolivegreen")
    maximize.create_text(8,8, text="⁄", fill="green")
    go=True
    root.after(1000, show)

def offmax(e):
    global go
    go=False
    maximize.delete(ALL)
    maximize.create_oval(1,1, 15,15, outline="darkolivegreen", fill="green")

def maximize_me(e):
    run=True
    def loop():
        global window, run
        if run == True:
            if window < 2000:
                window += 132
                root.geometry(f"{window}x{window}")
            else:
                window = 0
                run=False
            root.after(50, loop)
    loop()
    root.geometry("+0+0")

maximize=Canvas(title, bg=gray, height=16, width=16, highlightthickness=0)
maximize.create_oval(1,1, 15,15, outline="darkolivegreen", fill="green")
maximize.pack(side=LEFT, pady=3, padx=2)
maximize.bind("<Enter>", onmax)
maximize.bind("<Leave>", offmax)
maximize.bind("<Button-1>", maximize_me)

text=Frame(title, bg=gray)
text.pack(pady=3)
textintext=Frame(text, bg=gray)
textintext.pack()
l=Label(textintext, text=Title, bg=gray, fg=black, font="Helvetica 8")
l.grid(row=1, column=2)
icn=Label(textintext, text="I", font="Helvetica 8")
icn.grid(row=1, column=1)

sg = Sizegrip(root, cursor="lr_angle")
sg.pack(anchor=SE, side=BOTTOM, pady=1, padx=9)

Here is a different version.

import tkinter as tk

def nothing():
    pass

class BTk():
    def __init__(self, title="tk", bg="systemWindowBackgroundColor", fg="systemTextColor", titlebar_c="systemMenu"):
        self.BG_COLOR = bg
        self.FG_COLOR = fg
        self.TRANS_COLOR = "systemTransparent"
        self.TITLEBAR_COLOR = titlebar_c

        ## Create borderless window
        self.root = tk.Tk()
        self.root.geometry("400x170+400+200")
        self.root.overrideredirect(True)
        self.root.resizable(True, True)
        self.root.attributes("-transparent", True)
        self.root["bg"] = self.BG_COLOR

        ## Create fake window to show in doc
        self.doc_dummy = tk.Tk()
        self.doc_dummy.geometry("50x50+500+500")
        self.doc_dummy.attributes("-alpha", 0.0)
        self.doc_dummy.protocol("WM_DELETE_WINDOW", nothing)

        ## Create titlebar and window frame
        self.titlebar = tk.Label(self.root, highlightthicknes=0, bd=0, text=title, bg=self.TITLEBAR_COLOR)
        self.titlebar.pack(fill=tk.X)

        self.top_left = tk.Canvas(self.titlebar, highlightthicknes=0, bg=self.TRANS_COLOR, width=60, height=30)
        self.top_left.pack(side=tk.LEFT)
        self.top_left.create_oval(0, 0, 18, 18, fill=self.TITLEBAR_COLOR, width=0)
        self.top_left.create_polygon(9, -1, 60, -1, 60, 30, -1, 30, -1, 9, fill=self.TITLEBAR_COLOR, width=0)

        self.top_right = tk.Canvas(self.titlebar, highlightthicknes=0, bg=self.TRANS_COLOR, width=30, height=30)
        self.top_right.pack(side=tk.RIGHT)
        self.top_right.create_oval(30, 0, 12, 18, fill=self.TITLEBAR_COLOR, width=0)
        self.top_right.create_polygon(21, -1, -1, -1, -1, 30, 30, 30, 30, 9, fill=self.TITLEBAR_COLOR, width=0)

        self.bottom_left = tk.Canvas(self.root, highlightthicknes=0, bg=self.TRANS_COLOR, width=30, height=30)
        self.bottom_left.place(x=0, y=-30, relx=0, rely=1)
        self.bottom_left.create_oval(0, 30, 18, 12, fill=self.BG_COLOR, width=0)
        self.bottom_left.create_polygon(9, 30, 30, 30, 30, -1, -1, -1, -1, 21, fill=self.BG_COLOR, width=0)

        self.bottom_right = tk.Canvas(self.root, highlightthicknes=0, bg=self.TRANS_COLOR, width=30, height=30)
        self.bottom_right.place(x=-30, y=-30, relx=1, rely=1)
        self.bottom_right.create_oval(30, 30, 12, 12, fill=self.BG_COLOR, width=0)
        self.bottom_right.create_polygon(21, 30, -1, 30, -1, -1, 30, -1, 30, 21, fill=self.BG_COLOR, width=0)

        self.main_frame = tk.Frame(self.root, highlightthicknes=0, bd=0, bg=self.BG_COLOR)
        self.main_frame.pack(expand=True, fill=tk.BOTH, pady=[0, 9])

        ## Create buttons
        self.close_btn = self.top_left.create_oval(7, 7, 18, 18, fill="#ED6A5F", outline="#CE4C3E")
        self.minimize_btn = self.top_left.create_oval(27, 7, 38, 18, fill="#F4BE4F", outline="#D59E3A")
        self.options_btn = self.top_left.create_oval(47, 7, 58, 18, fill="#62C554", outline="#4DA438")
        self.close_smbl = self.top_left.create_text(13, 13, text="✕", fill="#dd0000", font="Arial 8 bold")
        self.top_left.itemconfig(self.close_smbl, state="hidden")
        self.minimize_smbl = self.top_left.create_text(33, 13, text="⎯", fill="#994500", font="Arial 8 bold")
        self.top_left.itemconfig(self.minimize_smbl, state="hidden")
        self.options_smbl = self.top_left.create_text(53, 13, text="◼", fill="#009900")
        self.top_left.create_text(53, 13, text="╲", fill="#62C554", font="Arial 8 bold")
        self.top_left.itemconfig(self.options_smbl, state="hidden")

        ## Setup bindings
        self.top_left.tag_bind(self.close_smbl, "<Button-1>", lambda _e: self.quit())
        self.top_left.tag_bind(self.close_btn, "<Button-1>", lambda _e: self.quit())
        self.top_left.tag_bind(self.minimize_smbl, "<Button-1>", lambda _e:self.minimize())
        self.top_left.tag_bind(self.minimize_btn, "<Button-1>", lambda _e:self.minimize())
        self.top_left.tag_bind(self.options_smbl, "<Button-1>", lambda _e: self.maximize())
        self.top_left.tag_bind(self.options_btn, "<Button-1>", lambda _e: self.maximize())
        self.titlebar.bind("<Double-Button-1>", lambda _e: self.maximize())
        self.titlebar.bind("<B1-Motion>", self.drag)
        self.titlebar.bind("<Button-1>", self.get_pos)
        self.doc_dummy.bind("<Motion>", self.check_btns_hover)

        ## adding widgets and other window setup
        self.tk = self.main_frame.tk
        self._last_child_ids = self.main_frame._last_child_ids
        self._w = self.main_frame._w
        self.children = self.main_frame.children

        self.winfo_width = self.root.winfo_width
        self.winfo_height = lambda: self.root.winfo_height()-37
        self.winfo_x = self.root.winfo_x
        self.winfo_y = self.root.winfo_y
        self.winfo_pointerx = self.root.winfo_pointerx
        self.winfo_pointery = self.root.winfo_pointery
        self.winfo_screenwidth = self.root.winfo_screenwidth
        self.winfo_screenheight = self.root.winfo_screenheight

        self.title(title)
        self.destroy_fun = None

    def minimize(self):
        self.root.withdraw()
        self.doc_dummy.iconify()
        def loop():
            if self.doc_dummy.state() == "iconic":
                self.doc_dummy.after(50, loop)
            else:
                self.unminimize()
        loop()

    def unminimize(self):
        self.root.deiconify()
        self.focus_force()

    def get_pos(self, event):
        self.last_x = event.x_root
        self.last_y = event.y_root

    def drag(self, event):
        deltax = event.x_root - self.last_x
        deltay = event.y_root - self.last_y
        x = self.root.winfo_x() + deltax
        y = self.root.winfo_y() + deltay
        self.root.geometry("+%s+%s" % (x, y))
        self.last_x = event.x_root
        self.last_y = event.y_root

    def mainloop(self):
        self.root.mainloop()

    def bind(self, key, fun):
        self.doc_dummy.bind(key, fun)

    def quit(self):
        if self.destroy_fun == None:
            self.root.quit()
            self.doc_dummy.destroy()
            self.root.withdraw()
        else:
            self.destroy_fun()

    def destroy(self):
        self.root.quit()
        self.doc_dummy.destroy()
        self.root.withdraw()

    def maximize(self):
        self.root.geometry(f"{self.root.winfo_screenwidth()}x{self.root.winfo_screenheight()}+0+0")
        self.root.update_idletasks()

    def focus_force(self):
        self.root.focus_force()
        self.doc_dummy.focus_force()

    def title(self, title):
        self.root.title(title)
        self.titlebar.text = title
        self.doc_dummy.title(title)

    def geometry(self, string):
        global wh, x, y, w, h
        if "+" in string and "x" in string:
            wh, x, y = string.split("+")
            w, h = wh.split("x")
            self.root.geometry(f"{w}x{int(h)+30}+{x}+{y}")
        elif "x":
            w, h = string.split("x")
            self.root.geometry(f"{w}x{int(h)+30}")
        else:
            x, y = string.split("+")
            self.root.geometry(f"+{x}+{y}")

    def check_btns_hover(self, _e):
        px = self.root.winfo_pointerx()-self.root.winfo_x()
        py = self.root.winfo_pointery()-self.root.winfo_y()
        if px < 60 and px > 0 and py < 30 and px > 0:
            self.top_left.itemconfig(self.close_smbl, state="normal")
            self.top_left.itemconfig(self.minimize_smbl, state="normal")
            self.top_left.itemconfig(self.options_smbl, state="normal")
        else:
            self.top_left.itemconfig(self.close_smbl, state="hidden")
            self.top_left.itemconfig(self.minimize_smbl, state="hidden")
            self.top_left.itemconfig(self.options_smbl, state="hidden")

    def after(self, time, fun, *args):
        self.root.after(time, fun, *args)

    def protocol(self, call, fun):
        if call == "WM_DELETE_WINDOW":
            self.destroy_fun = fun
        else:
            root.protocol(call, fun)

    def iconphoto(self, img, compound=tk.LEFT):
        self.titlebar["image"] = img
        self.titlebar["compound"] = compound

example use:

root = BTk(title="Custom Modern Window")
tk.Label(root, text="Try to resize, move minimize or maximize me.\nI can also recive inputs. Press a key.").pack()
root.bind("<KeyPress>", print)
img = tk.PhotoImage("path/to/file.png") # for icon
root.iconphoto(img)
root.mainloop()

Upvotes: 1

Related Questions