0

I'm having troubles using PyQt4 slots/signals.

I'm using PyLIRC and I'm listening for button presses on a remote. This part I have gotten to work outside of Qt. My problem comes when emitting the signal from the button listening thread and attempting to call a slot in the main thread.

My button listener is a QObject initialized like so:

buttonPressed = pyqtSignal(int)

def __init__(self):
    super(ButtonEvent, self).__init__()
    self.buttonPressed.connect(self.onButtonPressed)

def run(self):
    print 'running'
    while(self._isListening):
        s = pylirc.nextcode()
        if (s):
            print 'emitting'
            self.buttonPressed.emit(int(s[0]))

The onButtonPressed slot is internal to the button listener for testing purposes.

To move the button listener to another thread to do the work, I use the following:

event = ButtonEvent()
eventThread = QThread()
event.moveToThread(eventThread)
eventThread.started.connect(event.run)

Then in the main thread, I have my VideoTableController class that contains the slot in the main thread that doesn't get called. Inside of __init__ I have this:

class VideoTableController(QObject):
    def __init__(self, buttonEvent):
        buttonEvent.buttonPressed.connect(self.onButtonPressed)

Where onButtonPressed in this case is:

@pyqtSlot(int)
def onButtonPressed(self, bid):
    print 'handling button press'
    if bid not in listenButtons: return
    { ButtonEnum.KEY_LEFT : self.handleBack,
    #...

So when I start the event thread, it starts listening properly. When I press a button on the remote, the onButtonPressed slot internal to the ButtonEvent class is properly called, but the slot within VideoTableController, which resides in the main thread, is not called. I started my listening thread after connecting the slot to the signal, and I tested doing it the other way around, but to no avail.

I have looked around, but I haven't been able to find anything. I changed over to using QObject after reading You're doing it wrong. Any help with this is greatly appreciated. Let me know if you need anything else.

EDIT: Thanks for the responses! Here is a big chunk of code for you guys:

ButtonEvent (This class uses singleton pattern, excuse the poor coding because I'm somewhat new to this territory of Python also):

import pylirc
from PyQt4.QtCore import QObject, pyqtSignal, QThread, pyqtSlot
from PyQt4 import QtCore

class ButtonEvent(QObject):
    """
    A class used for firing button events
    """

    _instance = None
    _blocking = 0
    _isListening = False

    buttonPressed = pyqtSignal(int)

    def __new__(cls, configFileName="~/.lircrc", blocking=0, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(ButtonEvent, cls).__new__(cls, args, kwargs)
            cls._blocking = blocking
            if not pylirc.init("irexec", configFileName, blocking):
                raise RuntimeError("Problem initilizing PyLIRC")
            cls._isListening = True

        return cls._instance

    def __init__(self):
        """
        Creates an instance of the ButtonEvent class
        """
        super(ButtonEvent, self).__init__()
        self.buttonPressed.connect(self.button)
    ### init

    def run(self):
        print 'running'
        while(self._isListening):
            s = pylirc.nextcode()
            if (s):
                print 'emitting'
                self.buttonPressed.emit(int(s[0]))

    def stopListening(self):
        print 'stopping'
        self._isListening = False

    @pyqtSlot(int)
    def button(self, bid):
        print 'Got ' + str(bid)


def setupAndConnectButtonEvent(configFileName="~/.lircrc", blocking=0):
    """
    Initializes the ButtonEvent and puts it on a QThread.
    Returns the QThread it is running on.
    Does not start the thread
    """
    event = ButtonEvent().__new__(ButtonEvent, configFileName, blocking)
    eventThread = QThread()
    event.moveToThread(eventThread)
    eventThread.started.connect(event.run)
    return eventThread

Here is the VideoTableController:

from ControllerBase import ControllerBase
from ButtonEnum import ButtonEnum
from ButtonEvent import ButtonEvent
from PyQt4.QtCore import pyqtSlot
from PyQt4 import QtCore

class VideoTableController(ControllerBase):

    listenButtons = [ ButtonEnum.KEY_LEFT,   
                      ButtonEnum.KEY_UP,     
                      ButtonEnum.KEY_OK,     
                      ButtonEnum.KEY_RIGHT,  
                      ButtonEnum.KEY_DOWN,   
                      ButtonEnum.KEY_BACK ]

    def __init__(self, model, view, parent=None):
        super(VideoTableController, self).__init__(model, view, parent)
        self._currentRow = 0
        buttonEvent = ButtonEvent()
        buttonEvent.buttonPressed.connect(self.onButtonPressed)
        self.selectRow(self._currentRow)

    @pyqtSlot(int)
    def onButtonPressed(self, bid):
        print 'handling button press'
        if bid not in listenButtons: return
        { ButtonEnum.KEY_LEFT : self.handleBack,
          ButtonEnum.KEY_UP : self.handleUp,
          ButtonEnum.KEY_OK : self.handleOk,
          ButtonEnum.KEY_RIGHT : self.handleRight,
          ButtonEnum.KEY_DOWN : self.handleDown,
          ButtonEnum.KEY_BACK : self.handleBack,
        }.get(bid, None)()

And here is my startup script:

import sys
from PyQt4 import QtCore, QtGui
from ui_main import Ui_MainWindow
from VideoTableModel import VideoTableModel
from VideoTableController import VideoTableController
from ButtonEvent import *

class Main(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.buttonEvent = ButtonEvent()
        self.bEventThread = setupAndConnectButtonEvent()
        model = VideoTableModel("/home/user/Videos")
        self.ui.videoView.setModel(model)
        controller = VideoTableController(model, self.ui.videoView)
        self.bEventThread.start()

    def closeEvent(self, event):
        self.buttonEvent.stopListening()
        self.bEventThread.quit()
        event.accept()


if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    buttonEvent = ButtonEvent()
    myapp = Main()
    myapp.show()
    sys.exit(app.exec_())    
  • What is `ButtonEvent`? Also, please paste the complete code for the button listener, including the class ButtonListener(Foo): alike lines as well. Also, have you tried queued connection in here instead of the default auto here? buttonEvent.buttonPressed.connect(self.onButtonPressed) Also, do you have an event loop in the main thread listening? – László Papp Dec 31 '13 at 03:47
  • Can you provide a minimilistic example? (aka a complete working script). At a glance everything looks right, so I'm guessing something is blocking your main thread which is why only on eof your slots runs (the slot that is called in the thread runs, but the one in the main thread never does because the event is not being processed) – three_pineapples Dec 31 '13 at 03:51
  • Also, have you tried to call http://doc-snapshot.qt-project.org/qdoc/qcoreapplication.html#processEvents explicitly for a test? Does `event` have a parent before the thread move? – László Papp Dec 31 '13 at 03:53
  • @LaszloPapp I added the code for the 3 important classes. `ButtonEvent` is just basically a class that uses PyLIRC to listen for button presses and emits a signal when it gets one. From what I read, calling `app.exec_()` is good enough to start the event loop in the main thread. If this is where I went wrong it sounds like a simple fix. – Ryan Rossiter Dec 31 '13 at 05:50
  • @three_pineapples I added my Main.py file (along with a few other). It's not very simple but the sections are broken down pretty well. – Ryan Rossiter Dec 31 '13 at 05:52
  • possible duplicate of [PyQt: Connecting a signal to a slot to start a background operation](http://stackoverflow.com/questions/20752154/pyqt-connecting-a-signal-to-a-slot-to-start-a-background-operation) – László Papp Dec 31 '13 at 06:44
  • @user244929: Thanks for the extension. The situation is much clearer now since your issue seems to have been in the hidden code. Do you ever see `emitting` printed, or the run function is not called in your case? If not, does the answer in the other thread solve your issue? – László Papp Dec 31 '13 at 06:51
  • Taking another look, I think the proper way would be to solve your issue with relying on the QButton's signal rather than having your own. Is there any reason why you needed a run method, or at least an own signal at all? What is wrong with the builtin signal? If that is possible, this thread is not a duplicate though. :) – László Papp Dec 31 '13 at 06:59

3 Answers3

1

It turns out I was just making a foolish Python mistake. The signal was being emitted correctly, and the event loop was running properly in all threads. My problem was that in my Main.__init__ function I made a VideoTableController object, but I did not keep a copy in Main, so my controller did not persist, meaning the slot also left. When changing it to

self.controller = VideoTableController(model, self.ui.videoView)

Everything stayed around and the slots were called properly.

Moral of the story: it's not always a misuse of the library, it may be a misuse of the language.

0

It seems that the quickest workaround would be change your ButtonEvent code here:

...
def run(self):
    print 'running'
    while(self._isListening):
        s = pylirc.nextcode()
        if (s):
            print 'emitting'
            self.buttonPressed.emit(int(s[0]))
...

to this:

@pyqtSlot()
def run(self):
    print 'running'
    while(self._isListening):
        s = pylirc.nextcode()
        if (s):
            print 'emitting'
            self.buttonPressed.emit(int(s[0]))

The short explanation to this issue is that PyQt uses a proxy internally, and this way you can make sure to avoid that. After all, your method is supposed to be a slot based on the connect statement.

Right... Now, I would encourage you to give some consideration for your current software design though. It seems that you are using a class in a dedicated thread for handling Qt button events. It may be good idea, I am not sure, but I have not seen this before at least.

I think you could get rid of that class altogether in the future with a better approach where you connect from the push button signals directly to your handler slot. That would not be the run "slot" in your dedicated thread, however, but the cannonical handler.

It is not a good design practice to introduce more complexity, especially in multi-threaded applications, than needed. Hope this helps.

László Papp
  • 51,870
  • 39
  • 111
  • 135
  • I tried adding the `@pyqtSlot` decorator but to no avail. As for the "why" of this, I'm using this class as an abstraction from hardware. I have my IR remote that I am pressing buttons on, and I'm using this class to listen for presses on the remote (akin to listening for a keyboard). Not sure if this is the best method but I'm still in proof of concept stage right now. – Ryan Rossiter Dec 31 '13 at 15:25
  • @user244929: can you upload the whole project to somewhere? – László Papp Dec 31 '13 at 15:41
-1

I haven't actually tested this (because I don't have access to your compiled UI file), but I'm fairly certain I'm right.

Your run method of your ButtonEvent (which is supposed to be running in a thread) is likely running in the mainthread (you can test this by importing the python threading module and adding the line print threading.current_thread().name. To solve this, decorate your run method with @pyqtSlot()

If that doesn't solve it, add the above print statement to various places until you find something running in the main thread that shouldn't be. The lined SO answer below will likely contain the answer to fix it.

For more details, see this answer: https://stackoverflow.com/a/20818401/1994235

Community
  • 1
  • 1
three_pineapples
  • 11,579
  • 5
  • 38
  • 75
  • This feels like a comment. It is not a sure answer. Also, if it is a duplicate, you have enough reputation to mark so. Currently, you seem to be only reiterating the answer over there, in a nutshell. – László Papp Dec 31 '13 at 06:37
  • @LaszloPapp It was too long for a comment...maybe I should have just split it over 2 comments instead. – three_pineapples Dec 31 '13 at 06:41
  • IMO, you could just send the link in the comment with minor elaboration. I marked it as duplicate now. – László Papp Dec 31 '13 at 06:42
  • I tried the thread printing and the `run` function and my slot are on Dummy-1 and MainThread, respectively. I am still able to interact with my UI, so that means my main thread should not be locked up, correct? – Ryan Rossiter Dec 31 '13 at 15:29