我正在写一个PyQt程序,我想让用户启动他们喜欢的编辑器来填写TextEdit字段。
因此,目标是在tmp文件外部启动编辑器(比如vim),并在编辑器关闭时将其上下文转换为python变量。
我发现了一些类似的问题,如Opening vi from Python,call up an EDITOR (vim) from a python script,invoke an editor ( vim ) in python。但它们都处于“阻塞”状态,就像git commit
命令一样。我所追求的是一种“非阻塞”方式(因为它是一个GUI),类似于zimwiki中的“编辑源”功能。
我目前的尝试:
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()
我认为它失败了因为Popen.wait()
只等到编辑器打开,直到它关闭。所以它没有从编辑器中捕获任何内容。
不知道怎么解决这个问题?谢谢!
编辑:
我发现这个answer我认为是相关的。我正在努力让os
等待process group
,但它仍然无法正常工作。代码如下:
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
我假设这个gid=os.getpgid(pid)
给了我这个组的id,而os.waitid()
等着这个组。我也试过os.waitpid(gid, 0)
,也没用。
我走在正确的轨道上?
更新:
对于一些有效的编辑来说,似乎就像xed
一样。 vim
和gvim
都失败了。
使用QProcess,您可以在不阻塞Qt事件循环的情况下启动进程。
在这种情况下,我使用xterm,因为我不知道在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_())
我想在你的情况下你应该改变
"xterm -e vim".split()
至
[os.environ['TERMCMD'], "--", "vim"]
可能的命令:
- xterm -e vim
- xfce4-terminal --disable-server -x vim
更新:
实现与pyinotify一样的逻辑,用于监视文件,但在这种情况下使用QFileSystemWatcher这是一个多平台解决方案:
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_())
我再现的问题是proc
是gnome-terminal进程而不是vim进程。
以下是适用于我的两个选项。
1)找到文本编辑器的过程而不是终端的过程。使用正确的进程ID,代码可以等待文本编辑器的完成过程。
用psutil(便携式)
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
没有psutil(适用于Linux,但不能移植到Mac OS或Windows)
从https://stackoverflow.com/a/2704947/241866和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)作为最后的手段,您可以在更新文本之前捕获UI事件:
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
我认为@ eyllanesc的解决方案非常接近于zim正在做的事情(zim正在使用GObject.spawn_async()
和GObject.child_watch_add()
,我没有使用GObject
,我猜这相当于QProcess.start()
)。但是我们遇到一些关于某些终端(如gnome-terminal
)处理新终端会话启动的问题。
我试图监视编辑器打开的临时文件,在写入/保存临时文件时我可以调用我的回调。监测是使用pyinotify完成的。我已经尝试过gnome-terminal
,xterm
,urxvt
和普通的gvim
,一切似乎都有效。
代码如下:
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_())
但pyinotify
仅适用于Linux。如果你能找到一个跨平台的解决方案(至少在Mac上),请告诉我。
更新:这似乎并不健全。 pyinotify
报告文件写入而不仅仅是文件关闭。我很沮丧。