2020年1月2日DAZ ScriptDAZ Script,DAZ Studio,TypeScript

最初に結論(tl;dr)

DAZ Scriptはthisの挙動もjavaScriptと同じですが、以下の場合においてのみ挙動が異なります。

コールバック関数内でthisを使うとグローバル変数を参照するが、このときDAZ Scriptではグローバル変数には誰もいないことにされる。

以上です。

グローバル変数上にvar x = 0;などを宣言していても、コールバック関数からはこれを参照することが出来ません。

詳しい説明は「thisで指定される領域を保持する」の項を参照してください。

概要

DAZ Scriptはthisの挙動もjavaScriptと同じです。
JavaScriptのthisの挙動は以下の記事が参考になります。

JavaScriptにおける関数の呼び出し方のパターンとそれに伴う関数内のthisの変化

大規模なプログラムが作られることが無いDAZ Scriptでは、thisの出番は少ないかもしれませんが、イベント時に呼び出すコールバック関数では用いる機会もでてくると思います。

通常の呼び出しではJavaScriptと違いはありませんが、「最初に結論(tl;dr)」で述べたように、クリック処理などコールバック関数に指定した時、若干挙動が異なります。

参考に挙げた記事に書かれている4パターンをDAZ Scriptで検証してみます。

  • パターン1.関数として呼び出す
  • パターン2.オブジェクトのメソッドとして呼び出す
  • パターン3.コンストラクタとして呼び出す
  • パターン4.apply()、もしくはcall()で呼び出す

続いて、コールバック関数に指定した時の挙動も検証してみたいと思います。

  • パターン5.コールバック関数として呼び出す(「1.関数として呼び出す」と同じ)
  • パターン6.コールバック関数として呼び出す(「2.オブジェクトとして呼び出す」と同じ)

また、TypeScriptを用いての解決方法も解説したいと思います。

通常の関数呼び出しのthisの挙動を検証する

次の項より、JavaScriptとDAZ Scriptでthisの挙動を検証していきます。

左がJavaScriptをChromeで実行した結果で、右がDAZ StudioのScript IDEペインで実行した結果です。

パターン1.関数として呼び出す

このパターンでは、thisを指定したときはグローバル変数を取得しています。正確に言うと、JavaScriptではWindowオブジェクトを参照します。これがJavaScript環境においてグローバル変数に相当するものです。
DAZ Script側には、Windowオブジェクトに相当するものがないため、エラーとなってしまいます。

パターン2.オブジェクトのメソッドとして呼び出す

パターン1と異なり、thisを指定したときは、example2オブジェクトを取得していることに注目してください。このため、this.xとすると、example2オブジェクト内のプロパティにアクセスすることが出来ます。

逆にthisを指定しなかったときは、グローバル変数領域のxを参照しようとしますが、グローバル変数にはxがないためエラーとなっています。

これはJavaScript、DAZ Scriptともに共通の動作です。

パターン3.コンストラクタとして関数を呼び出す

DAZ Scriptでは使う機会は無いかもしれませんが、コンストラクタなどもJavaScriptと同様に使うことが出来ます。

パターン1のときとコードはほぼ同じなのですが、JavaScriptではthisを指定したときにグローバル変数ではなくExample3オブジェクトを指し示します。

これはコンストラクタが特殊な呼ばれ方をするためです。関数をコンストラクタとして使用するには、newキーワードで関数を呼び出します。

DAZ Scriptも同様にthisを指定したときの挙動が異なりますが、内部の処理がわからないため、これ以上の解説はできません。すみません…。

ただ参考記事の方で、

コンストラクタ呼び出しでは新しいオブジェクトが生成されます。

なので、上記のコードを実行させるとコンソールにはexample {}と新しく生成された空のオブジェクトが入ります。

ちなみに本題には関係ないのですが、JavaScriptにおけるコンストラクタには
・呼び出されると新しい空のオブジェクトを生成する
・関数に明示的な返り値がなければ、その新しいオブジェクトがコンストラクタの値として返される
という特徴があり、普通の関数とは明確に違う使われ方をします。

とありますので、print(this);の実行結果の[Objects Object]は、空のExample3オブジェクトを指し示していると思われます。

その後の処理のprints(this.x);がundefinedとなっているのも、まだこの生成されたばかりの空のExample3オブジェクトにxプロパティが無いためと思われます。

パターン4.apply()、もしくはcall()で呼び出す

こちらもDAZ Scriptでは使う機会が無いと思いますが、紹介しておきます。

パターン1と同じですね。

コールバック呼び出しでのthisの挙動を検証する

パターン5.コールバック関数として呼び出す(「パターン1.関数呼び出し」と同じ)

今回解説したかったのは、このコールバック関数のパターンです。 QtScriptに由来するためか、DAZ Scriptはコールバック関数での挙動がJavaScriptと若干異なります。

この差異が際立つのは、DAZ Scriptのコールバック関数がグローバル変数にアクセスしようとしたときです。
例えば、グローバル変数を用意していない下記の例では特に違いがありません。

しかし、グローバル変数を追加して、実行してみると、JavaScriptとDAZ Scriptで実行結果が異なっているのがわかります。

JavaScriptではthisを指定するとグローバル変数に定義したxとyを呼び出します。(これが直感的に意図した動作かどうかは置いといて…)

DAZ Scriptでは依然NaNと表示され、グローバル変数のxとyは存在しないかのような扱いになっています。

パターン6.コールバック関数として呼び出す(「パターン2.オブジェクトのメソッドとして呼び出す」と同じ)

続いて、さらに別の方法でコールバック関数を指定してみます。
このパターン6では、パターン2と同様にオブジェクトのメソッドを用意し、それをコールバック関数として指定しています。

まず、グローバル変数無し版です。

続いて、グローバル変数を追加して実行してみます。

パターン5と同じ様に、DAZ Scriptの方はグローバル変数のxとyを探し出せず、NaNとなっています。

このように、DAZ ScriptとJavaScriptでは、コールバック関数にthisを使ったときの挙動が若干異なります。

しかしながら、通常thisを使うシーンではこのような使い方をすることはないと思います。

JavaScriptでもそうですが、thisを用いるときはインスタンスを模した挙動を実現したいときです。そのため、後述するように_this変数を用いて、変数を束縛してコールバックするテクニックが活用されることが多いと思います。

コールバック時にもthisを扱えるようにしたい(パターン6改定)

thisは何が嬉しいのか?

JAVAなどのオブジェクト指向言語のthisと同じ様な使い方できるのが、JavaScriptのthisの使い所です。

JAVAなどのオブジェクト指向の言語では、thisは自身のインスタンスのプロパティへアクセスするために用いられます。
JavaScriptにもthisキーワードはありましたが、本来の目的は異なっていました。[1]オブジェクト参照のためのthisの使用しかし、使い方によってはオブジェクト指向のものと同じような使い方ができることがわかり、今日ではそちらの使われ方のほうが主流となっています。

ところが、そもそもクラスがないJavaScriptでは、thisは使用する場所や使い方によって、指定されるオブジェクトが大きく異なるという問題が発生してしまいました。これはバグの温床となり、JavaScript開発者の頭を悩ませました。

ES2015以降はかなり改善され、JavaScript界隈では問題視されることも少なくなりましたが、DAZ Scriptでは依然古いECMA Script(gen3)に準拠した言語仕様となっているため、この問題が避けて通れないものとなっています。

thisで指定される領域を保持する

パターン6の例では、「オブジェクトのメソッド」に処理を格納しています。
その後、その「オブジェクトのメソッド」をコールバック関数にしています。

パターン2で示したように、本来this.xSwitchTimer2.xを示し、xはグローバル変数のxを示します。 インスタンスを模すことができ、ここがまさにthisの本来の使い所なのですが、コールバック関数にすると、同じオブジェクト内のxyには、もはやアクセスすることができなくなってしまいました。

これは困りました。わざわざオブジェクトを用意した意味がありません。

そこで、コールバック関数を指定するときに同じオブジェクト内のプロパティをいったん_thisのような変数に格納しておいて、コールバック時にもその変数経由でプロパティにアクセスするテクニックがJavaScriptではよく用いられます。

具体的には下記のようになります。(ソース名:「パターン6改定」)

このテクニックを使うと、そもそものコールバック関数の指定方法も変わり、「関数を生成する関数を実行し、生成された関数をコールバック時に呼び出されるように指定する」というものになります。

早口言葉のようでややこしいのですが、「パターン6改定」のDAZ Script側で35行目connect( oTimer2, "timeout()", SwitchTimer2.getInner() );としている部分がそうです。関数名を指定をしていたパターン6から、関数の実行に変わっています。また、関数の名称もgetプレフィックスが追加され、パターン6で言うところのinner関数を取得する趣旨の名称になっています。

this.xでそのままSwitchTimer2.xにアクセスしようとしたパターン6と、_this変数を用いてthisの内容をその変数経由でアクセスするようにした「パターン6改定」の概念を図にして比べてみます。

【パターン6で起こっていること】

【「パターン6改定」で起こっていること】

そもそも、コールバック時にはそれまでとは別の世界のinner関数が実行されていると思ってください。
その世界にはSwitchTimer2オブジェクトはもはや居ません。これがコールバック関数でSwitchTimer2.xにアクセスすることができない理由です。 もはやこれは、thisが云々とかは関係のない話です。

_this変数を用いることで、SwitchiTimer2にあったxとyを「コールバック時に参照される世界」にまで持ってくることができます。これによってコールバック関数でxとyにアクセスすることが出来るようになります。 (SwitchTimer2そのものは「コールバック時に参照される世界」には持ってこれないので、残念ながらSwitchTimer2.xといった形式ではアクセスできません。代わりに_this.xと言った形式でアクセスすることになります。)

ちなみに黄色の吹き出しは、DAZ Script独特の現象を説明しています。

JavaScriptではコールバック関数でthisを用いたとき、「通常のスクリプト実行時に参照される世界」と同じグローバル変数を参照しますが、DAZ Scriptではそれとは異なる「コールバック時に参照される世界」のグローバル変数を参照しようとするようです。

このため、DAZ Scritptではコールバック関数でグローバル変数を参照できません。
これがパターン5とパターン6で発生していた、

グローバル変数のxとyを探し出せず、NaNとなっています。

の原因です。

TypeScriptを活用してみる

以前の記事「TypeScriptのアロー関数を用いて、DAZ Scriptのクリック処理を実装する」でも示したとおり、このややこしいthisの挙動は、TypeScriptのアロー関数を活用することでだいぶ緩和できます。

ちょうど上で紹介した_this = this;のように、変数を用いてコールバック関数にオブジェクト内のプロパティに渡すテクニックを、自動で実装してくれるためです。

TypeScriptでは、classとアロー関数を活用することで、パターン6のような構成の関数を作ることが出来ます。

なお、アロー関数で示すthisとJavaScriptのthisは、見た目は同じですが厳密にはその機能は異なります。 あくまでクラスを使った時に、インスタンスとしてアクセスしたい時に使うのがTypeScriptのthisです。オブジェクト指向言語のthisそのものです。
JavaScriptのthisは、自身のオブジェクトを参照するときに使います。これはJavaScript独特の設計です。
[2]脚注1にも書いてあるとおり、JavaScriptのthisは、元々JavaScriptがHTML内に埋め込まれて使われる事を念頭に置いて用意されています。

それでは、アロー関数を用いたTypeScriptの例を以下に示します。

//////////////////////////////////////////////////////
///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(x + 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を表示(グローバル変数の計算結果)
})();

=>がアロー関数です。
15行目のinner = (): void =>{は、オブジェクトのメソッドinnerに「引数なし、戻り値void」の無名関数を格納するを意味します。
この中で実行されるthisはSwitchTimerクラスのインスタンスを参照してくれるようになります。

今回、3行目と4行目にグローバル変数のxとyを用意していますが、これがないと19行目のthisを指定していないconsole.log(x + y);はTypeScriptではコンパイルエラーになります。

TypeScriptは通常のオブジェクト指向言語と同様にthisを扱うため、このように曖昧な指定は許しません。thisがついているものはインスタンスのプロパティ、ついていないものはグローバル変数として区別します。オブジェクト指向言語では当たり前といえば当たり前なんですが、JavaScriptはそうではないので…。

さて、これをJavaScriptに変換すると次のようになります。

//////////////////////////////////////////////////////
///TypesCriptのアロー関数を使って異なるインスタンスのコールバック関数を実装した例
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.innerGlobal = function () {
            console.log(x + y);
        };
        this.x = in_x;
        this.y = in_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を表示(グローバル変数の計算結果)
})();

「パターン6改定」と同様に_this変数を用いた処理が自動で生成されています。
先程の15行目のinner = (): void =>{で示した中身は、11行目のthis.inner = function () {に変換され、その中の処理はconsole.log(this.x + this.y);からconsole.log(_this.x + _this.y);に変換されています。

このJavaScriptをChromeで実行した結果と、さらにDAZ Scriptで動くように調整した結果は次のようになります。

どちらとも想定通りの同じ結果が出力されています。

まとめ

DAZ ScriptはJavaScriptとほぼ同じ動作が期待できますが、主にブラウザ上で利用するJavaScriptと、DAZ Studio上でのみ利用するDAZ Scriptとでは、this等の環境にアクセスする機構で若干挙動が異なります。 コールバック関数でthisを使用したときという限定的なものですが、クリック処理やイベント処理で活用する機会も多いので、うまく実装しないと泥沼にハマりそうです。(まさに、この記事を書いていてハマりました…)

またTypeScriptを用いて、ややこしいバッドノウハウを自動で実装する方法も紹介しました。

尚、イベント処理を実装する方法はいくつかあり、以前の記事「DAZ Scriptにおける、イベント処理の実装方法」で説明しています。

単純にイベント処理を実装するだけなら、上記記事をご参照いただければと思います。

補足資料

DAZ ScriptはQtScriptの拡張版です。
そのため、公式サイトのDAZ Scriptのリファレンスではわからない箇所は、QtScriptのリファレンスを参照することが出来ます。

今回thisの挙動がよくわからず、藁をも掴む思いで参照したのですが、ブラウザ上のJavaScriptと特に比較して説明しているわけでもなく、あまり参考にはなりませんでした…。

thisについての説明は、下記にありました。

Qt Script – The this Object

一応、下記に翻訳を載せておきます。

thisオブジェクト

When a Qt Script function is invoked from a script, the way in which it is invoked determines the this object when the function body is executed, as the following script example illustrates:

Qtスクリプト関数がスクリプトから呼び出されるとき、次の例が示すように、関数が実行されるときの方法により、thisオブジェクトが決まります。

var o = { a: 1, b: 2, sum: function() { return a + b; } };
print(o.sum()); // reference error, or sum of global variables a and b!!
TypeScript

You will get a reference error saying that 'a is not defined’ or, worse, two totally unrelated global variables a and b will be used to perform the computation, if they exist. Instead, the script should look like this:

「aが定義されていません」エラー、または関係のない2つのグローバル変数aとbが存在する場合は、それらを使用して参照エラーが発生します。このエラーを解消したスクリプトは次のようになります。

var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } };
print(o.sum()); // 3
TypeScript

Accidentally omitting the this keyword is a typical source of error for programmers who are used to the scoping rules of C++ and Java.

誤ってthisキーワードを省略することは、C ++およびJavaのスコープ規則に慣れているプログラマーにとっての典型的なエラーの原因です。

脚注

脚注
1 オブジェクト参照のためのthisの使用
2 脚注1にも書いてあるとおり、JavaScriptのthisは、元々JavaScriptがHTML内に埋め込まれて使われる事を念頭に置いて用意されています。

2019年12月31日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を用いて開発ができる環境があるのならば、今回示したようにコールバック関数に指定する関数はアロー関数で実装し、それ以外の関数は通常のクラス内関数で実装するのが、もっとも最適な設計になると思います。

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

2020年1月1日DAZ ScriptDAZ Script,DAZ Studio,TypeScript

DAZ ScriptはJavaScriptと同じですので、JavaScriptにあるイベント処理方法がそのまま使えます。

こちらの記事「ダイアログボックスのボタンをクリックするたびに表示画像を切り替える」でも記載したように、connect関数を使ってイベントとイベント発生時に実行する関数とを紐付けることが出来ます。

この紐付ける関数には、

  • クロージャ
  • オブジェクトのメソッド

が使えます。

このとき、紐付ける関数のことをコールバック関数と呼び、イベント発生時に呼び出すことをコールバックと呼びます。

今回は、上記2つのタイプの関数をコールバック関数の指定する方法を説明します。
またTypeScriptのアロー関数を使うと、クラスを模してインスタンスのメソッドを紐付けることも出来るので、より柔軟性が増すと思います。その方法も解説します。

コールバック関数とは

コールバック関数とは、あるイベントが発生したときに、OSに対して予め呼び出すように指定した関数のことを言います。
DAZ Scriptの例でいうと、ボタンをクリックしたときに呼び出される関数のことをコールバック関数と呼びます。

以前の記事、「DAZ Scriptでウィンドウを表示する」がわかりやすいのですが、スクリプト実行中において、connect関数のところでボタンをクリックしたときに呼び出す関数をOSに指定しています。
そして、スクリプトのメインの処理はそのまま最後まで走り、終了します。

いったんスクリプトは終了していますが、その後ユーザーがボタンをクリックしたとき、connect関数で予め指定しておいた関数をOSが呼び出し、その関数の処理が実行されます。

これが、コールバック関数と一連の処理の流れです。

先の記事でも使用している下記の図では、doit関数がコールバック関数に相当します。

パターン1.クロージャをコールバック関数にする

こちらの記事「ダイアログボックスのボタンをクリックするたびに表示画像を切り替える」で紹介した方法ですね。

DAZ Scriptで例を示してみます。

function SwitchTimer1(in_x, in_y){
    var x = in_x;
    var y = in_y;
    function inner(){
        print( x + y );
    }
    return inner;  //上記で宣言したinner関数を返す。
}
(function(){
	var timer1 = SwitchTimer1(1,2);  //inner関数を取得
    var oTimer1 = new DzTimer();
    oTimer1.singleShot = true;
    connect( oTimer1, "timeout()", timer1);  //タイマーのイベントとinner関数を紐づけ
    oTimer1.start( 1000 );  //1秒後に"timeout"イベントを発火する
    sleep(3000);  //これを入れておかないと、"print( x + y )"が実行される前に、
                  //スクリプトが終了してしまう。
})();

実行結果は次のように「3」と出力されます。

DzTimerオブジェクトは、DAZ Scriptの標準ライブラリの機能です。
この例では、1000ミリ秒後に1度だけ「timeout」イベントを発生するようにしています。これを発火と呼んだりします。
スクリプト実行中、oTimer1.start(1000);の行で1秒後に「timeout」イベントが発火するように仕込みます。この行が実行されてから1秒後、「timeout」イベントが発火します。
すると、事前にconnect関数で紐づけていた関数(inner関数)が呼び出され、実行され、コンソール上に「3」と表示されます。

inner関数はtimeoutイベントのコールバック関数として実行されたわけです。

パターン2.オブジェクトのメソッドをコールバック関数にする

次に示す方法は、新しいオブジェクトを定義し、そこに定義したメソッドをコールバック関数として指定する方法です。
何だかややこしいですが、要はパターン1とは違う方法でもコールバック関数を指定できるということです。

クロージャが用意できるかどうかはケースバイケースですので、設計の都合上クロージャが作れないときはこの方法になります。

なお、コード中に記載のthisとは何か?については、こちら「DAZ Scriptにおけるthisの挙動」で解説しています。

var SwitchTimer1 = {};
SwitchTimer1.x = 1;
SwitchTimer1.y = 2;
SwitchTimer1.getInner = function (){
    var _this = this;
    return function (){
        print( _this.x + _this.y);
    }
};
(function(){
    var oTimer1 = new DzTimer();
    oTimer1.singleShot = true;
    connect( oTimer1, "timeout()", SwitchTimer1.getInner());
        //"print( _this.x + _this.y);"する関数(名前無し)を取得して紐付ける。
    oTimer1.start( 1000 );
    sleep(3000);
})();

実行結果は次のように、パターン1と同じく「3」と出力されます。

この方法では、先にオブジェクトを定義し、その中に含まれるメソッドをコールバック関数として紐づけています。JavaScriptエンジンから見ると、パターン1と同じに見えているのですが、コードを書く設計者側からすると「考え方」の観点でパターン1とパターン2には違いがあります。

パターン1の場合はオブジェクトという概念は関係なく、ただの処理のまとまりを渡しているだけと捉えてください。

パターン2の場合は、繰り返しますが先にオブジェクトを定義し、その中に含まれるメソッドを渡しています。

またこれは、次のように書いても同じです。

var SwitchTimer1 = {
    x : 1,
    y : 2,
    getInner : function (){
        var _this = this;
        return function (){
            print( _this.x + _this.y);
        }
    }
};
(function(){
    var oTimer1 = new DzTimer();
    oTimer1.singleShot = true;
    connect( oTimer1, "timeout()", SwitchTimer1.getInner());
        //"print( _this.x + _this.y);"する関数(名前無し)を取得して紐付ける。
    oTimer1.start( 1000 );
    sleep(3000);
})();

実行結果は先程と同等「3」と出力されます。

パターン3.TypeScriptを用いて、インスタンスのメソッドをコールバック関数にする

こちらの記事「DAZ Script開発環境の構築方法」で説明しているTypeScriptによるDAZ Scriptの開発環境が必要となりますが、パターン2はTypeScriptのアロー関数を用いることで、簡潔に実装することが出来ます。
特にオブジェクト指向に慣れている方は、インスタンスのメソッドをコールバック関数に指定できる方が、直感的にわかりやすいと思いますので、この方法がおすすめです。

ただ、TypeScriptから変換したDAZ Scriptはかなり複雑なものになりますので、後から編集したいときは、Typescript経由になってしまうのを覚悟しておく必要があります。

今回は、インスタンスが別れていることを明示するため、DzTimerオブジェクトを2つ用意して、それぞれ別々のタイミングで「timeout」イベントが発生するようにしてみます。

TypeScriptで次のように書きます。

class SwitchTimer {
    x: number;
    y: number;
    constructor(in_x: number, in_y: number) {
        this.x = in_x;
        this.y = in_y;
    }
    //アロー関数を用いたオブジェクトのメソッド。
    //timeoutイベント時に実行するコールバック関数にする
    inner = (): void => {
        console.log(this.x + this.y);
    }
}
(function () {
    let timer1 = new SwitchTimer(1, 2);
    let timer2 = new SwitchTimer(3, 4);
    var oTimer1 = new DzTimer();
    var oTimer2 = new DzTimer();
    oTimer1.singleShot = true;
    oTimer2.singleShot = true;
    //timeoutイベント時に実行するコールバック関数を設定
    connect(oTimer1, "timeout()", timer1.inner);
    connect(oTimer2, "timeout()", timer2.inner);
    oTimer1.start(1000); //1秒後に3を表示
    oTimer2.start(3000); //3秒後に7を表示
    sleep(5000);
})();

これがJavaScript(DAZ Script)に変換されると、次のようになります。

var SwitchTimer = /** @class */ (function () {
    function SwitchTimer(in_x, in_y) {
        var _this = this;
        //アロー関数を用いたオブジェクトのメソッド。
        //timeoutイベント時に実行するコールバック関数にする
        this.inner = function () {
            print(_this.x + _this.y);
        };
        this.x = in_x;
        this.y = in_y;
    }
    return SwitchTimer;
}());
(function () {
    var timer1 = new SwitchTimer(1, 2);
    var timer2 = new SwitchTimer(3, 4);
    var oTimer1 = new DzTimer();
    var oTimer2 = new DzTimer();
    oTimer1.singleShot = true;
    oTimer2.singleShot = true;
    //timeoutイベント時に実行するコールバック関数を設定
    connect(oTimer1, "timeout()", timer1.inner);
    connect(oTimer2, "timeout()", timer2.inner);
    oTimer1.start(1000); //1秒後に3を表示
    oTimer2.start(3000); //3秒後に7を表示
    sleep(5000);
})();

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

JavaScript上でインスタンスを模すために、コンストラクタなどややこしい機能を使っていますが、いざコールバックされたときの機構はパターン2と何ら変わりません。

まとめ

DAZ Scriptでのイベント処理の実装方法について、

  • パターン1.クロージャをコールバック関数にする
  • パターン2.オブジェクトのメソッドをコールバック関数にする

の2パターンを紹介しました。

また、パターン2をTypeScriptでアロー関数を用いて実装する方法も紹介しました。
この方法については、次の記事「TypeScriptのアロー関数を用いて、DAZ Scriptのクリック処理を実装する」で詳細に解説しています。

尚、これらの機構はJavaScriptと同じですので、ネット上にあるJavaScriptについて解説した記事がそのまま参考にできます。今回登場したthisやコンストラクタについては「DAZ Scriptにおけるthisの挙動」で解説していますが、『JavaScript thisキーワード』などでググると、参考になる情報がでてきますので、そちらの方がおすすめです。

2020年1月1日DAZ ScriptDAZ Script,DAZ Studio

前回の記事「ダイアログボックスに画像を表示する」では、ダイアログボックス上に画像を表示させました。

さて、ダイアログボックス上に画像を表示させるのなら、当然ボタンをクリックしたり、DAZ Studio上の状況変化に応じて表示内容を変化させたくなります。

クリック処理をクロージャで実装する

以前の記事「DAZ Scriptでウィンドウを表示する」でもクリック処理は実装しました。

今回はJavaScriptでよく用いられる「クロージャ」というテクニックを用いて、クリック処理を実装します。

次のようにPixmapオブジェクトを切り替える処理をclicked()シグナルに紐付けることで、ボタンをクリックするたびに画像を切り替えることができます。

function SwitchGen(_label, _xyz) {
    var label = _label;
    var xyz = _xyz;
    var index = 0;
    
    //XYZ画像を切り替えるクロージャ
    function inner() {
        index++;
        index = index % xyz.length;
        label.pixmap = xyz[index];
    }
    return inner;
}
(function () {
    var filepath = "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_label = new DzLabel(dialog);
    var pixImage = new Pixmap(filepath);
    dialog_label.pixmap = pixImage;
    
    //ラベル要素2(XYZ画像)
    var pix_XYZ = [
        new Pixmap(pix_X),
        new Pixmap(pix_Y),
        new Pixmap(pix_Z)
    ];
    
    var dialog_label_xyz = new DzLabel(dialog);
    dialog_label_xyz.pixmap = pix_XYZ[0];
    dialog_label_xyz.x = 20;
    dialog_label_xyz.y = 50;
    
    //ボタン要素(XYZ画像の表示を切り替える。)
    var dialog_button = new DzPushButton(dialog);
    dialog_button.text = "switch dialog_label_xyz";
    
    //XYZ画像を切り替えるクロージャを取得
    var switch_xyz = SwitchGen(dialog_label_xyz, pix_XYZ);
    
    connect(dialog_button, "clicked()", switch_xyz);
    
    //ダイアログ設定
    dialog.width = pixImage.width;
    dialog.height = pixImage.height;
    dialog.exec();
})();

実行結果は次のようになります。
「switch dialog_label_xyz」ボタンをクリックするたびに「X」「Y」「Z」を切り替えます。
なお、PNG画像の透過設定はそのまま適用できます。

dialog_labelはただの背景として用意しただけですので、本流の処理とは関係ありません。dialog_label_xyzが今回メインとなる、切り替えに使用するラベルです。

前回と同様に、画像ファイルをPixmapオブジェクトに読み込み、DzLabel#pixmapに設定していきます。

Pixmapの配列としてpix_XYZ[]を生成しておき、dialog_label_xyz.pixmapには初期値としてpix_XYZ[0](Xの画像)を格納しておきます。

45行目のswitchGen関数が今回のポイントで、切り替え操作対象のラベル(今回はdialog_label_xyz)とPixmap配列のpix_XYZを渡し、それを用いて切り替え処理を行うinner関数を受け取ります。

受け取ったinner関数はswitch_xyzに格納しておき、これが「クロージャ」になります。

connect関数で、切り替え用ボタン(dialog_button)とシグナル(clicked())とコールバック関数(クロージャにしたinner関数ことswitch_xyz)を紐づけます。

exec関数でダイアログを表示したら、スクリプトは一旦停止します。

その後、「switch dialog_label_xyz」ボタンをクリックするたびにinner関数が呼ばれ、操作対象ラベルのpixmapプロパティに格納する画像が順次差し替えられます。

このときinner関数はクロージャにしてあるため、labelとindexとxyzの値は保持され、処理が終了しても値がリセットされることはありません。

ボタンをクリックするたびに順番にX→Y→Z→X…と表示することができます。

まとめ

JavaScriptでよく見られる「クロージャ」というテクニックについては、他に詳しく説明しているサイトが多数ありますので割愛します。

今回のポイントは、DAZ ScriptでもJavaScriptと同じようなテクニックが使えるということです。

これは便利な半面、例えばクロージャになるinner関数内でthisを使うと、上手くxyzを取得できないといった、JavaScript特有の制約もそのままDAZ Scriptでも発生してしまいます。

こういった制約については、すでにJavaScriptで様々なバッドノウハウがありますので、制約に遭遇するたびに検索して解決策を見つけても良いのですが、個人的にはTypeScriptを活用して、素直に組んだコードで開発するほうが、デバッグ環境もおぼつかないDAZ Scriptにおいては効果的だと思います。

先ほどのthisについても、TypeScriptのアロー関数式とclassを組み合わせることで回避可能です。

このやり方については、また別記事で改めて書こうと思います。

2020年1月2日DAZ ScriptDAZ Script,DAZ Studio

DAZ Scriptで開いたダイアログボックスに画像を表示するには、ラベルのpixmapプロパティにPixmapオブジェクトを格納し、ダイアログのexec()を実行します。
具体的には以下のようになります。

(function () {
    var filepath = "C:/dazscript/images/local-user-product.png";
    var dialog = new DzDialog;
    
    //ラベル要素
    var dialog_label = new DzLabel(dialog);
    var pixImage = new Pixmap(filepath);
    dialog_label.pixmap = pixImage;
    
    //ダイアログ設定
    dialog.width = pixImage.width;
    dialog.height = pixImage.height;
    
    dialog.exec();
})();

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

処理順序としては、まずDzDialogの表示するDzLabelをnewで生成します。
その後、new Pixmap()で「C:/dazscript/images/local-user-product.png」の画像ファイルを読み込み、DzLabelのpixmapに格納します。
あとは、dialogの大きさを調整して、dialog.exec()でダイアログを表示します。
今回はDzLayoutを使用していないためダイアログの大きさを自分で設定する必要があり、設定しない場合は最小サイズでダイアログが表示されてしまいます。

Document Centerにより詳しいサンプルもある[1]How can I display images in a dialog box[2]Simple Image Dialogのですが、スクリプトらしく手軽に表示するだけなら、上記で十分です。

ちなみにDocument Centerのサンプルの実行結果は次のようになります。