wxpythonにてフラグによって動画再生や切り替えを行う方法。
- 評価
- クリップ 0
- VIEW 755
以前質問させて頂いたあとの別のやりかたの質問をさせて下さい。
https://teratail.com/questions/256690#reply-369648
前提・実現したいこと
wxpythonでgif動画を再生するデスクトップアプリを作っています。
10秒後(不確定な第三者の反応に応じた後<例えばメール受信等々>)にgif動画を切り替えたい。
Architectureと発生している問題・エラーメッセージ
<方法1>
wxpythonのmain部分に動画再生メソッドと、チェック機能(例えばwhileで常に第三者の反応をチェックするメソッド)をmainに一緒に置いてしまうと、main側の動画再生ができませんでした。
# coding:utf-8
import os
import sys
import time
import cv2
import numpy as np
import wx.adv
import time
import os
import wx
class MainWindow(wx.Frame):
# def __init__(self):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(700, 600))
self.ctrl = wx.adv.AnimationCtrl(self)
self.ctrl.Stop()
from glob import glob
from itertools import cycle
from functools import partial
def showNextImage():
filepath = 'someiyoshino1.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
def showNextImage2():
filepath = 'someiyoshino2.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
count = 0
# 第三者アクション ボタン押下などではなく(メール受信など予測が難しいタイミング等)
# フラグが立った時に動画を再生や切り替えを行いたい。
while True:
count += 1
print(count)
if(count == 10):
showNextImage2()
time.sleep(1)
from multiprocessing import Process, Value
import time
def main(a:Value):
print("mainスタート")
app = wx.App()
win = MainWindow(None, wx.ID_ANY, 'TOTO')
win.Centre()
win.Show()
app.MainLoop()
def func1(a: Value):
print("[func1]スタート")
while True:
if(a.Value == 1):
wx.CallLater(1000*1, self.showNextImage)
if __name__ == "__main__":
a = Value('d', 0)
func0_proc = Process(target=main, args=(a,))
func1_proc = Process(target=func1, args=(a,))
func0_proc.start()
func1_proc.start()
func0_proc.join()
func1_proc.join()
print(a.value)
<方法2>
あらかじめ同じメモリ内にa.Valueの変数を作り、wxpythonのmain部分に動画再生メソッドを置き、チェック機能(例えばwhileで常に第三者の反応をチェックするメソッド)を別のスレッドにして並行稼働してチェックし、一定条件でa.Valueを変化させ、mainスレッドでa.Valueの変化を察知してCallLaterで同main内の動画再生メソッド呼び出しを行いましたが、実行されませんでした。
ソースコードは以下です。
# coding:utf-8
import os
import sys
import time
import cv2
import numpy as np
import wx.adv
import time
import os
import wx
choose_text_h5 = ""
class MainWindow(wx.Frame):
# def __init__(self):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(700, 600))
self.ctrl = wx.adv.AnimationCtrl(self)
self.ctrl.Stop()
from glob import glob
from itertools import cycle
from functools import partial
def showNextImage():
filepath = 'someiyoshino1.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
def showNextImage2():
filepath = 'someiyoshino2.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
if(a.Value == 1):
wx.CallLater(1000*1, showNextImage)
from multiprocessing import Process, Value
import time
def main(a:Value):
print("mainスタート")
# app = wx.App(False)
# Main().Show(True)
# app.MainLoop()
app = wx.App()
win = MainWindow(None, wx.ID_ANY, 'TOTO')
win.Centre()
win.Show()
app.MainLoop()
def func1(a: Value):
print("[func1]スタート")
count = 0
while True:
count += 1
print(count)
if(count == 10):
a.Value = 1
time.sleep(1)
print(a.value)
def func2(self, a: Value):
print("[func2]スタート")
# while True:
# if(a.Value == 1):
# wx.CallLater(1000*1, self.showNextImage)
if __name__ == "__main__":
a = Value('d', 0)
func0_proc = Process(target=main, args=(a,))
func1_proc = Process(target=func1, args=(a,))
func2_proc = Process(target=func2, args=(a,))
func0_proc.start()
func1_proc.start()
func2_proc.start()
func0_proc.join()
func1_proc.join()
func2_proc.join()
print(a.value)
<方法3>
あらかじめ同じメモリ内にa.Valueの変数を作り、wxpythonのmain部分に動画再生メソッドを置き、チェック機能(例えばwhileで常に第三者の反応をチェックするメソッド)を別のスレッドにして並行稼働してチェックし、一定条件でa.Valueを変化させ、3つ目のスレッドでa.Valueの変化を察知してmain側に対してCallLaterでメソッド呼び出しを行いましたが、実行されませんでした。
# coding:utf-8
import os
import sys
import time
import cv2
import numpy as np
import wx.adv
import time
import os
import wx
choose_text_h5 = ""
class MainWindow(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(700, 600))
self.ctrl = wx.adv.AnimationCtrl(self)
self.ctrl.Stop()
from glob import glob
from itertools import cycle
from functools import partial
self.MainPanel = wx.Panel(self, size=(1300, 900))#メイン画面の大きさ
def showNextImage():
filepath = 'someiyoshino1.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
def showNextImage2():
filepath = 'someiyoshino2.gif'
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
from multiprocessing import Process, Value
import time
def main():
print("mainスタート")
# app = wx.App(False)
# Main().Show(True)
# app.MainLoop()
app = wx.App()
win = MainWindow(None, wx.ID_ANY, 'TOTO')
win.Centre()
win.Show()
app.MainLoop()
def func1(self):
print("[func1]スタート")
count = 0
while True:
count += 1
print(count)
if(count == 10):
wx.CallLater(1000*1, self.showNextImage)
time.sleep(1)
if __name__ == "__main__":
# a = Value('d', 0)
func0_proc = Process(target=main)
func1_proc = Process(target=func1)
func0_proc.start()
func1_proc.start()
func0_proc.join()
func1_proc.join()
print(a.value)
<方法4>
動画再生側をmain側ではなく、別のメソッドとして外部に配置する方法を試みようとしましたが、外部メソッド実行じたいができませんでした。
import wx
import wx.adv
from glob import glob
from itertools import cycle
from functools import partial
def showNextImage():
filepath = 'someiyoshino1.gif'
print(filepath)
wx.adv.AnimationCtrl.SetAnimation(wx.adv.Animation(filepath))
wx.adv.AnimationCtrl.Play()
showNextImage()
方法1~4まで全て試したのですが、うまくいきませんでした。
任意のフラグによって動画を差し替えたり再生したりなどはどのように行えばよいのでしょうか。
よろしくお願い致します。
補足情報(FW/ツールのバージョンなど)
win10
python3.7
wxpython
参考サイト
https://teratail.com/questions/158458
https://teratail.com/questions/233764
https://qiita.com/wikipediia/items/2919362de582a7d8de9e
https://www.python-beginners.com/entry/20191125/1574687207
https://qiita.com/asakbiz/items/5a34cae7b6c00c87a7e6
https://dev.classmethod.jp/articles/python-asyncio/
https://www.rhoboro.com/2019/02/09/coroutine-abstract.html
https://qiita.com/satsukiya/items/f7a3c7fdae566ed96306
https://www.yoheim.net/blog.php?q=20170601
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
checkベストアンサー
+1
PostEvent を用いて通知する方法です。
import os
import sys
import time
from threading import Thread
import wx
from wx.adv import Animation, AnimationCtrl
from wx.lib.newevent import NewEvent
UpdateImageEvent, EVT_UPDATE_IMAGE = NewEvent()
class CountTimer(Thread):
daemon = True
def __init__(self, win):
super().__init__()
# イベント通知対象のウィジェット。(MainWindow)
self.win = win
# 次の画像ファイルを所得する関数 (仮: カレントディレクトリの gif ファイルが対象)
from glob import glob
from itertools import cycle
from functools import partial
self.nextImageFile = partial(next, cycle(glob("*.gif")))
def run(self):
num = 0
while True:
num += 1
if num % 10 == 0: # 剰余で十秒毎を判別
# 次に表示する画像ファイル
filepath = self.nextImageFile()
# イベント通知する為、引数となるイベントオブジェクトを作成
event = UpdateImageEvent(filepath=filepath)
# 通知 (MainLoopのキューにイベントを入れる)
wx.PostEvent(self.win, event)
del event, filepath
time.sleep(1)
class MainWindow(wx.Frame):
def __init__(self, parent=None, ID=wx.ID_ANY, title="", *args, **kw):
super().__init__(parent, ID, title, *args, **kw)
self.animeCtrl = AnimationCtrl(self)
# イベントとイベント通知受信時に呼び出されるコールバック関数の結び付け
self.Bind(EVT_UPDATE_IMAGE, self.OnUpdateImage)
def OnUpdateImage(self, event):
# PostEvent での通知により *メインスレッドで* 実行される
print("OnUpdateImage", event.filepath)
self.SetImageFile(event.filepath)
def SetImageFile(self, filepath):
# 画像・動画を変更し再生する
assert os.path.isfile(filepath)
self.animeCtrl.SetAnimation(Animation(filepath))
self.animeCtrl.Play()
def main():
app = wx.App()
win = MainWindow()
win.SetSize(400, 400)
win.Centre()
win.Show()
thread = CountTimer(win)
thread.start()
app.MainLoop()
if __name__ == '__main__':
main()
数値のカウントの方法について、
Pythonの数値型は多倍長なので、桁溢れの心配はないとはいえ
気になる部分なので、改善案。追記: インデントが深くならないように再度修正。
カウント自体の処理と、カウント毎の処理の分離もできます。
class CountTimer(Thread):
daemon = True
# 省略
def onCountInterval(self):
# 次に表示する画像ファイル
filepath = self.nextImageFile()
# イベント通知する為、引数となるイベントオブジェクトを作成
event = UpdateImageEvent(filepath=filepath)
# 通知 (MainLoopのキューにイベントを入れる)
wx.PostEvent(self.win, event)
def run(self, count=10, interval=1):
while True:
for _ in range(count):
time.sleep(interval)
else:
self.onCountInterval()
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+1
<方法1>
wxpythonのmain部分に動画再生メソッドと、チェック機能(例えばwhileで常に第三者の反応をチェックするメソッド)をmainに一緒に置いてしまうと、main側の動画再生ができませんでした。
wx等のGUIではマウスイベント等を処理する為の MainLoop を持ちます。
その為メインスレッドでループを組んでしまうと、他のイベントが処理されなくなります。
各種イベントで呼ばれるコールバックは別スレッドで実行されてるわけではなく、
イベント処理のループ → 自分で定義したイベント → イベント処理のループ という流れです。
ループ内で wx.Yield() や wx.YieldIfNeeded() を呼ぶことで一時的には回避できますが、
根本的な問題解決にはなりません。
上記のコードであれば MainWindow のコンストラクタ内で止ってしまうので、
win.Show() 等が呼ばれません。
この方法(メインスレッドでループを実行したい)を取りたい場合は、MainLoop()を使わずに、
「非同期プログラミング」という手法を取ります。wxではあまり事例がない上に、
コードも設計から大幅な変更を要求される為、詳細な説明は見送ります。
<方法2>
あらかじめ同じメモリ内にa.Valueの変数を作り、
- 見た感じ、a.value と a.Value の typoが?
- MainWindow に変数 a が渡っていない
- func2 呼び出し時の引数不一致。(ここはself不要)
但し、方法自体にまだ課題があり、
a.value == 1 になった時に切り替わりますが、
a.value == 1 の時に常に画像が切り替わり続けます。(剰余で判断したほうが良いのでは)
コードの不要な部分を省き、エラーのみ修整したので動作を確かめて見て下さい。
# coding:utf-8
import os
import sys
import time
import wx
import wx.adv
from multiprocessing import Process, Value
class MainWindow(wx.Frame):
# def __init__(self):
def __init__(self, parent, id, title, a):
wx.Frame.__init__(self, parent, id, title, size=(700, 600))
self.ctrl = wx.adv.AnimationCtrl(self)
self.ctrl.Stop()
self.a = a
self.watchValueChanged()
from glob import glob
from functools import partial
from itertools import cycle
self.nextImageFile = partial(next, cycle(glob("*.gif")))
def showNextImage(self):
filepath = self.nextImageFile() # ここの戻り値を毎回変化させる。
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
self.ctrl.Play()
def watchValueChanged(self):
if(self.a.value == 1):
wx.CallAfter(self.showNextImage)
wx.CallLater(1000*10, self.watchValueChanged)
def main(a:Value):
print("mainスタート")
app = wx.App()
win = MainWindow(None, wx.ID_ANY, 'TOTO', a)
win.Centre()
win.Show()
app.MainLoop()
def func1(a: Value):
print("[func1]スタート")
count = 0
while True:
count += 1
print(count)
if(count == 10):
a.value = 1
time.sleep(1)
print(a.value)
if __name__ == "__main__":
a = Value('d', 0)
func0_proc = Process(target=main, args=(a,))
func1_proc = Process(target=func1, args=(a,))
func0_proc.start()
func1_proc.start()
func0_proc.join()
func1_proc.join()
print(a.value)
<方法3>
あらかじめ同じメモリ内にa.Valueの変数を作り、wxpythonのmain部分に動画再生メソッドを置き、
3つ目のスレッドでa.Valueの変化を察知してmain側に対してCallLaterでメソッド呼び出しを行いましたが、実行されませんでした。
これは、「スレッド」ではなく「プロセス」であるためです。
もう一点、showNextImage関数は、内部で定義された関数の為、
外から呼び出せるようにはなっていません。
この場合は、mainはスレッドにする必要はなく、
秒数カウントのみを別スレッドにして、MainWindowのインスタンスを渡します。
# coding:utf-8
import os
import sys
import time
from threading import Thread
import wx
import wx.adv
class MainWindow(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(700, 600))
self.ctrl = wx.adv.AnimationCtrl(self)
self.ctrl.Stop()
self.MainPanel = wx.Panel(self, size=(1300, 900))#メイン画面の大きさ
from glob import glob
from functools import partial
from itertools import cycle
self.nextImageFile = partial(next, cycle(glob("*.gif")))
def showNextImage(self):
filepath = self.nextImageFile()
print(filepath)
self.ctrl.SetAnimation(wx.adv.Animation(filepath))
wx.CallAfter(self.ctrl.Play)
def func1(win):
print("[func1]スタート")
count = 1
while True:
count += 1
print(count)
if(count % 10 == 0): # ※ 10秒毎に切り替わるように剰余で判断
win.showNextImage()
time.sleep(1)
if __name__ == "__main__":
app = wx.App()
win = MainWindow(None, wx.ID_ANY, 'TOTO')
thread = Thread(target=func1, args=(win,))
thread.start()
win.Centre()
win.Show()
app.MainLoop()
※ この例では、Play() のみを CallAfter で呼び出すようにしました。
Set 系のメソッドは直接描画に関与せず、
外部スレッド側でデータ更新 ⇒ メインスレッドで描画という流れになります。
ただし、ロックはしてないので別スレッドから上書きされる可能性はあります。
ここで一点訂正なのですが、以前、スレッドからCallLaterを使いと言ってた部分、
wxではスレッドからはタイマーを作成できませんでした。(他のGUIライブラリと混同していた)
可能ならCallLater,CallAfter等のタイマーではなく、
以前のコードで使われていた PostEvent を使い通知する方がより安全です。
<方法4>
動画再生側をmain側ではなく、別のメソッドとして外部に配置する方法を試みようとしましたが、外部メソッド実行じたいができませんでした。
これは簡単な例を使って説明します。
(他にも方法2, 3と同様の問題も有りますが、上で説明した通りです)
Python のクラスメソッドとインスタンスメソッドとの違いの為、
引数が正しく渡ってません。
class MyClass:
def __init__(self, value):
self.value = value
def add(self, num):
return self.value + num
obj = MyClass(10)
# 通常の呼び出し (インスタンスメソッド経由)
print(obj.add(20)) # => 30
# 直接呼出し (第一引数(self)のインスタンスを明示的に渡す必要があります)
print(MyClass.add(obj, 20)) # => 30
obj.add はインスタンス経由での呼び出し、インスタンスメソッドといって
第一引数のself が自動的に自分自身が束縛されているので、呼び出すときの引数に省略できます。
obj.add(obj, 10) とは書きません。MyClass.add はクラスMyClass に置かれた通常の関数です
通常の関数と同じように呼び出せますが、第一引数の self を明示的に要求します。
インスタンスを要求されますが、
インスタンスがあれば、そもそも通常インスタンスメソッドの呼び出しで良いので、
スコープ問題の解消として、この解決策は適してません。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.34%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2020/04/28 18:35
UpdateImageEvent, EVT_UPDATE_IMAGE = NewEvent()
ここの部分はUpdateImageEventとEVT_UPDATE_IMAGEを繋げているという解釈でいいのでしょうか。
そうでなければ下の⑥から⑦に指令が伝搬するコードの繋がりができなくなってしまうと解釈しています。
①win = MainWindow()
↓
②
thread = CountTimer(win) #Mainwindowのインスタンスを引数に入れて
thread.Start() # スタートさせて
↓
③
self.target = win # 代入して
↓
④
run # これは自動で実行されるのですね…
↓
⑤
event = UpdateImageEvent(filepath=filepath) #UpdateImageEventのイベントオブジェクト作成
↓
⑥
wx.PostEvent(self.target, event) # MainWindowでUpdateImageEventを実行指令
↓
⑦
self.Bind(EVT_UPDATE_IMAGE, self.OnUpdateImage) # bindされたOndataImage実行指令
↓
⑧
self.SetImageFile(event.filepath) #SetImageFile実行指令
↓
⑨
self.animeCtrl.Play() # 再生開始
2020/04/28 20:39
概ねその理解で合ってます。
> self.target = win # 代入して
釈明なのですが。ここは、変数の命名 "target" が紛らわしい名前でした。
(実行に支障はありません)
wxのイベントを送るターゲットとして付けたつもりでしたが、
pythonのThread も target という引数を持ちます。
run()メソッドをオーバーライドしない場合は、
コンストラクタに実行する関数をtargetとして渡すことができます。
> (4) run # これは自動で実行されるのですね…
Python のThread クラスの仕組みでそうなってます。
この辺りはオブジェクト指向のデメリット部分で、
中の仕組みを知らないと、ここでコードの流れが追えなくなるのが欠点。
明示的に Thread のtarget引数で関数を渡した方が良い場合もあります。
https://docs.python.org/ja/3/library/threading.html
----
一点だけ、
コードが実際に実行される順序は、(7) は事前準備なのでこの順序ではなく
(6) PostEvent で イベントキューに追加
-> (メインスレッド側で) MainLoop()
各イベント処理の中で -> (8) という流れになります。
(コードを追う流れとしては正解です。実行順とは違うというだけ)
意識して見てほしいのは、
それぞれのコードがどのスレッドで実行されているかという点。
(threading.current_threadやget_identを表示させて確かめて見て下さい)
====
最後に参考リソース
- 非同期プログラムでマルチスレッドが動かない場合と対処方法
https://rightcode.co.jp/blog/information-technology/python-asynchronous-program-multithread-does-not-work-approach
なぜマルチスレッドだとうまくいかないかの説明と、
別アプローチの方法ですが、
「マルチスレッドで画像を動画として再生してみる」をタイマーを使って実装されてます。
- 公式wikiより (英語)
NewEventやキューを使ったスレッド間通信のサンプルコード
https://wiki.wxpython.org/LongRunningTasks
この後良く問題になる事で、終了時にスレッドを安全に(エラーを出さず)
アプリケーションを終了させるにはどうするかという事があります。
説明を省いてしまったのですが、
上のコードではdaemon=True として、メインスレッド終了時に
同時に強制終了しています。が、ネットワーク等にアクセスしてる場合等は
daemon=Trueにして強制終了にはせず、正規の手続きで終了した方が良いことも有ります。
以前のスレッドのコードにはあった、self.keepGoing 等のフラグ判断で
スレッドのループを抜けたりする処理ですね。
wikiにあるサンプルコードでは "abort" として実装されてるので、
必要になったら参考にして見て下さい。
2020/04/28 22:08