Python 3 pyQt4 updating GUI with variables from multiple modules/classes - python

I've written a large program with nested classes/threads and multiple modules.
I would now like to add a simple GUI and some Labels to display some variables.
However, the variables are scattered throughout the modules and classes.
I'm looking for a way to update these variables into the GUI without altering
the current code too much.
I have a rudimentary understanding of Pyqt4 (i will accept tkinter answers also).
I've tried not to use signals/emits because to my knowledge emits
must be sent from a Qthread which would mean a complete overhaul of my code, changing
classes and threads over to Qthreads. Id like to avoid needing to do this if possible.
Here is one example I've attempted.
test.py
class Update(Thread):
def __init__(self):
Thread.__init__(self)
def run(self):
for i in range(10):
time.sleep(2)
import test
wa.label.setText(str(i))
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.label = QLabel(" ")
layout = QVBoxLayout()
layout.addWidget(self.label)
self.setLayout(layout)
Update1 = Update()
Update1.start()
Update1.refresh1 = 'ba'
self.label.setText(Update1.refresh1)
if __name__ == "__main__":
app = QApplication(sys.argv)
wa = MyWindow()
wa.show()
sys.exit(app.exec_())
This code works, but my variables need to update from other modules/classes or threads. The moment I move 'class Update' into a new module like THIS:
test.py
import test2
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.label = QLabel(" ")
layout = QVBoxLayout()
layout.addWidget(self.label)
self.setLayout(layout)
Update1 = test2.Update()
Update1.start()
Update1.refresh1 = 'ba'
self.label.setText(Update1.refresh1)
if __name__ == "__main__":
app = QApplication(sys.argv)
wa = MyWindow()
wa.show()
sys.exit(app.exec_())
test2.py #updates GUI
class Update(Thread):
def __init__(self):
Thread.__init__(self)
def run(self):
for i in range(10):
time.sleep(2)
import test
test.wa.label.setText(str(i))
I get: AttributeError: 'module' object has no attribute 'wa'
Also, I was also considering putting class Update() into a Qthread, running it from any module/class where a variable has been updated and using the emit function inside Update(). this would solve having to change my current classes/threads to be Qthreads.
If anyone knows of a simple way I could update my GUI simply by calling a class like update() an example would be appreciated

Because wa is only set when __name__ == "__main__" and that only happens when test.py is the main file.
When you do import test, you are running another instance of the test.py file which is not the main script so it has __name__ == 'test' not __main__. So, even if wa was set, you would be changing another instance of it.
Possible solution:
You can get a reference to the __main__ module and set on the test2.py module:
On test.py:
import test2
test2.parent = sys.modules[__name__]
Now, on test2.py (do not import test but make sure test imports test2):
parent.wa.label.setText('Blablabla')

Related

How to switch between two PyQt5 MainWindow widgets

I'm writing a program that has two different parts to it - let's call them sub1 and sub2. When I initially run my program, sub1 displays and I'm loading sub2 in the background but not displaying it. I have a menu action in sub1 that allows you to switch to sub2 and there is a menu action in sub2 that allows you to switch back to sub1. The problem I have is when trying to switch back from sub2 to sub1. Going from sub1 to sub2 works fine; sub1 gets hidden and sub2 is displayed. However, when trying to show sub1 again, sub2 doesn't get hidden. I'm new to PyQt as well as Python so I don't know all the intricacies yet. So, the method I'm using is just something I figured through trial and error and by no means needs to be this way. Simplified code below.
#mass module
class MASS(PyQt5.QtWidgets.QMainWindow, massUi.Ui_MainWindow):
def __init__(self):
super(MASS, self).__init__()
self.actionSwitchToCompEval.triggered.connect(self.switchToCompEval)
def switchToCompEval(self):
massForm = main.massForm
massForm.hide()
compForm = main.compForm
compForm.show()
def showMass(self):
main(1)
def main(initiate=None):
if initiate == None:
main.app = PyQt5.QtWidgets.QApplication(sys.argv)
main.massForm = MASS()
main.compForm = CompEval.CompEval()
main.massForm.show()
main.app.exec_()
elif initiate == 1:
main.massForm = MASS()
main.compForm = CompEval.CompEval()
main.compForm.hide()
main.massForm.show()
elif initiate == 2:
pass
if __name__ == '__main__':
main()
#comp module
class CompEval(PyQt5.QtWidgets.QMainWindow, compEvalUi.Ui_MainWindow):
def __init__(self):
super(CompEval, self).__init__()
self.actionSwitchToMASS.triggered.connect(self.switchToMass)
def switchToMass(self):
mass.MASS().showMass()
def main():
form = CompEval()
form.show()
In the switchToCompEval function, it seems to work fine to reference the main.massForm and main.compForm variables but when I try to go from sub2(comp) back to sub1(mass) I get an error that the function does not contain that variable, which seems odd to me. I am aware that several aspects of how I have this setup at the moment is odd and far from ideal, so any suggestions would be appreciated. Thanks.
So after much experimentation I determined the best solution to this problem is to combine the modules into one. IF you have multiple MainWindow widgets and you need to be able to switch back and forth between them, keep your classes that access the widgets all in the same module.
So I have my two widget classes:
import PyQt5
import sys
#Below are the modules that are auto-generated when using Qt Designer to make an interface
import compWidget as compEvalUi
import massWidget as massUi
class MASS(PyQt5.QtWidgets.QMainWindow, massUi.Ui_MainWindow
def __init__(self):
super(MASS, self).__init__()
#setupUi function is in the auto-generated module and contains all the attributes of the interface
self.setupUi(self)
#menuSwitch is the name of the button/menu item that would need to be clicked
#in order to switch windows.
#The example shown here is if the action was attached to a menu-dropdown item
#which is why 'triggered' is used as opposed to 'clicked' if it were attached to a button
self.menuSwitch.triggered.connect(self.switchToCompWidget)
def switchToCompWidget(self):
INITIALIZE.massForm.hide()
INITIALIZE.compForm.show()
class CompEval(PyQt5.QtWidgets.QMainWindow, compEvalUi.Ui_MainWindow):
def __init__(self):
super(CompEval, self).__init__()
self.setupUi(self)
self.menuSwitch.triggered.connect(self.switchToMassWidget)
def switchToMassWidget(self):
INITIALIZE.compForm.hide()
INITIALIZE.massForm.show()
class INITIALIZE:
def __init__(self):
app = PyQt5.QtWidgets.QApplication(sys.argv)
INITIALIZE.massForm = MASS()
INITIALIZE.massForm.show()
INITIALIZE.compForm = CompEval()
app.exec_()
def main():
program = INITIALIZE()
if __name__ =='__main__':
main()
You could use signals and slots to separate the logic instead of sharing a global parameters between classes.

Change Button Color in Qt Thread Python

I need to change the color of QPushButton, but an error occurred: "AttributeError: type object 'ProyectoTFM' has no attribute 'ui'".
I don't know hoy to acced to a ui variable from my thread.
This is my code:
import sys
import OpenOPC
import time
import threading
from proyectoQt import *
def actualizarDatosOPC():
while 1:
time.sleep(5)
if(itemsOPC[15])[1]!=0:
#Error on next line
ProyectoTFM.ui.AP08Button.setStyleSheet("background-color: red")
return
class ProyectoTFM(QtGui.QMainWindow):
def __init__(self,parent=None):
QtGui.QMainWindow.__init__(self,parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.startTheThread()
print('Init')
def startTheThread(self):
threadQt = threading.Thread(target = actualizarDatosOPC)
threadQt.start()
def clienteOPC():
opc=OpenOPC.client()
opc.connect('Kepware.KEPServerEX.V6')
global itemsOPC
while 1:
itemsOPC = opc.read(opc.list('PLC.PLC.TAGS'))
time.sleep(5)
return
threads = list()
threadOPC = threading.Thread(target=clienteOPC)
threads.append(threadOPC)
threadOPC.start()
time.sleep(5)
if __name__== "__main__":
app=QtGui.QApplication(sys.argv)
myapp = ProyectoTFM()
myapp.show()
sys.exit(app.exec_())
threadOPC.__delete()
Sorry for my English and thanks.
It is not correct to modify the view from a different thread to the main one, a way to solve the problem without using QThread is to create a signal that connects to some slot that changes the color of the button. To be able to emit the signal from the new thread we must pass the object to him through the parameter args.
def actualizarDatosOPC(obj):
while 1:
time.sleep(5)
if(itemsOPC[15])[1]!=0:
#Error on next line
obj.sendChangeColor.emit()
return
class ProyectoTFM(QtGui.QMainWindow):
sendChangeColor = QtCore.pyqtSignal()
def __init__(self,parent=None):
QtGui.QMainWindow.__init__(self,parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.startTheThread()
print('Init')
self.sendChangeColor.connect(lambda: self.ui.AP08Button.setStyleSheet("background-color: red"))
def startTheThread(self):
threadQt = threading.Thread(target = actualizarDatosOPC, args=(self,))
threadQt.start()
Even if you got this to work, you can't modify the UI from a thread directly.
A few things:
You never actually pass the UI to the function actualizarDatosOPC
so it doesn't know it exists.
Is there any reason you can't use PyQt's built in threading tools? If you are going to use PyQt it might make sense to buy into the whole framework.
def startTheThread(self):
self.threadQt = QThread()
d = actualizarDatosOPC(self)
d.moveToThread(self.threadQt)
self.threadQt.start()
def actualizarDatosOPC(widget):
.... widget.AP08Button.setStyleSheet("background-color: red")
If you do choose to go this route, I'd take a look at this thread which has a good example:
How to use QThread correctly in pyqt with moveToThread()?
Additionally, while the way you initialize your Window works, this is the more standard way to do it:
class ProyectoTFM(QMainWindow, Ui_MainWindow):
def __init__(self, parent):
# General Init Stuff
super(Login, self).__init__(parent)
self.setupUi(self)
After that, whenever you want to refer to something in the UI all you need to do is refer to self._____. For example, if you have a button named buttonA, self.buttonA would be the appropriate reference.
Edit:
As mentioned in another answer, the proper way to actually change the button color would be to emit a trigger that to the main thread which could then respond by changing the button color.

interactive python - keeping console interactive with a GUI mainloop

I am wondering how one would create a GUI application, and interact with it from the console that started it.
As an example, I would like to create a GUI in PyQt and work with it from the console. This could be for testing settings without restarting the app, but in larger projects also for calling functions etc.
Here is a simple example using PyQt:
import sys
from PyQt4 import QtGui
def main():
app = QtGui.QApplication(sys.argv)
w = QtGui.QWidget()
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
when this is run with python -i example.py the console is blocked as long as the main-loop is executed.
How can I call w.resize(100,100) while the GUI is running?
ops, posted wrong answer before
there is a post in Stack about that
Execute Python code from within PyQt event loop
The following example uses the code module to run a console in the command prompt (be sure to run the script from the command line). Subclassing QThread provides a route by which the console can be run in a separate thread from that of the main GUI and enables some interaction with it. The stub example below should be easy enough to incorporate into a larger packaged PyQt program.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import threading #used only to id the active thread
import code
import sys
class Worker(QThread): #Subclass QThread and re-define run()
signal = pyqtSignal()
def __init__(self):
super().__init__()
def raise_sys_exit(self): #more gracefully exit the console
print('(Deactivated Console)')
raise SystemExit
def setup_console(self,global_dict):
console_exit = {'exit': self.raise_sys_exit}
self.console = code.InteractiveConsole(locals=dict(global_dict,**console_exit))
def run(self):
try:
print('worker', threading.get_ident())
self.console.interact()
except SystemExit:
self.signal.emit()
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args,**kwargs)
self.data = [1,2,3,4] #some data we might want to look at
layout = QVBoxLayout()
self.b = QPushButton("Interact")
self.b.clicked.connect(self.b_clicked)
layout.addWidget(self.b)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.worker = Worker()
self.worker.signal.connect(self.finished)
def finished(self):
self.b.setEnabled(True)
def b_clicked(self):
print('main',threading.get_ident())
self.worker.setup_console(globals()) #pass the global variables to the worker
self.worker.start()
self.b.setEnabled(False) #disable the GUI button until console is exited
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
The easiest way is to use IPython:
ipython --gui=qt4
See ipython --help or the online documentation for more options (e.g. gtk, tk, etc).

How to connect a signal from the controller in PyQt4? (iOS like MVC structure in PyQt4)

Why doesn't the following example work?
from PyQt4 import QtGui
import sys
class TestView(QtGui.QWidget):
def __init__(self):
super(TestView, self).__init__()
self.initUI()
def initUI(self):
self.btn = QtGui.QPushButton('Button', self)
self.btn.resize(self.btn.sizeHint())
self.btn.move(50, 50)
class TestViewController():
def __init__(self, view):
view.btn.clicked.connect(self.buttonClicked)
view.show()
def buttonClicked(self):
print 'clicked'
def main():
app = QtGui.QApplication(sys.argv)
view = TestView()
TestViewController(view)
app.exec_()
if __name__ == '__main__':
main()
The example is supposed to represent an MVC structure (like the one in Figure 4 -- without the Model) where the controller (TestViewController) receives a reference to the view (TestView) and connects the clicked signal from the view's button view.btn to its function self.buttonClicked.
I'm sure the line view.btn.clicked.connect(self.buttonClicked) is executed but, apparently, it has no effect. Does anyone knows how to solve that?
Update (awful solution):
In the example, if I replace the line
view.btn.clicked.connect(self.buttonClicked)
with
view.clicked = self.clicked
view.btn.clicked.connect(view.clicked)
it works. I'm still not happy with that.
The reason it is not working is because the controller class is being garbage collected before you can ever click anything for it.
When you set view.clicked = self.clicked, what you're actually doing is making one of the objects from the controller persist on the view object so it never gets cleaned up - which isn't really the solution.
If you store your controller to a variable, it will protect it from collection.
So if you change your code above to read:
ctrl = TestViewController(view)
You'll be all set.
That being said - what exactly you are trying to do here, I am not sure...it seems you're trying to setup an MVC system for Qt - but Qt already has a pretty good system for that using the Qt Designer to separate the interface components into UI (view/template) files from controller logic (QWidget subclasses). Again, I don't know what you are trying to do and this may be a dumb down version of it, but I'd recommend making it all one class like so:
from PyQt4 import QtGui
import sys
class TestView(QtGui.QWidget):
def __init__(self):
super(TestView, self).__init__()
self.initUI()
def initUI(self):
self.btn = QtGui.QPushButton('Button', self)
self.btn.resize(self.btn.sizeHint())
self.btn.move(50, 50)
self.btn.clicked.connect(self.buttonClicked)
def buttonClicked(self):
print 'clicked'
def main():
app = QtGui.QApplication(sys.argv)
view = TestView()
view.show()
app.exec_()
if __name__ == '__main__':
main()
Edit: Clarifying the MVC of Qt
So this above example doesn't actually load the ui dynamically and create a controller/view separation. Its a bit hard to show on here. Best to work through some Qt/Designer based examples/tutorials - I have one here http://bitesofcode.blogspot.com/2011/10/introduction-to-designer.html but many can be found online.
The short answer is, your loadUi method can be replace with a PyQt4.uic dynamic load (and there are a number of different ways to set that up) such that your code ultimately reads something like this:
from PyQt4 import QtGui
import PyQt4.uic
import sys
class TestController(QtGui.QWidget):
def __init__(self):
super(TestController, self).__init__()
# load view
uifile = '/path/to/some/widget.ui'
PyQt4.uic.loadUi(uifile, self)
# create connections (assuming there is a widget called 'btn' that is loaded)
self.btn.clicked.connect(self.buttonClicked)
def buttonClicked(self):
print 'clicked'
def main():
app = QtGui.QApplication(sys.argv)
view = TestController()
view.show()
app.exec_()
if __name__ == '__main__':
main()
Edit 2: Storing UI references
If it is easier to visualize this concept, you Can also store a reference to the generated UI object:
from PyQt4 import QtGui
import PyQt4.uic
import sys
class TestController(QtGui.QWidget):
def __init__(self):
super(TestController, self).__init__()
# load a view from an external template
uifile = '/path/to/some/widget.ui'
self.ui = PyQt4.uic.loadUi(uifile, self)
# create connections (assuming there is a widget called 'btn' that is loaded)
self.ui.btn.clicked.connect(self.buttonClicked)
def buttonClicked(self):
print 'clicked'
def main():
app = QtGui.QApplication(sys.argv)
view = TestController()
view.show()
app.exec_()
if __name__ == '__main__':
main()

PyQt - is it possible to run two applications?

Two files. Each runs new window and works by itself. I need to run them both.
When I run first.pyw, only one (second) window is shown.
Is it possible two run them both?
first.pyw:
import sys
from PyQt4.QtGui import *
import second
class first(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setWindowTitle('first')
app = QApplication(sys.argv)
firstApp = first()
firstApp.show()
sys.exit(app.exec_())
second.pyw:
import sys
from PyQt4.QtGui import *
class second(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setWindowTitle('second')
app2 = QApplication(sys.argv)
secondApp = second()
secondApp.show()
sys.exit(app2.exec_())
How can I run two applications that are in different modules?
The accepted answer is essentially right, but there are cases where you want to run multiple QApplications one after the other, e.g. :
Unit tests
A command-line tool that shouldn't require a running X server (hence no QApplication on startup), but can optionally show a window if the user's system supports it
I ended up using the multiprocessing module to start each QApplication in a separate process, so that each one is independent from the others.
from multiprocessing import Queue, Process
class MyApp(Process):
def __init__(self):
self.queue = Queue(1)
super(MyApp, self).__init__()
def run(self):
app = QApplication([])
...
self.queue.put(return_value)
app1 = MyApp()
app1.start()
app1.join()
print("App 1 returned: " + app1.queue.get())
app2 = MyApp()
app2.start()
app2.join()
print("App 2 returned: " + app1.queue.get())
You can only run a single application at a time, although your application can have multiple top-level windows. The QCoreApplication docs say that:
...there should be exactly one QCoreApplication object.
This also holds true for QApplication as it derives from QCoreApplication. You can get access to that application through the QCoreApplication.instance() method or the qApp macro in C++.
What do you expect to get out of having two different applications running? Instead, you could have each module provide a top-level window that then gets displayed by the application launcher.
You import second. Hence it is interpreted before you even reach the definition of class first. As the last line of second.pyw is sys.exit, nothing behind it can be executed.

Categories