1

When I run the code in the in the doWork method, by clicking the button1, the progress bar works as expected.

However, when I pass the list to the doWork method from other methods (i.e. btn2, btn3), the progress bar just jumps to 100% after it starts.

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from selenium import webdriver

class SeleniumWorker(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    def doWork(self, lst=['http://www.somesite.com/',
        'http://www.somesite.com/page2',
        'http://www.somesite.com/page3']):
        progress = 0
        browser = webdriver.Firefox()
        links = lst
        for link in links:
            browser.get(link)
            progress += 100 / len(links)
            self.progressChanged.emit(progress)
        browser.close()

class Widget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        QtWidgets.QWidget.__init__(self, *args, **kwargs)
        lay = QtWidgets.QHBoxLayout(self)
        progressBar = QtWidgets.QProgressBar()
        progressBar.setRange(0, 100)
        button1 = QtWidgets.QPushButton("Start1")
        button2 = QtWidgets.QPushButton("Start2")
        button3 = QtWidgets.QPushButton("Start3")
        lay.addWidget(progressBar)
        lay.addWidget(button1)
        lay.addWidget(button2)
        lay.addWidget(button3)
        self.thread = QtCore.QThread()
        self.worker = SeleniumWorker()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.doWork)
        button1.clicked.connect(self.thread.start)
        button2.clicked.connect(self.btn2)
        button3.clicked.connect(self.btn3)
        self.worker.progressChanged.connect(progressBar.setValue)


    def btn2(self):
        self.lst2 = ['http://www.somesite.com/page4',
        'http://www.somesite.com/page5',
        'http://www.somesite.com/page6']
        self.worker.doWork(self.lst2)

    def btn3(self):
        self.lst3 = ['http://www.somesite.com/page7',
        'http://www.somesite.com/page8',
        'http://www.somesite.com/page9']
        self.worker.doWork(self.lst3)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())
Joe T. Boka
  • 6,554
  • 6
  • 29
  • 48
  • In what order do you click the buttons? Does a different order give different result (i.e. not clicking button1 first)? Have you tried delays and debug prints of `progress` from your worker thread? – handle Apr 04 '18 at 09:30
  • @handle The order doesn't make any difference, except that when I click on `button1` the second time it won't execute. But, I can click on `button2` and `button3` any number of times, in any order, I will always get the same results. – Joe T. Boka Apr 04 '18 at 09:38
  • I just installed pyqt5 and selenium plus geckodriver; progress bar stays at 100 percent. Might have to do with moving the object to the thread. I'll play with it a bit... – handle Apr 04 '18 at 09:43
  • To run the code in the thread you need to do so via `QThread.start()` – user3419537 Apr 04 '18 at 09:43
  • @JoeT.Boka In my answer I have placed an explanation of the operation of the QThread with a worker, check it. :D – eyllanesc Apr 04 '18 at 09:46
  • As the answer probably already states, it's not running as thread via the additional buttons and thus blocking the Qt event loop, causing the progress bar to jump straight to 100 only after the work is done (so the order actually does make a difference!). – handle Apr 04 '18 at 09:49

1 Answers1

3

It seems that you have not understood the logic of my previous solution, I will detail the procedure:

[1] self.thread = QtCore.QThread()
[2] self.worker = SeleniumWorker()
[3] self.worker.moveToThread(self.thread)
[4] self.worker.progressChanged.connect(progressBar.setValue, QtCore.Qt.QueuedConnection)
[5] self.thread.started.connect(self.worker.doWork)
  1. QThread is a thread handler, so an object of that class allows us to execute a task on a thread other than the main thread called the GUI thread.

  2. You are creating a SeleniumWorker object that has the doWork method, which is a blocking task that should not be executed in the GUI thread, and that will be achieved with the previous QThread.

  3. Since the function must be executed in another thread, the object that has that method must move to that other thread.

  4. The signal of the object that is in the other thread is connected to the QProgressBar that is in the GUI thread, this connection must be made with the flag QtCore.Qt.QueuedConnection.

  5. When the thread is started it will call the doWork function and since the self.worker object is in another thread then the function will also be executed in that other thread.


In your case in the code that is shown in the next part you are calling the doWork function when the thread has not yet started so it will be executed in the main thread.

def btn2(self):
    ...
    # main thread
    self.worker.doWork(self.lst2)

One way to pass the urls is through a setter method, and then start the thread. When the thread starts, it will call doWork, and when the doWork is executed, the progressChanged signal will be emited.

With all the above we obtain the following:

import sys

from PyQt5 import QtCore, QtGui, QtWidgets
from selenium import webdriver

class SeleniumWorker(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()
    def setUrls(self, urls=['http://www.somesite.com/',
    'http://www.somesite.com/page2',
    'http://www.somesite.com/page3']):
        self.urls = urls

    def doWork(self):
        self.started.emit()
        progress = 0
        self.progressChanged.emit(progress)
        browser = webdriver.Firefox()
        links = self.urls
        for link in links:
            browser.get(link)
            progress += 100 / len(links)
            self.progressChanged.emit(progress)
        browser.close()
        self.finished.emit()

class Widget(QtWidgets.QWidget):
    def __init__(self, *args, **kwargs):
        QtWidgets.QWidget.__init__(self, *args, **kwargs)
        lay = QtWidgets.QHBoxLayout(self)
        self.progressBar = QtWidgets.QProgressBar()
        self.progressBar.setRange(0, 100)
        button1 = QtWidgets.QPushButton("Start1")
        button2 = QtWidgets.QPushButton("Start2")
        button3 = QtWidgets.QPushButton("Start3")
        lay.addWidget(self.progressBar)
        lay.addWidget(button1)
        lay.addWidget(button2)
        lay.addWidget(button3)
        self.thread = QtCore.QThread()
        self.worker = SeleniumWorker()
        self.worker.moveToThread(self.thread)
        self.worker.progressChanged.connect(self.progressBar.setValue, QtCore.Qt.QueuedConnection)
        self.thread.started.connect(self.worker.doWork)
        button1.clicked.connect(self.btn1)
        button2.clicked.connect(self.btn2)
        button3.clicked.connect(self.btn3)
        self.worker.finished.connect(self.on_finished)
        self.worker.started.connect(lambda: self.buttons_setEnable(False))

    def on_finished(self):
        self.buttons_setEnable(True)
        if self.thread.isRunning():
            self.thread.quit()
            self.thread.wait()

    def buttons_setEnable(self, enable):
        for btn in self.findChildren(QtWidgets.QPushButton):
            btn.setEnabled(enable)

    def btn1(self):
        self.worker.setUrls()
        self.thread.start()

    def btn2(self):
        lst2 = ['http://www.somesite.com/page4',
        'http://www.somesite.com/page5',
        'http://www.somesite.com/page6']
        self.worker.setUrls(lst2)
        self.thread.start()

    def btn3(self):
        lst3 = ['http://www.somesite.com/page7',
        'http://www.somesite.com/page8',
        'http://www.somesite.com/page9']
        self.worker.setUrls(lst3)
        self.thread.start()


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

Note: I have added the functionality of disabling the buttons while it is running as it is appropriate that the thread does not start again while it is working.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks again. Yes, I definitely didn't fully understand the logic in your previous solution. I just ran the code you provided here. When I click on any of the buttons, it works for the first time. But, when I click any buttons the second time, nothing is executed. – Joe T. Boka Apr 04 '18 at 10:08
  • You mean you will update your code with the new implementation you mentioned? Because, your current code still has the same issue. I think it's not updated yet. – Joe T. Boka Apr 04 '18 at 10:19
  • I tested you last update 17 minutes ago. The program is crashing when I click any of the buttons. Is it working on your end? – Joe T. Boka Apr 04 '18 at 10:41
  • @JoeT.Boka Yes, I realized the problem, I just corrected it and it's tested, sorry for the inconveniences, I've been a little distracted. – eyllanesc Apr 04 '18 at 10:46
  • @JoeT.Boka This observing that there is another inconvenience, I am fixing it, when I finish it I notice – eyllanesc Apr 04 '18 at 10:48
  • @JoeT.Boka Now if you are 100% tested, please try it. :D – eyllanesc Apr 04 '18 at 11:23
  • YES! This one is working! Thanks so much for doing this. This is really a complex problem. Awesome solution. – Joe T. Boka Apr 04 '18 at 11:42