Вызовите виджет, содержащий QProgressBar между двумя QWizardPages - PullRequest
1 голос
/ 10 июля 2019

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

Теперь, на этапе создания виртуальной среды (это происходит при переключении с первой страницы на вторую), я включил виджет, который отображает индикатор выполнения, чтобы соединить несколько секунд, пока venv создает виртуальную среду. Это работает, но виджет показывает только черный контент, когда он появляется.

Я пытался исправить это с помощью потоков, а также с помощью многопроцессорной обработки (вызывая две функции одновременно), но это не сработало. Хотя виджет появляется, анимация не работает как обычно и уже на 100%, как только она видна. Также появляется после среда была создана.

Вот скриншот :

enter image description here


Вот части кода для воспроизведения :

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 с? Если нет, что может быть лучшим способом достижения этого?

1 Ответ

2 голосов
/ 10 июля 2019

В этом случае у меня есть 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)
...