python launch editor externally and get texts - python

I'm writing a PyQt programe where I'd like to allow the user to launch their preferred editor to fill in a TextEdit field.
So the goal is to launch an editor (say vim) externally on a tmp file, and upon editor closing, get its contexts into a python variable.
I've found a few similar questions like Opening vi from Python, call up an EDITOR (vim) from a python script, invoke an editor ( vim ) in python. But they are all in a "blocking" manner that works like the git commit command. What I am after is a "non-blocking" manner (because it is a GUI), something like the "Edit Source" function in zimwiki.
My current attempt:
import os
import tempfile
import threading
import subprocess
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs)
# this immediately finishes OPENING vim.
rec=proc.wait()
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
def openEditor():
fd, filepath=tempfile.mkstemp()
print('filepath=',filepath)
def cb(tmppath):
print('# <cb>: cb tmppath=',tmppath)
with open(tmppath, 'r') as tmp:
lines=tmp.readlines()
for ii in lines:
print('# <cb>: ii',ii)
return
with os.fdopen(fd, 'w') as tmp:
cmdflag='--'
editor_cmd='vim'
cmd=[os.environ['TERMCMD'], cmdflag, editor_cmd, filepath]
print('#cmd = ',cmd)
popenAndCall(cb, cmd)
print('done')
return
if __name__=='__main__':
openEditor()
I think it failed because the Popen.wait() only waits until the editor is opened, not until its closing. So it captures nothing from the editor.
Any idea how to solve this? Thanks!
EDIT:
I found this answer which I guess is related. I'm messing around trying to let os wait for the process group, but it's still not working. Code below:
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs, preexec_fn=os.setsid)
pid=proc.pid
gid=os.getpgid(pid)
#rec=proc.wait()
rec=os.waitid(os.P_PGID, gid, os.WEXITED | os.WSTOPPED)
print('# <runInThread>: rec=', rec, 'pid=',pid, 'gid=',gid)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
I assume this gid=os.getpgid(pid) gives me the id of the group, and os.waitid() wait for the group. I also tried os.waitpid(gid, 0), didn't work either.
I'm on the right track?
UPDATE:
It seems that for some editors that works, like xed. vim and gvim both fails.

With QProcess you can launch a process without blocking the Qt event loop.
In this case I use xterm since I do not know which terminal is established in TERMCMD.
from PyQt5 import QtCore, QtGui, QtWidgets
class EditorWorker(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
self._process.finished.connect(self.on_finished)
self._text = ""
if self._temp_file.open():
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
#QtCore.pyqtSlot()
def on_finished(self):
if self._temp_file.isOpen():
self._text = self._temp_file.readAll().data().decode()
self.finished.emit()
#property
def text(self):
return self._text
def __del__(self):
self._process.kill()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker("xterm -e vim".split(), self)
worker.finished.connect(self.on_finished)
#QtCore.pyqtSlot()
def on_finished(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I guess in your case you should change
"xterm -e vim".split()
to
[os.environ['TERMCMD'], "--", "vim"]
Possible commands:
- xterm -e vim
- xfce4-terminal --disable-server -x vim
Update:
Implementing the same logic that you use with pyinotify that is to monitor the file, but in this case using QFileSystemWatcher which is a multiplatform solution:
from PyQt5 import QtCore, QtGui, QtWidgets
class EditorWorker(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
self._text = ""
self._watcher = QtCore.QFileSystemWatcher(self)
self._watcher.fileChanged.connect(self.on_fileChanged)
if self._temp_file.open():
self._watcher.addPath(self._temp_file.fileName())
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
#QtCore.pyqtSlot()
def on_fileChanged(self):
if self._temp_file.isOpen():
self._text = self._temp_file.readAll().data().decode()
self.finished.emit()
#property
def text(self):
return self._text
def __del__(self):
self._process.kill()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker("gnome-terminal -- vim".split(), self)
worker.finished.connect(self.on_finished)
#QtCore.pyqtSlot()
def on_finished(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

The issue I reproduced is that proc is the gnome-terminal process and not the vim process.
Here are the two options that work for me.
1) Find the process of your text editor and not that of your terminal. With the right process ID, the code can wait for the process of your text editor to finish.
With psutil (portable)
Finds the latest editor process in the list of all running processes.
import psutil
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
editor_cmd=popenArgs[-2] # vim
proc = subprocess.Popen(popenArgs)
proc.wait()
# Find the latest editor process in the list of all running processes
editor_processes = []
for p in psutil.process_iter():
try:
process_name = p.name()
if editor_cmd in process_name:
editor_processes.append((process_name, p.pid))
except:
pass
editor_proc = psutil.Process(editor_processes[-1][1])
rec=editor_proc.wait()
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
Without psutil (works on Linux, but not portable to Mac OS or Windows)
Draws from https://stackoverflow.com/a/2704947/241866 and the source code of psutil.
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
editor_cmd=popenArgs[-2] # vim
proc = subprocess.Popen(popenArgs)
proc.wait()
# Find the latest editor process in the list of all running processes
pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
editor_processes = []
for pid in pids:
try:
process_name = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read().split('\0')[0]
if editor_cmd in process_name:
editor_processes.append((process_name, int(pid)))
except IOError:
continue
editor_proc_pid = editor_processes[-1][1]
def pid_exists(pid):
try:
os.kill(pid, 0)
return True
except:
return
while True:
if pid_exists(editor_proc_pid):
import time
time.sleep(1)
else:
break
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread
2) As a last resort, you can catch a UI event before updating the text:
def popenAndCall(onExit, popenArgs):
def runInThread(onExit, popenArgs):
tmppath=popenArgs[-1]
proc = subprocess.Popen(popenArgs)
# this immediately finishes OPENING vim.
rec=proc.wait()
raw_input("Press Enter") # replace this with UI event
print('# <runInThread>: rec=', rec)
onExit(tmppath)
os.remove(tmppath)
return
thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
thread.start()
return thread

I think #eyllanesc's solution is very close to what zim is doing (zim is using GObject.spawn_async() and GObject.child_watch_add(), I've no experiences with GObject, I guess that's the equivalent to QProcess.start()). But we run into some problems regarding how some terminals (like gnome-terminal) handles new terminal session launching.
I tried to monitor the temporary file opened by the editor, and on writing/saving the temp file I could call my callback. The monitoring is done using pyinotify. I've tried gnome-terminal, xterm, urxvt and plain gvim, all seem to work.
Code below:
import threading
from PyQt5 import QtCore, QtGui, QtWidgets
import pyinotify
class EditorWorker(QtCore.QObject):
file_close_sig = QtCore.pyqtSignal()
edit_done_sig = QtCore.pyqtSignal()
def __init__(self, command, parent=None):
super(EditorWorker, self).__init__(parent)
self._temp_file = QtCore.QTemporaryFile(self)
self._process = QtCore.QProcess(self)
#self._process.finished.connect(self.on_file_close)
self.file_close_sig.connect(self.on_file_close)
self._text = ""
if self._temp_file.open():
program, *arguments = command
self._process.start(
program, arguments + [self._temp_file.fileName()]
)
tmpfile=self._temp_file.fileName()
# start a thread to monitor file saving/closing
self.monitor_thread = threading.Thread(target=self.monitorFile,
args=(tmpfile, self.file_close_sig))
self.monitor_thread.start()
#QtCore.pyqtSlot()
def on_file_close(self):
if self._temp_file.isOpen():
print('open')
self._text = self._temp_file.readAll().data().decode()
self.edit_done_sig.emit()
else:
print('not open')
#property
def text(self):
return self._text
def __del__(self):
try:
self._process.kill()
except:
pass
def monitorFile(self, path, sig):
class PClose(pyinotify.ProcessEvent):
def my_init(self):
self.sig=sig
self.done=False
def process_IN_CLOSE(self, event):
f = event.name and os.path.join(event.path, event.name) or event.path
self.sig.emit()
self.done=True
wm = pyinotify.WatchManager()
eventHandler=PClose()
notifier = pyinotify.Notifier(wm, eventHandler)
wm.add_watch(path, pyinotify.IN_CLOSE_WRITE)
try:
while not eventHandler.done:
notifier.process_events()
if notifier.check_events():
notifier.read_events()
except KeyboardInterrupt:
notifier.stop()
return
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._button = QtWidgets.QPushButton(
"Launch VIM", clicked=self.on_clicked
)
self._text_edit = QtWidgets.QTextEdit(readOnly=True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self._button)
lay.addWidget(self._text_edit)
#QtCore.pyqtSlot()
def on_clicked(self):
worker = EditorWorker(["gnome-terminal", '--', "vim"], self)
worker.edit_done_sig.connect(self.on_edit_done)
#QtCore.pyqtSlot()
def on_edit_done(self):
worker = self.sender()
prev_cursor = self._text_edit.textCursor()
self._text_edit.moveCursor(QtGui.QTextCursor.End)
self._text_edit.insertPlainText(worker.text)
self._text_edit.setTextCursor(prev_cursor)
worker.deleteLater()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
BUT pyinotify only works in Linux. If you could find a cross-platform solution (at least on Mac) please let me know.
UPDATE: this doesn't seem to be robust. pyinotify reports file writing instead of just file closing. I'm depressed.

Related

QThread is not emitting signal even after run() finished

When User clicks on "Update" Push Button, box_updatetool_fcn will be executed. MainThread will update the Progressbar in the QMainWindow while the other QThread will download the file. But it executes perfectly, but the problem is signal emited by the WorkerThread is not updating immediately.
Even I have gone through many questions, none solved my problem. I don't know why my fcn_qthread_output is not executing immediately after Qthread finished.
Even the QThread finished() function also executing after the Main Function finished, which I am not using in my current program. I don't know what's wrong in the program, is something missing?
Here is the following console output -
Run Function Closed
Out1 : False, FileName
Main Function Ended
Out2 : True, FileName
What I am expecting is -
Run Function Closed
Out2 : True, FileName
Out1 : True, FileName
Main Function Ended
Below is the program execution flow -
class WorkerThread(QtCore.QThread):
outResult = QtCore.pyqtSignal(tuple)
def __init__(self,target,args,parent=None):
super(WorkerThread, self).__init__(parent)
self.fcn = target
self.args = args
def run(self):
outResult = self.fcn(*self.args)
self.outResult.emit(outResult)
print('Run Function Closed')
class ApplicationWindow(QtWidgets.QMainWindow):
def box_updatetool_fcn(self):
t1 = WorkerThread(target=self.boxapi.fcn_downloadfile, args=(fileID,)) #ThreadWithResult
t1.outResult.connect(self.fcn_qthread_output)
t1.start()
self.box_pbar_fcn(tempfilename,fsize)
print(f'Out1 : {self.var_box_output}')
self.boxapi.fcn_updatetool(file_fullpath,self.progpath)
print('Main Function Ended')
def fcn_qthread_output(self,result):
self.var_box_output = result
print(f'Out2 : {self.var_box_output}')
def box_pbar_fcn(self,filename,fullsize):
pobj = self.ui.progressbar
while(barprogress < 100):
if os.path.exists(filename):
currfilesize = os.path.getsize(filename)
barprogress = int(currfilesize/fullsize*100)
pobj.setValue(barprogress)
Reproducible Code -
I have used box_pbar_fcn (progress bar which shows the downloading progress) and t1 Thread (downloading the file) will Run in simultaneously.
import os, sys
from PyQt5 import QtWidgets, QtGui, QtCore
class WorkerThread(QtCore.QThread):
outResult = QtCore.pyqtSignal(tuple)
def __init__(self,parent=None):
super(WorkerThread, self).__init__(parent)
def run(self):
outResult = (True,'Out1')
self.outResult.emit(outResult)
print('Run Function Closed')
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.var_box_output = (False,'Inp1')
self.box_updatetool_fcn()
def box_updatetool_fcn(self):
tempfilename,fsize = 'Test',300
t1 = WorkerThread() #ThreadWithResult
t1.outResult.connect(self.fcn_qthread_output)
t1.start()
self.box_pbar_fcn(tempfilename,fsize)
print(f'Out1 : {self.var_box_output}')
print('Main Function Ended')
def fcn_qthread_output(self,result):
self.var_box_output = result
print(f'Out2 : {self.var_box_output}')
def box_pbar_fcn(self,filename,fullsize):
pass
#pobj = self.ui.progressbar
#while(barprogress < 100):
#if os.path.exists(filename):
# currfilesize = os.path.getsize(filename)
# barprogress = int(currfilesize/fullsize*100)
# pobj.setValue(barprogress)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mw = ApplicationWindow()
mw.show()
sys.exit(app.exec_())
The problem is that you are blocking the main thread with your while True (There should never be an instruction that blocks the eventloop for long). In Qt the logic is to do the job asynchronously. In this case I suspect that in a thread you are downloading a file and you want to show the download progress in the QProgressBar and for this you are continuously measuring the size of the file. If so, a possible solution is to use a QTimer to run the periodic task of measuring the file size.
Disclaimer: The following code that has not been tested should work. As I do not know how is the way you save the file, it could be that it does not work since many functions only write the file at the end and not in parts for efficiency reasons.
import os
import sys
from functools import cached_property
from PyQt5 import QtCore, QtGui, QtWidgets
class Downloader(QtCore.QThread):
outResult = QtCore.pyqtSignal(tuple)
def run(self):
import time
time.sleep(10)
outResult = (True, "Out1")
self.outResult.emit(outResult)
print("Run Function Closed")
class FileObserver(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._filename = ""
self._current_size = 0
#property
def filename(self):
return self._filename
def start(self, filename, interval=100):
self._current_size = 0
self._filename = filename
self.timer.setInterval(interval)
self.timer.start()
def stop(self):
self.timer.stop()
#cached_property
def timer(self):
timer = QtCore.QTimer()
timer.timeout.connect(self._measure)
return timer
def _measure(self):
if os.path.exists(self.filename):
file_size = os.path.getsize(filename)
if self._current_size != file_size:
self._current_size = file_size
self.file_size_changed.emit(file_size)
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.var_box_output = (False, "Inp1")
self.box_updatetool_fcn()
#cached_property
def file_observer(self):
return FileObserver()
def box_updatetool_fcn(self):
tempfilename, fullsize = "Test", 300
self.ui.progressbar.setMaximum(fullsize)
t1 = WorkerThread(self)
t1.outResult.connect(self.fcn_qthread_output)
t1.finished.connect(self.file_observer.stop)
t1.start()
self.file_observer.start(tempfilename, interval=10)
def fcn_qthread_output(self, result):
self.var_box_output = result
print(f"Out2 : {self.var_box_output}")
def box_pbar_fcn(self, file_size):
self.ui.progressbar.setValue(file_size)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mw = ApplicationWindow()
mw.show()
sys.exit(app.exec_())

How to update UI with output from QProcess loop without the UI freezing?

I am wanting to have a list of commands being processed through a QProcess and have its output be appended to a textfield I have. I've found a these two pages that seems to do each of the things i need (updating the UI, and not freezing the UI via QThread):
Printing QProcess Stdout only if it contains a Substring
https://nikolak.com/pyqt-threading-tutorial/
So i tried to combine these two....
import sys
from PySide import QtGui, QtCore
class commandThread(QtCore.QThread):
def __init__(self):
QtCore.QThread.__init__(self)
self.cmdList = None
self.process = QtCore.QProcess()
def __del__(self):
self.wait()
def command(self):
# print 'something'
self.process.start('ping', ['127.0.0.1'])
processStdout = str(self.process.readAll())
return processStdout
def run(self):
for i in range(3):
messages = self.command()
self.emit(QtCore.SIGNAL('dataReady(QString)'), messages)
# self.sleep(1)
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def dataReady(self,outputMessage):
cursorOutput = self.output.textCursor()
cursorSummary = self.summary.textCursor()
cursorOutput.movePosition(cursorOutput.End)
cursorSummary.movePosition(cursorSummary.End)
# Update self.output
cursorOutput.insertText(outputMessage)
# Update self.summary
for line in outputMessage.split("\n"):
if 'TTL' in line:
cursorSummary.insertText(line)
self.output.ensureCursorVisible()
self.summary.ensureCursorVisible()
def initUI(self):
layout = QtGui.QHBoxLayout()
self.runBtn = QtGui.QPushButton('Run')
self.runBtn.clicked.connect(self.callThread)
self.output = QtGui.QTextEdit()
self.summary = QtGui.QTextEdit()
layout.addWidget(self.runBtn)
layout.addWidget(self.output)
layout.addWidget(self.summary)
centralWidget = QtGui.QWidget()
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
# self.process.started.connect(lambda: self.runBtn.setEnabled(False))
# self.process.finished.connect(lambda: self.runBtn.setEnabled(True))
def callThread(self):
self.runBtn.setEnabled(False)
self.get_thread = commandThread()
# print 'this this running?'
self.connect(self.get_thread, QtCore.SIGNAL("dataReady(QString)"), self.dataReady)
self.connect(self.get_thread, QtCore.SIGNAL("finished()"), self.done)
def done(self):
self.runBtn.setEnabled(True)
def main():
app = QtGui.QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The problem is that once I click the "Run" button the textfield on the right doesn't seem to populate, and i am no longer getting any errors so I am not sure what is happening.
I tried referring to this page as well but I think i am already emulating what it is describing...?
https://www.qtcentre.org/threads/46056-QProcess-in-a-loop-works-but
Ultimately what I want to build is for a main window to submit a series of commands via subprocess/QProcess, and open up a little log window that constantly updates it on the progress via displaying the console output. Similar to what you kind of see in like Installer packages...
I feel like i am so close to an answer, yet so far away. Is anyone able to chime in on this?
EDIT: so to answer eyllanesc's question, the list of commands has to be run one after the previous one has completed, as the command i plan to use will be very CPU intensive, and i cannot have more than one process of it running. also the time of each command completing will completely vary so I can't just have a arbitrary hold like with time.sleep() as some may complete quicker/slower than others. so ideally figuring out when the process has finished should kickstart another command (which is why i have a for loop in this example to represent that).
i also decided to use threads because apparently that was one way of preventing the UI to freeze while the process was running,so i assumed i needed to utilize this to have a sort of live feed/update in the text field.
the other thing is in the UI i would ideally in addition to updating a text field with console logs, i would want it to have some sort of label that gets updated that says something like "2 of 10 jobs completed". so something like this:
It would be nice too when before a new command is being processed a custom message can be appended to the text field indicating what command is being run...
UPDATE: apologies for taking so long to post an update on this, but based on eyllanesc's answer, I was able to figure out how to make this open a separate window and run the "ping" commands. here is the example code I have made to achieve my results in my main application:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
#^ this is how we can send a signal and can declare what type
# of information we want to pass with this signal
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
#this 'iter()' method creates an iterator object
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
# QtCore.Slot()
#^ we don't need this line here
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
# #QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.outputWindow = outputLog(parentWindow=self)
self._button = QtGui.QPushButton("Start")
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
self.setCentralWidget(central_widget)
self._button.clicked.connect(self.showOutput)
def showOutput(self):
self.outputWindow.show()
self.outputWindow.startProcess()
#property
def startButton(self):
return self._button
class outputLog(QtGui.QWidget):
def __init__(self, parent=None, parentWindow=None):
QtGui.QWidget.__init__(self,parent)
self.parentWindow = parentWindow
self.setWindowTitle('Render Log')
self.setMinimumSize(225, 150)
self.renderLogWidget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(self.renderLogWidget)
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
self._button = QtGui.QPushButton("Close")
self._button.clicked.connect(self.windowClose)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
lay.addWidget(self._button)
self._manager = SequentialManager(self)
self.setLayout(lay)
def startProcess(self):
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
self.parentWindow.startButton.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
def windowClose(self):
self.parentWindow.startButton.setEnabled(True)
self.close()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
i still don't really understand the use of the QtCore.Slot() decorators as when I commented them out it didn't really seem to change the result. But i kept them in just to be safe.
It is not necessary to use threads in this case since QProcess is executed using the event loop. The procedure is to launch a task, wait for the finishes signal, get the result, send the result, and execute the next task until all the tasks are finished. The key to the solution is to use the signals and distribute the tasks with an iterator.
Considering the above, the solution is:
from PySide import QtCore, QtGui
class Task:
def __init__(self, program, args=None):
self._program = program
self._args = args or []
#property
def program(self):
return self._program
#property
def args(self):
return self._args
class SequentialManager(QtCore.QObject):
started = QtCore.Signal()
finished = QtCore.Signal()
progressChanged = QtCore.Signal(int)
dataChanged = QtCore.Signal(str)
def __init__(self, parent=None):
super(SequentialManager, self).__init__(parent)
self._progress = 0
self._tasks = []
self._process = QtCore.QProcess(self)
self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
self._process.finished.connect(self._on_finished)
self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
def execute(self, tasks):
self._tasks = iter(tasks)
self.started.emit()
self._progress = 0
self.progressChanged.emit(self._progress)
self._execute_next()
def _execute_next(self):
try:
task = next(self._tasks)
except StopIteration:
return False
else:
self._process.start(task.program, task.args)
return True
QtCore.Slot()
def _on_finished(self):
self._process_task()
if not self._execute_next():
self.finished.emit()
#QtCore.Slot()
def _on_readyReadStandardOutput(self):
output = self._process.readAllStandardOutput()
result = output.data().decode()
self.dataChanged.emit(result)
def _process_task(self):
self._progress += 1
self.progressChanged.emit(self._progress)
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self._button = QtGui.QPushButton("Start")
self._textedit = QtGui.QTextEdit(readOnly=True)
self._progressbar = QtGui.QProgressBar()
central_widget = QtGui.QWidget()
lay = QtGui.QVBoxLayout(central_widget)
lay.addWidget(self._button)
lay.addWidget(self._textedit)
lay.addWidget(self._progressbar)
self.setCentralWidget(central_widget)
self._manager = SequentialManager(self)
self._manager.progressChanged.connect(self._progressbar.setValue)
self._manager.dataChanged.connect(self.on_dataChanged)
self._manager.started.connect(self.on_started)
self._manager.finished.connect(self.on_finished)
self._button.clicked.connect(self.on_clicked)
#QtCore.Slot()
def on_clicked(self):
self._progressbar.setFormat("%v/%m")
self._progressbar.setValue(0)
tasks = [
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
Task("ping", ["8.8.8.8"]),
]
self._progressbar.setMaximum(len(tasks))
self._manager.execute(tasks)
#QtCore.Slot()
def on_started(self):
self._button.setEnabled(False)
#QtCore.Slot()
def on_finished(self):
self._button.setEnabled(True)
#QtCore.Slot(str)
def on_dataChanged(self, message):
if message:
cursor = self._textedit.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertText(message)
self._textedit.ensureCursorVisible()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

How can I keep my gui responsive while making a Popen call?

I am trying to have a throbber (in the form of an animated chasing arrows gif) playing while I call a Popen command but it doesn't work because I think the gui is completely unresponsive while the Popen command is running. How can I fix this?
Please check my code below.
import subprocess
import os
import sys
from PyQt4 import QtCore, QtGui
class Test(QtGui.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.setMinimumSize(200, 200)
self.buttonUpdate = QtGui.QPushButton()
self.buttonUpdate.setText("Get updates")
self.lbl1 = QtGui.QLabel()
self.lbl2 = QtGui.QLabel()
self.lblm2 = QtGui.QLabel()
gif = os.path.abspath("chassingarrows.gif")#throbber
self.movie = QtGui.QMovie(gif)
self.movie.setScaledSize(QtCore.QSize(20, 20))
self.pixmap = QtGui.QPixmap("checkmark.png")#green checkmark
self.pixmap2 = self.pixmap.scaled(20, 20)
verticalLayout = QtGui.QVBoxLayout(self)
h2 = QtGui.QHBoxLayout()
h2.addWidget(self.lblm2)
h2.addWidget(self.lbl2)
h2.setAlignment(QtCore.Qt.AlignCenter)
verticalLayout.addWidget(self.lbl1)
verticalLayout.addLayout(h2)
verticalLayout.addWidget(self.buttonUpdate, 0, QtCore.Qt.AlignRight)
self.buttonUpdate.clicked.connect(self.get_updates)
def get_updates(self):
try:
self.lbl1.setText("Updating")
self.lblm2.setMovie(self.movie)
self.movie.start()
self.setCursor(QtCore.Qt.BusyCursor)
p1 = subprocess.Popen(['apt', 'update'], stdout=subprocess.PIPE, bufsize=1)
p1.wait()
self.movie.stop()
self.lblm2.setPixmap(self.pixmap2)
self.unsetCursor()
self.lbl1.setText("Done update")
except subprocess.CalledProcessError, e:
print e.output
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Instead of using subprocess.Popen, use QProcess which allow to callback when the process finished using finished signal:
def get_updates(self):
self.lbl1.setText("Updating")
self.lblm2.setMovie(self.movie)
self.movie.start()
self.setCursor(QtCore.Qt.BusyCursor)
self.p1 = QProcess()
self.p1.finished.connect(self.on_apt_update_finished)
self.p1.start('apt', ['update'])
def on_apt_update_finished(self, exit_code, exit_status):
self.movie.stop()
self.lblm2.setPixmap(self.pixmap2)
self.unsetCursor()
self.lbl1.setText("Done update")

Can't show window from thread

I have several threads that need to work with window. Here's thread definition:
class MyThread(QtCore.QThread):
def __init__(self, id, window, mutex):
super(MyThread, self).__init__()
self.id = id
self.window = window
self.mutex = mutex
self.connect(self, QtCore.SIGNAL("load_message_input()"), self.window, QtCore.SLOT("show_input()"))
def run(self):
self.mutex.lock()
self.emit(QtCore.SIGNAL("load_message_input()"))
self.connect(self.window, QtCore.SIGNAL("got_message(QString)"), self.print_message)
self.window.input_finished.wait(self.mutex)
self.mutex.unlock()
def print_message(self, str):
print "Thread %d: %s" % (self.id, str)
And here's window definition:
class MyDialog(QtGui.QDialog):
def __init__(self, *args, **kwargs):
super(MyDialog, self).__init__(*args, **kwargs)
self.last_message = None
self.setModal(True)
self.message_label = QtGui.QLabel(u"Message")
self.message_input = QtGui.QLineEdit()
self.dialog_buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.accepted.connect(self.accept)
self.dialog_buttons.rejected.connect(self.reject)
self.hbox = QtGui.QHBoxLayout()
self.hbox.addWidget(self.message_label)
self.hbox.addWidget(self.message_input)
self.vbox = QtGui.QVBoxLayout()
self.vbox.addLayout(self.hbox)
self.vbox.addWidget(self.dialog_buttons)
self.setLayout(self.vbox)
self.input_finished = QtCore.QWaitCondition()
#QtCore.pyqtSlot()
def show_input(self):
self.exec_()
def on_accepted(self):
self.emit(QtCore.SIGNAL("got_message(QString)"), self.message_input.text())
self.input_finished.wakeOne()
And here's main:
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
mutex = QtCore.QMutex()
threads = []
window = test_qdialog.MyDialog()
for i in range(5):
thread = MyThread(i, window, mutex)
thread.start()
threads.append(thread)
for t in threads:
t.wait()
sys.exit(app.exec_())
I can't figure out why window isn't shown when executing the script.
Update:
For some reason other threads don't stop on line with self.mutex.lock(). Can't figure out why.
You have several problems in your code:
If you want a QThread to use slots you need to create an event loop for it (which is easy, just call QThread.exec_), but QThreads with event loops needs to be coded differently (next I'll post you an example)
You need to connect on_accepted to accepted if you want to emit the messages, unless you use the auto-connect features of Qt.
If you want to use QThread first you need to start a QApplication so for t in threads: t.wait() can't be executed before the call to QApplication.exec_ (in my example just removed it).
The last but not less important issue: If you want your threads to consume resources exclusively you should think of a consumer-producer approach (the problem is that when you emit a signal every slot will get a copy of the data and if you try to block a thread with an event loop the application just freezes, to solve the problem of consumer-producer I pass an extra mutex to the signal of the message and try to lock it [never blocking!] to know if the thread con consume the event)
As promised there is an example of how to use event loops on QThreads:
from PyQt4 import QtCore, QtGui
class MyThread(QtCore.QThread):
load_message_input = QtCore.pyqtSignal()
def __init__(self, id, window):
super(MyThread, self).__init__()
self.id = id
self.window = window
self.load_message_input.connect(self.window.show_input)
self.window.got_message.connect(self.print_message)
self.started.connect(self.do_stuff)
def run(self):
print "Thread %d: %s" % (self.id,"running")
self.exec_()
#QtCore.pyqtSlot()
def do_stuff(self):
print "Thread %d: %s" % (self.id,"emit load_message_input")
self.load_message_input.emit()
#QtCore.pyqtSlot("QString","QMutex")
def print_message(self, msg, mutex):
if mutex.tryLock():
print "Thread %d: %s" % (self.id, msg)
self.do_stuff()
class MyDialog(QtGui.QDialog):
got_message = QtCore.pyqtSignal("QString","QMutex")
def __init__(self, *args, **kwargs):
super(MyDialog, self).__init__(*args, **kwargs)
self.last_message = None
self.setModal(True)
self.message_label = QtGui.QLabel(u"Message")
self.message_input = QtGui.QLineEdit()
self.dialog_buttons = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel)
self.dialog_buttons.accepted.connect(self.accept)
self.dialog_buttons.accepted.connect(self.on_accepted)
self.dialog_buttons.rejected.connect(self.reject)
self.hbox = QtGui.QHBoxLayout()
self.hbox.addWidget(self.message_label)
self.hbox.addWidget(self.message_input)
self.vbox = QtGui.QVBoxLayout()
self.vbox.addLayout(self.hbox)
self.vbox.addWidget(self.dialog_buttons)
self.setLayout(self.vbox)
self.input_finished = QtCore.QWaitCondition()
#QtCore.pyqtSlot()
def show_input(self):
print "showing input"
window.show()
window.setModal(True)
#QtCore.pyqtSlot()
def on_accepted(self):
print "emit: ", self.message_input.text()
self.got_message.emit(self.message_input.text(), QtCore.QMutex())
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
mutex = QtCore.QMutex()
threads = []
window = MyDialog()
for i in range(5):
thread = MyThread(i, window)
thread.start()
threads.append(thread)
print "start app"
sys.exit(app.exec_())
Note: almost always the thread who receives the signal first will be the one with id 1.
My recommendation, do not use slots in your threads (which will make safe the use of mutex and wait-conditions) and implement a consumer-producer approach for the messages.
You are waiting for the threads to exit, before calling app.exec_(). You should probably monitor the threads in a GUI idle loop or connect to the thread's finished() signal.

using threading for a qt application

I have the following two files:
import sys
import time
from PyQt4 import QtGui, QtCore
import btnModule
class WindowClass(QtGui.QWidget):
def __init__(self):
super(WindowClass, self).__init__()
self.dataLoaded = None
# Widgets
# Buttons
thread = WorkerForLoop(self.runLoop)
# thread.start()
self.playBtn = btnModule.playpauselBtnClass \
('Play', thread.start)
# Layout
layout = QtGui.QHBoxLayout()
layout.addWidget(self.playBtn)
self.setLayout(layout)
# Window Geometry
self.setGeometry(100, 100, 100, 100)
def waitToContinue(self):
print self.playBtn.text()
while (self.playBtn.text() != 'Pause'):
pass
def runLoop(self):
for ii in range(100):
self.waitToContinue()
print 'num so far: ', ii
time.sleep(0.5)
class WorkerForLoop(QtCore.QThread):
def __init__(self, function, *args, **kwargs):
super(WorkerForLoop, self).__init__()
self.function = function
self.args = args
self.kwargs = kwargs
def __del__(self):
self.wait()
def run(self):
print 'let"s run it'
self.function(*self.args, **self.kwargs)
return
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
wmain = WindowClass()
wmain.show()
sys.exit(app.exec_())
and the second file btnModule.py:
from PyQt4 import QtGui, QtCore
class playpauselBtnClass(QtGui.QPushButton):
btnSgn = QtCore.pyqtSignal()
def __init__(self, btnName, onClickFunction):
super(playpauselBtnClass, self).__init__(btnName)
self.clicked.connect(self.btnPressed)
self.btnSgn.connect(onClickFunction)
def btnPressed(self):
if self.text() == 'Play':
self.setText('Pause')
self.btnSgn.emit()
print 'Changed to pause and emited signal'
elif self.text() == 'Pause':
self.setText('Continue')
print 'Changed to Continue'
elif self.text() == 'Continue':
self.setText('Pause')
print 'Changed to Pause'
If in the first file I remove the comment at thread.start() it works as expected, it starts the thread and then hangs until I click Play on the UI. However, I thought it should work even if I didn't start it there as the signal is btnSgn is connected to onClickFunction which in this case takes the value thread.start.
Used this as a reference
http://joplaete.wordpress.com/2010/07/21/threading-with-pyqt4/
I think the reason it doesn't work when you try to call thread.start() in your second file (via self.btnSgn.emit()) is that the thread object goes out of scope outside of the init function where you created it. So you are calling start() on an already deleted thread.
Just changing thread -> self.thread (i.e. making the thread object a member of the WindowClass object) works fine when I tried it, since thread is then kept alive till the end of the program.

Categories