PySide6 how to use `QMetaObject.connectSlotsByName(MainWindow)` when using also `QUiLoader().load(...)` - qt-designer

I have the following test code:
from os import path
from PySide6.QtCore import QObject, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication
class MyWin(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
def show(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
with its associated MainWindow.ui:
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<widget class="QTableView" name="tableView"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>19</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
... which works as expected.
Question is: how do I replace the line:
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
with an equivalent using QMetaObject.connectSlotsByName(???) ?
Problem here is PySide6 QUiLoader is incapable to add widgets as children of self (as PyQt6 uic.loadUi(filename, self) can do) and thus I'm forced to put UI in a separate variable (self.ui) while slots are defined in "parent" MyWin.
How can I circumvent limitation?
Reason why I ask is my real program has zillions of signals/slots and connect()'ing them manually is a real PITA (and error-prone)
UPDATE:
Following advice I modified MyWin to inherit from QWidget, but enabling self.ui.setParent(self) is enough to prevent display of UI.
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = QUiLoader().load(path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.pushButton.clicked.connect(self.on_pushButton_clicked)
self.ui.setParent(self)
# QMetaObject.connectSlotsByName(self)
def myshow(self):
self.ui.show()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.myshow()
app.exec()
I also see some strange errors:
mcon#ikea:~/projects/pyside6-test$ venv/bin/python t.py
qt.pysideplugin: Environment variable PYSIDE_DESIGNER_PLUGINS is not set, bailing out.
qt.pysideplugin: No instance of QPyDesignerCustomWidgetCollection was found.
Qt WebEngine seems to be initialized from a plugin. Please set Qt::AA_ShareOpenGLContexts using QCoreApplication::setAttribute and QSGRendererInterface::OpenGLRhi using QQuickWindow::setGraphicsApi before constructing QGuiApplication.
^C^C^C^C
Terminated
I need to kill process from another terminal, normal Ctrl-C is ignored.
UPDATE2:
I further updated code following #ekhumoro advice:
from os import path
from PySide6.QtCore import QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget, QMainWindow
class UiLoader(QUiLoader):
_baseinstance = None
def createWidget(self, classname, parent=None, name=''):
if parent is None and self._baseinstance is not None:
widget = self._baseinstance
else:
widget = super(UiLoader, self).createWidget(classname, parent, name)
if self._baseinstance is not None:
setattr(self._baseinstance, name, widget)
return widget
def loadUi(self, uifile, baseinstance=None):
self._baseinstance = baseinstance
widget = self.load(uifile)
QMetaObject.connectSlotsByName(widget)
return widget
class MyWin(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
UiLoader().loadUi(path.join(path.dirname(__file__), "MainWindow.ui"), self)
# self.pushButton.clicked.connect(self.on_pushButton_clicked)
QMetaObject.connectSlotsByName(self)
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication([])
win = MyWin()
win.show()
app.exec()
This doesn't work either: it shows GUI, but button click is not connected (unless I explicitly do it uncommenting the line).
What am I doing wrong?

To answer the question as stated in the title:
It's possible to fix the original example by setting the container-widget as the parent of the ui-widget. However, there are a few extra steps required. Firstly, the flags of the ui-widget must include Qt.Window, otherwise it will just become the child of an invisble window. Secondly, the close-event of the ui-widget must be reimplemented so that the application shuts down properly. And finally, the auto-connected slots must be decorated with QtCore.Slot.
Here's a fully working example:
from os import path
from PySide6.QtCore import Qt, QEvent, Slot, QMetaObject
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import QApplication, QWidget
class MyWin(QWidget):
def __init__(self):
super().__init__()
self.ui = QUiLoader().load(
path.join(path.dirname(__file__), "MainWindow.ui"))
self.ui.setParent(self, self.ui.windowFlags() | Qt.WindowType.Window)
self.ui.installEventFilter(self)
QMetaObject.connectSlotsByName(self)
def eventFilter(self, source, event):
if event.type() == QEvent.Type.Close and source is self.ui:
QApplication.instance().quit()
return super().eventFilter(source, event)
def myshow(self):
self.ui.show()
#Slot()
def on_pushButton_clicked(self):
print("button pushed!")
app = QApplication(['Test'])
win = MyWin()
win.myshow()
app.exec()
PS: see also my completely revised alternative solution using a loadUi-style approach that now works properly with both PySide2 and PySide6.

connectSlotsByName() is a static function, and it can only accept an argument (the "target" object), meaning that it can only operate with children of the object and its own functions.
The solution, then, is to make the top level widget a child of the "controller".
This cannot be directly done with setParent() in your case, though, since the QWidget override of setParent() expects a QWidget as argument, and your MyWin class is a simple QObject instead.
While the theoretical solution could be to call QObject.setParent(self.ui, self) to circumvent the override, this won't work and will sooner or later crash. As explained in this related post, the only available solution is to make the "controller" a QWidget subclass, even if you're not showing it.
Note that there is an alternative solution that should provide the same results as uic.loadUi using a QUiLoader subclass, as explained in this answer. It obviously misses the other PyQt parameters, but for general usage it shouldn't be a problem.
Finally, remember that you could always use the loadUiType function that works exactly like the PyQt one (again, without extra parameters); it's a slightly different pattern, since it generates the class names dynamically, but has the benefit that it parses the ui just once, instead of doing it every time a new instance is created.
With a custom function you can even create a class constructor with the path and return a type that also calls setupUi() on its own:
def UiClass(path):
formClass, widgetClass = loadUiType(path)
name = os.path.basename(path).replace('.', '_')
def __init__(self, parent=None):
widgetClass.__init__(self, parent)
formClass.__init__(self)
self.setupUi(self)
return type(name, (widgetClass, formClass), {'__init__': __init__})
class Win(UiClass('mainWindow.ui')):
def __init__(self):
super().__init__()
# no need to call setupUi()
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
test = Win()
test.show()
sys.exit(app.exec())
With the above, print(Win.__mro__) will show the following (this is from PyQt, PySide will obviously have the appropriate module name):
(<class '__main__.Win'>, <class '__main__.mainWindow_ui'>,
<class 'PyQt5.QtWidgets.QMainWindow'>, <class 'PyQt5.QtWidgets.QWidget'>,
<class 'PyQt5.QtCore.QObject'>, <class 'sip.wrapper'>,
<class 'PyQt5.QtGui.QPaintDevice'>, <class 'sip.simplewrapper'>,
<class 'Ui_MainWindow'>, <class 'object'>)
As noted in the comment by #ekhumoro, since PySide6 the behavior has changed, since uic is now able to directly output python code, so you need to ensure that the uic command of Qt is in the user PATH.
PS: note that, with PyQt, using connectSlotsByName() always calls the target function as many overrides as the signal has, which is the case of clicked signal of buttons; this is one of the few cases for which the #pyqtSlot decorator is required, so in your case you should decorate the function with #pyqtSlot(), since you are not interested in the checked argument. For PySide, instead, the #Slot is mandatory in order to make connectSlotsByName() work.
See this related answer.

Related

Replace built-in method of class instance. Python3.8. Pyside2

I want to replace the built-in method closeEvent of QMainWindow class instance that handles the form close event.
CODE #1
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication
from PySide2.QtCore import QFile, QIODevice
app = QApplication(sys.argv)
ui_file_name = "ui\Main.ui"
ui_file = QFile(ui_file_name)
if not ui_file.open(QIODevice.ReadOnly):
print("Cannot open {}: {}".format(ui_file_name, ui_file.errorString()))
sys.exit(-1)
loader = QUiLoader()
window = loader.load(ui_file)
ui_file.close()
if not window:
print(loader.errorString())
sys.exit(-1)
def MainFormCloseEvent(event):
print(event)
event.ignore()
print(window.closeEvent)
window.closeEvent=MainFormCloseEvent
print(window.closeEvent)
window.show()
sys.exit(app.exec_())
This code does not cause the MainFormCloseEvent function to be called when the form closes.
This code print the following information:
<built-in method closeEvent of PySide2.QtWidgets.QMainWindow object at 0x000000000573BF80>
<function MainFormCloseEvent at 0x0000000002C37430>
But this code works well
CODE #2
import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication
from PySide2.QtCore import QFile, QIODevice
from PySide2.QtWidgets import QMainWindow
def MainFormCloseEvent(event):
print(event)
event.ignore()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
def closeEvent(self, event):
print('Original class method')
app = QApplication(sys.argv)
window = MainWindow()
print(window.closeEvent)
window.closeEvent=MainFormCloseEvent
print(window.closeEvent)
window.show()
sys.exit(app.exec_())
This code print the following information:
<bound method MainWindow.closeEvent of <main.MainWindow(0x51522b0) at 0x0000000004E8AF40>>
<function MainFormCloseEvent at 0x0000000002C37430>
<PySide2.QtGui.QCloseEvent object at 0x0000000004E8F340>
I can't understand the fundamental difference between these codes. I replace the class instance method in the same way, but in the first case it does not work, but in the second it works.
I only noticed the difference that in the first code, the closeEvent method is built-in and in the second code, the closeEvent method is bound. But I did not find in Google what it means and how to make the first code work.
No, you're not replacing the method in the same way.
While creating a method as class attribute and overwriting an instance method at runtime usually has similar results, at the lower level it's not the same, which is extremely important when using complex modules like python binding to libraries written in other languages.
In general, attribute overwriting for methods should be done with extreme care only when it's safe to do it (and you do know what you're doing). It's also important to note that doing it for event handlers is risky, it makes debugging confusing and it also makes calling the default implementation more complex and awkward (you cannot call super()).
Unfortunately, PySide doesn't directly supports setting the UI on an existing widget instance, which is what you would do with PyQt and using a proper class, like in your second example), but there is a possible workaround, as explained in this related post.
class UiLoader(QtUiTools.QUiLoader):
_baseinstance = None
def createWidget(self, classname, parent=None, name=''):
if parent is None and self._baseinstance is not None:
widget = self._baseinstance
else:
widget = super().createWidget(classname, parent, name)
if self._baseinstance is not None:
setattr(self._baseinstance, name, widget)
return widget
def loadUi(self, uifile, baseinstance=None):
self._baseinstance = baseinstance
widget = self.load(uifile)
QtCore.QMetaObject.connectSlotsByName(baseinstance)
return widget
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
ui_file_name = "ui\Main.ui"
ui_file = QFile(ui_file_name)
ui_file.open(QIODevice.ReadOnly)
loader = UiLoader()
loader.loadUi(ui_file, self)
def closeEvent(self, event):
print('Original class method')
Still, you shouldn't overwrite the closeEvent with a basic function, instead you should probably opt for signals, further subclassing, or implement alternate ways to change the behavior (ie, using instance attributes).

Text of label in UI file can't be changed

I tried it.
from PySide2 import QtWidgets
from PySide2 import QtGui
from PySide2 import QtCore
from PySide2.QtUiTools import QUiLoader
from maya.app.general.mayaMixin import MayaQWidgetBaseMixin
import shiboken2 as shiboken
import os
UIFILEPATH = 'D:/MAYA/pyside_pick/ui/PicsTest5.ui'
class MainWindow(MayaQWidgetBaseMixin,QtWidgets.QMainWindow):
def __init__(self,parent=None):
super(MainWindow,self).__init__(parent)
self.UI = QUiLoader().load(UIFILEPATH)
self.setWindowTitle(self.UI.windowTitle())
self.setCentralWidget(self.UI)
#image
img = QtGui.QPixmap('D:/MAYA/pyside_pick/images/imgKohaku.png')
self.scene = QtWidgets.QGraphicsScene(self)
item = QtWidgets.QGraphicsPixmapItem(img)
self.scene.addItem(item)
self.UI.graphicsView_char_1.setScene(self.scene)
#filter
self._filter = Filter()
self.installEventFilter(self._filter)
self.UI.pSphere1.installEventFilter(self._filter)
#primary
self.UI.label.setStyleSheet("QLabel {color : white;}")
self.UI.label.setText("A")
def labelTest(self):
self.UI.label.setStyleSheet("QLabel {color : red;}")
self.UI.label.setText("B")
print('D')
return False
class Filter(QtCore.QObject):
def eventFilter(self, widget, event):
win = MainWindow()
if event.type() == QtCore.QEvent.MouseButtonPress:
print(widget.objectName())
cmds.select(widget.objectName())
win.labelTest()
return False
def main():
win = MainWindow()
win.show()
if __name__ == '__main__':
main()
I clicked the button that 'pSphere1', but
self.UI.label.setStyleSheet("QLabel {color : red;}") self.UI.label.setText("B")
were look like it's not working.
I can change it inside define with UI loaded, but can't I do setText from outside?
How can I change the label of an imported UI file?
I find this, but I really do not understand. I couldn't find any mention of them beyond this page.
Change comboBox values in Qt .ui file with PySide2
If you know, I also want you to tell me where to put them.
Your issue is within the eventFilter(), and specifically the first line:
win = MainWindow()
This will create a new main window instance, which clearly doesn't make sense, since you obviously want to interact with the existing one.
While you could add the instance as an argument in the filter constructor in order to get a reference to the instance and directly call the function, that wouldn't be very good from the OOP point of view, as objects should never directly access attributes of their "parents".
A better and more correct approach would be to use a custom signal instead, and connect it from the main window instance:
class Filter(QtCore.QObject):
testSignal = QtCore.Signal()
def eventFilter(self, widget, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
print(widget.objectName())
cmds.select(widget.objectName())
self.testSignal.emit()
return False
class MainWindow(MayaQWidgetBaseMixin, QtWidgets.QMainWindow):
def __init__(self, parent=None):
# ...
self._filter.testSignal.connect(self.labelTest)
Note that widgets could accept events and prevent propagation (for instance, buttons or graphics views that have selectable or movable items), so you might not receive the event in the filter in those cases.

QThread closes whenever QFileDialog Called After Migrating from PyQt5 to Pyside2

First of all, I'm currently migrating my source code from PyQt5 to PySide2 which requires me to change some of the syntaxes. As this site said that it only needs 3 things to do migrate from PyQt to Pyside2.
1.app.exec_. exec_ was used as exec is a Python2 keyword. Under Python3, PyQt5 allows the use of exec but not PySide2.
2.Under PyQt5 it’s QtCore.pyqtSignal and QtCore.pyqtSlot and under PySide2 it’s QtCore.Signal and QtCore.Slot .
3.loading Ui files.
But anyway later on when I tried to run my code it gave me this following error:
QThread: Destroyed while thread is still running
I had more than 2000 lines of code and I cannot determine which is the cause of this other than my last action which is trying to call QFileDialog which shouldn't be a problem (I've tested this with PyQt import and there's no problem and no warning at all). But in PySide2 it definitely might be the cause of it. I look up into this, he doesn't have the same problem as mine exactly. I'm not trying to call QFileDialog from different thread.
this is the minimal reproducible example of my working code in PyQt5:
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QWidget, QDialog
import random
class MyWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.path = os.path.abspath(os.path.dirname(sys.argv[0]))
self.button = QtWidgets.QPushButton("Open File")
self.labelFile = QtWidgets.QLabel("empty")
self.labelData = QtWidgets.QLabel("None")
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.button)
self.layout.addWidget(self.labelFile)
self.layout.addWidget(self.labelData)
self.setLayout(self.layout)
self.button.clicked.connect(self.open_file)
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_data_value)
timer.start(1000)
def open_file(self):
x = QFileDialog.getOpenFileName(self,"Pilih File CSV yang Ingin Diproses",self.path,"CSV Files (*.csv)")
self.labelFile.setText(x[0])
def update_data_value(self):
self.DataProcess = DataProcess()
self.DataProcess.progress.connect(self.update_data_label)
self.DataProcess.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.pyqtSignal(object)
def __init__(self):
QtCore.QThread.__init__(self)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
and this is the non-working one in PySide2 after renaming import accordingly to PySide2 also renaming 'pyqtsignal' to 'Signal'
import sys
import os
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QWidget, QDialog
import random
class MyWidget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.path = os.path.abspath(os.path.dirname(sys.argv[0]))
self.button = QtWidgets.QPushButton("Open File")
self.labelFile = QtWidgets.QLabel("empty")
self.labelData = QtWidgets.QLabel("None")
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.button)
self.layout.addWidget(self.labelFile)
self.layout.addWidget(self.labelData)
self.setLayout(self.layout)
self.button.clicked.connect(self.open_file)
timer = QtCore.QTimer(self)
timer.timeout.connect(self.update_data_value)
timer.start(1000)
def open_file(self):
x = QFileDialog.getOpenFileName(self,"Pilih File CSV yang Ingin Diproses",self.path,"CSV Files (*.csv)")
self.labelFile.setText(x[0])
def update_data_value(self):
self.DataProcess = DataProcess()
self.DataProcess.progress.connect(self.update_data_label)
self.DataProcess.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.Signal(object)
def __init__(self):
QtCore.QThread.__init__(self)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
so after creating this minimal example, I realized that PySide QFileDialog makes the QThread stop while PyQt QFileDialog doesn't freeze the main thread. Is there anything I could do to handle this in similar syntax architecture? (e.g not using "movetothread" or "QObject")
The problem is that you're overwriting self.DataProcess every time a new thread is created, which may cause the previous object to be garbage-collected by Python before Qt has a chance to delete it. This can result in a core-dump if Qt tries to delete an object which is no longer there. Problems of this kind are quite common in both PyQt and PySide, and are usually caused by not keeping proper references to dependant objects. The normal solution is to ensure that the affected objects are given a parent and, if necessary, explicitly delete them at an appropriate time.
Here is one way to fix your example:
class MyWidget(QtWidgets.QWidget):
...
def update_data_value(self):
# ensure the thread object has a parent
process = DataProcess(self)
process.progress.connect(self.update_data_label)
process.start()
def update_data_label(self,x):
self.labelData.setText(str(x[0]))
class DataProcess(QtCore.QThread):
progress = QtCore.Signal(object)
def __init__(self, parent):
# ensure the thread object has a parent
QtCore.QThread.__init__(self, parent)
def run(self):
x = random.randint(1,100)
self.progress.emit([str(x)+ " from thread"])
# explicitly schedule for deletion
self.deleteLater()
It's hard to say exactly why PySide behaves differently to PyQt in this particular case. It usually just comes down to low-level differences between the two implementations. Probably there are equivalent cases that affect PyQt but not PySide. However, if you manage object references and cleanup carefully, such differences can usually be eliminated.

How to avoid parent widget warning when using QFileDialog class?

I'm studying Python and PyQt5 in Microsoft Windows 7. My IDE is PyCharm 4.5 CE.
I am trying to make the file dialog to users can select the files or directories easily.
My code is...
# coding: utf-8
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.init_gui()
def init_gui(self):
file_names = QFileDialog.getOpenFileNames(self, "Select one or more files to open", "C:/Windows", "")
print(file_names)
self.setGeometry(100, 100, 500, 300)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec_())
This code works properly. But the only thing that annoying me is this.
There are many buttons in parent main window and one of the button shows file dialog.
What is the proper parent in this situation?
From PyQt5 documentation the method signature is:
QStringList getOpenFileNames (QWidget parent=None, QString caption=QString(), QString directory=QString(), QString filter=QString(), QString selectedFilter=None, Options options=0)
The parent must be an instance of QWidget or of some class that inherits from QWidget, and that's exactly what QMainWindow is (and this explains why everything works as expected).
Now, to understand why PyCharm displays a warning: if you look in the QFileDialog.py file, which is automatically generated by PyCharm from PyQt5\QtWidgets.pyd you will see that the method getOpenFileNames is not declared as staticmethod nor as classmethod:
def getOpenFileNames(self, QWidget_parent=None, str_caption='', str_directory='', str_filter='', str_initialFilter='', QFileDialog_Options_options=0): # real signature unknown; restored from __doc__
""" QFileDialog.getOpenFileNames(QWidget parent=None, str caption='', str directory='', str filter='', str initialFilter='', QFileDialog.Options options=0) -> (list-of-str, str) """
pass
so PyCharm expects (wrongly) the method to be called on an instance of QFileDialog, but here you have no instance of QFileDialog (as the method docstring suggests the real method signature is unknown), hence it expects the first argument of the method (self) to be an instance of QFileDialog and thus it throws a warning.
You can turn off such warning by disabling the inspection just for the desired statement:
# noinspection PyTypeChecker,PyCallByClass
file_names = QFileDialog.getOpenFileNames(self, "Select one or more files to open", "C:/Windows", "")
print(file_names)

How can Twisted call Qt back?

I am practicing PyQt+Twisted by writing a chat client. This requires getting the two event loops to play nice. I want to figure out how to do this without the Twisted QTReactor. The pattern I'm trying to achieve is
Qt runs normally, Twisted runs in another thread (say a QThread).
Qt functions/methods call Twisted functions with callFromThread.
We hook up the deferreds given to us by Twisted so that they cause emission of Qt signals. In this way the Twisted thread can call back the Qt thread.
I've actually gotten that part working. The heart of the solution is this function
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback
func is the Twisted function you want to invoke. It probably returns a
deferred. successSignal is the signal you want to emit once the asynchronous
call finishes. This signal will be emitted with the result of func as an
argument
"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
The problem is that I don't know what to do when I get "un-asked-for" data. For example, someone else might send out a chat message. I need to be able to detect the receipt of the data in the Twisted thread and somehow get that data into the Qt thread. This seems tricky because in this case I haven't provided the Twisted part of the program with a signal to use as a callback. How can I do that?
Below please find complete example code to run my almost working chat client and server. To use these programs first copy the server and client as .py files and the ui specification as a .ui file. Run the server and then run the client. Once the client window is up, click the "connect" button. You should see a message on the server indicating a new connection. Then try typing into the line edit and clicking the "send" button. You'll see that the data makes it to the server and back, but that it doesn't show up in the client's QTextEdit box. This is because I can't figure out how to get the data from the ChatProtocol to the Qt part of the program. How can we do this?
SERVER (asyncore)
import asyncore
import socket
import constants as C
HOST = 'localhost'
PORT = 12344
class ChatServer(asyncore.dispatcher):
"""Receive and forward chat messages
When a new connection is made we spawn a dispatcher for that
connection.
"""
ADDRESS_FAMILY = socket.AF_INET
SOCKET_TYPE = socket.SOCK_STREAM
def __init__(self, host, port):
self.map = {}
self.address = (host,port)
self.clients = []
asyncore.dispatcher.__init__(self, map=self.map)
def serve(self):
"""Bind to socket and start asynchronous loop"""
self.create_socket(self.ADDRESS_FAMILY, self.SOCKET_TYPE)
self.bind(self.address)
print("ChatServer bound to %s %s"%self.address)
self.listen(1)
asyncore.loop(map=self.map)
def writable(self):
return False
def readable(self):
return True
def newMessage(self, data, fromWho):
"""Put data in all clients' buffers"""
print("new data: %s"%data)
for client in self.clients:
client.buffer = client.buffer + data
def handle_accept(self):
"""Deal with newly accepted connection"""
(connSock, clientAddress) = self.accept()
print("New connection accepted from %s %s"%clientAddress)
self.clients.append(ChatHandler(connSock, self.map, self))
class ChatHandler(asyncore.dispatcher):
def __init__(self, sock, map, server):
self.server = server
self.buffer = ''
asyncore.dispatcher.__init__(self, sock, map)
def writable(self):
return len(self.buffer) > 0
def readable(self):
return True
def handle_read(self):
"""Notify server of any new incoming data"""
data = self.recv(4096)
if data:
self.server.newMessage(data, self)
def handle_write(self):
"""send some amount of buffer"""
sent = self.send(self.buffer)
self.buffer = self.buffer[sent:]
if __name__=='__main__':
if HOST is None:
HOST = raw_input('Host: ')
if PORT is None:
PORT = int(raw_input('Port: '))
s = ChatServer(HOST, PORT)
s.serve()
CLIENT (PyQt + Twisted)
import sys
import PyQt4.QtGui as QtGui
import PyQt4.QtCore as QtCore
import PyQt4.uic as uic
import twisted.internet.reactor as reactor
import twisted.internet.defer as defer
import twisted.internet.protocol as protocol
class MainWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = uic.loadUi('ui.ui')
self.networkThread = NetworkThread()
self.networkThread.start()
self.ui.sendButton.clicked.connect(self.sendMessage)
self.ui.connectButton.clicked.connect(self.getNetworkConnection)
#Make connections from network thread signals to our slots
self.connect(self.networkThread,
self.networkThread.sigConnected,
self.onConnected)
self.connect(self.networkThread,
self.networkThread.sigNewData,
self.onNewData)
self.ui.show()
def getNetworkConnection(self):
factory = protocol.ClientCreator(reactor, ChatProtocol)
self.networkThread.callFromMain(factory.connectTCP,
self.networkThread.sigConnected,
'localhost', 12344)
def onConnected(self, p):
print("Got a protocol!")
self.cxn = p
def onNewData(self, data):
self.ui.outputBox.append('\r\n'+data)
def sendMessage(self):
message = str(self.ui.inputBox.text())
self.networkThread.callFromMain(self.cxn.send, None, message)
class NetworkThread(QtCore.QThread):
"""Run the twisted reactor in its own thread"""
def __init__(self):
QtCore.QThread.__init__(self)
self.sigConnected = QtCore.SIGNAL("sigConnected")
self.sigNewData = QtCore.SIGNAL("sigNewData")
def run(self):
reactor.run(installSignalHandlers=0)
def callFromMain(self, func, successSignal, *args):
"""Call an async I/O function with a Qt signal as it's callback"""
def succeed(result):
self.emit(successSignal, result)
def wrapped():
d = defer.maybeDeferred(func, *args)
if successSignal is not None:
d.addCallback(succeed)
reactor.callFromThread(wrapped)
class ChatProtocol(protocol.Protocol):
def dataReceived(self, data):
print("Got data: %s"%data)
print("...but I don't know how to pass it to Qt :(")
def send(self, data):
self.transport.write(data)
class ChatFactory(protocol.ClientFactory):
protocol = ChatProtocol
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
UI file
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTextEdit" name="outputBox"/>
</item>
<item>
<widget class="QLineEdit" name="inputBox"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="sendButton">
<property name="text">
<string>send</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectButton">
<property name="text">
<string>connect</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

Resources