Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. PySide2 thread issue in the search engine suggestions I implemented
Forum Updated to NodeBB v4.3 + New Features

PySide2 thread issue in the search engine suggestions I implemented

Scheduled Pinned Locked Moved Unsolved Qt for Python
8 Posts 3 Posters 454 Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • M Offline
    M Offline
    makalidap
    wrote on last edited by
    #1

    I am developing a program that downloads comics from a website. The program has a search engine that allows users to search. Each time a letter is typed, a GET request is sent to the site, and the names and covers of the first two comics in the search results are retrieved. This part works smoothly.

    However, since adding the comic covers to the interface takes a long time, I wanted to perform this operation on a separate thread. But there is a conflict between threads, and the program crashes when typing too quickly in the search box.

    I tried keeping active threads in a dictionary and ensuring that if a new thread starts while an active thread exists, the active thread would be terminated before starting the new thread, but it didn't work.

    from sys import exit, argv
    from os import listdir, path
    from importlib import util
    from mylib.PySide2.helpers import clear_layout
    from PySide2.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, QGraphicsDropShadowEffect, QFrame
    from PySide2.QtGui import QPixmap, QPainter, QColor, QFont, QPalette
    from PySide2.QtCore import Qt, QByteArray, QThread, Signal, QTimer
    
    import requests
    from urllib.parse import urlencode
    
    
    def get_response(query: str, per_page: int) -> dict:
        response = requests.get(f"https://mangagalaxy.org/api/query?searchTerm={query}&perPage={per_page}")
        return response.json()
    
    
    def get_datas(json: dict) -> list[tuple[str, str]]:
        data = []
        for i in json["posts"]:
            data.append((
                i["postTitle"],
                convert_url(i["featuredImage"])
            ))
        return data
    
    
    def convert_url(original_url):
        new_params = {
            'url': original_url,
            'w': 128,
            'q': 30
        }
    
        encoded_params = urlencode(new_params)
    
        new_url = f"https://mangagalaxy.org/_next/image?{encoded_params}"
    
        return new_url
    
    
    def search(query: str, per_page: int) -> list[tuple[str, str]]:
        return get_datas(get_response(query=query, per_page=per_page))
    
    
    class ImageLoad(QThread):
        success = Signal(QPixmap)
    
        def __init__(self, parent, url, label):
            super().__init__(parent)
            self.url = url
            self.label = label
    
        def run(self):
            try:
                image_data = requests.get(self.url).content
                if image_data:
                    pixmap = QPixmap(128, 180)
                    data = QByteArray(image_data)
                    if pixmap.loadFromData(data):
                        self.success.emit(pixmap)
            except Exception as e:
                print(e)
    
    
    class ResultWidget(QWidget):
        def __init__(self, parent=None, title=None, image=None, no=None):
            super().__init__(parent)
    
            self.no = no
            self.parent = parent
            layout = QHBoxLayout()
    
            self.photo_label = QLabel()
            self.load_photo(image)
            layout.addWidget(self.photo_label)
    
            title_label = QLabel(title)
            layout.addWidget(title_label)
    
            self.setLayout(layout)
    
        def load_photo(self, url):
            self.photo_label.setPixmap(loading_pixmap)
            self.photo_label.setFixedSize(loading_pixmap.size())
            def finished():
                self.parent.active_threads[self.no] = None
    
            self.worker = ImageLoad(self, url, self.photo_label)
            self.worker.success.connect(lambda pixmap: self.photo_label.setPixmap(pixmap))
            self.parent.active_threads[self.no] = self.worker
            self.worker.start()
            self.worker.finished.connect(finished)
    
    
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.active_threads = {}
            self.init_ui()
    
        def init_ui(self):
            main_layout = QVBoxLayout()
            # ########################################### Main layout
            self.search_layout = QVBoxLayout()
            # ############################ Search layout
    
            self.search_box = QLineEdit()
            self.search_box.setPlaceholderText("Search")
            self.search_box.textChanged.connect(self.search)
            self.search_layout.addWidget(self.search_box)
    
    
            self.results_layout = QVBoxLayout()
            self.results_layout.setContentsMargins(0, 0, 0, 0)
            # ############ Result layout
    
            self.search_layout.addLayout(self.results_layout)
            # ############ Result layout end
    
    
            main_layout.addLayout(self.search_layout)
    
            # ########################################### Main layout end
            self.main_widget = QWidget()
            self.main_widget.setLayout(main_layout)
    
            self.setCentralWidget(self.main_widget)
    
    
        def search(self, event):
            result = search(event, 2)
            self.set_results(result)
    
        def set_results(self, result):
            clear_layout(self.results_layout)
            for no, i in enumerate(result):
                def a():
                    r = ResultWidget(self, i[0], i[1], no)
                    self.results_layout.addWidget(r)
                if no in self.active_threads and self.active_threads[no]:
                    print("Stopping")
                    self.active_threads[no].terminate()
                    print("Stopped")
                    self.active_threads[no] = None
                a()
    
    
    if __name__ == '__main__':
        app = QApplication(argv)
    
        loading_pixmap = QPixmap("image.png")
    
        window = MainWindow()
        window.show()
        exit(app.exec_())
    

    I even made such an addition, but it still didn't work.

    def set_results(self, result):
        clear_layout(self.results_layout)
        for no, i in enumerate(result):
            def a():
                r = ResultWidget(self, i[0], i[1], no)
                self.results_layout.addWidget(r)
            if no in self.active_threads and self.active_threads[no]:
                print("Stopping")
                self.active_threads[no].terminate()
                print("Stopped")
                self.active_threads[no] = None
                timer = QTimer()
                timer.setInterval(1000)
                timer.timeout.connect(a)
                timer.start()
    
            else:
                a()
    

    In the end, I reluctantly tried something like this, but even with this, there are occasional issues. What method should I use to resolve the problem?

    self.search_box.textChanged.connect(self.on_text_changed)
    
    self.timer = QTimer(self)
    self.timer.setSingleShot(True)
    self.timer.timeout.connect(self.search)
    
    def on_text_changed(self):
        self.timer.start(1000)
    
    jsulmJ 1 Reply Last reply
    0
    • M makalidap

      I am developing a program that downloads comics from a website. The program has a search engine that allows users to search. Each time a letter is typed, a GET request is sent to the site, and the names and covers of the first two comics in the search results are retrieved. This part works smoothly.

      However, since adding the comic covers to the interface takes a long time, I wanted to perform this operation on a separate thread. But there is a conflict between threads, and the program crashes when typing too quickly in the search box.

      I tried keeping active threads in a dictionary and ensuring that if a new thread starts while an active thread exists, the active thread would be terminated before starting the new thread, but it didn't work.

      from sys import exit, argv
      from os import listdir, path
      from importlib import util
      from mylib.PySide2.helpers import clear_layout
      from PySide2.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QLineEdit, QGraphicsDropShadowEffect, QFrame
      from PySide2.QtGui import QPixmap, QPainter, QColor, QFont, QPalette
      from PySide2.QtCore import Qt, QByteArray, QThread, Signal, QTimer
      
      import requests
      from urllib.parse import urlencode
      
      
      def get_response(query: str, per_page: int) -> dict:
          response = requests.get(f"https://mangagalaxy.org/api/query?searchTerm={query}&perPage={per_page}")
          return response.json()
      
      
      def get_datas(json: dict) -> list[tuple[str, str]]:
          data = []
          for i in json["posts"]:
              data.append((
                  i["postTitle"],
                  convert_url(i["featuredImage"])
              ))
          return data
      
      
      def convert_url(original_url):
          new_params = {
              'url': original_url,
              'w': 128,
              'q': 30
          }
      
          encoded_params = urlencode(new_params)
      
          new_url = f"https://mangagalaxy.org/_next/image?{encoded_params}"
      
          return new_url
      
      
      def search(query: str, per_page: int) -> list[tuple[str, str]]:
          return get_datas(get_response(query=query, per_page=per_page))
      
      
      class ImageLoad(QThread):
          success = Signal(QPixmap)
      
          def __init__(self, parent, url, label):
              super().__init__(parent)
              self.url = url
              self.label = label
      
          def run(self):
              try:
                  image_data = requests.get(self.url).content
                  if image_data:
                      pixmap = QPixmap(128, 180)
                      data = QByteArray(image_data)
                      if pixmap.loadFromData(data):
                          self.success.emit(pixmap)
              except Exception as e:
                  print(e)
      
      
      class ResultWidget(QWidget):
          def __init__(self, parent=None, title=None, image=None, no=None):
              super().__init__(parent)
      
              self.no = no
              self.parent = parent
              layout = QHBoxLayout()
      
              self.photo_label = QLabel()
              self.load_photo(image)
              layout.addWidget(self.photo_label)
      
              title_label = QLabel(title)
              layout.addWidget(title_label)
      
              self.setLayout(layout)
      
          def load_photo(self, url):
              self.photo_label.setPixmap(loading_pixmap)
              self.photo_label.setFixedSize(loading_pixmap.size())
              def finished():
                  self.parent.active_threads[self.no] = None
      
              self.worker = ImageLoad(self, url, self.photo_label)
              self.worker.success.connect(lambda pixmap: self.photo_label.setPixmap(pixmap))
              self.parent.active_threads[self.no] = self.worker
              self.worker.start()
              self.worker.finished.connect(finished)
      
      
      
      
      class MainWindow(QMainWindow):
          def __init__(self):
              super().__init__()
              self.active_threads = {}
              self.init_ui()
      
          def init_ui(self):
              main_layout = QVBoxLayout()
              # ########################################### Main layout
              self.search_layout = QVBoxLayout()
              # ############################ Search layout
      
              self.search_box = QLineEdit()
              self.search_box.setPlaceholderText("Search")
              self.search_box.textChanged.connect(self.search)
              self.search_layout.addWidget(self.search_box)
      
      
              self.results_layout = QVBoxLayout()
              self.results_layout.setContentsMargins(0, 0, 0, 0)
              # ############ Result layout
      
              self.search_layout.addLayout(self.results_layout)
              # ############ Result layout end
      
      
              main_layout.addLayout(self.search_layout)
      
              # ########################################### Main layout end
              self.main_widget = QWidget()
              self.main_widget.setLayout(main_layout)
      
              self.setCentralWidget(self.main_widget)
      
      
          def search(self, event):
              result = search(event, 2)
              self.set_results(result)
      
          def set_results(self, result):
              clear_layout(self.results_layout)
              for no, i in enumerate(result):
                  def a():
                      r = ResultWidget(self, i[0], i[1], no)
                      self.results_layout.addWidget(r)
                  if no in self.active_threads and self.active_threads[no]:
                      print("Stopping")
                      self.active_threads[no].terminate()
                      print("Stopped")
                      self.active_threads[no] = None
                  a()
      
      
      if __name__ == '__main__':
          app = QApplication(argv)
      
          loading_pixmap = QPixmap("image.png")
      
          window = MainWindow()
          window.show()
          exit(app.exec_())
      

      I even made such an addition, but it still didn't work.

      def set_results(self, result):
          clear_layout(self.results_layout)
          for no, i in enumerate(result):
              def a():
                  r = ResultWidget(self, i[0], i[1], no)
                  self.results_layout.addWidget(r)
              if no in self.active_threads and self.active_threads[no]:
                  print("Stopping")
                  self.active_threads[no].terminate()
                  print("Stopped")
                  self.active_threads[no] = None
                  timer = QTimer()
                  timer.setInterval(1000)
                  timer.timeout.connect(a)
                  timer.start()
      
              else:
                  a()
      

      In the end, I reluctantly tried something like this, but even with this, there are occasional issues. What method should I use to resolve the problem?

      self.search_box.textChanged.connect(self.on_text_changed)
      
      self.timer = QTimer(self)
      self.timer.setSingleShot(True)
      self.timer.timeout.connect(self.search)
      
      def on_text_changed(self):
          self.timer.start(1000)
      
      jsulmJ Offline
      jsulmJ Offline
      jsulm
      Lifetime Qt Champion
      wrote on last edited by
      #2

      @makalidap It is not allowed to modify UI from other threads than the main (UI) thread.

      https://forum.qt.io/topic/113070/qt-code-of-conduct

      M 1 Reply Last reply
      1
      • jsulmJ jsulm

        @makalidap It is not allowed to modify UI from other threads than the main (UI) thread.

        M Offline
        M Offline
        makalidap
        wrote on last edited by
        #3

        Actually if ı write letters one by one it works.

        JonBJ 1 Reply Last reply
        0
        • M makalidap

          Actually if ı write letters one by one it works.

          JonBJ Offline
          JonBJ Offline
          JonB
          wrote on last edited by
          #4

          @makalidap
          If you are still modifying the UI thread from other threads your code is unacceptable and needs to be altered per @jsulm's true statement.

          M 1 Reply Last reply
          0
          • JonBJ JonB

            @makalidap
            If you are still modifying the UI thread from other threads your code is unacceptable and needs to be altered per @jsulm's true statement.

            M Offline
            M Offline
            makalidap
            wrote on last edited by
            #5

            @JonB Got it, thank you. But if I change the interface in the main thread, the screen freezes until the data is processed and the image is loaded. So can you give me some idea what to do?

            JonBJ 1 Reply Last reply
            0
            • M makalidap

              @JonB Got it, thank you. But if I change the interface in the main thread, the screen freezes until the data is processed and the image is loaded. So can you give me some idea what to do?

              JonBJ Offline
              JonBJ Offline
              JonB
              wrote on last edited by JonB
              #6

              @makalidap

              However, since adding the comic covers to the interface takes a long time, I wanted to perform this operation on a separate thread.

              Since you now know you are not allowed any UI part in a separate thread if that is what you are saying "takes a long time" there is no point threading it. If you have a lot of computations to do (do you?) you can do them in their own thread, but if "what takes time" is doing UI operations off the resulting computed data it won't help. Start by establishing what the case is. I'm not saying this is the case for you, but about 50% people/newcomers use threads in Qt when it's unnecessary and they could just take advantage of Qt's already-asynchronous paradigm.

              IIRC, you can share a QImage between threads, but not a QPixmap (the latter is UI-oriented, the former is not). I don't know whether that helps, I think you can produce a QImage if that helps and the conversion to a QPixmap (which must be done in UI thread) is not too expensive.

              1 Reply Last reply
              0
              • M Offline
                M Offline
                makalidap
                wrote on last edited by
                #7

                Thank you for all the advice. I will take these into account and rewrite the code. But I want to ask one more thing. Do you think it makes more sense to use QNetworkAccessManager or requests?

                1 Reply Last reply
                0
                • JonBJ Offline
                  JonBJ Offline
                  JonB
                  wrote on last edited by JonB
                  #8

                  QNAM deals with requests, so I don't understand the question.

                  Oic, you mean some Python module named requests. Personally I would use Qt classes where possible rather than picking Python ones. And if the Python stuff is synchronous then no wonder you find you need threads, which you shouldn't need with Qt asynchronous calls.

                  1 Reply Last reply
                  0

                  • Login

                  • Login or register to search.
                  • First post
                    Last post
                  0
                  • Categories
                  • Recent
                  • Tags
                  • Popular
                  • Users
                  • Groups
                  • Search
                  • Get Qt Extensions
                  • Unsolved