Calling variables located in a method of another class (Tkinter) - python-3.x

I am a beginner at this, so I appreciate your patience and understanding with regards to the problem I have got.
Background
What I am using: macOS, Python3, ATOM
I am trying to build a library that stores information on books available (Title, Author, Year, ISBN). My plan for this is to create a script for backend and frontend separately. Ultimately connecting it all on the frontend script through importing the backend script and using the functions designed there. And yes, I have used OOP before but only for building a blackjack game. tkinter's usage of class is kinda wack for me and I got lost.
Current Situation
I have the UI looking just the way I want it to and am currently creating functions to be used for commands attached to buttons. The issue is, I have the entry widget and the ScrolledText widget in two different classes representing two different frames (Top and Bottom) and while calling for the variable
title.val = tk.StringVar()
an error popped up:
Traceback (most recent call last):
/LIBfront.py", line 105, in <module>
class main():
/LIBfront.py", line 111, in main bottomleft = BottomFrame(win)
/LIBfront.py", line 52, in __init__ self.search_cmd()
/LIBfront.py", line 64, in search_cmd for row in LIBback.search_entry(TopFrame.title_val.get(), self.author_val.get(), year_val.get(), isbn_val.get()):
AttributeError: type object 'TopFrame' has no attribute 'title_val'
Here is a reduced version of the code, containing only one of the sections. EDIT: changes done based on feedback
import tkinter as tk
import tkinter.scrolledtext as tkst
import LIBback # This is just the backend script
# Creating top frame for user input
class TopFrame():
def __init__(self, win):
self.win = win
self.frame = tk.Frame(win, relief = 'groove')
self.frame.pack(fill = 'x', expand = False)
self.topwidgets()
def topwidgets(self):
self.title_val = tk.StringVar()
self.lbl1 = tk.Label(self.frame, text = 'Title:')
self.e1 = tk.Entry(self.frame, width = 25, textvariable = self.title_val)
self.lbl1.grid(row = 0, column = 0, ipadx = 10, ipady = 10)
self.e1.grid(row = 0, column = 1, sticky = 'e')
# Creating bottom frame for user interaction and results display
class BottomFrame():
def __init__(self, win):
self.win = win
self.frame1 = tk.Frame(win)
self.frame1.pack(fill = 'both', side = "left", expand = False)
self.frame2 = tk.Frame(win)
self.frame2.pack(fill = 'both', side = "left", expand = True)
self.widgets()
self.search_cmd()
def search_cmd(self):
self.txtbox.delete('1.0',tk.END) # Below this line is where the issue began
for row in LIBback.search_entry(self.title_val.get()):
self.txtbox.insert(tk.END, row)
def widgets(self):
self.button2 = tk.Button(self.frame1, text = 'Search Entry', height = 2 , width = 10, command = self.search_cmd)
self.button2.pack(side = 'top', fill = 'y', pady = 5, padx = 5)
def main():
win = tk.Tk()
win.title('Book Shop')
win.geometry("630x370")
top = TopFrame(win)
bottom = BottomFrame(top)
win.mainloop()
if __name__ == '__main__':
main()
What I have tried or tried to understand
The variable of interest is not declared as an attribute even though the method is declared and using that variable.
Accessing it through TopFrame.topwidgets.title_val.get(), in the form of [CLASS.FUNCTION.VAR.get()] did not work for me.
Mimicking other examples of OOP using tkinter whereby there is a 'master'? class, and declaring self.var = tk.StringVar() there and using a 'controller' to refer to it. Failed due to my lack of understanding in doing it that way.
My Question
How do I, in this case, call on that variable? and possibly, can you walk me through the process as to why it failed to declare it as an attribute of the class or how I failed to make them connect to each other?
Thank you very much for your help in advance! Really appreciate it!

You should pass top as an argument to BottomFrame so that title_val inside TopFrame can be accessed inside BottomFrame:
class BottomFrame():
def __init__(self, win, top):
self.win = win
self.top = top
...
def search_cmd(self):
self.txtbox.delete('1.0',tk.END)
for row in LIBback.search_entry(self.top.title_val.get()):
self.txtbox.insert(tk.END, row)
...
def main():
win = tk.Tk()
win.title('Book Shop')
win.geometry("630x370")
top = TopFrame(win)
bottom = BottomFrame(win, top) # pass top to BottomFrame
win.mainloop()

Related

Code using Tkinter works in IDLE but not in terminal

This is (part of) my very first program. It uses Tkinter to output text in a scrolled textbox. I used Python 3.6.4 and IDLE and it works perfectly, but when I run it from terminal/Atom once I click ok after selecting the options from the dropdown menus it just closes without errors, while in IDLE it correctly outputs all the text in the textbox.
I want to use Py2app to make a standalone, but for this the code needs to execute properly from terminal.
Here are the main snippets from the code. I'm just coding for a few months so any detailed help would be much appreciated.
from tkinter import *
from collections import OrderedDict
from tkinter.scrolledtext import *
from collections import Counter
master = Tk()
master.title("App")
master.geometry("600x665")
master.lift()
master.attributes('-topmost', True)
mvar = IntVar()
mvar1 = IntVar()
var = StringVar(master)
var.set("Asc")
var1 = StringVar(master)
var1.set("Ar")
x = OptionMenu(master, var, "Ar", "Ta", "Ge","Can","Le","Vi","Li","Sc","Sa","Cap","Aq","Pi")
x.grid(column =2,row =1)
x1 = OptionMenu(master, var1, "Ar", "Ta", "Ge","Can","Le","Vi","Li","Sc","Sa","Cap","Aq","Pi")
x1.grid(column =2,row =2)
def redirector(inputStr):
txt.insert(INSERT, inputStr)
sys.stdout.write = redirector
def ok():
redirector("Thanks for using the app")
master.quit()
label1 = Label(text=" Welcome to the app",bg="#C2DFFF",font=("Times New Roman",18))
label1.grid(column=0,row=0)
label2 = Label(text="Ma: ",bg="#C2DFFF")
label2.grid(column=0,row=2)
txt = ScrolledText(master, bg="#C2DFFF", width = 97, height= 25, font = "Arial 11")
txt.grid(column = 0, row = 14, columnspan=3)
button = Button(master, text="OK", default ='active',command=ok).grid(column=2,row=11)
button = Button(master, text="Reset", default ='active',command=reset).grid(column=2,row=12)
button = Button(master, text ="Cancel",command = cancel).grid(column=0,row=11)
C1 = Checkbutton(master, state = ACTIVE, variable = mvar)
C1.grid(column = 1, row=2)
C2 = Checkbutton(master, state = ACTIVE, variable = mvar1)
C2.grid(column = 1, row=3)
master.mainloop()
This is how the GUI looks like
You cannot generally reassign sys.stdout.write - it is normally a read-only attribute of a built-in file object. The proper way to do output redirection is to assign a new object to sys.stdout, that has a write() method.
Your code works in IDLE only because IDLE has already replaced the built-in sys.stdout with its own redirection object, which is fully modifiable.

Print Result to Canvas Not to the shell window

Attached is part of a GUI i have constructed in Tkinter using a canvas, hence makes it possible to insert an image in the background.
When I call the function Relay_1: the result is sent to the Python shell window.
What i would like is a text box in the canvas, and show the result (i.e print the result) on the canvas and not in the shell.
Any ideas would be appreciated.
import Tkinter
#Function
def Relay_1():
arduinoData.write(b'1')
print ("This is a test\n")
class ProgramGUI:
def __init__(self):
# Creates the Main window for the program
self.main = tkinter.Tk()
# Change Default ICON
self.main.iconbitmap(self,default = 'test.ico')
# Create title, window size and make it a non re sizable window
self.main.title('Test')
self.main.resizable(False, False)
self.main.geometry('680x300')
self.canvas = tkinter.Canvas(self.main, width = 680, height = 300)
self.canvas.pack()
self.logo = tkinter.PhotoImage(file = 'test.png')
self.canvas.create_image(0,0,image = self.logo, anchor = 'nw')
# Create 3 Frame to group the widgets
self.top = tkinter.Frame(self.main)
self.middle = tkinter.Frame(self.main)
self.bottom = tkinter.Frame(self.main)
etc etc
The tkinter canvas widget has a very simple and easy to use method to draw text called create_text(). You can use it this way,
self.canvas.create_text(10, 10, text='This is a test\n')
The text can be customized by passing a wide range of arguments including font,fill and justify. Check the full list of passable arguments here:http://effbot.org/tkinterbook/canvas.htm#Tkinter.Canvas.create_text-method
To add the text when the code is executed, you could create a class inside ProgramGUI() method:
def draw_text(self, text):
self.canvas.create_text(10, 10, text=text)
And use it after creating an object.

Trying to embed tkinter checkboxes in a text box, within a toplevel

I am working on a simple database that tracks bicycle inventory. Deep within my program I am hitting a snag that has stumped me all day. I am trying to create a list of checkboxes in a text box, (to make use of the scrollbar), all within a toplevel popup.
I can't upload my entire code, it's just too much, but here is the snippet dumbed down to get the thing working:
class Delete_bike:
def __init__(self, master):
self.master = master
top = self.top = tk.Toplevel(self.master)
text_frame = tk.Frame(self.top)
text_frame.grid(row = 0, padx = 10, pady = 10)
scb = tk.Scrollbar(text_frame)
scb.grid(row = 0, column = 1, sticky = 'ns')
d_tb = tk.Text(text_frame, height = 8, width = 40, yscrollcommand = scb.set)
d_tb.configure(font = ('Times', 10, 'bold'))
d_tb.grid(row = 0, column = 0)
scb.config(command = d_tb.yview)
test_d = {}
for i in range(10):
test_d[i] = 0
for i in test_d:
test_d[i] = tk.IntVar()
cb = tk.Checkbutton(text = i, variable = test_d[i])
d_tb.window_create(tk.END, window = cb)
d_tb.insert(tk.END, '\n')
The true version makes use of drawing from a shelved dictionary to populate the extensive list of bikes.
When I run this, I get the following exception, which I do not understand at all:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python34\lib\tkinter\__init__.py", line 1538, in __call__
return self.func(*args)
File "C:\Users\Gregory\Desktop\Bike Roster v2.0.pyw", line 115, in delete_bike
x = Delete_bike(self.master)
File "C:\Users\Gregory\Desktop\Bike Roster v2.0.pyw", line 239, in __init__
d_tb.window_create(tk.END, window = cb)
File "C:\Python34\lib\tkinter\__init__.py", line 3296, in window_create
+ self._options(cnf, kw))
_tkinter.TclError: can't embed .52341336 in .52340888.52341000.52341112
So next, I copied the snippet to a stand alone program, copied next, which works perfectly fine. So can I not achieve my desired result in a toplevel? Granted I am new at all this and have no formal training or instruction on programming, but that seems to be the only difference I can see.
import tkinter as tk
from tkinter import ttk
import tkinter.scrolledtext as tkst
class Delete_bike:
def __init__(self, master):
self.master = master
# top = self.top = tk.Toplevel(self.master)
text_frame = tk.Frame(self.master)
text_frame.grid(row = 0, padx = 10, pady = 10)
scb = tk.Scrollbar(text_frame)
scb.grid(row = 0, column = 1, sticky = 'ns')
d_tb = tk.Text(text_frame, height = 8, width = 40, yscrollcommand = scb.set)
d_tb.configure(font = ('Times', 10, 'bold'))
d_tb.grid(row = 0, column = 0)
scb.config(command = d_tb.yview)
test_d = {}
for i in range(10):
test_d[i] = 0
for i in test_d:
test_d[i] = tk.IntVar()
cb = tk.Checkbutton(text = i, variable = test_d[i])
d_tb.window_create(tk.END, window = cb)
d_tb.insert(tk.END, '\n')
root = tk.Tk()
app = Delete_bike(root)
root.mainloop()
If I remove the hash-tag to activate the toplevel line of code and place the frame inside the toplevel, it generates the same error message. Left like this, it works.
And a second quick question, if I am doing something wrong here, and this can be achieved within a toplevel, can the scrolledtext module be used in lieu of the text box and scrollbar combination?
Thanks as always!
You aren't specifying the parent for the checkbutton, so it defaults to the root window. You can't have a widget with a parent in the root, but try to display it in a toplevel.
Make the checkbutton a child of the text widget.
cb = tk.Checkbutton(d_tb, ...)
Yes, you can use scrolledtext instead of a text and a scrollbar, but I don't see any advantage in doing so.

Change the display text in a Tkinter label widget based on a condition

The code below is what I'm referencing
from tkinter import *
root = Tk()
#Variables
answer = "Enter Answer"
data = ""
#Functions
def function():
data = e.get()
while data == "":
if data == 5:
answer = "Correct"
if data != 5:
answer = "Incorrect"
print(answer)
top = Label(root, text = "Test")
top.pack()
e = Entry(root)
e.pack()
e.focus_set()
b = Button(root, text = "Enter", command = function)
b.pack()
check = Label(root, text = answer)
check.pack()
mainloop()
I can't seem to update the label widget (name 'check').
I want to be able to update it based checking a condition, but I can't get it to work.
I placed the 'print(answer)' line to check the variable, but I get the error:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python34\lib\tkinter\__init__.py", line 1533, in __call__
return self.func(*args)
File "G:/Portable Apps/Portable Python 3.2.5.1/Documents/TEST.py", line 22, in function
print(answer)
UnboundLocalError: local variable 'answer' referenced before assignment
This occurs when I run the program, type a value, then select the enter button.
Your answer variable is not defined within the function's scope. To accomplish this in the simplest way I suggest you use a class to hold all of the widgets from your UI.
from tkinter import *
class Window():
def __init__(self, root):
self.top = Label(root, text = "Test")
self.top.pack()
self.e = Entry(root)
self.e.pack()
self.e.focus_set()
self.b = Button(root, text = "Enter", command = self.function)
self.b.pack()
self.answer = StringVar()
self.answer.set("Enter answer")
self.check = Label(root, text = self.answer.get(), textvariable = self.answer)
self.check.pack()
#Functions
def function(self):
data = self.e.get()
if data == "5":
self.answer.set("Correct")
else:
self.answer.set("Incorrect")
root = Tk()
w = Window(root)
root.mainloop()
Additionally, since widget command's work as callbacks, you won't need to use a while loop to accomplish what you want. Just put the if/else check, as I have done, and every time you click the button it will check again.
I also changed your answer variable to be an instance of StringVar(). This is a type of Tkinter variable specifically designed to accomplish what you're trying to do. I can then use the config option textvariable = self.answer to allow the label to update whenever the StringVar is changed. To access the text of the StringVar you must call self.answer.get(); to change the data you call self.answer.set("text") like I have done within function.
Finally, since your Entry self.e is an instance of StringVar as well, I had to change your if condition to if data == "5": as data will be a String rather than an int.

How to display text of listbox items in the canvas with Python3x tkiner?

I am trying to display the text of the selected item of a listbox into the canvas. When I bind the listbox to a helper event handler, it throws Attribute Error: CLASS object has no attribute "HELPER EVENT HANDLER".
What I want is as follows:
1) When double clicking an item in the listbox to the left, its text should be displayed on the canvas. This particular line of code is causing all the troulbe to me.
lstbox.bind("<Double-Button-1>", self.OnDouble)
Could you please help me fixing this error?
2) I believe that there must be a way to make the lines' height on the listbox larger than they appear in my application. However, I don't know how to do it. I tried providing several options but these options are not recognized by tkinter. Could you please suggest to me how to do it?
Here is the code:
import tkinter as tk
languages = ['Mandarin', 'English', 'French']
class LanguageFamilies(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
canv = tk.Canvas(self, width=675, height=530, bg="white", relief="sunken")
canv.config(scrollregion=(0,0,300,650), highlightthickness=0)
canv.pack(side="right", expand=True, fill="both")
# Create scroll bar
sbar = tk.Scrollbar(self)
canv.config(yscrollcommand=sbar.set)
sbar.config(command=canv.yview)
sbar.pack(side="right", fill="both")
# Create Scroll List
lstbox = tk.Listbox(self, width=240, height=530, relief="sunken", font="Courier")
lst_scrollbar = tk.Scrollbar(self)
lstbox.config(yscrollcommand=lst_scrollbar.set)
lst_scrollbar.config(command=lstbox.yview)
lstbox.pack(side="left", expand=True, fill="both")
lst_scrollbar.pack(side="right", fill="both")
lstbox.bind("<Double-Button-1>", self.OnDouble) # THIS IS THE LINE CAUSING THE ERROR
# Add items to the lstbox
i = 0
while i < len(languages):
lstbox.insert(i, languages[i])
i += 1
# Create a text inside canvas
canv_id = canv.create_text(50, 50, font="Times 14", anchor="nw")
msg = "This is a text inside canvas."
canv.insert(canv_id, "end", msg)
#Binding Handler
def OnDouble(self, event):
self.widget = event.widget
selection = self.widget.curselection()
content = self.widget.get(selection[0])
print("You selected", content)
if __name__ == "__main__":
root = tk.Tk()
root.geometry("930x530")
root.title("Language Families")
LanguageFamilies(root).pack(fill="both", expand=True)
root.mainloop()
And this is the error message:
Traceback (most recent call last):
File "C:/Python33/new_stack_overflow.py", line 43, in <module>
LanguageFamilies(root).pack(fill="both", expand=True)
File "C:/Python33/new_stack_overflow.py", line 23, in __init__
lstbox.bind("<Double-Button-1>", self.OnDouble)
AttributeError: 'LanguageFamilies' object has no attribute 'OnDouble'
Your help is highly appreciated!
the problem is that the def for OnDouble is defined inside __init__, making it not a class method but a method inside the scope of __init__. You need to remove one level of indentation for OnDouble.
HERE IS THE COMPLETE AND CORRECT ANSWER! BELOW IS STAGES OF SOLVING THE PROBLEM.
Bryan's suggestions of decreasing the indentation of the OnDouble event handler by one level were the first steps to the solution.
The crucial thing I learnt from this experience is that printing the text of a listbox on the canvas directly is not the right option. Rather, the best option is to put a text widget on the canvas so that manipulating text matters will be much easier. I came up to this conclusion after watching a nice video tutorial here. As such, a text widget was defined ontop of the canvas. An if condition was used to control what to print on the text-canvas and how many times it should be printed - (in this case, only onece exactly the same as an electronic dictionary - which is my application).
Here is the COMPLETE WORKING code:
import tkinter as tk
languages = ['Mandarin', 'English', 'French']
global counter
counter = 1
class LanguageFamilies(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
global canv, canv_id # IN ORDER TO MAKE THEM ACCESSIBLE TO OnDouble EVENT HANDLER
canv = tk.Canvas(self, width=675, height=530, bg="white", relief="sunken")
canv.config(scrollregion=(0,0,300,650), highlightthickness=0)
canv.pack(side="right", expand=True, fill="both")
# Create scroll bar
sbar = tk.Scrollbar(self)
canv.config(yscrollcommand=sbar.set)
sbar.config(command=canv.yview)
sbar.pack(side="right", fill="both")
# Create Scroll List
lstbox = tk.Listbox(self, width=240, height=530, relief="sunken", font="Courier")
lst_scrollbar = tk.Scrollbar(self)
lstbox.config(yscrollcommand=lst_scrollbar.set)
lst_scrollbar.config(command=lstbox.yview)
lstbox.pack(side="left", expand=True, fill="both")
lst_scrollbar.pack(side="right", fill="both")
lstbox.bind("<Double-Button-1>", self.OnDouble)
# Add items to the lstbox
i = 0
while i < len(languages):
lstbox.insert(i, languages[i])
i += 1
# Create a text inside canvas
global textmatter
textmatter = tk.Text(canv) # THE TEXT SHOULD BE PRINTED ON THE CANVAS
textmatter.insert("end", "Double-click on a language on the left to view information.")
textmatter.pack()
#Binding Handler, ONE INDENTATION LEVEL IS REMOVED AS PER BRYAN'S SUGGESTION
def OnDouble(self, event):
global counter
self.widget = event.widget
selection = self.widget.curselection()
content = self.widget.get(selection[0])
if counter == 1:
textmatter.delete(1.0, "end") # CLEAR THE TEXT WHICH IS DISPLAYED AT FIRST RUNNING
textmatter.insert("end", content)
counter += 1 #ONE ITEM HAS BEEN SELECTED AND PRINTED SUCCESSFULLY
elif counter > 1:
textmatter.delete(1.0, "end")
textmatter.insert("end", content)
counter = 1 # RESET THE COUNTER SO THAT NEXT SELECTED ITEM DISPLAYS PROPERLY
if __name__ == "__main__":
root = tk.Tk()
root.geometry("930x530")
root.title("Language Families")
LanguageFamilies(root).pack(fill="both", expand=True)
root.mainloop()
That is it. I hope this post will be of help to you!
SALAM,
Mohammed

Resources