Strange visual bug happening on QFocusEvents in my child QLineEdit - python

I'm making this class to make my QEditLines to have a default value, that will work as a label for the line, it seems to be working, but when changing the focus with 'Tab' the line will add a '|' at the end of the line and keep it until restarting and it's just visual, it doesn't change the Entry's value.
My child class of QLineEdit:
class Entry(QtWidgets.QLineEdit):
def __init__(self, frame: QtWidgets.QFrame):
super(Entry, self).__init__(frame)
self.default_text = ''
#QtCore.pyqtSlot(QtGui.QFocusEvent)
def focusInEvent(self, a0: QtGui.QFocusEvent) -> None:
if self.text() == self.default_text:
self.clear()
if 'PASSWORD' in self.default_text:
self.setEchoMode(self.Password)
#QtCore.pyqtSlot(QtGui.QFocusEvent)
def focusOutEvent(self, a0: QtGui.QFocusEvent) -> None:
if self.text() == "":
self.setText(self.default_text)
if 'PASSWORD' in self.default_text:
self.setEchoMode(self.Normal)
can be reproducible with a simple GUI with the code below:
def print_value(entry: Entry, entry2: Entry):
print(entry.text())
print(entry2.text())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
root = QtWidgets.QMainWindow()
root.resize(500,500)
frame = QtWidgets.QWidget(root)
frame.resize(500,500)
entry = Entry(frame)
entry.setGeometry(QtCore.QRect(0, 0, 100, 50))
entry.setText('PASSWORD')
entry.default_text = 'PASSWORD'
entry2 = Entry(frame)
entry2.setGeometry(QtCore.QRect(0, 100, 100, 50))
entry2.setText('USERNAME')
entry2.default_text = 'USERNAME'
button = QtWidgets.QPushButton(frame)
button.setGeometry(QtCore.QRect(0, 200, 50, 50))
button.pressed.connect(lambda: print_value(entry, entry2))
root.show()
sys.exit(app.exec_())
In the terminal is the output of the values in the Entry objects, Password, and USERNAME QLineEdits. [Screenshot]

The default implementation of focusInEvent and focusOutEvent has the task of enabling and disabling the cursor, but by override those methods you are eliminating that behavior. One possible solution is to also invoke the default implementation:
class Entry(QtWidgets.QLineEdit):
def __init__(self, frame: QtWidgets.QFrame):
super(Entry, self).__init__(frame)
self.default_text = ""
def focusInEvent(self, a0: QtGui.QFocusEvent) -> None:
if self.text() == self.default_text:
self.clear()
if "PASSWORD" in self.default_text:
self.setEchoMode(self.Password)
super().focusInEvent(a0)
def focusOutEvent(self, a0: QtGui.QFocusEvent) -> None:
if self.text() == "":
self.setText(self.default_text)
if "PASSWORD" in self.default_text:
self.setEchoMode(self.Normal)
super().focusOutEvent(a0)

Related

Changing one scale widget will change the secone one

I have two scale widgets in my code that are supposed to display values from a list. I want to be able to set individual values for both sliders. For example, for file 1 "2.4" and for file 2 "5.025". But currently I can only update the second (lower) slider without it triggering the first (upper) one. If I use the upper one, the value of the second one is also changed.
I'm stuck implementing the lambda function, so it would be create if some could give me a hint.
Thanks!
import tkinter as tk
class File_Selection():
def __init__(self, frame, text):
self.frame = frame
self.text = text
self.label_file = tk.Label(self.frame, text=text)
self.label_file.pack(side="left", anchor="s")
self.label_test = tk.Label(self.frame, text="| Select value: ")
self.label_test.pack(side="left", anchor="s")
self.scale = tk.Scale(self.frame, orient=tk.HORIZONTAL, from_=0)
self.scale.pack(side="left", anchor="s")
class View:
def __init__(self, view):
self.view = view
self.frame = tk.Frame(self.view)
self.frame.pack()
self.frame_row1 = tk.Frame(self.frame)
self.frame_row1.pack(side="top")
self.frame_row2 = tk.Frame(self.frame)
self.frame_row2.pack(side="top")
self.file_one = File_Selection(self.frame_row1, "File 1")
self.file_two = File_Selection(self.frame_row2, "File 2")
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
def run(self):
self.root.mainloop()
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)
if __name__ == "__main__":
c = Controller()
c.run()
The problem is that you have both scales set to active_scales. You can take advantage of the event and do away with active_scales.
You can get the widget associated with an event with event.widget. So, you can change the update_scales function from
def update_scale(self, event):
self.active_scales = [self.view.file_one.scale, self.view.file_two.scale]
for scale in self.active_scales:
scale.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=scale: self.set_scale(value, scale))
to
def update_scale(self, event):
event.widget.config(from_=min(self.values), to=max(self.values), resolution=0.001, command=lambda value=event.widget: self.set_scale(value, event.widget))
Additionally, since self.values don't appear to change, it does not seem necessary to config each scale to change its bounds and resolution on each event. You can instead pull that out of the event and make you Controller class as follows.
class Controller:
def __init__(self):
self.root = tk.Tk()
self.view = View(self.root)
self.values = [1.01,2.4,3.6,4.89,5.025,6.547]
self.view.file_one.scale.bind('<Enter>', self.update_scale)
self.view.file_two.scale.bind('<Enter>', self.update_scale)
self.view.file_one.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
self.view.file_two.scale.config(from_=min(self.values), to=max(self.values), resolution=0.001)
def run(self):
self.root.mainloop()
def update_scale(self, event):
event.widget.config(command=lambda value=event.widget: self.set_scale(value, event.widget))
def set_scale(self, value, scale):
self.newvalue = min(self.values, key=lambda x: abs(x-float(value)))
scale.set(self.newvalue)

Associate idle keyboard events to widget text tkinter

I am trying to create my own python code editor. For that I tried to use the tkcode module. Tkcode partially covers my needs (colors in the text for example), but it does not have the keyboard events that Idle has (automatic indentation when pressing enter, when pressing tab puts 4 spaces, etc). Some of them I can try to recreate, but automatic indentation is difficult. Is there any way to associate the Idle events to my code editor using the idlelib without creating another window (since I am making a notebook)? I went through the source code of Idle and couldn't find the way.
I know that this site is not to ask for recommendations, but if you recommend a better module to create a text widget that allows me to create this editor, it would be great too.
This is the code I have made:
from tkcode import CodeEditor
from tkinter import ttk
import tkinter as tk
import re
class CodeEditor(CodeEditor):
def __init__(Self, *args, **kargs):
super().__init__(*args, **kargs)
Self.bind("<Return>", Self.enter)
def get_current_line(Self):
return Self.get("insert linestart", "insert lineend")
def enter(Self, event):
for index, char in enumerate(Self.get_current_line()):
if(char != " "):
break
else:
index += 1
Self.insert("insert", "\n")
Self.insert("insert", " "*index)
return "break"
class FileHandler:
def __init__(Self, progs_path, filetabs):
Self.files = {}
Self.progs_path = progs_path
Self.filetabs = filetabs
def askopen(Self):
v = tk.Toplevel()
v.transient()
v.resizable(0, 0)
prog = ttk.Entry(v)
prog.pack(padx=10, pady=10)
prog.bind("<Return>", lambda Event:(Self.open(prog.get()), v.destroy()))
def open(Self, prog):
progfile = str(prog)[0]
progfile = f"prog{progfile}00A{progfile}99.py"
if(progfile in Self.files):
text = Self.files[progfile][prog]
else:
functions = {}
name = None
with open(f"{Self.progs_path}/{progfile}") as file:
for line in file:
match = re.match("(def|class) prog(\w+)", line)
if(match):
name = match[2]
functions[name] = line
if(line.startswith(" ") and name):
functions[name] += line
Self.files[progfile] = functions
text = functions[prog]
frame = ttk.Frame(Self.filetabs)
code_editor = CodeEditor(frame, language="python", highlighter="mariana", font="TkFixedFont", autofocus=True, padx=10, pady=10)
code_editor.pack(fill="both", expand=True)
code_editor.content = text
Self.filetabs.add(frame, text="prog"+prog)
v = tk.Tk()
filetabs = ttk.Notebook()
fh = FileHandler(".", filetabs)
menubar = tk.Menu(tearoff=0)
file = tk.Menu(tearoff=0)
menubar.add_cascade(label="Archivo", menu=file)
file.add_command(label="Abrir prog", command=fh.askopen)
v["menu"] = menubar
filetabs.pack(fill="both", expand=True)
fh.open("833")
In the end I was able to fix it. I was able to recreate almost all but one of the functions. For the last one, I made an impersonator class for idlelib.editor.EditorWindow to be able to do the automatic indentation, in addition to adding two new functions to the tkcode.CodeEditor.
However, I find this solution to be very unstable. Any better answer is still appreciated c:
This is the code:
from tkcode import CodeEditor
from tkinter import ttk
from idlelib.editor import EditorWindow
import tkinter as tk
import re
class FakeEditorWindow(EditorWindow):
def __init__(Self, text):
Self.text = text
Self.indentwidth = 4
Self.tabwidth = 4
Self.prompt_last_line = ''
Self.num_context_lines = 50, 500, 5000000
Self.usetabs = False
def is_char_in_string(Self, text_index):
return 1
class CodeEditor(CodeEditor):
def __init__(Self, *args, **kargs):
super().__init__(*args, **kargs)
Self.fake_editor_window = FakeEditorWindow(Self)
Self.bind("<Tab>", Self.tab)
Self.bind("<Control-Shift_L>", Self.dedent)
Self.bind("<BackSpace>", Self.backspace)
Self.bind("<Home>", Self.inicio)
Self.bind("<Return>", Self.enter)
def undo_block_start(Self):
pass
def undo_block_stop(Self):
pass
def get_current_line(Self):
return Self.get("insert linestart", "insert lineend")
def selection_get(Self):
if(Self.tag_ranges("sel")):
return Self.get("sel.first", "sel.last")
else:
return ""
def get_selection_zone(Self):
return (map(int, Self.index('sel.first').split(".", 1)),
map(int, Self.index('sel.last').split(".", 1)))
def tab(Self, event):
selection = Self.selection_get()
if(selection):
(startline, startcolumn), (endline, endcolumn) = Self.get_selection_zone()
if(startcolumn == 0):
for line in range(startline, endline+1):
Self.insert(f"{line}.0", " "*4)
Self.tag_add("sel", f"{startline}.0", "sel.last")
Self.mark_set("insert", f"{endline+1}.0")
else:
Self.insert("insert", " "*4)
return "break"
def dedent(Self, event):
if(Self.tag_ranges("sel")):
(startline, startcolumn), (endline, endcolumn) = Self.get_selection_zone()
if(startcolumn == 0):
for line in range(startline, endline+1):
if(Self.get(f"{line}.0", f"{line}.4") == " "*4):
Self.delete(f"{line}.0", f"{line}.4")
def backspace(Self, event):
if(not Self.tag_ranges("sel") and Self.get("insert linestart", "insert").isspace()):
cursor_line, cursor_col = map(int, Self.index("insert").split(".", 1))
Self.delete(f"{cursor_line}.{cursor_col-4}", "insert")
return "break"
def inicio(Self, event):
cursor_line, cursor_column = map(int, Self.index('insert').split(".", 1))
if(not Self.get("insert linestart", f"{cursor_line}.{cursor_column}").isspace()):
for i in range(cursor_column, -1, -1):
if(Self.get("insert linestart", f"{cursor_line}.{i}").isspace()):
Self.mark_set("insert", f"{cursor_line}.{i}")
return "break"
def enter(Self, event):
return EditorWindow.newline_and_indent_event(Self.fake_editor_window, event)
class FileHandler:
def __init__(Self, progs_path, filetabs):
Self.files = {}
Self.progs_path = progs_path
Self.filetabs = filetabs
def askopen(Self):
v = tk.Toplevel()
v.transient()
v.resizable(0, 0)
prog = ttk.Entry(v)
prog.pack(padx=10, pady=10)
prog.bind("<Return>", lambda Event:(Self.open(prog.get()), v.destroy()))
def open(Self, prog):
progfile = str(prog)[0]
progfile = f"prog{progfile}00A{progfile}99.py"
if(progfile in Self.files):
text = Self.files[progfile][prog]
else:
functions = {}
name = None
with open(f"{Self.progs_path}/{progfile}") as file:
for line in file:
match = re.match("(def|class) prog(\w+)", line)
if(match):
name = match[2]
functions[name] = line
if(line.startswith(" ") and name):
functions[name] += line
Self.files[progfile] = functions
text = functions[prog]
frame = ttk.Frame(Self.filetabs)
code_editor = CodeEditor(frame, language="python", highlighter="mariana", font="TkFixedFont", autofocus=True, padx=10, pady=10)
code_editor.pack(fill="both", expand=True)
code_editor.content = text
Self.filetabs.add(frame, text="prog"+prog)
v = tk.Tk()
filetabs = ttk.Notebook()
fh = FileHandler(".", filetabs)
menubar = tk.Menu(tearoff=0)
file = tk.Menu(tearoff=0)
menubar.add_cascade(label="Archivo", menu=file)
file.add_command(label="Abrir prog", command=fh.askopen)
v["menu"] = menubar
filetabs.pack(fill="both", expand=True)
fh.open("833")

tkinter event handler reference lost on widgets

I put a event handler in a class Verification in a separated file verification.py (tried to make the code modular). The main GUI code is as follows,
from tkinter import *
from make_widget import *
from verification import *
class LoginFrame(Frame):
def __init__(self, parent):
super(LoginFrame, self).__init__()
self.parent = parent
self.initUI()
# initialize the login screen UI
def initUI(self):
# Set up login frame properties
self.parent.title("Login Screen")
# creating instruction label
self.inst_lbl = MakeWidget.make_label(self.parent, "Please login to continue")
# creating labels and entries for user name and password
self.user_name = MakeWidget.make_entry(self.parent, caption="User Name:")
self.pwd = MakeWidget.make_entry(self.parent, caption="User Password:", show="*")
# create a login button
login_btn = MakeWidget.make_button(self.parent, Verification.verify_user(self.user_name, self.pwd, self.inst_lbl), "Login")
def main():
top = Tk()
app = LoginFrame(top)
top.mainloop()
if __name__ == '__main__':
main()
# verification.py
from tkinter import *
class Verification:
# verify user name and password
#----------------------------------------------------------------------
#staticmethod
def verify_user(user_name, pwd, inst_lbl):
"""verify users"""
if user_name.get() == "admin" and pwd.get() == "123":
inst_lbl.configure(text="User verified")
else:
inst_lbl.configure(text="Access denied. Invalid username or password")
# make_widget.py
from tkinter import *
class MakeWidget(Frame):
def __init__(self, parent):
super(MakeWidget, self).__init__()
self.parent = parent
# create a button widget
#----------------------------------------------------------------------
#staticmethod
def make_button(parent, command, caption=NONE, side=TOP, width=0, **options):
"""make a button"""
btn = Button(parent, text=caption, command=command)
if side is not TOP:
btn.pack(side=side)
else:
btn.pack()
return btn
# create a label widget
#staticmethod
def make_label(parent, caption=NONE, side=TOP, **options):
label = Label(parent, text=caption, **options)
if side is not TOP:
label.pack(side=side)
else:
label.pack()
return label
# create a entry widget
#staticmethod
def make_entry(parent, caption=NONE, side=TOP, width=0, **options):
MakeWidget.make_label(parent, caption, side)
entry = Entry(parent, **options)
if width:
entry.config(width=width)
if side is not TOP:
entry.pack(side=side)
else:
entry.pack()
return entry
Now, I expected the inst_lbl in LoginFrame to be able to configure and display the new text based on the user_name and pwd, but inst_lbl didn't change the text (no error was generated). So how to resolve the issue.
The problem is this line:
login_btn = MakeWidget.make_button(self.parent, Verification.verify_user(self.user_name, self.pwd, self.inst_lbl), "Login")
You are immediately calling Verification.verify_user and assigning the result so the button command. You must pass in a reference to a function, not the result of a function.
See this answer to a similar question: https://stackoverflow.com/a/5771787/7432

How to disable the close button in GTK?

I have created a One Time Password mechanism in OpenERP 6.0.3's GTK client. After login the GTK client shows a window to enter the One Time Password as below.
Now I want to disable the close button at the top left of the window. How can I do that? I am using python and the code to create the window is:
EDIT
class sms_auth(gtk.Dialog):
def run_thread(self):
code=self.textbox_code.get_text()
self.result = rpc.session.rpc_exec_auth('/object', 'execute', 'res.users', 'check_code', code)
return self.result
def run(self):
self.show_all()
res = super(sms_auth, self).run()
result = None
if res == gtk.RESPONSE_ACCEPT:
result = self.run_thread()
self.destroy()
return result
def hide(*args):
window.hide()
return gtk.TRUE
def __init__(self, parent, response):
# To use cancel butto add gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.Dialog.__init__(
self, 'Sms Authentication', parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
)
label = gtk.Label("Please enter sms code :")
self.parent_widget = parent
self.response = False
self.db_login_response = response
self.connect('delete_event', hide)
self.textbox_code = gtk.Entry()
label.set_alignment(0,0)
table = gtk.Table(1, 7)
table.set_homogeneous(False)
table.set_col_spacings(40)
table.attach(label, 0, 6, 0, 1, ypadding=4)
table.attach(self.textbox_code, 5, 6, 0, 1, ypadding=4)
self.vbox.pack_start(table,False, False, 0)
Try like this
def hide(self, *args):
window.hide()
return gtk.TRUE
self.window.connect('delete_event', self.hide)
Note: Refer here
import pygtk
pygtk.require('2.0')
import gtk
class DialogExample(gtk.Dialog):
def __init__(self, parent=None):
gtk.Dialog.__init__(self, "My Dialog", parent,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
)
self.set_default_size(150, 100)
label = gtk.Label("This is a dialog to display additional information")
box = self.get_content_area()
box.add(label)
self.show_all()
self.connect('delete-event', self.delete_event)
def delete_event(self, widget, event=None):
print "Here"
return True
def main():
# rest in gtk_main and wait for the fun to begin!
gtk.main()
return 0
if __name__ == "__main__":
DialogExample()
main()

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