ITエンジニア勉強ブログ

自分が学んだことを共有していきます。

Blocklyのカスタムブロックの各種設定

この記事では、ビジュアルプログラミング言語のBlocklyで、カスタムブロックを定義する方法を紹介します。


See the Pen
Blockly on CodePen 2
by Imai (@imai1)
on CodePen.

なお、Blocklyの基本的な使い方について知りたい方は前の記事をご覧いただけたら嬉しいです。

imai1.hatenablog.com

カスタムブロックの追加とツールボックスへの登録

まず、新しいカスタムブロックを定義するためには、Blockly.Blocks[新しいカスタムブロックの識別子]に要素を追加します。

Blockly.Blocks["block1"] = {
  init: function() {
    // 設定を記述。
  }
}

ブロックから特定の言語へのコード生成を定義する場合は、Blockly.言語[新しいカスタムブロックの識別子]に要素を追加します。

Blockly.JavaScript["block1"] = function(block) {
  // コード生成処理を記述。
}

定義したカスタムブロックは、ツールボックスに登録するなどで利用できます。

// ツールボックス内に設置するブロックの定義
var toolbox = {
  "contents": [
    {
      "kind": "block",
      "type": "block1"
    },
  ]
}

// 指定したIDのタグにワークスペース(Blocklyのウィジェット)を注入。
const workspace = Blockly.inject("blocklyDiv", {toolbox: toolbox});

カスタムブロックの下・上・左にブロックを接続できるよう設定

まずはblock1の定義をご覧ください。ツールボックスの一番上のブロックです。

Blockly.Blocks["block1"] = {
  init: function() {
    this.setNextStatement(true);  // 下にブロック(次の処理)のジョイントを追加。
    this.setPreviousStatement(true, "Number");  // 上にブロック(前の処理)のジョイントを追加。第二引数で型を指定。
    this.setOutput(true, "Number");  // 左にブロック(このブロックを引数として受け取る処理)のジョイントを追加。第二引数で型を指定。
    this.setColour(160);  // HSVで0°〜360°を指定。
  }
};

initメソッド内で、setNextStatement setPreviousStatement setOutputを実行することで、カスタムブロックの下、上、左のジョイントを追加することができます。

なお、この三つのメソッドでは第二引数に型を指定できます。次の節で説明する右向きのジョイントにも型を設定することで、一致するブロックのみ接続できるよう制限が可能です。ただし、ここで言う型とは、コード生成先の言語の型などではなく、ユーザが任意に定義できる単なる文字列です。

カスタムブロックの右にブロックを接続できるよう設定

次は先にこちらのコードをご覧ください。ツールボックスの上から2番目のブロックに対応するblock2の定義です。

Blockly.Blocks["block2"] = {
  init: function() {
    // 何も接続しないInputを追加。
    this.appendDummyInput()
        .appendField("ABC")  // 文字列を追加。
        .appendField("DEF")  // 文字列を追加。
        .appendField("GHI");  // 文字列を追加。
    // setOutput(true)のブロックを接続できるInputを追加。
    this.appendValueInput("VALUE")
        .appendField("JKL")  // 文字列を追加。
        .appendField(new Blockly.FieldNumber(1), "NUM1")  // 初期値1の数値入力を追加。
        .appendField(new Blockly.FieldTextInput("xyz"), "TEXT1")  // 初期値xyzの文字列入力を追加。
        .setCheck("Number");  // 接続を数値型に限定。
    // setPreviousStatement(true)のブロックを接続できるInputを追加。
    this.appendStatementInput("STATEMENT")
        .appendField(new Blockly.FieldMultilineInput("abc\ndef"), "TEXT2")  // 初期値"abc\ndef"(2行)の複数行文字列入力を追加。
        .setCheck("String");  // 接続を数値型に限定。
    this.setColour(160);  // HSVで0°〜360°を指定。
  }
};

まず前提として、Blocklyのブロックの内部は、論理的には行列風の構造になっています。一行は一つのInputと対応し、最大一つの右向きジョイントを設定できます。また、Inputの中にはFieldと呼ばれる要素を列方向に並べていきます。

公式ドキュメントより一部抜粋(https://developers.google.com/static/blockly/images/input-types.png

Inputの種類は以下の3つです。

  • DummyInput:何も接続できないInput。
  • ValueInputsetOutput(true)のブロックを接続できるInput。
  • StatementInputsetPreviousStatement(true)のブロックを接続できるInput。

Fieldはラベルやパラメータを設定するための要素です。

  • FieldLabel("文字列")または単に文字列:単なるラベル。
  • FieldNumber():数値型パラメータの入力欄。デフォルト値を設定する場合は第一引数を追加。
  • FieldTextInput():文字列型パラメータの入力欄。デフォルト値を設定する場合は第一引数を追加。
  • FieldMultilineInput():文字列型パラメータの複数行入力欄。デフォルト値を設定する場合は第一引数を追加。

コードと表示を見比べたりコードを書き換えたりすると、対応関係がより理解できるのではないかと思います。

なお、appendFieldの第二引数は、コード生成時にFieldを参照するための識別子です。

setCheckInputに接続されるブロックの型チェックです。冒頭のCodePenで、block2に対してblock1を接続しようとすると、VALUEには接続できるのにSTATEMENTには接続できないことが確認できます。

Inputのインライン化

Blocklyの利用例において、一行に複数のInputを持っていたり、Inputの右にもラベルを含んでいるブロックを見たことがあるのではないでしょうか。例えば、日本語で「%1 を %2 に代入」というようなカスタムブロックで、この文章がそのまま一行に横並びしているブロックです。

block3のようにsetInputsInline(true)を実行すると、連続するValueInputDummyInputを一行に並べることができます。StatementInputは必ず一行に独立するセパレータのようになっており、それ以外のInputが横に連結されます。

Blockly.Blocks["block3"] = {
  init: function() {
    // ブロックを諸々定義。
    this.setInputsInline(true);  // StatementInput以外を1行にまとめる。
  }
}

その他の設定

setEditable setDeletable setMovableでフィールドパラメータの変更、ブロックの削除、ブロックの移動を制御できます。

Blockly.Blocks["block4"] = {
  init: function() {
    this.appendDummyInput()
        .appendField("消せないし動かせないし変更できない")
        .appendField(new Blockly.FieldNumber(1));
    this.setEditable(false);
    this.setDeletable(false);
    this.setMovable(false);
  }
}

例えば教育用に、デフォルトで配置されているブロックを変更されたくない場合などに利用できます。

なお、これらの設定はGUIからの操作のみに適用されます。JavaScriptのコードでblock.dispose()またはblock.moveBy(dx, dy)を実行すれば、ブロックを直接削除・移動することはいつでも可能です。

コード生成の定義

ブロックがInputおよびFieldから構成されることを把握できていれば、コード生成は簡単に理解できると思います。

Blockly.JavaScript["block2"] = function(block) {
  const value = Blockly.JavaScript.valueToCode(block, "VALUE", Blockly.JavaScript.ORDER_ATOMIC);
  const statement = Blockly.JavaScript.statementToCode(block, "STATEMENT");
  console.log("VALUE in block2: " + value);
  console.log("STATEMENT in block2: " + statement);
  console.log("NUM1 in block2: " + block.getFieldValue("NUM1"));
  console.log("TEXT1 in block2: " + block.getFieldValue("TEXT1"));
  console.log("TEXT2 in block2: " + block.getFieldValue("TEXT2"));
  return ["This is a code of block2", Blockly.JavaScript.ORDER_NONE];
}
  • valueToCode:指定した識別子のValueInputに接続されたブロックのコードを生成。
  • statementToCode:指定した識別子のStatementInputに接続されたブロックのコードを生成。
  • getFieldValue:指定した識別子のFieldに接続されたブロックのコードを生成。Fieldの識別子はappendFieldの第二引数で指定。

block2およびその他のブロックを適当に接続して「実行」ボタンを押すと、ブラウザのコンソールにblock2の接続先のブロックのコードが出力されるはずです。動作確認用に実装したのでJavaScriptのコードとしては意味を為していませんが、実際に触ってみて確かめてみてください。

カスタムブロックの右に接続されたブロックをコード化する方法がわかったら、後はカスタムブロックのコード生成をスクラッチで自由に定義するだけです。

Blockly.JavaScript.ORDER_ホニャララは、Blocklyがブロックの処理の優先順序(和より積の方が優先、など)に応じて自動的に括弧を追加してくれる機能のために定めるものらしいです。コード生成処理に優先順序付けのための括弧を適切に設定しさえすれば、valueToCodeにはORDER_ATOMICを、コード生成関数の戻り値ではORDER_NONEに固定して問題ないとのことです。