省型旧形国電の残影を求めて

戦前型旧形国電および鉄道と変褪色フィルム写真を中心とした写真補正編集の話題を扱います。他のサイトでは得られない、筆者独自開発の写真補正ツールや補正技法についても情報提供しています。写真補正技法への質問はコメント欄へどうぞ

イベントリスナーを用いた ImageJ 双方向ユーザインターフェースサンプルプログラム

 ImageJ / Python プラグイン・ユーザインターフェース作成のお勉強として、イベントリスナーを使った双方向型ダイアログのサンプルプログラムを作ってみました。下記のように、ダイアログにスライドバー、チェックボックス、数値入力ボックス、ドロップダウンメニューが2つずつ並んでいます。それぞれ、上のダイアログ要素を動かすと、同じ種類の下の要素 (Copy 〜 と書かれているもの)がそれに連動して動く、というものです。

ダイアログ

 ImageJ / Python 上で双方向型ダイアログを作るには、基本的に Python (Jython) から JavaAPIを呼び出して使います。以下のプログラムですが、メインはダイアログを呼び出す1行のみ、そして、実質的なメインであるダイアログを定義する部分が def scaleImageUI() 、それに対して、リアルタイムに表示を動かすために、DialogPreviewer というクラスを定義して、こちらに、Javaの各イベントリスナーを読み込んでいます。

 イベントリスナー関連の要素はすべて Java の要素なので、どのようなメソッドやプロパティがあるのかを調べるためには、 ImageJのAPIリファレンスを見るのではなく、Java のリファレンスを調べる必要があります。

 また、このプログラムは上の要素の動作を感知して、下の同じ要素を上の要素に連動させるというものなので、プレビューアーを仕掛けるのは、それぞれ上の要素のみでOKです。どの要素にプレビューアーを仕掛けるかの定義は、scaleImageUI で行っています。

 プログラムの例示の下に、解説を付します。

-------------

# Interactive UI sample with event listeners    
    
from ij import IJ, ImagePlus 
from ij.gui import GenericDialog    
from ij import WindowManager as WM    
from java.awt.event import AdjustmentListener, ItemListener, TextListener, KeyListener    
    
class DialogPreviewer(AdjustmentListener, ItemListener, TextListener, KeyListener):    
    def __init__(self, slider, checkbox, slider2, checkbox2, \
        nfield, nfield2, choice1, choice2):    
        """ 
             slider, slider2: a java.awt.Scrollbar UI element 
             checkbox: a java.awt.Checkbox 
             nfield: a java.awt.TextField
             choice: a java.awt.Choice
        """    
        self.slider = slider    
        self.checkbox = checkbox    
        self.slider2 = slider2    
        self.checkbox2 = checkbox2    
        self.nfield = nfield    
        self.nfield2 = nfield2    
        self.choice1 = choice1    
        self.choice2 = choice2    

        
    def adjustmentValueChanged(self, event):    
        """ event: an AdjustmentEvent with data on the state of the scroll bar. """    
        preview = self.checkbox.getState()
        print "check preview", str(preview)
        print str(self.slider.getValue())
        if preview:    
            if event.getValueIsAdjusting():    
                return # there are more scrollbar adjustment events queued already    
            print "Scaling to", event.getValue() / 100.0
            slider_value = self.slider.getValue()    
            self.slider2.setValue(slider_value)    
            return

    def itemStateChanged(self, event):    
        """ event: an ItemEvent with data on what happened to the checkbox. """    
#        if event.getStateChange() == event.SELECTED:
        check_state = self.checkbox.getState() 
        choice_item  = self.choice1.getSelectedItem()   
        print "itemStateChanged chekbox: ", str(check_state)
        print "itemStateChanged choice: ", str(choice_item)
        self.choice2.select(choice_item)    
        textStrings = self.nfield.getText()
        print "itemStateChanged textStrings: ", textStrings
        self.nfield2.setText(textStrings)    
        return

    def textValueChanged(self, event):
        textStrings = self.nfield.getText()
        print "Text Changed", textStrings
        self.nfield2.setText(textStrings)    
        return

def scaleImageUI():    
    gd = GenericDialog("Scale")    
    gd.addSlider("Scale", 1, 200, 100)    
    gd.addCheckbox("Checkbox", True)    
    gd.addSlider("Copy Scale", 1, 200, 100)    
    gd.addCheckbox("Copy Checkbox", True)    
    gd.addNumericField("Input numeric", 0.0)
    gd.addNumericField("Copy Input numeric", 0.0)
    choice = ["1","2"]
    gd.addChoice("Select Choice", choice, choice[0])
    gd.addChoice("Copy Select Choice", choice, choice[0])

    # The UI elements for the above inputs    
    slider = gd.getSliders().get(0)     
    checkbox = gd.getCheckboxes().get(0)     
    slider2 = gd.getSliders().get(1)     
    checkbox2 = gd.getCheckboxes().get(1)     
    # Attention! Sliders has also NumericFields, so attention to index number for NFields! 
    nfield = gd.getNumericFields().get(2) 
nfield2 = gd.getNumericFields().get(3) choice1 = gd.getChoices().get(0) choice2 = gd.getChoices().get(1) previewer = DialogPreviewer(slider, checkbox, slider2, checkbox2, nfield, nfield2, choice1, choice2) slider.addAdjustmentListener(previewer) checkbox.addItemListener(previewer) nfield.addTextListener(previewer) # nfield.addKeyListener(previewer) choice1.addItemListener(previewer) gd.showDialog() if gd.wasCanceled(): print "User canceled dialog!" scaleImageUI()

-----------------------

 まず、冒頭で各種ライブラリを読み込みます。ImageJのみならずJavaのライブラリも読み込んでいます。

 次に、class DialogPreviewer ですが、引数として先程読み込んだ Java のイベントリスナー4つを取ります。なおここではKeyListnerを読み込んでいますが、あとのプログラムでは使っていません。

 その後にこのクラスのコンストラクタが来ますが、こちらに、scaleImageUI から呼び出す時の引数を定義します。この中で self が定義され、かつコンストラクタで読み込んだ引数の頭に、self. をつけて定義し直されていますが、これはコンストラクタで読み込んだ引数をクラス内で、いわばセミグローバル変数として使えるようにするためのおまじないです。

 このあと、3つの event を引数に取る関数 (→このクラスのメソッドになります) が定義されています。これらは、リスナーで感知したイベントに対応する動作を定義する関数です。ここでの関数名は、任意につけることができません。かならずイベントリスナーに定義されている所定のイベント名に合わせる必要があります。各イベントリスナーにどのような機能でどのような名前のイベントがあるかは、Java の各イベントリスナーのレファレンスをご参照下さい。

 この関数のうち、例えば itemStateChanged の場合、ダイアログ上の各要素になにか変化があった場合に発生するイベントで、この中では、チェックボックスと、ダウンロードリストの内容を取得したら、それぞれ同じ種類の2番めの要素に、その取得内容を設定する、ということを定義しています。

 イベントに対応した関数の最後には return がありますが、このリターン文には戻り値を設定してはいけません。また return 文を省略すると、この関数で与えた設定が実際のダイアログボックスの表示に反映されない場合もあるようです(一部省略しても反映される動作もありますが...)。

 scaleImageUI はダイアログを定義する部分です。以下イベントリスナー関連に絞って解説します。

  # The UI elements for the above inputs 以下の部分が双方向関連の定義になります。ここで各要素を変数に取得していますが、ここでの取得方法、例えば

    nfield2 = gd.getNumericFields().get(3)

と、通常のダイアログからのデータ取得方法

    nfield2 = gd.getNextNumber()

とでは、どう違うのでしょうか?

 前者の方法では、nfield2には、オブジェクトとして取得され、後者の方法では、データ(数値)として取得されます。しかも前者の取得方法では、基本的に ImageJではなくJavaのオブジェクト、この場合は、JavaのTextFieldオブジェクトして取得されます。従って、ダイアログをNumericFieldに入力された値に応じて、変化させたい場合は、JavaのTextFieldからデータを取得して、それに基づいて動かさなければなりません。しかも、Javaのオブジェクトなので、ImageJのメソッドやプロパティが使えず、Javaのメソッドやプロパティを探してデータを取得することになります。この辺りについては、以下の記事に書きました。

yasuo-ssi.hatenablog.com

 また、要注意点として、このサンプルの場合スライダーを使っていますが、スライダーの数値表示欄も NumericField になってしまいます。従って、input numeric とある数値入力欄は3番目(インデックス番号は2)、Copy input numeric とある欄は4番目(インデックス番号は3)のNumericField になります。最初それに気づかず、いくらこの欄に数値を入れても何も動作せず、原因を探すのにしばらく時間を無駄にしました。

 またスライダーにはもう一点要注意点があり、ImageJ上では(ImageJのオブジェクトとしては)、スライダーの値に小数を用いることができます。しかし、それをイベントリスナー上で扱うために、Javaのオブジェクトとして把握すると、Java の scrollbar オブジェクトとなります。ところがscrollbarオブジェクトでは小数の値を取ることができず値が整数であることが要求されます。したがって、双方向型UIでスライダーを扱う場合は必ず値を整数にする必要があります。

[※2023.7追記]
 パラメータをスライダーから読み込むと上記のように整数しか扱えず、しかも入力したままの数値で使えず不便ですので、スライダー付属の Numeric Field から読み込むようにしたほうが良いです。

 その後、DialogPreviewerをインスタンス化した previewer を使って、変化を監視する必要のあるダイアログ要素 (このプログラムの場合は各種類のダイアログ要素のうち、最初の要素) のみに各イベントリスナーを add〜Listener メソッドを使って設置します。なお、監視の必要がなくなった場合は、remove〜Listener メソッドを使います。このプログラムでは、単なるサンプルなのでここまでで、その後のコードが省略されていますが、本来のダイアログなら、このあと、各ダイアログ要素からデータを変数に取得するコードが入り、それが終了した後、remove〜Listener メソッドで監視を終了させることになります。

 また、上でみたように、同じオブジェクトでもImageJオブジェクトとして把握 (get) するのと、Javaオブジェクトとして把握するのではオブジェクトの性質に齟齬が出る可能性があります。しかしイベントリスナー上で扱おうとすると、各オブジェクトをその両面で把握する必要があり、そのあたりを考えたプログラミングが要求されます。

 

 これらの要素を使ったプログラミングは、Javaでのプログラミングサンプルは結構ネット上にありますが、Pythonから呼び出したプログラミングサンプルは殆ど無いので、ご参考いただければと。