I have built a Python tkinter GUI application which is an application for running different tasks. The application window is divided into 2 halves horizontally, first half shows the options the user can choose for the selected menu option and second half shows the progress of the task by showing the log messages. Each task has a separate menu option, the user selects the menu option and first half is refreshed with user option along with a Submit button.
The GUI is built using the object oriented method where each task in the menu option is an class method of the GUI object.
I now have about 5-6 menu options and working fine but the code size is becoming huge and it is becoming hard to debug any issue or add new features.
Is there any way to write the method of a class in separate file which can be called from within the main class. The logging of messages in the GUI is written in the main class so if the method is written in a separate file the how will the log messages written in the other file appear in the main window.
Please suggest alternatives.
This might not help you completely, but this is what I use. I divide my tkinter code into 2 files. First gui.py contains the GUI components (widgets) and the second methods.py contains the methods.
Both the files should be in same directory.
Here is an example of a simple app that changes the label on a button click. The method change() is stored in a different file.
gui.py
from tkinter import *
from tkinter import ttk
from methods import change #Using absolute import instead of wildcard imports
class ClassNameGoesHere:
def __init__(self,app):
self.testbtn = ttk.Button(app,text="Test",command = lambda: change(self))
#calling the change method.
self.testbtn.grid(row=0,column=0,padx=10,pady=10)
self.testlabel = ttk.Label(app,text="Before Button Click")
self.testlabel.grid(row=1,column=0,padx=10,pady=10)
def main():
root = Tk()
root.title("Title Goes Here")
obj = ClassNameGoesHere(root)
root.mainloop()
if __name__ == "__main__":
main()
methods.py
from tkinter import *
from tkinter import ttk
def change(self):
self.testlabel.config(text="After Button Click")
Related
Following is code for a tkinter listbox, created as a class, and saved as a module named ModListbox:
import tkinter as tk
class Lstbox(tk.Listbox):
def __init__(self,master,listname):
super().__init__(master)
# insert list
self.insert(0,*listname)
# place listbox
self.place(x=10,y=10)
Likewise, following is the simplest code for a main app that imports the module and displays the listbox.
import tkinter as tk
root=tk.Tk()
MyList = ['first','second','third','fourth','fifth']
import ModListbox
NewListbox = ModListbox.Lstbox(master=root,listname=MyList)
root.mainloop()
In order to add functionality to the listbox, the obvious approach is to load the module and create an instance of the class, then to bind the listbox to a function, as follows, within the main application code. So clicking on the listbox triggers an event.
import tkinter as tk
root=tk.Tk()
# function on user selection in listbox
def UserClickedNewListbox(event):
SelectedIndex = NewListbox.curselection()[0]
SelectedText = NewListbox.get(SelectedIndex)
print("You selected ", SelectedText)
MyList = ['first','second','third','fourth','fifth']
import ModListbox
NewListbox = ModListbox.Lstbox(master=root,listname=MyList)
# bind listbox to function
NewListbox.bind('<<ListboxSelect>>',UserClickedNewListbox)
root.mainloop()
However, one reason among many that I'm creating tkinter widgets as classes is to keep as much code in the modules as possible, and reduce the code in the main app. So here is the same module, but with the binding and function included inside the module.
import tkinter as tk
class Lstbox(tk.Listbox):
def __init__(self,master,listname):
super().__init__(master)
# insert list
self.insert(0,*listname)
# place listbox
self.place(x=10,y=10)
# bind user selection to function
self.bind('<<ListboxSelect>>',self.UserClickedListbox)
def UserClickedListbox(self,event):
SelectedIndex = self.curselection()[0]
# How do I trigger an action inside the main app from here?
So now, inside the module itself, a user click on the listbox triggers the function. The function 'knows' the index of the listbox clicked by the user. How, if it is possible at all, would I trigger an event inside the main application, while also passing the selected index? Is this possible? Any advice appreciated.
My GUI app has two files: gui.py that contains all the Tkinter objects and controller.py contains the logic. The logic is one main function def automation(): that nests several other functions. The app is very simple it's only one button that calls automation().
I would like to add the print statements and errors that appear in the terminal in the GUI widget so that the user can see what's going on. I can't find a way to do that for imported modules.
gui.py
import tkinter as tk
from tkinter import *
from controller import automation
root = tk.Tk()
frame_button = tk.Frame(root)
button = Button(frame_button, text="Ship", command=lambda:automation())
lower_frame = tk.Frame(root)
terminal = tk.Label(lower_frame)
frame_button.place()
button.place()
lower_frame.place()
terminal.place()
controller.py
def automation():
def folder_cleaner():
print('Folders cleaned')
def dispatch():
print('Dispatch done')
def ship():
print('Shipment successful')
def process():
folder_cleaner()
dispatch()
ship()
process()
This is very simplified but each function has many different kinds of outputs. How can I redirect all of them to gui.py and inside the terminal widget?
To show the output in the UI, I've added an output function in gui.py. So the function can be used in controller.py, I've added a parameter outputFunc when calling automation. This can then be used instead of print to display the strings in the UI.
gui.py
import tkinter as tk
from tkinter import *
from controller import automation
def output(text):
terminal.insert("end", text + "\n")
root = tk.Tk()
frame_button = tk.Frame(root)
button = Button(frame_button, text="Ship", command=lambda:automation(output))
lower_frame = tk.Frame(root)
#Changed to text widget as it is more ideal for this purpose
terminal = tk.Text(lower_frame)
#Changed place to pack so widgets actually display
frame_button.pack()
button.pack()
lower_frame.pack()
terminal.pack()
controller.py
def automation(outputFunc):
def folder_cleaner():
outputFunc('Folders cleaned')
def dispatch():
outputFunc('Dispatch done')
def ship():
outputFunc('Shipment successful')
def process():
folder_cleaner()
dispatch()
ship()
process()
Here is one approach to Your issue (this works especially well if Your automation runs in a loop or basically is not as fast, it will make sure to update when any change happens):
from tkinter import Tk, Button, Label, Frame
from _thread import start_new_thread
from time import sleep
values = {'random_values': []}
def automation():
for i in range(10):
values['random_values'].append(i)
sleep(3)
def watch_values():
compare = None
while True:
value = str(values['random_values'])
if compare != value:
label.config(text=values['random_values'])
compare = value
root = Tk()
Button(root, text='start', command=lambda: start_new_thread(automation, ())).pack()
label = Label(root)
label.pack()
start_new_thread(watch_values, ())
root.mainloop()
So basically first it starts a thread that watches the values dictionary (it is important for it to interact with tkinter only when there are changes tho because otherwise it will cause issues with tkinter GUI so that is why there is compare).
Whenever there is a change in that particular variable it sets the text of the label as that variable (variable being values['random_values']).
Then there is the automation function. I suggest that You put everything it returns in a dictionary that will be watched by the other thread. You also need to thread Your automation function so that it can work in parallel. (And You can obviously import that function from another file, just add it to the threads, also it may seem weird but those empty tuples there are necessary since that is an argument (in those tuples it is also possible to put the function argument if You have those but in this case there are none))
And You only need one watcher function, just add more compares and checks to that loop and so on (mentioned in case You wanted to create such a function for every key in the dictionary which is completely unnecessary)
I have my principal script running with terminal that works perfectly. Im trying to make a gui for it but im stuck at this point.
Like you see on the screen, at the start of the script it asks if it should check the database. And just after, it asks first the platform before opening the captcha for the database check. The problem happens exactly here on my GUI version, look.
Like you see, the gui starts, but when i click on check for new database, it directly opens the captcha without asking the platform... And it asks me the platform only after i solved the captcha which i dont want to after...
Here is the main testkinter.py code:
import tkinter as tk
from tkinter import messagebox
import commands
import CheckDatabase
import SetPlatformfile
def check_and_hide():
CheckDatabase.db_download(root)
checkdb.pack_forget()
checkdb1.pack_forget()
root = tk.Tk()
checkdb = tk.Button(root, text="Check for new databases", command=check_and_hide)
checkdb.pack()
checkdb1 = tk.Button(root, text="No")
checkdb1.pack()
root.mainloop()
Here is the set_platform function called in the Checkdatabse file:
import tkinter as tk
import config
from tkinter import messagebox
def set_platform(root):
platform = tk.Label(root,text="'a'|Android -- 'i'|iOS: ")
platform.pack()
androidbutton=tk.Button(root,text="Android",command=renameplatformandroid)
iosbutton=tk.Button(root,text="iOS",command=renameplatformios)
androidbutton.pack()
iosbutton.pack()
def renameplatformandroid():
config.platform = 'android'
print(config.platform)
def renameplatformios():
config.platform = 'ios'
print(config.platform)
And cuz of my checkdatabase file is really really long, i'll just put a screen at the exact moment where set_platform is called (its called in the func signup which itself is directly called at the beginning of db_download) .
I hope my question is clear! Let me know if you need more details.
i'm hoping anyone can help me out here. i'm having an issue with a tkinter gui i built. the issue only happens in windows. My GUI creates a results frame with some labels in it, when it's time to calculate something else, the user clicks on the "newPort" button and that button is supposed to remove the results frame and set to False some instance attributes internal to the calculation. The issue i'm having, which is apparent only in windows is that sometimes the results frame, and its descendant labels don't disappear every time. Sometimes they do, sometimes they don't. The instance variable is correctly set to False but the widgets are still visible on the main GUI. The GUI also contains a couple checkboxes and radiobuttons but they don't impact the creation of the results frame nor its expected destruction. I have not been able to pin point a pattern of actions the user takes before clicking on the newPort button which causes the frame and labels to not get destroyed. This happens when i freeze my app with py2exe, as well as running the app from the python interpreter within the eclipse IDE. I have not tried running the app from the python interpreter directly (i.e. without the IDE) and this problem does not happen on my Mac when i run the app using the eclipse python interpreter. Thanks very much all! My code looks like this:
import Tkinter as TK
class widget(object):
def __init__(self,parent=None):
self.parent = TK.Frame(parent)
self.parent.grid()
self.frame = TK.Frame(self.parent)
self.frame.grid()
newLedger = TK.Button(self.parent,command=self.newPort).grid()
self.calcButton = TK.Button(self.frame,command=self.showResults)
self.calcButton.grid()
self.calcVariable = True
def newPort(self):
self.calcVariable = False
try:
self.second.grid_forget()
self.first.grid_forget()
self.resultsFrame.grid_forget()
self.second.destroy()
self.first.destroy()
self.resultsFrame.destroy()
except:
raise
self.frame.update_idletasks()
def showResults(self):
self.resultsFrame = TK.Frame(self.frame)
self.resultsFrame.grid()
self.first = TK.Label(self.resultsFrame,text='first')
self.first.grid()
self.second = TK.Label(self.resultsFrame,text='second')
self.second.grid()
if __name__ == '__main__':
root = TK.Tk()
obj = widget(root)
root.mainloop()
You don't need to destroy or call grid_forget on the labels, and you don't need to call grid_forget on the resultsFrame; when you destroy the resultsFrame it will cause all off its children to be destroyed, and when these widgets are destroyed they will no longer be managed by grid.
The only way I can get widgets to not be destroyed is if I click on the "calc" button twice in a row without clicking on the "new" button in-between. I'm doing this by running your program from the command line.
I have written a short module that can be passed an image and simply creates a Tkinter window and displays it. The problem that I am having is that even when I instantiate and call the method that displays the image in a separate thread, the main program will not continue until the Tkinter window is closed.
Here is my module:
import Image, ImageTk
import Tkinter
class Viewer(Tkinter.Tk):
def __init__(self,parent):
Tkinter.Tk.__init__(self,parent)
self.parent = parent
self.initialize()
def initialize(self):
self.grid()
def show(self,img):
self.to_display = ImageTk.PhotoImage(img)
self.label_image = Tkinter.Label(self,image=self.to_display)
self.label_image.grid(column = 0, row = 0, sticky = "NSEW")
self.mainloop()
It seems to work fine, except when I call it from my test program like the one below, it will not seem to allow my test program to continue, even when started in a different thread.
import Image
from viewer import Viewer
import threading
def showimage(im):
view = Viewer(None)
view.show(im)
if __name__ == "__main__":
im = Image.open("gaben.jpg")
t = threading.Thread(showimage(im))
t.start()
print "Program keeps going..."
I think that perhaps my problem is that I should be creating a new thread within the module itself, but I was wanting to just try and keep it simple, as I am new to Python.
Anyway, thanks in advance for any assistance.
edit: To clarity, I am just trying to make a module that will display an image in a Tkinter window, so that I can use this module any time I want to display an image. The problem that I am having is that any time a program uses this module, it cannot resume until the Tkinter window is closed.
Tkinter isn't thread safe, and the general consensus is that Tkinter doesn't work in a non-main thread. If you rewrite your code so that Tkinter runs in the main thread, you can have your workers run in other threads.
The main caveat is that the workers cannot interact with the Tkinter widgets. They will have to write data to a queue, and your main GUI thread will have to poll that queue.
If all you're doing is showing images, you probably don't need threading at all. Threading is only useful when you have a long running process that would otherwise block the GUI. Tkinter can easily handle hundreds of images and windows without breaking a sweat.
From your comments it sound's like you do not need a GUI at all. Just write the image to disk and call an external viewer.
On most systems it should be possible to launch the default viewer using something like this:
import subprocess
subprocess.Popen("yourimage.png")
From what I can tell, Tkinter doesn't like playing in other threads. See this post...I Need a little help with Python, Tkinter and threading
The work around is to create a (possibly hidden) toplevel in your main thread, spawn a separate thread to open images, etc - and use a shared queue to send messages back to the Tk thread.
Are you required to use Tkinter for your project? I like Tkinter. It's "quick and dirty." - but there are (many) cases where other GUI kits are the way to go.
I have tried to run tkinter from a separate thread, not a good idea, it freezes.
There is one solution that worked. Run the gui in the main thread, and send events to the main gui. This is similar example, it just shows a label.
import Tkinter as t
global root;
root = t.Tk()
root.title("Control center")
root.mainloop()
def new_window(*args):
global root
print "new window"
window = t.Toplevel(root)
label = t.Label(window, text="my new window")
label.pack(side="top", fill="both", padx=10, pady=10)
window.mainloop()
root.bind("<<newwin>>",new_window)
#this can be run in another thread
root.event_generate("<<newwin>>",when="tail")