Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions customtkinter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .windows import CTk
from .windows import CTkToplevel
from .windows import CTkInputDialog
from .windows import CTkTooltip

# import font classes
from .windows.widgets.font import CTkFont
Expand Down
1 change: 1 addition & 0 deletions customtkinter/windows/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .ctk_tk import CTk
from .ctk_toplevel import CTkToplevel
from .ctk_input_dialog import CTkInputDialog
from .ctk_tooltip import CTkTooltip
136 changes: 136 additions & 0 deletions customtkinter/windows/ctk_tooltip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from .widgets.theme.theme_manager import ThemeManager
from .ctk_toplevel import CTkToplevel
from .widgets.ctk_label import CTkLabel
from .widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from typing import Union, Tuple
import sys


class CTkTooltip(CTkToplevel):
# Mouse hover tooltips that can be attached to widgets
def __init__(self, master,
text: str = 'CTk Tooltip',
delay: int = 500,
wrap_length: int = -1,
bg_color: Union[str, Tuple[str, str]] = "transparent",
fg_color: Union[str, Tuple[str, str]] = "default",
mouse_offset: Tuple[int, int] = (1, 1),
**kwargs):
self.wait_time = delay # milliseconds until tooltip appears
self.wrap_length = wrap_length # wrap length of the tooltip text
self.master = master # parent widget
self.text = text # text to display
self.mouse_offset = mouse_offset # offset from mouse position (x, y)
self.master.bind("<Enter>", self._schedule, add="+")
self.master.bind("<Leave>", self._leave)
self.master.bind("<ButtonPress>", self._leave, add="+")
label = self.master.winfo_children()[0]
label.bind("<Enter>", self._schedule, add="+")
self._id = None
self.kwargs = kwargs
self._visible = False
self._is_hovering_tooltip = False
self._bg_color_is_default = True if bg_color == "transparent" else False # used on linux because rounded corners doesnt seem to be possible usually

# determine colors
self.__appearance_mode = AppearanceModeTracker.get_mode()
if fg_color == "default":
self.fg_color = '#CBCBCB' if self.__appearance_mode == 0 else '#545454'
else:
self.fg_color = fg_color
if bg_color == "transparent":
if bg_color.startswith('#'):
color_list = [int(self.fg_color[i:i + 2], 16) for i in range(1, len(self.fg_color), 2)]
if not any(color == 255 for color in color_list):
for i in range(len(color_list)):
color_list[i] += 1
else:
for i in range(len(color_list)):
color_list[i] -= 1

self.bg_color = "#" + ''.join(['{:02x}'.format(x) for x in color_list])
else:
if self.__appearance_mode == 0:
self.bg_color = 'gray86' if self.fg_color != 'gray86' else 'gray84'
else:
self.bg_color = 'gray17' if self.fg_color != 'gray17' else 'gray15'
else:
self.bg_color = bg_color


def _leave(self, event=None):
self._unschedule()
if self._visible: self.hide()

def _schedule(self, event=None):
self._unschedule()
self._id = self.master.after(self.wait_time, self.show)

def _unschedule(self):
# Unschedule scheduled popups
id = self._id
self._id = None
if id:
self.master.after_cancel(id)

def show(self, event=None):
# Get the position the tooltip needs to appear at
if self._visible: return
super().__init__(self.master)
super().withdraw() # hide and reshow window once all code is ran to fix issues due to slower machines (??)
self._visible = True
x = y = 0
x, y, cx, cy = self.master.bbox("insert")
# Has to be offset from mouse position, otherwise it will appear and disappear instantly because it left the parent widget
x += self.master.winfo_pointerx() + self.mouse_offset[0]
y += self.master.winfo_pointery() + self.mouse_offset[1]
if sys.platform.startswith("win"):
self.wm_attributes('-transparentcolor', self.bg_color) # used for rounded corners
self.wm_attributes("-toolwindow", True) # removes icon from taskbar
super().configure(bg_color=self.bg_color)
elif sys.platform == 'darwin':
self.wm_attributes('-transparent', True) # used for rounded corners
super().configure(bg_color='systemTransparent')
elif sys.platform.startswith("linux"):
if self._bg_color_is_default:
self.bg_color = self.fg_color # create square edge tooltips
self.wm_overrideredirect(True)
self.wm_geometry(f'+{x}+{y}')
label = CTkLabel(self, text=self.text, corner_radius=10, bg_color=self.bg_color, fg_color=self.fg_color, width=1, wraplength=self.wrap_length, **self.kwargs)
label.pack()
if sys.platform == 'darwin': label.configure(bg_color='systemTransparent')
label.bind("<Enter>", self._leave, add="+")
super().deiconify()

def hide(self):
self._unschedule()
self.withdraw()
self._visible = False

def configure(self, **kwargs):
# Change attributes of the tooltip, and redraw if necessary
require_redraw = False
if "fg_color" in kwargs:
self.fg_color = kwargs.pop("fg_color")
require_redraw = True
if "bg_color" in kwargs:
self.bg_color = kwargs.pop("bg_color")
require_redraw = True
if "text" in kwargs:
self.text = kwargs.pop("text")
require_redraw = True
if "delay" in kwargs:
self.wait_time = kwargs.pop("delay")
if "wrap_length" in kwargs:
self.wrap_length = kwargs.pop("wrap_length")
require_redraw = True
if "mouse_offset" in kwargs:
self.mouse_offset = kwargs.pop("mouse_offset")
require_redraw = True
self.kwargs = kwargs
if require_redraw:
self.hide()
self.show()

def is_visible(self):
return self._visible
1 change: 1 addition & 0 deletions examples/simple_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def slider_callback(value):

button_1 = customtkinter.CTkButton(master=frame_1, command=button_callback)
button_1.pack(pady=10, padx=10)
tooltip_1 = customtkinter.CTkTooltip(master=button_1)

slider_1 = customtkinter.CTkSlider(master=frame_1, command=slider_callback, from_=0, to=1)
slider_1.pack(pady=10, padx=10)
Expand Down