見出し画像

【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つの関数に分解しなくてはいけません。

  1. 初期化関数。必須。処理要素をPythonならリスト、Luaならテーブルに入れて返す。

  2. 処理関数。必須。1.で準備した要素一つ一つに対する処理を記述する。map関数のような高階関数に渡す関数をイメージする。

  3. キャンセル時の追加実行関数。省略可能。

  4. 処理完了時の追加実行関数。省略可能。

一定回数で中断し、次回は同じところから再開するという仕様上、どうしてもこうやって分解しないといけません。ラッパー関数では渡された関数定義そのものを弄ることはできませんからね……。

でもコルーチン化するのと違って、中断と再開処理がコードのあちこちに分散しないのはメリットだと思います。
今回実現したいのはキャンセル処理で合って、マルチスレッド化ではありませんからね。マルチスレッドはあくまで手段の一種。


あとがき

というわけで、前回記事の改修版でした。

先日noteへの投稿を開始してから初めてのサポートも頂けましたし、今年はもっと頑張って、それに見合うような技術情報を投稿していきたいと思います。
あとBloggerへの移行ですね。これは新年の抱負になるのかな?

それではここまで読んでいただき、ありがとうございました。
新年の初めから地震が起きるなど幸先が悪いですが、それでも良い一年になりますように。

サポートしていただけるとその分の価値を提供できてるんだなって励みになります。