tkinter binding doesn't do anything and attribute error - python

I'm new to Tkinter and I'm trying to add event handling to a GUI.
I have a list that contains sentences and words( the list contains a sublist consisting of the sent as a string as its first element and a list of its words as its second element), and I first want to display the sentences using a Label widget. What I'd like to do is switch between the sentences using the Up and Down keys.
My first problem, however is a different one. I want to store the sentence that is currently displayed in a variable called current_sent, so I try to assign 0 to self.current_sent in the constructor of the app. However, when I try to reference this variable in my code, I get an attribute error. When I initialize self.current_sent in the initialize() method of my app, I don't get the error. Can anybody tell me why this is?
Now if I set self.current_sent = 0 in the initialize method, the gui starts, but I don't get any changes when pushing the Down button.
I also tried this using only '' as an event, but that also doesn't cause the second sentence to be displayed.
If I try to call print statements from the next_sent method, nothing is displayed, so I never enter the event handling function.
Can anybody tell me, what I'm doing wrong, please?
import nltk
import Tkinter as tk
import os
class Annotator(tk.Tk):
def __init__(self, parent):
tk.Tk.__init__(self, parent)
self.sents = self.get_sents()
self.initialize()
self.current_sent = 0
self.current_word = 0
def sent_tokenize(self, textfile):
f = open(textfile)
s = f.readlines()
text = " ".join(s)
sents = nltk.sent_tokenize(text)
tags = [[x,nltk.word_tokenize(x)] for x in sents]
return tags
def get_sents(self):
article_files = self.get_articles()
list_of_sents = [self.sent_tokenize(x) for x in article_files]
sents = [sent for sublist in list_of_sents for sent in sublist]
return sents
def get_articles(self):
directory = "/Users/------------/Documents/reuters/reuters/articles"
list_of_articles = []
for f in os.listdir(directory):
if not f.startswith('.'):
filename = directory + "/" + f
list_of_articles.append(filename)
return list_of_articles
def next_sent(self,event):
if (self.current_sent < len(self.sents) - 1):
self.current_sent += 1
self.label.config(text = self.sents[self.current_sent][0])
def initialize(self):
self.label = tk.Label(text = self.sents[self.current_sent][0])
self.label.bind('<KeyPress-Down>', self.next_sent)
self.label.grid(row = 0, column = 0, columnspan = 2)
if __name__ == "__main__":
app = Annotator(None)
app.mainloop()

The AttributeError is coming up because __init__ calls initialize before defining self.current_sent so you just need to rearrange the __init__ a little bit:
def __init__(self, parent):
tk.Tk.__init__(self, parent)
self.current_sent = 0
self.current_word = 0
self.sents = self.get_sents()
self.initialize()
As for the Binding issue, only the widget with keyboard focus will respond to events, either try clicking on the label before testing the events or set it up to respond regardless of what has focus like this:
self.bind_all('<KeyPress-Down>', self.next_sent)

Related

Python self.attribute cannot be seen when calling class method

I have a problem related to a TKinter GUI I am creating, but the problem is not necessarily specific to this library.
Background
I am currently in the advanced stage of a python self-learning course. The learning module I am on is covering TKinter for creating interactive GUI's. I am making a game whereby randomly generated numbered buttons must be clicked in succession in the quickest time possible.
Brief: https://edube.org/learn/pcpp1-4-gui-programming/lab-the-clicker
Problem
Under my class, game_grid, I have created an instance variable; 'self.holder', a 25 entry dictionary of {Key : TkinterButtonObject} form
When calling this instance variable for use in a class method, I get the following error:
AttributeError: 'game_grid' object has no attribute 'holder'
I have a print statement under class init which proves this attribute has been successfully created. I have made sure my spacing and tabs are all OK, and have tried every location for this variable, including using as a class variable, and a global variable to no avail - as it is an semi-complex object. I don't see what difference it should make, but any ideas would be much appreciated. I am also aware this could be done without classes, but I am trying to adopt DRY principles and orthogonality in all of my programs.
Thanks in advance.
Full Code:
import tkinter as tk
from tkinter import*
import random
from tkinter import messagebox
import time
win = tk.Tk()
class game_grid:
def __init__(self, win):
self.last_number = 0
self.number_buttons = {}
self.row_count = 0
self.column_count = 0
#Generate a list of 25 random numbers
self.number_list = random.sample(range(0, 999), 25)
#Puts the numbers in a dictionary (number : buttonobject)
self.holder = {i: tk.Button(win, text = str(i), command = game_grid.select_button(self, i)) for i in self.number_list}
#pack each object into window by iterating rows and columns
for key in self.holder:
self.holder[key].grid(column = self.column_count, row = self.row_count)
if self.column_count < 4:
self.column_count += 1
elif self.column_count == 4:
self.column_count = 0
self.row_count += 1
print(self.holder)
def select_button(self, number):
if number > self.last_number:
self.holder[number].config(state=tk.DISABLED)
self.last_number = number
else:
pass
class stopclock():
def __init__(self):
#Stopclock variable initialisation
self.time_begin = 0
self.time_end = 0
self.time_elapsed= 0
def start(self):
if self.time_begin == 0:
self.time_begin = time.time()
return("Timer started\nStart time: ", self.time_begin)
else:
return("Timer already active")
def stop(self):
self.time_end = time.time()
self.time_elapsed = time_end - time_begin
return("Timer finished\nEnd time: ", time_begin,"\nTime Elapsed: ", time_elapsed)
play1 = game_grid(win)
win.mainloop()
Perhaps you meant:
command = self.select_button(self, i)
Update:
Though from research:How to pass arguments to a Button command in Tkinter?
It should be:
command = lambda i=i: self.select_button(i)
You call select_button from inside the dict comprehension of holder. select_button then tries to use holder, but it is not yet defined. You don't want to actually call select_button, but assign a function to the button, like that:
self.holder = {i: tk.Button(window, text=str(i), command=lambda i=i: self.select_button(i)) for i in self.number_list}

Using list in parameter to create radiobuttons in GUI

I used a list as a parameter for the GUI window, and each individual item in the list is supposed to be created as a radio button in the GUI window. Currently, I used a for loop to generate different values for each radio button and another for loop to generate different columns. However, I am only able to create a radio button for the last item in the list.
This is what I currently have:
from tkinter import *
from tkinter.scrolledtext import *
class ExpenditureGUI:
def __init__(self, listOfCategories):
self._listOfCategories = listOfCategories
self._tk = Tk()
self._tk.title('Expenditure Tracker')
self._tk.geometry('500x200')
self._tk.resizable(False,False)
self.initWidgits()
self._tk.mainloop()
#property
def listOfCategories(self):
return self._listOfCategories
#listOfCategories.setter
def listOfCategories(self, newValue):
self._listOfCategories = newValue
def initWidgits(self):
#flow layout
topF = Frame(self._tk)
self._lblAmount = Label(topF, text = 'Amount: ')
self._txtAmount = Entry(topF, width=30)
rbtnF = Frame(topF)
self._rbtnVar = IntVar()
self._rbtnVar.set(0)
self._lblExpenditure = Label(topF, text = 'Type of Expenditure: ')
n = 0
for type in self._listOfCategories:
self._rbtntype = Radiobutton(rbtnF, text = f'{type}', value = n, variable = self._rbtnVar)
self._lblExpenditure.grid(row = 0, column = 0, sticky = E)
n = 0
for type in self._listOfCategories:
self._rbtntype.grid(row = 0, column = n)
I’m not an expert in the GUI library, but this looks pretty suspect:
for type in self._listOfCategories:
self._rbtntype = Radiobutton(rbtnF, text = f'{type}', value = n, variable = self._rbtnVar)
^^^^^^^^^^^^^^^^
After this loop ends, the self._rbtntype variable will contain text referring to only the last element in the list of categories (i.e. the loop variable type).
You may want to construct a new Radiobutton in the final loop in your code sample. Maybe something like this would work. It probably needs to update n in each loop iteration.
for type in self._listOfCategories:
rbtntype = Radiobutton(rbtnF, text = f'{type}', value = n, variable = self._rbtnVar)
rbtntype.grid(row = 0, column = n)

Updating tkinter window from another module

I have a piece of code that does some textual analysis and displays the results in a tkinter window.
Since users can choose to do the same analysis on multiple sources, and this can take quite some time, I would like to display the results as they become available.
However, the tkinter window pops up only after the last result becomes available. From other questions on Stackoverflow, I understand why this does not work (simplified version of my code):
class Output:
def __init__(self):
self.set_main_variables()
def set_main_variables(self):
self.names= []
#and other variables...
def initialize(self):
self.t = Tk()
self.make_frames()
self.populate_frames()
self.t.mainloop()
def update(self):
self.populate_frames()
def populate_frames(self):
<uses the data in self.names to display the results>
output = Output()
for i, s in enumerate(sources):
results = analyze(s)
output.names = results
if i == 0:
output.initialize()
else:
output.update()
Therefore, I tried taking the initialize() out of the loop, and creating a button in the window by which the user can update the results (if any are available):
output = Output()
first_results = analyze(s[0])
output.names = first_results
output.initialize()
for i, s in enumerate(sources):
results = analyze(s)
output.names = results
This does not solve the problem, though; the window still pops up only after the last source has been analyzed.
I have read many possible options to deal with this (using after, guiloop, etc.) but I don't see how these could help me in this situation.
Could anyone set me on the right path?
A colleague found the solution to my question.
Eric, in the comments, put me partly on the right path. He proposed using update_idletasks() in the GUI, but that did not work: it opened a blank window, which was filled in with widgets only when the "sources" loop was finished.
The solution was to put update() not inside the GUI, but inside the loop that is sending the updates to the GUI.
A simplified example with working code:
from tkinter import *
class SimpleTestClass:
def __init__(self):
self.names = []
self.top = Tk()
self.names_string = StringVar()
self.label = Label(self.top, textvar=self.names_string).pack()
#self.top.mainloop()
def updateLabel(self):
self.names_string.set("\n".join(self.names))
def test(gui):
names = ["a", "b", "c"]
sources = [11, 2, 3]
for i, s in enumerate(sources):
print(i)
gui.names.append(names[i])
print(gui.names)
gui.updateLabel()
gui.top.update()
#gui.top.update_idletasks()
time.sleep(3) # this simulates the program taking /
# some time to do its analysis of the source
s = SimpleTestClass()
test(s)
Two notes:
using update_idletasks() works as well, but displays a strange black
border at the bottom of the window until the sources loop is
finished.
calling self.top.mainloop() blocks the window update
I also tried an alternative version, where the user can push a button to manually update the list of results, but this did not function well (the button reacts only when the sources loop is finished):
class SimpleTestClass:
def __init__(self):
self.names = []
self.top = Tk()
self.names_string = StringVar()
self.label = Label(self.top, textvar=self.names_string).pack()
Button(self.top, text="update", command=self.updateLabel).pack()
def updateLabel(self):
self.names_string.set("\n".join(self.names))
def test(gui):
names = ["a", "b", "c"]
sources = [11, 2, 3]
for i, s in enumerate(sources):
gui.names.append(names[i])
print(gui.names)
gui.top.update()
time.sleep(3) # this simulates the program taking /
# some time to do its analysis of the source
s = SimpleTestClass()
test(s)

return a value from class python

Edit
So I asked this question earlier and I received some good insight, but I feel like my question wasn't really answered. I'm building a small program to practice with python and making GUI's and I'm having a small problem with a button command.
#This is the temperature menu:
def temperM(self, *args):
self.clearscreen(self.frame)
self.frame2 = Frame(self.root)
self.frame2.grid(column = 0, row = 0)
self.firstunit = StringVar()
self.secondunit = StringVar()
self.entryspace = IntVar()
self.displayspace = IntVar()
#Create back button
#This is the part that needs to be fixed
self.back = Button(self.frame2, text = "< Back",
command = lambda: self.redo(self.frame2))
self.back.grid(column = 1, row = 3)
self.label = Label(self.frame2, text = "Convert from: ")
self.label.grid(column = 1, row = 1, padx = 4)
#Create the check boxes
self.celsius = Checkbutton(self.frame2, text = "Celsius",
variable = self.firstunit, onvalue = 'celsius')
self.celsius.grid(column = 2, row = 1)
self.fahrenheit = Checkbutton(self.frame2, text = "Fahrenheit",
variable = self.secondunit, onvalue = 'fahrenheit')
self.fahrenheit.grid(column = 3, row = 2)
#Create entry space to recieve text
#This is where the problem starts.
self.entry = Entry(self.frame2, width = 7,
textvariable = self.entryspace)
self.entry.grid(column = 3, row = 3)
self.compute = Calculate(self.entryspace.get())
self.button = Button(self.frame2, text = "Calculate",
command = lambda: self.displayspace.set(self.compute.celtoFah()))
self.button.grid(column = 3, row = 4)
self.display = Label(self.frame2, textvariable = self.displayspace)
self.display.grid(column = 2, row = 2)
I have this function inside of a class Menu with def__init__(self, root) which creates all the different menu options.
class Calculate:
def __init__(self, number):
self.number = number
def celtoFah(self):
try:
self.temp = Temperature()
self.number = float(self.number)
return self.temp.C2F(self.number)
except ValueError:
pass
And I have this class which holds all the different calculations that will be used in the code.
What I'm having trouble with is with my button command command = lambda: self.displayspace.set(self.compute.celtoFah()). When I run the code and press 'Calculate' which runs the command, self.displayspace.set(), it doesn't set self.displayspace to what I believe the returned value should be. Instead it returns and sets self.displayspace to what self.entryspace.get() is originally without modifications which is 32 and 0 respectively, which causes me to believe that the line self.compute = Calulate(self.entryspace.get()) is not updating when I put in a new value so self.entryspace is not getting a new value but its retaining the same initial value established by IntVar(). Am I doing something wrong in my code for self.entryspace not to be updating with a new value? At first I had it as a StringVar() which would convert to a float in celtoFah but I was throwing ValueError because it was receiving an empty string even after a user inputs a value. I really want to keep all calculations in a separate class since I will be having 20+ in the final version, but should I move these commands into class Menu or is there another I can do this by having a separate class? If you need to see my full code here is a link to it on github: https://github.com/Flameancer/Unit-Conversion-Program-in-Python
In general, you don't pass values between classes, but between instances of those classes. At any given time, you may have 0, 1, or 30 different Foo instances; how does a Bar instance even know which one you want?
The first question is, who's calling that something method on that Bar? Whoever it is, he has the value. Maybe it should be the Foo instance that's doing the calling.
For that to happen, the foo instance has to know about a bar instance. Maybe you want to create one in the constructor:
class Foo:
def __init__(self, argument):
# ...
self.bar = Bar(42)
# ...
… and now you can use it the same way as any other member:
def function(self, *args):
# ...
randomness = self.bar.something()
self.displayfield.set(randomness)
# ...
Or maybe you're already constructing one somewhere, and you just want to pass it to the Foo instance as a constructor:
class Foo:
def __init__(self, argument, bar):
# ...
self.bar = bar
# ...
bar = Bar(42)
foo = Foo(23, bar)
Or maybe you want to construct a new one locally each time you call the function method. Or maybe you want a global Bar instance shared by everyone, no matter how many Foo instances you have. Or…
As you can see, there are many different possible relationships between a Foo instance and a Bar instance, and which one is appropriate depends entirely on what Foo and Bar actually represent. This is the code idea behind object modeling: there are things of some kind that your objects represent, and the relationships between those things are reflected in the relationships between those objects.

Python Tkinter class inheritance problems

I'm trying to manipulate a listbox in Tkinter but I'm having some troubles. I used to have everything in one class, on one page, and it worked fine. I separated the methods into different classes on two different pages (one for displaying things, one for modifying them) and now I'm having some issues.
I'm getting the following error AttributeError: Actions has no attribute 'listbox'. I'm assuming it's something inheritance related because it worked fine before I split it into two files.
Here's the first file
from Tkinter import *
import Tkinter
import SortActions
class MakeList(Tkinter.Listbox):
def BuildMainWindow(self):
menubar = Frame(relief=RAISED,borderwidth=1)
menubar.pack()
mb_file = Menubutton(menubar,text='file')
mb_file.menu = Menu(mb_file)
mb_file.menu.add_command(label='open', command = self.BuildListbox)
mb_file.pack(side=LEFT)
mb_edit = Menubutton(menubar,text='edit')
mb_edit.menu = Menu(mb_edit)
mb_edit.pack(padx=25,side=RIGHT)
mb_file['menu'] = mb_file.menu
mb_edit['menu'] = mb_edit.menu
return
def BuildListbox(self):
self.listbox = Tkinter.Listbox()
index = SortActions.Actions()
self.listbox.bind('<<ListboxSelect>>', index.GetWindowIndex)
MoveItem = SortActions.Actions()
self.listbox.bind('<B1-Motion>', index.MoveWindowItem)
for item in ["one", "two", "three", "four"]:
self.listbox.insert(END, item)
self.listbox.insert(END, "a list entry")
self.listbox.pack()
#print self.listbox.get(0, END)
return
if __name__ == '__main__':
start = MakeList()
start.BuildMainWindow()
mainloop()
And the second file, the one that I'm having issues with
from FileSort import MakeList
class Actions(MakeList):
#gets the current item that was clicked in the window
def GetWindowIndex(self, event):
w = event.widget
self.curIndex = int(w.curselection()[0])
#moves the current item in the window when clicked/dragged
def MoveWindowItem(self, event):
i = self.listbox.nearest(event.y) #here is where the error is occurring
print i
I assumed since I inherit the MakeList class I should have access. I also tried changing it so I directly accessed MakeList (an object) but instead of the error saying "Actions instance has no...." it said "MakeList has no attribute..."
I posted something previously but I accidentally ran an older version of the code, so I was referencing the wrong error. Sorry if you saw that post. It's gone now
As I see it, there's no reason for the Actions to be in a class ...
#SortActions.py
#gets the current item that was clicked in the window
def GetWindowIndex(self, event):
w = event.widget
self.curIndex = int(w.curselection()[0])
#moves the current item in the window when clicked/dragged
def MoveWindowItem(self, event):
i = self.nearest(event.y) #here is where the error is occurring
print i
Now you can use the actions:
...
def BuildListbox(self):
#self.listbox = Tkinter.Listbox() #??? This has no master widget ...
#Since this is already a listbox, there's no point in building another ...
self.bind('<<ListboxSelect>>', lambda e:SortActions.GetWindowIndex(self,e))
self.bind('<B1-Motion>', lambda e:SortActions.MoveWindowItem(self,e)
for item in ("one", "two", "three", "four"):
self.insert(END, item)
self.insert(END, "a list entry")
self.pack()

Categories

Resources