在两个 QWizardPages 之间弹出一个包含 QProgressBar 的 Widget

Pop up a Widget containing a QProgressBar between two QWizardPages

我正在开发用于为 Python 3 创建和管理虚拟环境的 GUI。为此,我使用 Python 3.7.4 和 PyQt5。我希望虚拟环境的创建过程由向导完成,并使用 Python 的 venv 模块的 create() 方法。到目前为止,一切都按预期进行。虚拟环境创建成功,向导切换到下一页

现在,在创建虚拟环境的步骤(从第一页切换到第二页时会发生这种情况),我添加了一个小部件,其中包含一个进度条,用于桥接 venv创建虚拟环境。这有效,但小部件显示时仅显示黑色内容。

我尝试用线程和多处理(通过同时调用这两个函数)修复它,但这没有用。虽然小部件出现了,但动画并没有像往常一样 运行,而是一看到就已经是 100%。在创建环境后它也会出现。

这是截图:


以下是要重现的部分代码:

from subprocess import Popen, PIPE, CalledProcessError
from venv import create

from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtCore import (Qt, QRect, QSize, QMetaObject, QDir, QFile, QRegExp,
                          QBasicTimer)
from PyQt5.QtGui import (QIcon, QFont, QPixmap, QStandardItemModel,
                         QStandardItem)

from PyQt5.QtWidgets import (QMainWindow, QApplication, QAction, QHeaderView,
                             QFileDialog, QWidget, QGridLayout, QVBoxLayout,
                             QLabel, QPushButton, QSpacerItem, QSizePolicy,
                             QTableView, QAbstractItemView, QMenuBar, QMenu,
                             QStatusBar, QMessageBox, QWizard, QWizardPage,
                             QRadioButton, QCheckBox, QLineEdit, QGroupBox,
                             QComboBox, QToolButton, QProgressBar, QDialog,
                             QHBoxLayout)




#]===========================================================================[#
#] FIND INSTALLED INTERPRETERS [#============================================[#
#]===========================================================================[#

# look for installed Python versions in common locations
versions = ['3.9', '3.8', '3.7', '3.6', '3.5', '3.4', '3.3', '3']

notFound = []
versFound = []
pathFound = []

for i, v in enumerate(versions):
    try:
        # get installed python3 versions
        getVers = Popen(["python" + v, "-V"],
                            stdout=PIPE, universal_newlines=True)
        version = getVers.communicate()[0].strip()

        # get paths of the python executables
        getPath = Popen(["which", "python" + v],
                            stdout=PIPE, universal_newlines=True)
        path = getPath.communicate()[0].strip()

        versFound.append(version)
        pathFound.append(path)

    except (CalledProcessError, FileNotFoundError):
        notFound.append(i)


这是进度条:


#]===========================================================================[#
#] PROGRESS BAR [#===========================================================[#
#]===========================================================================[#

class ProgBarWidget(QWidget):
    def __init__(self):
        super().__init__()

        self.initMe()


    def initMe(self):
        # basic window settings
        self.setGeometry(600, 300, 300, 80)
        self.setFixedSize(325, 80)
        self.setWindowTitle("Creating")
        self.setWindowFlag(Qt.WindowCloseButtonHint, False)
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)

        horizontalLayout = QHBoxLayout(self)
        verticalLayout = QVBoxLayout()

        statusLabel = QLabel(self)
        statusLabel.setText("Creating virtual environment...")

        self.progressBar = QProgressBar(self)
        self.progressBar.setFixedSize(300, 23)

        self.timer = QBasicTimer()
        self.timer.start(0, self)
        self.i = 0

        verticalLayout.addWidget(statusLabel)
        verticalLayout.addWidget(self.progressBar)

        horizontalLayout.addLayout(verticalLayout)
        self.setLayout(horizontalLayout)


    def timerEvent(self, e):
        if self.i >= 100:
            self.timer.stop()
            #self.close()

        self.i += 1
        self.progressBar.setValue(self.i)


这是向导部分:


#]===========================================================================[#
#] VENV WIZARD [#============================================================[#
#]===========================================================================[#

class VenvWizard(QWizard):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Venv Wizard")
        self.resize(535, 430)
        self.move(578, 183)

        self.setStyleSheet(
            """
            QToolTip {
                background-color: rgb(47, 52, 63);
                border: rgb(47, 52, 63);
                color: rgb(210, 210, 210);
                padding: 2px;
                opacity: 325
            }
            """
        )

        self.addPage(BasicSettings())
        self.addPage(InstallPackages())
        self.addPage(Summary())


向导第一页:

class BasicSettings(QWizardPage):
    def __init__(self):
        super().__init__()

        folder_icon = QIcon.fromTheme("folder")

        self.setTitle("Basic Settings")
        self.setSubTitle("This wizard will help you to create and set up "
                         "a virtual environment for Python 3. ")



        interpreterLabel = QLabel("&Interpreter:")
        self.interprComboBox = QComboBox()
        interpreterLabel.setBuddy(self.interprComboBox)

        # add items from versFound to combobox
        self.interprComboBox.addItem("---")
        for i in range(len(versFound)):
            self.interprComboBox.addItem(versFound[i], pathFound[i])

        venvNameLabel = QLabel("Venv &name:")
        self.venvNameLineEdit = QLineEdit()
        venvNameLabel.setBuddy(self.venvNameLineEdit)

        venvLocationLabel = QLabel("&Location:")
        self.venvLocationLineEdit = QLineEdit()
        venvLocationLabel.setBuddy(self.venvLocationLineEdit)

        selectFolderToolButton = QToolButton()
        selectFolderToolButton.setFixedSize(26, 27)
        selectFolderToolButton.setIcon(folder_icon)
        selectFolderToolButton.setToolTip("Browse")

        # TODO: remove placeholder and add a spacer instead
        placeHolder = QLabel()


        # options groupbox
        groupBox = QGroupBox("Options")

        self.withPipCBox = QCheckBox("Install and update &Pip")
        self.sysSitePkgsCBox = QCheckBox(
            "&Make system (global) site-packages dir available to venv")
        self.launchVenvCBox = QCheckBox(
            "Launch a terminal with activated &venv after installation")
        self.symlinksCBox = QCheckBox(
            "Attempt to &symlink rather than copy files into venv")


        # events
        self.withPipCBox.toggled.connect(self.collectData)
        self.sysSitePkgsCBox.toggled.connect(self.collectData)
        self.launchVenvCBox.toggled.connect(self.collectData)
        self.venvNameLineEdit.textChanged.connect(self.collectData)
        self.venvLocationLineEdit.textChanged.connect(self.collectData)
        self.interprComboBox.currentIndexChanged.connect(self.collectData)
        self.symlinksCBox.toggled.connect(self.collectData)
        selectFolderToolButton.clicked.connect(self.selectDir)


        # store the collected values
        self.interprVers = QLineEdit()
        self.interprPath = QLineEdit()
        self.venvName = QLineEdit()
        self.venvLocation = QLineEdit()
        self.withPip = QLineEdit()
        self.sysSitePkgs = QLineEdit()
        self.launchVenv = QLineEdit()
        self.symlinks = QLineEdit()


        # register fields
        self.registerField("interprComboBox*", self.interprComboBox)
        self.registerField("venvNameLineEdit*", self.venvNameLineEdit)
        self.registerField("venvLocationLineEdit*", self.venvLocationLineEdit)

        self.registerField("interprVers", self.interprVers)
        self.registerField("interprPath", self.interprPath)
        self.registerField("venvName", self.venvName)
        self.registerField("venvLocation", self.venvLocation)
        self.registerField("withPip", self.withPip)
        self.registerField("sysSitePkgs", self.sysSitePkgs)
        self.registerField("launchVenv", self.launchVenv)
        self.registerField("symlinks", self.symlinks)


        # grid layout
        gridLayout = QGridLayout()
        gridLayout.addWidget(interpreterLabel, 0, 0, 1, 1)
        gridLayout.addWidget(self.interprComboBox, 0, 1, 1, 2)
        gridLayout.addWidget(venvNameLabel, 1, 0, 1, 1)
        gridLayout.addWidget(self.venvNameLineEdit, 1, 1, 1, 2)
        gridLayout.addWidget(venvLocationLabel, 2, 0, 1, 1)
        gridLayout.addWidget(self.venvLocationLineEdit, 2, 1, 1, 1)
        gridLayout.addWidget(selectFolderToolButton, 2, 2, 1, 1)
        gridLayout.addWidget(placeHolder, 3, 0, 1, 2)
        gridLayout.addWidget(groupBox, 4, 0, 1, 3)
        self.setLayout(gridLayout)


        # options groupbox
        groupBoxLayout = QVBoxLayout()
        groupBoxLayout.addWidget(self.withPipCBox)
        groupBoxLayout.addWidget(self.sysSitePkgsCBox)
        groupBoxLayout.addWidget(self.launchVenvCBox)
        groupBoxLayout.addWidget(self.symlinksCBox)
        groupBox.setLayout(groupBoxLayout)



    #]=======================================================================[#
    #] SELECTIONS [#=========================================================[#
    #]=======================================================================[#

    def selectDir(self):
        """
        Specify path where to create venv.
        """
        fileDiag = QFileDialog()

        folderName = fileDiag.getExistingDirectory()
        self.venvLocationLineEdit.setText(folderName)


    def collectData(self, i):
        """
        Collect all input data.
        """
        self.interprVers.setText(self.interprComboBox.currentText())
        self.interprPath.setText(self.interprComboBox.currentData())
        self.venvName.setText(self.venvNameLineEdit.text())
        self.venvLocation.setText(self.venvLocationLineEdit.text())

        # options
        self.withPip.setText(str(self.withPipCBox.isChecked()))
        self.sysSitePkgs.setText(str(self.sysSitePkgsCBox.isChecked()))
        self.launchVenv.setText(str(self.launchVenvCBox.isChecked()))
        self.symlinks.setText(str(self.symlinksCBox.isChecked()))


向导第二页:

class InstallPackages(QWizardPage):
    def __init__(self):
        super().__init__()

        self.setTitle("Install Packages")
        self.setSubTitle("Specify the packages which you want Pip to "
                         "install into the virtual environment.")

        # ...

        self.progressBar = ProgBarWidget()


    def initializePage(self):
        #interprVers = self.field("interprVers")
        interprPath = self.field("interprPath")
        self.venvName = self.field("venvName")
        self.venvLocation = self.field("venvLocation")
        self.withPip = self.field("withPip")
        self.sysSitePkgs = self.field("sysSitePkgs")
        #launchVenv = self.field("launchVenv")
        self.symlinks = self.field("symlinks")

        # overwrite with the selected interpreter
        sys.executable = interprPath

        # run the create process
        self.createProcess()

        # tried threading, but didn't really change the behaviour
        #Thread(target=self.progressBar.show).start()
        #Thread(target=self.createProcess).start()


    def createProcess(self):
        """
        Create the virtual environment.
        """
        print("Creating virtual environment...")  # print to console
        self.progressBar.show()  # the window containing the progress bar

        # the create method from Python's venv module
        create('/'.join([self.venvLocation, self.venvName]),
            system_site_packages=self.sysSitePkgs,
            symlinks=self.symlinks, with_pip=self.withPip)

        self.progressBar.close()  # close when done
        print("Done.")  # print to console when done


向导最后一页(与本例无关。):


class Summary(QWizardPage):
    def __init__(self):
        super().__init__()

        self.setTitle("Summary")
        self.setSubTitle("...............")

        # ...



if __name__ == "__main__":
    import sys

    app = QApplication(sys.argv)

    ui = VenvWizard()
    ui.show()

    sys.exit(app.exec_())

我的问题

这是在两个 QWizardPage 之间显示进度条的正确方法吗? 如果没有,有什么更好的方法可以实现这一目标?

在这种情况下,我有 2 个观察结果:

  • 检查您提供的代码,我看不出如何计算进度百分比,因此您应该使用 QProgressBar 来指示有工作 运行 因为它不使用 QBasicTimer 而只使用 setRange(0, 0)
class ProgBarWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.initMe()

    def initMe(self):
        # basic window settings
        self.setGeometry(600, 300, 300, 80)
        self.setFixedSize(325, 80)
        self.setWindowTitle("Creating")
        self.setWindowFlag(Qt.WindowCloseButtonHint, False)
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)

        horizontalLayout = QHBoxLayout(self)
        verticalLayout = QVBoxLayout()

        statusLabel = QLabel(self)
        statusLabel.setText("Creating virtual environment...")

        self.progressBar = QProgressBar(self)
        self.progressBar.setFixedSize(300, 23)
        self.progressBar.setRange(0, 0)

        verticalLayout.addWidget(statusLabel)
        verticalLayout.addWidget(self.progressBar)

        horizontalLayout.addLayout(verticalLayout)
        self.setLayout(horizontalLayout)
  • 观察一个黑色的小部件让我假设创建函数消耗了大量时间,因此任务必须在另一个线程中执行,但 GUI 不能直接从另一个线程修改它,而是使用信号来传输信息,为此我实现了一个 worker(QObject),它位于另一个线程中,它通知消耗大量时间的任务的开始和结束。
from functools import partial
from PyQt5.QtCore import QObject, QTimer, QThread, pyqtSignal, pyqtSlot

# ...

class InstallWorker(QObject):
    started = pyqtSignal()
    finished = pyqtSignal()

    @pyqtSlot(tuple)
    def install(self, args):
        self.started.emit()
        location, name, site_packages, symlinks, withPip = args
        create(
            "/".join([location, name]),
            system_site_packages=site_packages,
            symlinks=symlinks,
            with_pip=withPip,
        )
        self.finished.emit()

# ...

class InstallPackages(QWizardPage):
    def __init__(self):
        super().__init__()

        self.setTitle("Install Packages")
        self.setSubTitle("Specify the packages which you want Pip to "
                         "install into the virtual environment.")

        self.progressBar = ProgBarWidget()

        thread = QThread(self)
        thread.start()
        self.m_install_worker = InstallWorker()
        self.m_install_worker.moveToThread(thread)
        self.m_install_worker.started.connect(self.progressBar.show)
        self.m_install_worker.finished.connect(self.progressBar.close)

    def initializePage(self):
        # ...

        # run the create process
        self.createProcess()

    def createProcess(self):
        """
        Create the virtual environment.
        """
        args = (
            self.venvName,
            self.venvLocation,
            self.withPip,
            self.sysSitePkgs,
            self.symlinks,
        )
        wrapper = partial(self.m_install_worker.install, args)
        QTimer.singleShot(0, wrapper)