20200517のC#に関する記事は8件です。

htmlテーブルにtablesorter.jsでfilter,sorter,pager機能を追加する

概要

tablesorter.jsはhtmlのtableにソート機能やフィルタ機能を追加できるjQueryになります。

こちらの記事で作成したテーブルにtablesorterを使用してソート機能とフィルタ機能を追加してみたいと思います。
サイトには様々なサンプルがありますので、参考にしてみてください。

追加前のページ

image.png

追加後のページ

image.png

環境

  • Visual Studio 2019
  • .Net Framework 4.8
  • Tablesorter 2.31.3

tablesorterの追加

ダウンロード

サイトからファイルをダウンロードします。
image.png

またはGitHubからでもOK。
image.png

プロジェクトに追加

zipファイルを解凍後、distディレクトリ内のcssディレクトリとjsディレクトリのファイルをプロジェクトに追加します。

  • プロジェクトのContentディレクトリとScriptsディレクトリにtablesorterディレクトリを作成
  • Content\tablesorterにcssの内容をコピー
  • Scripts\tablesorterにjsの内容をコピー
  • ソリューションエクスプローラ上で、Content\tablesorterとScripts\tablesorterをプロジェクトに含める
追加前 追加後
image.png image.png
image.png image.png

コード

変更前

Views/Weather/Index.cshtml
@model IEnumerable<MySqlTestWebApp.weather>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.observational_day)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.city.name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.temperature_ave)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.temperature_min)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.temperature_max)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.precipitation)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.wind_speed)
        </th>
        <th></th>
    </tr>

    @foreach (var item in Model)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.observational_day)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.city.name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.temperature_ave)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.temperature_min)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.temperature_max)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.precipitation)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.wind_speed)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { observationalDay = item.observational_day, cityID = item.city_id }) |
                @Html.ActionLink("Details", "Details", new { observationalDay = item.observational_day, cityID = item.city_id }) |
                @Html.ActionLink("Delete", "Delete", new { observationalDay = item.observational_day, cityID = item.city_id }) |
            </td>
        </tr>
    }

</table>

変更後

Views/Weather/Index.cshtml
@model IEnumerable<MySqlTestWebApp.weather>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>

<button class="reset-filter-button">Reset Filter</button>

<table id="table" class="tablesorte">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.observational_day)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.city.name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.temperature_ave)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.temperature_min)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.temperature_max)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.precipitation)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.wind_speed)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.observational_day)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.city.name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.temperature_ave)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.temperature_min)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.temperature_max)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.precipitation)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.wind_speed)
                </td>
                <td>
                    @Html.ActionLink("Edit", "Edit", new { observationalDay = item.observational_day, cityID = item.city_id }) |
                    @Html.ActionLink("Details", "Details", new { observationalDay = item.observational_day, cityID = item.city_id }) |
                    @Html.ActionLink("Delete", "Delete", new { observationalDay = item.observational_day, cityID = item.city_id }) |
                </td>
            </tr>
        }
    </tbody>
</table>

<div id="pager" class="pager">
    <button type="button" class="first"><<</button>
    <button type="button" class="prev"><</button>
    <span class="pagedisplay"></span>
    <button type="button" class="next">></button>
    <button type="button" class="last">>></button>
    <select class="pagesize" title="Select page size">
        <option value="10">10</option>
        <option value="20">20</option>
        <option value="30">30</option>
        <option value="40">40</option>
    </select>
    <select class="gotoPage" title="Select page number"></select>
</div>

@section scripts {
    <link href="~/Content/tablesorter/theme.blue.css" rel="stylesheet" />
    <script src="~/Scripts/tablesorter/jquery.tablesorter.combined.min.js"></script>
    <script src="~/Scripts/tablesorter/extras/jquery.tablesorter.pager.min.js"></script>

    <script type="text/javascript">
        $(document).ready(function () {
            $("table").tablesorter({
                theme: 'metro-dark',
                widthFixed: true,
                widgets: ['zebra', 'columns', 'filter', 'pager'],
                widgetOptions: {
                    filter_reset: 'button.reset-filter-button',
                }
            });
            $("table").tablesorterPager({
                container: $(".pager"),
            });
        });
    </script>
}

tableの設定

tableにidとclassを設定し、theadとtbodyを追加します。

table
<!-- idとclassを設定 -->
<table id="table" class="tablesorte">
    <!-- theadを追加 -->
    <thead>
        <tr>
        ...略
        </tr>
    </thead>
    <!-- tbodyを追加 -->
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
            ...略
            </tr>
        }
    </tbody>
</table>

class="tablesorter-blue"とすることで使用するスタイルの設定を行うことができます。
もしくはjavascriptで設定出来ます。

<table id="table" class="tablesorter-blue">

リセットボタン

フィルタのリセットボタンを追加します。
image.png

リセットボタン
<button class="reset-filter-button">Reset Filter</button>

スライダー

ページ操作に使用するスライダーを追加します。
image.png

スライダー
<div id="pager" class="pager">
    <!-- 戻るボタン -->
    <button type="button" class="first"><<</button>
    <button type="button" class="prev"><</button>
    <!-- 表示ページ -->
    <span class="pagedisplay"></span>
    <!-- 進むボタン -->
    <button type="button" class="next">></button>
    <button type="button" class="last">>></button>
  <!-- ページサイズ(1ページに何行表示させるか)のコンボボックス -->
    <select class="pagesize" title="Select page size">
      <!-- コンボボックスの値を設定 -->
        <option value="10">10</option>
        <option value="20">20</option>
        <option value="30">30</option>
        <option value="40">40</option>
    </select>
    <!-- 指定ページへのジャンプ用のコンボボックス -->
    <select class="gotoPage" title="Select page number"></select>
</div>

cssとjsファイル読み込み

tablesorter用のcssファイルとjsファイルを読み込みます。
※_Layout.cshtmlのheadなどで読み込んでもOKです。
Visual Studioではソリューションエクスプローラーから対象のファイルをドラッグ&ドロップで追加できます。

ファイル読み込み
<!-- スタイルシート 使用するスタイルのスタイルシートを読み込んでください。-->
<link href="~/Content/tablesorter/theme.blue.css" rel="stylesheet" />
<!-- フィルタやソートなど用 -->
<script src="~/Scripts/tablesorter/jquery.tablesorter.combined.min.js"></script>
<!-- pagerを使用する場合に必要 -->
<script src="~/Scripts/tablesorter/extras/jquery.tablesorter.pager.min.js"></script>

tablesorterの設定

$("table").tablesorterがテーブル部分の設定、$("table").tablesorterPagerがページの部分の設定です。
themeプロパティで使用するスタイルの設定を行うことができます。
tablesorterのwidgetsにfilterを設定することでフィルタ機能を追加できます。
tablesorterPagerのcontainerにスライダーを設定します。

<script type="text/javascript">
    $(document).ready(function () {
        $("table").tablesorter({
            //スタイルを設定(theme.blue.css)
            theme: 'blue',
            widthFixed: true,
            //zebra:1行ごとに色を変える
            //columns:選択した列の色を変える
            //filter:列にフィルタ機能を追加する
            widgets: ['zebra', 'columns', 'filter'],
            //フィルタのリセットボタンを追加する場合に設定する。
            widgetOptions: {
                filter_reset: 'button.reset-filter-button',
            }
        });
        //pagerの設定
        $("table").tablesorterPager({
            container: $(".pager"),
        });
    });
</script>

まとめ

今回紹介したtablesorterを使用することで、htmlテーブルに簡単にフィルタ、ソート、ページ機能を追加することができました。
tablesorterを使用するための記述をするだけでhtmlテーブルに便利な機能を追加することができ、さらにさまざまなスタイルも用意されていて、まさに至れり尽くせりという感じです。

参考

https://mottie.github.io/tablesorter/docs/index.html#Examples
https://beiznotes.org/install-tablesorter/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity超初心者が引き出し開閉アクションをつくるその2

はじめに

こちらの勉強メモの内容のつづきです
https://qiita.com/okapy0922/items/7f2de09d0f2d8f4c1c60

今回もこちらのチュートリアルを参考にざっくり手順を残したいと思います。
https://www.youtube.com/watch?v=a5WXiMN3APk

引き出しに触れた状態で引き出しを開閉するアクションと、
開けた引き出しの中に球体が入っている状態を再現しました。

再現したもの

20200517_162037.gif

Raycastというみえないレーザー状の光線をとばして、引き出しのコライダに接触していたら開閉を行います。

スクリプト追加

FPSControllerに新しくAdd Componentでスクリプトを追加します。

Interactive.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//gameObjectにカーソルが接触していたらRaycastで触れ、開閉を行う

public class Interactive : MonoBehaviour{
    // Float型 Rayの有効範囲を値で入力
    [SerializeField] private float interactRange;

    //InteractiveObject.csを呼び出すため
    private InteractiveObject interactiveObject;

    /* カメラ型の変数cam 
    (FPSコントローラのインスペクター内を確認する、
    メインカメラとしてタグ付けされたCameraコンポーネントがある場合にRaycastが機能するようにしている)
    メインカメラがタグ付けされているオブジェクトはシーン内で1つだけにすること*/
    private Camera cam;

    // Raycast型の変数hit
    private RaycastHit hit;

    void Start(){
        // シーン開始時にメインカメラのCameraコンポーネントを変数camに取得する
        cam = Camera.main;
    }

    // Eキーを押した瞬間にRaycastを実行する
    void Update(){
        if (Input.GetKeyDown(KeyCode.E)) {
            /* Raycastの判定
             レーザービームが伸びた先までに何かにぶつかるものがあるかどうか、
             Rayの有効距離はメインカメラ(Player)の座標位置からinteractRangeに格納されている位置情報の値まで*/ 
            Physics.Raycast(cam.transform.position, cam.transform.forward, out hit, interactRange);
            // Raycastがゲームオブジェクトにhitしたら
            if (hit.transform) {
                // InteractiveObject.cs内の開閉処理をよびだす
                interactiveObject = hit.transform.GetComponent<InteractiveObject>();
            }else{
            // Raycastで触れていないときは開閉処理しない、nullを指定する
            //(これがないと引き出しに触れ開閉した後、触れてない状態でも引き出しの開閉ができてしまう)
                interactiveObject = null;
            }
            // nullであるかどうか判定処理(NullRefarenceExeptionを回避)
            if (interactiveObject) {
            // InteractiveObject.csのPerformAtionメソッドを呼び出す
            interactiveObject.PerformAction();
            }
        }
    }
}

レティクル(照準器)追加

Unity画面の左上[Gameobject]-[UI]-[Image]を押下後、
インスペクター内の[Image(Script)]-[Source Image]の右側の丸を選択し、
レティクルのデザインを選びます。[GUIReticle]がちょうどよさげだったので
これを使いました。
ゲーム画面を見ながら照準器が真ん中にくるように置いてあげます。
image.png

オーディオソース追加

こちらから引き出しの開閉音をダウンロードして使用しました。
フリー素材の効果音をダウンロードできます、種類が豊富です。
http://soundbible.com/
ダウンロードしたファイルは保存先のフォルダからUnity上のAseets内に適宜保存先を用意して
ドラッグドロップします。
[AudioClip]の丸を押下後、保存したオーディオファイルを選択し、
下側にある[play on Awake]のチェックボックスは外します。
(チェックを入れてるとゲーム起動後と同時に設定した効果音が鳴ります)

image.png

スクリプト編集

前回追加したInteractiveObject.csを編集します。

InteractiveObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InteractiveObject : MonoBehaviour{
    // Vector3型 引き出しが開いた時(openPositon)と閉じたとき(closePosition)の座標位置をインスペクタ表示、数値入力で設定
    [SerializeField] private Vector3 openPositon, closedPosition;

    // Float型 引き出しの開閉スピードをインスペクタ表示、数値入力で設定
    [SerializeField] private float animationTime;

    /* ブーリアン型 開いているか閉じているかを「isOpen」(真偽)の値で調べている
     (これがないとEキーで開いたとき、引き出しが閉まらなくなる)
     ブール値はデフォルトの状態でfalseにする*/
    [SerializeField] private bool isOpen = false;

    // God_P.234 ハッシュテーブル:キーと値の組み合わせの情報で、
    // キーにより値を出し入れすることができる連想配列と呼ばれる配列の一種のこと
    // (これがないと引き出しを開けたとき空間に飛び出たままになった)
    private Hashtable iTweenArgs;

    // audioSource作成(オーディオクリップを再生するための変数)
    private AudioSource audioSource;

    void Start(){
        /* P.234 ハッシュテーブル作成_渡す引数(arguments)は2個ペア
        Property NameとTypeをカンマ区切りで追加する*/
        iTweenArgs = iTween.Hash();
        // GameObjectがアニメーション化する空間内のポイント。
        iTweenArgs.Add("position", openPositon);
        // アニメーションが完了するまでの秒数
        iTweenArgs.Add("time", animationTime);
        // 「islocal」ワールド空間でアニメ化するか、親(引き出し)を基準にしてアニメ化するか。
        // (デフォルト値はfalseでtrueにすることで親オブジェクトを基準としている)
        iTweenArgs.Add("islocal", true);

        // オーディオソースのコンポーネント取得(GetComponentでAudioSourceを取得)
        audioSource = GetComponent<AudioSource>();
    }

    // 他のスクリプトからアクセスが可能となるようにpublic宣言
    public void PerformAction(){
        // 引き出しが開閉されたら開閉音が再生される
        if (audioSource) {
            // 与えたオーディオクリップを取得して自動的に再生
            audioSource.Play();
        }
        // (isOpenの値が開いているか閉じているかの状態を保持している)
        if (isOpen){
        // 引き出しが開いていたら閉じる
       iTweenArgs["position"] = closedPosition;
       }else{
       // 引き出しが閉じていたら開く
       iTweenArgs["position"] = openPositon;
       }
       //  (真偽を交互に置き換えする、ここにこれがないとEボタン連打したときに引き出しの開け閉めがうまく動作しなかった)
       isOpen = !isOpen;
       // インスペクタで設定した座標位置にアニメーション移動(引き出し開閉)
       iTween.MoveTo(gameObject, iTweenArgs);
    }
}

FPSコントローラの設定変更

シーン上のFPSコントローラはカプセルコライダを設定していますが、Raycastを使用するときは
このコライダが邪魔をしてしまい開閉のアクションがうまくいかないことが起きてしまうようです。
image.png

そこでFPSコントローラの[Layer]プルダウンから[Ignore Raycast]を選択して設定しておくと
コライダの判定を無視したRaycast判定が利用できるようになります。
image.png

引き出しに物体を入れる

引き出しなので、中に何かが入っている状態を再現します。
動画内の内容に沿って引き出しの中に球体をいれました。
球体にはコンポネント追加で[Rigidbody]を設定します。
image.png

物体をいれる引き出しにはRigidBody内の[Is Kinematic]にチェックをいれます。
Is Kinematicを有効化すると物理演算の影響が有効化されるため
引き出しの中にモノが入っている状態が再現できるのと、
引き出しを開いた勢いと同時に球体がコロコロと転がってきます。
image.png

次回

動画チュートリアルの内容のつづきとなりますが、
引き出しのスライドの動きに加え、宝箱の開閉、ドア開閉の軸回転の動き(Rotate)を同シーン状に再現していきたいと思います。

参考にしたもの

https://qiita.com/4_mio_11/items/4b10c6fe37fd7a856350
http://megumisoft.hatenablog.com/entry/2015/08/13/172136
https://ekulabo.com/rigidbody-is-kinematic

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスを CSV にデバインドする カラム名自由編 (C#)

クラスを CSV にデバインドする カラム名自由編 (C#)

クラスを CSV にデバインドする (C#) は有用だが、CSV のヘッダには日本語やカッコなどの識別子に使えないカラム名を使いたいということがあるのでそれに対応する.

[AttributeUsage(AttributeTargets.Property)]
public class HeaderNameAttribute : Attribute
{
    public string Name { get; private set; }
    public HeaderNameAttribute(string name) { Name = name; }
}

public class Contract
{
    public int RowId { get; set; }
    [HeaderName("契約ID")]
    public string Id { get; set; }
    [HeaderName("開始日")]
    public DateTime StartDate { get; set; }
    [HeaderName("終了日")]
    public DateTime? StopDate { get; set; }
    [HeaderName("顧客ID")]
    public int CustomerId { get; set; }
}

みたいなクラスがあって

RowId,契約ID,開始日,終了日,顧客ID
0,C0001,2020-03-01T00:00:00.0000000,2020-04-01T00:00:00.0000000,0
1,C0002,2020-03-01T00:00:00.0000000,,1

みたいな、CSV にデバインドしたいという話です. CSV 変換は今回は話題にしたくないので、string.Join(",", v) で片付くということにしましょう.

Save を以下のコードに置き換えると、Save(stream, contracts); でデバインドできます.

public static void Save<T>(Stream stream, IEnumerable<T> content, Encoding encoding = null)
    where T : class
{
    if (encoding == null) encoding = Encoding.UTF8;
    using (var writer = new StreamWriter(stream, encoding))
    {
        var headerName = typeof(T).GetProperties().Select(e =>
        {
            var a = Attribute.GetCustomAttributes(e, typeof(HeaderNameAttribute));
            if (a.Length == 0)
            {
                return e.Name;
            }
            else
            {
                return (a[0] as HeaderNameAttribute).Name;
            }
        }).ToList();
        writer.WriteLine(string.Join(",", headerName));

        var header = typeof(T).GetProperties().Select(e => e.Name).ToList();
        var headerMap = new Dictionary<string, int>();
        for (var i = 0; i < header.Count; i++)
        {
            headerMap[header[i]] = i;
        }

        foreach (var o in content)
        {
            writer.WriteLine(string.Join(",", Debind(headerMap, o)));
        }
    }
}

関連記事: CSV をクラスにバインドする カラム名自由編 (C#)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CSV をクラスにバインドする カラム名自由編 (C#)

CSV をクラスにバインドする カラム名自由編 (C#)

CSV をクラスにバインドする (C#) は有用だが、CSV のヘッダには日本語やカッコなどの識別子に使えないカラム名を使いたいということがあるのでそれに対応する.

例えば

RowId,契約ID,開始日,終了日,顧客ID
0,C0001,2020-03-01T00:00:00.0000000,2020-04-01T00:00:00.0000000,0
1,C0002,2020-03-01T00:00:00.0000000,,1

みたいな CSV があるとする.

カラム名を指定するための HeaderName 属性を用意します.

[AttributeUsage(AttributeTargets.Property)]
public class HeaderNameAttribute : Attribute
{
    public string Name { get; private set; }
    public HeaderNameAttribute(string name) { Name = name; }
}

そしてバインド対象のクラスのプロパティーにHeaderName 属性を使ってカラム名をアノテーションします.

public class Contract
{
    public int RowId { get; set; }
    [HeaderName("契約ID")]
    public string Id { get; set; }
    [HeaderName("開始日")]
    public DateTime StartDate { get; set; }
    [HeaderName("終了日")]
    public DateTime? StopDate { get; set; }
    [HeaderName("顧客ID")]
    public int CustomerId { get; set; }
}

後は Load を以下に変更すると、var contracts = Load<Contract>(fstream).ToArray(); でバインドできます.

public static IEnumerable<T> Load<T>(Stream stream, Encoding encoding = null)
    where T : class, new()
{
    if (encoding == null) encoding = Encoding.UTF8;
    using (var reader = new StreamReader(stream, encoding))
    {
        var header = reader.ReadLine().Split(",");
        var headerNameMap = typeof(T).GetProperties().Select(e =>
        {
            var a = Attribute.GetCustomAttributes(e, typeof(HeaderNameAttribute));
            if (a.Length == 0)
            {
                return new KeyValuePair<string, string>(e.Name, e.Name);
            }
            else
            {
                return new KeyValuePair<string, string>((a[0] as HeaderNameAttribute).Name, e.Name);
            }
        }).ToDictionary(e => e.Key, e => e.Value);
        var headerMap = new Dictionary<string, int>();
        for (var i = 0; i < header.Length; i++)
        {
            headerMap[headerNameMap[header[i]]] = i;
        }

        while (true)
        {
            var line = reader.ReadLine();
            if (line == null) break;
            yield return Bind<T>(headerMap, line.Split(","));
        }
    }
}

関連記事: クラスを CSV にデバインドする カラム名自由編 (C#)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでも宣言的UI使いたくない??

Unity でも宣言的 UI 使いたくない??

1 年前に Unity で 状態管理をするフレームワーク を作りました。
しかし、まだ辛いところがあったのです。

「宣言的に UI を書きたくない??」

宣言的 UI とは?

Wikipediaより

宣言的 UI は宣言型プログラミングを用いて構成された GUI、それを実現する手法である。GUI の生成・更新を変更前状態に基づいた更新命令によってコーディングするのではなく、あるべき状態を宣言してコーディングする。状態を分離することで UI の状態をより予測しやすいものにできる。テンプレートエンジンは静的テンプレートと動的変数の関係を宣言しているとみなせるため、更新された状態とテンプレートからテンプレートエンジンによって UI 生成をおこなって UI を更新する形は宣言的 UI といえる。そういった意味でも宣言的 UI 自体は古くから存在する GUI 実装手法の 1 つである。

簡単に言うと「この状態のときはこの UI にする」というのを設定することです。

HTML でいうと、「HPが10のときには下のようなHTMLにする」

<div>
  <span>HP:</span>
  <span>10</span>
</div>

見たいな感じですね

しかし、UI って動的な値を反映させてあげないといけないので愚直に宣言的 UI を実装しようとすると UI の構成要素を全部新しく作らないといけません。
これってかなり重たい処理で、Unity で言うと UI に反映させたい値(HP とか)を更新するたびに uGUI の構成要素を Instantiate するということです(逆に使わなくなった UI は、Destory するということ)

作ったもの

そこで今回作ったライブラリは、効率よくUIの変更を更新できるようになっています!

仕組み

参考にしたのは、React や Vue などの Web フロントエンドのフレームワークで使われている 仮想 DOM の概念です。

仮想 DOM

仮想 DOM の概念はシンプルなもので表示に使っている Object(Web なら DOM、Unity なら GameObject)を仮想的なもの(木構造になっているデータ構造)で表して、更新の際はその木構造の差分を計算して必要な変更だけをしてあげる。

というものです。

機能

この Veauty 自体の機能は主に2つで

  • 仮想 Object の Tree を作成
  • 差分の計算

があります。

ここで気になった人もいると思いますが、必要な変更だけをしてあげる が入ってないんですね。
この部分は来る UIElement に向けて 無駄に 抽象化をしていてこのライブラリを使って実装するようにしています。(詳細は次の章で)

使い方

先程も言ったとおりこのライブラリだけだと GameObject に反映出来ないので以下のライブラリを利用します。

今回はカウンターを作りながら使い方を見ていきます。

Image from Gyazo

インストール方法

Unity Package Manager を利用しているのでプロジェクトルート以下にある Packages/manifest.json

{
  "dependencies" : {
    ...
    "com.uzimaru.veauty": "https://github.com/uzimaru0000/Veauty.git",
    "com.uzimaru.veauty-gameobject": "https://github.com/uzimaru0000/Veauty-GameObject.git",
    ...
  }
}

と追記してエディタに行くとインストールされます.

ボイラープレート

若干のボイラープレート的な物を書かないといけないのでそれにコードを追加していく形で解説していきます。

// UIRoot.cs

using UnityEngine;
using UI = UnityEngine;
using Veauty;
using Veauty.VTree;
using Veauty.GameObject;

public class UIRoot : MonoBehaviour
{

  private VeautyObject veauty;

  void Start()
  {
    this.veauty = new VeautyObject(gameObject, Render, true);
  }

  void Render()
    => new Node("GameObject", IAttribute[] {}, IVTree[] {});
}

急にいろいろなクラスが出てきていますが順を追って説明していきます。
ここで作成された UIRoot クラスは Canvas に Attach してください。

UI を作成

察しのいい人は分かると思うのですが、Render メソッドに UI の定義を書いていきます。

GameObject の作成

早速、ただの GameObject は以下のように宣言します。

// UIRoot.cs

IVTree Render() =>
  new Node("GameObject", IAttribute[] {}, IVTree[] {});

Node クラスが何も component がついていない GameObject を生成する要素です。
第1引数の文字列は、GameObject の名前を示していてここの文字列が違うと前回とは違う要素だと判断して再描画されます。
第2引数の IAttribute の配列は、この GameObject の Component に何かしらの値を反映させるためのものです。(transform.position の変更とか)
最後の引数の IVTree の配列は、この GameObject の子要素の配列になります。

Component のついた GameObject の作成

このままでは何もすることが出来ないので GameObject に Component をつけていきます。
HorizontalLayoutGroup のついた GameObject を作成してみましょう。

// UIRoot.cs

IVTree Render() =>
  new Node<UI.HorizontalLayoutGroup>(
    "HorizontalLayoutGroup",
    new IAttribute[] {},
    new IVTree[] {}
  );

Node クラスの Generics に Attach したい Component の型をつけてあげるだけです。簡単ですね!

Button を作成する

カウンターの値を加算・減算するための Button を作成しましょう。
前のコードと同じように Button クラスのついたNodeを作成しましょう!

// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Node<UI.Button>("Button", IAttribute[] {}, new IVTree[] {})
        }
    );

これで一旦動かして見ましょう。

スクリーンショット 2020-05-15 23.13.02.png

ヒエラルキー上で定義したような階層構造になっていることが分かると思います。
しかし、uGUI のButtonクラスは押すために Graphic クラスをtargetGraphic に設定しないといけないため現状では動きません。。。
そんな少し複雑になっている UI を作成するために使うのが Widget クラスです。

// ButtonWidget.cs

public class ButtonWidget : Widget
{
    private IAttribute[] attrs;
    private IVTree[] kids;

    public ButtonWidget(IAttribute[] attrs, IVTree[] kids)
    {
        this.attrs = attrs;
        this.kids = kids;
    }

    public override GameObject Init(GameObject go)
    {
        var image = go.AddComponent<UI.Image>();
        var btn = go.GetComponent<UI.Button>();
        btn.targetGraphic = image;

        return go;
    }

    public override IVTree Render() =>
        new Node<UI.Button>(
            "Button",
            this.attrs,
            this.kids
        );

    public override void Destroy(GameObject go) { }

    public override IVTree[] GetKids() => this.kids;
}

少し長いですが、こんな感じのコードです。
順を追って説明していきます。

Widget

今回のメインの Widget クラスを継承します。このクラスは抽象クラスになっているので以下のメソッドをオーバーライドしなければいけません。

  • GameObject Init(GameObject go)
  • IVTree Render()
  • void Destory(GameObject go)
  • IVTree[] GetKids()
Initメソッド

このメソッドは、実体化した GameObject の初期設定をするためのメソッドです。
今回でいうと、Image クラスを Attach してButton クラスの targetGraphic に設定しています。(Button クラスは Node クラスの Generics で設定済み)

Render メソッド

widget 内での UI の宣言です。
今回は、Node クラスにButton クラスをつけてコンストラクタで受け取った Attributes と子要素を渡しています。

Destory メソッド

この Widget が削除されるときに実行されるメソッドです(実はまだ未実装)

GetKids メソッド

子要素を返します。

早速ここで作成した、Button を使ってボタンを作成してみましょう!

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Button(IAttribute[] {}, new IVTree[] {})
        }
    );

これでボタンが生成されたと思います!

テキストに文字を指定する

ボタンは出来ましたが、中に入る Text が出来ていません。
とりあえず Text を出す Widget を作成します。

using Veauty.VTree;
using UnityEngine;
using Veauty;
using UI = UnityEngine.UI;

public class Text : Widget
{
    private IAttribute[] attrs;

    public Text(IAttribute[] attrs)
    {
        this.attrs = attrs;
    }

    public override IVTree[] GetKids() => new IVTree[0];

    public override GameObject Init(GameObject go)
    {
        var textComponent = go.GetComponent<UI.Text>();
        textComponent.font = Resources.GetBuiltinResource<Font>("Arial.ttf");
        textComponent.alignment = TextAnchor.MiddleCenter;
        textComponent.color = Color.black;

        return go;
    }

    public override IVTree Render() => new Node<UI.Text>("Text", attrs, GetKids());

    public override void Destroy(GameObject go) { }
}

Widget の中身は Button と同じようなものなので省略します。
さて、ここで文字を指定するにはどうしたら良いでしょう?
普通の Unity だったら UI.Texttext に表示したい文字を入れます。では、Veauty だったら?
答えは Attribute を使います。
上でも少し説明したように Attribute とは GameObject の Component に何かしらの値を反映させるためのもの です。
なので今回は UI.Texttext に表示したい文字列を反映させる Attribute を作成しましょう。

ValueAttribute

Text だと Widget の方とかぶってしまうので Value という名前にします。

// Value.cs

using Veauty;
using UI = UnityEngine.UI;

public class Value : IAttribute
{
    private string value;

    public Value(string value)
    {
        this.value = value;
    }

    public string GetKey() => "Value";

    public void Apply(GameObject obj)
    {
        var textComponent = obj.GetComponent<UI.Text>();
        if (textComponent)
        {
            textComponent.text = this.value;
        }
    }

    public bool Equals(IAttribute attr)
    {
        if (attr is Value other)
        {
            return this.value == other.value;
        }

        return false;
    }
}

IAttribute インターフェースで実装するメソッドは 3 つです。

string GetKey()

この Attribute を識別するためのものです。

void Apply(GameObject obj)

渡ってきた Object に対してこの Attribute がしたい操作を反映させます。

bool Equals(IAttribute attr)

渡ってきた IAttribute を見てこの Attribute と等しいかを判定します。

実際に使って見ましょう。

// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
        new Button(IAttribute[] {}, new IVTree[]
            {
                new Text(new IAttribute[] { new Value("↑") })
            }),
        new Text(new IAttribute[] { new Value("0") }),
        new Button(IAttribute[] {}, new IVTree[]
            {
                new Text(new IAttribute[] { new Value("↓") })
            }),
        }
    );

だんだん形が見えて来ましたね。

OnClick を実装する

Button に対する OnClick もAttribute として実装します。

コードは以下のようになります。

// OnClick.cs

using UnityEngine;
using UI = UnityEngine.UI;
using Events = UnityEngine.Events;
using Veauty;

public class OnClick : IAttribute
{
    private Events.UnityAction action;

    public OnClick(Events.UnityAction action)
    {
       this.action = action;
    }

    public string GetKey() => "OnClick";

    public void Apply(GameObject obj)
    {
        var button = obj.GetComponent<UI.Button>();
        if (button)
        {
            button.onClick.RemoveAllListeners();
            button.onClick.AddListener(this.action);
        }
    }

    public bool Equals(IAttribute attr)
    {
        if (attr is OnClick other)
        {
            return this.action == other.action;
        }

        return false;
    }
}

これを使うとこんな感じですね

// UIRoot.cs

IVTree Render() =>
    new Node<UI.HorizontalLayoutGroup>(
        "HorizontalLayoutGroup",
        new IAttribute[] {},
        new IVTree[]
        {
            new Button(new IAttribute[] { new OnClick(() => Debug.Log("↑")) }, new IVTree[]
                {
                    new Text(new IAttribute[] { new Value("↑") })
                }),
            new Text(new IAttribute[] { new Value("0") }),
            new Button(new IAttribute[] { new OnClick(() => Debug.Log("↓")) }, new IVTree[]
                {
                    new Text(new IAttribute[] { new Value("↓") })
                }),
        }
    );

これでボタンを押すと console に Log が出ると思います。

State を更新する

最後に State を更新してみましょう!
Veauty では State の更新をするために VeautyObjectSetState メソッドを使って State 更新用の関数を生成します。
コードで見るとこんな感じです。今回は、counter という int 型の値を State とします。

// UIRoot.cs

using UnityEngine;
using UI = UnityEngine.UI;
using Veauty;
using Veauty.VTree;
using Veauty.GameObject;

public class Sample : MonoBehaviour
{
    private VeautyObject veauty;
    private int counter = 0;
    private System.Action<int> setCounter;

    void Start()
    {
        this.veauty = new VeautyObject(gameObject, Render, true);
        this.setCounter = this.veauty.SetState<int>(n => this.counter = n);
    }

    IVTree Render() =>
        new Node<UI.HorizontalLayoutGroup>(
            "HorizontalLayoutGroup",
            new IAttribute[] { },
            new IVTree[]
            {
                new ButtonWidget(new IAttribute[] {new OnClick(() => this.setCounter(this.counter + 1))}, new IVTree[]
                {
                    new Text(new IAttribute[] {new Value("↑")})
                }),
                new Node("Display", new IAttribute[0], new IVTree[]
                {
                    new Text(new IAttribute[] {new Value($"{this.counter}")}),
                }),
                new ButtonWidget(new IAttribute[] {new OnClick(() => this.setCounter(this.counter - 1))}, new IVTree[]
                {
                    new Text(new IAttribute[] {new Value("↓")})
                }),
            }
        );
}

ここで State を OnClick 内で直接更新しないで this.setCounter を経由して居ることを確認してください。
これは、VeautyObject に State が変わったこと(再描画をしてほしいこと)を通知するためにこのような更新の仕方をしています。
普通にcounterを更新をしてしまうと State が変わったことを検知できないため再描画がされません。。。

肝心の this.setCounter ですが、Start メソッドで初期化しています。
VeautyObjectSetState メソッドに更新するための操作を渡してあげると更新のための関数が生成されます。
これを利用して State を更新すると再描画がされるといった仕組みです。

最後に

これが Veauty のおおよその使い方になります!

しかし、まだまだ不備があったりと完全なものではないので興味のある人は是非コントリビュートをしていただけるとありがたいです :bow:
(ちなみにここで Widget と Attribute の作り方を丁寧に説明したのは、uGUI 用の Widget や Attribute をまとめたライブラリの Veauty-uGUI に協力してほしいからです...!)

また、Veautyでこんなの作ったよ!というのもの常に受け付けているのでぜひ触って見てください!!

それでは!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TwainDotNetによるスキャン

1. はじめに

TwainDotNetを使用してC#のWinFormからTWAINでスキャンしました。これを元に、スキャンアプリケーション ITScan - Qiitaを作成しています。

2. 画面

「SelectSource」でスキャナを選択します。
「Scan」でTWAINドライバのダイアログを表示してスキャンします。

20200517.png

3. TwainDotNetの準備

TwainDotNetはnugetで取得できます。バージョン1.0.0を取得しました。
nugetする前にx86でビルドするように変更しないと、警告が表示されます。nugetした後に変更しても大丈夫です。

4. ソース

Form1.cs
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
using TwainDotNet;
using TwainDotNet.WinFroms;

namespace TwainDotNetTest1
{
    public partial class Form1 : Form
    {
        private const string SCAN_DIR = @"C:\scan";

        private Twain twain = null;
        private ScanSettings settings = null;
        private Bitmap resultImage = null;
        private int scanCount = 1;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            twain = new Twain(new WinFormsWindowMessageHook(this));
            twain.TransferImage += twain_TransferImage;
            twain.ScanningComplete += twain_ScanningComplete;

            settings = new ScanSettings();
            settings.ShowTwainUI = true;
        }

        private void btnSelectSource_Click(object sender, EventArgs e)
        {
            twain.SelectSource();
        }

        private void btnScan_Click(object sender, EventArgs e)
        {
            try
            {
                twain.StartScanning(settings);
            }
            catch (Exception ex)
            {
            }
        }

        private void twain_TransferImage(object sender, TransferImageEventArgs e)
        {
            resultImage = e.Image;

            string filename = String.Format("{0:D6}", scanCount) + ".png";
            string fullname = Path.Combine(SCAN_DIR, filename);
            resultImage.Save(fullname, ImageFormat.Png);

            scanCount++;
        }

        private void twain_ScanningComplete(object sender, ScanningCompleteEventArgs e)
        {
        }
    }
}

5. 解説

WinForm用のTwainオブジェクトを作成し、イベントハンドラをセットします。WPF用もあります。

            twain = new Twain(new WinFormsWindowMessageHook(this));
            twain.TransferImage += twain_TransferImage;
            twain.ScanningComplete += twain_ScanningComplete;

後述のStartScanning()に渡すScanSettingsオブジェクトを作成します。StartScanning()のタイミングでTWAINドライバのダイアログを表示するためにShowTwainUIにtrueを代入します。

            settings = new ScanSettings();
            settings.ShowTwainUI = true;

ソース選択ダイアログを表示します。

            twain.SelectSource();

20200517-2.png
ソース選択ダイアログ

スキャンを開始します。ShowTwainUIをtrueにしているため、TWAINドライバのダイアログが表示され、スキャンを開始するボタンを押下するとスキャンが開始されます。ShowTwainUIがfalseのままだと、TWAINドライバのダイアログが表示されずにスキャンが開始されます。
他のアプリケーションでTWAINドライバが使用中の場合には、TWAINドライバがエラーダイアログを表示して(TWAINドライバの実装に依存する)、ダイアログを閉じた後に例外が発生します。例外をcatchして握りつぶしています。

            try
            {
                twain.StartScanning(settings);
            }
            catch (Exception ex)
            {
            }

20200517-3.png
TWAINドライバのダイアログ(Canon DR-G1130)

1画像転送される毎にTransferImageが発生します。ADFスキャナの場合には1画像毎に連続して呼ばれます。ここでは0パディング6桁連番でPNGファイルとして保存しています。無条件上書きなので注意して下さい。

        private void twain_TransferImage(object sender, TransferImageEventArgs e)
        {
            resultImage = e.Image;

            string filename = String.Format("{0:D6}", scanCount) + ".png";
            string fullname = Path.Combine(SCAN_DIR, filename);
            resultImage.Save(fullname, ImageFormat.Png);

            scanCount++;
        }

スキャン完了時にScanningCompleteが発生します。ADFスキャナの場合には、連続したスキャンが完了した時に1回呼ばれます。また、TWAINドライバのダイアログを閉じたときにも呼ばれます。

        private void twain_ScanningComplete(object sender, ScanningCompleteEventArgs e)
        {
        }

6. 補足

GitHubのTwainDotNetのテストプログラムだと、FormのEnable制御も入っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

組込開発のシステムテスト自動化について

1. はじめに

組込ソフト開発では、最終段階のシステムテスト実行時、ソフトを実機上で動かしてテストします。その時に、オシロスコープ、パルスジェネレータ、通信モニタなど、複数の機器を接続して、設計通りに動作するかを確認します。ただし、これらの機器は高価なものが多く、数が少ないことで、占有出来ないことが多いです。そのため、実機との接続を毎回行うことが必要になり、工数ロスが非常に大きいものとなっていました。また、この工数増大を抑えようとすることが原因で、気軽に動作確認が出来ないことから、システムテストで見つかる不具合が発生しやすくなります。そして、この段階で見つかる不具合というのは、後戻りが大きいことが大半であり、不具合修正も、本来修正したい内容とは違う小手先の修正をすることが多く、次代への負の遺産となり、品質ロスも大きいものとなっていきます。これは、設計段階で機能分割して、機能ごとに仮想環境を作成して検証するのも一つの解ですが、今回はシステムテストの自動化ということで、この辺りの話はパスします。

ところで、実機の接続先は、マイコン搭載の実機であることが多くないでしょうか。あるいは、実機の外部入力信号をマイコンで作成し、実機の外部出力信号をマイコンで取り込めることが多くないでしょうか。これは、つまり、オシロスコープ、パルスジェネレータ、通信モニタの代わりに、マイコンでテストベンチを作成出来るということです。そして、最近のマイコンボードは、USBやLANでPCと通信出来ることが多く、実機の外部入力信号をPCから制御することでマイコンボード上で作成したり、実機の外部出力信号をマイコンボードで取り込み、それをPCへ送信することも出来ます。さらに、うまくハード構成を考慮すれば、リモート操作の可能性も秘めています。ただし、リモート操作となると、火災などのリスク対策が必要となり、反対にコストが掛かることが予想されますが。。。

この記事に興味ある人は、組込開発者だと考えられるため、テストベンチの役割を果たすマイコンボードのソフトは自力で開発出来ること前提とします。そして、以降は、自動化の考え方と、主にPC側のソフト開発について説明します。

ちなみに、テストベンチの一部であるマイコンボード部分は、LabView、dSpaceなどでも作成可能です。ただ、非常に高額であることから、個人では手が出ませんので、今回は、マイコンボードを使用します。

2. 概略

テストベンチの全体像は、以下の通りです。
EmVerif概要.jpg

2-1. マイコンボード

使用するマイコンボード

http://akizukidenshi.com/catalog/g/gM-10094/
マイコンボードは、上記を使用しました。性能が高く、何より日本語の仕様書なのがポイントとなりました。

使用するマイコン機能

この記事では、以下のマイコンインターフェイスを使用しています。
1. CAN(自分が車載開発者なので。。。)
2. SPIマスター(拡張用に使用)
3. LAN(PCとの通信用)
4. AD変換(6チャネル、オシロの代わり)
5. PWM(6チャネル、ファンクションジェネレータの代わり)

マイコンソフトの機能ブロック

全体像に記載の通り、マイコン側のソフトは、ドライバ(AD、PWM等)とユーザープログラムの構成となっています。システム検証環境に依存して、フレキシブルに変更したいこともあると考え、PC側でユーザープログラムを開発し、PCからマイコンボードへプログラムをアップロードする形としています。

2-2. マイコンボードとPCの接続

EmVerif接続.jpg
以降に、作成したサンプルを格納しましたが、試しに動かす場合の注意点としては、PCとGR Peachの接続はクロスケーブルを使用し、PCと直接LAN接続してください。これは、MACアドレスが適当な値のためです。また、マイコンボード側のIPアドレスは、192.168.0.100固定としています。PC側のIPアドレスは、192.168.0.*の固定としておいてください。

2-3. PC

使用する開発環境

VisualStudioをインストールしてください。自分は最新の2019を使用しました。

3. 自動化環境の作成方法

3-1. スクリプトを定義して、自動化する

自分は、組込開発のシステム検証環境をいくつか見てきました。

かなり悲惨な現場は、オシロスコープや、ファンクションジェネレータのパラメータを手動で設定し、システム検証を実行する手法を取っています。これの何が問題かというと、手動が入っているということです。例えば、不具合が発生したとしても、手動であれば再現が非常に困難となります。これが、もし、タイミングに依存した不具合であれば、不具合再現させるために試行回数が必要になることがあります。手動であれば、解析が進みづらく、最悪な場合、再現すら出来ないこともあり、手順ミスだとして不具合が無かったものとされてしまいます。それが本当の不具合であれば、そのまま商品が市場に出ると、後々大きな損害として返ることとなるでしょう。

先ほどより、だいぶマシな開発現場では、PCでツールを作成し、めんどくさい設定手順をPCで自動化していることが多いと感じます。しかし、それでも、GUI上のボタンを、順番に押すようになっていることが多く、タイミングが人依存となります。ここまでくれば、自動化はもう目前ですが、なぜか、そこで満足している現場が多いように見えます。

この記事で提案する案は、例えば、GUIのボタンを押す行為自体を、スクリプトで表現することです。そして、それらを組み合わせて、検証パタンとすることで、実行の自動化が可能となるのです。スクリプトを1から定義するのは、非常に困難ですが、C#のRoslynなるものがあり、それを使うと、簡単にスクリプトを定義することが可能となります。
https://qiita.com/siy1121/items/aee3ce1fff6e83e33b22
上記記事を参照しました。

ちなみに、10年近く前から、このような思想を持っており、実際に検証環境を作成したことがありましたが、このときは、エクセルで管理しているスクリプトをVBAで加工し、さらにそれをPerlプログラムに掛けてc++言語を生成し、それをコンパイルしてからPCで実行する、という非常に厄介な検証環境を作成しました。複数言語の組み合わせで作ったことで、検証環境自体のデバッグも大変となり、たぶん、現在の自分も含め、誰も検証環境をケア出来ないでしょう。C#とRoslynを使えば、VisualStudioで開発できるため、検証環境作成が楽となり、さらに、デバッグもしやすいと思われます。

3-2. スクリプトのコマンド定義について

どのようにスクリプトのコマンドを定義すれば良いでしょうか。それは、イベントドリブン型のコマンド仕様にすれば、解決できます。
http://e-words.jp/w/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%89%E3%83%AA%E3%83%96%E3%83%B3.html
イベントドリブンについては、上記を参照してください。

EmVerif状態遷移.jpg
例えば、実機に入力するサイン波を、時間ごとに周波数を変更して入力する検証項目を仮定します。これをどのように表現すれば良いでしょうか。

// Boot状態をトリガーにして、周波数20ヘルツを出力。
Set( Trig: "Boot", SineWave: new SineWave() { Freq = "20.0", Gain = "0.5" } );
// Boot状態をトリガーにして、2秒後にA1状態に遷移。
Wait( Trig: "Boot", Next: "A1", Time: 2.0 );
// A1状態をトリガーにして、周波数60ヘルツを出力。
Set( Trig: "A1", SineWave: new SineWave() { Freq = "60.0", Gain = "0.5" } );
// A1状態をトリガーにして、2秒後にA2状態に遷移。
Wait( Trig: "A1", Next: "A2", Time: 2.0 );
// A2状態をトリガーにして、周波数40ヘルツを出力。
Set( Trig: "A2", SineWave: new SineWave() { Freq = "40.0", Gain = "0.5" } );
// A2状態をトリガーにして、4秒後にEnd状態に遷移。
Wait( Trig: "A2", Next: "End", Time: 4.0 );

// Boot から End まで波形取得
GetWave( Trig: "Boot", Stop: "Next", FileName: "test.wav", InId: "0, 1, 2, 3, 4, 5", OutId: "0");

このように表現すれば、うまく動作するように見えないでしょうか。あとは、これを解釈して実行するプログラムをPC上で作成すれば良いだけです。そして、Roslynを使用すれば、上記スクリプトはRoslynが実行してくれるので、以前より楽に検証環境を作成することが可能となりました。

3-3. サンプルソフトの実行

スクリプト実行がどれだけ便利か、そして、その先に、リモートで作業出来る可能性を秘めていることが、実際に動かしてみたら分かると思い、サンプルを用意しました。

https://github.com/EmVerif/EmVerifPCSample
上記に、Roslynを利用したプログラムを格納しました。Roslynを利用するにはWPFが必須です。しかし、自分がWPF初心者であるため、フォームで開発している部分もあります。不細工な構造となっていますが、ご了承ください。

スクリプト入力.jpg
コンパイルが成功し、実行すると、上記のようなウィンドウが立ち上がると思います。

GUI.jpg
そして、開始を押すと、上記のようなGUIが起動し、約8秒後に終了すると思います。

サンプルプログラムの説明は、長くなると思われるため、別の記事にします。

4. まとめ

  1. システム検証に必要な入出力信号や通信の大半は、マイコンボードで作成可能。
  2. スクリプトを導入すれば、マイコンボード、dSpace、LabViewと連携し、自動化が可能。
  3. Roslynを利用すれば、スクリプトの実装が簡単。

もちろん、自分も、全てのケースで自動化が出来るとは思っていません。しかし、上記環境を使用すれば、大半の項目が自動化可能だと考えており、どうしても無理な場合のみ、既存のオシロ、ファンクションジェネレータなどを利用すれば良いと思っています。こうすることで、工数削減や、将来は、組込開発でも、リモートワークが出来るようになれば、と考えています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単なステートマシンを作ってみた

はじめに

これはArbor3とうまく自分の作成中のゲームを組み合わせられず, でもとりあえず簡単なステートマシンだけ欲しかった人が簡易的にステートマシン(有限オートマトン)を実装してみたので, それの記事です。

以下がそのコードです。

StateMachine.cs
using System;
using System.Collections.Generic;
using UniRx;

namespace Assets.Scripts.StateMachine
{
    public abstract class State<T> where T: struct{


        protected Dictionary<T, State<T>> NextState = new Dictionary<T, State<T>>();

        public void Connect(T message, State<T> state)
        {
            NextState[message] = state;
        }

        public readonly string Name;

        /// <summary>
        /// このStateに入ったときに発行されます。イベントの値は前のStateです
        /// </summary>
        /// <returns></returns>
        public abstract IObservable<State<T>> OnEnterAsObservable();

        /// <summary>
        /// このStateにから出る時に発行されます。イベントの値は次のStateです
        /// </summary>
        /// <returns></returns>
        public abstract IObservable<State<T>> OnExitAsObservable();

        protected State(string name)
        {
            Name = name;
        }

        public override string ToString()
        {
            return $"{Name} State({typeof(T)})";
        }
    }

    public class StateMachine<T> where T : struct
    {
        private class InnerState : State<T>
        {


            public Subject<State<T>> OnEnter;
            public Subject<State<T>> OnExit;

            public override IObservable<State<T>> OnEnterAsObservable() => OnEnter = OnEnter ?? new Subject<State<T>>();

            public override IObservable<State<T>> OnExitAsObservable() => OnExit = OnExit ?? new Subject<State<T>>();

            public InnerState(string name) : base(name)
            {
            }

            public InnerState SendMessage(T message)
            {
                return (InnerState)NextState[message];
            }
        }

        public readonly State<T> Entry;

        private InnerState current;
        public State<T> CurrentState => current;

        public string CurrentStateName => current?.Name;

        public StateMachine()
        {
            Entry = current = new InnerState(default);
        }

        /// <summary>
        /// メッセージを送出します
        /// </summary>
        /// <param name="message"></param>
        public void SendMessage(T message)
        {
            var next = current.SendMessage(message);
            var prev = current;

            current.OnExit?.OnNext(next);
            current = next;
            next.OnEnter?.OnNext(current);
        }

        private Dictionary<string, InnerState> states = new Dictionary<string, InnerState>();

        public State<T> Create(string name)
        {
            var c = new InnerState(name);

            states.Add(name, c); // 例外出すため

            return c;
        }

        public void Jump(string name)
        {
            var next = states[name];
            var prev = current;

            current.OnExit?.OnNext(next);
            current = next;
            next.OnEnter.OnNext(current);
        }

        public State<T> GetState(string name)
        {
            return states[name];
        }



    }
}

使い方

ステートマシンの定義

  1. メッセージ用のenumを作成します
  2. Createメソッドで状態を作成します
  3. State.ConnectメソッドでそのStateの状態でメッセージが来た場合に遷移する状態を定義します。
PlayerStateMachine.cs
using Assets.Scripts.StateMachine;

namespace Assets.Scripts.StateMachine
{
    public enum PlayerMessage
    {
        Start,
        TakeDamage,

    }

    class PlayerStateMachine : StateMachine<PlayerMessage>
    {
        public State<PlayerMessage> Alive { get; private set; } 
        public State<PlayerMessage> Dead { get; private set; }

        public PlayerStateMachine()
        {
            Alive = Create("Alive");
            Dead = Create("Dead");


            Entry.Connect(PlayerMessage.Start, Alive);

            Alive.Connect(PlayerMessage.TakeDamage, Dead);
        }
    }
}

ステートマシンの使用

  1. ステートマシンは必ずEntryから開始されます
  2. StateにはOnEnterAsObservableとOnExitAsObservableという状態になった時状態から遷移したときのIObservableを入れています
  3. SendMessageメソッドでメッセージを送って遷移します
UseStateMachine.cs
var playerState = new PlayerStateMachine();
playerState.Dead.OnEnterAsObservable().subscribe(_ =>{/*死んだ*/});

playerState.SendMessage(PlayerMessage.Start);

// playerがダメージをうけたら
playerState.SendMessage(PlayerMessage.TakeDamage);

まとめ

というわけで簡単なステートマシン作ってみました。拡張はしやすいかも??

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む