PyQt4: Only the last signal is being processed - python

I ran into a strange problem when working on my project. I have a GUI and a QTextEdit that serves as a status browser. When a button is clicked, I want the QTextEdit to display a 10 second countdown while another function is happening in a separate thread. Even though I emit a signal every second, the QTextEdit hangs for 9 seconds, then displays the last countdown number.
I thought this might have something to do with stuff happening in a separate thread, so I created a separate example to test this out. In my simple example, there are two things: a QTextEdit and a Button. When the button is clicked, the status browser should display '5' for two seconds, then '4'.
Here is the code:
import sys
from PyQt4 import QtGui, uic
from PyQt4.QtCore import QObject, pyqtSignal
import time
class MainUI(QObject):
status_signal = pyqtSignal(str)
def __init__(self, window):
super(QObject, self).__init__()
self.ui = uic.loadUi(r'L:\internal\684.07\Mass_Project\Software\PythonProjects\measure\testing\status_test.ui', window)
self.ui.statusBrowser.setReadOnly(True)
self.ui.statusBrowser.setFontPointSize(20)
self.status_signal.connect(self.status_slot)
self.ui.button.clicked.connect(self.counter)
window.show()
def status_slot(self, message):
self.ui.statusBrowser.clear()
self.ui.statusBrowser.append(message)
def counter(self):
print 'clicked'
i = 5
self.status_signal.emit(str(i))
time.sleep(2)
self.status_signal.emit(str(i-1))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
app.setStyle("cleanlooks")
main_window = QtGui.QDialog()
main_ui = MainUI(main_window)
sys.exit(app.exec_())
In this example, the same thing happens. The status browser hangs for 2 seconds, then only displays '4'. When I alter the status_slot function so that it doesn't clear the status browser before appending to it, the status browser waits for 2 seconds, then emits both signals at once, displaying '5 \n 4'. Does anyone know why this is happening and what I can do to constantly update the display? Thanks in advance!

time.sleep() blocks the Qt main loop so it can't process window redraw events. Use a QTimer to periodically call a method which emits your signal so that control is returned to the Qt event loop regularly.

Related

Is there any way to bind a differnt click handler to QPushButton in PyQt5?

I have a QPushbutton:
btn = QPushButton("Click me")
btn.clicked.connect(lambda: print("one"))
Later in my program, I want to rebind its click handler, I tried to achieve this by calling connect again:
btn.clicked.connect(lambda: print("two"))
I expected to see that the console only prints two, but actually it printed both one and two. In other words, I actually bound two click handlers to the button.
How can I rebind the click handler?
Signals and slots in Qt are observer pattern (pub-sub) implementation, many objects can subscribe to same signal and subscribe many times. And they can unsubscribe with disconnect function.
from PyQt5 import QtWidgets, QtCore
if __name__ == "__main__":
app = QtWidgets.QApplication([])
def handler1():
print("one")
def handler2():
print("two")
button = QtWidgets.QPushButton("test")
button.clicked.connect(handler1)
button.show()
def change_handler():
print("change_handler")
button.clicked.disconnect(handler1)
button.clicked.connect(handler2)
QtCore.QTimer.singleShot(2000, change_handler)
app.exec()
In case of lambda you can only disconnect all subscribers at once with disconnect() (without arguments), which is fine for button case.

How to use a timer or equivalent within a python thread without blocking the GUI

I am a writing a GUI based application using the PyQT framework which connects to a device, sends commands, reads the corresponding data and displays this in real time to a table, graph widget and to a file.
Once the run button is clicked it starts a thread which sends the external device commands according to a procedure table and emits signals with the data to various methods to change the GUI.
When the run button is clicked it executes the following lines:
worker = Worker(self.runProcedure)
worker.signals.updateResults.connect(self.updateResultsTable)
worker.signals.writeResults.connect(self.writeResultsFile)
worker.signals.finished.connect(self.procedure_complete)
self.threadpool.start(worker)
within the runProcedure commands are sent to the device from the procedure table and the data read from the device is put into a list 'hfData' using code similar to that listed below:
while float(currentForceReading) <= float(target) and stopAlarm == 0:
ts = dt.now().timestamp()
hfData = (connection.readline()).split()
updateResults_callback.emit(ts, hfData) #method to update results table and graphs
writeResults_callback.emit(ts, hfData) #method to write results to a file
One of the options in the application is to hold for a period of time where it continues collecting data from the device without sending new commands.
I am looking for a way to continue taking measurements whilst in this hold time and continue updating the GUI.
I have tried to implement the following code, however this while loop blocks the GUI from updating:
stepHoldTime = float(procedureModel.data(procedureModel.index(row,4), Qt.DisplayRole))
if(stepHoldTime != 0):
endTime = time.monotonic() + stepHoldTime
while(time.monotonic() < endTime):
ts = dt.now().timestamp()
hfData = (connection.readline()).split()
updateResults_callback.emit(ts,hfData)
Is there a correct way to implement this functionality?
Instead of while-loop you could run QTimer which will execute code every few milliseconds.
It is minimal example which shows how to run function every 1000ms and update time in label.
from PyQt5.QtWidgets import QApplication, QLabel
from PyQt5.QtCore import QTimer, QDateTime
class Window(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.showTime()
help(QTimer)
self.timer = QTimer()
self.timer.timeout.connect(self.showTime)
self.timer.start(1000)
def showTime(self):
text = QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')
self.setText(text)
if __name__ == '__main__':
app = QApplication([])
win = Window()
win.show()
app.exec()
But your problem can be more complex and it may need more complex solution. You may have to show minimal working code which we could run and see problem - and test some ideas to resolve problem.
You can start your "reader" from a separate thread
class YourUi(QtWidgets.QMainWindow):
update_data = QtCore.pyqtSignal(str)
def __init__(self):
super(YourUi, self).__init__()
self.update_data.connect(self.do_update_data)
t = threading.Thread(target=self.update_worker, args=(self.update_data,), daemon=True)
t.start()
#staticmethod
def update_worker(signal):
connection = create_conncetion()
while True:
hfData = (connection.readline()).split()
signal.emit(hfData)
time.sleep(0.1)
def do_update_data(self, hf_data):
# access GUI elemetns from main thread to prevent freezing
self.some_qt_label.setText(str(hf_data))

PySide2 UI stops responding when entering a while loop after showing it

I'm having trouble where my QtWidget stops responding if I try to use a while loop after calling the widget.show() method for the QtWidget object. I initially thought the issue was in how I was using the signals and slots. I had created my own signal with new_data = Signal(float) and I was sampling data at an interval established by a time.sleep() call within that while loop and emitting the new_data signal each time the data was sampled. That was connected to a method in my QtWidget that just set the text of a label in the QtWidget to display the new data.
However, after some testing I found that if I ONLY try to print("in loop") inside that while loop, I get the same behavior. The QtWidget object stops responding. What is the proper way to update PySide2 interface at a periodic interval from outside the interface object? Can I perhaps run the interface as a process and feed it with updated data with a queue? I imaging that is possible, but am having trouble finding an example. The interface is only one piece of this application primarily made in Python and I will have multiple processes and multiple queues besides the Qt interface. Here is the code:
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QWidget
from PySide2.QtCore import QFile, QObject, Signal, Slot
import time
import NI9213
class MyDaq(QObject):
new_daq_data = Signal(float)
def __init__(self):
QObject.__init__(self)
daq_channels = "cDAQ1Mod2/ai0"
self.daq = NI9213.NI9213(channels=daq_channels)
def sample_daq(self):
data = self.daq.read_all()
self.new_daq_data.emit(data)
class DigitalDisplay(QWidget):
def __init__(self):
#Initialize the QWidget object used to create the user interface
QWidget.__init__(self)
#Load the user interface
designer_file = QFile("signal_digital_display.ui")
designer_file.open(QFile.ReadOnly)
loader = QUiLoader()
self.ui = loader.load(designer_file, self)
designer_file.close()
#Add title to the UI window
self.setWindowTitle("Digital Display")
self.mode = 'run'
self.ui.stopButton.clicked.connect(self.stopMode)
self.sampling_period = 0.1
#Slot(float)
def refresh_data(self, data):
self.ui.label.setText(str(data))
def stopMode(self):
self.mode = 'stop'
if __name__ == "__main__":
app = QApplication(sys.argv)
digital_display = DigitalDisplay()
digital_display.show()
## data = MyDaq()
## data.new_daq_data.connect(digital_display.refresh_data)
##
while(digital_display.mode=='run'):
print("after display.show")
## data.sample_daq()
time.sleep(digital_display.sampling_period)
sys.exit(app.exec_())
The problem here is that you have an infinite loop before you start the Qt event loop.
if __name__ == "__main__":
app = QApplication(sys.argv) # [1]
digital_display = DigitalDisplay()
digital_display.show() # [2]
while(digital_display.mode=='run'):
print("after display.show")
time.sleep(digital_display.sampling_period) # [3]
sys.exit(app.exec_()) # [4]
[1] This creates the QApplication object. It does not start the event loop yet.
[2] Create your widget and open a new window to display it. The operating system will create events for the window, but they have no effect on the application itself until we start the event loop.
[3] digital_display.mode will never change. This is an infinite loop and Python will never advance past this point.
[4] Here we start the application event loop and close the process once the application has finished. But we never get here.
What you should do instead is to create a QTimer on the DigitalDisplay widget that periodically fires a signal that can be connected to data.sample_daq.

Click event is sent immediately after I start application (PySide and Python)

I have an application with a button for which the clicked signal is connected to a slot that opens a QFileDialog. I want to manipulate the state of the button (sender) within the slot depending on the actions taken by the user in the QFileDialog.
However, with the code I have presently, my application do not starts correctly. It starts immediately with QFileDialogOpen and I do not understand why. When I comment the line that connect the button's clicked signal to the slot, the application starts normally though.
How can I correctly pass the button as an argument when I want to connect a clicked signal of a button to a slot? Here is a MCWE of my problem:
from PySide import QtGui
import sys
class MyApplication(QtGui.QWidget):
def __init__(self, parent=None):
super(MyApplication, self).__init__(parent)
self.fileButton = QtGui.QPushButton('Select File')
self.fileButton.clicked.connect(self.select_file(self.fileButton))
layout = QtGui.QGridLayout()
layout.addWidget(self.fileButton)
self.setLayout(layout)
def select_file(self, button):
file_name = QtGui.QFileDialog.getOpenFileName()
if str(file_name[0]) is not "":
button.setEnabled(True)
else:
button.setDisabled(True)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
w = MyApplication()
w.show()
sys.exit(app.exec_())
You don't bind an actual function call using PySide signals/slots, you bind a function, method, or function-like object using PySide's signals/slots.
You have:
self.fileButton.clicked.connect(self.select_file(self.fileButton))
This tells Qt to bind the click event to something that is returned from the self.select_file function call, which presumably has no __call__ attribute and is called immediately (causing the opening of the QFileDialog)
What you want is the following:
from functools import partial
self.fileButton.clicked.connect(partial(self.select_file, self.fileButton))
This creates a callable, frozen function-like object with arguments for Qt to call.
This is comparable to saying:
self.fileButton.clicked.connect(self.select_file)
Rather than saying:
self.fileButton.clicked.connect(self.select_file())

How does PySide/PyQt QMainWindow close a QDockWidget?

I need to know how the QDockWidget is normally closed. I have a serial port/thread attached to a QDockWidget, and I need to make sure the thread and serial port close properly.
class SerialDock(QDockWidget):
...
def close(self):
print("Close")
self.serialport.close()
self.thread.close()
def closeEvent(self, event):
print("closeEvent")
self.serialport.close()
self.thread.close()
The close and closeEvents are not called when I click the QMainWindow X button. Do I have to call the close method from the QMainWindow close? The only way I know to solve this is to use the QApplication.aboutToQuit signal, and I really don't want to have to remember to set that for one specific widget. How does the QDockWidget get destroyed or closed?
You can use the destroyed signal in the QDockWidget:
import PyQt4.QtGui as ui
import PyQt4.QtCore as core
app = ui.QApplication([])
mw = ui.QMainWindow()
mw.setCentralWidget(ui.QTextEdit())
dw = ui.QDockWidget("Test",mw)
dw.setWidget(ui.QLabel("Content"))
mw.addDockWidget(core.Qt.RightDockWidgetArea, dw)
def onDestroy(w):
print("Do stuff here")
print(w)
dw.destroyed.connect(onDestroy)
mw.show()
app.exec_()

Categories