I am currently trying to implement a GUI for a task at work. I have found some open source code (see below) where we make a GUI having 3 pages where one can go between the pages using next/previous buttons. If you run the code you'll see what each button and so on is indended to do.
However, when you run the code and click the "count++" button, the overall count increases by one and not the count for the individual page (e.g. being on page 1 and clicking the count++ 4 times, still makes the count for page 2 or 3, 4 as well, and not zero). The same problem occurs when I try to update the text in each of the textboxes on each page (supposed to be number of clicks), as it won't update. I am not sure how to actually get to the textframe for each individual page.
Any suggestions on where to go from here? In the long run I would like to have scroll-down menus where the selections will be put onto each individual text-frame.
Thanks,
import ttk
from Tkinter import *
import tkMessageBox
class Wizard(object, ttk.Notebook):
def __init__(self, master=None, **kw):
npages = kw.pop('npages', 3)
kw['style'] = 'Wizard.TNotebook'
ttk.Style(master).layout('Wizard.TNotebook.Tab', '')
ttk.Notebook.__init__(self, master, **kw)
self._children = {}
self.click_count = 0
self.txt_var = "Default"
for page in range(npages):
self.add_empty_page()
self.current = 0
self._wizard_buttons()
def _wizard_buttons(self):
"""Place wizard buttons in the pages."""
for indx, child in self._children.iteritems():
btnframe = ttk.Frame(child)
btnframe.pack(side='left', fill='x', padx=6, pady=4)
txtframe = ttk.Frame(child)
txtframe.pack(side='right', fill='x', padx=6, pady=4)
nextbtn = ttk.Button(btnframe, text="Next", command=self.next_page)
nextbtn.pack(side='top', padx=6)
countbtn = ttk.Button(txtframe, text="Count++..", command=self.update_click)
countbtn.grid(column=0,row=0)
txtBox = Text(txtframe,width = 50, height = 20, wrap = WORD)
txtBox.grid(column=1,row=0)
txtBox.insert(0.0, self.txt_var)
rstbtn = ttk.Button(btnframe, text="Reset count!", command=self.reset_count)
rstbtn.pack(side='top', padx=6)
if indx != 0:
prevbtn = ttk.Button(btnframe, text="Previous",
command=self.prev_page)
prevbtn.pack(side='right', anchor='e', padx=6)
if indx == len(self._children) - 1:
nextbtn.configure(text="Finish", command=self.close)
def next_page(self):
self.current += 1
def prev_page(self):
self.current -= 1
def close(self):
self.master.destroy()
def add_empty_page(self):
child = ttk.Frame(self)
self._children[len(self._children)] = child
self.add(child)
def add_page_body(self, body):
body.pack(side='top', fill='both', padx=6, pady=12)
def page_container(self, page_num):
if page_num in self._children:
return self._children[page_num]
else:
raise KeyError("Invalid page: %s" % page_num)
def _get_current(self):
return self._current
def _set_current(self, curr):
if curr not in self._children:
raise KeyError("Invalid page: %s" % curr)
self._current = curr
self.select(self._children[self._current])
current = property(_get_current, _set_current)
def update_click(self):
self.click_count += 1
message = "You have clicked %s times now!" % str(self.click_count)
tkMessageBox.showinfo("monkeybar", message)
self.txt_var = "Number of clicks: %s" % str(self.click_count) #this will not change the text in the textbox!
def reset_count(self):
message = "Count is now 0."
#ctypes.windll.user32.MessageBoxA(0, message, "monkeybar", 1)
tkMessageBox.showinfo("monkeybar", message)
self.click_count = 0
def combine_funcs(*funcs):
def combined_func(*args, **kwargs):
for f in funcs:
f(*args, **kwargs)
return combined_func
def demo():
root = Tk()
nbrpages = 7
wizard = Wizard(npages=nbrpages)
wizard.master.minsize(400, 350)
wizard.master.title("test of GUI")
pages = range(nbrpages)
for p in pages:
pages[p] = ttk.Label(wizard.page_container(p), text='Page %s'%str(p+1))
wizard.add_page_body(pages[p])
wizard.pack(fill='both', expand=True)
root.mainloop()
if __name__ == "__main__":
demo()
Your update_click method operate on the attribute click_count of your wizard. If you want different count, you could either create a class for your pages and thus, each object would manage its own count, or manage several counter, for instance in a list, the same way you handle your _children list.
For the former case, you can create a page class inheriting ttk.Frame with the body of your _wizard_buttons loop as constructor. In the later case, you could try something like this
class Wizard(object, ttk.Notebook):
def __init__(self, master=None, **kw):
[...]
#replace self.click_count = 0 with
self.click_counters = [0 for i in range(npages)]
def update_click(self):
self.click_counters[self.current] += 1
# and so on...
Regarding the Text widget update, you can not handle it though a variable, it works with Entry(one line text field) but not on Text (multiline, rich text field). If you continue with Text, the usual recipe to what you seems to want is
text.delete(1.0, END)
text.insert(END, content)
Related
i have 2 classes and when i run the first one, it works fine but when it gets to the second class i get an error saying AttributeError: 'Question2' object has no attribute 'correct'. how do i make it so that the functions work in both of the class? is there something wrong with my indent? please help me fix this code, thanks:
Edit: i have a problem with using self, if i remove self from the functions it wouldnt work, if i indent it to not be a part of the class, it still wont work, the self gets turns white
class Question1:
def __init__ (self, master):
x = random.randint(5, 12)
y = random.randint(5, 12)
self.master = master
self.user_choice = StringVar()
self.user_choice.set("")
self.frame = Frame(master, padx=200, pady=200)
self.frame.grid()
self.q = Label(self.frame, text="What is {} + {} ?".format(x, y))
self.q.grid(row=0)
self.ans = Entry(self.frame, width=50, textvariable=self.user_choice)
self.ans.grid(row=1)
self.answer = x+y
self.sub = Button(self.frame, text="submit", command=self.correct)
self.sub.grid(row=3)
def correct(self):
global p
if int(self.user_choice.get()) == self.answer:
cor = Label(self.frame,text="Correct!")
cor.grid(row=5, pady=20)
p += 1
if p >= 3:
Question2(self.master)
else:
self.sub.destroy()
nex = Button(self.frame, text="Next", command=self.necs)
nex.grid(row=4)
else:
inc = Label(self.frame,text="incorrect")
inc.grid(row=5, pady=20)
self.sub.destroy()
nex = Button(self.frame, text="Next", command=self.necs)
nex.grid(row=4)
self.frame.destroy()
Question1(self.master)
def necs(self):
self.frame.destroy()
Question1(self.master)
class Question2:
def __init__(self, master):
x = random.randint(2, 2)
y = random.randint(2, 3)
self.master = master
self.user_choice = StringVar()
self.user_choice.set("")
self.frame = Frame(master, padx=200, pady=200)
self.frame.grid()
self.q = Label(self.frame, text="What is {} x {} ?".format(x, y))
self.q.grid(row=0)
self.ans = Entry(self.frame, width=50, textvariable=self.user_choice)
self.ans.grid(row=1)
self.answer = x * y
self.sub = Button(self.frame, text="submit", command=self.correct)
self.sub.grid(row=3)
You can do that by inheriting the properties of Question1 to Question2:
That can be:
class Question2(Question1):
#you can access that by:
self.correct()
Other way is you can define a global function outside both the classes and you can easily access it.
Example:
#somewhere globally:
def correct():
#some code
class Question1():
correct()
class Question2():
correct()
I think you can develop more such ideas of using a function which will be required by multiple classes.
As #JenilDave answered, you need to define function outside class, inherit from other class.explicitly call class.
i.e. for last case:
class Question1:
def correct():
<codes>
class Question2:
q1 = Question1()
q1.correct()
or
Question1.correct(<Question1 instance>)
But since your 'correct' function are heavily dependent to Question1, you can't use either way, and reconstruct your codes.
Working example below:
Instead of generating question Class per questions, send lists of questions to one class.
Every time you succeed third time, you'll move on to next questions by poping list you've provided before.
When Lists are empty, pop() causes IndexError and program closes.
...
Since I can't get what variable 'p' stands for, I'm guessing it's number of successes(passes).
Full Code:
import random
from tkinter import Frame, Label, Entry, Button, StringVar, Tk
class QuestionFrame(Frame):
def __init__(self, master, question_answer):
super().__init__(master)
self.x, self.y = 0, 0
self.master = master
self.entries = question_answer
self.question, self.answer = self.entries.pop(0)
self.success = 0
self.user_choice = StringVar()
self.frame = Frame(master)
self.frame.grid()
self.quest_label = Label(self.frame)
self.ans = Entry(self.frame, width=50, textvariable=self.user_choice)
self.sub = Button(self.frame)
self.quest_label.grid(row=0)
self.ans.grid(row=1)
self.sub.grid(row=3)
self.reload_question()
def reload_question(self):
self.x, self.y = random.sample(range(5, 12), 2)
next_quest = f"What is {self.question.format(self.x, self.y)} ?"
self.quest_label.configure(text=next_quest)
self.ans.delete(0, 'end')
self.sub.configure(text="submit", command=self.correct)
def next(self):
print(self.success)
if self.success == 3:
# loads next entry
try:
self.question, self.answer = self.entries.pop(0)
except IndexError:
self.master.destroy()
else:
self.success = 0
self.reload_question()
else:
self.reload_question()
def correct(self):
self.sub.configure(text="Next", command=self.next)
if int(self.user_choice.get()) == self.answer(self.x, self.y):
self.quest_label['text'] = "Correct!"
self.success += 1
else:
self.quest_label['text'] = "Incorrect!"
if __name__ == '__main__':
# Passing questions with list of (question, answer) tuples.
tests = [("{} x {}", lambda x, y: x*y),
("{} - {}", lambda x, y: x-y)]
root = Tk()
root.title(f'Some fancy title')
window = QuestionFrame(root, tests)
window.mainloop()
Problem Description
I have an application in Tkinter that uses a Listbox that displays search results. When I press command + down arrow key, I am putting the focus from the search field to the first item in the Listbox. This is exactly how I want the behaviour but instead for just the down arrow.
However, I am already binding the down arrow to this Listbox by self.bind("<Down>", self.moveDown). I can not understand why command + down works while simply down (to which I literally bind'ed it to) does not. Specifically the result of pressing the down arrow is the following
While pressing command + down gives the intended result:
How can I let down behave just like command + down, and what is the reason why command is required at all?
Code snippets
def matches(fieldValue, acListEntry):
pattern = re.compile(re.escape(fieldValue) + '.*', re.IGNORECASE)
return re.match(pattern, acListEntry)
root = Tk()
img = ImageTk.PhotoImage(Image.open('imgs/giphy.gif'))
panel = Label(root, image=img)
panel.grid(row=1, column=0)
entry = AutocompleteEntry(autocompleteList, panel, root, matchesFunction=matches)
entry.grid(row=0, column=0)
root.mainloop()
With AutocompleteEntry being:
class AutocompleteEntry(Tkinter.Entry):
def __init__(self, autocompleteList, df, panel, rdi, *args, **kwargs):
self.df = df
self.product_row_lookup = {key:value for value, key in enumerate(autocompleteList)}
temp = df.columns.insert(0, 'Product_omschrijving')
temp = temp.insert(1, 'grams')
self.result_list = pd.DataFrame(columns=temp)
self.panel = panel
self.rdi = rdi
# self.bind('<Down>', self.handle_keyrelease)
# 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
Entry.__init__(self, *args, **kwargs)
self.focus()
self.autocompleteList = autocompleteList
self.var = self["textvariable"]
if self.var == '':
self.var = self["textvariable"] = StringVar()
self.var.trace('w', self.changed)
self.bind("<Right>", self.selection)
self.bind("<Up>", self.moveUp)
self.bind("<Down>", self.moveDown)
self.bind("<Return>", self.selection)
self.listboxUp = False
self._digits = re.compile('\d')
def changed(self, name, index, mode):
if self.var.get() == '':
if self.listboxUp:
self.listbox.destroy()
self.listboxUp = False
else:
words = self.comparison()
if words:
if not self.listboxUp:
self.listbox = Listbox(width=self["width"], height=self.listboxLength)
self.listbox.bind("<Button-1>", self.selection)
self.listbox.bind("<Right>", self.selection)
self.listbox.bind("<Down>", self.moveDown)
self.listbox.bind("<Tab>", self.selection)
self.listbox.place(x=self.winfo_x(), y=self.winfo_y() + self.winfo_height())
self.listboxUp = True
self.listbox.delete(0, END)
for w in words:
self.listbox.insert(END, w)
else:
if self.listboxUp:
self.listbox.destroy()
self.listboxUp = False
else:
string = self.get()
if '.' in string:
write_to_file(self, string)
def contains_digits(self, d):
return bool(self._digits.search(d))
def selection(self, event):
if self.listboxUp:
string = self.listbox.get(ACTIVE)
self.var.set(string + ' ')
self.listbox.destroy()
self.listboxUp = False
self.icursor(END)
def moveDown(self, event):
self.focus()
if self.listboxUp:
if self.listbox.curselection() == ():
index = '0'
print "ok"
else:
index = self.listbox.curselection()[0]
print "blah"
if index != END:
self.listbox.selection_clear(first=index)
print "noo"
if index != '0':
index = str(int(index) + 1)
self.listbox.see(index) # Scroll!
self.listbox.selection_set(first=index)
self.listbox.activate(index)
else:
print "not up"
def comparison(self):
return [w for w in self.autocompleteList if self.matchesFunction(self.var.get(), w)]
Both command+down and down should produce the same output excepted that down also types question mark onto the entry which made the last letter typed is the question mark box.
This is because pressing command, your computer checks the option menu to see if there's a shortcut with that key, if there isn't any, it will not do anything. While tkinter registered the down button as being pressed, so the event was triggered.
In contrast, With out pressing command, the Entry first displays the value of "down", which there isn't any, then executes the event binding, what you can do is, in the event, remove the last letter of the Entry. You can do so by self.delete(len(self.get())-1) in your event. Or add a return 'break' at the end of your event to prevent it from being typed.
Unfortunately, it's really hard to understand your real problem because you've posted too much unrelated code and not enough related code. It seems to me that what you're trying to accomplish is to let the user press the down or up arrow while an entry has focus, and have that cause the selection in a listbox to move down or up. Further, it seems that part of the problem is that you're seeing characters in the entry widget that you do not want to see when you press down or up.
If that is the problem, the solution is fairly simple. All you need to do is have your binding return the string "break" to prevent the default binding from being processed. It is the default binding that is inserting the character.
Here is an example. Run the example, and press up and down to move the selection of the listbox. I've left out all of the code related to autocomplete so you can focus on how the event binding works.
import Tkinter as tk
class Example(object):
def __init__(self):
self.root = tk.Tk()
self.entry = tk.Entry(self.root)
self.listbox = tk.Listbox(self.root, exportselection=False)
for i in range(30):
self.listbox.insert("end", "Item #%s" % i)
self.entry.pack(side="top", fill="x")
self.listbox.pack(side="top", fill="both", expand=True)
self.entry.bind("<Down>", self.handle_updown)
self.entry.bind("<Up>", self.handle_updown)
def start(self):
self.root.mainloop()
def handle_updown(self, event):
delta = -1 if event.keysym == "Up" else 1
curselection = self.listbox.curselection()
if len(curselection) == 0:
index = 0
else:
index = max(int(curselection[0]) + delta, 0)
self.listbox.selection_clear(0, "end")
self.listbox.selection_set(index, index)
return "break"
if __name__ == "__main__":
Example().start()
For a fairly thorough explanation of what happens when an event is triggered, see this answer: https://stackoverflow.com/a/11542200/7432
Again, leaving aside the autocomplete requirement, I came up with a solution that uses that standard available commands and events for Listbox and Scrollbar. <<ListboxSelect>> lets you capture changes in selection from any of the lists and align the others. In addition, the Scrollbar and Listbox callbacks are directed to a routing function that passes things on to all of the listboxes.
# updownmultilistbox.py
# 7/24/2020
#
# incorporates vsb to propagate scrolling across lists
#
import tkinter as tk
class Example(object):
def __init__(self):
self.root = tk.Tk()
self.listOfListboxes = []
# self.active_lb = None
self.vsb = tk.Scrollbar(orient='vertical', command=self.OnVsb)
self.vsb.pack(side='right', fill='y')
self.lb1 = tk.Listbox(self.root, exportselection=0,
selectmode= tk.SINGLE, yscrollcommand=self.vsb_set)
self.lb2 = tk.Listbox(self.root, exportselection=0,
selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
self.lb3 = tk.Listbox(self.root, exportselection=0,
selectmode=tk.SINGLE, yscrollcommand=self.vsb_set)
self.listOfListboxes.append(self.lb1)
self.listOfListboxes.append(self.lb2)
self.listOfListboxes.append(self.lb3)
for i in range(30):
self.lb1.insert("end", "lb1 Item #%s" % i)
self.lb2.insert("end", "lb2 Item #%s" % i)
self.lb3.insert("end", "lb3 Item #%s" % i)
self.lb1.pack(side="left", fill="both", expand=True)
self.lb2.pack(side="left", fill="both", expand=True)
self.lb3.pack(side="left", fill="both", expand=True)
for lb in self.listOfListboxes:
lb.bind('<<ListboxSelect>>', self.handle_select)
for lb in self.listOfListboxes:
lb.selection_set(0)
lb.activate(0)
self.listOfListboxes[0].focus_force()
def start(self):
self.root.title('updownmultilistbox')
self.root.mainloop()
def OnVsb(self, *args):
for lb in self.listOfListboxes:
lb.yview(*args)
def vsb_set(self, *args):
print ('vsb_set args: ', *args)
self.vsb.set(*args)
for lb in self.listOfListboxes:
lb.yview_moveto(args[0])
def handle_select(self, event):
# set evey list to the same selection
print ('select handler: ', event, event.widget.curselection())
# self.active_lb = event.widget
for lb in self.listOfListboxes:
if lb != event.widget:
lb.selection_clear(0, 'end') # have to avoid this for the current widget
lb.selection_set(event.widget.curselection())
lb.activate(event.widget.curselection())
if __name__ == "__main__":
Example().start()
I have two python files gui.py, student.py. i have imported tkinter, i will ask the user to enter their name, id, email, and address, Using tkinter widget i will display all in a list. How to do this using class ?
this is gui.py
import tkinter
import student
class MyGUI:
def __init__(self):
self.__students = []
# Create the main window widget
self.main_window = tkinter.Tk()
self.name_f = tkinter.Frame(self.main_window)
self.id_f = tkinter.Frame(self.main_window)
self.email_f = tkinter.Frame(self.main_window)
self.addy_f = tkinter.Frame(self.main_window)
self.buttons_f = tkinter.Frame(self.main_window)
# Create a Label and Entry widget for each item in
# the Student class
self.name_l = tkinter.Label(self.name_f, text='Name: ')
self.name_e = tkinter.Entry(self.name_f, width=10)
self.id_l = tkinter.Label(self.id_f, text='ID: ')
self.id_e = tkinter.Entry(self.id_f, width=10)
self.email_l = tkinter.Label(self.email_f, text='Email: ')
self.email_e = tkinter.Entry(self.email_f, width=10)
self.addy_l = tkinter.Label(self.addy_f, text='Address: ')
self.addy_e = tkinter.Entry(self.addy_f, width=10)
self.add_b = tkinter.Button(self.buttons_f, text='Add Current Data', command=self.add)
self.display_b = tkinter.Button(self.buttons_f, text='List All', command=self.display)
self.quit_b = tkinter.Button(self.buttons_f, text='Quit', command=self.main_window.destroy)
self.name_l.pack(side='left')
self.name_e.pack(side='left')
self.id_l.pack(side='left')
self.id_e.pack(side='left')
self.email_l.pack(side='left')
self.email_e.pack(side='left')
self.addy_l.pack(side='left')
self.addy_e.pack(side='left')
self.add_b.pack(side='left')
self.display_b.pack(side='left')
self.quit_b.pack(side='left')
self.name_f.pack()
self.id_f.pack()
self.email_f.pack()
self.addy_f.pack()
self.buttons_f.pack()
#Enter the tkinter main loop
tkinter.mainloop()
def add(self):
# we will do this in class
pass
def display(self):
# we will do this in class
pass
# Create an instance of the MyGUI class
my_gui = MyGUI()
and this is the student.py
class Student:
# this a comment
# most languages define attributes sep
# Declare String name
def setName(self, n):
self.name = n
def setId(self, i):
self.sid = i
def setEmail(self, e):
# check to see if e has an # sign
self.email = e
def setAddy(self, a):
self.addy = a
def getName(self):
return self.name
def getId(self):
return self.sid
def getEmail(self):
return self.email
def getAddy(self):
return self.addy
def printInfo(self):
info = "Name: "
info += self.name
info += '\nID: '
info += self.sid
info += '\nEmail: '
info += self.email
info += '\nAddress: '
info += self.addy
info += '\n'
return info
Asking for code solutions to something you should be learning yourself probably isn't a good idea.
Just look up the documentation for TKinter here: https://docs.python.org/3/library/tkinter.html
As a note, you may want to consider opening separate windows when displaying the information. When I first learned TKinter, I first practiced by printing all my data to the console with the print command before trying to force them into tables.
Also, consider fleshing out the GUI and make it look like a table. Give each label a fixed length and each box a fixed length, this will make it look better.
A good example of this in action is on this amazing website for learning TKinter: http://www.tkdocs.com/tutorial/firstexample.html
Just scroll down until you find the Python implementation of the code :)
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.
I'm working on simple GUI but I'm stuck. This is the basic flow:
Show text text and:
save time in time_pressed
start the progressbar and update it until time_notallow expires.
If a user presses <Next> see if seconds specified in time_notallow have passed,
and if not, don't allow the display of the next text.
Basically, I want to prevent users form calling a method bind to the <Right> key until time_notallow passes, and show a progressbar to inform them how long they'll have to wait. Since I use bind, such as...
self.master.bind('<Right>', self.text_next)
...I don't have .after(), as in widgets.
What I've tried
master.after() to set bind to None and after time_notallow to bind to self.text_next, but it didn't work.
Created a thread which looped with while True to check constantly if time_notallow is passed or not, but the application crashes.
Any help appreciated.
Edit: A Solution. Using lambda() in .after to count seconds (thanks to Bryan Oakley)
"""
Stripped-down version to figue out time/event/threading stuff.
What this has to do:
1. Show the window and some text.
2. User presses Next arrow and new text shows. Paint the label red.
3. Prevent user form pressing again (unbind all keys), until 2 seconds passed
(time_wait).
4. Make notice of passed time and after 2 seconds bind the keys again and
paint the label green.
5. Loop the steps 2-4.
"""
import sys
import tkinter as tk
from tkinter import W, E, S, N
class Test(tk.Frame):
def __init__(self, master=None):
"""Draw the GUI"""
tk.Frame.__init__(self, master)
self.draw_widgets()
self.grid()
self.time_wait = 2
self.locked = False
self.bind_keys()
self.counter = 0
def draw_widgets(self):
"""Draw all the widgets on the frame."""
text = 'Just a sample sentence.'
#Label with the sentence
self.lbl_text = tk.Label(self, anchor="center", relief='groove')
self.lbl_text['text'] = text
self.lbl_text['font'] = ('Helvetica', 27)
self.lbl_text.grid(column=0, row=0, sticky=W+E+S+N)
self.lbl_note = tk.Label(self, anchor="center", relief='groove',
bg='green')
self.lbl_note.grid(column=0, row=1, sticky=W+E+S+N)
def text_next(self, event):
"""Get next text"""
if not self.locked:
self.counter += 1
self.lbl_text['text'] = 'The text number %s!' % self.counter
self.bind_tonone()
self.lock(self.time_wait)
def text_previous(self, event):
"""Get previous text"""
if not self.locked:
self.counter -= 1
self.lbl_text['text'] = 'The text number %s!' % self.counter
self.bind_tonone()
self.lock(self.time_wait)
def bind_keys(self):
"""Bind the keys"""
self.master.bind('<Left>', self.text_previous)
self.master.bind('<Right>', self.text_next)
self.master.bind('<Escape>', self.exit)
self.lbl_note['bg'] = 'green'
def bind_tonone(self):
"""Unbind the keys"""
self.master.bind('<Left>', None)
self.master.bind('<Right>', None)
self.master.bind('<Escape>', None)
self.lbl_note['bg'] = 'red'
def lock(self, n):
if n == 0:
self.locked = False
self.lbl_note['text'] = ''
self.lbl_note['bg'] = 'green'
else:
self.locked = True
self.lbl_note['text'] = 'Locked for %s more seconds' % n
self.lbl_note.after(1000, lambda n = n - 1: self.lock(n))
def exit(self, event):
"""Exit the program."""
sys.exit()
def start():
"""Start the gui part."""
root = tk.Tk()
app = Test(master=root)
app.mainloop()
if __name__ == '__main__':
start()
You don't need threads or timers to solve this problem. All you need is a procedure that takes a number of seconds to wait, and have it call itself once a second until the number gets down to zero.
It would look something like this (off the top of my head, untested):
def __init__(...):
...
self.locked = False
...
def text_next(self, event):
if not self.locked:
<do the "next" logic>
self.lock(10) # lock for 10 seconds
def text_previous(self, event):
if not self.locked:
<do the "previous" logic>
self.lock(10) # lock for 10 seconds
def lock(self, n):
if n == 0:
self.locked = False
self.status.config(text="")
else:
self.locked = True
self.status.config(text="Locked for %s more seconds" % n)
self.status.after(1000, lambda n=n-1: self.lock(n))
How about instead of a spin-waiting thread, you try the specifically-designed threading.Timer object?
A quick fix using your timer is to refactor a bit. Set your self.track initially to None, then only set it to run/block on arrow.
import sys
import threading
import Tkinter as tk
from Tkinter import W, E, S, N
class Test(tk.Frame):
def __init__(self, master=None):
"""Draw the GUI"""
tk.Frame.__init__(self, master)
self.draw_widgets()
self.grid()
# Track wait times
self.time_wait = 2
# Timer
self.track = None
self.bind_keys()
self.counter = 0
def draw_widgets(self):
"""Draw all the widgets on the frame."""
text = 'Just a sample sentence.'
#Label with the sentence
self.lbl_text = tk.Label(self, anchor="center", relief='groove')
self.lbl_text['text'] = text
self.lbl_text['font'] = ('Helvetica', 27)
self.lbl_text.grid(column=0, row=0, sticky=W+E+S+N)
self.lbl_note = tk.Label(self, anchor="center", relief='groove',
bg='green')
self.lbl_note.grid(column=0, row=1, sticky=W+E+S+N)
def text_next(self, event):
"""Get next text"""
if not self.track or not self.track.is_alive():
self.track = threading.Timer(self.time_wait, self.bind_keys)
self.counter += 1
self.lbl_text['text'] = 'The text number %s!' % self.counter
self.bind_tonone()
self.track.start()
def text_previous(self, event):
"""Get previous text"""
self.counter -= 1
self.lbl_text['text'] = 'The text number %s!' % self.counter
def bind_keys(self):
"""Bind the keys"""
self.master.bind('<Left>', self.text_previous)
self.master.bind('<Right>', self.text_next)
self.master.bind('<Escape>', self.exit)
self.lbl_note['bg'] = 'green'
def bind_tonone(self):
"""Unbind the keys"""
self.master.bind('<Left>', None)
self.master.bind('<Right>', None)
self.master.bind('<Escape>', None)
self.lbl_note['bg'] = 'red'
def exit(self, event):
"""Exit the program."""
sys.exit()
def start():
"""Start the gui part."""
root = tk.Tk()
app = Test(master=root)
app.mainloop()
if __name__ == '__main__':
start()
On preview though, #Bryan Oakley, has a better solution.