Function that is connected to button opens and regexp replace some text:
def process(self):
# first do this!!
self.label.setText('Processing for 5-10 sec.....')
self.button.setEnabled(0)
# and only then heavy files operations
file = open(self.filename, mode='r', encoding='utf-8')
text = re.sub(r'^ (.+)\n ', r' \[\1\]\n ', file.read(), flags=re.MULTILINE)
newfile = open(self.filename+'temp', mode='w', encoding='utf-8')
file_new.write(text)
self.label.setText('Ready')
Python 3.1, PyQt 4.8.2
File is big enough, operation takes ~10sec.
I want, when button is pressed, fist replace some text in label and disable button. So user can see, that he should wait some time.
But nothing happens. System just hangs for 10 seconds, and then "ready" label appeared and button disabled.
How can I make Qt first do label change and button disabled, and only after this do file operation?
I can imagine two solutions.
First, try calling QCoreApplication.processEvents() after you set the label's text and disable the button.
Second, if the first solution doesn't work, split your method in two. Then, in the first method, set the label's text, disable the button, and use QTimer.singleShot() to call your second method. It will be called asynchronously, so Qt event processing loop will have a chance to update the GUI.
I think using a thread is the better way to do it.
However, you may try put
while self.button.isEnabled():
pass
after where you set it.
Related
first of all let me start off by saying I started coding around 6 weeks ago in self-study, so it is very likely, that the approach may still be a total mess.
I have a program running (fine for me) in cli, but I want to make it usable for people that faint, when they see plain white text on a black background which they have to operate without a mouse.
Besides my main window, which is running in the background I wanted to have a message window, which displays the information if all necessary files where selected. which is shown below.
files_to_open = {'File_1':'', 'File_2':'', 'File_3':''}
def selectfiles_window():
global message_window
message_window = Tk()
...
content = Label(message_window, text=get_open_file_status_txt(), **txt_status_general)
content.pack(side='top')
button_select_file1 = Button(message_window,text = 'File 1',font = txt_general['font'],command = lambda:(
select_file_action(name='File 1', filetypename='Excel workbook', extension='*.xlsx'),
content.configure(text=get_open_file_status_txt())))
button_select_file1(side='bottom')
message_window.mainloop()
def select_file_action(name, filetypename, extension):
global files_to_open
files_to_open[name] = filedialog.askopenfilename(title = f'Select {name}', filetypes=[(filetypename, extension)])
def get_open_file_status_txt():
global files_to_open
message =f'''
[File_1] is {"NOT SET" if (files_to_open["File_1"] == "") else "SET"}'''
return message
I expected, that the text is updated after the filedialog was closed (which is partly working as expected).
Now what I don't understand: If I click the button to select File_1 and cancel it for the first time, the value for key File_1 is set to (). Any time after that, if I click the button to select File_1 and cancel it, the value for key File_1 is set to ''. If I select a file the path is saved correctly as value (also on the first attempt). If I cancel it is set again to ''.
Anybody an idea about why the value is set to () on the first cancel but afterwards runs as expected?
I would also be grateful for a different solution to update the text, if my approach is totally off.
Thank you and best regards,
Thomas
Turns out, that the function call was not the issue, rather that it is a (strange to me but maybe intended) behavior of filedialog.askopenfilename, which returns an empty tuple if cancel is selected on first call but an empty string on every further canceled calls.
I borrowed a design that I found on stackoverflow to redirect console output to a PyQt5 GUI textEdit widget. This works fine, but the text is not displayed in "real-time". It seems to output the text to the GUI once a process has completed. This has not been a problem until I tried to use time.sleep(secs) to print something, pause, then print something else. What ends up happening is that the program pauses for secs, then it prints all of the statements at once.
This class is in the mainWindow file for the GUI:
class EmittingStream(QtCore.QObject):
textWritten = QtCore.pyqtSignal(str)
def write(self, text):
self.textWritten.emit(str(text))
This is in the __init__ method of my event handling file:
sys.stdout = EmittingStream(textWritten=self.normalOutputWritten)
self.case_setup_console.setReadOnly(True)
self.main_console.setReadOnly(True)
This function is in the main class of event handling file (outside __init__):
def normalOutputWritten(self, text):
"""Append text to the QTextEdit."""
# Maybe QTextEdit.append() works as well, but this is how I do it:
cursor = self.case_setup_console.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(text)
self.case_setup_console.setTextCursor(cursor)
self.case_setup_console.ensureCursorVisible()
This works as intended to re-route the output to the text edit widget self.case_setup_console. But, when I try to run a code such as:
print('This is the first message')
time.sleep(5)
print('This should print 5 seconds later')
What happens is that the program waits 5 seconds, then it prints both statements together.
When programing for GUI code, there is a fundamental shift in how the program is designed. To make it short: after building and initialisation, the program is all the time running in an "event loop" provided by the GUI framework, and your code is only called when specific events take place.
That is in contrast with a terminal application where your code is running all the time, and you tell when to do "print"s, "input"s and pauses with "time.sleep".
The GUI code is responsible for taking notes of events (keyboard, UI, network, etc...), redrawing window contents and calling your code in response to events, or just when it is time to redraw a content that is defined in your code (like updating a background image).
So, it can only render the text that is supposed to show up in a window, with your redirected "print", when the control is passed back to its event loop. When you do time.sleep you pause the return - no code in the event loop is run, and it can't, of course, do any screen drawing.
What is needed is that you write your pauses in the program in a way that during the pause, the GUI event loop is running - not "time.sleep", that just suspends your whole thread.
In Qt the way to do that is create a QTimer object to call the code you want to use to print text at a particular moment, and then just surrender the execution to the the QtMainloop by returning from your function.
Thanks to Python's support for nested functions, that can be done in painless ways, even using lambda functions when setting the timer itself.
...
print('This is the first message')
timer = QtCore.QTimer
timer.singleShot(5000, lambda *_: print('This should print 5 seconds later'))
Should work for the given example. (The call, as usual for UIs, takes the pause time in miliseconds rather than in seconds).
If you will need to schedule more text to be printed after each phrase is output, you will need to call the scheduling inside the callback itself, and will need a little more sophistication, but it still could be as simple as:
phrases = iter(("hello", "world", "I", "am", "talking", "slowly!"))
timer = QtCore.QTimer()
def talker(*_):
phrase = next(phrases, None)
if not phrase:
return
print(phrase)
timer.singleShot(1000, talker)
timer.singleShot(1000, talker)
(Note that there is nothing special about the *_ parameter name either: I am just indicating that there might be any number of positional arguments for the callback - (The "*" part, that is Python syntax) - and that I won't care about then (I call the argument sequence as "_" to indicate I don't care how this is called, as it won't be used anyway - that is a coding convention) )
The iter and next calls are more "Python dialect" than one might be used, but one could just use a list and a counter up to the list length all the same.
Using Python 3.5. I have a tkinter form. The user clicks a button to import many files into a Listbox. Another button loops thru the files and reads and extracts data from them.
I have a Label on the form that indicates the status of the loop. The status, for the most part, works as expected except that extra characters are added on the end. I'm not sure where the characters come from. I also print() the same content as the Label back to the screen and the print() displays exactly what it should.
My question is why is my Label not displaying the correct string?
My code, greatly shortened:
class tk_new_db:
def __init__(self, master):
self.master = master # sets 'root' to the instance variable 'master'
self.var_text_2 = StringVar()
self.var_text_2.set('STATUS: Active')
self.label_6 = Label(master, textvariable=self.var_text_2,
font=self.font_10)
self.label_6.grid(row=15, sticky=W, padx=15)
def execute_main(self): # extract data from files
file_num = 0
nf = len(self.listbox_1.get(0, END))
for fr in li:
file_num += 1
print('STATUS: Extracting Loads from File '
'{} in {}'.format(file_num, nf))
self.var_text_2.set('STATUS: Extracting Loads from File '
'{} in {}'.format(file_num, nf))
self.master.update_idletasks()
The print() is writing the following:
STATUS: Extracting Loads from File 1 in 5
The Label is writing the following:
STATUS: Extracting Loads from File 1 in 5 nce...
It always adds ' nce...' on the Form.
EDIT:
I do use self.var_text_2 earlier in the program. The ' nce...' looks like it is a fragment of the previous string. I've since tried resetting the variable in two different ways, but I'm still getting the same result.
self.var_text_2.set('STATUS: Checking .F06 Files for Convergence...')
self.master.update_idletasks()
self.var_text_2.__del__()
self.var_text_2.set('STATUS: Checking .F06 Files for Convergence...')
self.master.update_idletasks()
self.var_text_2.set('')
How do you properly delete the StringVar() for reuse?
The only explanation I can think of is that you're stacking labels on top of each other in the same row and column, so when you add a long string that ends in "nce..." and later update the screen by adding a shorter label in the same grid cell, the trailing text of the longer label underneath is showing through.
The reasons I draw this conclusion are:
this is a common mistake I've seen several times
there are no bugs in tkinter that would cause this
there is nothing in the code that you've shown that could possibly add "nce..." to the end of the string
the fact that you use sticky="w" rather than sticky="ew", which will cause a shorter label to not completely overlay a longer label if placed in the same row and column
I think I can explain. Root.mainloop repeatedly calls root.update. Root.update executes tasks on the 'main' event queue. In brief, it only executes idletasks when the event queue is empty after checking for file events (on non-Windows), window-gui events, and timer events. Idletasks, which are not well documented but seem to primarily be screen update tasks, are never added to the main event queue. It is therefore possible that idletasks will remain undone indefinitely. Root.update_idletasks exists to force execution of idletasks anyway.
When mainloop is running, calling update within a task run by update is usually unnecessary and possibly worse. It is something only for adventurous experts. Hence the warnings you have read (which assume that you are running mainloop).
When mainloop is not running and update is not being called repeatedly, either because you never called mainloop or because you block it with a long running loop, then you likely must call update yourself. What you seem to have discovered is that part of completely handling a StringVar update is a main event and not just an idletask.
I sympathize with your desire to do repeated tasks with a for loop, but doing so currently means taking responsibility for event handling yourself. I hope in the future that it will be possible to have 'async for' (new in 3.5) interoperate with Tk.mainloop, but it is not as easy as I hoped.
In the meanwhile, you could put the body of the for loop into a callback and loop with root.after.
I went here: http://effbot.org/zone/vroom.htm
And tried this out:
filename = raw_input("Filename?")
editor = Text()
editor.pack(fill=Y, expand=1)
editor.config(font="Courier 12")
editor.focus_set()
mainloop()
#save
f = open(filename, "w")
text = str(editor.get(0.0,END))
try:
f.write(text.rstrip())
f.write("\n")
However, I was given an error:
TclError: invalid command name ".40632072L"
How can i fix this problem?
I'm not comfortable with object-oriented programming, so I would prefer an imperative solution (without any class keywords).
The problem is that, after the mainloop finishes, all of your widgets, including editor, get destroyed, so you can't call editor.get.
What you want to do is add some code that stashes the value of editor in a plain old string while the main loop is running, and then use that variable. For example:
text=''
def stash(*args):
global text
text = str(editor.get(0.0,END))
editor.bind_all('<<Modified>>', stash)
Or, of course, do the simpler thing: write the file from within the GUI instead of after the GUI has exited. If you go look farther down the same page, you'll see how they do that.
This is related to another question I found here that seems to be inactive for a few months, so I think it's worth asking again.
I have created a simple QDialog that has a QTextEdit and a QPushButton. This pops up in my application when a user right-clicks and selects the option to "add comments". I want them to be able to write free-form text and I'll just save whatever they write as a long string with no concern for new lines, etc.
When the user clicks the button, it executes code like this:
self.connect(accept_button,QtCore.SIGNAL('clicked()'),lambda arg=str(view_textedit.toPlainText()): self.updateGroupComments(arg))
def updateGroupComments(self,new_comment_str):
print "Updating user comment to have new string: " + new_comment_str
self.group_entry.list_of_user_comments[self.currentFrameCounter] = new_comment_str
This is not detecting the TextEdit text that is visible (it only detects whatever the text edit text is set to when it is created). How do I make a simple command that returns the currently visible text from a QTextEdit. Again, the function
toPlainText()
is not working correctly... it doesn't find the currently visible text, only whatever text was on screen before changes or additions started being made by the user.
If this can't be done without subclassing and appealing to cursor positions, it makes the whole thing seem worthless... so please keep suggestions only to those implemented without subclassing or manipulating cursors. It should be really simple and straightforward to just return all currently visible text... what am I missing?
Objects that are being bound to default arguments are evaluated at the definition time. The function is working correctly, it returns whatever was in the text field when it was executed. Your code simply calls it at the wrong moment. If you want to use lambda, then do:
self.connect(
accept_button, QtCore.SIGNAL('clicked()'),
lambda: self.updateGroupComments(str(view_textedit.toPlainText()))
)
Or make view_textedit an instance attribute instead, and do simply
self.connect(
accept_button, QtCore.SIGNAL('clicked()'), self.updateGroupComments
)
And change updateGroupComments to call self.view_textedit.toPlainText instead of taking an argument.
BTW, this is not PyQt specific, this is how Python works in general.
To illustrate my last comment, that lambda can very well be replaced with:
def slot():
self.updateGroupComments(str(view_textedit.toPlainText()))
self.connect(accept_button, QtCore.SIGNAL('clicked()'), slot)