Tkinter Toplevel Window Not Movable - python

In my Tkinter app I have a button that opens a Toplevel Window which dispays an event log. There are a few things I need the Toplevel Window to be able to do:
Show previous log entries when opened, as well as update with new ones.
Disable the user's ability to move the window around the while also making the user able to close the window
Have the window always be anchored with it's top right corner being at the root window's top right corner
I have figured out #1. I am able to have the window open and display previous entries as well as update those entries while the window is open. My problem is with #2 and #3.
For #2 I am not sure how to disable the user's ability to move the window. I am assuming this may also disable the user's ability to close the window so I am not sure how to keep that functionality intact. Maybe a button with self.quit() as it's command?
As for #3, I have no idea how to go about doing this. Maybe I suck as Googling but I can't seem to find out how to accomplish this.
This is the code I have at the moment, which is able to properly implement feature #1.
import tkinter as tk
class guiapp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.value = 0.0
self.alive = True
self.list_for_toplevel = []
btn = tk.Button(self.master, text = "Click", command = self.TextWindow)
btn.pack()
def TextWindow(self):
self.textWindow = tk.Toplevel(self.master)
self.textFrame = tk.Frame(self.textWindow)
self.textFrame.pack()
self.textArea = tk.Text(self.textWindow, height = 10, width = 30)
self.textArea.pack(side = "left", fill = "y")
bar = tk.Scrollbar(self.textWindow)
bar.pack(side = "right", fill = "y")
bar.config(command = self.textArea.yview)
self.alive = True
self.timed_loop()
def timed_loop(self):
if self.alive == True and tk.Toplevel.winfo_exists(self.textWindow):
self.master.after(1000, self.timed_loop)
self.value += 1
self.list_for_toplevel.append(self.value)
self.textArea.delete(1.0, "end-1c")
for item in self.list_for_toplevel:
self.textArea.insert('end', "{}\n".format(item))
self.textArea.see('end')
else:
self.alive = False
if __name__ == "__main__":
root = tk.Tk()
root.geometry("800x480")
myapp = guiapp(root)
root.mainloop()

We can remove the tool bar from the top of the toplevel window and prevent the user from moving the window with self.textWindow.overrideredirect(True).
Then we can make sure that the toplevel window is positioned in the top right corner by getting the root windows location and then setting the toplevel window to the same location with self.master.winfo_x() and self.master.winfo_y().
Last I would add a button that closes the window because we no longer have the tool bar for the toplevel window.
UPDATE: I have added the ability for the toplevel window to stay on top of the root window and to move around with the root window when root is dragged.
we can use bind() to track when the root window is moved and then have a function that will update the toplevel windows position to match the root windows.
We can also use self.textWindow.attributes("-topmost", True) to tell tkinter to keep out toplevel window on top of all other windows.
Take a look at the modified version of your code below. Let me know what you think or if you have any questions.
import tkinter as tk
class guiapp(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.master = master
self.textWindow = None
self.master.bind("<Configure>", self.move_me)
self.value = 0.0
self.list_for_toplevel = []
btn = tk.Button(self.master, text = "Click", command = self.TextWindow)
btn.pack()
def TextWindow(self):
x = self.master.winfo_x()
y = self.master.winfo_y()
self.textWindow = tk.Toplevel(self.master)
self.textFrame = tk.Frame(self.textWindow)
self.textWindow.overrideredirect(True)
self.textFrame.pack()
self.textWindow.attributes("-topmost", True)
self.textWindow.geometry('+{}+{}'.format(x+10, y+30))
self.close_toplevel = tk.Button(self.textWindow, text = "close", command = self.close_textWindow)
self.close_toplevel.pack()
self.textArea = tk.Text(self.textWindow, height = 10, width = 30)
self.textArea.pack(side = "left", fill = "y")
bar = tk.Scrollbar(self.textWindow)
bar.pack(side = "right", fill = "y")
bar.config(command = self.textArea.yview)
self.alive = True
self.timed_loop()
def close_textWindow(self):
self.textWindow.destroy()
self.textWindow = None
def move_me(self, event):
if self.textWindow != None:
x = self.master.winfo_x()
y = self.master.winfo_y()
self.textWindow.geometry('+{}+{}'.format(x+10, y+30))
def timed_loop(self):
if self.textWindow != None:
self.master.after(1000, self.timed_loop)
self.value += 1
self.list_for_toplevel.append(self.value)
self.textArea.delete(1.0, "end-1c")
for item in self.list_for_toplevel:
self.textArea.insert('end', "{}\n".format(item))
self.textArea.see('end')
if __name__ == "__main__":
root = tk.Tk()
root.geometry("800x480")
myapp = guiapp(root)
root.mainloop()

Related

Is there a way to create a second window that connects to the parent window like a dropdown menu

I'm trying to make it so that new information shows in in a new window, but I want the new window to be connected to the parent window, even when the parent window is clicked the new window should still show up similar to how a dropdown menu works. I'm also planning on having some of the new windows have treeviews later on.
from tkinter import *
win = Tk()
win.geometry("500x500+0+0")
def button_function ():
win2 = Toplevel()
label = Label(win2,text='dropdown', width=7)
label.pack()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y()+30}")
button = Button(win, command=lambda: button_function (), width=12)
button.pack()
win.mainloop()
Ok so with a little bit of googling I came across this post: tkinter-detecting-a-window-drag-event
In that post they show how you can keep track of when the window has moved.
By taking that code and making some small changes we can use the dragging() and stop_drag() functions to move the top level window back to where it was set to relative to the main window.
That said this will only work in this case. You will need to write something more dynamic to track any new windows you want so they are placed properly and on top of that you will probably want to build this in a class so you do not have to manage global variables.
With a combination of this tracking function and using lift() to bring the window up we get closer to what you are asking to do.
That said you will probably want remove the tool bar at the top of the root window to be more clean. I would also focus on using a dictionary or list to keep track of open and closed windows and their locations to make the dynamic part of this easier.
import tkinter as tk
win = tk.Tk()
win.geometry("500x500+0+0")
win2 = None
drag_id = ''
def dragging(event):
global drag_id
if event.widget is win:
if drag_id == '':
print('start drag')
else:
win.after_cancel(drag_id)
print('dragging')
drag_id = win.after(100, stop_drag)
if win2 is not None:
win2.lift()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y() + 30}")
def stop_drag():
global drag_id, win2, win
print('stop drag')
drag_id = ''
if win2 is not None:
win2.lift()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y() + 30}")
win.bind('<Configure>', dragging)
def button_function():
global win2
win2 = tk.Toplevel()
label = tk.Label(win2, text='drop down', width=7)
label.pack()
win2.geometry(f"+{win.winfo_x()}+{win.winfo_y()+30}")
tk.Button(win, command=button_function, width=12).pack()
win.mainloop()
EDIT:
Ok so I took some time to write this up in a class so you could see how it could be done. I have also added some level of dynamic building of the buttons and pop up windows.
We use a combination of lists and lambdas to perform a little bit of tracking and in the end we pull off exactly what you were asking for.
let me know if you have any questions.
import tkinter as tk
class Main(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('500x500')
self.pop_up_list = []
self.drag_id = ''
self.button_notes = ['Some notes for new window', 'some other notes for new window', 'bacon that is all!']
self.bind('<Configure>', self.dragging)
for ndex, value in enumerate(self.button_notes):
print(ndex)
btn = tk.Button(self, text=f'Button {ndex+1}')
btn.config(command=lambda b=btn, i=ndex: self.toggle_button_pop_ups(i, b))
btn.grid(row=ndex, column=0, padx=5, pady=5)
self.pop_up_list.append([value, 0, None, btn])
def dragging(self, event):
if event.widget is self:
if self.drag_id == '':
pass
else:
self.after_cancel(self.drag_id)
self.drag_id = self.after(100, self.stop_drag)
for p in self.pop_up_list:
if p[1] == 1:
p[2].lift()
p[2].geometry(f"+{p[3].winfo_rootx() + 65}+{p[3].winfo_rooty()}")
def stop_drag(self):
self.drag_id = ''
for p in self.pop_up_list:
if p[1] == 1:
p[2].lift()
p[2].geometry(f"+{p[3].winfo_rootx() + 65}+{p[3].winfo_rooty()}")
def toggle_button_pop_ups(self, ndex, btn):
p = self.pop_up_list
if p[ndex][1] == 0:
p[ndex][1] = 1
p[ndex][2] = tk.Toplevel(self)
p[ndex][2].overrideredirect(1)
tk.Label(p[ndex][2], text=self.pop_up_list[ndex][0]).pack()
p[ndex][2].geometry(f"+{btn.winfo_rootx() + 65}+{btn.winfo_rooty()}")
else:
p[ndex][1] = 0
p[ndex][2].destroy()
p[ndex][2] = None
if __name__ == '__main__':
Main().mainloop()

Tkinter doesn't respond to alt-release events sometimes

Tkinter doesn't respond to alt-release events sometimes, which can be seen from the demo below.
import tkinter
class ModeSwitcher:
def __init__(self) -> None:
self.__tkCanvas = None
def bind2Canvas(self,tkCanvas:tkinter.Canvas):
self.__tkCanvas = tkCanvas
# Single selection mode
self.__tkCanvas.bind_all("<KeyPress-Control_L>",self.__control_pressed_bindable,add=True)
self.__tkCanvas.bind_all("<KeyRelease-Control_L>",self.__control_released_bindable,add=True)
# Deselection mode
self.__tkCanvas.bind_all("<KeyPress-Alt_L>",self.__alt_pressed_bindable,add=True)
self.__tkCanvas.bind_all("<KeyRelease-Alt_L>",self.__alt_released_bindable,add=True)
self.__info_text = self.__tkCanvas.create_text(100,100,text="None")
def __control_pressed_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='control down')
def __control_released_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='control up')
def __alt_pressed_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='alt down')
def __alt_released_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='alt up')
if __name__ == '__main__':
tkWindow = tkinter.Tk()
canvas_width = 600
canvas_height = 600
canvas = tkinter.Canvas(tkWindow,width = canvas_width, height = canvas_height)
canvas.pack()
mode_switcher = ModeSwitcher()
mode_switcher.bind2Canvas(canvas)
tkWindow.mainloop()
NB:
The text is still alt down when the alt key is released, sometimes.
But, the text refreshes after the GUI is clicked.
This is expected behaviour when you press the Alt-key, as you will (by default) navigate to the window's menu just like the default behaviour of your browser. There are no menus defined in your window so this is not visible to you, but your focus is leaving the canvas after which it is no longer updated until you click it or press a key.
Disable the default alt-key behaviour, or change it so that the focus does not leave the canvas.
I've added three lines before your mainloop(), which adds a menu bar to your canvas. This shows you what happens to your focus when you press Alt.
if __name__ == '__main__':
tkWindow = tkinter.Tk()
canvas_width = 600
canvas_height = 600
canvas = tkinter.Canvas(tkWindow,width = canvas_width, height = canvas_height)
canvas.pack()
mode_switcher = ModeSwitcher()
mode_switcher.bind2Canvas(canvas)
menu = tkinter.Menu(canvas)
menu.add_cascade(label="File")
tkWindow.config(menu=menu)
tkWindow.mainloop()
It's solved after those add=True when binding is removed.
The issue is caused by the inner function of the alt key (when it pressed,the software would show the shortcuts of menu items)
import tkinter
class ModeSwitcher:
def __init__(self) -> None:
self.__tkCanvas = None
def bind2Canvas(self,tkCanvas:tkinter.Canvas):
self.__tkCanvas = tkCanvas
# Single selection mode
self.__tkCanvas.bind_all("<KeyPress-Control_L>",self.__control_pressed_bindable,add=True)
self.__tkCanvas.bind_all("<KeyRelease-Control_L>",self.__control_released_bindable,add=True)
# Deselection mode
self.__tkCanvas.bind_all("<KeyPress-Alt_L>",self.__alt_pressed_bindable)
self.__tkCanvas.bind_all("<KeyRelease-Alt_L>",self.__alt_released_bindable)
self.__info_text = self.__tkCanvas.create_text(100,100,text="None")
def __control_pressed_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='control down')
def __control_released_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='control up')
def __alt_pressed_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='alt down')
def __alt_released_bindable(self,event):
self.__tkCanvas.itemconfig(self.__info_text,text='alt up')
if __name__ == '__main__':
tkWindow = tkinter.Tk()
canvas_width = 600
canvas_height = 600
canvas = tkinter.Canvas(tkWindow,width = canvas_width, height = canvas_height)
canvas.pack()
mode_switcher = ModeSwitcher()
mode_switcher.bind2Canvas(canvas)
tkWindow.mainloop()

How can i move a frame with buttons in tkinter?

I made a button that plus the x axis with 50 if i press it.If i press the button tho it doesn't move,yes it changes the value but doesn't move. I tried a while loop but it crashed the program.
In general we use pack and grid geometry managers to handle widget placement in Tk. However, if you want to explicitly control the placement then you can use place which allows you to specify the location in either pixel or relative coordinates.
Here is an example of a frame with a button inside that moves when you click the button. Note that the button is positioned relative to its parent container so moves with the frame.
import tkinter as tk
import tkinter.ttk as ttk
class App(ttk.Frame):
def __init__(self, master, **kwargs):
super(App, self).__init__(master=master, **kwargs)
master.wm_geometry('640x480')
self.frame = f = tk.Frame(self, width=200, height=80, relief=tk.SUNKEN, borderwidth=2)
b = ttk.Button(f, text="Move", command=self.move_frame)
b.place(x=2, y=2)
f.place(x=2, y=2)
self.place(relheight=1.0, relwidth=1.0)
def move_frame(self):
x = self.frame.winfo_x()
x = x + 10
self.frame.place(x=x)
def main():
root = tk.Tk()
app = App(root)
root.mainloop()
if __name__ == '__main__':
main()

Switch between multiple windows in Tkinter

I have created few windows using Tkinter. I need help in the implementation of switching from one window to another when the button has been clicked.
All windows that are created should have the same size.
And also I want to clear existing window data and show next window data.
If you want to have multiple windows opened and want to switch between each window with all of their widgets intact then I don't think destroying a window each time you switch is a good idea instead you can try to withdraw and deiconify the windows.
I've created something like this which can switch between windows and maintain the same geometry of the previous window as you said.
import tkinter as tk
class Window(tk.Toplevel):
# List to keep the reference of all the toplevel windows
_info_pages = []
def __init__(self, master=None, cnf={}, **kw):
kw = tk._cnfmerge( (cnf,kw) )
width = kw.pop('width', master.winfo_width()) # 250x250 will be the standard size of the window
height = kw.pop('height', master.winfo_height())
title = kw.pop('title', 'Win %s' %(len(self._info_pages)+1) )
super(Window, self).__init__(master=master, cnf=cnf, **kw)
for i in self._info_pages: i.wm_withdraw() # Hide the previous windows
if self._info_pages and width == master.winfo_width():
self.wm_geometry(self._info_pages[-1].winfo_geometry())
else:
self.wm_geometry("%dx%d+%d+%d" % (width, height,
master.winfo_rootx()+master.winfo_width(), master.winfo_rooty()))
self._info_pages.append(self)
self.title(title)
self.B1 = tk.Button(self, text='◀ Prev', padx=5, command=self.switch_to_prev)
self.B1.place(relx=0, rely=1, anchor='sw')
self.B2 = tk.Button(self, text='Next ▶', padx=5, command=self.switch_to_next)
self.B2.place(relx=1, rely=1, anchor='se')
self.enable_disable_button()
def enable_disable_button(self):
"""Enable and disable the buttons accordingly if there is no window."""
for i in self._info_pages:
if i == self._info_pages[0]: i.B1['state'] = 'disabled'
else: i.B1['state'] = 'normal'
if i == self._info_pages[-1]: i.B2['state'] = 'disabled'
else: i.B2['state'] = 'normal'
def switch_to_prev(self):
"""Switch to the previous window"""
index = self._info_pages.index(self)
if index != 0:
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index-1].geometry(self.winfo_geometry())
self._info_pages[index-1].wm_deiconify()
def switch_to_next(self):
"""Switch to the next window"""
index = self._info_pages.index(self)
if index+1 != len(self._info_pages):
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index+1].geometry(self.winfo_geometry())
self._info_pages[index+1].wm_deiconify()
def destroy(self):
"""if a window is destroyed this will open the last window in the list"""
self._info_pages.remove(self)
if self._info_pages:
self._info_pages[-1].geometry(self.winfo_geometry())
self._info_pages[-1].wm_deiconify()
self.enable_disable_button()
return super().destroy()
# This is just a demo
if __name__ == '__main__':
import random as rnd
root = tk.Tk()
root.geometry('250x250')
root.title("I'm the main window")
colorlist = ['beige','bisque','black','blanchedalmond','blue','blueviolet',
'burlywood', 'cadetblue','chartreuse','chocolate' ]
def create_window():
Window(root, bg=rnd.choice(colorlist))
tk.Button(root, text='Create Window', command=create_window).pack()
root.mainloop()

Display message when hovering over something with mouse cursor in Python

I have a GUI made with TKinter in Python. I would like to be able to display a message when my mouse cursor goes, for example, on top of a label or button. The purpose of this is to explain to the user what the button/label does or represents.
Is there a way to display text when hovering over a tkinter object in Python?
I think this would meet your requirements.
Here's what the output looks like:
First, A class named ToolTip which has methods showtip and hidetip is defined as follows:
from tkinter import *
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"Display text in tooltip window"
self.text = text
if self.tipwindow or not self.text:
return
x, y, cx, cy = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 57
y = y + cy + self.widget.winfo_rooty() +27
self.tipwindow = tw = Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
label = Label(tw, text=self.text, justify=LEFT,
background="#ffffe0", relief=SOLID, borderwidth=1,
font=("tahoma", "8", "normal"))
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
def CreateToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
The widget is where you want to add the tip. For example, if you want the tip when you hover over a button or entry or label, the instance of the same should be provided at the call time.
Quick note: the code above uses from tkinter import *
which is not suggested by some of the programmers out there, and they have valid points. You might want to make necessary changes in such case.
To move the tip to your desired location, you can change x and y in the code.
The function CreateToolTip() helps to create this tip easily. Just pass the widget and string you want to display in the tipbox to this function, and you're good to go.
This is how you call the above part:
button = Button(root, text = 'click mem')
button.pack()
CreateToolTip(button, text = 'Hello World\n'
'This is how tip looks like.'
'Best part is, it\'s not a menu.\n'
'Purely tipbox.')
Do not forget to import the module if you save the previous outline in different python file, and don't save the file as CreateToolTip or ToolTip to avoid confusion.
This post from Fuzzyman shares some similar thoughts, and worth checking out.
You need to set a binding on the <Enter> and <Leave> events.
Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.
For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.l1 = tk.Label(self, text="Hover over me")
self.l2 = tk.Label(self, text="", width=40)
self.l1.pack(side="top")
self.l2.pack(side="top", fill="x")
self.l1.bind("<Enter>", self.on_enter)
self.l1.bind("<Leave>", self.on_leave)
def on_enter(self, event):
self.l2.configure(text="Hello world")
def on_leave(self, enter):
self.l2.configure(text="")
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand="true")
root.mainloop()
You can refer to this- HoverClass
It is exactly what you need. Nothing more, nothing less
from Tkinter import *
import re
class HoverInfo(Menu):
def __init__(self, parent, text, command=None):
self._com = command
Menu.__init__(self,parent, tearoff=0)
if not isinstance(text, str):
raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
toktext=re.split('\n', text)
for t in toktext:
self.add_command(label = t)
self._displayed=False
self.master.bind("<Enter>",self.Display )
self.master.bind("<Leave>",self.Remove )
def __del__(self):
self.master.unbind("<Enter>")
self.master.unbind("<Leave>")
def Display(self,event):
if not self._displayed:
self._displayed=True
self.post(event.x_root, event.y_root)
if self._com != None:
self.master.unbind_all("<Return>")
self.master.bind_all("<Return>", self.Click)
def Remove(self, event):
if self._displayed:
self._displayed=False
self.unpost()
if self._com != None:
self.unbind_all("<Return>")
def Click(self, event):
self._com()
Example app using HoverInfo:
from Tkinter import *
from HoverInfo import HoverInfo
class MyApp(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.grid()
self.lbl = Label(self, text='testing')
self.lbl.grid()
self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)
def HelloWorld(self):
print('Hello World')
app = MyApp()
app.master.title('test')
app.mainloop()
Screenshot:
I have a very hacky solution but it has some advantages over the current answers so I figured I would share it.
lab=Label(root,text="hover me")
lab.bind("<Enter>",popup)
def do_popup(event):
# display the popup menu
root.after(1000, self.check)
popup = Menu(root, tearoff=0)
popup.add_command(label="Next")
popup.tk_popup(event.x_root, event.y_root, 0)
def check(event=None):
x, y = root.winfo_pointerxy()
widget = root.winfo_containing(x, y)
if widget is None:
root.after(100, root.check)
else:
leave()
def leave():
popup.delete(0, END)
The only real issue with this is it leaves behind a small box that moves focus away from the main window
If anyone knows how to solve these issues let me know
If anyone is on Mac OSX and tool tip isn't working, check out the example in:
https://github.com/python/cpython/blob/master/Lib/idlelib/tooltip.py
Basically, the two lines that made it work for me on Mac OSX were:
tw.update_idletasks() # Needed on MacOS -- see #34275.
tw.lift() # work around bug in Tk 8.5.18+ (issue #24570)
Here is a simple solution to your problem that subclasses the tk.Button object. We make a special class that tk.Button inherits from, giving it tooltip functionality. The same for tk.Labels.
I don't know what would be cleanest and the easiest way to maintain code for keeping track of the text that goes into the tooltips. I present here one way, in which I pass unique widget IDs to MyButtons, and access a dictionary for storing the tooltip texts. You could store this file as a JSON, or as a class attribute, or as a global variable (as below). Alternatively, perhaps it would be better to define a setter method in MyButton, and just call this method every time you create a new widget that should have a tooltip. Although you would have to store the widget instance in a variable, adding one extra line for all widgets to include.
One drawback in the code below is that the self.master.master syntax relies on determining the "widget depth". A simple recursive function will catch most cases (only needed for entering a widget, since by definition you leave somewhere you once were).
Anyway, below is a working MWE for anyone interested.
import tkinter as tk
tooltips = {
'button_hello': 'Print a greeting message',
'button_quit': 'Quit the program',
'button_insult': 'Print an insult',
'idle': 'Hover over button for help',
'error': 'Widget ID not valid'
}
class ToolTipFunctionality:
def __init__(self, wid):
self.wid = wid
self.widet_depth = 1
self.widget_search_depth = 10
self.bind('<Enter>', lambda event, i=1: self.on_enter(event, i))
self.bind('<Leave>', lambda event: self.on_leave(event))
def on_enter(self, event, i):
if i > self.widget_search_depth:
return
try:
cmd = f'self{".master"*i}.show_tooltip(self.wid)'
eval(cmd)
self.widget_depth = i
except AttributeError:
return self.on_enter(event, i+1)
def on_leave(self, event):
cmd = f'self{".master" * self.widget_depth}.hide_tooltip()'
eval(cmd)
class MyButton(tk.Button, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Button.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class MyLabel(tk.Label, ToolTipFunctionality):
def __init__(self, parent, wid, **kwargs):
tk.Label.__init__(self, parent, **kwargs)
ToolTipFunctionality.__init__(self, wid)
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.tooltip = tk.StringVar()
self.tooltip.set(tooltips['idle'])
self.frame = tk.Frame(self, width=50)
self.frame.pack(expand=True)
MyLabel(self.frame, '', text='One Cool Program').pack()
self.subframe = tk.Frame(self.frame, width=40)
self.subframe.pack()
MyButton(self.subframe, 'button_hello', text='Hello!', command=self.greet, width=20).pack()
MyButton(self.subframe, 'button_insutl', text='Insult', command=self.insult, width=20).pack()
MyButton(self.subframe, 'button_quit', text='Quit', command=self.destroy, width=20).pack()
tk.Label(self.subframe, textvar=self.tooltip, width=20).pack()
def show_tooltip(self, wid):
try:
self.tooltip.set(tooltips[wid])
except KeyError:
self.tooltip.set(tooltips['error'])
def hide_tooltip(self):
self.tooltip.set(tooltips['idle'])
def greet(self):
print('Welcome, Fine Sir!')
def insult(self):
print('You must be dead from the neck up')
if __name__ == '__main__':
app = Application()
app.mainloop()
The best way I have found to create a popup help window is to use the tix.Balloon. I have modified it below to make it look better and show an example (note the use of tix.Tk):
import tkinter as tk
import tkinter.tix as tix
class Balloon(tix.Balloon):
# A modified tix popup balloon (to change the default delay, bg and wraplength)
init_after = 1250 # Milliseconds
wraplength = 300 # Pixels
def __init__(self, master):
bg = root.cget("bg")
# Call the parent
super().__init__(master, initwait=self.init_after)
# Change background colour
for i in self.subwidgets_all():
i.config(bg=bg)
# Modify the balloon label
self.message.config(wraplength=self.wraplength)
root = tix.Tk()
l = tk.Label(root, text="\n".join(["text"] * 5))
l.pack()
b = Balloon(root.winfo_toplevel())
b.bind_widget(l, balloonmsg="Some random text")
root.mainloop()
OLD ANSWER:
Here is an example using <enter> and <leave> as #bryanoakley suggested with a toplevel (with overridedirect set to true). Use the hover_timer class for easy use of this. This needs the widget and help-text (with an optional delay argument - default 0.5s) and can be easily called just by initiating the class and then cancelling it.
import threading, time
from tkinter import *
class hover_window (Toplevel):
def __init__ (self, coords, text):
super ().__init__ ()
self.geometry ("+%d+%d" % (coords [0], coords [1]))
self.config (bg = "white")
Label (self, text = text, bg = "white", relief = "ridge", borderwidth = 3, wraplength = 400, justify = "left").grid ()
self.overrideredirect (True)
self.update ()
self.bind ("<Enter>", lambda event: self.destroy ())
class hover_timer:
def __init__ (self, widget, text, delay = 2):
self.wind, self.cancel_var, self.widget, self.text, self.active, self.delay = None, False, widget, text, False, delay
threading.Thread (target = self.start_timer).start ()
def start_timer (self):
self.active = True
time.sleep (self.delay)
if not self.cancel_var: self.wind = hover_window ((self.widget.winfo_rootx (), self.widget.winfo_rooty () + self.widget.winfo_height () + 20), self.text)
self.active = False
def delayed_stop (self):
while self.active: time.sleep (0.05)
if self.wind:
self.wind.destroy ()
self.wind = None
def cancel (self):
self.cancel_var = True
if not self.wind: threading.Thread (target = self.delayed_stop).start ()
else:
self.wind.destroy ()
self.wind = None
def start_help (event):
# Create a new help timer
global h
h = hover_timer (l, "This is some additional information.", 0.5)
def end_help (event):
# If therre is one, end the help timer
if h: h.cancel ()
if __name__ == "__main__":
# Create the tkinter window
root = Tk ()
root.title ("Hover example")
# Help class not created yet
h = None
# Padding round label
Frame (root, width = 50).grid (row = 1, column = 0)
Frame (root, height = 50).grid (row = 0, column = 1)
Frame (root, width = 50).grid (row = 1, column = 2)
Frame (root, height = 50).grid (row = 2, column = 1)
# Setup the label
l = Label (root, text = "Hover over me for information.", font = ("sans", 32))
l.grid (row = 1, column = 1)
l.bind ("<Enter>", start_help)
l.bind ("<Leave>", end_help)
# Tkinter mainloop
root.mainloop ()
I wanted to contribute to the answer of #squareRoot17 as he inspired me to shorten his code while providing the same functionality:
import tkinter as tk
class ToolTip(object):
def __init__(self, widget, text):
self.widget = widget
self.text = text
def enter(event):
self.showTooltip()
def leave(event):
self.hideTooltip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
def showTooltip(self):
self.tooltipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1) # window without border and no normal means of closing
tw.wm_geometry("+{}+{}".format(self.widget.winfo_rootx(), self.widget.winfo_rooty()))
label = tk.Label(tw, text = self.text, background = "#ffffe0", relief = 'solid', borderwidth = 1).pack()
def hideTooltip(self):
tw = self.tooltipwindow
tw.destroy()
self.tooltipwindow = None
This class can then be imported and used as:
import tkinter as tk
from tooltip import ToolTip
root = tk.Tk()
your_widget = tk.Button(root, text = "Hover me!")
ToolTip(widget = your_widget, text = "Hover text!")
root.mainloop()

Categories

Resources