TypeScriptのアロー関数を用いて、DAZ Scriptのクリック処理を実装する

2019年1月18日DAZ ScriptDAZ Script, DAZ Studio, TypeScript

以前の記事、「ダイアログボックスに画像を表示する」では、ボタンクリックによる処理をクロージャを用いて実装しました。また、「DAZ Scriptにおける、イベント処理の実装方法」では、クロージャ以外のパターンのイベント処理について説明しました。

その中で3番目に説明したTypeScriptのアロー関数について、今回はもう少し詳しく説明しようと思います。

TypeScriptのアロー関数概要

JavaScriptでは変数に関数を入れることが出来ます。
通常、関数には名前をつけますが、このように変数へ直接格納する関数には、わざわざ名前をつけることはありません。このような関数を「無名関数」と呼びます。

TypeScriptのアロー関数は、この「無名関数」を明示的に示す機能です。
アロー関数の説明は、他に詳しい説明をしているサイトがありますので、そちらをご参照ください。

TypeScript入門 09: アロー関数式

この記事の「アロー関数」の項がわかりやすいです。

TypeScriptの機能と文法、まずはこの3つを押さえよう! 構造的部分型、ジェネリクス、アロー関数式

アロー関数を用いたときのメリットとして、

  1. 無名関数を明示的に示せる。
  2. ラムダ式の記法で無名関数を書ける。(functionなどを省略できる)
  3. コールバック関数のときにも、thisを束縛する。

が挙げられます。
この内、1と2は書き方の問題ですので、好みが分かれるところであまり重要でありませんが、3は最も重要な機能であり、これから説明するコールバック関数で大いに活躍してくれます。

DAZ Scriptでコールバック関数を使用したときの課題

DAZ Script、JavaScriptともに共通する課題なのですが、コールバック関数を用いたとき、関数内で使用するthisがグローバル変数を参照してしまい、オブジェクト内のプロパティにアクセスすることが出来ません。これについては「DAZ Scriptにおけるthisの挙動」で詳しく説明します。

これを回避するためのバッドノウハウとしてthisをいったん_thisという別の変数格納して、コールバックさせるなどの解決策が一般的に用いられています。
TypeScriptのアロー関数は、JavaScriptへの変換時に、このコールバック関数特有のバッドノウハウを含んだコードに変換してくれます。
これが、「3.コールバック関数のときにも、thisを束縛する」で説明している重要な機能です。

アロー関数で、イベント発生時に呼び出す関数(コールバック関数)を定義する

TypeScriptのclassとアロー関数を用いた具体例として、次のコードでコールバック関数を作ることが出来ます。

Chromeで実行できるJavaScriptで見てみましょう。

TypeScript版

let x = 1;
let y = 2;
class SwitchTimer {
    x: number;
    y: number;
    constructor(in_x: number, in_y: number) {
        this.x = in_x;
        this.y = in_y;
    }
    innerThis = (): void => {
        console.log(this);
    }
    inner = (): void => {
        console.log(this.x + this.y);
    }
    innerGlobal() :void {
        console.log(this.x + this.y);
    }
}
(function () {
    let timer1 = new SwitchTimer(3, 4);
    let timer2 = new SwitchTimer(5, 6);
    let timer3 = new SwitchTimer(7, 8);
    setTimeout(timer1.innerThis, 1000); //1秒後にthisの内容を表示
    setTimeout(timer2.inner, 3000); //3秒後に11を表示
    setTimeout(timer3.innerGlobal, 5000); //5秒後に3を表示(グローバル変数の計算結果)
})();

JavaScriptへ変換すると次のようになります。

var x = 1;
var y = 2;
var SwitchTimer = /** @class */ (function () {
    function SwitchTimer(in_x, in_y) {
        var _this = this;
        this.innerThis = function () {
            console.log(_this);
        };
        this.inner = function () {
            console.log(_this.x + _this.y);
        };
        this.x = in_x;
        this.y = in_y;
    }
    SwitchTimer.prototype.innerGlobal = function () {
        console.log(this.x + this.y);
    };
    return SwitchTimer;
}());
(function () {
    var timer1 = new SwitchTimer(3, 4);
    var timer2 = new SwitchTimer(5, 6);
    var timer3 = new SwitchTimer(7, 8);
    setTimeout(timer1.innerThis, 1000); //1秒後にthisの内容を表示
    setTimeout(timer2.inner, 3000); //3秒後に11を表示
    setTimeout(timer3.innerGlobal, 5000); //5秒後に3を表示(グローバル変数の計算結果)
})();

setTimeout関数はJavaScriptの標準ライブラリの機能で、第二引数に指定したミリ秒後に、第一引数に指定したコールバック関数を呼び出す関数です。

変換後のJavaScriptでは、コールバック関数に指定するinner関数内のthisが、別名の_thisに書き換わり、クラス内のプロパティ(ローカル変数)を参照しているになっていることがわかると思います。(_thisというただの変数経由でアクセスしているため、本来はプロパティにアクセスできているわけではありません。あくまでです。)

これにより、コールバック関数はグローバル変数を参照することがなくなり、期待通りそれぞれのコールバック関数で異なる計算結果を出力することができます。(timer1とtimer2)

アロー関数を使用しなかったとき(timer3のinnerGlobal関数)は、このような変換は行われず、thisをそのまま使用してしまいます。コールバック関数では、thisはグローバル変数変数を参照してしまうため、グローバル変数のxとyの計算結果「3」が出力されてしまいます。

ちなみにTypeScript側で、グローバル変数のxとyが無い状態でthisを省くと、コンパイルエラーになります。このため実際には、thisを省いたinnerGlobal関数のようなメソッドは書くことが出来ないと思ってください。

具体的にDAZ scriptで使用する

DAZ Scriptでもウィンドウ内のボタンクリックや、オブジェクトに変化などイベントが発生したときは、コールバック関数が呼び出されます。

次の例では、以前の記事「ダイアログボックスのボタンをクリックするたびに表示画像を切り替える」で作ったダイアログボックスをボタンを2つに改造し、それぞれのボタンをクリックしたときにXYZの画像を切り替えられるようにしてみます。

TypeScript版

class SwitchClass {
    label: DzLabel;
    xyz: Pixmap[];
    index: number;
    constructor(_label: DzLabel, _xyz: Pixmap[]) {
        this.label = _label;
        this.xyz = _xyz;
        this.index = 0;
    }

    //XYZ画像を切り替えるコールバック関数
    inner = (): void => {
        this.index++;
        this.index = this.index % this.xyz.length;
        this.label.pixmap = this.xyz[this.index];
    }
}

(function () {

    let backgroundpath = "C:/dazscript/images/local-user-product.png";
    let pix_X = "C:/dazscript/images/X.png";
    let pix_Y = "C:/dazscript/images/Y.png";
    let pix_Z = "C:/dazscript/images/Z.png";

    let dialog = new DzDialog;

    //ラベル要素(背景)
    let dialog_background = new DzLabel(dialog);
    dialog_background.pixmap = new Pixmap(backgroundpath);

    //ラベル要素(XYZ画像)
    let pix_XYZ = [
        new Pixmap(pix_X),
        new Pixmap(pix_Y),
        new Pixmap(pix_Z)
    ];

    //ボタン要素
    let dialog_button1 = new DzPushButton(dialog);
    dialog_button1.text = "switchXYZ1";
    dialog_button1.x = 0;
    dialog_button1.y = 0;

    let dialog_button2 = new DzPushButton(dialog);
    dialog_button2.text = "switchXYZ2";
    dialog_button2.x = 0;
    dialog_button2.y = 60;

    let pix_XYZ1 = new DzLabel(dialog)
    pix_XYZ1.pixmap = pix_XYZ[0];
    pix_XYZ1.x = 100;
    pix_XYZ1.y = 0;

    let pix_XYZ2 = new DzLabel(dialog)
    pix_XYZ2.pixmap = pix_XYZ[0];
    pix_XYZ2.x = 100;
    pix_XYZ2.y = 60;

    //XYZ画像を切り替えるコールバック関数を指定
    let switch_xyz1 = new SwitchClass(pix_XYZ1, pix_XYZ);
    let switch_xyz2 = new SwitchClass(pix_XYZ2, pix_XYZ);

    connect(dialog_button1, "clicked()", switch_xyz1.inner);
    connect(dialog_button2, "clicked()", switch_xyz2.inner);

    //ダイアログ設定
    dialog.width = dialog_background.pixmap.width
    dialog.height = dialog_background.pixmap.height;

    dialog.exec();

})();

JavaScript版

var SwitchClass = /** @class */ (function () {
    function SwitchClass(_label, _xyz) {
        var _this = this;
        //XYZ画像を切り替えるコールバック関数
        this.inner = function () {
            _this.index++;
            _this.index = _this.index % _this.xyz.length;
            _this.label.pixmap = _this.xyz[_this.index];
        };
        this.label = _label;
        this.xyz = _xyz;
        this.index = 0;
    }
    return SwitchClass;
}());
(function () {
    var backgroundpath = "C:/dazscript/images/local-user-product.png";
    var pix_X = "C:/dazscript/images/X.png";
    var pix_Y = "C:/dazscript/images/Y.png";
    var pix_Z = "C:/dazscript/images/Z.png";
    var dialog = new DzDialog;
    //ラベル要素(背景)
    var dialog_background = new DzLabel(dialog);
    dialog_background.pixmap = new Pixmap(backgroundpath);
    //ラベル要素(XYZ画像)
    var pix_XYZ = [
        new Pixmap(pix_X),
        new Pixmap(pix_Y),
        new Pixmap(pix_Z)
    ];
    //ボタン要素
    var dialog_button1 = new DzPushButton(dialog);
    dialog_button1.text = "switchXYZ1";
    dialog_button1.x = 0;
    dialog_button1.y = 0;
    var dialog_button2 = new DzPushButton(dialog);
    dialog_button2.text = "switchXYZ2";
    dialog_button2.x = 0;
    dialog_button2.y = 60;
    
    var pix_XYZ1 = new DzLabel(dialog);
    pix_XYZ1.pixmap = pix_XYZ[0];
    pix_XYZ1.x = 100;
    pix_XYZ1.y = 0;
    var pix_XYZ2 = new DzLabel(dialog);
    pix_XYZ2.pixmap = pix_XYZ[0];
    pix_XYZ2.x = 100;
    pix_XYZ2.y = 60;
    //XYZ画像を切り替えるコールバック関数を指定
    var switch_xyz1 = new SwitchClass(pix_XYZ1, pix_XYZ);
    var switch_xyz2 = new SwitchClass(pix_XYZ2, pix_XYZ);
    connect(dialog_button1, "clicked()", switch_xyz1.inner);
    connect(dialog_button2, "clicked()", switch_xyz2.inner);
    //ダイアログ設定
    dialog.width = dialog_background.pixmap.width;
    dialog.height = dialog_background.pixmap.height;
    dialog.exec();
})();

実行結果は次のようになります。

2つのボタンそれぞれをクリックしたとき、対応する画像が切り替わることを確認できると思います。

SwitchClassオブジェクトには、newで生成時に表示画像の配列(pix_XYZ)、操作対象のラベル(pix_XYZ1pix_XYZ2)を渡し、内部のプロパティに保持させています。また、現在何番目の画像を表示しているかをindexで保持しています。

これが_thisに格納され、クリックするたびに呼び出され、画像が順次切り替わるようになっています。

50行目、51行目のvar switch_xyz1 = new SwitchClass( pix_XYZ1, pix_XYZ)が該当箇所です。それぞれインスタンスを生成し、各ボタンがそれぞれのコールバック関数を呼べるようにしています。

図示すると以下のようになります。

これにより、別々のメモリ空間でindexを保持できるようになり、それぞれで画像を切り替えることができるようになります。

まとめ

DAZ Scriptでコールバック関数を実装するときは、JavaScriptのときと同様、thisの参照先に気をつける必要があります。これについては「DAZ Scriptにおけるthisの挙動」で解説しています。

そのノウハウはJavaScriptで養われたものがそのまま使えますので、解決の仕方はケースバイケースです。

しかし、TypeScriptを用いて開発ができる環境があるのならば、今回示したようにコールバック関数に指定する関数はアロー関数で実装し、それ以外の関数は通常のクラス内関数で実装するのが、もっとも最適な設計になると思います。

なお途中でも補足しましたが、このアロー関数が必要になるのは、コールバック関数に関数を指定するときだけです。通常の関数呼び出しで用いるのであれば、クラス内関数としての実装で問題ありません。

Posted by lowpolysnow