Efficient multi window tkinter - python

So I am writing this code with multiple states where each state is represented with its own Tkinter window. the question is how to do this efficiently knowing that each window is defined as a class on a separate file. I'm doing this right now but I'm not sure this okay as the interface seems to be lagging compared to when I launch the class directly (not from the method call)
def StartExperimentButtonCallback(self):
ErrorMessage=''
#build error message
if(len(ErrorMessage)>0):
ErrorMessage += '\n'
messagebox.showerror("Error", ErrorMessage)
else:
self.parent.destroy()
factory = eego_sdk.factory()
v = factory.getVersion()
print('version: {}.{}.{}.{}'.format(v.major, v.minor, v.micro, v.build))
print('delaying to allow slow devices to attach...')
time.sleep(1)
amplifier=factory.getAmplifiers()
amplifier=amplifier[0]
root = tk.Tk()
# This is the section of code which creates the main window
root.geometry('1280x740')
root.configure(background='#FFFFFF')
root.title('Impedances')
timer = ImpedanceGUI(root, amplifier, self.SubjectID.get(), [float(self.RightSphere.get()),float(self.LeftSphere.get()),float(self.RightCylinder.get()), float(self.LeftCylinder.get())])
del self
root.mainloop()

Related

How can I open multiple popups in succession?

I'm currently working on an application written in Kivy in python.
I have 2 multiprocessing.Processes running:
One is a process for the RFID-reader, I open a popup that lets me choose a screen to go to.
The second process is for Serial communication.
I use queues to communicate with my main thread, and that works great. The problem I'm facing at the moment is I need to introduce a learning process to my Kivy program.
I have 4 screens which I call
mainscreen
adminscreen
managementscreen
initialscreen <- This screen runs only once, once it's set, the screen won't be accessable anymore.
This is the function that gets called when I push the button inside the initialscreen:
def startLearningProcess(self, lockeramount):
self.lockeramount = int(lockeramount)
with Database(self.databasepath) as db:
db.truncate_table('LOCKERS')
# resetting primary key sequence to make the ids start from 1
db.reset_primary_key_sequence('LOCKERS')
for locker in range(int(lockeramount)):
# use a with statement to automatically close the db after the operations
with Database(self.databasepath) as db:
# inserting the given amount lockers to the database as rows.
db.insertLockerRow(locker+1,'Pieter')
if I add the following to the function, 5 popups get opened all at once:
while self.lockeramount != 0:
popup = Popup(title='Test popup', auto_dismiss=True, content=Label(text='Hello world'), size_hint=(None, None), size=(400, 400))
popup.open()
self.lockeramount -= 1
When I input the number 5 into my interface, I want to have 5 popups to open up for me one by one. How can I make it so when I push a button I open up 1 popup, instead of all 5 at once? I apologize for my grammar, english is not my first language.
EDIT:
while John's answer worked perfectly, I was looking for another solution that did not use threading. I solved it by doing the following:
In my class InitialScreen(Screen): I added 2 variables, a bool that starts out with False (booleanUp) and a int variable that starts at 0 (lockeramount).
When I enter my def startLearningProcess I set the lockeramount variable to the number I input into my screen. I added an interval to the startLearningProcess function: Clock.schedule_interval(lambda dt: self.schedule_popups(), 1). I then added the following functions:
def close_popup(self, instance):
self.booleanUp = False
def schedule_popups(self):
if self.lockeramount > 0 and not self.booleanUp:
print(f'opening up popup {self.lockeramount}')
popup = Popup(title='MyPopup', content=Label(text='Abba ' + str(self.lockeramount)), size_hint=(0.5, 0.5))
popup.bind(on_dismiss=self.close_popup)
self.lockeramount -= 1
self.booleanUp = True
popup.open()
else:
print('not opening another popup')
When I open a new popup, I set the boolean to true, so that with the next interval it won't open another interval. I made an on_dismiss event that resets the variable back to False and bound it to my popup.
You can use a Queue to make the Popups wait. Define a custom Popup that accepts a Queue in its __init__() method, and sends something (could even be None) to the Queue when it is dismissed. And your loop can use the Queue to wait for the Popups to be dismissed.
Here is a custom Popup that uses a Queue:
class MyPopup(Popup):
queue = ObjectProperty(None)
def dismiss(self, *_args, **kwargs):
super(MyPopup, self).dismiss(*_args, **kwargs)
if self.queue:
self.queue.put(None)
For this to work, you must run it in another thread. Otherwise, waiting for the Queue on the main thread will never end, because holding the main thread will prevent the Popup from dismissing. Here is some code that shows 5 Popups in succession, one at a time:
def doit(self):
threading.Thread(target=self.popup_thread).start()
def popup_thread(self):
self.queue = Queue()
for i in range(5):
Clock.schedule_once(self.show_popup)
self.queue.get()
def show_popup(self, dt):
popup = MyPopup(title='MyPopup', content=Label(text='Abba ' + str(dt)), size_hint=(0.5, 0.5), queue=self.queue)
popup.open()
To start the Popups, just call the doit() method, probably as an action associated with a Button.

Perform modifications in the scene and update custom window from a QThread in Maya

Context
I'm creating a PySide2 tool running in Maya. The tool is executing a lot of long tasks, some modifying the scene (cleaning tasks), some creating files (exporting tasks).
Because this is a long task, I'd like to display feedback (progress bar) while it's running.
Problems
Unfortunately, so far, the whole UI does not seem to be updated during the executing.
Also, because I had odd behaviors (Maya freezing forever) in the real code, I'm guessing this is not a safe use of threads.
Example code
Here is a simplified bit of code showing where I am so far. Is this the right way to use QThread? I'm from a CG Artist background, not a professional programmer, so I'm probably misusing or misunderstanding the concepts I'm trying to use (threads, PySide...)
import time
from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *
import maya.cmds as cmds
class Application(object):
def __init__(self):
self.view = View(self)
def do_something(self, callback):
start = int(cmds.playbackOptions(q=True, min=True))
end = int(cmds.playbackOptions(q=True, max=True))
# First operation
for frame in xrange(start, end + 1):
cmds.currentTime(frame, edit=True)
# Export ...
callback(33)
time.sleep(1)
# Second operation
for frame in xrange(start, end + 1):
cmds.currentTime(frame, edit=True)
# Export ...
callback(66)
time.sleep(1)
# Third operation
for frame in xrange(start, end + 1):
cmds.currentTime(frame, edit=True)
# Export ...
callback(100)
time.sleep(1)
class View(QWidget):
def __init__(self, controller):
super(View, self).__init__()
self.controller = controller
self.thread = None
self.setLayout(QVBoxLayout())
self.progress = QLabel()
self.layout().addWidget(self.progress)
self.button = QPushButton('Do something')
self.layout().addWidget(self.button)
self.button.clicked.connect(self.do_something)
self.show()
def do_something(self):
self.thread = DoSomethingThread(self.controller)
self.thread.updated.connect(lambda progress: self.progress.setText(str(progress) + '%'))
self.thread.run()
class DoSomethingThread(QThread):
completed = Signal()
updated = Signal(int)
def __init__(self, controller, parent=None):
super(DoSomethingThread, self).__init__(parent)
self.controller = controller
def run(self):
self.controller.do_something(self.update_progress)
self.completed.emit()
def update_progress(self, progress):
self.updated.emit(int(progress))
app = Application()
Threads are difficult to use correctly in Maya Python (you can see this from the number of questions listed here)
Generally there are two hard rules to observe:
all work that touches the Maya scene (say selecting or moving an object) has to happen in the main thread
all work that touches Maya GUI also has to happen in the main thread.
"main thread" here is the thread you get when you run a script from the listener, not on you're creating for yourself
This obviously makes a lot of things hard to do. Generally a solution will involve the a controlling operation running on the main thread while other work that does not touch Maya GUI or scene objects is happening elsewhere. A thread-safe container (like a python Queue can be used to move completed work out of a worker thread into a place where the main thread can get to it safely, or you can use QT signals to safely trigger work in the main thread.... all of which is a bit tricky if you're not far along in your programming career.
The good news is -- if all the work you want to do in Maya is in the scene you aren't losing much by not having threads. Unless the work is basically non-Maya work -- like grabbing data of the web using an HTTP request, or writing a non-Maya file to disk, or something else that does not deal with Maya-specific data -- adding threads won't get you any additional performance. It looks like your example is advancing the time line, doing work, and then trying to update a PySide GUI. For that you don't really need threads at all (you also don't need a separate QApplication -- Maya is already a QApplication)
Here's a really dumb example.
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import maya.cmds as cmds
class DumbWindow(QWidget):
def __init__(self):
super(DumbWindow, self).__init__()
#get the maya app
maya_app = QCoreApplication.instance()
# find the main window for a parent
for widget in maya_app.topLevelWidgets():
if 'TmainWindow' in widget.metaObject().className():
self.setParent(widget)
break
self.setWindowTitle("Hello World")
self.setWindowFlags(Qt.Window)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
start_button = QPushButton('Start', self)
stop_button = QPushButton('Stop', self)
self.layout.addWidget(start_button)
self.layout.addWidget(stop_button)
self.should_cancel = False
self.operation = None
self.job = None
# hook up the buttons
start_button.clicked.connect(self.start)
stop_button.clicked.connect(self.stop)
def start(self):
'''kicks off the work in 'this_is_the_work'''
self.operation = self.this_is_the_work()
self.should_cancel = False
self.job = cmds.scriptJob(ie=self.this_makes_it_tick)
def stop(self):
''' cancel before the next step'''
self.should_cancel = True
def this_is_the_work(self):
print "--- started ---"
for frame in range(100):
cmds.currentTime(frame, edit=True)
yield "advanced", frame
print "--- DONE ----"
def bail(self):
self.operation = None
def kill_my_job():
cmds.scriptJob(k=self.job)
print "job killed"
cmds.scriptJob(ie = kill_my_job, runOnce=True)
def this_makes_it_tick(self):
'''
this is called whenever Maya is idle and thie
'''
# not started yet
if not self.operation:
return
# user asked to cancel
if self.should_cancel:
print "cancelling"
self.bail()
return
try:
# do one step. Here's where you can update the
# gui if you need to
result = next(self.operation)
print result
# example GUI update
self.setWindowTitle("frame %i" % result[-1])
except StopIteration:
# no more stpes, we're done
print "completed"
self.bail()
except Exception as e:
print "oops", e
self.bail()
test = DumbWindow()
test.show()
Hitting start creates a maya scriptJob that will try to run whatever operation is in the function called this_is_the_work(). It will run to the next yield statement and then check to make sure the user hasn't asked to cancel the job. Between yields Maya will be busy (just as it would if you entered some lines in the listener) but if you're interacting with Maya when a yield comes up, the script will wait for you instead. This allows for safe user interaction without a separate thread, though of course it's not as smooth as a completely separate thread either.
You'll notice that this kicks off a second scriptJob in the bail() method -- that's because a scriptJob can't kill itself, so we create another one which will run during the next idle event and kill the one we don't want.
This trick is basically how most of the Maya's MEL-based UI works under the hood -- if you run cmds.scriptJob(lj=True) in the listener you'll usually see a lot of scriptJobs that represent UI elements keeping track of things.

Python multiprocessing won't work

Here's the relevant part of the code:
x=None
def pp():
global x
x=MyClass()
x.start()
def main():
global x
p=Process(target=pp)
p.start()
while x==None:
print("Not yet...")
while 1:
print(x.getoutput(),end="")
p.join()
if __name__=='__main__':
main()
The x.start() method opens a TKInter window, so it runs forever (or at least until the user closes the window). I'm trying to run another process that would get information from the used window, but it doesn't work.
How can i make it work?
I feel like the first thing to point out here is that each child process will import the main script and have it's own local copy. It's not possible to use global variables in the way you're trying to here, because the child processes don't use the same namespace. If you're set on using multiprocessing for this, you need to use a communication pipe of some description, as described in the following documentation:
http://pymotw.com/2/multiprocessing/communication.html#multiprocessing-queues
I am a little curious what your ultimate objective is here for using multiprocessing. Still, if you really want to do it, it's possible:
import multiprocessing
import tkinter
import time
def worker(q):
#q is a queue for communcation.
#Set up tkinter:
root = tkinter.Tk()
localvar = tkinter.StringVar()
wind = tkinter.Entry(root,textvariable=localvar)
wind.grid()
#This callback puts the contents of the entry into the queue
#when the entry widget is modified
def clbk(name,index,mode,q=q):
q.put(localvar.get())
localvar.trace_variable('w',clbk)
#Some window dressing that will signal to the main process that
#the window has been closed
def close(root=root,q=q):
q.put('EXIT')
root.destroy()
root.quit()
root.protocol("WM_DELETE_WINDOW",close)
root.mainloop()
def main():
#Make a queue to facilitate communication:
queue = multiprocessing.Queue()
p = multiprocessing.Process(target=worker,args=(queue,))
p.start()
#Wait for input...
while True:
ret = queue.get()
print(ret)
if ret=='EXIT':
break
time.sleep(0.1)
#Finally, join the process back in.
p.join()
if __name__ == '__main__':
main()
Ignoring the window dressing, that will now print text entered into your tkinter entry widget, and exits when the window is closed.

How to get tk to display something before a function is finished

I have this code which downloads the files specified in an online list and whilst doing that, display a loading screen with a label telling of which file is being downloaded and an indeterminate progress bar showing to the user something is happening. The download works very well, but what doesn't work is tkinter, which doesn't work until the process is finished, only showing the "Download Finished" label at the end. How can I get tkinter to display a window before a function is finished?
I have already tried
Spliting it up into multiple functions
Adding a sleep function to see if slowing it down helps
To display this what I am talking about, I have replaced my original code with some examples. Does anyone know how to make tkinter update more actively (before the function has finished)?
#Imports
import urllib.request as ur
import os
from tkinter import *
import tkinter.ttk as ttk
import time as T
#Globals
tk = None
where = None
progressbar = None
progresstitle = None
progressinfo = None
transfer = None
#Make sure that the download directory exists
def checkdir(filename):
directory = os.path.dirname(filename)
try:
os.stat(directory)
except:
os.mkdir(directory)
#First part (read q to see why I split up)
def part1():
#Get Globals
global tk
global where
global progressbar
global progresstitle
global progressinfo
global transfer
#Create Window
tk = Tk()
tk.title("Downloading...")
#Find out the location of the online files to download by reading the online txt file which contains their locations
where = str(ur.urlopen("http://example.com/whatfilestodownload.txt").read())
where = where[2:(len(where)-1)]
where = where.split(";")
#Create the progress bar
progressbar = ttk.Progressbar(tk, orient=HORIZONTAL, length=200, mode='indeterminate')
progressbar.grid(row = 2, column = 0)
#Create the labels
progresstitle = Label(tk, text = "Downloading Files!", font = ("Helvetica", 14))
progresstitle.grid(row = 0, column = 0)
progressinfo = Label(tk, text = "Starting Download...", font = ("Helvetica", 10))
progressinfo.grid(row = 1, column = 0)
#Engage Part Two
part2()
#Part Two
def part2():
#Get Globals
global tk
global where
global progressbar
global progresstitle
global progressinfo
global transfer
#Repeat as many times as files described in the only file describing .txt
for x in where
#The online file contains "onlinelocation:offlinelocation" This splits them up
x1 = x.split(":")[0]
x2 = x.split(":")[1]
#Read the online file and update labels
transfer = None
progressinfo.config(text = str("Downloading " + x2 + "..."))
transfer = str(ur.urlopen("http://example.com/" + x1).read())
progressinfo.config(text = str("Configuring downloaded file..."))
transfer = transfer [2:(len(transfer)-1)]
#Fix python turning "\n" to "\\n" by reversing
transfer = transfer.split("\\n")
transtemp = ""
for x in transfer:
transtemp = transtemp + "\n" + x
transfer = transtemp[1:len(transtemp)]
progressinfo.config(text = str("Installing " + x2 + "..."))
tw = None
checkdir(str(os.getcwd()+"/Downladed/"+x2))
tw = open(str(os.getcwd()+"/Downloaded/"+x2), "w")
tw.write(transfer)
tw.close()
#See if waiting helps
T.sleep(0.5)
part3()
def part3():
#Get Globals
global tk
global where
global progressbar
global progresstitle
global progressinfo
global transfer
#Final Screen
progressbar.grid_remove()
progressinfo.grid_remove()
progresstitle.config(text="You have downloaded\n the required files!")
progressbar.stop()
part1()
If updating the display at the end of each file being downloaded in your part2() function is enough, you can use the update_idletasks() method, putting it in place of T.sleep(), which will allow the GUI to refresh between going back to another iteration of your for loop.
Ref: http://effbot.org/tkinterbook/widget.htm#Tkinter.Widget.update_idletasks-method
This is why Tcl has asynchronous I/O functions. You need to keep processing windowing system events in a timely manner so you cannot wait for a complete file to download. Instead you need to do it in pieces. In Tcl we would use the fileevent command to set a procedure to be called each time some input became available from the socket. The rest of the time we can process other events. In Python the common way to do this is the Twisted package. This allows you to register event sources with twisted and make the entire application event oriented. You could also use threads and do the downloading on worker threads but that doesn't really help you with the progress notifications. There is some special handling to link up Tkinter and Twisted - see the documentation.

how to create an extra infinite loop in an gtk/python program?

New at this, I know that the program is supposed to enter an infinite loop when you call gtk.main. The loop in gtk.main will break when self.quit is called.
But I need another loop active that would check a log file for changes, and keep updating the changes, into a gtk.Textbuffer, to be shown in a gtk.Textbox. So where can i add this loop in the following code.
class MessageBox:
def __init__(self):
builder = gtk.Builder()
builder.add_from_file("mbx.glade")
self.window = builder.get_object("window")
dic = { "on_buttonSend_clicked" : self.sendmsg,
"on_entry_activate" : self.sendmsg,
"on_buttonWhois_clicked" : self.sendwhois,
"on_buttonIdleTime_clicked" : self.sendidletime,
"on_window_destroy" : self.exitfunc}
builder.connect_signals(dic)
self.entry = builder.get_object("entry")
self.display = builder.get_object("display")
self.displaybuff=self.display.get_buffer()
def exitfunc(self, widget):
def sendmsg(self, widget):
def sendwhois (self, widget):
def sendidletime (self, widget):
if __name__ == "__main__":
msgbox = MessageBox()
msgbox.window.show()
gtk.main()
Only exists one mainloop in gtk at the same time, that is, gtk.main enters into a new level and gtk.main_quit exists from that level.
Usually, what you'd need to do is is a create worker thread that keeps working in parallel and updates the widgets when it gets new information. However, according to the problem you describe, I believe you just need to monitor a file for changes with gio as follows:
monitor = gio.File(filename).monitor()
monitor.connect('changed', file_changed_cb)
Whenever the file changes a signal will be emitted and the file_changed_cb callback method will be executed. There you can update the widgets to match the new information from the file that is being monitored. However, note that if your callback method takes too long to complete, your application might look unresponive, then you have to go for the thread strategy.

Categories