Interact with continuously running Python Gui by external and exchangeable python script - python

my long term goal is to build a gui for an experiment in experimental physics which has a continuously running gui. By pushing a button I would like to be able to run a pyhton script of my choice which can interact with the running gui. For example setting a number to a spin box.
I attached a starting project. A spinbox and a button. If the button is pressed a random number is set to the spinbox and as soon as the number in the spinbox changes, it prints the number.
Is there a way to call a script (at the moment with a hard coded path) by pushing the button, which then sets the number in the gui to my choice. The content of the script (in this case the number which is set to the spin box) has to be editable during the runtime of the gui.
If you could provide an example for this, I would be grateful and could build the rest myself.
Thanks in advance!
import sys
import random
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QWidget, QDoubleSpinBox, QPushButton
class GuiInteraction(QWidget):
def __init__(self):
super().__init__()
self.initGUI()
self.CallBackFunctions()
def initGUI(self):
self.resize(400, 500)
self.move(300, 300)
self.setWindowTitle('Gui Interaction')
self.doubleSpinBox = QDoubleSpinBox(self)
self.doubleSpinBox.setGeometry(QtCore.QRect(120, 130, 120, 25))
self.doubleSpinBox.setDecimals(5)
self.doubleSpinBox.setMaximum(1000)
self.doubleSpinBox.setObjectName("doubleSpinBox")
self.pushButton = QPushButton("Run Script", self)
self.pushButton.setGeometry(QtCore.QRect(100, 300, 100, 40))
self.pushButton.setObjectName("pushButton")
def CallBackFunctions(self):
self.pushButton.clicked.connect(self.buttonClicked)
self.doubleSpinBox.valueChanged.connect(self.valueChanged)
def buttonClicked(self):
self.doubleSpinBox.setValue(random.uniform(1, 200))
def valueChanged(self):
print(self.doubleSpinBox.value())
if __name__ == '__main__':
app = QApplication(sys.argv)
MyWindow = GuiInteraction()
MyWindow.show()
sys.exit(app.exec_())

I'm thinking you can call a FileDialog, pick a script and use:
mod = __import__(path)
, and than should the script be adequately built with a "run" function of some kind you can just launch it by:
mod.run()
Check this question also.

One way would be to just communicate with an external script through stdin/stdout
my_script.py
def main():
print '4.2'
if __name__ == '__main__':
main()
gui_script.py
import subprocess
...
def buttonClicked(self):
try:
value = float(subprocess.check_output(['python', 'my_script.py']).strip())
except ValueError:
value = 1.0
self.doubleSpinBox.setValue(value)
If you need to pass arguments to your function you can just pass them as additional arguments to the subprocess.check_output call, and them read them from sys.argv (they'll all come in as strings, so you'd have to convert the types if necessary, or use a library like argparse, which can do the type-casting for you) in the called script.

I went for the solution of #armatita, even though I changed it a little. After a brief research __import__ seems to be replaced by the libimport libary, which I now use. The following lines have been added:
Header:
import importlib
import threading
and in the main function:
def buttonClicked(self):
ScriptControlModule = importlib.import_module("ExternalScript")
ScriptControlModule = importlib.reload(ScriptControlModule)
ScriptControlThread = threading.Thread(target=ScriptControlModule.execute,args=(self,))
ScriptControlThread.start()
My question is answered by the importlib lines. I also wanted to start the script in a subthread, so in the case it crashes due to a typo or anything else, the whole gui does not follow the crash.
The ExternalScript is in the same folder and named ExternalScript.py
import random
def execute(self):
self.doubleSpinBox.setValue(random.uniform(1, 5))
Three simple lines of code. I can change these lines while running and get different values in the SpinBox. Works out perfectly!

Related

Using qtDesigner with python seamlessly [duplicate]

This question already has answers here:
QtDesigner changes will be lost after redesign User Interface
(2 answers)
Closed 4 years ago.
I've been looking for a better way to work with frontends made using qtDesigner connected to a python backend. All the methods I have found have the following form:
Make GUI in designer
Output to python code using pyuic (usually with -x option)
Write backend code inside this output file
This methodology is not simple to maintain or edit. Any time you change the UI, it completely breaks workflow: you have to reconvert, generate a new file, fix that file back up to where you were before, then finally get back on track. This requires a lot of manual copy-paste of code, which is an invitation to errors on multiple levels (newly generated file layout may not be the same, manually fixing name changes while pasting, etc.). You can also end up losing work if you aren't careful, since you could accidentally overwrite the file and destroy the backend code.
Also, this doesn't use any of the control in qtDesigner like the Signal/Slot and Action editors. These must be here for something, but I can't find a way to actually direct these to call backend functions.
Is there a better way to work with this, possibly using the features of qtDesigner?
You don't have to add your code in the output file :
If you take a 'home.py' generated by PYUIC, containing a QMainWindow which name set by QtDesigner/generated by Puic would be Ui_Home(), your main code could be :
from home import Ui_Home
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class window_home(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
#set up the user interface from Designer
self.ui = Ui_Home()
self.ui.setupUi(parent)
#then do anything as if you were in your output file, for example setting an image for a QLabel named "label" (using QtDesigner) at the root QMainWindow :
self.ui.label.setPixmap(QPixmap("./pictures/log.png"))
def Home():
f=QMainWindow()
c=window_home(f)
f.show()
r=qApp.exec_()
if __name__=="__main__":
qApp=QApplication(sys.argv)
Home()
I found an even cleaner method for working with this, that does not require preemptive conversion after each edit at all. Instead it takes the .ui file itself, so all you need to do is restart the program itself to update the design.
import sys
import os
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5 import uic
path = os.path.dirname(__file__) #uic paths from itself, not the active dir, so path needed
qtCreatorFile = "XXXXX.ui" #Ui file name, from QtDesigner, assumes in same folder as this .py
Ui_MainWindow, QtBaseClass = uic.loadUiType(path + qtCreatorFile) #process through pyuic
class MyApp(QMainWindow, Ui_MainWindow): #gui class
def __init__(self):
#The following sets up the gui via Qt
super(MyApp, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#set up callbacks
self.ui.NAME OF CONTROL.ACTION.connect(self.test)
def test(self):
#Callback Function
if __name__ == "__main__":
app = QApplication(sys.argv) #instantiate a QtGui (holder for the app)
window = MyApp()
window.show()
sys.exit(app.exec_())
Note that this is Qt5. Qt5 and Qt4 are not API compatible, so it will be a little different in Qt4 (and presumably earlier as well).

Keep PyQt UI Responsive With Threads

I have created a relatively complex PyQt program and am trying to implement threads so that when the program encounters a part of the program which is particularly CPU intensive, the GUI will remain refreshed and responsive throughout. Sadly though, I am having some difficulties with the threading.
I am using Python 2.7 for reasons that I don't believe to be relevant.
Anyway, the entire program runs within one class and calls upon a PyQt designer .ui file in order to display the actual GUI. When a particular button is pressed, in order to shred a file, it calls a function within that class that then starts a thread using the 'thread' module, yes, outdated, I know. The shredding function that is then called from this commences the shredding of the file. Throughout the shredding of the file, the actual shredding function interacts and adds bits to the GUI in order to keep the user up to date on what is happening.
During the execution of the function the GUI continues to be refreshed, however it does become a little laggy, I can cope with that. However, when that function is complete, instead of smoothly continuing and allowing the user to keep using the program, the program throws a complete hissy fit and simply just stops working and has to be closed.
Hopefully someone can assist me here. I would greatly appreciate as much detail as possible as I have been searching around for a way to cope with this for a good number of weeks now.
I am using PyQt4.
Here's a simple demo of threading in pyqt5. Qt has it's own threading class that works pretty well.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import pyqtSignal
import sys
import time
class TheBoss(QtWidgets.QWidget):
def __init__(self, parent=None):
super(TheBoss, self).__init__(parent)
self.resize(300,200)
self.VL = QtWidgets.QVBoxLayout(self)
self.label = QtWidgets.QLabel()
self.VL.addWidget(self.label)
self.logger = Logger()
self.logger.sec_signal.connect(self.label.setText)
self.logger.start()
def closeEvent(self,event):
self.logger.terminate()
class Logger(QtCore.QThread):
sec_signal = pyqtSignal(str)
def __init__(self, parent=None):
super(Logger, self).__init__(parent)
self.current_time = 0
self.go = True
def run(self):
#this is a special fxn that's called with the start() fxn
while self.go:
time.sleep(1)
self.sec_signal.emit(str(self.current_time))
self.current_time += 1
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setApplicationName("Thread Example")
window = TheBoss()
window.show()
sys.exit(app.exec_())

Different program behavior depending if its main part is enclosed in a function

Right at the beginning I would like to say that I am a very beginner when it comes to writing GUI applications and this is the topic of this question.
I am using Anaconda 4.1.1 as my Python distribution and Spyder as my IDE. I was trying to write a very simple window application using PyQt4 (already installed in Anaconda) or - to be precise - I copied some examples form the Internet and try to make some "experiments" with them. The results are really surprising for me.
The thing is that when I am running the following example in the IPython console:
import sys
from PyQt4 import QtGui
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.show()
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
everything seems to be OK - I can close the window of an application whenever I want and it successfully opens once again when I am running the code once again. But when I make a small modification in the code, namely - getting rid of the main function:
import sys
from PyQt4 import QtGui
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
then I can run my program only once, because when I close it and try to run it once again - the IPython console displays the following message:
It seems the kernel died unexpectedly. Use 'Restart kernel' to continue using this console.
repeating every few seconds. The Kernel itself displays something like that:
QWidget: Must construct a QApplication before a QPaintDevice
I don't really understand how can it happen because for me these two pieces of code should work exactly the same since the only difference between them is presence of the main function which (I believe) is not necessary to make this program work. Why does it give an error the second way?

Error "QObject::startTimer: QTimer can only be used with threads started with QThread" many times when closing application

I know this has been asked many times before. I read all of those threads, and my case seems different. Everybody else who has this trouble has a few straightforward causes that I think I’ve ruled out, such as:
Starting a timer with no event loop running
Starting/stopping a timer from a thread other than the one that created the timer
Failing to set the parent property of a widget, leading to problems with the order of destruction
Below I have a minimal code sample that demonstrates the problem. Notice that I’ve started no threads or timers. I also have set the parent of every widget. If I remove the graph widgets, the problem goes away, so one is tempted to blame pyQtGraph, however, if I include the plot widgets but exclude all the blank tabs (i.e. every tab except tabCatchaTiger), the problem also goes away, and that seems to vindicate pyQtGraph.
Versions:
Windows 7
Python 2.7.8
Wing IDE 5.0.9-1
PyQt 4.11.1
PyQwt 5.2.1
PyQtGraph 0.9.8
Test case:
from PyQt4 import Qt, QtGui, QtCore
import PyQt4.Qwt5 as Qwt
import pyqtgraph as pg
pg.functions.USE_WEAVE = False # Lets pyqtgraph plot without gcc
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
# GUI for visualizing data from database
class crashyGUI(QtGui.QWidget) :
def __init__(self) :
# Make the window
QtGui.QWidget.__init__(self)
self.resize(700, QtGui.QDesktopWidget().screenGeometry(self).height()*.85)
self.setWindowTitle('Data Visualization')
# Create tab interface
tabWidget = QtGui.QTabWidget(self)
# define the tab objects
self.tabEeny = QtGui.QWidget(tabWidget)
self.tabMeeny = QtGui.QWidget(tabWidget)
self.tabMiney = QtGui.QWidget(tabWidget)
self.tabMoe = QtGui.QWidget(tabWidget)
self.tabCatchaTiger = QtGui.QWidget(tabWidget)
self.tabByThe = QtGui.QWidget(tabWidget)
self.tabToe = QtGui.QWidget(tabWidget)
# Initialize the tab objects
self.initTabCatchaTiger()
###########################################
############### Main Layout ###############
###########################################
tabWidget.addTab(self.tabEeny, 'Eeny')
tabWidget.addTab(self.tabMeeny, 'Meeny')
tabWidget.addTab(self.tabMiney, 'Miney')
tabWidget.addTab(self.tabMoe, 'Moe')
tabWidget.addTab(self.tabCatchaTiger, 'Catch a Tiger')
tabWidget.addTab(self.tabByThe, 'By The')
tabWidget.addTab(self.tabToe, 'Toe')
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.addWidget(tabWidget)
self.setLayout(self.mainLayout)
def initTabCatchaTiger(self):
###########################################
############# ADC Capture Tab #############
###########################################
# define tab layout
grid = QtGui.QGridLayout(self.tabCatchaTiger)
# create copy of adc plot and add to row 3 of the grid
self.catchaTigerPlot1 = pg.PlotWidget(name = 'Catch a Tiger 1', parent = self.tabCatchaTiger)
self.catchaTigerPlot1.setTitle('Catch a Tiger 1')
grid.addWidget(self.catchaTigerPlot1, 2, 0, 1, 8)
self.catchaTigerPlot2 = pg.PlotWidget(name = 'Catch a Tiger 2', parent = self.tabCatchaTiger)
self.catchaTigerPlot2.setTitle('Catch a Tiger 2')
grid.addWidget(self.catchaTigerPlot2, 3, 0, 1, 8)
# set layout for tab
self.tabCatchaTiger.setLayout(grid)
def closeEvent(self, event) :
pass
def main() :
# open a QApplication and dialog() GUI
app = QtGui.QApplication([])
windowCrashy = crashyGUI()
windowCrashy.show()
app.exec_()
main()
There seem to be two closely-related issues in the example.
The first one causes Qt to print the QObject::startTimer: QTimer can only be used with threads started with QThread messages on exit.
The second one (which may not affect all users) causes Qt to print QPixmap: Must construct a QApplication before a QPaintDevice, and then dump core on exit.
Both of these issues are caused by python deleting objects in an unpredicable order when it exits.
In the example, the second issue can be fixed by adding the following line to the __init__ of the top-level window:
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
Unless QApplication.setQuitOnLastWindowClosed has been changed to False, this will ensure that the application quits at the right time, and that Qt has a chance to automatically delete all the children of the top-level window before the python garbage-collector gets to work.
However, for this to be completely successful, all the relevant objects must be linked together in a parent-child hierarchy. The example code does this where it can, but there seem to be some critical places in the initialization of the PlotWidget class where it is not done.
In particular, there is nothing to ensure that the central item of the PlotWidget has a parent set when it is created. If the relevant part of the code is changed to this:
class PlotWidget(GraphicsView):
...
def __init__(self, parent=None, background='default', **kargs):
GraphicsView.__init__(self, parent, background=background)
...
self.plotItem = PlotItem(**kargs)
# make sure the item gets a parent
self.plotItem.setParent(self)
self.setCentralItem(self.plotItem)
then the first issue with the QTimer messages also goes away.
Here's a better answer:
You are allowing the QApplication to be collected before python exits. This causes two different issues:
The QTimer error messages are caused by pyqtgraph trying to track its ViewBoxes after the QApplication has been destroyed.
The crash appears to be intrinsic to Qt / PyQt. The following crashes in the same way:
from PyQt4 import Qt, QtGui, QtCore
def main() :
app = QtGui.QApplication([])
x = QtGui.QGraphicsView()
s = QtGui.QGraphicsScene()
x.setScene(s)
x.show()
app.exec_()
main()
You can fix it by adding global app to your main function, or by creating the QApplication at the module level.
Try to write this in block of __init__:
self.setAttribute(Qt.WA_DeleteOnClose)
Personally, I don't put any effort into chasing exit crashes anymore--just use pg.exit() and be done with it.
(but if you do happen to find a bug in pyqtgraph, don't hesitate to open an issue on github)
I had this happen as well and in my case it was caused by a call to deleteLater() on the aboutToQuit-Signal of the application, like so:
def closeEvent(self, *args, **kwargs):
self.deleteLater()
if __name__ == "__main__":
application = QtWidgets.QApplication(sys.argv)
window = testApplication()
# Handle application exit
application.aboutToQuit.connect(window.closeEvent)
# System exit
sys.exit(application.exec_())
Getting rid of the deleteLater on the whole window seemed to solve it.

PyQt4 SIGNAL/SLOT problem when using sub-directories

Thanks in advance for taking the time to read this. Apologies that it is somewhat verbose. But hopefully it fully explains the problem. Stripped code demonstrating the issue is included.
I'm having an issue with PyQt4 SIGNAL/SLOTS. While I can make everything work fine if I am writing in a single file, I can't make things work if I some of the functions I wish to use are moved to sub-directories/classes.
I've looked through the Python Bindings document I can see how this works when using a single file. But what I am trying to do is this:
main.py file in root dir which contains the MainWindow __init__ code.
This file imports a number of widgets. Each widget is stored in its own sub-directory. All sub-directories contain an __init__.py file. These sub-directories are inside of a directory called 'bin', which is itself in the root dir
Some of these widgets need to have SIGNAL/SLOT links between them This is where I fall down.
So the file structure is:
- main.py
- bin/textEditor/__init__.py
- bin/textEditor/plugin.py
- bin/logWindow/__init__.py
- bin/logWindow/plugin.py
The following code shows the problem. This code creates a very basic main window that contains a central QTextEdit() widget and a dockable QTextEdit() widget. All that happens is that when the text in the central widget is changed, the same text is shown in the dockable widget. The example works. But it does so by connecting the signal textChanged() in the bin/textEditor/plugin.py file that creates the central QTextEdit() with a function in main.py. I would like it to do exactly the same thing but connexted to the updateUi function in bin/textEditor/plugin.py
If anyone could shed some light on this, I would be hugely grateful. I'm sure it is simple. But direction to any tutorials that cover this or statements that I am doing it all very wrong are equally appreciated!. Thanks again for your time:
### main.py
import os
import sys
# Import PyQT modules
from PyQt4.QtCore import *
from PyQt4.QtGui import *
# Start the main class
class MainWindow(QMainWindow):
# Initialise
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# Name and size the main window
self.setWindowTitle("EDITOR/LOG")
self.resize(800, 600)
import bin.logWindow.plugin as logWindow
logWindow.create(self)
import bin.textEditor.plugin as textEditor
textEditor.create(self)
def updateUi(self):
# I can connect to this function from within bin/textEditor/plugin.py (see
# below) but I want to connect to the function located in
# bin/textEditor/plugin.py instead
text = self.editor.toPlainText()
self.logWidget.setText(text)
# Run the app
def main():
app = QApplication(sys.argv)
form = MainWindow()
form.show()
app.exec_()
# Call main
main()
The code inside of the two plugin files is:
### bin/textEditor/plugin.py
# Import PyQT modules
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def create(self):
# Add a dockable widget
self.logDockWidget = QDockWidget("Log", self)
self.logDockWidget.setObjectName("LogDockWidget")
self.logDockWidget.setAllowedAreas(Qt.LeftDockWidgetArea|
Qt.RightDockWidgetArea)
self.logWidget = QTextEdit()
self.logDockWidget.setWidget(self.logWidget)
self.addDockWidget(Qt.LeftDockWidgetArea, self.logDockWidget)
And
### bin/logWindow/plugin.py
Import PyQT modules
from PyQt4.QtCore import *
from PyQt4.QtGui import *
def create(self):
# Create a text editing box
self.editor = QTextEdit()
# Add to main window
self.setCentralWidget(self.editor)
# connect text change to update log window. This is presumably what I need to
# change so that it connects to the function below instead of the on in main.py
self.connect(self.editor, SIGNAL("textChanged()"), self.updateUi)
def updateUi(self):
text = self.editor.toPlainText()
self.logWidget.setText(text)
For starters, is there a reason you're using a very old version of the PyQt release document? The new one is: here
There are a few things you are doing that are a bit unusual. Generally import statements in python are placed at the top of the file (to more easily see dependencies), but I assume you're doing this to support a more generalized import system for plugins in the future.
It seems like the basic problem is you're trying to connect a signal source to a slot in another object, without storing that other object in a particular place. To do this you probably need to either make the connection in main, make a neutral "updateUi" slot that emits it's own special signal that all the plugins are waiting for, or just keep a reference to those subobjects in main and be careful with the initialization order.

Categories