PySide2 thread issue in the search engine suggestions I implemented
-
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)
-
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)
@makalidap It is not allowed to modify UI from other threads than the main (UI) thread.
-
@makalidap It is not allowed to modify UI from other threads than the main (UI) thread.
-
@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. -
@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?
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 aQPixmap
(the latter is UI-oriented, the former is not). I don't know whether that helps, I think you can produce aQImage
if that helps and the conversion to aQPixmap
(which must be done in UI thread) is not too expensive. -
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.