PyQt5:一个回调有效,另一个无效 - 为什么?

PyQt5: one callback works, the other doesn't - why?

我的研究包括:

PyQt reference to callable problem?

Python PyQt callback never runs - how to debug?

Passing extra arguments to PyQt slot

我正在构建一个 linux 'launcher' 程序,目前有两个回调。一个简单地启动点击的应用程序,另一个创建一个新的启动器。第一个工作正常 - 第二个非常棘手。我做了很多工作来解决这个问题。

  1. 运行 PyCharm 调试脚本并观察自己的值等以了解更多信息
  2. 将 NewLauncher 函数移动到 InitUI 方法中。
  3. 无休止地更改 "self"、"centralWidget" 和其他对象引用。
  4. 部分使用 functools。

我得到的错误是 "AttributeError: 'QWidget' object has no attribute 'newLauncher'"

这是代码:(如果太长,我深表歉意 - 最近有人建议我不要编辑太多)。

import sys, os
import subprocess
from functools import partial

from PyQt5.QtWidgets import QFileDialog, QToolButton, QHBoxLayout, QGridLayout, QSizePolicy, QSpacerItem, QWidget, QPushButton, QFormLayout, QLineEdit, QAction, QApplication, QDesktopWidget, QMainWindow, QTabWidget, QVBoxLayout

from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSize

from ruamel.yaml import YAML


yaml = YAML()
file_object = open("/home/tsc/PycharmProjects/launcher/Matrix.yaml", "r")
code = file_object.read()
matrix = yaml.load(code)
file_object.close()


class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.initUI()

    def launch(self, filepath):
        subprocess.run(filepath)


    def newLauncher(self):
        num_butts = len(matrix)
        btn_str = 'btn' + str(num_butts)

        file_object = open("/home/tsc/PycharmProjects/launcher/Matrix.yaml", "a")
        btn_str = 'btn' + str(num_butts + 1)
        file_object.write("\n" + btn_str + ":\n")

        self.setStyleSheet('padding: 3px; background: white');
        fname, _ = QFileDialog.getOpenFileName(self, "select an executable or document to launch:", "",
                                               "all files (*.*)")

        path = fname
        fname = os.path.basename(fname)

        file_object.write("  " + "name: " + str(fname) + "\n" + "  " + "path: " + str(path) + "\n")

        self.setStyleSheet('padding: 3px; background: white');
        icon, _ = QFileDialog.getOpenFileName(self, "select an image file for the icon:", "",
                                              "all files (*.*)")

        file_object.write("  " + "icon: " + str(icon) + "\n")
        file_object.close()


    def initUI(self):
        super(App, self).__init__()

        centralWidget = QWidget()
        tabWidget = QTabWidget()

        lay = QVBoxLayout(centralWidget)

        for i in range(3):
            page = QWidget()
            pagelay = QGridLayout(page)
            bmatrix = {}

            for btn in matrix:
                name = matrix[btn]['name']
                filepath = matrix[btn]['path']
                icon = matrix[btn]['icon']
                bmatrix[btn] = QToolButton(page)
                bmatrix[btn].setIcon(QIcon(icon))
                bmatrix[btn].setIconSize(QSize(64, 64))
                bmatrix[btn].resize(100, 100)
                bmatrix[btn].clicked.connect(lambda checked, arg=filepath: self.launch(arg))

                pagelay.addWidget(bmatrix[btn])

            tabWidget.addTab(page, 'tab{}'.format(i))

        mainMenu = self.menuBar()
        fileMenu = mainMenu.addMenu('File')
        mainMenu.addMenu(fileMenu)
        newAction = QAction('&New', centralWidget)

        #1 newAction.triggered.connect(lambda checked, arg=matrix: centralWidget.newLauncher(arg)) - shows window.
        #2 newAction.triggered.connect(partial(self.NewLauncher, self)) - shows nothing, App has no NewLauncher

        fileMenu.addAction(newAction)
        editMenu = mainMenu.addMenu('Edit')

        lay.addWidget(mainMenu)
        lay.addWidget(tabWidget)

        centralWidget.setGeometry(100, 100, 1080, 630)
        centralWidget.setWindowTitle('LaunchMaster')
        qtRectangle = centralWidget.frameGeometry()
        centerPoint = QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        centralWidget.move(qtRectangle.topLeft())

        centralWidget.show()


if __name__ == '__main__':

    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())

这里是 yaml 配置文件。如果你想测试它,你需要自定义路径等。该界面有一个 menuBar 和一个包含页面的 tabWidget,这些页面本身包含启动器按钮。

Matrix.yaml: 用空格代替下划线(缩进为 2 个字符)。我还不确定这种标记语法,抱歉给您带来麻烦。

btn1:  
  name: firefox  
  path: firefox-esr  
  icon: /home/tsc/PycharmProjects/launcher/icons/firefox.jpeg  

btn2:  
  name: thunderbird  
  path: /home/tsc/thunderbird/thunderbird  
  icon: /home/tsc/PycharmProjects/launcher/icons/thunderbird.jpeg  

你问的那行(我想)是这个 commented-out 代码:

newAction.triggered.connect(partial(self.NewLauncher, self))

评论说"shows nothing, App has no NewLauncher"。

如果是这样,这里有两个问题。第一个是一个简单的打字错误——你写的是 NewLauncher 而不是 newLauncher——我假设你在实际测试时已经修复了这个错误。第二个有点深,你可能会遇到问题。

self.newLauncher 是绑定方法。也就是说,它知道它的意思是 self,当您调用它时,self 将作为第一个参数传入。如果您随后编写 partial(self.newLauncher, self),当它被调用时,它会做与 self.newLauncher(self) 相同的事情——也就是说,它会传入 两个 副本 self作为单独的参数。

打字错误会很明显地失败,在 connect 调用时出现 AttributeError。但是额外的 self 只会失败,在 button-clicking 信号内带有 TypeError。我相信,这意味着 PyQt 会向 stderr 写一些警告(你可能没有看到 - 特别是如果你在 Windows 上,甚至没有 command-line window 附加)并且不为点击做任何事情。

您可能只想这样做:

newAction.triggered.connect(self.newLauncher)

偶尔,您希望将 unbound 方法从 class 对象 (App.newLauncher) 传递给实例 partial :

newAction.triggered.connect(partial(App.newLauncher, self))

…但在大多数情况下,包括这种情况,这只是一种可读性较差(且较慢)的方式来执行与传递绑定方法相同的事情。

  • 如果您不需要传递某些参数,则没有必要使用 lambda 函数,因此您有一个常规连接。

  • 另一方面你不应该调用centralWidget.show(),但是要显示,你还必须用setCentralWidget设置centralWidget。

  • 还有一点就是一定要验证用户是否选择了路径

  • 您的代码的另一个改进是使用 QProcess.startDetached() 而不是 subprocess.run(),因为它是阻塞的。


import sys
import os

from PyQt5.QtWidgets import QFileDialog, QToolButton, QHBoxLayout, QGridLayout, QSizePolicy, QSpacerItem, QWidget, QPushButton, QFormLayout, QLineEdit, QAction, QApplication, QDesktopWidget, QMainWindow, QTabWidget, QVBoxLayout

from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QSize, QProcess

from ruamel.yaml import YAML

yaml_filename = "/home/tsc/PycharmProjects/launcher/Matrix.yaml" 


yaml = YAML()
file_object = open(yaml_filename, "r")
code = file_object.read()
matrix = yaml.load(code)
file_object.close()


class App(QMainWindow):
    def __init__(self):
        super(App, self).__init__()

        self.initUI()

    def launch(self, filepath):
        QProcess.startDetached(filepath)

    def newLauncher(self):
        fname, _ = QFileDialog.getOpenFileName(self, "select an executable or document to launch:", "",
                                               "all files (*.*)")
        if fname == "":
            return

        icon, _ = QFileDialog.getOpenFileName(self, "select an image file for the icon:", "",
                                              "all files (*.*)")
        if icon == "":
            return

        num_butts = len(matrix)
        btn_str = 'btn' + str(num_butts)
        file_object = open(yaml_filename, "a")
        btn_str = 'btn' + str(num_butts + 1)
        file_object.write("\n" + btn_str + ":\n")

        path = fname
        fname = os.path.basename(fname)
        file_object.write("  " + "name: " + str(fname) + "\n" + "  " + "path: " + str(path) + "\n")
        file_object.write("  " + "icon: " + str(icon) + "\n")
        file_object.close()


    def initUI(self):
        super(App, self).__init__()

        centralWidget = QWidget()
        tabWidget = QTabWidget()

        lay = QVBoxLayout(centralWidget)

        for i in range(3):
            page = QWidget()
            pagelay = QGridLayout(page)
            bmatrix = {}

            for btn in matrix:
                name = matrix[btn]['name']
                filepath = matrix[btn]['path']
                icon = matrix[btn]['icon']
                bmatrix[btn] = QToolButton(page)
                bmatrix[btn].setIcon(QIcon(icon))
                bmatrix[btn].setIconSize(QSize(64, 64))
                bmatrix[btn].resize(100, 100)
                bmatrix[btn].clicked.connect(lambda checked, arg=filepath: self.launch(arg))

                pagelay.addWidget(bmatrix[btn])

            tabWidget.addTab(page, 'tab{}'.format(i))

        mainMenu = self.menuBar()
        fileMenu = mainMenu.addMenu('File')
        mainMenu.addMenu(fileMenu)
        newAction = QAction('&New', centralWidget)
        newAction.triggered.connect(self.newLauncher)

        fileMenu.addAction(newAction)
        editMenu = mainMenu.addMenu('Edit')

        lay.addWidget(mainMenu)
        lay.addWidget(tabWidget)

        self.setGeometry(100, 100, 1080, 630)
        self.setWindowTitle('LaunchMaster')
        qtRectangle = self.frameGeometry()
        centerPoint = QDesktopWidget().availableGeometry().center()
        qtRectangle.moveCenter(centerPoint)
        self.move(qtRectangle.topLeft())
        self.setCentralWidget(centralWidget)

        self.show()


if __name__ == '__main__':

    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())