【DaVinci Resolve API】続・長い処理を途中でキャンセルする処理の実装方法
まえがき
あけましておめでとうございます。
火注ゆかなです。
新年早々ですけど、前回投稿した処理を途中でキャンセルする処理の実装方法の別のやり方を思いつきました。
対象の処理を2つ~4つの関数に分解しなくてはいけない等の制約は付きますが、コルーチン化しなくて良いので気に入ってます。
マルチスレッド全然慣れてないので、コルーチン化するあたりの記述を読むの面倒なんですよね。
改修バージョン
Python版コード
import sys
sys.path.append("C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting\Modules")
import DaVinciResolveScript as bmd
if __name__ == '__main__' :
# GUI設定
resolve = bmd.scriptapp('Resolve')
fusion = resolve.Fusion()
ui = fusion.UIManager
disp = bmd.UIDispatcher(ui)
win = disp.AddWindow({
'ID' : "MyWindow",
'WindowTitle' : 'My First Window',
'Geometry' : [ 100, 100, 500, 150 ],
'Spacing' : 10
},
[
ui.VGroup({},
[
ui.HGroup({ 'Weight': 0 }, [
ui.Button({
'ID' : "btnStart",
'Text' : "Start",
}),
ui.Button({
'ID' : "btnCancel",
'Text' : "Cancel",
})
])
])
])
itm = win.GetItems()
# イベント:ウィンドウクローズ
def OnClose(ev):
disp.ExitLoop()
def CancellableEventWrapper(widget, event_name:str, init_func, execute_func, cancel_func=None, complete_func=None, step:int=10, button_disabled:bool=True) :
values = None
cancel = False
# 実行関数
def execute_handler(ev) :
# 内部変数cancelがTrueの場合、処理を中断
nonlocal values
nonlocal cancel
if cancel :
print(f'{widget["ID"]}.{event_name} is canceled.')
if cancel_func is not None :
cancel_func()
values = None # ここでvaluesを初期化しなければ、次回ボタンクリック時に続きから実行できる
cancel = False
if button_disabled : # 実行中のボタン無効化設定が有効なら、元に戻す
widget['Enabled'] = True
return
#ev.infoが空ならボタンクリックによるものと判断し、初期値設定を行う
if values is None:
values = iter(init_func()) # イテレータにして取得
if button_disabled : # 実行中のボタン無効化設定が有効なら、無効化する
widget['Enabled'] = False
# 要素に対して指定回数分を処理
count = 0
v = next(values, None)
while v is not None :
execute_func(v) # 要素に対する処理
count += 1
if count >= step :
break
# 全ての要素を処理しきったら終了
else :
print(f'{widget["ID"]}.{event_name} is completed.')
values = None # イテレータを全て処理したらNoneになるとすれば、これは要らないかも
if complete_func is not None :
complete_func()
if button_disabled : # 実行中のボタン無効化設定が有効なら、元に戻す
widget['Enabled'] = True
return
# 処理しきれなかったら次回分を予約
widget.QueueEvent(event_name, {'continue':True}) # 適当な引数を与えないと何故か実行されない模様
# キャンセル用関数
# 引数に'cancel'を追加してイベントキューに突っ込むだけ。キーの有無で判別するので値は何でも良い
def cancel_handler(ev) :
nonlocal cancel
cancel = True
return (execute_handler, cancel_handler)
# ロングプロセス初期化(イテラブルな要素を返却すること)
def long_process_init_values() :
return range(10)
# 要素処理関数
def long_process_execute(v) :
print(f'{v} sec')
bmd.wait(1.0) # 一秒待機
# 実行用関数とキャンセル用関数取得
# 各種イベントハンドラに関数設定
win.On.MyWindow.Close = OnClose
win.On.btnStart.Clicked, win.On.btnCancel.Clicked = CancellableEventWrapper(itm['btnStart'], 'Clicked', long_process_init_values, long_process_execute, step=1, button_disabled=True)
# GUIウィンドウ表示、タイマー起動
win.Show()
print('Window Show')
disp.RunLoop()
Lua版コード
こちらはWeSuckLessスレッドへの返信用に組み直したものなので、コメントは英語です。
やってることはPython版と一緒。
local ui = fusion .UIManager
local disp = bmd.UIDispatcher(ui)
-- original function
local some_long_process = function ()
print (' beginning long process')
for i = 1, 10 do
if cancel then
print('long process cancelled')
return
end
print ('executing: ', i)
bmd.wait(1)
end
print ('ending long process')
return
end
-- (1) Function to return a table with numerical values as keys.
local some_long_process_init = function()
print('beginning long process')
return {1,2,3,4,5,6,7,8,9,10}
end
-- (2) Functon that is passed as an argument to a higher-order function.
local some_long_process_execute = function(v)
print('executing:' .. v)
bmd.wait(1)
end
-- (3) Runs when cancelled.
local some_long_process_cancel = function()
print('long process cancelled')
return
end
-- (4) Runs when completed.
local some_long_process_complete = function()
print('ending long process')
return
end
local function cancellable_event_wrapper(widget, event_name, init_func, execute_func, cancel_func, complete_func, step, widget_disable)
local step = step or 1
local widget_disable = widget_disable or true -- Whether the widget should be disabled during event execution.
local values = nil
local index = nil
local cancel = false
local execute_handler = function(ev)
-- Aborts if the internal variable CANCEL is true.
if cancel then
print('canceled')
if cancel_func ~= nil then
cancel_func()
end
values = nil -- If you do not change the internal variable 'values' to nil here, you can resume from where you left off next time.
cancel = false
if widget_disable then
widget.Enabled = true
end
return
end
-- If the internal variable values is nil, the initial value is set.
if values == nil then
values = init_func()
index = 1
if widget_disable then
widget.Enabled = false
end
end
-- repeat or the specified number of times.
local count = 0
print('index : ' .. index)
print('length : ' .. #values)
print('index+step : ' .. index + step)
for i = index, #values do
execute_func(values[i])
count = count + 1
if count >= step then break end
end
-- Finish when all elements have been processed.
index = index + step
if index > #values then
print('finished')
if complete_func ~= nil then
complete_func()
end
values = nil
if widget_disable then
widget.Enabled = true
end
return
end
print()
-- If all elements are not fully processed, the next processing is reserved.
widget:QueueEvent(event_name, {next=true}) -- QueueEvent did not work without a second argument, so pass unused keys and values.
end
local cancel_handler = function(ev)
print('cancel click')
cancel = true
end
return {execute=execute_handler, cancel=cancel_handler}
end
local function my_window()
local width,height = 200, 50
local win = disp:AddWindow({
ID = "my_window",
WindowTitle = "My Window",
WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
Geometry = {100, 100, width, height},
Spacing = 10,
Margin = 20,
ui:VGroup{
ID = 'root',
Weight = 0,
ui:HGroup{
Weight = 0,
ui:Button{
ID = "start",
Text = "Start",
},
ui:Button{
ID = "cancel",
Text = "Cancel",
},
},
},
})
win:RecalcLayout()
handlers = cancellable_event_wrapper(
win:Find('start'),
'Clicked',
some_long_process_init,
some_long_process_execute,
some_long_process_cancel,
some_long_process_complete,
1
)
function win.On.my_window.Close(ev)
handlers.cancel()
disp:ExitLoop()
end
win.On.start.Clicked = handlers.execute
win.On.cancel.Clicked = handlers.cancel
return win
end
local my_window = my_window()
my_window:Show()
disp:RunLoop()
my_window:Hide()
やってることの説明
まず、前提条件としてキャンセルしたい処理とはどういう処理か? を決めておきます。
そういう処理は大体ループ処理を含んでます。なので、1回の実行は一瞬で終わるけど、何十、何百回と繰り返すと時間がかかる処理だと仮定します。
なので、処理関数を一度呼び出したら10回ぐらいやって中断。他のイベント処理などが終わったら続きから再開する……と繰り返してやれば、他のGUI操作なども受け付けることができます。
中断はともかくどうやって再開するのかという点ですが、GUIウィジェットにはQueueEventというメソッドがあります。これは例えば「ボタンをクリックする」というGUIイベントをスクリプト側から任意のタイミングで追加できるメソッドです。
これを中断する前に実行してやれば、次回の処理を予約できます。
あとはラッパー関数に対象の関数を渡してあげると、「処理の開始」「処理のキャンセル」という2つのイベントハンドラ関数を返すように実装したのが上記のコードです。
と書くとかなり楽なのですが、デメリットとして対象の処理を下記のように2つ~4つの関数に分解しなくてはいけません。
初期化関数。必須。処理要素をPythonならリスト、Luaならテーブルに入れて返す。
処理関数。必須。1.で準備した要素一つ一つに対する処理を記述する。map関数のような高階関数に渡す関数をイメージする。
キャンセル時の追加実行関数。省略可能。
処理完了時の追加実行関数。省略可能。
一定回数で中断し、次回は同じところから再開するという仕様上、どうしてもこうやって分解しないといけません。ラッパー関数では渡された関数定義そのものを弄ることはできませんからね……。
でもコルーチン化するのと違って、中断と再開処理がコードのあちこちに分散しないのはメリットだと思います。
今回実現したいのはキャンセル処理で合って、マルチスレッド化ではありませんからね。マルチスレッドはあくまで手段の一種。
あとがき
というわけで、前回記事の改修版でした。
先日noteへの投稿を開始してから初めてのサポートも頂けましたし、今年はもっと頑張って、それに見合うような技術情報を投稿していきたいと思います。
あとBloggerへの移行ですね。これは新年の抱負になるのかな?
それではここまで読んでいただき、ありがとうございました。
新年の初めから地震が起きるなど幸先が悪いですが、それでも良い一年になりますように。
サポートしていただけるとその分の価値を提供できてるんだなって励みになります。