解決 PySide6 崩潰/閃退(自定義訊號)

L小庸 發表於 2022-06-13

問題描述

自己有個爬蟲專案,使用 PySide6 只做 GUI,包裝 Selenium,其中在 GUI 上會列印爬蟲進度,見下圖。
image.png
但當 Selenium 出現錯誤我重新點選開始的時候,發現 PySide6 crash 了!錯誤資訊如下:

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

問題調研

網上查閱資料知道這是因為

You're trying to read/write a file which is open. In this case, simply closing the file and rerunning the script solved the issue

即正在讀/寫正在開啟的檔案,解決方案就是關閉已經開啟的檔案就好,然後重新執行指令碼。但問題是關閉哪個檔案啊 😂

自定義訊號

深入分析我的程式發現,我的自定義訊號沒有寫好。

為什麼需要自定義訊號?當我們需要更新 UI 時,我們既不能在主執行緒直接操作(會阻塞 UI),又不能在子執行緒直接操作(會有意想不到的 BUG,比如我這個 crash)。當我們需要操作 UI,就需要發出一個自定義訊號,主執行緒收到訊號後,會在合適的時機儘快更新 UI。

首先,自定義訊號程式碼如下

class MySignal(QObject):
    # 定義更新日誌的 signal
    update_log = Signal(str)

my_signal = MySignal()

錯誤程式碼:

# PySide UI
class MainWindow(QMainWindow):
    def __init__(self):
        self.bind_signal()

    def bind_signal(self):
        # 繫結了自定義 Signal 到特定函式,但傳送時出錯了
        my_signal.update_log.connect(self.update_log)

    def update_log(self, log_info):
        self.ui.log_box.appendPlainText(log_info)

    def start_buy(self):
        # ControlBuy 是 Selenium 控制類,這裡把更新日誌的函式傳遞進去了
        # 但 update_log 是直接更新了 UI,所以出現了錯誤
        # 應該是 emit 更新日誌的 Signal
        control_buy = ControlBuy(self.update_log)
        thread = Thread(target=control_buy.start)
        thread.start()

正確程式碼

# emit 更新 log 的 signal,Selenium 直接呼叫的是 emit signal 的函式
# 而非直接呼叫 update_log
def send_log_signal(log_info):
    my_signal.update_log.emit(log_info)

# PySide UI
class MainWindow(QMainWindow):
    def __init__(self):
        self.bind_signal()

    def bind_signal(self):
        my_signal.update_log.connect(self.update_log)

    def update_log(self, log_info):
        self.ui.log_box.appendPlainText(log_info)

    def start_buy(self):
        # 傳給 Selenium 控制類的是 send_log_signal 而非 self.update_log!!!
        control_buy = ControlBuy(send_log_signal)
        thread = Thread(target=control_buy.start)
        thread.start()

回顧

子執行緒直接更新 UI,可能導致多個執行緒同時操作 Pyside 元件,這樣就會導致程式崩潰。解決方法就是藉助自定義訊號更新 UI。

自定義訊號的關鍵是:將自定義訊號 connect 到某個函式(在這個函式中更新 UI),在需要更新 UI 的地方,emit 自定義訊號(同時傳遞必要引數)

# 繫結自定義訊號到特定函式
my_signal.update_log.connect(self.update_log)

# emit 自定義訊號
my_signal.update_log.emit(log_info)