wxPythonで長時間実行されるタスク中にGUIの応答性を維持する方法

wxpythonのwx.CallLater - メグタンの何でもブログ の続き 画面のボタンはStartとAbortの2つになってます。

長時間実行されるタスク中にGUIの応答性を維持する

CPUを使い続ける計算等でメインスレッドを占有すると、ウインドウが固まってしまい操作性が阻害される。
回避するには次のような方法がある。

wx.Yield()を使用する方法

wx.Yield()によって一時的に関数処理を中断しメインスレッドに他の処理をさせる。
計算ループ内で実行される頻度によって応答性は変わる。 画面の処理が行われるので、計算が終わるまで起動ボタンを無効化する。

    def m_buttonStartOnButtonClick( self, event ):
        self.m_buttonStart.Enable(False)
        self.guiloop()
        self.m_buttonStart.Enable(True)
    def guiloop(self):
        for i in range(0,100,1):
            time.sleep(0.1) # ここで時間の掛かる処理
            self.m_gauge1.SetValue(i)
            wx.Yield()

wx.lib.delayedresultを使用する方法

import wx.lib.delayedresult as delayedresult

中断用にAbortEventを作っておく

        self.abortEvent = delayedresult.AbortEvent()
        self.jobID = 0

Startボタンを押した時の処理
Startボタンを無効化してAbortボタンを有効にする

    def m_buttonStartOnButtonClick( self, event ):
        self.m_buttonStart.Enable(False)
        self.m_buttonAbort.Enable(True)
        self.abortEvent.clear()
        self.jobID += 1
        delayedresult.startWorker(self.guiloop_finish, self.guiloop,
                          wargs=(self.jobID,self.abortEvent), jobID=self.jobID)

Abortボタンを押した時の処理 AbortEventをセットする
Startボタンを有効化してAbortボタンを無効化する

    def m_buttonAbortOnButtonClick( self, event ):
        self.abortEvent.set()
        self.m_buttonStart.Enable(True)
        self.m_buttonAbort.Enable(False)

別スレッドでの重たい処理
途中経過で画面を操作したい時にはCallAfterでメインスレッドに委託

    def guiloop(self,jobID, abortEvent):
        for i in range(0,20,1):
            time.sleep(0.1) # ここで時間の掛かる処理
            wx.CallAfter(self.update,i) #
            ret = i
            if abortEvent():
                print("Abort")
                break
        return(str(ret))

    def update(self, data):
        print('update id: {0}'.format(th.get_ident()))  #UIスレッドで実行
        self.m_gauge1.SetValue(data)

別スレッドでの処理が終了した時に呼ばれる処理
Startボタンを有効化してAbortボタンを無効化する
この関数はメインスレッドで呼ばれる
Abortによる終了時には呼ばれない

    def guiloop_finish(self ,delayedResult):
        self.m_buttonStart.Enable(True)
        self.m_buttonAbort.Enable(False)
        jobID = delayedResult.getJobID()
        assert jobID == self.jobID
        try:
            result = delayedResult.get()
        except Exception as exc:
            print( "Result for job %s raised exception: %s" % (jobID, exc) )
            return
        print( "Got result for job %s: %s" % (jobID, result) )

wx.ProgressDialogで進捗の表示とキャンセルをする例

ProgressDialogでメインウインドウがロックされるのでボタンの有効/無効は不要

    def m_buttonStartOnButtonClick(self, event ):
        self.abortEvent.clear()
        self.jobID += 1
        delayedresult.startWorker(self.guiloop_finish, self.guiloop,
                          wargs=(self.jobID,self.abortEvent), jobID=self.jobID)
        self.max = 80
        self.dlg = wx.ProgressDialog("Progress dialog example",
                               "An informative message",
                               maximum = self.max,
                               parent=self,
                               style = 0
                                | wx.PD_APP_MODAL
                                | wx.PD_CAN_ABORT
                                #| wx.PD_CAN_SKIP
                                #| wx.PD_ELAPSED_TIME
                                | wx.PD_ESTIMATED_TIME
                                | wx.PD_REMAINING_TIME
                                #| wx.PD_AUTO_HIDE
                                )

guiloop_finishの最後でProgressDialogをDestroy

    def guiloop_finish(self ,delayedResult):
        jobID = delayedResult.getJobID()
        assert jobID == self.jobID
        try:
            result = delayedResult.get()
        except Exception as exc:
            print( "Result for job %s raised exception: %s" % (jobID, exc) )
            return
        print( "Got result for job %s: %s" % (jobID, result) )
        self.dlg.Destroy()

guiloopは同じ

    def guiloop(self,jobID, abortEvent):
        ret = 0
        for i in range(0,self.max,1):
            time.sleep(0.1) # ここで時間の掛かる処理
            if abortEvent():
                print("Abort")
                break
            wx.CallAfter(self.update,i)
            ret = i
        return(str(ret))

guiloopから進捗を伝達
キャンセルボタンが押されていたらAbortしてProgressDialogをDestroy

    def update(self, data):
        if data >= self.max / 2:
            (keepGoing, skip) = self.dlg.Update(data, "Half-time!")
        else:
            (keepGoing, skip) = self.dlg.Update(data)
        if not keepGoing:
            self.abortEvent.set()
            self.dlg.Destroy()