""" CustomTkinter Messagebox Author: Akash Bora Version: 2.5 """ import customtkinter from PIL import Image, ImageTk import os import sys import time from typing import Literal class CTkMessagebox(customtkinter.CTkToplevel): ICONS = { "check": None, "cancel": None, "info": None, "question": None, "warning": None } ICON_BITMAP = {} def __init__(self, master: any = None, width: int = 400, height: int = 200, title: str = "CTkMessagebox", message: str = "This is a CTkMessagebox!", option_1: str = "OK", option_2: str = None, option_3: str = None, options: list = [], border_width: int = 1, border_color: str = "default", button_color: str = "default", bg_color: str = "default", fg_color: str = "default", text_color: str = "default", title_color: str = "default", button_text_color: str = "default", button_width: int = None, button_height: int = None, cancel_button_color: str = None, cancel_button: str = None, # types: circle, cross or none button_hover_color: str = "default", icon: str = "info", icon_size: tuple = None, corner_radius: int = 15, justify: str = "right", font: tuple = None, header: bool = False, topmost: bool = True, fade_in_duration: int = 0, sound: bool = False, option_focus: Literal[1, 2, 3] = None): super().__init__() self.master_window = master self.width = 250 if width<250 else width self.height = 150 if height<150 else height if self.master_window is None: self.spawn_x = int((self.winfo_screenwidth()-self.width)/2) self.spawn_y = int((self.winfo_screenheight()-self.height)/2) else: self.spawn_x = int(self.master_window.winfo_width() * .5 + self.master_window.winfo_x() - .5 * self.width + 7) self.spawn_y = int(self.master_window.winfo_height() * .5 + self.master_window.winfo_y() - .5 * self.height + 20) self.after(10) self.geometry(f"{self.width}x{self.height}+{self.spawn_x}+{self.spawn_y}") self.title(title) self.resizable(width=False, height=False) self.fade = fade_in_duration if self.fade: self.fade = 20 if self.fade<20 else self.fade self.attributes("-alpha", 0) if not header: self.overrideredirect(1) if topmost: self.attributes("-topmost", True) else: self.transient(self.master_window) if sys.platform.startswith("win"): self.transparent_color = self._apply_appearance_mode(self.cget("fg_color")) self.attributes("-transparentcolor", self.transparent_color) default_cancel_button = "cross" elif sys.platform.startswith("darwin"): self.transparent_color = 'systemTransparent' self.attributes("-transparent", True) default_cancel_button = "circle" else: self.transparent_color = '#000001' corner_radius = 0 default_cancel_button = "cross" self.lift() self.config(background=self.transparent_color) self.protocol("WM_DELETE_WINDOW", self.button_event) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.x = self.winfo_x() self.y = self.winfo_y() self._title = title self.message = message self.font = font self.justify = justify self.sound = sound self.cancel_button = cancel_button if cancel_button else default_cancel_button self.round_corners = corner_radius if corner_radius<=30 else 30 self.button_width = button_width if button_width else self.width/4 self.button_height = button_height if button_height else 28 if self.fade: self.attributes("-alpha", 0) if self.button_height>self.height/4: self.button_height = self.height/4 -20 self.dot_color = cancel_button_color self.border_width = border_width if border_width<6 else 5 if type(options) is list and len(options)>0: try: option_1 = options[-1] option_2 = options[-2] option_3 = options[-3] except IndexError: None if bg_color=="default": self.bg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"]) else: self.bg_color = bg_color if fg_color=="default": self.fg_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) else: self.fg_color = fg_color default_button_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["fg_color"]) if sys.platform.startswith("win"): if self.bg_color==self.transparent_color or self.fg_color==self.transparent_color: self.configure(fg_color="#000001") self.transparent_color = "#000001" self.attributes("-transparentcolor", self.transparent_color) if button_color=="default": self.button_color = (default_button_color, default_button_color, default_button_color) else: if type(button_color) is tuple: if len(button_color)==2: self.button_color = (button_color[0], button_color[1], default_button_color) elif len(button_color)==1: self.button_color = (button_color[0], default_button_color, default_button_color) else: self.button_color = button_color else: self.button_color = (button_color, button_color, button_color) if text_color=="default": self.text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"]) else: self.text_color = text_color if title_color=="default": self.title_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkLabel"]["text_color"]) else: self.title_color = title_color if button_text_color=="default": self.bt_text_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["text_color"]) else: self.bt_text_color = button_text_color if button_hover_color=="default": self.bt_hv_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkButton"]["hover_color"]) else: self.bt_hv_color = button_hover_color if border_color=="default": self.border_color = self._apply_appearance_mode(customtkinter.ThemeManager.theme["CTkFrame"]["border_color"]) else: self.border_color = border_color if icon_size: self.size_height = icon_size[1] if icon_size[1]<=self.height-100 else self.height-100 self.size = (icon_size[0], self.size_height) else: self.size = (self.height/4, self.height/4) self.icon = self.load_icon(icon, icon_size) if icon else None self.frame_top = customtkinter.CTkFrame(self, corner_radius=self.round_corners, width=self.width, border_width=self.border_width, bg_color=self.transparent_color, fg_color=self.bg_color, border_color=self.border_color) self.frame_top.grid(sticky="nswe") if button_width: self.frame_top.grid_columnconfigure(0, weight=1) else: self.frame_top.grid_columnconfigure((1,2,3), weight=1) if button_height: self.frame_top.grid_rowconfigure((0,1,3), weight=1) else: self.frame_top.grid_rowconfigure((0,1,2), weight=1) self.frame_top.bind("", self.move_window) self.frame_top.bind("", self.oldxyset) if self.cancel_button=="cross": self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=0, height=0, hover=False, border_width=0, text_color=self.dot_color if self.dot_color else self.title_color, text="✕", fg_color="transparent", command=self.button_event) self.button_close.grid(row=0, column=5, sticky="ne", padx=5+self.border_width, pady=5+self.border_width) elif self.cancel_button=="circle": self.button_close = customtkinter.CTkButton(self.frame_top, corner_radius=10, width=10, height=10, hover=False, border_width=0, text="", fg_color=self.dot_color if self.dot_color else "#c42b1c", command=self.button_event) self.button_close.grid(row=0, column=5, sticky="ne", padx=10, pady=10) self.title_label = customtkinter.CTkLabel(self.frame_top, width=1, text=self._title, text_color=self.title_color, font=self.font) self.title_label.grid(row=0, column=0, columnspan=6, sticky="nw", padx=(15,30), pady=5) self.title_label.bind("", self.move_window) self.title_label.bind("", self.oldxyset) self.info = customtkinter.CTkButton(self.frame_top, width=1, height=self.height/2, corner_radius=0, text=self.message, font=self.font, fg_color=self.fg_color, hover=False, text_color=self.text_color, image=self.icon) self.info._text_label.configure(wraplength=self.width/2, justify="left") self.info.grid(row=1, column=0, columnspan=6, sticky="nwes", padx=self.border_width) if self.info._text_label.winfo_reqheight()>self.height/2: height_offset = int((self.info._text_label.winfo_reqheight())-(self.height/2) + self.height) self.geometry(f"{self.width}x{height_offset}") self.option_text_1 = option_1 self.button_1 = customtkinter.CTkButton(self.frame_top, text=self.option_text_1, fg_color=self.button_color[0], width=self.button_width, font=self.font, text_color=self.bt_text_color, hover_color=self.bt_hv_color, height=self.button_height, command=lambda: self.button_event(self.option_text_1)) self.option_text_2 = option_2 if option_2: self.button_2 = customtkinter.CTkButton(self.frame_top, text=self.option_text_2, fg_color=self.button_color[1], width=self.button_width, font=self.font, text_color=self.bt_text_color, hover_color=self.bt_hv_color, height=self.button_height, command=lambda: self.button_event(self.option_text_2)) self.option_text_3 = option_3 if option_3: self.button_3 = customtkinter.CTkButton(self.frame_top, text=self.option_text_3, fg_color=self.button_color[2], width=self.button_width, font=self.font, text_color=self.bt_text_color, hover_color=self.bt_hv_color, height=self.button_height, command=lambda: self.button_event(self.option_text_3)) if self.justify=="center": if button_width: columns = [4,3,2] span = 1 else: columns = [4,2,0] span = 2 if option_3: self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10) self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10) elif option_2: self.frame_top.columnconfigure((0,5), weight=1) columns = [2,3] self.button_1.grid(row=2, column=columns[0], sticky="news", padx=(0,5), pady=10) self.button_2.grid(row=2, column=columns[1], sticky="news", padx=(5,0), pady=10) else: if button_width: self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) else: self.frame_top.columnconfigure((0,2,4), weight=2) self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=(0,10), pady=10) elif self.justify=="left": self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) if button_width: columns = [0,1,2] span = 1 else: columns = [0,2,4] span = 2 if option_3: self.button_1.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(0,10), pady=10) self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) self.button_3.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) elif option_2: self.button_1.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) self.button_2.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) else: self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(10,0), pady=10) else: self.frame_top.columnconfigure((0,1,2,3,4,5), weight=1) if button_width: columns = [5,4,3] span = 1 else: columns = [4,2,0] span = 2 self.button_1.grid(row=2, column=columns[0], columnspan=span, sticky="news", padx=(0,10), pady=10) if option_2: self.button_2.grid(row=2, column=columns[1], columnspan=span, sticky="news", padx=10, pady=10) if option_3: self.button_3.grid(row=2, column=columns[2], columnspan=span, sticky="news", padx=(10,0), pady=10) if header: self.title_label.configure(text="") self.title_label.grid_configure(pady=0) self.button_close.configure(text_color=self.bg_color) self.frame_top.configure(corner_radius=0) if self.winfo_exists(): self.grab_set() if self.sound: self.bell() if self.fade: self.fade_in() if option_focus: self.option_focus = option_focus self.focus_button(self.option_focus) else: if not self.option_text_2 and not self.option_text_3: self.button_1.focus() self.button_1.bind("", lambda event: self.button_event(self.option_text_1)) self.bind("", lambda e: self.button_event()) def focus_button(self, option_focus): try: self.selected_button = getattr(self, "button_"+str(option_focus)) self.selected_button.focus() self.selected_button.configure(border_color=self.bt_hv_color, border_width=3) self.selected_option = getattr(self, "option_text_"+str(option_focus)) self.selected_button.bind("", lambda event: self.button_event(self.selected_option)) except AttributeError: return self.bind("", lambda e: self.change_left()) self.bind("", lambda e: self.change_right()) def change_left(self): if self.option_focus==3: return self.selected_button.unbind("") self.selected_button.configure(border_width=0) if self.option_focus==1: if self.option_text_2: self.option_focus = 2 elif self.option_focus==2: if self.option_text_3: self.option_focus = 3 self.focus_button(self.option_focus) def change_right(self): if self.option_focus==1: return self.selected_button.unbind("") self.selected_button.configure(border_width=0) if self.option_focus==2: self.option_focus = 1 elif self.option_focus==3: self.option_focus = 2 self.focus_button(self.option_focus) def load_icon(self, icon, icon_size): if icon not in self.ICONS or self.ICONS[icon] is None: if icon in ["check", "cancel", "info", "question", "warning"]: image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons', icon + '.png') else: image_path = icon if icon_size: size_height = icon_size[1] if icon_size[1] <= self.height - 100 else self.height - 100 size = (icon_size[0], size_height) else: size = (self.height / 4, self.height / 4) self.ICONS[icon] = customtkinter.CTkImage(Image.open(image_path), size=size) self.ICON_BITMAP[icon] = ImageTk.PhotoImage(file=image_path) self.after(200, lambda: self.iconphoto(False, self.ICON_BITMAP[icon])) return self.ICONS[icon] def fade_in(self): for i in range(0,110,10): if not self.winfo_exists(): break self.attributes("-alpha", i/100) self.update() time.sleep(1/self.fade) def fade_out(self): for i in range(100,0,-10): if not self.winfo_exists(): break self.attributes("-alpha", i/100) self.update() time.sleep(1/self.fade) def get(self): if self.winfo_exists(): self.master.wait_window(self) return self.event def oldxyset(self, event): self.oldx = event.x self.oldy = event.y def move_window(self, event): self.y = event.y_root - self.oldy self.x = event.x_root - self.oldx self.geometry(f'+{self.x}+{self.y}') def button_event(self, event=None): try: self.button_1.configure(state="disabled") self.button_2.configure(state="disabled") self.button_3.configure(state="disabled") except AttributeError: pass if self.fade: self.fade_out() self.grab_release() self.destroy() self.event = event if __name__ == "__main__": app = CTkMessagebox() app.mainloop()