简体   繁体   中英

Python PyQt4 Threading, sharing variables

I have been researching this topic for the past few days and have not found a suitable answer for my application. I am building a GUI in python 2.7 with PyQt4 to interact with a robot. I need one thread(main) to run the GUI created with QT Designer and another thread(worker) to continuously transmit data to and from the robot. I want to use data entered in the GUI(main thread) in the infinite worker thread. In all of the tutorials I have seen, data is only sent from the worker to the main thread; like the usual "loading bar" example.

This is what I have so far. This program imports the GUI output from QT Designer and prints values to the console when they are pressed. I also have an infinite thread set up that prints "hello" to the console which does work. Just to get me pointed in the right direction, can you show me how to change the infinite print "hello" to something else when the the button called "pushbutton" is clicked? I am familiar with C/C++ but this is my first Python project and any help is appreciated.

from PyQt4 import QtGui, QtTest, QtCore # Import the PyQt4 module we'll need
import sys # We need sys so that we can pass argv to QApplication

import gui # Import the GUI output from QT Designer


class GUI(QtGui.QMainWindow, gui.Ui_MainWindow):  # This class is to interact         with the GUI
def __init__(self):
    super(self.__class__, self).__init__()
    self.setupUi(self)  
    self.threadclass = ThreadClass()
    self.threadclass.start() # start thread class

    #########  Binds GUI widgets with functions  #####
    self.connectbutton.clicked.connect(self.pushbutton) # Call function "pushbutton" when pressed
    self.motor_up.clicked.connect(self.motorup) # Call function motorup when pressed
    self.motor_down.clicked.connect(self.motordown) # Call function motorup when pressed
    self.motor_speed_slider.valueChanged.connect(self.slider) # Call function slider
    self.auto_manual_check.stateChanged.connect(self.checkbox) # Call function checkbox
    self.Kp_box.valueChanged.connect(self.kp_entry)
    self.Ki_box.valueChanged.connect(self.ki_entry)
    self.Kd_box.valueChanged.connect(self.kd_entry)


    ######### Functions to run when GUI event is encountered

def pushbutton(self): # stuff to do if connect button is pressed                ## THIS BUTTON
    self.connectlabel.setText("Connected")
    QtTest.QTest.qWait(2000)        
    self.connectlabel.setText("Disconnected")
    self.emit(SIGNAL("text(Qstring)"),"it worked")

def slider(self): # Stuff to do when slider is changed
    print(self.motor_speed_slider.value()) # Print to monitor

def motorup(self): # Run if motor up is clicked
    print("motor up")

def motordown(self): # Run if motor down is clicked
    print("motor down")

def checkbox(self): # Run if checkbox is changed
    if self.auto_manual_check.isChecked():
        print("is checked")
    else:
        print("unchecked")

def kp_entry(self): # Run if kp is changed
    print(self.Kp_box.value())

def ki_entry(self): # Run if ki is changed
    print(self.Ki_box.value())

def kd_entry(self):# Run if kd is changed
    print(self.Kd_box.value())



class ThreadClass(QtCore.QThread): # Infinite thread to communicate with  robot
def __init__(self, parent = None):
    super(ThreadClass, self).__init__(parent)

def run(self):
    while 1:            # Runs continuously 
        print "hello"   #                           CHANGE THIS WHEN pushbutton IS PRESSED




def main():  # Function to run the main GUI
app = QtGui.QApplication(sys.argv)  # A new instance of QApplication
form = GUI()                 # We set the form to be our ExampleApp 
form.show()                         # Show the form
app.exec_()                         # and execute the app


if __name__ == '__main__':              # if we're running file directly and not importing it
main()                              # run the main function

EDIT: Thank you for the quick and detailed responses. After experimenting with your solutions I think I'm getting closer, however I'm not getting them to work for my application, I think I may have been unclear in my initial question. I am essentially trying to pass variables between threads, the actual printing was just to show that the variable was successfully transferred. I think this stripped down code will clear it up.

from PyQt4 import QtGui, QtTest, QtCore # Import the PyQt4 module we'll need
import sys # We need sys so that we can pass argv to QApplication

import gui # Import the GUI output from QT Designer


class GUI(QtGui.QMainWindow, gui.Ui_MainWindow):            # This class is to interact with the GUI
def __init__(self):
    super(self.__class__, self).__init__()
    self.setupUi(self)  
    self.threadclass = ThreadClass()
    self.threadclass.start() 
    self.Kp_box.valueChanged.connect(self.kp_entry)     # Call function kp_entry if box value is changed

def kp_entry(self):                                     # Run if kp is changed                  
    print(self.Kp_box.value())                          # I WANT TO SEND THE BOX VALUE TO THE WORKER THREAD        

class ThreadClass(QtCore.QThread):                          # Infinite thread to communicate with robot
def __init__(self, parent = None):
    super(ThreadClass, self).__init__(parent)

def run(self):
    while 1:             
        print self.Kp_box.value()                       #CONTINUOUSLY PRINT THE UPDATED BOX VALUE    


def main():                             # Run the main GUI
   app = QtGui.QApplication(sys.argv)  # A new instance of QApplication
   form = GUI()                        # We set the form to be our ExampleApp 
   form.show()                         # Show the form
   app.exec_()                         # and execute the app

if __name__ == '__main__':              # if we're running file directly and not importing it
main()                              # run the main function

All I want to do is enable the user to update a value in the GUI and then pass that value to the worker thread. In this case, I have an entry box so the user can change the number. I then need the program to transfer that number to the worker thread which will communicate with the robot. Right now the code does not work, I just got it as close as I could to better show what I'm trying to do.

If it helps, this is my GUI here

There are a couple of ways to do this. The first (simplest) one is to put a line of code in your method pushbutton :

def pushbutton(self):
    self.threadclass.got_a_click()

Add a corresponding method and instance variable to ThreadClass, and change the run method:

def __init__(self):
   super(ThreadClass, self).__init__(parent)
   self.flag = False

def got_a_click(self):
    self.flag = True

def run(self):
    while not self.flag:
        print "hello"
    do_something_useful()

This may be good enough. Note that the method got_a_click runs in the main loop context, and the run method is in the secondary QThread context.

A more sophisticated approach is to use Qt's ability to send Signals to Slots in other threads. In order to do that, you must run a QEventLoop in the secondary thread. The QThread base class is already set up for this: it has a run method that consists of just an event loop. So in order to have a QThread subclass that runs an event loop, simply don't override the run method. This QThread will just sit there doing nothing until a Signal gets sent to one of its Slots. When that happens the Slot will execute in the secondary thread. The code looks like this:

class ThreadClass(QtCore.QThread): 
    def __init__(self, parent = None):
        super(ThreadClass, self).__init__(parent)
        self.moveToThread(self)

    def onButtonPress(self):
        pass  # Your code here

Add this line to the constructor of GUI : self.connectbutton.clicked.connect(self.threadclass.onButtonPress)

Each time you click the button, onButtonPress runs in the secondary thread context.

Note the statement self.moveToThread(self) in the ThreadClass constructor. This is very important. It tells the Qt Signal/Slot mechanism that all the Slots in this object are to be invoked through the event loop in the ThreadClass thread. Without it, the Slot onButtonPress would run in the context of the main thread, even though its code is located in ThreadClass .

Another pitfall to watch out for is calling onButtonPress directly. That will not use the Signal/Slot mechanism, and onButtonPress will execute like any ordinary Python function. For example, if you called onButtonPress from your existing function pushbutton it would run in the main thread context. In order to get the threading behavior you desire, you must cause a Qt Signal to be emitted to the onButtonPress Slot.

Here is the answer to your re-phrased question. The following code fragment works as you intend:

from PySide import QtGui, QtTest, QtCore # Import the PySide module we'll need
import sys

class GUI(QtGui.QMainWindow):            # This class is to interact with the GUI
    def __init__(self):
        super(self.__class__, self).__init__()
        self.Kp_box = QtGui.QSpinBox(self)
        self.threadclass = ThreadClass(self)
        self.threadclass.start() 
        self.Kp_box.valueChanged.connect(self.kp_entry)     # Call function kp_entry if box value is changed

    def kp_entry(self):                                     # Run if kp is changed                  
        print("Main", self.Kp_box.value())                          # I WANT TO SEND THE BOX VALUE TO THE WORKER THREAD        

class ThreadClass(QtCore.QThread):                          # Infinite thread to communicate with robot
    def __init__(self, main_window):
        self.main_window = main_window
        super(ThreadClass, self).__init__(main_window)

    def run(self):
        while 1:             
            print(self.main_window.Kp_box.value())                       #CONTINUOUSLY PRINT THE UPDATED BOX VALUE    


def main():                             # Run the main GUI
    app = QtGui.QApplication(sys.argv)  # A new instance of QApplication
    form = GUI()                        # We set the form to be our ExampleApp 
    form.show()                         # Show the form
    app.exec_()                         # and execute the app

if __name__ == '__main__':              # if we're running file directly and not importing it
    main()                              # run the main function

What I changed from your code:

  1. I use PySide not PyQt but they're mostly compatible (or at least they're supposed to be). I had to change the first import statement; you will have to change it back.
  2. To remove the dependency on the GUI tool, I replaced your main window with one that has a single SpinBox. That demonstrates the essential features of your multithreading problem and decouples it from your other application requirements.
  3. Your secondary thread could not access the SpinBox because it has no variable that references it. I suspect that's the big problem you're having. It's fixed by passing the main window object to the ThreadClass constructor and storing it in an instance variable.

In no sense is anything being transferred from one thread to another. This code simply uses the fact that all the variables in a Python program live in the same memory space, even if the program is multi-threaded. [That wouldn't be true if the program used multiple processes .]

This little program doesn't provide any way of closing gracefully.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM