• 5671阅读
  • 9回复

解决第4章“信号和槽”中的例子signals对PyQt5+python3兼容性的问题 [复制链接]

上一主题 下一主题
离线wps2000
 

只看楼主 倒序阅读 楼主  发表于: 2016-10-13
先上原来的代码:

import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"),
                     spinbox.setValue)
        self.connect(spinbox, SIGNAL("valueChanged(int)"),
                     dial.setValue)
        self.setWindowTitle("Signals and Slots")


class Form2(QDialog):

    def __init__(self, parent=None):
        super(Form2, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"),
                     spinbox, SLOT("setValue(int)"))
        self.connect(spinbox, SIGNAL("valueChanged(int)"),
                     dial, SLOT("setValue(int)"))
        self.setWindowTitle("Signals and Slots")


class ZeroSpinBox(QSpinBox):

    zeros = 0

    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        self.connect(self, SIGNAL("valueChanged(int)"), self.checkzero)


    def checkzero(self):
        if self.value() == 0:
            self.zeros += 1
            self.emit(SIGNAL("atzero"), self.zeros)


class Form3(QDialog):

    def __init__(self, parent=None):
        super(Form3, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        zerospinbox = ZeroSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(zerospinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"),
                     zerospinbox, SLOT("setValue(int)"))
        self.connect(zerospinbox, SIGNAL("valueChanged(int)"),
                     dial, SLOT("setValue(int)"))
        self.connect(zerospinbox, SIGNAL("atzero"), self.announce)
        self.setWindowTitle("Signals and Slots")


    def announce(self, zeros):
        print "ZeroSpinBox has been at zero %d times" % zeros


class Form4(QDialog):

    def __init__(self, parent=None):
        super(Form4, self).__init__(parent)

        lineedit = QLineEdit()

        layout = QHBoxLayout()
        layout.addWidget(lineedit)
        self.setLayout(layout)

        self.connect(lineedit, SIGNAL("textChanged(QString)"),
                     self.consoleEcho)
        self.setWindowTitle("Signals and Slots")


    def consoleEcho(self, text):
        print unicode(text)
        


class TaxRate(QObject):

    def __init__(self):
        super(TaxRate, self).__init__()
        self.__rate = 17.5


    def rate(self):
        return self.__rate


    def setRate(self, rate):
        if rate != self.__rate:
            self.__rate = rate
            self.emit(SIGNAL("rateChanged"), self.__rate)


def rateChanged(value):
    print "TaxRate changed to %.2f%%" % value


app = QApplication(sys.argv)
form = None
if len(sys.argv) == 1 or sys.argv[1] == "1":
    form = Form()
elif sys.argv[1] == "2":
    form = Form2()
elif sys.argv[1] == "3":
    form = Form3()
elif sys.argv[1] == "4":
    form = Form4()
if form is not None:
    form.show()
    app.exec_()
else: # if sys.argv[1] == "5"
    vat = TaxRate()
    vat.connect(vat, SIGNAL("rateChanged"), rateChanged)
    vat.setRate(17.5)    # No change will occur (new rate is the same)
    vat.setRate(8.5)     # A change will occur (new rate is different)
离线wps2000

只看该作者 1楼 发表于: 2016-10-13
这一部分的内容是讲“信号和槽”,在改写代码之前,先来吐一吐槽:作者在讲解这一部份的时候,只涉及到了signals.pyw的部份内容,一些细节语焉不详。仔细分析代码, 才发现里面有无数的大坑,因为PyQt5之后, 信号和槽这部份变化是比较大的。 我先把改好后的代码贴上:

==============================================================================
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

class Form(QDialog):

    def __init__(self, parent=None):
        super(Form, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        dial.valueChanged.connect(spinbox.setValue)
        spinbox.valueChanged.connect(dial.setValue)
        self.setWindowTitle("Signals and Slots")


class Form2(QDialog):

    def __init__(self, parent=None):
        super(Form2, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        spinbox = QSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(spinbox)
        self.setLayout(layout)

        dial.valueChanged.connect(spinbox.setValue)
        spinbox.valueChanged.connect(dial.setValue)
        self.setWindowTitle("Signals and Slots")



class ZeroSpinBox(QSpinBox):

    zeros = 0

    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        self.valueChanged.connect(self.checkzero)

    atzero = pyqtSignal(int)

    def checkzero(self):
        if self.value() == 0:#ZeroSpinBox是从QSpinBox继承而来,value()是QSpinBox的Public Functions之一。
            self.zeros += 1
            self.atzero.emit(self.zeros)



class Form3(QDialog):

    def __init__(self, parent=None):
        super(Form3, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        zerospinbox = ZeroSpinBox()


        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(zerospinbox)
        self.setLayout(layout)

        dial.valueChanged.connect(zerospinbox.setValue)
        zerospinbox.valueChanged.connect(dial.setValue)
        zerospinbox.atzero.connect(self.announce)
        self.setWindowTitle("Signals and Slots")

    @pyqtSlot(int)
    def announce(self, zeros):
        print("ZeroSpinBox has been at zero %d times" % zeros)



class Form4(QDialog):

    def __init__(self, parent=None):
        super(Form4, self).__init__(parent)

        lineedit = QLineEdit()

        layout = QHBoxLayout()
        layout.addWidget(lineedit)
        self.setLayout(layout)

        lineedit.textChanged.connect(self.consoleEcho)
        self.setWindowTitle("Signals and Slots")

    @pyqtSlot(str)
    def consoleEcho(self, text):
        print(text)



class TaxRate(QObject):

    def __init__(self):
        super(TaxRate, self).__init__()
        self.__rate = 17.5


    def rate(self):
        return self.__rate

    rateChanged = pyqtSignal(float)

    def setRate(self, rate):
        if rate != self.__rate:
            self.__rate = rate
            self.rateChanged.emit(self.__rate)


def rateChanged(value):
    print("TaxRate changed to %.2f%%" % value)


app = QApplication(sys.argv)
form = None
if len(sys.argv) == 1 or sys.argv[1] == "1":
    form = Form()
elif sys.argv[1] == "2":
    form = Form2()
elif sys.argv[1] == "3":
    form = Form3()
elif sys.argv[1] == "4":
    form = Form4()
if form is not None:
    form.show()
    app.exec_()
else: # if sys.argv[1] == "5"
    vat = TaxRate()
    vat.rateChanged.connect(rateChanged)
    vat.setRate(17.5)    # No change will occur (new rate is the same)
    vat.setRate(8.5)     # A change will occur (new rate is different)

离线wps2000

只看该作者 2楼 发表于: 2016-10-13
第一个坑,类Form。里面涉及到需要修改的,还是connet方法,原来的是这样:

=========================================
self.connect(dial, SIGNAL("valueChanged(int)"),
                     spinbox.setValue)
self.connect(spinbox, SIGNAL("valueChanged(int)"),
                     dial.setValue)
=========================================
在PyQt5,必须改成:
=========================================
dial.valueChanged.connect(spinbox.setValue)
spinbox.valueChanged.connect(dial.setValue)
=========================================
也就是说,建立各类的连接必须由该类的connect方法去实现。这样语法更为简洁。

第二个坑,类Form2。在“connet”这一部份,作者使用了QT槽,据说用SLOT()语法可能会更高效一些(中文版书P96):

=========================================
self.connect(dial, SIGNAL("valueChanged(int)"),
                     spinbox, SLOT("setValue(int)"))
self.connect(spinbox, SIGNAL("valueChanged(int)"),
                     dial, SLOT("setValue(int)"))
=========================================

查了一下官网,这种方法已经不支持了。 SIGNAL和SLOT已经作为装饰器使用了。总之,Form2还是得改成上面Form的样子:

=========================================
dial.valueChanged.connect(spinbox.setValue)
spinbox.valueChanged.connect(dial.setValue)
=========================================

第三个坑,类ZeroSpinBox和Form3。这两个类是相关联的。也是最大的一个坑。先上原来的代码:

=======================================================================
class ZeroSpinBox(QSpinBox):

    zeros = 0

    def __init__(self, parent=None):
        super(ZeroSpinBox, self).__init__(parent)
        self.connect(self, SIGNAL("valueChanged(int)"), self.checkzero)


    def checkzero(self):
        if self.value() == 0:
            self.zeros += 1
            self.emit(SIGNAL("atzero"), self.zeros)


class Form3(QDialog):

    def __init__(self, parent=None):
        super(Form3, self).__init__(parent)

        dial = QDial()
        dial.setNotchesVisible(True)
        zerospinbox = ZeroSpinBox()

        layout = QHBoxLayout()
        layout.addWidget(dial)
        layout.addWidget(zerospinbox)
        self.setLayout(layout)

        self.connect(dial, SIGNAL("valueChanged(int)"),
                     zerospinbox, SLOT("setValue(int)"))
        self.connect(zerospinbox, SIGNAL("valueChanged(int)"),
                     dial, SLOT("setValue(int)"))
        self.connect(zerospinbox, SIGNAL("atzero"), self.announce)
        self.setWindowTitle("Signals and Slots")


    def announce(self, zeros):
        print "ZeroSpinBox has been at zero %d times" % zeros

=======================================================================
在类ZeroSpinBox里面,有一个很让人费解的东西,就是SIGNAL("atzero"),简直就是凭空出来的,原书上说“它会发射自定义的atzero”信号,但在PYQT5中,这种自定义的信号必须先用 pyqtSignal初始化:

atzero = pyqtSignal(int)

然后才能发射出去。

self.atzero.emit(self.zeros),对比一下原来的方法: self.emit(SIGNAL("atzero"), self.zeros),和connect的使用方法很相似。

另外,在类Form3中的这几个语句:

self.connect(dial, SIGNAL("valueChanged(int)"),
                     zerospinbox, SLOT("setValue(int)"))
self.connect(zerospinbox, SIGNAL("valueChanged(int)"),
                     dial, SLOT("setValue(int)"))
self.connect(zerospinbox, SIGNAL("atzero"), self.announce)

要改成:

dial.valueChanged.connect(zerospinbox.setValue)
zerospinbox.valueChanged.connect(dial.setValue)
zerospinbox.atzero.connect(self.announce)

这下简洁多了。zerospinbox.atzero.connect(self.announce)这一句,实际上是将atzero与announce方法相连接。只要拔号盘归零一次,就将归零的次数显示出来。announce为作一个“槽”,是要加上装饰器的,所以才有了下面的改动:


@pyqtSlot(int)
def announce(self, zeros):
    print("ZeroSpinBox has been at zero %d times" % zeros)

第四个,有了前面的坑,这都不算坑了,类From4:

self.connect(lineedit, SIGNAL("textChanged(QString)"),
                     self.consoleEcho)

改成
lineedit.textChanged.connect(self.consoleEcho)

将consoleEcho方法,加上装饰器,作为槽使用:

@pyqtSlot(str)
def consoleEcho(self, text):
    print(text)

原来的unicode(text),要将unicode去掉,因为phtyon3.x默认是unicode


第五个坑,类TaxRate和外部函数rateChanged

================================================
class TaxRate(QObject):

    def __init__(self):
        super(TaxRate, self).__init__()
        self.__rate = 17.5


    def rate(self):
        return self.__rate


    def setRate(self, rate):
        if rate != self.__rate:
            self.__rate = rate
            self.emit(SIGNAL("rateChanged"), self.__rate)


def rateChanged(value):
    print "TaxRate changed to %.2f%%" % value

================================================

这里迷惑人的是,SIGNAL("rateChanged")里的rateChanged,不是那个外部函数, rateChanged(value),而是和前面的atzero一样,是一个信号。所以,事先也要是必须要初始化的:

rateChanged = pyqtSignal(float)

然后,setRate方法里的emit方法,要改成:self.rateChanged.emit(self.__rate)。用rateChanged这个信号将self.__rate发射出去,发射到 rateChanged(value)这个函数里。因此,最后的代码:

================================================
vat = TaxRate()
vat.connect(vat, SIGNAL("rateChanged"), rateChanged)
vat.setRate(17.5)    # No change will occur (new rate is the same)
vat.setRate(8.5)     # A change will occur (new rate is different)
================================================
vat对象的connet方法,要改成:vat.rateChanged.connect(rateChanged),vat后面跟的rateChanged是信号,connect后面跟的rateChanged,是那个外部函数 rateChanged(value)。

第六个坑:

书中的源代码文件是signals.pyw,必须另存为signals.py,这个程序在命令提示符中显示出输出的内容。为什么会这样?因为pyw文件是静默模式,所有向原有的 stdout 和 stderr 的输出都无效,所有从原有的 stdin 的读取都只会得到 EOF

最后,上个图,理解一下ZeroSpinBox和 Form3通过信号和槽的关联方式:




需要特别注意的是,在运行程序的时候,在命令行下边,要用完整的 python signals.py 参数  运行,单独运行signals.py,参数无法接收。目前我也没找到好的解决办法。
离线suandefujian

只看该作者 3楼 发表于: 2016-12-10
离线huskyzoom

只看该作者 4楼 发表于: 2017-01-25
很翔实。。。多谢
离线jxgzhelong

只看该作者 5楼 发表于: 2017-03-17
离线hooloongge

只看该作者 6楼 发表于: 2017-03-17
  所以我纠结了好久 还是决定长期坚持在py2.7 + pyqt4 的道路上。

只看该作者 7楼 发表于: 2017-03-17
楼主赶快更新啊
离线莫小漠

只看该作者 8楼 发表于: 2017-07-05
感谢楼主,像我这种基于py35+qt5的,看此书完全摸不着头脑。再次感谢。

看到楼主已经发第七章的东西了,
期待楼主的更新。
离线kite99

只看该作者 9楼 发表于: 2018-07-24
感谢楼主。学习pyqt,如果使用pyqt4,又觉得放弃新技术比较可惜;使用pyqt5,例子中n多的坑,无法逾越。幸好在书的后封面看到这个论坛,看到楼主的文章,可以继续学习下去。
快速回复
限100 字节
 
上一个 下一个