tkinter: How to get a subclass of Label to show on screen? - python

I'm trying to do something with classes in Python (I come from a procedural languages background). Trying to create a version of tkinter's Label widget supporting a couple of new methods to manipulate the text of the label.
My problem is that I can't get the label to actually be visible on the screen.
Here's the code:
from tkinter import *
DEFAULT_BG = '#f0f0f0'
class cngMsg(Label):
"""Message Display label"""
def __init__(self, parent, w, h):
"""Init the Message Label"""
self.parent = parent
Label.__init__(self, parent)
self.msgText = "Hello World"
self.msgLabel = Label(parent, text=self.msgText)
self.msgLabel.config(height=h, width=w, bg=DEFAULT_BG)
def clear(self):
self.msgText = ""
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
def newMessage(self, message):
print("about to display <" + message + ">")
self.msgText = message
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
def show(self, message, sameLine=None):
if (not sameLine) and len(self.msgText) > 0:
self.msgText += '/n'
print("about to show: <" + message + ">")
self.msgText = self.msgText + message
print(len(self.msgText))
self.msgLabel.config(text=self.msgText)
#Root Stuff
if __name__ == "__main__":
app = Tk()
app.title("Message Test")
# this is the start of the application
print("initialise the Message Test")
gMsg = cngMsg(app, 60, 20)
gMsg.pack()
gMsg.newMessage("new message")
gMsg.show("this is a test")
gMsg.show("second test")
app.mainloop()
The debug print messages appear on the console but the application window doesn't display the Label.

GUI programming requires using a non-procedural paradigm since they are user-input driven. The question Tkinter — executing functions over time discusses this and has some sample code.
Personally I have often found it useful when creating GUI apps to think of them as FSMs (Finite State Machines) in which user inputs cause them to change their state.
Here's how to do something similar to what I think you were trying to in your sample code which is based on the #Bryan Oakley's answer to the linked question (updated to Python 3). It also shows the proper way to subclass tkinter classes. In addition it mostly follows the PEP 8 - Style Guide for Python Code guideline, which I strongly suggest your read and start following.
from tkinter import *
DEFAULT_BG = '#f0f0f0'
DEFAULT_MSG_TEXT = "Hello World"
DELAY = 1000 # Milliseconds.
class CngMsg(Label):
"""Message Display label"""
def __init__(self, parent, w, h):
# Intialize with default text and background color.
super().__init__(parent, text=DEFAULT_MSG_TEXT, height=h, width=w, bg=DEFAULT_BG)
def clear(self):
self.config(text='')
def newMessage(self, message):
self.config(text=message)
def show(self, message, same_line=False):
text = self.cget('text') # Get value of current option.
if not same_line and text:
text += '\n'
text += message
self.config(text=text)
class MyApp(Tk):
def __init__(self):
super().__init__()
self.frame = Frame(self)
self.frame.pack()
self.test_msg = CngMsg(self.frame, 60, 20)
self.test_msg.pack()
self.state = 0
self.do_test()
def do_test(self):
if self.state == 0:
self.test_msg.newMessage("start message")
self.state = 1
elif self.state == 1:
self.test_msg.show("this is a test")
self.state = 2
elif self.state == 2:
self.test_msg.show("second test")
self.state = 3
elif self.state == 3:
self.test_msg.clear()
self.test_msg.show("TEST COMPLETED")
self.state = -1 # Enter final state.
elif self.state != -1:
self.quit() # Stop mainloop.
raise RuntimeError("Unknown state encountered")
if self.state != -1: # Not final state?
self.after(DELAY, self.do_test) # Schedule another call.
if __name__ == "__main__":
root = MyApp()
root.title("Message Test")
root.mainloop()

Your cngMsg class is creating two labels. The first is the instance of that class itself. The second is created when you do self.msgLabel = Label(parent, text=self.msgText). This second label is a child of the cngMsg label. Since you do not call pack, place, or grid on that second label it will not appear anywhere.
In your newMessage method you are updating the text in the invisible label instead of the actual label.
You don't need this second label, and in newMessage you should configure itself like this:
def newMessage(self, message):
self.msgText = msgText
self.configure(text=self.msgText)
Similarly, show and clear should be defined in a similar way:
def clear(self):
self.msgText = ""
self.config(text=self.msgText)
def show(self, message, sameLine=None):
if (not sameLine) and len(self.msgText) > 0:
self.msgText += '/n'
self.msgText = self.msgText + message
self.config(text=self.msgText)

Related

Tkinter RSS Ticker accelerating at every update

There's no help on google, I've asked some people as well but none of them seem to know how to answer my question.
I'm programming a GUI for a project, and it contains an RSS-Feed ticker.
It scrolls through the news and when it updates (every 3 seconds for obvious debug reasons) it speeds up a bit.
This means, if I run the program, after two hours the ticker is scrolling at a non-human readable speed.
The main code wasn't written by me, I modified it and added the update function.
main():
import tkinter as tk
from Press import RSSTicker
def displayRSSticker(win):
# Place RSSTicker portlet
tickerHandle = RSSTicker(win, bg='black', fg='white', highlightthickness=0, font=("avenir", 30))
tickerHandle.pack(side='bottom', fill='x', anchor='se')
def main():
# Set the screen definition, root window initialization
root = tk.Tk()
root.configure(background='black')
width, height = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (width, height))
label = tk.Label(root, text="Monitor Dashboard", bg='black', fg='red')
label.pack(side='bottom', fill='x', anchor='se')
# Display portlet
displayRSSticker(root)
# Loop the GUI manager
root.mainloop(0)
###############################
# MAIN SCRIPT BODY PART #
###############################
if __name__ == "__main__":
main()
RSSTicker class:
import feedparser
import tkinter as tk
class RSSTicker(tk.Text):
# Class constructor
def __init__(self, parent, **params):
super().__init__(parent, height=1, wrap="none", state='disabled', **params)
self.newsFeed = feedparser.parse('http://www.repubblica.it/rss/homepage/rss2.0.xml')
self.update()
# Class methods
def update(self):
self.headlineIndex = 0
self.text = ''
self.pos = 0
self.after_idle(self.updateHeadline)
self.after_idle(self.scroll)
self.after(4000, self.update)
def updateHeadline(self):
try:
self.text += ' ' + self.newsFeed['entries'][self.headlineIndex]['title']
except IndexError:
self.headlineIndex = 0
self.text = self.feed['entries'][self.headlineIndex]['title']
self.headlineIndex += 1
self.after(5000, self.updateHeadline)
def scroll(self):
self.config(state='normal')
if self.pos < len(self.text):
self.insert('end', self.text[self.pos])
self.pos += 1
self.see('end')
self.config(state='disabled')
self.after(180, self.scroll)
I thought the problem lied in the self.pos variable, printing it out resulted in it counting up, resetting to 1 and counting up faster.. But it doesn't seem to be problem causing the acceleration of the ticker.
From what I've understood tho, the problem must be in the scroll method.
If someone understand how to keep the original scroll speed when updated, thank you.
I think you can use a couple tracking variables to make sure that update only starts the loop once and then next time update is called it will just run scroll without starting a new loop. At the same time if scroll is not called by update then it will continue looping as needed.
Change your RSSTicker class to the following:
class RSSTicker(tk.Text):
# Class constructor
def __init__(self, parent, **params):
self.scroll_started = False # Tracker for first update.
super().__init__(parent, height=1, wrap="none", state='disabled', **params)
self.newsFeed = feedparser.parse('http://www.repubblica.it/rss/homepage/rss2.0.xml')
self.update()
def update(self):
self.headlineIndex = 0
self.text = ''
self.pos = 0
self.after_idle(self.updateHeadline)
self.after_idle(lambda: self.scroll('update'))
self.after(4000, self.update)
def updateHeadline(self):
try:
self.text += ' ' + self.newsFeed['entries'][self.headlineIndex]['title']
except IndexError:
self.headlineIndex = 0
self.text = self.feed['entries'][self.headlineIndex]['title']
self.headlineIndex += 1
self.after(5000, self.updateHeadline)
def scroll(self, from_after_or_update = 'after'):
self.config(state='normal')
if self.pos < len(self.text):
self.insert('end', self.text[self.pos])
self.pos += 1
self.see('end')
self.config(state='disabled')
# Check if the loop started by after.
if from_after_or_update != 'update':
self.scroll_started = True
self.after(180, self.scroll)
# If not started by after check to see if it is the 1st time loop is started by "update".
elif self.scroll_started is False and from_after_or_update == 'update':
self.scroll_started = True
self.after(180, self.scroll)
# If neither of the above conditions then do not start loop to prevent multiple loops.
else:
print("ran scroll method without adding new after loop!")

TypeError: destroy() missing 1 required positional argument: 'event'

Hi I keep getting the error above at tkinter.
I kept searching in the __init__.py of tkinter and destroy doesn't seem to be getting 2 arguments (so no event is needed).
This is invoked when tkinter is trying to destroy a custom class that I have created which is a subclass of Frame.
Before I added this class to the main window, the root.destroy() ,where root in that case is the main window, was working fine.
After the addition of the custom class, it destroys only a part of the custom class and then (when probably tries to destroy the other part) it throws that error and does not destroy the root window.
The code for the custom class is:
class inputBox(tk.Frame):
def __init__(self, root, parentWindow, attrName, label, valueType, query=None ):
super().__init__(root)
self.attrName = attrName
self.label = tk.Label(self,text=label)
self.valueType = valueType
if 'enum' in valueType:
values = valueType.replace("enum(","").replace(")","").split(",")
elif 'varchar' in valueType:
maxChars = int(valueType.replace("varchar(","").replace(")",""))
if query:
self.query=query
success,result = DAO().executeQuery(query,'select')
self.autoCompletedEntry = AutocompletedEntry(self, result, parentWindow, listboxLength=6, width = maxChars)
self.label.pack()
self.autoCompletedEntry.pack()
The code for the AutocompletedEntry (which is the class that probably creates the error) is:
class AutocompletedEntry(tk.Frame):
#rootsParent is needed only for the popup autocompletion list
def __init__(self, root, autocompleteList, rootsParent, *args, **kwargs):
super().__init__(root)
self.rootsParent = rootsParent
self.var = tk.StringVar(self)
self.var.trace("w", self.changed)#lambda name, index, mode, self.var=self.var: callback(self.var))
if 'width' in kwargs:
self.width = int(int(kwargs['width'])//3)
del kwargs['width']
if self.width >40:
self.width = 40
else:
self.width = 25
self.entry = tk.Entry(self, width=self.width, textvariable=self.var)
self.entry.pack(side='left',fill='both')
self.button = tk.Button(self, text='▼',command=lambda: self.changed('','','arrow'))
self.button.pack(side='right')
# Listbox length
if 'listboxLength' in kwargs:
self.listboxLength = kwargs['listboxLength']
del kwargs['listboxLength']
else:
self.listboxLength = 8
# Custom matches function
if 'matchesFunction' in kwargs:
self.matchesFunction = kwargs['matchesFunction']
del kwargs['matchesFunction']
else:
def matches(fieldValue, acListEntry):
pattern = re.compile(re.escape(fieldValue) + '.*', re.IGNORECASE)
return re.match(pattern, acListEntry)
self.matchesFunction = matches
self.focus()
self.autocompleteList = sorted(autocompleteList)
self.listboxUp = False
self.entry.bind("<Right>", self.selection)
self.entry.bind("<Return>", self.selection)
self.entry.bind("<Up>", self.moveUp)
self.entry.bind("<Down>", lambda e: self.moveDown(e))
self.entry.bind("<Escape>", self.destroy)
Any help would be much appreciated.
Since you using event to destroy you need parse event as parameter to the function.So i will suggest you create function and parse event as parameter for the window to be destroy
Example:
def destroy_root(self, event):
self.root.destroy
then you use it for your event "<Escape>" to able to destroy your window.

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()

Python Tkinter frame inside frame limitation or user error?

I've been building an app to track stock prices. The user should see a window with an entry widget and a button that creates a new frame with a label and a button. The label is the stock price and symbol, the button is a delete button, and should hide that frame if clicked.
I've re-written this program 4 times now, and it's been a great learning experience, but what I've learned is that I can't have the "mini-frames" being called from methods part of the main GUI class - this funks up the delete buttons, and updates the value behind frame.pack_forget() so it only deletes the last item ever.
I've moved my mini-frame widgets down into the class for the actual stock values. I've packed them (what I assume to be correct) but they don't show up. They also don't error out, which isn't very helpful. Here's my code, although I've omitted a lot of the functional parts to show what is happening with my frames. Keep in mind I need to keep it so that I can call my updater (self.update_stock_value) with a .after method against myapp.myContainer.
Is there a better way to do this?? Thanks in advance, my head hurts.
import re
import time
import urllib
from Tkinter import *
import threading
from thread import *
runningThreads = 0
# each object will be added to the gui parent frame
class MyApp(object):
def __init__(self, parent):
self.myParent = parent
self.myContainer = Canvas(parent)
self.myContainer.pack()
self.create_widgets()
# METHOD initiates basic GUI widgets
def create_widgets(self):
root.title("Stocker")
self.widgetFrame = Frame(self.myContainer)
self.widgetFrame.pack()
self.input = Entry(self.widgetFrame)
self.input.focus_set()
self.input.pack()
self.submitButton = Button(self.widgetFrame, command = self.onButtonClick)
self.submitButton.configure(text = "Add new stock")
self.submitButton.pack(fill = "x")
# METHOD called by each stock object
# returns the "symbol" in the entry widget
# clears the entry widget
def get_input_value(self):
var = self.input.get()
self.input.delete(0, END)
return var
# METHOD called when button is clicked
# starts new thread with instance of "Stock" class
def onButtonClick(self):
global runningThreads # shhhhhh im sorry just let it happen
runningThreads += 1 # count the threads open
threading.Thread(target = self.init_stock,).start() # force a tuple
if runningThreads == 1:
print runningThreads, "thread alive"
else:
print runningThreads, "threads alive"
def init_stock(self):
new = Stock()
class Stock(object):
def __init__(self):
# variable for the stock symbol
symb = self.stock_symbol()
# lets make a GUI
self.frame = Frame(myapp.myContainer)
self.frame.pack
# give the frame a label to update
self.testLabel = Label(self.frame)
self.testLabel.configure(text = self.update_stock_label(symb))
self.testLabel.pack(side = LEFT)
# create delete button to kill entire thread
self.killButton = Button(self.frame, command = self.kill_thread)
self.killButton.configure(text = "Delete")
self.killButton.pack(side = RIGHT)
# create stock label
# call updater
def kill_thread(self):
global runningThreads
runningThreads -= 1
self.stockFrame.pack_forget() # hide the frame
self.thread.exit() # kill the thread
def update_stock_label(self, symb):
self.testLabel.configure(text = str(symb) + str(get_quote(symb)))
myapp.myContainer.after(10000, self.update_stock_label(symb))
def stock_symbol(self):
symb = myapp.get_input_value()
print symb
# The most important part!
def get_quote(symbol):
try:
# go to google
base_url = "http://finance.google.com/finance?q="
# read the source code
content = urllib.urlopen(base_url + str(symbol)).read()
# set regex target
target = re.search('id="ref_\d*_l".*?>(.*?)<', content)
# if found, return.
if target:
print "found target"
quote = target.group(1)
print quote
else:
quote = "Not Found: "
return quote
# handling if no network connection
except IOError:
print "no network detected"
root = Tk()
root.geometry("280x200")
myapp = MyApp(root)
root.mainloop()
Your code won't run because of numerous errors, but this line is definitely not doing what you think it is doing:
self.frame.pack
For you to call the pack function you must include (), eg:
self.frame.pack()
You ask if your code is the best way to do this. I think you're on the right track, but I would change a few things. Here's how I would structure the code. This just creates the "miniframes", it doesn't do anything else:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.entry = tk.Entry(self)
self.submit = tk.Button(self, text="Submit", command=self.on_submit)
self.entry.pack(side="top", fill="x")
self.submit.pack(side="top")
def on_submit(self):
symbol = self.entry.get()
stock = Stock(self, symbol)
stock.pack(side="top", fill="x")
class Stock(tk.Frame):
def __init__(self, parent, symbol):
tk.Frame.__init__(self, parent)
self.symbol = tk.Label(self, text=symbol + ":")
self.value = tk.Label(self, text="123.45")
self.symbol.pack(side="left", fill="both")
self.value.pack(side="left", fill="both")
root = tk.Tk()
Example(root).pack(side="top", fill="both", expand=True)
root.mainloop()

How to redirecting "stdout" to a Label widget?

I am trying to redirect stdout to a Label widget. The goal is to "print" into the Label all the Python prints that are in my script.
But when I click on BUTTON1 nothing happens...
Here is my code:
from Tkinter import *
import sys
import tkMessageBox
class App:
def __init__(self, master):
self.frame = Frame(master, borderwidth=5, relief=RIDGE)
self.frame.grid()
class IORedirector(object):
def __init__(self,TEXT_INFO):
self.TEXT_INFO = TEXT_INFO
class StdoutRedirector(IORedirector):
def write(self,str):
self.TEXT_INFO.config(text=str)
self.TEXT_HEADER = self.text_intro = Label(self.frame, bg="lightblue",text="MY SUPER PROGRAMM") ## HEADER TEXT
self.TEXT_HEADER.grid(row=0, column=0, columnspan=2, sticky=W+E+N+S)
self.MENU = Frame(self.frame, borderwidth=5, relief=RIDGE, height=12)
self.MENU.grid(row=1, column=0, sticky=N)
self.button = Button(self.MENU, text="QUIT", fg="red", bg="red", command=self.frame.quit)
self.button.grid(row=4, column=0)
self.BUTTON1 = Button(self.MENU, text="BUTTON1", command=self.BUTTON1_CMD)
self.BUTTON1.grid(row=0, column=0,sticky=W+E)
self.TEXT_INFO = Label(self.frame, height=12, width=40, text="I WANT TO SEE THE STDOUT OUTPUT HERE", bg="grey",borderwidth=5, relief=RIDGE)
self.TEXT_INFO.grid(row=1, column=1)
sys.stdout = StdoutRedirector(self.TEXT_INFO)
def BUTTON1_CMD(self):
print "TEST NUMBER ONE"
print "TEST NUMBER TWO"
root = Tk()
app = App(root)
root.mainloop()
The reason you are not seeing the text set is that it is set correctly for a split second and then immediately set to blank. This is because print is sending a newline to stdout after the print statements. Here is a modified version that appends to the Label rather than overwrite it for every print statement.
class StdoutRedirector(IORedirector):
def write(self,str):
self.TEXT_INFO.config(text=self.TEXT_INFO.cget('text') + str)
I made a class which copies stdout write calls to a tkinter widget be it a Label or a Text. Works for me on Python3.3.1/WindowsXp.:
import sys
class StdoutToWidget:
'''
Retrieves sys.stdout and show write calls also in a tkinter
widget. It accepts widgets which have a "text" config and defines
their width and height in characters. It also accepts Text widgets.
Use stop() to stop retrieving.
You can manage output height by using the keyword argument. By default
the class tries to get widget\'s height configuration and use that. If
that fails it sets self.height to None which you can also do manually.
In this case the output will not be trimmed. However if you do not
manage your widget, it can grow vertically hard by getting more and
more inputs.
'''
# Inspired by Jesse Harris and Stathis
# http://stackoverflow.com/a/10846997/2334951
# http://stackoverflow.com/q/14710529/2334951
# TODO: horizontal wrapping
# make it a widget decorator (if possible)
# height management for Text widget mode
def __init__(self, widget, height='default', width='default'):
self._content = []
self.defstdout = sys.stdout
self.widget = widget
if height == 'default':
try:
self.height = widget.cget('height')
except:
self.height = None
else:
self.height = height
if width == 'default':
try:
self.width = widget.cget('width')
except:
self.width = None
else:
self.width = width
def flush(self):
'''
Frame sys.stdout's flush method.
'''
self.defstdout.flush()
def write(self, string, end=None):
'''
Frame sys.stdout's write method. This method puts the input
strings to the widget.
'''
if string is not None:
self.defstdout.write(string)
try:
last_line_last_char = self._content[-1][-1]
except IndexError:
last_line_last_char = '\n'
else:
if last_line_last_char == '\n':
self._content[-1] = self._content[-1][:-1]
if last_line_last_char != '\n' and string.startswith('\r'):
self._content[-1] = string[1:]
elif last_line_last_char != '\n':
self._content[-1] += string
elif last_line_last_char == '\n' and string.startswith('\r'):
self._content.append(string[1:])
else:
self._content.append(string)
if hasattr(self.widget, 'insert') and hasattr(self.widget, 'see'):
self._write_to_textwidget()
else:
self._write_to_regularwidget(end)
def _write_to_regularwidget(self, end):
if self.height is None:
self.widget.config(text='\n'.join(self.content))
else:
if not end:
content = '\n'.join(self.content[-self.height:])
else:
content = '\n'.join(self.content[-self.height+end:end])
self.widget.config(text=content)
def _write_to_textwidget(self):
self.widget.insert('end', '\n'.join(self.content))
self.widget.see('end')
def start(self):
'''
Starts retrieving.
'''
sys.stdout = self
def stop(self):
'''
Stops retrieving.
'''
sys.stdout = self.defstdout
#property
def content(self):
c = []
for li in self._content:
c.extend(li.split('\n'))
if not self.width:
return c
else:
result = []
for li in c:
while len(li) > self.width:
result.append(li[:self.width])
li = li[self.width:]
result.append(li)
return result
#content.setter
def content(self, string):
self._content = string.split('\n')
#property
def errors(self):
return self.defstdout.errors
#property
def encoding(self):
return self.defstdout.encoding
EDIT1: I received a downvote, so here is the updated one. I use this in a Label widget and print() functions appear smoothly in my widget. Moreover as an extra feature if I pass None to the write call and let's say -1 as end argument, then it won't show last line (careful with indexing). I use this because I attached a slider to the widget. I will publish a demo soon.

Categories

Resources