wxPythonでファイルダイアログ

wxPythonで長時間実行されるタスク中にGUIの応答性を維持する方法 - メグタンの何でもブログ の続き

wxPythonでファイルダイアログを開く

Openダイアログ

    def m_menuOpenOnMenuSelection( self, event ):
        wildcard = "Python source (*.py)|*.py|"     \
           "Compiled Python (*.pyc)|*.pyc|" \
           "SPAM files (*.spam)|*.spam|"    \
           "Egg file (*.egg)|*.egg|"        \
           "All files (*.*)|*.*"
        dlg = wx.FileDialog(self,
            message="Choose a file",
            defaultDir=os.getcwd(),
            defaultFile="",
            wildcard=wildcard,
            style=wx.FD_OPEN | wx.FD_MULTIPLE |
                  wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST |
                  wx.FD_PREVIEW
            )

        if dlg.ShowModal() == wx.ID_OK:
        # This returns a Python list of files that were selected.
            paths = dlg.GetPaths()
        dlg.Destroy()

Saveダイアログ

    def m_menuSaveOnMenuSelection( self, event ):
        wildcard = "Python source (*.py)|*.py|"     \
           "Compiled Python (*.pyc)|*.pyc|" \
           "SPAM files (*.spam)|*.spam|"    \
           "Egg file (*.egg)|*.egg|"        \
           "All files (*.*)|*.*"
        dlg = wx.FileDialog(self,
            message="Save file as ...",
            defaultDir=os.getcwd(),
            defaultFile="",
            wildcard=wildcard,
            style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
            )
        # This sets the second filter as the default filter  
        dlg.SetFilterIndex(2)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            # fp = file(path, 'w') # Create file anew
            # fp.write(data)
            # fp.close()
        dlg.Destroy()

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()

wxpythonのwx.CallLater

wxpythonのwx.CallAfter - メグタンの何でもブログ の続き

ディレイを入れて実行するwx.CallLater

wx.CallLaterでは指定したms後に処理を行うことができる。
自身の中で繰り返しリスタートする事でwx.Timerの様に定期実行ができる。
メインスレッド(UIスレッド)のみから実行できる。

    def m_button1OnButtonClick( self, event ):
        self.count = 0
        wx.CallLater(100, self.laterTimer)
    def laterTimer(self):
        print('laterTimer id: {0}'.format(th.get_ident()))
        self.count = self.count + 1
        if self.count == 100:
            return
        self.m_gauge1.SetValue(self.count)
        wx.CallLater(100, self.laterTimer)

wx.CallLater
wx.CallLater — wxPython Phoenix 4.2.0 documentation

wxpythonのwx.CallAfter

wxpythonのwx.Timer - メグタンの何でもブログ の続き

他のスレッドからGUI操作する時の wx.CallAfter

wx.CallAfterは処理をメインスレッド(GUIスレッド)のキューに登録して実行してくれる関数です。
他のスレッドからGUIの部品操作を行う時に使用する事が出来ます。

    def m_button1OnButtonClick( self, event ):
        if self.thread.is_alive() == True:
            self.threadStop = True #スレッドを終了させる
            self.thread.join() #スレッドの終了を待つ
        else:
            self.thread = th.Thread(target=self.worker, daemon=True)
            self.thread.start()
    self.thread = th.Thread(target=self.worker, daemon=True)
    self.threadStop = True

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

    def worker(self):
        print('id: {0}'.format(th.get_ident()))  #スレッドの確認
        self.threadStop = False
        for i in range(0,100,1):
            if self.threadStop == True:
                break
            time.sleep(0.5) # ここで時間の掛かる処理
            wx.CallAfter(self.update,i) # GUI操作はメインスレッドから呼び出す (MainLoop内で呼び出される)

wx.CallAfterが実行されると、メインスレッドでupdateが実行されるまでサブのスレッドは停止する事になります。

実行されているスレッドを確認するには

import threading as th
print('id: {0}'.format(th.get_ident()))

wx.CallAfter
wx Functions — wxPython Phoenix 4.2.0 documentation

wxpythonのwx.Timer

wxpythonのMatplotlibにラバーバンドを表示する - メグタンの何でもブログ の続き

GUIで一定周期で処理を繰り返したい時にはwx.Timerが便利

GUI上で一定周期で点滅表示とかを行いたい時にはメインスレッド(UIスレッド)で動くwx.Timerが便利
手書きで追加してもいいけれども wxFormBuilder で追加しよう。
タイマーで動かすための プログレスバーも追加
タイマーは additional の一番右に wxTimer があるので追加
プログレスバーは common の一番右に wxGage がある。

wxTimer には OnTimer イベントを追加して置く。
Generate Code で画面のコードを生成する。
このコードは修正してはいけない。 継承したクラスコードの方に、生成された OnTImer イベントをコピペして処理内容を追加する。

    def m_timer1OnTimer( self, event ):
        self.count = self.count + 1
        if self.count == 100:
            self.m_timer1.Stop()
        self.m_gauge1.SetValue(self.count)

タイマーの起動と停止の為に、ボタンクリックのイベントを記述する。
(ボタンクリックのイベント追加も上記と同じ)
ボタンを押す度にタイマーのスタート/ストップをトグルする。

    def m_button1OnButtonClick( self, event ):
        print("button pushed")
        if self.m_timer1.IsRunning() == False:
            self.count = 0
            self.m_timer1.Start(10)
        else:
            self.m_timer1.Stop()

wx.Timer の詳細は下記を参照
wx.Timer — wxPython Phoenix 4.2.0 documentation

wxpythonのMatplotlibにラバーバンドを表示する

wxpythonでmatplotlibの表示(高速化) - メグタンの何でもブログ の続き

マウスでラバーバンドを表示してmatplotlibの領域を選択したい時

matplotlibにはRectangleSelectorがあるのでそれを利用すると良い。

from matplotlib.widgets import RectangleSelector
    # drawtype is 'box' or 'line' or 'none'
    self.toggle_selector = RectangleSelector(self.ax, 
                                self.select_callback,
                                drawtype='box', useblit=True,
                                button=[1],  # use left button
                                minspanx=5, minspany=5,
                                spancoords='pixels',
                                interactive=True)
    def select_callback(self, eclick, erelease):
        x0, y0 = eclick.xdata, eclick.ydata
        x1, y1 = erelease.xdata, erelease.ydata
        self.p1 = [min(x0, x1), min(y0, y1)]
        self.p2 = [max(x0, x1), max(y0, y1)]
        t = 'Select Rect : (%f, %f) (%f, %f)' % (self.p1[0],self.p1[1],self.p2[0],self.p2[1])
        self.m_statusBar1.SetStatusText(t)

結果はこんな感じ

wxpythonでmatplotlibの表示(高速化)

wxpythonでmatplotlibのクリック座標取得 - メグタンの何でもブログ の続き

Matplotlibの表示データが多い時に表示の負荷を減らす方法

import matplotlib as mpl
    mpl.rcParams['path.simplify_threshold'] = 1.0

デフォルトでは1/9が設定されている。
値は0.0~1.0の間で設定し、0.0では表示の省略無し、1.0で省略最大。

詳しくは
Performance — Matplotlib 3.5.3 documentation

rcParamsのデフォルトを見たい時には

print(plt.rcParams)

printの結果はこんな感じ

_internal.classic_mode: False
agg.path.chunksize: 0
animation.bitrate: -1
animation.codec: h264
animation.convert_args: []
animation.convert_path: convert
animation.embed_limit: 20.0
animation.ffmpeg_args: []
animation.ffmpeg_path: ffmpeg
animation.frame_format: png
animation.html: none
animation.writer: ffmpeg
axes.autolimit_mode: data
axes.axisbelow: line
axes.edgecolor: black
axes.facecolor: white
axes.formatter.limits: [-5, 6]
axes.formatter.min_exponent: 0
axes.formatter.offset_threshold: 4
axes.formatter.use_locale: False
axes.formatter.use_mathtext: False
axes.formatter.useoffset: True
axes.grid: False
axes.grid.axis: both
axes.grid.which: major
axes.labelcolor: black
axes.labelpad: 4.0
axes.labelsize: medium
axes.labelweight: normal
axes.linewidth: 0.8
axes.prop_cycle: cycler('color', ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'])
axes.spines.bottom: True
axes.spines.left: True
axes.spines.right: True
axes.spines.top: True
axes.titlecolor: auto
axes.titlelocation: center
axes.titlepad: 6.0
axes.titlesize: large
axes.titleweight: normal
axes.titley: None
axes.unicode_minus: True
axes.xmargin: 0.05
axes.ymargin: 0.05
axes.zmargin: 0.05
axes3d.grid: True
backend: WXAgg
backend_fallback: True
boxplot.bootstrap: None
boxplot.boxprops.color: black
boxplot.boxprops.linestyle: -
boxplot.boxprops.linewidth: 1.0
boxplot.capprops.color: black
boxplot.capprops.linestyle: -
boxplot.capprops.linewidth: 1.0
boxplot.flierprops.color: black
boxplot.flierprops.linestyle: none
boxplot.flierprops.linewidth: 1.0
boxplot.flierprops.marker: o
boxplot.flierprops.markeredgecolor: black
boxplot.flierprops.markeredgewidth: 1.0
boxplot.flierprops.markerfacecolor: none
boxplot.flierprops.markersize: 6.0
boxplot.meanline: False
boxplot.meanprops.color: C2
boxplot.meanprops.linestyle: --
boxplot.meanprops.linewidth: 1.0
boxplot.meanprops.marker: ^
boxplot.meanprops.markeredgecolor: C2
boxplot.meanprops.markerfacecolor: C2
boxplot.meanprops.markersize: 6.0
boxplot.medianprops.color: C1
boxplot.medianprops.linestyle: -
boxplot.medianprops.linewidth: 1.0
boxplot.notch: False
boxplot.patchartist: False
boxplot.showbox: True
boxplot.showcaps: True
boxplot.showfliers: True
boxplot.showmeans: False
boxplot.vertical: True
boxplot.whiskerprops.color: black
boxplot.whiskerprops.linestyle: -
boxplot.whiskerprops.linewidth: 1.0
boxplot.whiskers: 1.5
contour.corner_mask: True
contour.linewidth: None
contour.negative_linestyle: dashed
date.autoformatter.day: %Y-%m-%d
date.autoformatter.hour: %m-%d %H
date.autoformatter.microsecond: %M:%S.%f
date.autoformatter.minute: %d %H:%M
date.autoformatter.month: %Y-%m
date.autoformatter.second: %H:%M:%S
date.autoformatter.year: %Y
date.converter: auto
date.epoch: 1970-01-01T00:00:00
date.interval_multiples: True
docstring.hardcopy: False
errorbar.capsize: 0.0
figure.autolayout: False
figure.constrained_layout.h_pad: 0.04167
figure.constrained_layout.hspace: 0.02
figure.constrained_layout.use: False
figure.constrained_layout.w_pad: 0.04167
figure.constrained_layout.wspace: 0.02
figure.dpi: 100.0
figure.edgecolor: white
figure.facecolor: white
figure.figsize: [6.4, 4.8]
figure.frameon: True
figure.max_open_warning: 20
figure.raise_window: True
figure.subplot.bottom: 0.11
figure.subplot.hspace: 0.2
figure.subplot.left: 0.125
figure.subplot.right: 0.9
figure.subplot.top: 0.88
figure.subplot.wspace: 0.2
figure.titlesize: large
figure.titleweight: normal
font.cursive: ['Apple Chancery', 'Textile', 'Zapf Chancery', 'Sand', 'Script MT', 'Felipa', 'Comic Neue', 'Comic Sans MS', 'cursive']
font.family: ['sans-serif']
font.fantasy: ['Chicago', 'Charcoal', 'Impact', 'Western', 'Humor Sans', 'xkcd', 'fantasy']
font.monospace: ['DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Computer Modern Typewriter', 'Andale Mono', 'Nimbus Mono L', 'Courier New', 'Courier', 'Fixed', 'Terminal', 'monospace']
font.sans-serif: ['DejaVu Sans', 'Bitstream Vera Sans', 'Computer Modern Sans Serif', 'Lucida Grande', 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica', 'Avant Garde', 'sans-serif']
font.serif: ['DejaVu Serif', 'Bitstream Vera Serif', 'Computer Modern Roman', 'New Century Schoolbook', 'Century Schoolbook L', 'Utopia', 'ITC Bookman', 'Bookman', 'Nimbus Roman No9 L', 'Times New Roman', 'Times', 'Palatino', 'Charter', 'serif']
font.size: 10.0
font.stretch: normal
font.style: normal
font.variant: normal
font.weight: normal
grid.alpha: 1.0
grid.color: #b0b0b0
grid.linestyle: -
grid.linewidth: 0.8
hatch.color: black
hatch.linewidth: 1.0
hist.bins: 10
image.aspect: equal
image.cmap: viridis
image.composite_image: True
image.interpolation: antialiased
image.lut: 256
image.origin: upper
image.resample: True
interactive: False
keymap.back: ['left', 'c', 'backspace', 'MouseButton.BACK']
keymap.copy: ['ctrl+c', 'cmd+c']
keymap.forward: ['right', 'v', 'MouseButton.FORWARD']
keymap.fullscreen: ['f', 'ctrl+f']
keymap.grid: ['g']
keymap.grid_minor: ['G']
keymap.help: ['f1']
keymap.home: ['h', 'r', 'home']
keymap.pan: ['p']
keymap.quit: ['ctrl+w', 'cmd+w', 'q']
keymap.quit_all: []
keymap.save: ['s', 'ctrl+s']
keymap.xscale: ['k', 'L']
keymap.yscale: ['l']
keymap.zoom: ['o']
legend.borderaxespad: 0.5
legend.borderpad: 0.4
legend.columnspacing: 2.0
legend.edgecolor: 0.8
legend.facecolor: inherit
legend.fancybox: True
legend.fontsize: medium
legend.framealpha: 0.8
legend.frameon: True
legend.handleheight: 0.7
legend.handlelength: 2.0
legend.handletextpad: 0.8
legend.labelcolor: None
legend.labelspacing: 0.5
legend.loc: best
legend.markerscale: 1.0
legend.numpoints: 1
legend.scatterpoints: 1
legend.shadow: False
legend.title_fontsize: None
lines.antialiased: True
lines.color: C0
lines.dash_capstyle: butt
lines.dash_joinstyle: round
lines.dashdot_pattern: [6.4, 1.6, 1.0, 1.6]
lines.dashed_pattern: [3.7, 1.6]
lines.dotted_pattern: [1.0, 1.65]
lines.linestyle: -
lines.linewidth: 1.5
lines.marker: None
lines.markeredgecolor: auto
lines.markeredgewidth: 1.0
lines.markerfacecolor: auto
lines.markersize: 6.0
lines.scale_dashes: True
lines.solid_capstyle: projecting
lines.solid_joinstyle: round
markers.fillstyle: full
mathtext.bf: sans:bold
mathtext.cal: cursive
mathtext.default: it
mathtext.fallback: cm
mathtext.fontset: dejavusans
mathtext.it: sans:italic
mathtext.rm: sans
mathtext.sf: sans
mathtext.tt: monospace
patch.antialiased: True
patch.edgecolor: black
patch.facecolor: C0
patch.force_edgecolor: False
patch.linewidth: 1.0
path.effects: []
path.simplify: True
path.simplify_threshold: 0.111111111111
path.sketch: None
path.snap: True
pcolor.shading: auto
pcolormesh.snap: True
pdf.compression: 6
pdf.fonttype: 3
pdf.inheritcolor: False
pdf.use14corefonts: False
pgf.preamble:
pgf.rcfonts: True
pgf.texsystem: xelatex
polaraxes.grid: True
ps.distiller.res: 6000
ps.fonttype: 3
ps.papersize: letter
ps.useafm: False
ps.usedistiller: None
savefig.bbox: None
savefig.directory: ~
savefig.dpi: figure
savefig.edgecolor: auto
savefig.facecolor: auto
savefig.format: png
savefig.orientation: portrait
savefig.pad_inches: 0.1
savefig.transparent: False
scatter.edgecolors: face
scatter.marker: o
svg.fonttype: path
svg.hashsalt: None
svg.image_inline: True
text.antialiased: True
text.color: black
text.hinting: force_autohint
text.hinting_factor: 8
text.kerning_factor: 0
text.latex.preamble:
text.usetex: False
timezone: UTC
tk.window_focus: False
toolbar: toolbar2
webagg.address: 127.0.0.1
webagg.open_in_browser: True
webagg.port: 8988
webagg.port_retries: 50
xaxis.labellocation: center
xtick.alignment: center
xtick.bottom: True
xtick.color: black
xtick.direction: out
xtick.labelbottom: True
xtick.labelcolor: inherit
xtick.labelsize: medium
xtick.labeltop: False
xtick.major.bottom: True
xtick.major.pad: 3.5
xtick.major.size: 3.5
xtick.major.top: True
xtick.major.width: 0.8
xtick.minor.bottom: True
xtick.minor.pad: 3.4
xtick.minor.size: 2.0
xtick.minor.top: True
xtick.minor.visible: False
xtick.minor.width: 0.6
xtick.top: False
yaxis.labellocation: center
ytick.alignment: center_baseline
ytick.color: black
ytick.direction: out
ytick.labelcolor: inherit
ytick.labelleft: True
ytick.labelright: False
ytick.labelsize: medium
ytick.left: True
ytick.major.left: True
ytick.major.pad: 3.5
ytick.major.right: True
ytick.major.size: 3.5
ytick.major.width: 0.8
ytick.minor.left: True
ytick.minor.pad: 3.4
ytick.minor.right: True
ytick.minor.size: 2.0
ytick.minor.visible: False
ytick.minor.width: 0.6
ytick.right: False