pythonmultithreadingpyqtpyqt4chaco

Signal fails to emit with Chaco and PyQt


I am trying to work with Chaco and pyqt in plotting a real-time data acquisition task for laboratory hardware. I was previously using matplotlib, however it proved to be too slow (I even tried animation). The following code works fine when I embedded a matplotlib plot in a pyqt window, but with chaco, nothing happens when I emit my update signal from inside a thread. This code will work if you do not use a thread for the simulated acquisition. I have tried using qthreads to no avail either (including something like this: Threading and Signals problem in PyQt). Is there anyone out there who has used pyqt + chaco + threading that could help me find where I am going wrong, or what is happening?

import sys
import threading, time
import numpy as np

from enthought.etsconfig.etsconfig import ETSConfig
ETSConfig.toolkit = "qt4"

from enthought.enable.api import Window
from enthought.chaco.api import ArrayPlotData, Plot

from PyQt4 import QtGui, QtCore


class Signals(QtCore.QObject):
    done_collecting = QtCore.pyqtSignal(np.ndarray, np.ndarray)

class PlotWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)

        x = np.linspace(0,2*np.pi,200)
        y = np.sin(x)
        plotdata = ArrayPlotData(x=x, y=y)
        plot = Plot(plotdata, padding=50, border_visible=True)
        plot.plot(('x', 'y'))

        window = Window(self,-1, component=plot)
        self.setCentralWidget(window.control)
        self.resize(500,500)

        self.pd = plotdata

    def update_display(self, x, y):
        print 'updating'
        self.pd.set_data('x', x)
        self.pd.set_data('y', y)


def run_collection(signal):
    # this is where I would start and stop my hardware,
    # but I will just call the read function myself here
    for i in range(1,10):
        every_n_collected(i, signal)
        time.sleep(0.5)

def every_n_collected(frequency, signal):
    # dummy data to take place of device read
    x = np.linspace(0,2*np.pi,200)
    y = np.sin(x*frequency)
    print 'emitting'
    signal.emit(x, y)
    QtGui.QApplication.processEvents()

def main():
    plt = PlotWindow()
    plt.show()
    QtGui.QApplication.processEvents()

    signals = Signals()
    signals.done_collecting.connect(plt.update_display)

    t = threading.Thread(target=run_collection, args=(signals.done_collecting,))
    t.start()
    t.join()
    QtGui.QApplication.processEvents()    

    # it works without threads though...
    # run_collection(signals.done_collecting)

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    main()

Solution

  • Your call to join on the mainthread (which is the UI thread) is blocking that thread and prevents the events to be processed by the UI. If you started the app/GUI event loop in the main function and wait for the app to be closed without calling t.join(), it should work fine.

    This is the way to do it with regular Traits/TraitsUI/Chaco apps.

    import time
    import threading
    
    import numpy as np
    
    from traits.etsconfig.etsconfig import ETSConfig
    ETSConfig.toolkit = "qt4"
    
    from enable.api import ComponentEditor
    from chaco.api import ArrayPlotData, Plot
    
    from traits.api import Event, HasTraits, Instance
    from traitsui.api import View, Item
    
    class PlotWindow(HasTraits):
    
        dataset = Instance(ArrayPlotData)
        plot = Instance(Plot)
    
        def _dataset_default(self):
            x = np.linspace(0,2*np.pi,200)
            y = np.sin(x)
            plotdata = ArrayPlotData(x=x, y=y)
            return plotdata
    
        def _plot_default(self):
            plot = Plot(self.dataset, padding=50, border_visible=True)
            plot.plot(('x', 'y'))
            return plot
    
        def update_display(self, x, y):
            print 'updating', threading.current_thread()
            self.dataset.set_data('x', x)
            self.dataset.set_data('y', y)
    
        traits_view = View(
            Item('plot', editor=ComponentEditor(size=(400, 400)), show_label=False)
        )
    
    def run_collection(datamodel):
        # this is where I would start and stop my hardware,
        # but I will just call the read function myself here
        for i in range(1,10):
            x = np.linspace(0,2*np.pi,200)
            y = np.sin(x*i)
            datamodel.update_display(x, y)
            time.sleep(0.5)
    
    def main():
        plot = PlotWindow()
    
        t = threading.Thread(target=run_collection, args=(plot,))
        t.start()
    
        # Starts the UI and the GUI mainloop
        plot.configure_traits()
    
        # don't call t.join() as it blocks the current thread...
    
    if __name__ == "__main__":
        main()