Всплывающий виджет, содержащий QProgressBar между двумя QWizardPages

Я работаю над графическим интерфейсом для создания и управления виртуальными средами для Python 3. Для этого я использую Python 3.7.4 и PyQt5. Я хотел бы, чтобы процесс создания виртуальной среды выполнялся мастером и с использованием метода create() модуля Python venv. Пока все работает так, как ожидалось. Виртуальная среда создается правильно, и мастер переключается на следующую страницу.

Теперь, когда создается виртуальная среда (это происходит при переключении с первой страницы на вторую), я включил виджет, который содержит индикатор выполнения, чтобы преодолеть несколько секунд, пока 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_())

Мои вопросы:

Это правильный способ показать полосу прогресса между двумя QWizardPages? Если нет, что может быть лучшим способом достижения этого?


person Joey    schedule 10.07.2019    source источник


Ответы (1)


В этом случае у меня есть 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)
  • Наблюдение за виджетом в черном заставляет меня предположить, что функция создания потребляет много времени, поэтому задача должна выполняться в другом потоке, но графический интерфейс не должен изменять его напрямую из другого потока, а использовать сигналы для передачи информации, для этого я реализую рабочий (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)
person eyllanesc    schedule 10.07.2019