Help us understand the problem. What is going on with this article?

ThingWorx ウィジェット開発 その2 アナログ時計を作る 前編

More than 1 year has passed since last update.

はじめに

この記事は、「ThingWorx カスタムウィジェット開発 その1 環境構築編」からの続きです。

目次

さて、前回・第一回で Eclipse に ThingWorx エクステンション開発用プラグインをインストールするところまでやりました。今回は、早速ウィジェットを作っていきましょう。

題材として、標準で提供されていそうで実はそうでもない、アナログ時計を作ります。完成予想図ははこんな感じでしょうか。

image.png

第三歩: Eclipse でプロジェクトを作る。

Eclipse の Package Explorer 上で右クリックし、New → ThingWorx Extension Project を選択します。

image.png

"New ThingWorx Extension" というダイアログ・ウィンドウが表示されるので、次のように設定します。

  • Project Name: AnalogClock
  • Build Framework: Ant
  • SDK Location: 第一回で入手した ThingWorx Extension SDK を指定します。
  • Vendor: ウィジェットを開発する会社や人などの名前を入れます。

その他の項目はデフォルト状態のままで構いません。注意する必要があるのは、Build Framework に Ant を選んでおくことです。Ant 以外に Gradle も使えますが、Gradle のセットアップや設定がわかる人向けです。ウィジェットの作成目的であれば、ビルドツールは Ant で十分ですし、通常の使い方であれば Ant の設定も特に必要ありません。

image.png

"Next" ボタンをクリックすると Java の設定画面が表示されますが、ここでは特に設定する項目はありません。

image.png

"Finish" をクリックしてプロジェクトを作成します。そうすると、Package Explorer に新しく AnalogClock というプロジェクトが表示されます。

image.png

第四歩: 必要なフォルダとファイルを準備する。

さて、プロジェクトという「箱」が準備できていよいよウィジェットを作成できるようになりました。早速、ウィジェットの本体となる JavaScript やスタイルシートを記述するためのファイルを作成していきましょう。

Eclipse の "ThignWorx" メニューから "New Widget" を選択します。

image.png

そうすると、"New ThingWorx Widget" ダイアログが表示されますので、ウィジェットの設定をしていきます。
まず、どのプロジェクトに対してウィジェットを追加するのかを指定するために、"Select the parenet project:" で先ほど作成したプロジェクト・ AnalogClock を選択します。

つぎに "Widget Name" に Analog Clock と入力してください。空白文字を含んでも大丈夫です。マッシュアップビルダーのウィジェット一覧には、ここで指定した名前で表示されます。

入力が完了したら "Finish" ボタンをクリックします。

image.png

"Finish" ボタンをクリックすると次のような画面になります。

image.png

Eclipse のコードエディタで開いているのは analog_clock.ide.js というファイルで、マッシュアップビルダーや ThingWorx ランタイムが読み込む「ウィジェットの動作や性格を定義づける」ファイルになります。

また、Package Explorer では、AnalogClock プロジェクトに ui フォルダが追加され、いくつかの JavaScript ファイルとスタイルシートが作成されていることがわかります。

image.png

ウィジェット開発の基本は、この手順で生成されたファイルを編集することになります。

第四歩: 必要な画像をプロジェクトに含める

AnalogClock → ui → analog_clock フォルダに、image という名前で新たにフォルダを作成します。これから作るウィジェットが使う画像は、全てこのフォルダに集めることにしましょう。

image フォルダができたら、次の URL から時計のサンプル画像をダウンロードして、保存してください。

最終的に次のようなフォルダ構成になっていれば大丈夫です。

image.png

Eclipse で File メニュー → Refresh を実行すると、Package Explorer 上で image フォルダと格納した画像ファイルの一覧が見えることを確認してください。

image.png

第五歩:ウィジェットの定義をする

時計の画像も準備できたので、次はこのウィジェットの振る舞いを定義します。振る舞いというのは、このウィジェットの外観(大きさ)や使用するパラメータ(変数)、発行するイベントなどの、ウィジェットの「おおまかな機能」のことです。これらの振る舞いは、.ide.js ファイルに記述します。今回の場合は、analog_clock.ide.js ですね。

analog_clock.ide.js の、this.widgetProperties() 関数を編集します。下記のコードをみてください。初期状態から若干行数が増えていると思います。

analog_clock.ide.js
    this.widgetProperties = function () {
        return {
            'name': 'Analog Clock',
            'description': '',
            'category': ['Common'],
            'isResizable': true,
            'supportsAutoResize': true,
            'properties': {
                'date' : {
                    'baseType': 'DATETIME',
                    'isBindingTarget' : true
                },
                'Width': {
                    'description': TW.IDE.I18NController.translate('tw.textbox-ide.properties.width.description'),
                    'defaultValue': 120,
                    'isEditable': true,
                    'isVisible': true
                },
                'Height': {
                    'description': TW.IDE.I18NController.translate('tw.textbox-ide.properties.height.description'),
                    'defaultValue': 120,
                    'isEditable': true,
                    'isVisible': true
                }
            }
        }
    };

この記述を書き加えたことで、マッシュアップビルダーは「Analog Clock ウィジェットは DATETIME 型の date というプロパティを持っていて、それはターゲットとしてバインド可能なんだな」ということを理解します。ウィジェットがマッシュアップビルダーやランタイムに対して追加して公開するプロパティは、すべてここで定義します。

Width と Height プロパティの定義は、マッシュアップビルダにウィジェットの初期サイズを知らせるためにあえてここに書いています。この例では、縦横ともに 120ピクセルの大きさですね。別の方法でウィジェットの初期サイズを決めるのであれば、プロパティとして宣言する必要はありません。

また、isResizablesupportsAutoResizeを追加しています。これらはユーザーがマッシュアップビルダー上で自由にウィジェットのサイズを変更するために必要な設定です。

なお、最初から定義されている Analog Clock Property プロパティは、削除してもそのままでも構いません。実害はないので、残しておいても問題はないです。

次に、renderHtml() 関数を編集します。

analog_clock.ide.js
    this.renderHtml = function () {
        // return any HTML you want rendered for your widget
        // If you want it to change depending on properties that the user
        // has set, you can use this.getProperty(propertyName).
        return  '<div class="widget-content widget-analog_clock">' +
                    // '<span class="analog-clock-property">' + this.getProperty('Analog Clock Property') + '</span>' +
                    '<img class="aClock" src="../Common/extensions/AnalogClock/ui/analog_clock/image/AnalogClock_Back.svg"> ' +
                '</div>';
    };

最初から定義されていた <span class="analog-clock-property">...の行をコメントにします。削除してしまっても構いません。

次に、先ほどダウンロードして保存した画像のうち、時計の文字盤にあたる AnalogClock_Back.svg を img 要素で読み込みます。相対パスの位置に注意してください。エクステンション内部のリソースは、相対パスとしては ../Common/extensions/<エクステンション名>/ で参照できます。

また、class として aClock を指定していることにも注意してください。このクラス指定は、img 要素のスタイルづけのために後ほどスタイルシートで 利用します。

続いて、afterRender()関数を修正します。

analog_clock.ide.js
    this.afterRender = function () {
        // NOTE: this.jqElement is the jquery reference to your html dom element
        //       that was returned in renderHtml()
    };

初期状態でセットされている余分な Analog Clock Property 関連の記述を削除しました。結果として、afterRender()では何も実行しません。

次に、afterSetProperty()関数を修正します。

analog_clock.ide.js
    this.afterSetProperty = function (name, value) {
        var thisWidget = this;
        var refreshHtml = false;
        switch (name) {
            case 'Style':
            case 'Width':
                this.setProperty('Height', value);
            case 'Height':
                this.setProperty('Width', value);
            case 'Alignment':
                refreshHtml = true;
                break;
            default:
                break;
        }
        return refreshHtml;
    };

ここでは、マッシュアップビルダーのプロパティペインで Width や Height が変更された時の処理を書き加えています。時計ウィジェットは常に縦と横の比率が 1:1 の正方形で表示されて欲しいため、Height(高さ)を変更した際には同じ値を Width(幅)にセットし、逆に Width が変更された際には Height を Width に合わせる処理をしています。

以上でウィジェットの振る舞いの定義は終わりました。おさらいですが、このセクションでは analog_clock.ide.js ファイルを編集することで、

  • DATETIME 型のプロパティを date という名前で定義した
  • マッシュアップビルダー上でウィジェットが表示されるための HTML コードを記述した

というふたつの作業を行いました。全体として、analog_clock.ide.js ファイルは次のようなコードになっています。

analog_clock.ide.js
TW.IDE.Widgets.analog_clock = function () {

    this.widgetIconUrl = function() {
        return  "'../Common/extensions/AnalogClock/ui/analog_clock/default_widget_icon.ide.png'";
    };

    this.widgetProperties = function () {
        return {
            'name': 'Analog Clock',
            'description': '',
            'category': ['Common'],
            'isResizable': true,
            'supportsAutoResize': true,
            'properties': {
                'date' : {
                    'baseType': 'DATETIME',
                    'isBindingTarget' : true
                },
                'Width': {
                    'description': TW.IDE.I18NController.translate('tw.textbox-ide.properties.width.description'),
                    'defaultValue': 120,
                    'isEditable': true,
                    'isVisible': true
                },
                'Height': {
                    'description': TW.IDE.I18NController.translate('tw.textbox-ide.properties.height.description'),
                    'defaultValue': 120,
                    'isEditable': true,
                    'isVisible': true
                }
            }
        }
    };

    this.afterSetProperty = function (name, value) {
        var thisWidget = this;
        var refreshHtml = false;
        switch (name) {
            case 'Style':
            case 'Width':
                this.setProperty('Height', value);
            case 'Height':
                this.setProperty('Width', value);
            case 'Alignment':
                refreshHtml = true;
                break;
            default:
                break;
        }
        return refreshHtml;
    };

    this.renderHtml = function () {
        // return any HTML you want rendered for your widget
        // If you want it to change depending on properties that the user
        // has set, you can use this.getProperty(propertyName).
        return  '<div class="widget-content widget-analog_clock">' +
                    '<img class="aClock" src="../Common/extensions/AnalogClock/ui/analog_clock/image/AnalogClock_Back.svg"> ' +
                '</div>';
    };

    this.afterRender = function () {
        // NOTE: this.jqElement is the jquery reference to your html dom element
        //       that was returned in renderHtml()
    };

};

余談: widgetProperties(), renderHtml(), afterSetProperty() について

マッシュアップビルダーでウィジェットをレイアウトエリアへドラッグ&ドロップで追加したとき、何が起こっているのでしょうか?

ウィジェットがマッシュアップに追加された際、まず最初に widgetProperties() が呼ばれます。widgetPropertis() はそのウィジェットが持つプロパティの一覧を JSON 形式で戻します。マッシュアップビルダーは戻された JSON オブジェクトに従ってプロパティペインに必要なパラメタなどを表示します。

続いて renderHtml() が呼ばれます。renderHtml() は HTML を戻します。結果として戻された HTML が DOM に追加されます。

DOM に HTML ブロックが追加されると、さらに afterRender() が呼ばれます。HTML に紐づく JavaScript(onClick や onChange イベントで呼ばれる JavaScript の関数)は、この afterRender() で定義します。

renderHtml() に <script> タグを書いても、その内部にある JavaScript は有効になりません。これは、<script> タグをはじめとする JavaScript はすべてのウィジェットが読み込まれ、DOM が最終的に確定した後にまとめて処理されるからです。このため、ウィジェット内の renderHtml() がブラウザに読み込まれた段階では、<script>内で定義された JavaScript は実行されてないため、関数名などが undefined 状態になっています。

こうした動作を行うため、ウィジェットでは必ず renderHtml() でまず DOM の要素を生成し、その後 afterRender() で DOM の要素にアクセスしたり JavaScript の関数と紐付けたり、という処理にします。

ウィジェットの設定をプロパティペインで変更した際には、ウィジェットの afterSetProperty()が呼ばれます。ここでユーザーが入力した値のチェックや、スタイルの変更を行います。

第六歩:マッシュアップビルダーに表示される際のスタイルを指定する

さて、直前のステップで時計の画像を読み込む img タグのクラスとして指定した aClock に対してスタイルづけをします。

マッシュアップビルダーにウィジェットを表示する際のスタイルを制御しているのが、analog_clock.ide.css ファイルですので、このファイルを編集しましょう。Package Explorer 上で analog_clock.ide.css をダブルクリックすると、編集モードでファイルが開きます。

image.png

最初は widget-analog_clock クラスは空のスタイルシートとなっていますね。ここに記述を付け加えます。全体としては以下のようになります。

analog_clock.ide.css
/* Place custom CSS styling for Analog Clock widget in Composer in this file */

.widget-analog_clock {
    position: relative;
}

.aClock {
    position: absolute;
    left: 0;
    right: 0;
    max-width: 100%;
    height: auto;
    transition: 0.5s;
}

analog_clock.ide.css の編集は、今の段階ではこれで十分です。

ウィジェットをビルドする方法は次回以降の説明に回しますが、この段階でもウィジェットを ThingWorx にインポートできます。下記の画像は、作ったウィジェットをマッシュアップビルダーで使っている様子のスクリーンショットです。

image.png

「そして、次の曲が始まるのです」

ここまでで、「マッシュアップビルダーが必要とするウィジェットのすべて」を定義し終えました。簡単ですよね? 次回 →「ThingWorx ウィジェット開発 その2 アナログ時計を作る 後編」では、いよいよ ThingWorx ランタイムが必要とする(つまりエンドユーザーが直接目にする)ウィジェットの振る舞いを作り込んでいきます。

余談

このサンプルでは、使用する画像を SVG 形式にしています。これには理由があります。

Retina をはじめとする高解像度のモニタを利用する際、伝統的な JPEG や PNG といった画像形式は面積比で 4倍に引き伸ばされ、その結果焦点の合わないボケた画像として表示されてしまいます。

これを回避するために、HTML5 からは img タグに srcset 属性が新設されました。

analog_clock.runtime.js
<img class="aClock"
    src="../Common/extensions/AnalogClock/ui/analog_clock/images/s_s_1x.png"
    srcset="../Common/extensions/AnalogClock/ui/analog_clock/images/s_s_1x.png 1x,
            ../Common/extensions/AnalogClock/ui/analog_clock/images/s_s_2x.png 2x" />'

srcset 属性では、ディスプレイデバイスの複数の解像度に対応するために、ふたつのイメージファイルを指定できます。低解像度用には 1xで指定された画像が使用され、Retina などの高解像度用には 2x で指定された画像が使われます。2x の画像ファイルは、1x の画像ファイルの 4倍の大きさを持ちます。

srcset で指定された画像の形式に注目すると、PNG 形式ですね。つまりこの方法だと、一つの画像につき通常サイズと 4倍サイズのふたつのファイルを常に用意しなければなりません。そして PNG 形式画像はラスター画像ですので、本来作られたサイズとは異なるサイズを HTML で指定すると、結局はブラウザ側で拡大・縮小処理が走るためにボケたり鮮鋭度が落ちたりします。

SVG 形式はベクター画像形式であり、すべての描画が関数で指定されていますので、リサイズした際の「画像がボケる」心配がありません。srcset で複数の画像を用意しなくても、低解像度・高解像度、どちらのデバイスでも最適な表示が期待できます。

一点注意すべきこととして、通常の SVG 画像では縦横比を変えることはできません。実際に今回のサンプルで使う、短針の SVG 画像をテキストエディタで開いてみましょう。

AnalogClock_shortArm.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
    <g id="短針" transform="matrix(1.62627,0,0,0.887975,-250.536,60.5022)">
        <path d="M389.058,385.014C389.823,356.02 393.315,238.994 396.155,144.447C396.205,142.349 397.92,140.675 400.018,140.675C402.116,140.675 403.831,142.349 403.881,144.447C406.721,238.994 410.214,356.02 410.978,385.014C411.079,385.367 411.131,385.73 411.131,386.099C411.131,386.224 411.099,386.647 411.038,387.337C411.099,389.719 411.131,391.182 411.131,391.612C411.131,396.003 410.513,400.102 409.445,403.57C407.969,418.167 405.77,439.461 403.867,457.806C403.701,459.808 402.027,461.349 400.018,461.349C398.009,461.349 396.335,459.808 396.169,457.806C394.266,439.461 392.067,418.166 390.592,403.57C389.523,400.102 388.906,396.003 388.906,391.612C388.906,391.182 388.937,389.719 388.998,387.337C388.937,386.647 388.906,386.224 388.906,386.099C388.906,385.73 388.958,385.367 389.058,385.014Z" style="fill:white;"/>
    </g>
</svg>

この SVG 画像の縦横比を自由に変える(CSS や HTML で指定する)には、preserveAspectRatio ディレクティブを noneに明示的に指定する必要があります。

AnalogClock_shortArm.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg preserveAspectRatio="none" width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">

svg タグの最初の属性として preserveAspectoRatio="none"が指定されていることに注目してください。こうしておくと、SVG 画像は任意の縦横比を取れるようになります。

CSS transform ScaleX や ScaleY などを利用してアニメーションする場合には、svg タグに preserveAspectRatio=noが必要です。rotate の場合は縦横比が変わりませんので、preserveAscectRatio の指定は必要ありません。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした