-1

I've been building a fairly simple program using PyQt5 for the GUI for the past few days and I have the following problem which I can't seem to find a solution for.

Here's an oversimplified version of my code (kept the absolute basics to keep things short):

def run_blueprint(file):
    # doing stuff to the file and returning the path to the output file
    return full_path


class Window(QMainWindow, Ui_MainWindow):
    # some variables here

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.connectSignalsSlots()

    def connectSignalsSlots(self):
        self.pushButton.clicked.connect(self.processFiles)

    def processFiles(self):
        self.toggleElements()
        for i, file in enumerate(self.selected_files_directories):
            temp_full_path = run_blueprint(file)
            self.listWidget_3.insertItem(i, temp_full_path)
        self.toggleElements()

    def toggleElements(self):
        self.pushButton_2.setEnabled(not self.pushButton_2.isEnabled())
        self.listWidget.setEnabled(not self.listWidget.isEnabled())


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = Window()
    win.show()
    sys.exit(app.exec())

Let's assume that I have selected an item from the listWidget and also have a list of file paths stored in the selected_files_directories[] variable.

If I run the code as is, when it's time for processFiles(self) to run I get some weird behavior. For some reason the for loop is ran first, then the first toggleElements() and after that the second toggleElements(). This results in the two elements that I want to temporarily disable until the for loop is over staying enabled the whole time.

However if I don't call the run_blueprint(file) method at all, but instead run any other code inside the for loop or even completely discard the loop, those two elements are disabled first, then the rest of the code runs and finally they are enabled again.

I believe the problem has something to do with me calling a static method outside the class. Do you have any ideas on how to solve this issue? Thanks in advance!


Edit: Below is the simplified version of the run_blueprint() function. The basic functionality of this program is as follows: I select a template file (.docx) and input file(s) (.pdf). Then using the docxtpl and pdfplumber libraries, for each of the selected input files I get specific text pieces from them and place them inside the template document and save that as a .docx file.

In the following code assume that template_path and bp_path show the path to the template file and blueprint file respectively.

def run_blueprint(file):
    doc = DocxTemplate(template_path) # docxtpl method
    global lns
    lns = export_data(file) # function I made that uses pdfplumber to get all the text from a pdf file

    global context
    context = {}

    with open(bp_path, 'r', encoding='utf8') as f:
        blueprint = f.read() # blueprint contains python code that populates the context variable. I do it this way to easily change between blueprints
    exec(blueprint, globals(), locals()) # I know it's not a good practice to use exec but it's intended for personal use

    doc.render(context) # docxtpl method
    output_path = "D:\\Desktop" # could be any path
    doc.save(output_path) # docxtpl method
    return output_path

The blueprint code usually contains API calls that take a few milliseconds to send a response. Apart from that it's mostly regex used to locate the text pieces I'm looking for.

Nikolas P.
  • 11
  • 1
  • 6
  • This might be an issue with the canvas being redrawn, rather than execution of the sctual code. Try [forcing a `repaint`](https://stackoverflow.com/questions/2052907/qt-repaint-redraw-update-do-something) after the `toggleElemnts() call and see if that helps. – bicarlsen Sep 12 '21 at 17:54
  • @bicarlsen Wow, it actually worked! I just added `self.repaint()` immediately after the first `self.toggleElements()` and it's now working like a charm. Thanks a lot! – Nikolas P. Sep 12 '21 at 18:03
  • Glad to hear :) Please accept the answer so other users know there has been a solution found. – bicarlsen Sep 12 '21 at 18:08
  • @NikolasP. is by any chance `run_blueprint` a function that takes some time to be completed? Knowing what it does would really help us to understand what is the *real* problem, which might be something that `repaint` doesn't really solve, as in many cases calling it is actually a workaround that just "hides" the actual problem. – musicamante Sep 12 '21 at 18:28
  • @musicamante Yes it does take some time to finish. Depends on the file it is processing. The file I'm doing the tests with takes around 2 seconds to be completed. – Nikolas P. Sep 12 '21 at 18:31
  • @NikolasP. (for future reference, knowing that from the beginning really helps understanding the problem, as it's a *very* important aspect). This is *not* an issue with the canvas. It's because the function is *blocking* and run in the main thread, which prevents the UI event loop to properly process events and update the widgets: the result is that not only the UI doesn't repaint buttons as disabled, but any mouse event on the disabled widgets will be queued and processed when `processFiles` finally returns. Are you completely sure that the function doesn't take more than a few seconds? – musicamante Sep 12 '21 at 18:37
  • I'm voting to reopen since the question is *not* a duplicate of the proposed one, and the issue can be answered and possibly solved with a better explanation than using comments. – musicamante Sep 12 '21 at 18:48
  • @musicamante Yes, you are right, I did notice some mouse events being queued up during one of the tests. The files I'm gonna be using with this program have very few pages so yeah, it won't be taking more than a few seconds for each file. However, is there a better way to do it? A better practice that I'm not aware of? – Nikolas P. Sep 12 '21 at 18:48
  • @NikolasP. it depends on the situation: if you're processing just one file, there are very basic solutions that ensure that the event queue is properly processed, but if the processing takes much more time, you *could* consider using threads, depending on how that processing is done and the level of control you have on the function. Is the computation done using external modules/apis? Is the network used to access/modify those files? As already said, knowing what that function *really* does would help a lot, so I suggest you to [edit] your question and explain that (if you can, with a [mre]). – musicamante Sep 12 '21 at 18:51
  • @musicamante I edited the original post to include an explanation about the `run_blueprint()` function. I tested it with 3 files and it did take around 5-6 seconds to complete. Meanwhile the mouse events did get queued up so using threads as you mentioned could actually be a good idea. – Nikolas P. Sep 12 '21 at 19:31
  • @NikolasP. as said, I've voted to reopen your question since the duplicate is not correct. Unfortunately, reopening is not so common, especially for questions with low rating or "niche" tags like pyqt. In the meantime, if you feel like trying to use threads, that's fine, only remember that to properly interact with UI elements you **must** use Qt signal/slot mechanism, as you *cannot* directly access UI elements from external threads. In order to properly do so (since you also need a return value) you need a QObject or QThread subclass, and create a custom signal that will notify the result. – musicamante Sep 12 '21 at 20:37
  • @musicamante Okay, thank for the tip. I'll try using threads and hopefully it'll work. – Nikolas P. Sep 13 '21 at 17:21

1 Answers1

0

This is an issue with the canvas being redrawn. Add a repaint call after the toggleElements call.

def processFiles(self):
    self.toggleElements()
    self.repaint()
    
    for i, file in enumerate(self.selected_files_directories):
        temp_full_path = run_blueprint(file)
        self.listWidget_3.insertItem(i, temp_full_path)
    
    self.toggleElements()

For more context based off the suggestions of the comments by @musicamante and to help clarify: There isn't an issue with the canvas being repainted. The issue is that the function blocks the main thread. A better solution, given the context, would be to offload the blocking task to a QThread or QProcess to unblock the main thread.

bicarlsen
  • 1,241
  • 10
  • 27
  • No, it's not an issue with the canvas, it's the function that doesn't return control to the event loop. So, your solution only (partially) solves the drawing problem, but not the event queue: clicking on the "disabled" widgets will still work, as those events will be queued and actually received *after* control has been returned to the UI thread, and at that point those widgets will be enabled again. – musicamante Sep 12 '21 at 18:40
  • @musicamante, great further explanation. Could you post a solution that solves both issues? – bicarlsen Sep 12 '21 at 18:43
  • I can't until the post is closed, I've voted to reopen it. – musicamante Sep 12 '21 at 18:45