20191230のC#に関する記事は10件です。

モデリング会に参加して気がついた、DDDにおけるモデリングと日本語プログラミングの可能性

皆さんこんにちは。

突発で開催された、おーひらさん、J.Nakajima さん主催のモデリング会に参加してきました。
神イベントで学びしかなかったのでアウトプットしようと思います。

なお文量が1万字を超えてしまいかねないので 『モデリング会 is 何?』というのは今回は紹介しません。
アバウトに言うと、お題をもとにわいわいモデリングしてみる会 です。
J.Nakajima さんの NoteTwitterの実況(?) を参照していただければイメージが湧くと思います。

余談

自作キーボードの聖地(?) 遊舎工房へ、人生初出舎しました。
あのキースイッチのサンプルマジ最高・・・

10時オープンで10時過ぎについたら既に3人くらいお客さん居たし、自作キーボード流行りまくり感ある・・・
作成中のキーボードもそのうち記事にします。

まとめ: 今回のモデリング会で学んだことや気付き

格言

  • 『モデリングとはコミュニケーションだ』

モデリングに対する気付き

  • ユースケース図を使わないという選択肢いいかも!
  • 横文字プログラマ用語を使ってはいけない
  • 名詞と動詞の付箋紙でモデリングが回りだす

日本語プログラミングに対する気付き

  • 日本語クラスと日本語メソッドの可能性すごい
  • もしかして非エンジニアさんと一緒にユースケース実装出来るんじゃないか?

感想

  • モデリング会楽しすぎた
  • 普段の会社生活の1週間分くらい脳みそ使った
  • ファシリ力が半端じゃなさすぎた

  
    
以下、詳細です。

ユースケース図を使わないという選択肢

1つ目の気付きは、
『もしかしてユースケースを考えるときに、ユースケース「図」を使わないほうが良いんじゃないか?』
というものです。

  
お客さんからこんな要望があります。
さあ、みんなでユースケースをモデリングしましょう!

こんなとき、ユースケース図を使う事が多いのではないでしょうか。
・・なんでユースケース「図」なんだろう?
  
モデリングしようぜ!となると、なぜかユースケース図を使ってしまいがちです。
ついつい何となく、共通言語っぽい UML を使ってしまうのです。
ほら、ユースケース図ってそれっぽく読み書きするの簡単だし。ああアレね、ってなるもん。

  
でもユースケース図そのものは、アクターとユースケースを線でつなぐ都合上、
書き直しが面倒だったり、グループ分けがし辛かったりなど、
『ユースケースを思考する』というフェーズ においてはデメリットが目立つように思います。

でも、ついモデリングに使ってしまう。
今回のモデリング会では、そのことに気がつくことが出来ました。
  
※ ユースケース図は、決まったユースケースを表すには良い方法だと思います。

絶対に「横文字プログラマ用語」を使ってはいけないモデリング

2つ目の気付きは、モデリングを進めるとき、
「ユースケース」に代表される 横文字プログラマ用語を使わないほうが良い ということです。

  
エンジニア用語には 邪悪な魔力 が宿っています。

「ユースケース」、「バリデーション」、「アクター」、etc...

このような魔力を帯びた単語を見聞きしたとき、意識が『プログラマの帽子をかぶった自分』に持っていかれる のです。
頭の中には即座に具体的かつ詳細なイメージが出来上がってしまいます。

たとえば「ユースケース」という言葉なら、棒人間とユースケースが線でつながった図を思い浮かべるかもしれません。
「バリデーション」という言葉なら、ビジネスルールを飛び越えた先の、もっと細かな制約条件にまで思いを馳せてしまいがちです。

するとどうなるか。
話がどんどん詳細になっていってしまうのです。
私達は、お客さんの抱える問題を解決しようとしていたはずなのに。

実際に今回のモデリング会でも、バリデーションの話が始まったとたん急に話が詳細になってとっちらかりかけました。

だから、極力エンジニア用語を使わない。
ていうかアクターも要らないし、〇〇が××するという書き方もいらない。

お客さんの問題領域と正しく相対するために、「気を散らす存在」をすべて排除する ことが、
モデリングにおいて重要なのだと思いました。

きっとこれが出来るようになると、お客さんを交えたモデリングが上手くいくようになりそうな気がしています。
DDD で共通言語を作るとき、エンジニア用語は邪魔なんだよね。

その言葉はエンジニアの都合でしかない。
私達が興味を持つべきなのは、ドメインエキスパートの言葉だった筈なんです。

  

名詞と動詞の付箋紙で回りだすモデリング

3つ目の気付きは、
ユースケースを考える時、名詞と動詞を付箋紙に書いて貼り付けるモデリング手法が良いのではないか?
ということです。

  
今回のモデリング会ではその場の流れで、具体的なモデリングを進める前に、
要望をもとに「動詞」と「名詞」をそれぞれ付箋紙に書き出してみるというフェーズがありました。
いわゆるブレストに近いですね。

(画像残ってなかった? のでイメージはこんな感じ↓)

□□■■
□□■
□□■■
□□■■■

□ = 名詞: 会議室、予約者、予約、予定人数 など。
■ = 動詞: 会議室を選択する、予約する、延長する など。

こうして出てきた付箋紙を、ユースケースとなるように組み合わせてホワイトボードに貼り付けていきます。

model01.jpg

するとあら不思議。
アクターという言葉を使わなくても棒人間を使わなくても、
〇〇を××すると書かなくても、近くに配置するだけで自然とユースケースが出来上がるのです。

線を描かなくていいので並び替えもやり直しも自由。
ジャンル分けもしやすいし、詳細化した付箋を上に貼り付けることも可能です。

これは、もしかして物凄く良い手法なんじゃないか・・・?

なんとなく、みんな頭の中では似たようなことをやっている気がしています。
でも先述したように、いざモデリングをしようとすると、
あるいはみんなでやろうとするとユースケース図になってしまうのです。

ユースケース図から意識を引き剥がすため、この方法をあえて名前で呼んだほうが良い気がする。
このやり方、名前はあるのかなぁ...?
ひとまず主催者のお二方の名前をとって 『ON図』と仮称 することにします。

皆で話しながらホワイトボードへ付箋紙を貼り付けていきます。
すごい、議論が活性化しまくる。
これってこっちじゃない? って言葉がどんどん出てくる。
付箋紙だからどんどん移動したり、上に貼り付けて上書きしたりもできる。

model03.jpg

この黄色の付箋紙は「ルール」を表しています。
名詞と動詞に加え、ルールが追加されていきます。

後で気がついたけど、モデリングを進めるにつれて動詞が墓場に捨てられていきました。

パーキングロットだったり墓場だったり名前は色々ありますが、
こうして「今は考えないもの」を置いておく場を作るという手法は、
『気になっていること』を頭の中から追い出す手法として滅茶苦茶有効でした。
言えないとずーっともやもやして頭を専有するので。

そしてこれ ↓ が最終的なモデリング結果です。

model06.jpg

『名詞』と『動詞』 を付箋紙に書いて並べることで、
「誰が何をやって、すると何が出来上がる」という関係性を表現することが出来ています。
また 黄色の付箋 で注意すべきルールを示しています。
ユースケース図よりも少し具体的な領域に足を踏み込んでいますね。

改めて見渡すと、『名詞』と僅かな『動詞』 が残っています。
しかしこの動詞も名詞で代替可能な予感もします。
もしかするとON図で本当に必要なのは 『名詞』だけ なのかもしれません。

ただし最終的に「動詞」はほぼ消えてしまったものの、議論の活性化に一役買ったので不要ではなかったと思います。

これらをユースケース図としてまとめるべきか、それとも他の表現にすべきかなどなど、
やり方を含めてもう少し検証が必要なように思います。

日本語クラスと日本語メソッドの可能性

4つ目の気付きは、日本語でプログラミングすることへの可能性です。

皆さんご存知ないかもしれませんが、実は C# は日本語クラスや日本語メソッド、日本語変数を使うことが出来ます

私も、ユニットテストを書くときには日本語メソッドを使っています。
しかし日本語でクラスや変数を作ることは流石にしていません。

・・が、今回はモデリング結果をコードに落としていくにあたって、
なんと日本語クラスや日本語変数を使う というある種の暴挙に出ることになりました?

  
JapaneseProgramming.png

するとどうでしょう。
何故かインスピレーションが湧いてくる のです。

何故だろう・・・?
当日皆で話したのは、違和感があるから かな、ということでした。
違和感が思いつきを生んでくれるのだと。

この説は今でも間違っているとは思っていません。
しかしあれから色々と考えた結果、
理由の中で最もウェイトが大きいものは 『私達が日本語ネイティブだから』 ではないか? と考えています。

  
私達は日々英語を駆使して調査やプログラミングをしています。
そのため技術的な英単語であればある程度使うことが出来ます。

でも ドメイン固有の名称は頭の中にありません
従ってドメイン固有の名称を英訳してしまうと、脳内で翻訳作業が発生 します。
ただ読解の難易度が上がるだけではなく、あらゆる作業に翻訳・変換という微妙なオーバーヘッド が生じています。

プログラムで JSON をパースしたり、シリアライズ・デシリアライズすることを思い描いてください。
・・・パフォーマンスを気にしますよね?
・・・メモリの利用量も気にしますよね?

同じことが私達エンジニアの頭の中でも起きているのです。

コードを読む時は英語を日本語にデシリアライズ(あるいはパース) します。
コードを書くときは日本語を英語にシリアライズします。

そう、英語を翻訳するという作業は、私達の貴重な貴重な脳内メモリや処理速度を浪費しています

慣れていなければ浪費の度合いも大きいはずです。
慣れていても、ブランクがあればまた慣れるまで浪費が続きます。
めっちゃ慣れていたとしても、やはりほんの少し脳内容量を使っているように思います。

であれば、脳内容量を浪費している枷を外すためにも、
私達は 日本語クラスや日本語変数を積極的に使うべきではないか? という疑問が生まれてきます。
※ VisualStudio を使っていれば日本語に Intellisense も効きます(マジで)。

  
DDD ではドメインエキスパートと共通言語を作っていきます。
そこでもやはり、ドメインエキスパートは日本語話者であり、用語も日本語 です。

なのに、クラス名は英語・・・
それって何かおかしくないでしょうか。

英語話者は、DDD で共通言語を作る時そのまま英語を利用できます。
では私達日本語話者はどうするべきなのでしょうか?

もしかすると『プログラムに日本語を使う』という行為は、日本人の DDD にとってものすごく効果的 なのかもしれません。

今度のプロジェクトでお試し導入してみようかな・・・

日本語クラスやメソッドのさらなる可能性の芽

日本語クラスやメソッドを使うと、非エンジニアさんと一緒にペアプロやモブプロが出来るのでは? という予感がしています。

var 予約結果 = 管理者.予約(予約情報);

変数の戻り地がーー とかいうプログラマ用語ではなく、
「管理者が」「予約すると」「予約結果が出来る」というように日本語で会話可能です。

もしかしてもしかすると、ドメインエキスパートさんと一緒にプログラムを書けるんじゃないだろうか
少なくともクラス構成やメソッドのシグネチャを考えながら、
ユースケースを形作るためにモデリングとプログラムを行ったり来たりする、という入り口は一緒にできる気がする。

なんだろう、すごくワクワクしてきた!

日本語をプログラムに使うことって、日本語話者による DDD と滅茶苦茶相性が良い気がする。
まだまだ思考も検証も必要だけど、これは追求して見る価値があると思う。

問題は、タイピング量がちょっと増えるってこと。
クラス 田中 があるとき、 とタイプしたら Intellisense が効いてくれたらマジ最強な気がする。

MS さん、どうでしょう?

それか t田中n中川 みたいな感じで頭文字をつけるのもいいかもしれない。
(システムハンガリアンみたいで体が拒否反応を示すけど。。

おわりに

いろいろ振り返っても、やはり学びの山でした。
皆でやった FDL(Fun-Done-Learn というふりかえりの手法) の結果を見ても、Learn が一杯あってすごかった。

また土日にあれば参加したい! と思えるすごくいい勉強会でした。刺激もいっぱい貰えた。
いつもこんな勉強会出来るなんて東京住みの人が羨ましいです?

おーひらさん、J.Nakajima さん、そして参加者の皆さん、ありがとうございました!

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

Windowsの高解像度対応がカオスすぎて嵌った話 - 未解決

経緯

C#で、クリックした位置のAutomationElementを取得して情報を表示するプログラムを作っていて、ディスプレイに描画するときにずれたので調べてみた。1

image.png

画面横幅のピクセル数とタスクバーのWidthの比を調べ、1.5:1になっている。

補正後

決め打ちで1.5倍して描画してみる
image.png

なおった?
試しに他のエレメントをクリックしてみる。
image.png
?!
あかんがな。

補正前に戻して確認してみると
image.png
合いました。

ということで、AutomationElementごとにスケーリング単位が違う??

拡大縮小の設定場所

image.png

image.png

色々調べて下記にたどり着いた。
関連するレジストリ
(参照先の公式:MSDN)

混在しているらしく、これが多分原因か?

WindowsではDPIスケーリング対応状況別にアプリケーションを以下のように分類しています。
・Unaware
・System Aware
・Per-Moniter Aware


  1. CurrentプロパティでAutomationElement.AutomationElementInformationを取得し、BoundingRectangleプロパティで得られる。 

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

Windowsの高解像度対応で嵌った話 - 未解決

経緯

C#で、クリックした位置のAutomationElementを取得して情報を表示するプログラムを作っていて、ディスプレイに描画するときにずれたので調べてみた。1

image.png

画面横幅のピクセル数とタスクバーのWidthの比を調べ、1.5:1になっている。

補正後

決め打ちで1.5倍して描画してみる
image.png

なおった?
試しに他のエレメントをクリックしてみる。
image.png
?!
あかんがな。

補正前に戻して確認してみると
image.png
合いました。

ということで、AutomationElementごとにスケーリング単位が違う??

拡大縮小の設定場所

image.png

image.png

色々調べて下記にたどり着いた。
関連するレジストリ
(参照先の公式:MSDN)

混在しているらしく、これが多分原因か?

WindowsではDPIスケーリング対応状況別にアプリケーションを以下のように分類しています。
・Unaware
・System Aware
・Per-Moniter Aware

確認用ソースコード

テキトウなのであしからず

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Automation;
using System.Windows.Forms;

public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }


    public const int WH_MOUSE_LL = 14;
    public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetWindowsHookEx(int idHook, HookProc callback, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);

    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

    public const int  WM_LBUTTONDOWN = 0x0201;

    // [DllImport("user32.dll")]
    // [return: MarshalAs(UnmanagedType.Bool)]
    // public static extern bool GetCursorPos(out POINT lpPoint);

    [DllImport("user32.dll",SetLastError = true)]
    public static extern IntPtr WindowFromPoint(POINT point);

    [DllImport("User32.dll")]
    public static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("User32.dll")]
    public static extern void ReleaseDC(IntPtr hwnd, IntPtr dc);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetProcessDPIAware();
}


public class AutomationTest : Form
{
    IntPtr _hHook;
    NativeMethods.HookProc _handler;
    GCHandle _hookProcHandle;
    NativeMethods.POINT _lastPoint;

    System.Windows.Forms.Timer timer;
    Button btn;
    ListView lsv;

    AutomationTest()
    {
        // NativeMethods.SetProcessDPIAware();

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 10;
        timer.Tick += (s,e)=>{Timer_Tick();};

        btn = new Button(){Text="get"};
        btn.Click += (s,e)=>{Btn_Click();};
        Controls.Add(btn);

        lsv = new ListView(){
            Location = new System.Drawing.Point(0, 40),
            Size = new System.Drawing.Size(400, 400),
            FullRowSelect = true,
            GridLines = true,
            HideSelection = false,
            MultiSelect = false,
            View = View.Details
        };
        lsv.MouseDoubleClick += Lsv_MouseDoubleClick;
        lsv.Columns.AddRange(new ColumnHeader[]{
            new ColumnHeader(){Text="ClassName",Width=100},
            new ColumnHeader(){Text="AutomationId",Width=50},
            new ColumnHeader(){Text="ControlType",Width=100},
            new ColumnHeader(){Text="FrameworkId",Width=50},
            new ColumnHeader(){Text="Name",Width=100},
            new ColumnHeader(){Text="ItemType",Width=50},
            new ColumnHeader(){Text="X",Width=50},
            new ColumnHeader(){Text="Y",Width=50},
            new ColumnHeader(){Text="Width",Width=50},
            new ColumnHeader(){Text="Height",Width=50},
        });
        Controls.Add(lsv);

        FormClosed += (s,e)=>{UnHook();};

        Load += (s,e)=>{MyResize();};
        Resize += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
    }

    void MyResize()
    {
        int h = ClientSize.Height - lsv.Top;
        if (h<50){h=50;}
        lsv.Size = new System.Drawing.Size(ClientSize.Width, h);
    }

    void Timer_Tick()
    {
        UnHook();
        if ( !timer.Enabled ) {
            return;
        }
        timer.Stop();
        btn.Enabled = true;

        var p = new System.Windows.Point(_lastPoint.x, _lastPoint.y);
        //var elem = AutomationElement.FromHandle(NativeMethods.WindowFromPoint(_lastPoint));
        var elem = AutomationElement.FromPoint(p);
        bool topElemFlag = true;

        lsv.BeginUpdate();
        try {
            while (elem != null) {
                AutomationElement.AutomationElementInformation elemInfo;
                try {
                    elemInfo = elem.Current;
                }
                catch( ElementNotAvailableException ) {
                    return;
                }

                if ( topElemFlag ) {
                    DrawPointAndRectToScreen(p, elemInfo.BoundingRectangle);
                }

                lsv.Items.Add(AeToListItem(elemInfo));
                elem = FindNextElementFromPoint(elem, p);

                topElemFlag = false;
            }
        }
        finally {
            lsv.EndUpdate();
        }
    }

    AutomationElement FindNextElementFromPoint(AutomationElement elem, System.Windows.Point p)
    {
        var childElements = elem.FindAll(TreeScope.Children, Condition.TrueCondition);

        foreach(AutomationElement childElem in childElements) {
            AutomationElement.AutomationElementInformation elemInfo;
            try {
                elemInfo = childElem.Current;
            }
            catch( ElementNotAvailableException ) {
                return null;
            }

            if ( elemInfo.BoundingRectangle.Contains(p) ) {
                return childElem;
            }
        }
        return null;
    }

    ListViewItem AeToListItem(AutomationElement.AutomationElementInformation a)
    {
        System.Windows.Rect r = a.BoundingRectangle;

        return new ListViewItem(new string[]{
            a.ClassName,
            a.AutomationId,
            a.ControlType.ToString(),
            a.FrameworkId,
            a.Name,
            a.ItemType,
            r.X.ToString(),
            r.Y.ToString(),
            r.Width.ToString(),
            r.Height.ToString()
        });
    }

    void Btn_Click()
    {
        try {
            SetHook();
        }
        catch (System.ComponentModel.Win32Exception e) {
            MessageBox.Show(e.ToString());
            return;
        }

        btn.Enabled = false;
        lsv.Items.Clear();
    }

    void Lsv_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        ListViewHitTestInfo info = lsv.HitTest(e.Location);
        if ( info.SubItem != null ) {
            SubForm f = new SubForm(info.SubItem.Text);
            f.ShowDialog();
        }
    }

    void DrawPointAndRectToScreen(System.Windows.Point p, System.Windows.Rect rect)
    {
        IntPtr desktopDC = NativeMethods.GetDC(IntPtr.Zero);

        if (desktopDC == IntPtr.Zero) {
            // failed
            return;
        }

        try {
            var pen = new System.Drawing.Pen(System.Drawing.Color.Red, 3.0f);
            // 描画が欠ける Scalingがうまくいっていないっぽい
            using (var g = System.Drawing.Graphics.FromHdc(desktopDC)) {
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale));
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale));
                g.DrawRectangle(pen, (float)(rect.X*_highDpiScale), (float)(rect.Y*_highDpiScale), (float)(rect.Width*_highDpiScale), (float)(rect.Height*_highDpiScale));
            }
        }
        finally {
            NativeMethods.ReleaseDC(IntPtr.Zero, desktopDC);
        }
    }

    void SetHook()
    {
        IntPtr module = IntPtr.Zero;
        _handler = CallbackProc;
        _hookProcHandle = GCHandle.Alloc(_handler);
        _hHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _handler, module, 0);

        if ( _hHook == IntPtr.Zero ) {
            // failed
            int errorCode = Marshal.GetLastWin32Error();
            _hookProcHandle.Free();
            _handler = null;
            throw new System.ComponentModel.Win32Exception(errorCode);
        }
    }

    void UnHook()
    {
        if ( _hHook != IntPtr.Zero ) {
            NativeMethods.UnhookWindowsHookEx(_hHook);
            _hHook = IntPtr.Zero;
            _hookProcHandle.Free();
            _handler = null;
        }
    }

    IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if ( nCode < 0 ) {
            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
        else {
            if ( (long)wParam == NativeMethods.WM_LBUTTONDOWN ) {
                //NativeMethods.GetCursorPos(out _lastPoint);
                var p = Cursor.Position;
                _lastPoint = new NativeMethods.POINT(){x=p.X, y=p.Y};
                Console.Write(_lastPoint.x);
                Console.Write(", ");
                Console.WriteLine(_lastPoint.y);
                timer.Start();

                // cancel
                return new IntPtr(1);
            }

            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
    }


    static float _highDpiScale = 1.0f;

    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length==1) {
            float scale = (float)(Convert.ToInt32(args[0])/100.0);
            if (scale>=1.0f && scale<=5.0f) {
                _highDpiScale = scale;
            }
        }
        Application.Run(new AutomationTest());
    }
}

internal class SubForm : Form
{
    internal SubForm(string text)
    {
        var txt = new TextBox(){
            Text = text,
            Multiline = true,
            ScrollBars = ScrollBars.Both,
            Dock = DockStyle.Fill
        };
        txt.KeyDown += (sender,e)=>{
            if (e.Control && e.KeyCode == Keys.A) { txt.SelectAll(); }
        };
        Controls.Add(txt);
    }
}

コンパイルバッチ

csc ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll ^
 %*

対策案

下記3が妥当か。

  1. 運用でカバーする。(個人で使う分には、100%に設定すればとりあえず回避できる。)
  2. ウィンドウハンドルをつかう2(描画はあきらめる)。
  3. SetProcessDPIAwareを使う。(Formとかのサイズが変わるので注意)

参考サイト

GetDeviceCapsが常にDPI96を返す問題と解決方法について


  1. CurrentプロパティでAutomationElement.AutomationElementInformationを取得し、BoundingRectangleプロパティで得られる。 

  2. AutomationElement.FromHandle(NativeMethods.WindowFromPoint(GetCursorPos()で得た座標)); ただし、デスクトップ上のアイコンが(良くも悪くも)取れなくなる。 

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

Windowsの高解像度対応で嵌った話

経緯

C#で、クリックした位置のAutomationElementを取得して情報を表示するプログラムを作っていて、ディスプレイに描画するときにずれたので調べてみた。1

image.png

画面横幅のピクセル数とタスクバーのWidthの比を調べ、1.5:1になっている。

補正後

決め打ちで1.5倍して描画してみる
image.png

なおった?
試しに他のエレメントをクリックしてみる。
image.png
?!
あかんがな。

補正前に戻して確認してみると
image.png
合いました。

ということで、AutomationElementごとにスケーリング単位が違う??

拡大縮小の設定場所

image.png

image.png

色々調べて下記にたどり着いた。
関連するレジストリ
(参照先の公式:MSDN)

混在しているらしく、これが多分原因か?

WindowsではDPIスケーリング対応状況別にアプリケーションを以下のように分類しています。
・Unaware
・System Aware
・Per-Moniter Aware

確認用ソースコード

テキトウなのであしからず

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Automation;
using System.Windows.Forms;

public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }


    public const int WH_MOUSE_LL = 14;
    public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetWindowsHookEx(int idHook, HookProc callback, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);

    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

    public const int  WM_LBUTTONDOWN = 0x0201;

    // [DllImport("user32.dll")]
    // [return: MarshalAs(UnmanagedType.Bool)]
    // public static extern bool GetCursorPos(out POINT lpPoint);

    [DllImport("user32.dll",SetLastError = true)]
    public static extern IntPtr WindowFromPoint(POINT point);

    [DllImport("User32.dll")]
    public static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("User32.dll")]
    public static extern void ReleaseDC(IntPtr hwnd, IntPtr dc);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetProcessDPIAware();
}


public class AutomationTest : Form
{
    IntPtr _hHook;
    NativeMethods.HookProc _handler;
    GCHandle _hookProcHandle;
    NativeMethods.POINT _lastPoint;

    System.Windows.Forms.Timer timer;
    Button btn;
    ListView lsv;

    AutomationTest()
    {
        // NativeMethods.SetProcessDPIAware();

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 10;
        timer.Tick += (s,e)=>{Timer_Tick();};

        btn = new Button(){Text="get"};
        btn.Click += (s,e)=>{Btn_Click();};
        Controls.Add(btn);

        lsv = new ListView(){
            Location = new System.Drawing.Point(0, 40),
            Size = new System.Drawing.Size(400, 400),
            FullRowSelect = true,
            GridLines = true,
            HideSelection = false,
            MultiSelect = false,
            View = View.Details
        };
        lsv.MouseDoubleClick += Lsv_MouseDoubleClick;
        lsv.Columns.AddRange(new ColumnHeader[]{
            new ColumnHeader(){Text="ClassName",Width=100},
            new ColumnHeader(){Text="AutomationId",Width=50},
            new ColumnHeader(){Text="ControlType",Width=100},
            new ColumnHeader(){Text="FrameworkId",Width=50},
            new ColumnHeader(){Text="Name",Width=100},
            new ColumnHeader(){Text="ItemType",Width=50},
            new ColumnHeader(){Text="X",Width=50},
            new ColumnHeader(){Text="Y",Width=50},
            new ColumnHeader(){Text="Width",Width=50},
            new ColumnHeader(){Text="Height",Width=50},
        });
        Controls.Add(lsv);

        FormClosed += (s,e)=>{UnHook();};

        Load += (s,e)=>{MyResize();};
        Resize += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
    }

    void MyResize()
    {
        int h = ClientSize.Height - lsv.Top;
        if (h<50){h=50;}
        lsv.Size = new System.Drawing.Size(ClientSize.Width, h);
    }

    void Timer_Tick()
    {
        UnHook();
        if ( !timer.Enabled ) {
            return;
        }
        timer.Stop();
        btn.Enabled = true;

        var p = new System.Windows.Point(_lastPoint.x, _lastPoint.y);
        //var elem = AutomationElement.FromHandle(NativeMethods.WindowFromPoint(_lastPoint));
        var elem = AutomationElement.FromPoint(p);
        bool topElemFlag = true;

        lsv.BeginUpdate();
        try {
            while (elem != null) {
                AutomationElement.AutomationElementInformation elemInfo;
                try {
                    elemInfo = elem.Current;
                }
                catch( ElementNotAvailableException ) {
                    return;
                }

                if ( topElemFlag ) {
                    DrawPointAndRectToScreen(p, elemInfo.BoundingRectangle);
                }

                lsv.Items.Add(AeToListItem(elemInfo));
                elem = FindNextElementFromPoint(elem, p);

                topElemFlag = false;
            }
        }
        finally {
            lsv.EndUpdate();
        }
    }

    AutomationElement FindNextElementFromPoint(AutomationElement elem, System.Windows.Point p)
    {
        var childElements = elem.FindAll(TreeScope.Children, Condition.TrueCondition);

        foreach(AutomationElement childElem in childElements) {
            AutomationElement.AutomationElementInformation elemInfo;
            try {
                elemInfo = childElem.Current;
            }
            catch( ElementNotAvailableException ) {
                return null;
            }

            if ( elemInfo.BoundingRectangle.Contains(p) ) {
                return childElem;
            }
        }
        return null;
    }

    ListViewItem AeToListItem(AutomationElement.AutomationElementInformation a)
    {
        System.Windows.Rect r = a.BoundingRectangle;

        return new ListViewItem(new string[]{
            a.ClassName,
            a.AutomationId,
            a.ControlType.ToString(),
            a.FrameworkId,
            a.Name,
            a.ItemType,
            r.X.ToString(),
            r.Y.ToString(),
            r.Width.ToString(),
            r.Height.ToString()
        });
    }

    void Btn_Click()
    {
        try {
            SetHook();
        }
        catch (System.ComponentModel.Win32Exception e) {
            MessageBox.Show(e.ToString());
            return;
        }

        btn.Enabled = false;
        lsv.Items.Clear();
    }

    void Lsv_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        ListViewHitTestInfo info = lsv.HitTest(e.Location);
        if ( info.SubItem != null ) {
            SubForm f = new SubForm(info.SubItem.Text);
            f.ShowDialog();
        }
    }

    void DrawPointAndRectToScreen(System.Windows.Point p, System.Windows.Rect rect)
    {
        IntPtr desktopDC = NativeMethods.GetDC(IntPtr.Zero);

        if (desktopDC == IntPtr.Zero) {
            // failed
            return;
        }

        try {
            var pen = new System.Drawing.Pen(System.Drawing.Color.Red, 3.0f);
            // 描画が欠ける Scalingがうまくいっていないっぽい
            using (var g = System.Drawing.Graphics.FromHdc(desktopDC)) {
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale));
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale));
                g.DrawRectangle(pen, (float)(rect.X*_highDpiScale), (float)(rect.Y*_highDpiScale), (float)(rect.Width*_highDpiScale), (float)(rect.Height*_highDpiScale));
            }
        }
        finally {
            NativeMethods.ReleaseDC(IntPtr.Zero, desktopDC);
        }
    }

    void SetHook()
    {
        IntPtr module = IntPtr.Zero;
        _handler = CallbackProc;
        _hookProcHandle = GCHandle.Alloc(_handler);
        _hHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _handler, module, 0);

        if ( _hHook == IntPtr.Zero ) {
            // failed
            int errorCode = Marshal.GetLastWin32Error();
            _hookProcHandle.Free();
            _handler = null;
            throw new System.ComponentModel.Win32Exception(errorCode);
        }
    }

    void UnHook()
    {
        if ( _hHook != IntPtr.Zero ) {
            NativeMethods.UnhookWindowsHookEx(_hHook);
            _hHook = IntPtr.Zero;
            _hookProcHandle.Free();
            _handler = null;
        }
    }

    IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if ( nCode < 0 ) {
            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
        else {
            if ( (long)wParam == NativeMethods.WM_LBUTTONDOWN ) {
                //NativeMethods.GetCursorPos(out _lastPoint);
                var p = Cursor.Position;
                _lastPoint = new NativeMethods.POINT(){x=p.X, y=p.Y};
                Console.Write(_lastPoint.x);
                Console.Write(", ");
                Console.WriteLine(_lastPoint.y);
                timer.Start();

                // cancel
                return new IntPtr(1);
            }

            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
    }


    static float _highDpiScale = 1.0f;

    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length==1) {
            float scale = (float)(Convert.ToInt32(args[0])/100.0);
            if (scale>=1.0f && scale<=5.0f) {
                _highDpiScale = scale;
            }
        }
        Application.Run(new AutomationTest());
    }
}

internal class SubForm : Form
{
    internal SubForm(string text)
    {
        var txt = new TextBox(){
            Text = text,
            Multiline = true,
            ScrollBars = ScrollBars.Both,
            Dock = DockStyle.Fill
        };
        txt.KeyDown += (sender,e)=>{
            if (e.Control && e.KeyCode == Keys.A) { txt.SelectAll(); }
        };
        Controls.Add(txt);
    }
}

コンパイルバッチ

csc ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll ^
 %*

対策案

下記3が妥当か。

  1. 運用でカバーする。(個人で使う分には、100%に設定すればとりあえず回避できる。)
  2. ウィンドウハンドルをつかう2(描画はあきらめる)。
  3. SetProcessDPIAwareを使う。(Formとかのサイズが変わるので注意)

参考サイト

GetDeviceCapsが常にDPI96を返す問題と解決方法について


  1. CurrentプロパティでAutomationElement.AutomationElementInformationを取得し、BoundingRectangleプロパティで得られる。 

  2. AutomationElement.FromHandle(NativeMethods.WindowFromPoint(GetCursorPos()で得た座標)); ただし、デスクトップ上のアイコンが(良くも悪くも)取れなくなる。 

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

Raspberry Pi で .Net Core の WebAPI を作成

次のページで行っているうちのページの途中までを行いました。
Building and Securing Web APIs with ASP.NET Core 3.0

Glossary の項目のひつとを Get するところまでです。

localhost からは次のように取り出しができます。

$ curl --insecure https://localhost:5001/api/glossary/jwt | jq .
{
  "term": "JWT",
  "definition": "An open, industry standard RFC 7519 method for representing claims securely between two parties. "
}

他のホストからは次のように取り出しができます。

$ curl --insecure https://violet.local:5001/api/glossary/jwt | jq
{
  "term": "JWT",
  "definition": "An open, industry standard RFC 7519 method for representing claims securely between two parties. "
}

1) プロジェクトの作成

dotnet new webapi -o Glossary

2)他のホストからアクセスが出来るように、次のファイルを書き換えます。

appsettings.Development.json
{
    "urls": "http://*:5000;https://*:5001",
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}
appsettings.json
{
    "urls": "http://*:5000;https://*:5001",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

3)この時点で実行

dotnet build
dotnet run

4)curl でアクセスして確認

curl --insecure https://localhost:5001/weatherforecast

他の端末からアクセス

curl --insecure https://violet.local:5001/weatherforecast

5)改造するにあたり不要となるファイルの削除

rm WeatherForecast.cs
rm Controllers/WeatherForecastController.cs

6) Glossary 関連のファイルの作成

データの項目は次の3つです。
 "Access Token", "JWT", "OpenID"

GlossaryItem.cs
//GlossaryItem.cs
namespace Glossary
{
    public class GlossaryItem
    {
        public string Term { get; set; }
        public string Definition { get; set; }
    }
}
Controllers/GlossaryController.cs
//Controllers/GlossaryController.cs
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;

namespace Glossary.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class GlossaryController: ControllerBase
    {
        private static List<GlossaryItem> Glossary = new List<GlossaryItem> {
        //leave the glossary untouched
            new GlossaryItem
            {
                Term= "Access Token",
                Definition = "A credential that can be used by an application to access an API. It informs the API that the bearer of the token has been authorized to access the API and perform specific actions specified by the scope that has been granted."
            },
            new GlossaryItem
            {
                Term= "JWT",
                Definition = "An open, industry standard RFC 7519 method for representing claims securely between two parties. "
            },
            new GlossaryItem
            {
                Term= "OpenID",
                Definition = "An open standard for authentication that allows applications to verify users are who they say they are without needing to collect, store, and therefore become liable for a user’s login information."
            }
        };


        //new code
        [HttpGet]
        [Route("{term}")]
        public ActionResult<GlossaryItem> Get(string term)
        {
            var glossaryItem = Glossary.Find(item =>
                    item.Term.Equals(term, StringComparison.InvariantCultureIgnoreCase));

            if (glossaryItem == null)
            {
                return NotFound();
            } else
            {
                return Ok(glossaryItem);
            }
        }
        //end new code
    }
}

7)実行

dotnet build
dotnet run

8)curl でアクセスして確認

curl --insecure https://localhost:5001/api/glossary/jwt

他の端末からアクセス

curl --insecure https://violet.local:5001/api/glossary/jwt
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でm5stackとhttp通信してみる

はじめに

前回記事、http通信を勉強するで実際に勉強した上でhttpクライアントのプログラムを作成していきます。

開発環境

  • windows 10 Home 64bit
  • Visual Studio Code 1.41.1
  • .NET Core 3.0

C#http通信するプログラムを作る段取り

  • VScodeC#の開発環境をセットアップする
  • C#http通信プログラムを作成する。

VScodeC#の開発環境を構築する

こちらの方はたくさんの方々が記事を挙げているのですごいざっくり紹介です。
せっかくなので.NET Coreを使っていきます。以下のドキュメントで開始手順が書いているので進めていきましょう。

C# および Visual Studio Code の使用を開始する
microsoft公式なので安心です。

C#http通信プログラムを作成する。

今回はGUIが面倒だったのでコンソールプログラムで作成します。
作るのはhttpのクライアント側なので以下の公式ドキュメントのサンプルを実行してみましょう。

HttpClient クラス
microsoft公式です。
このサンプルでクライアントが送る情報は、GETとHost情報(ドメイン名 or IPアドレス)だけでした。

こちらのサンプルはクライアントから非同期スレッド(GetAsyncが非同期スレッドの様子)でHttpResponseMessageを受け取って表示するといったサンプルプログラムになっていますので、HttpResponseMessageクラスについてもしらべてみました。

HttpResponseMessage クラス
レスポンスメッセージが詰まっている様子。
コンテンツはstring型のプロパティでゲットできます。
ヘッダー部分は各種特定の型として取得できる様子です。結構な種類があるので全部読むのは時間がかかりそうです。

実際にm5stackとつないでみたよ

C#のコード(アクセスするアドレスを変更しただけ)
sample
using System;
using System.Threading.Tasks;
using System.Net.Http;

namespace httptest_C_
{
    class Program
    {
        // HttpClient is intended to be instantiated once per application, rather than per-use. See Remarks.
        static readonly HttpClient client = new HttpClient();
        static async Task Main()
        {
            // Call asynchronous network methods in a try/catch block to handle exceptions.
            try
            {
                HttpResponseMessage response = await client.GetAsync("http://192.168.10.254/");
                response.EnsureSuccessStatusCode();
                string responseBody = await response.Content.ReadAsStringAsync();
                // Above three lines can be replaced with new helper method below
                // string responseBody = await client.GetStringAsync(uri);
                Console.WriteLine(responseBody);
            }
            catch (HttpRequestException e)
            {
                Console.WriteLine("\nException Caught!");
                Console.WriteLine("Message :{0} ", e.Message);
            }
        }
    }
}

結果は以下になりました。
最初はリクエストメッセージ(C# -> m5stack)

GET / HTTP/1.1

Host: 192.168.10.254

次はレスポンスメッセージ(C# <- m5stack)

Click <a href="/H">here</a> to turn ON the LED.<br>Click <a href="/L">here</a> to turn OFF the LED.<br>

うーん、ほんとにシンプルで男らしい・・・
m5stack側は上記のhtmlとステータスコード(HTTP/1.1 200 OK),コンテンツタイプ(Content-type:text/html)が来ているはずですが、表示するように作っていないので表示されていません

今回引っかかったこと

  • C#のビルドがない!?
    • この公式チュートリアルだといきなりコンソールプログラムを作成してrunコマンドで実行してデバッグの説明に入っていますが、buildに関してはコマンド名が出てくるのみとなっていたのでてっきりrunを実行するとビルドされるものだと勘違いしました。
    • buildに関して、VisualStudio2017などではコンパイルするとファイルの保存をコンパイルが同時に実行されますが、VScodeでは保存しないと前のファイルをコンパイルしてしまいます。パワーシェル?コマンドプロンプト?でビルドを行うので当たり前なのですが慣れないと絶対忘れますね・・・
  • .NET Coreにはhttpclientがない!?
    • 初めに、サンプルを実行しようとしたのですが、usingディレクティブが見つかりませんと出てきました。なんでだろう?と色々試した結果、system.NET.HttpHを大文字にしていなかったのが原因でした。コードインテリジェンスを使用してtabで楽していたのがはまった原因でした。

まとめ

今回はC#.NET Coreを使ったhttp通信をm5stackとやってみました。使うだけなら非常に楽なのですが、実際に作りこむとなるとHttpResponseMessageのヘッダー部分やステータスコードを判別対象に使わないといけないのでまた勉強が必要そうです。
また、VSCodeで今回は作りましたが、結構コードインテリジェンスが効かない印象を受けました。(Visual Studioが効きすぎなだけかもですが・・・)
ですが、自分のような浅い初心者ではコードインテリジェンスがないと型がわからなかったりこの型で合ってるのか確認するのに時間がかかるのでこれはこれでやりにくかったです。
m5stackVSCodeでやってるのでやれるのならばいっしょにしたいのですが、何かいい手はないでしょうかねぇ・・・

次回はswifthttp通信を行ってみたいと思います。
有意義な年末ライフをぜひお楽しみください!

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

UnityでSpriteから当たり判定(Polygon Collider 2D)を自動でつける

はじめに

attack2.gif

この記事では透過されたpng画像をSpriteにし、Polygon Collider 2Dを使って当たり判定を自動でつけるところまでやります

目次

  1. 背景透過画像を用意する
  2. Unityにimportする
  3. 画像をSpriteに変換する
  4. SpriteRendererとPolygonCollider2Dを使う

ターゲット

  • 2Dアクションゲーム作りたいけど、当たり判定の作り方がよくわからない
  • デザイナーじゃないし2DのAnimationが作れないから画像から当たり判定をつけたい

環境

Mac Mojave : 10.14.6
Unity : 2019.2.4f1
Rider : 2019.1
.Net : 4.x

1.背景透過画像を用意する

まず、元となるSprite画像を用意します。
今回はテストで作ったので、fire alpacaというソフトを使って背景透過のpng画像を描きアニメーションさせました。

image.pngimage.pngimage.pngimage.pngimage.pngimage.pngimage.png

今回はこの7枚の背景透過画像を元にします。
このとき注意しないといけないのが、攻撃するときに当たり判定をつけたい画像(武器など)と当たり判定をつけたくない画像(プレイヤー自身)などの出力を分ける必要があるということです

2.Unityにimportする

用意したpngファイルをそのままUnityのAssets内に置く
image.png

Assets/Resources/Sprites/Player/Attack3/Weapon

としました。

このままではUnityで使えないのでSpriteに変換していきます。

3.画像をSpriteに変換する

まず、importした画像を全部選択します
image.png

次に、Inspectorから、Texture TypeがDefaultになっているのをSprite(2D and UI)にします

image.png

image.png

Inspectorの下の方にApplyというボタンがあるので、そこをクリックします
image.png

すると、pngの画像をSpriteとして扱えるようになりました
image.png

4.SpriteRendererとPolygonCollider2Dを使う

ここまでで、透過画像のimportおよびSpriteへの変換が行えました。

最後に、自動でColliderを生成してくれるPolygonCollider2Dを使います。

image.png

  1. SpriteRendererをAddComponentする
  2. SpriteRendererのSpriteに自動でColliderをつけたい画像を設定する
  3. PolygonCollider2DをAddComponentする
  4. 自動で生成完了

流れはこれだけなのですが、順番が問題でこの順番じゃないとPolygonCollider2Dは自動で生成してくれません

また、アニメーションさせたくてScriptから連続でSpriteを変更し、自動的にそのSpriteから当たり判定を付与したいときは

private async void Renderer(){
    _spriteRenderer.sprite = _attackSprites[i];
    gameObject.AddComponent<PolygonCollider2D>();
    await UniTask.Delay(50);
    Destroy(GetComponent<PolygonCollider2D>());
}

このようにDestroyメソッドの中にGetComponent()を入れて削除し、またAddComponentで追加し自動生成を行うといっためんどくさい作業をしないといけないみたいです。
エディター上のPolygonCollider2DをResetすれば自動でColliderをつけてくれますが、Scriptから

private void Reset(){
  //hogehoge
}

このように書くと、Resetがoverrideされるみたいで、ここに買いた処理がResetボタンを押した時の処理になるみたいです。

なので、処理は重いですがこういう形にしました。

ちょっとハマったところ

  • 画像の出力は全てUI.Imageでやっていたので、SpriteRendererの存在を知らなかったた

もっとこうした方がいいよとかここ間違ってる等ありましたらご指摘ください!

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

ORiN2 SDKのチュートリアルをC#で書く

1. 概要

ORiN2は、FA/ロボット向けの通信インタフェース。このページを参照。
ORiNとは何か

ORiN2 SDKなどこちらの方の記事がよくまとまっている。
ORiNについて調べてみた

ユーザーズガイドに記載されていたサンプルを、Visual Studio 2017/C#に移植してみた。ORiN2 SDKのC#実装については、以下の動画が役に立った。
ORiN2 - Read Variable Example (RC8 Controller)

2. サンプルについて

元となっているサンプルは以下のユーザーズガイドから2つ。

ORiN2 SDKユーザーズガイド Version 2.1.32

  • 4.2 CAOチュートリアル
  • 4.3 CRDチュートリアル

4.3は4.2からの続きになっている。
本記事ではソースコードの詳しい説明は省略するので、上記ユーザーズガイドを参照。

3. CAOチュートリアル

指定したIPアドレスに対してpingを実行し、到達可能かを調べるアプリを作る。

3-1. プロジェクトの新規作成

Visual Studio 2017を起動し、ファイル>新規作成>プロジェクトでプロジェクトを作成する。

  1. 新しいプロジェクトダイアログが表示される
  2. 左メニューからインストール済み>Windowsデスクトップを選択
  3. Windows フォームアプリケーション(.NET Framework) Visual C#を選択
  4. プロジェクトの名前をORiNCaoAppに指定
  5. OKボタンを押す ORIN_newproj.png

3-2. フォームの作成

以下のコントロールを配置する。

(1) 入力用テキストボックス
Name: textBox_IP_Address
(2) メッセージ出力用テキストボックス
Name: textBox_Message
(3) pingボタン
Name: button_Ping
Text: ping

ついでにフォームの名前も変更し、サイズもいい感じにする。
Text: ORiNCaoApp
ORiN_form.png

3-3. コードを書く準備

ソリューションエクスプローラーからForm1.csを選んで右クリックし、コードの表示を選択する。
ORiN_03_codeview.png

コード入力の画面になる。
ORiN_04_codeview.png

3-4. CAOライブラリを追加する

  1. ソリューションエクスプローラーからMyORiNAppを選んで右クリック、追加>参照を選択する
    ORiN_05_add_reference.png

  2. 参照マネージャが表示されるので、左メニューCOM>タイプライブラリを選択し、中央メニューからCAO 1.0 Type Libraryをチェックする
    ORiN_06_refman.png

  3. OKボタンを押す

3-5. pingボタンのメソッドを追加

Form1.cs[デザイン]タブで、pingボタンをダブルクリックする。
Form1.csタブのソースコードに、button_Ping_Click()が追加されたことを確認する。
以下のようになっているはず。

Form1.cs
namespace ORiNCaoApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button_Ping_Click(object sender, EventArgs e)
        {

        }
    }
}

3-6. コーディング

Form1.csを以下のように編集する。

Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CAOLib;

namespace ORiNCaoApp
{
    public partial class Form1 : Form
    {
        private CaoEngine eng;
        private CaoWorkspace caoWS;
        private CaoController icmpCtrl;
        private CaoVariable icmpVar;
        public Form1()
        {
            InitializeComponent();
            // CAOエンジンの生成
            eng = new CaoEngine();
            caoWS = eng.Workspaces.Item(0);
        }

        private void button_Ping_Click(object sender, EventArgs e)
        {
            // ICMPプロバイダに接続
            icmpCtrl = caoWS.AddController("Sample", "CaoProv.ICMP", "",
                "Host=" + textBox_IP_Address.Text);
            icmpVar = icmpCtrl.AddVariable("@ERROR_CODE");
            // pingの送信
            textBox_Message.Text = icmpVar.Value.ToString();
            caoWS.Controllers.Remove(icmpCtrl.Index);
        }
    }
}

最後にCtrl+Shift+Sで全て保存にする。

3-7. 実行

実行ボタンを押して実行する。

ORiNCaoAppのダイアログが表示されるので、上のテキストボックスに到達可能なIPアドレスを入力し、pingボタンを押す。到達できた場合は、以下のようにメッセージのテキストボックスにエラーコード0が表示される。
ORiN_07_OK.png

到達できなかった場合は、以下のように0以外のエラーコードが表示される。
ORiN_08_NG.png

4. CRDチュートリアル

CAOチュートリアルの機能に追加して、CRDファイルでエラーコードとメッセージを登録し、エラーコードに対応したエラーメッセージを表示する。

4-1. tutorial.xmlを追加する

ソリューションエクスプローラからORiNCaoAppを選択し、右クリックメニューで追加>新しい項目をクリックする。

ORiN_09_new_item.png

新しい項目の追加ダイアログが表示されるので、左メニューからVisual C#アイテム>Dataを選ぶと候補が表示される。XMLを選択し、名前をtutorial.xmlにする。
ORiN_10_new_item.png

追加ボタンを押す。

4-2. tutorial.xmlを編集する

以下の内容を記載する。
先頭のcodingをutf-8に変更した以外は、元のサンプルからそのまま持ってくる。

tutorial.xml
<?xml version="1.0" encoding="utf-8" ?>
<CRD xmlns="http://www.orin.jp/CRD/CRDSchema"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.orin.jp/CRD/CRDSchema CRDSchema.xsd">
  <Controller name="PingStatus">
    <Variable name="0">
      <Value type="VT_BSTR">
        <bstrVal>IP_SUCCESS</bstrVal>
      </Value>
    </Variable>
    <Variable name="11001">
      <Value type="VT_BSTR">
        <bstrVal>IP_BUF_TOO_SMALL</bstrVal>
      </Value>
    </Variable>
    <Variable name="11002">
      <Value type="VT_BSTR">
        <bstrVal>IP_DEST_NET_UNREACHABLE</bstrVal>
      </Value>
    </Variable>
    <Variable name="11003">
      <Value type="VT_BSTR">
        <bstrVal>IP_DEST_HOST_UNREACHABLE</bstrVal>
      </Value>
    </Variable>
    <Variable name="11004">
      <Value type="VT_BSTR">
        <bstrVal>IP_DEST_PROT_UNREACHABLE</bstrVal>
      </Value>
    </Variable>
    <Variable name="11005">
      <Value type="VT_BSTR">
        <bstrVal>IP_DEST_PORT_UNREACHABLE</bstrVal>
      </Value>
    </Variable>
    <Variable name="11006">
      <Value type="VT_BSTR">
        <bstrVal>IP_NO_RESOURCES</bstrVal>
      </Value>
    </Variable>
    <Variable name="11007">
      <Value type="VT_BSTR">
        <bstrVal>IP_BAD_OPTION</bstrVal>
      </Value>
    </Variable>
    <Variable name="11008">
      <Value type="VT_BSTR">
        <bstrVal>IP_HW_ERROR</bstrVal>
      </Value>
    </Variable>
    <Variable name="11009">
      <Value type="VT_BSTR">
        <bstrVal>IP_PACKET_TOO_BIG</bstrVal>
      </Value>
    </Variable>
    <Variable name="11010">
      <Value type="VT_BSTR">
        <bstrVal>IP_REQ_TIMED_OUT</bstrVal>
      </Value>
    </Variable>
    <Variable name="11011">
      <Value type="VT_BSTR">
        <bstrVal>IP_BAD_REQ</bstrVal>
      </Value>
    </Variable>
    <Variable name="11012">
      <Value type="VT_BSTR">
        <bstrVal>IP_BAD_ROUTE</bstrVal>
      </Value>
    </Variable>
    <Variable name="11013">
      <Value type="VT_BSTR">
        <bstrVal>IP_TTL_EXPIRED_TRANSIT</bstrVal>
      </Value>
    </Variable>
    <Variable name="11014">
      <Value type="VT_BSTR">
        <bstrVal>IP_TTL_EXPIRED_REASSEM</bstrVal>
      </Value>
    </Variable>
    <Variable name="11015">
      <Value type="VT_BSTR">
        <bstrVal>IP_PARAM_PROBLEM</bstrVal>
      </Value>
    </Variable>
    <Variable name="11016">
      <Value type="VT_BSTR">
        <bstrVal>IP_SOURCE_QUENCH</bstrVal>
      </Value>
    </Variable>
    <Variable name="11017">
      <Value type="VT_BSTR">
        <bstrVal>IP_OPTION_TOO_BIG</bstrVal>
      </Value>
    </Variable>
    <Variable name="11018">
      <Value type="VT_BSTR">
        <bstrVal>IP_BAD_DESTINATION</bstrVal>
      </Value>
    </Variable>
  </Controller>
</CRD>

4-3. ソースコードの編集

以下のように修正する。
ただし、caoWS.AddController()のPathは各環境で修正すること。

Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using CAOLib;

namespace ORiNCaoApp
{
    public partial class Form1 : Form
    {
        private CaoEngine eng;
        private CaoWorkspace caoWS;
        private CaoController icmpCtrl;
        private CaoVariable icmpVar;
        private CaoController crdPingStatus;
        public Form1()
        {
            InitializeComponent();
            // CAOエンジンの生成
            eng = new CaoEngine();
            caoWS = eng.Workspaces.Item(0);
            crdPingStatus = caoWS.AddController("PingStatus", "CaoProv.CRD", "",
                "Path=//Mac/Home/Documents/project/ORiN2/Tutorial/CaoPingApp/CaoPingApp/tutorial.xml");
        }

        private void button_Ping_Click(object sender, EventArgs e)
        {
            // ICMPプロバイダに接続
            icmpCtrl = caoWS.AddController("Sample", "CaoProv.ICMP", "",
                "Host=" + textBox_IP_Address.Text);
            icmpVar = icmpCtrl.AddVariable("@ERROR_CODE");
            CaoVariable Result = icmpVar;
            // pingの送信
            CaoVariable stVar = crdPingStatus.AddVariable(Result.Value);
            textBox_Message.Text = stVar.Value.ToString();
            crdPingStatus.Variables.Clear();
            caoWS.Controllers.Remove(icmpCtrl.Index);
        }
    }
}

4-4. 実行

実行ボタンを押して実行する。

ORiNCaoAppのダイアログが表示されるので、上のテキストボックスに到達可能なIPアドレスを入力し、pingボタンを押す。到達できた場合は、テキストボックスにメッセージが表示される。

ORiN_11_OK.png

到達できなかった場合は、以下のようにエラーメッセージが表示される。
ORiN_12_NG.png

以上

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

Raspberry Pi で .Net Core の Web アプリケーションを Nginx で動かす

こちらで動かしたプロジェクトを Nginx で動かします。

Raspberry Pi で .Net Core の Web アプリケーションを publish

1) Nginx のインストール

sudo apt install nginx-full

2) ブラウザーで http://host にアクセスしてディフォールトの画面が表示されるのを確認。

/var/www/html/index.nginx-debian.html を改造して、表示が変わるのを確認。

3) /etc/nginx/sites-available/default を編集

/etc/nginx/sites-available/default
(省略)
location / {
                proxy_pass http://127.0.0.1:5000;
                proxy_set_header X-Forwarded-Host $host;
                #
                try_files $uri $uri/ =404;
        }
(省略)

編集に問題がないことを確認。

sudo nginx -t

4) Nginx を再起動

sudo systemctl restart nginx

5) ブラウザーで http://host にアクセス
nginx_kestrel.png

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

言語別int64素因数分解(試し割り法)処理時間比較

C言語、C#, Java8, Golang, Python3 で、
int64の範囲内の値を素因数分解したときの処理時間の比較してみました。

先に結果を見たい方はこちらへ。
処理時間一覧

C#, Java, Golangについては、bigintでも計算してみました。

利用した素因数分解のアルゴリズム

素因数分解のアルゴリズムについては、最も単純な試し割り法を利用しました。
2,3,5,7,13,17, ... (以下、+2, +4を交互に繰り返した値)で割れるかどうか試していきます。

処理の高速化のために、あらかじめ割れるかどうか照合する素数の表を用意しておく方法もありますが、
(63ビット程度のループ処理も10秒程度で終わったため、)今回は用意していません。

下記は、Javaでの試し割り処理の例です。

PrimeFactorization.java
private long[] trial_division(long n) {
    List<Long> prime_list = new ArrayList<>();
    long max = (long)(Math.sqrt(n)) + 1;

    // 2 で割っていく
    while (n % 2 == 0) {
        prime_list.add((long)2);
        n /= 2;
    }

    // 3 で割っていく
    while (n % 3 == 0) {
        prime_list.add((long)3);
        n /= 3;
    }

    // 5 ~ Math.sqrt(n) の数字で割っていく
    boolean flag = true;
    for (long i = 5; i < max; ) {
        while (n % i == 0) {
            prime_list.add(i);
            n /= i;
            if (n == 1)
                i = max;
        }
        if (flag)
            i += 2;
        else
            i += 4;

        flag = !flag;
    }

    if (n != 1) {
        prime_list.add(n);
    }
// (以下、省略)
}

使用言語

  • C言語 (gcc version 8.2.0(MinGW.org GCC-8.2.0-3))
  • C# (.NET Core 3.0)
  • Java8 (javac 11.0.4)
  • Golang (go version go1.12.7 windows/amd64)
  • Python3 (Python 3.7.4)

JavaとGolangについては、int64(long)型のほかに、
bigint(BigInteger)型でも同様の計算を行ってみました。

ちなみに、Python3については、整数型の上限がありませんね。

プログラムのソースは、下記に配置しています。
https://github.com/NobuyukiInoue/PrimeFactorization

計測に使用したPC

処理時間の計測は、以下のスペックもPCを使用しました。

CPU:   Intel Core-i7 8559U 2.7GHz(4.5GHz) 4コア/8スレッド
Memory: 16GB(LPDDR3 2,133MHz)
OS:    Windows 10 Professional

いわゆる、Macbook Pro 13インチ 2018モデルですね。

処理時間一覧

対象の値は、素因数分解したい値です。
("111..."が並んでいますが、2進数ではなく10進数です。)

試し割り法の場合、2,3,5,..などの小さな素数で割れた場合は、トータルのループ回数が少なくなるので、
値の大きさに比例して処理時間がかかるわけではありません。

int64の場合、64ビット程度の値でも、いまどきのPCであれば10秒以内で素因数分解ができてしまいます。

対象の値
(10進数)
C言語
long long int
C#
long
C#
BigInteger
Java
long
Java
BigInteger
Golang
int64
Golang
BigInt
Python3
int
11111111111111
(44ビット)
0.015[S] 0.004[S] 0.013[S] 0.000[S] 0.047[S] 0.003[S] 0.056[S] 0.030[S]
111111111111111
47ビット)
0.031[S] 0.008[S] 0.038[S] 0.016[S] 0.109[S] 0.007[S] 0.169[S] 0.088[S]
1111111111111111
(50ビット)
0.062[S] 0.015[S] 0.067[S] 0.031[S] 0.141[S] 0.015[S] 0.343[S] 0.176[S]
11111111111111111
(54ビット)
0.218[S] 0.257[S] 1.540[S] 0.250[S] 1.859[S] 0.271[S] 計測断念
(180秒以上)
5.021[S]
111111111111111111
(57ビット)
0.624[S] 0.002[S] 0.007[S] 0.000[S] 0.031[S] 0.001[S] 0.022[S] 0.016[S]
1111111111111111111
(60ビット)
2.156[S] 2.300[S] 15.025[S] 2.422[S] 15.688[S] 2.519[S] 計測断念
(180秒以上)
48.322[S]
6111111111111111111
(63ビット)
4.938[S] 5.396[S] 41.263[S] 5.703[S] 38.781[S] 5.937[S] 計測断念
(180秒以上
243.715[S]

(2019/12/30(Mon) 18:00更新)

感想

Golangのbigintは、ループ数が多いと、(ループ内の代入処理時にインスタンス生成が行っているため)、
とんでもなく時間がかかっています。

いくつかの言語で、57ビットの処理時間が44ビットよりも短くなっていますが、これは、

11111111111111(44ビット)の素因数[11, 239, 4649, 909091] の合計が913990に対し、
111111111111111111(57ビット)の素因数[3, 3, 7, 11, 13, 19, 37, 52579, 333667]の合計が386339であり、
ループ回数が少なく済んでいるからです。

bigintでの処理速度の比較では、C# > Java > Golang の順番といったところでしょうか。

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