PyCharm / PyQt:如何在动态加载的UI文件中获得代码补全

10

假设我有一个在Qt Designer中创建的UI文件,我想要动态加载它来操纵小部件,例如:

example.py:

from PyQt5 import QtWidgets, uic

class MyWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        uic.loadUi('example.ui', self)

        # No code completion here for self.myPushButton:
        self.myPushButton.clicked.connect(self.handleButtonClick)

        self.show()

是否有一种标准/方便的方法可以在PyCharm(2017.1.4)中为通过这种方式加载的小部件启用代码补完?

目前,我正在使用以下内容(在加载UI文件后在构造函数中编写):

self.myPushButton = self.myPushButton  # type: QtWidgets.QPushButton
# Code completion for myPushButton works at this point

我也考虑过这个,但似乎行不通:

assert isinstance(self.myPushButton, QtWidgets.QPushButton)
# PyCharm does not even recognise myPushButton as an attribute of self at this point

最后,我还考虑使用Python存根文件,例如:

example.pyi:

class MyWidget(QtWidgets.QWidget):

    def __init__(self):
        self.myPushButton: QtWidgets.QPushButton = ... 

然而,在example.py之外的代码中可以正确识别myPushButton,但在example.py内部的代码中却不能,这与我所希望的相反。

我还考虑采用第一种方法,但将所有那些行都放在一个永远不会被调用的私有方法中,例如:

example.py:

from PyQt5 import QtWidgets, uic

class MyWidget(QtWidgets.QWidget):

    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)
        uic.loadUi('example.ui', self)

        # Code completion now works here for self.myPushButton:
        self.myPushButton.clicked.connect(self.handleButtonClick)

        self.show()

    def __my_private_method_never_called():
        self.myPushButton = self.myPushButton  # type: QtWidgets.QPushButton

        # Or even this (it should have the same effect if this
        # function is never called, plus it is less verbose):
        self.myPushButton = QtWidgets.QPushButton()

        # If I want to make sure that this is never called
        # could raise an error at some point:
        raise YouShouldNotHaveCalledThisError()

这个方法看起来很好用,而且也让我能够把所有的类型提示代码分组,与其他代码隔离开来。我甚至可以编写一些脚本通过解析UI文件来为我生成所有这些代码行。我只是在想如果读我的代码的人会不会认为这种方法很不正统,即使我清楚地注释了为什么要编写一个在技术上毫无用处的私有函数。


Pycharm无法识别.ui文件。 - eyllanesc
1个回答

7

如果有人感兴趣,我已经准备好了一个脚本,可以解析.ui文件并生成桩代码,可复制到我的类中:

ui_stub_generator.py:

from __future__ import print_function

import os
import sys
import xml.etree.ElementTree


def generate_stubs(file):
    root = xml.etree.ElementTree.parse(file).getroot()
    print('Stub for file: ' + os.path.basename(file))
    print()
    print('    def __stubs(self):')
    print('        """ This just enables code completion. It should never be called """')

    for widget in root.findall('.//widget'):
        name = widget.get('name')
        if len(name) > 3 and name[:2] == 'ui' and name[2].isupper():
            cls = widget.get('class')
            print('        self.{} = QtWidgets.{}()'.format(
                name, cls
            ))

    print('        raise AssertionError("This should never be called")')
    print()


def main():
    for file in sys.argv[1:]:
        generate_stubs(file)


if __name__ == '__main__':
    main()

这仅解析名称以“ui”开头,后跟大写字母的小部件,例如“uiMyWidget”,这是我在Qt Designer中通常遵循的命名约定。通过这样做,忽略了Qt Designer自动生成的具有名称的小部件(如果我关心这些,我会给它们一个适当的名称)。更新此操作以适应任何其他命名约定或其他类型的对象(例如操作)应该很简单。

为方便起见,我还将其设置为PyCharm中的外部工具;请参阅截图here (根据需要更改路径)。这样,我只需右键单击项目窗口中的ui文件,然后选择External Tools -> Stub Generator for Qt UI Files,并在运行窗口中获得以下输出,可以随时复制:

C:\ProgramData\Anaconda3\python.exe D:\MyProject\bin\ui_stub_generator.py D:\MyProject\my_ui_file.ui
Stub for file: my_ui_file.ui

    def __stubs(self):
        """ This just enables code completion. It should never be called """
        self.uiNameLabel = QtWidgets.QLabel()
        self.uiOpenButton = QtWidgets.QPushButton()
        self.uiSplitter = QtWidgets.QSplitter()
        self.uiMyCombo = QtWidgets.QComboBox()
        self.uiDeleteButton = QtWidgets.QPushButton()
        raise AssertionError("This should never be called")

Process finished with exit code 0

这是个好主意。我也遇到了ui文件无法被自动完成识别的问题。我没有想到过。你将脚本包含在外部工具中也很受欢迎。我会拿来用的。谢谢。 - Stephane Piriou
@StephanePiriou 给你更多的想法:除了在控制台中显示__stubs方法之外,我的脚本的最终版本还会检查是否有与其同名的Python文件/类(例如my_ui_file.py / MyUiFile),并直接替换该方法。然后,我使用.ui文件监视器自动执行脚本。我是在工作中做的,所以不能在这里展示它,但我会在某个时候编辑我的答案并提供一个简化版本。 - FernAndr
@FenAndr 那么存根代码将添加在哪里?单独的.pyi文件中吗?PyCharm似乎没有对其进行索引... - maicol07
@maicol07 在普通的 .py 文件中,在类本身的某个地方。除非最新的 PyCharm 版本在这方面发生了巨大变化,否则这个解决方案仍然有效。据我所知,.pyi 文件仅用于外部代码访问您的类,而不是在类本身内部使用,因此不能用于此目的。 - FernAndr
@FernAndr 我正在尝试使用PySide2,所以目前我只在主函数中编写没有类的代码。我应该将self变量更改为我的窗口变量吗? - maicol07
@maicol07 如果是这样的话,也许将 def __stubs(self): 替换为 if False:,并将所有 self 引用替换为你所说的 window 可能会起到作用。否则,我建议将你的窗口包装在一个自定义类中(类似于我原始问题中的 example.py 文件),并将存根方法放在那里。 - FernAndr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接