20200725のC#に関する記事は15件です。

SQLiteでReadWrite設定にしてあるのに書き込みに失敗する

タイトルのような問題が発生し、解決したので備忘録として残しておく。

問題

Microsoft.Data.Sqliteを使ってSQLiteの対象のテーブルに書き込みを行う際に、以下のようなExceptionが発生していた。

sqlite attempt to write a readonly database

データベースのMode設定(SqliteConnectionStringBuilder.Mode)はReadWriteに設定済である。

原因

原因としては、Cacheの設定(SqliteConnectionStringBuilder.Cache)の設定がSharedになっていたこと。
MicrosoftのDocumentをみると以下のような記載がある。

Shared : Shared-cache mode. Connections share a cache. This mode can change the behavior of transaction and table locking.

おそらく、タスクがこのDatabaseを書き込みしている間はテーブルがロックされてしまう。
そのため、別のタスクが書き込みをしようとした場合にReadOnlyとみなされてしまい、上記のようなExceptionが発生したと思われる。

解決策

Cacheの設定(SqliteConnectionStringBuilder.Cache)の設定をprivateにする。
こうするごとにCacheがタスクごとに作成され、テーブルのロックが発生しない。(今のところ)

Cache=Defaultでもよいかもしれないが、Defaultがどのような動作を行うかDocumentに記載がないためひとまずPrivateに設定しておく。

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

再帰を使う時には Task を使おう

まず普通に再帰関数を書きます。

using System;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine(Recurse(0));
        }

        static int Recurse(int n)
        {
            if (n >= 100_000) return n;
            return Recurse(n + 1);
        }
    }
}

n が 100000 以上になるまで +1 し続ける関数です。しかし結果はこうなります。

Stack overflow.

Recurse メソッドが終了する前に次の Recurse メソッドを再帰呼び出しするため、引数などが解放される前にスタックに積まれていきます。その結果、必要なスタックがあらかじめ割り当てられたサイズを超えてしまい、このエラーが発生します。

これを Task を用いて書き直すと次のようになります。

using System;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main()
        {
            //Console.WriteLine(Recurse(0));
            Console.WriteLine(Recurse(0).Result);
        }

        //static int Recurse(int n)
        //{
        //  if (n >= 100_000) return n;
        //  return Recurse(n + 1);
        //}

        static Task<int> Recurse(int n)
        {
            if (n >= 100_000) return Task.FromResult(n);
            return Task.Run(() => Recurse(n + 1));
        }
    }
}

結果は次のように正常に終了します。

100000

メソッド Recurse は呼び出した Recurse の終了を待ちません。そのため引数などは逐次解放されます。返された Task は合成され、Result プロパティで戻り値を取得できます。

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

C# 8.0のswitch式が書きやすくなるコードスニペット

switch式

C# 8.0で追加されたswitch式は前からあったswitch文にくらべると格段にスマートです。
break,caseステートメントが不要になり、さらに強力なパターンマッチングが使えるようになりました。
例で比較するとこのようになります。

曜日のenumを引数としてうけとり、なんらかの文字列をコンソールに出力するメソッドがあったとして、このようになります。

switch文
private static void PrintDayOld(DayOfWeek day)
{
    string text;

    switch (day)
    {
        case DayOfWeek.Sunday:
            text = "休日";
            break;
        case DayOfWeek.Saturday:
            text = "だいたい休日";
            break;
        default:
            text = "まれに休日";
            break;
    }

    Console.WriteLine(text);
}

?

switch式
private static void PrintDayNew(DayOfWeek day)
{
    var text = day switch
    {
        DayOfWeek.Sunday => "休日",
        DayOfWeek.Saturday => "だいたい休日",
        _ => "まれに休日"
    };

    Console.WriteLine(text);
}

コードスニペット

そんな便利なswitch式ですが、私は記憶力激弱なので、書く時に「あれswitchって条件のまえだっけ?」「caseはいらないんだっけ?」と混乱してしまいます。

一度switch文を書いて、VisualStudioにswitch式にリファクタリングしてもらってもよいですが、そもそもswitch文が長くてダサいからswitch式にしたいので書きたくないです。

なのでコードスニペットを自作してVisualStudioに追加しました。

SwitchExpression.snippet
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
      <Title>SwitchExpression</Title>
      <Author>soi</Author>
      <Description>switch 式を生成します。</Description>
      <Shortcut>switchex</Shortcut>
    </Header>
    <Snippet>
      <Declarations>
        <Literal Editable="true">
          <ID>expressionswitch</ID>
          <ToolTip>expression switch</ToolTip>
          <Default>expression</Default>
        </Literal>
        <Literal Editable="true">
          <ID>case1</ID>
          <Default>case1</Default>
        </Literal>
        <Literal Editable="true">
          <ID>value1</ID>
          <Default>value1</Default>
        </Literal>
        <Literal Editable="true">
          <ID>defaultValue</ID>
          <Default>defaultValue</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp" Delimiter="$"><![CDATA[$expressionswitch$ switch
{
    $case1$ => $value1$,
    _ => $defaultValue$
};]]></Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

使用方法

SwitchExpression.snippet -GitHub
上記リンクからダウンロードしたファイルを
VisualStudio>ツール>コードスニペットマネージャー>インポート
で選択してください。

switchexと入力すると下記コードに変換されます。expressionなどはTabでジャンプできます。

expression switch
{
    case1 => value1,
    _ => defaultValue
};

参考

https://ufcpp.net/study/csharp/datatype/typeswitch/?p=5#switch-expression

環境

VisualStudio 2019
C# 8
.NET Core 3.1

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

Unityで作成したアプリにおいて、ネットワーク接続を確認する場合に使うメソッド「Application.internetReachability」!

筆者の環境:OS:Windows10, Unity 2019.3.5f1 使用言語:C#
利用Asset:DOTween,Destructible2D
筆者のプログラミング歴:2020年1月よりUnity上でC#の勉強を開始。

ネットワークに接続しているかどうかを確認してくれる「Application.internetReachability」

初制作ゲームにて、Admob広告を掲載したのですがその際に、

『広告のLoadを待たずにゲームを開始すると、バナー広告の操作が上手くいかず、消すべき時に消せない。』

という不具合が出たので、広告のLoadが完了するまで、スタートボタンを表示させない、という無理やりな方法で
不具合を回避していました。
その為、インターネット接続していない端末はスタート出来ないので、それに対しての回避策を用意する、という更に面倒な事になっています。
今思えば、愚策中の愚策ですね。

現在開発中のゲームでもランキングを搭載予定なのもあり、ネットワーク接続しているかどうかの確認が出来れば、この問題は簡単に解決出来ると思い、調べてみました!
すぐ見つかりました(というか、一度調べてました)。

Application.internetReachabilityを使えば、ネットワーク接続しているかどうかの確認どころか、接続しているネットワークがWi-Fiなのかキャリアネットワーク(4Gとかですね)なのか?も、わかるそうです。

 ●NotReachable : インターネットに接続していない
 ●ReachableViaCarrierDataNetwork : キャリアネットワークで接続している
 ●ReachableViaLocalAreaNetwork : Wi-Fiでネットワークに接続している

以下、実装例。

void CheckNetwork()
    {        
        if (Application.internetReachability == NetworkReachability.NotReachable)
        {            
            Debug.Log("ネットワーク接続無し");
        }
        else if(Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork)
        {
            Debug.Log("キャリアネットワークに接続");
        }
        else if((Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
        {           
            Debug.Log("Wi-Fiネットワークに接続");
        }
    }

実際スマホアプリで使用する場合は、ネットワーク接続が無ければ、警告を表示するとかになりますね。
今回の筆者の場合は、スタートボタンは最初から表示しておいて、ボタンを押した時に広告Loadとネットワーク接続の確認をして、それぞれ処理を行う、って感じで実装する事になると思います。

ここまで読んでいただき、ありがとうございました。

参考にさせていただいたサイト:エクスプラボ 様
https://ekulabo.com/network-reachability

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

Unityで作成したアプリにおいて、ネットワーク接続を確認する場合に使う「Application.internetReachability!」

筆者の環境:OS:Windows10, Unity 2019.3.5f1 使用言語:C#
利用Asset:DOTween,Destructible2D
筆者のプログラミング歴:2020年1月よりUnity上でC#の勉強を開始。

ネットワークに接続しているかどうかを確認してくれる「Application.internetReachability」

初制作ゲームにて、Admob広告を掲載したのですがその際に、

『広告のLoadを待たずにゲームを開始すると、バナー広告の操作が上手くいかず、消すべき時に消せない。』

という不具合が出たので、広告のLoadが完了するまで、スタートボタンを表示させない、という無理やりな方法で
不具合を回避していました。
その為、インターネット接続していない端末はスタート出来ないので、それに対しての回避策を用意する、という更に面倒な事になっています。
今思えば、愚策中の愚策ですね。

現在開発中のゲームでもランキングを搭載予定なのもあり、ネットワーク接続しているかどうかの確認が出来れば、この問題は簡単に解決出来ると思い、調べてみました!
すぐ見つかりました(というか、一度調べてました)。

Application.internetReachabilityを使えば、ネットワーク接続しているかどうかの確認どころか、接続しているネットワークがWi-Fiなのかキャリアネットワーク(4Gとかですね)なのか?も、わかるそうです。

 ●NotReachable : インターネットに接続していない
 ●ReachableViaCarrierDataNetwork : キャリアネットワークで接続している
 ●ReachableViaLocalAreaNetwork : Wi-Fiでネットワークに接続している

以下、実装例。

void CheckNetwork()
    {        
        if (Application.internetReachability == NetworkReachability.NotReachable)
        {            
            Debug.Log("ネットワーク接続無し");
        }
        else if(Application.internetReachability == NetworkReachability.ReachableViaCarrierDataNetwork)
        {
            Debug.Log("キャリアネットワークに接続");
        }
        else if((Application.internetReachability == NetworkReachability.ReachableViaLocalAreaNetwork)
        {           
            Debug.Log("Wi-Fiネットワークに接続");
        }
    }

実際スマホアプリで使用する場合は、ネットワーク接続が無ければ、警告を表示するとかになりますね。
今回の筆者の場合は、スタートボタンは最初から表示しておいて、ボタンを押した時に広告Loadとネットワーク接続の確認をして、それぞれ処理を行う、って感じで実装する事になると思います。

ここまで読んでいただき、ありがとうございました。

参考にさせていただいたサイト:エクスプラボ 様
https://ekulabo.com/network-reachability

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

UniRx 間隔の短いタップを検出する

概要

ポインターが押された時と離された時の間隔が一定以下のときだけ発火する物をUniRxで作成しました。

赤のエフェクトは押されたこと、青は離されたことを示します。その間隔が短いときだけLogが出力されてます。
shorttap.gif

プロジェクト全体はこちらから
https://github.com/Arihide/unirx-shorttap

説明

短いのでソースコード丸ごと乗っけます。

using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UniRx;
using UniRx.Triggers;

public class ShortTap : UIBehaviour
{
    protected override void Awake()
    {
        base.Awake();

        const double shortTapThreshold = 200d;

        this.OnPointerUpAsObservable()
            .Timestamp() // 時間情報付与
            .WithLatestFrom( // ポインターが離れたとき、最後に押された時の情報と合体させる
                this.OnPointerDownAsObservable().Timestamp(),
                (l, r) => new { up = l, down = r }
            )
            // 押した時と離れたときの間隔が shortTapThreshold 以下のときだけ通す
            .Where(pair => (pair.up.Timestamp - pair.down.Timestamp) <= TimeSpan.FromMilliseconds(shortTapThreshold))
            .Subscribe(_ => Debug.Log("Short Tap!"));
    }

}

やってることとしては、
- ポインターが離れたときに、最後にポインターが押されたときの情報とニコイチにする
- 押された時と離れたときの時刻を比較し、閾値以下なら通す
という感じですね。

この方法は押されたときではなく、離れたときに発火するのがミソです。
また、条件式を調整すればロングタップなどにも対応できます。

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

C# 小ネタ:C# 8.0 風の書き方アレコレ

筆休めのための、ちょっとした小ネタです。C# 9.0 が今年の秋くらいに出ると思われるのですが、今の最新の C# 8.0 すらキャッチアップできてないのに…という気持ちになりますね。

ということで、完全な機能の網羅は岩永さんのサイトにお任せするとして、ここでは C# 8.0 らしい書き方で書いた方が良さそうなもので、自分的に利用頻度高めかな?と思うものを書いていこうと思います。

因みにここで紹介するものは C# 6 や 7 あたりで追加されたものとかも入ると思います。最近の C# の書き方って感じです。

変数の型の判定方法

あまり引数の型を判定して何か処理するみたいなのが多くあると、ちょっとどうかな?という感じですが、変数の型判定する事はありますよね。

古き良き書き方だと大体以下のような感じですね。

void SomeFunction(object x)
{
  var foo = x as Foo;
  if (foo != null)
  {
    Console.WriteLine("x は Foo 型");
  }
}

前は as で型変換してから、変換がうまくいっているかチェックするという二段構えになっていました。他の方法としては is 演算子で型チェックをしてからキャストする方法もありました。以下のような感じですね。

void SomeFunction(object x)
{
  if (x is Foo)
  {
    var foo = (Foo)x;
    Console.WriteLine("x は Foo 型");
  }
}

因みに、この方法は内部的には is で型をチェックして、さらにキャストするという感じなのでちょっと無駄があるので as の方がいいと言われてました。

最近は is での型判定のところで、そのまま変数を定義できるようになっているので以下のように書くのがスムーズです。

void SomeFunction(object x)
{
  if (x is Foo foo) // foo 変数定義できる!
  {
    Console.WriteLine("x は Foo 型");
  }
}

out 引数での結果の受け取り方

out 引数で結果を受け取るときは、昔は以下のように書いてました。

var input = Console.ReadLine();
int result;
if (int.TryParse(input, out result))
{
  Console.WriteLine($"入力された値を整数に変換できた! ${result}");
}

out 引数に渡すための変数をあらかじめ定義しておいて渡すという感じですね。型の判定のところでも説明したのと同じように out 引数のところで変数の定義ができます。

var input = Console.ReadLine();
if (int.TryParse(input, out var result)) // ここで宣言できる
{
  Console.WriteLine($"入力された値を整数に変換できた! ${result}");
}

プロパティの組み合わせの値に応じて処理を分けたい

例えば X, Y という int 型のプロパティのある Point 型で (0, 0) と (1, 1) と (1, 任意の値) と (任意の値, 1) と (任意の値, 任意の値) で処理がわかれるようなケースを考えてみましょう。

素直に書くとこんな感じですね

var p = new Point { X = 1, Y = 1 };
if (p.X == 0 && p.Y == 0)
{
  // (0, 0)
}
else if (p.X == 1 && p.Y == 1)
{
  // (1, 1)
}
else if (p.X == 1)
{
  // (1, 任意の値)
}
else if (p.Y == 1)
{
  // (任意の値, 1)
}
else
{
  // (任意の値, 任意の値)
}

コンパイル通してないので、間違えてるかもしれませんが素直に書くとこんな感じですよね。最近の C# ではパターンマッチがあるので、こんな感じに書けます。

var p = new Point { X = 10, Y = 1 };
switch (p)
{
  case { X: 0, Y: 0 }:
    // (0, 0)
    break;
  case { X: 1, Y: 1 }:
    // (1, 1)
    break;
  case { X: 1, Y: var y }
    // (1, 任意の値)
    break;
  case { X: var x, Y: 1 }
    // (任意の値, 1)
    break;
  case { X; var x, Y: var y }:
    // (任意の値, 任意の値)
    break;
  default:
    // ここにはこない
    throw new InvalidOperationException();
}

もし上のような switch での分岐の結果行う処理が単一ステートメントの実行結果を返すだけなら switch 式も使えます。

var p = new Point { X = 10, Y = 1 };
var result = p switch 
{
  { X: 0, Y: 0 } => "(0, 0)",
  { X: 1, Y: 1 } => "(1, 1)",
  { X: 1, Y: _ } => "(1, 任意の値)",
  { X: _, Y: 1 } => "(任意の値, 1)",
  { X: _, Y: _ } => "(任意の値, 任意の値)",
}

_ を指定することで値を捨てることができます、_ の代わりに var x のように書くとプロパティの値を変数に入れることができます。

因みに、この { ... } の書き方は型の判定も併せて行いたい場合は型名と合わせて使うこともできます。switch 式に特化した書き方でもないので if 文の中でも使うことができます。

void SomeFunction(object x)
{
  if (x is string { Length: 5 } s)
  {
    // 長さ 5 の文字列
  }
}

複数の値を返したい

タプルを返すようにすると擬似的に複数の戻り値を返すようなメソッドを定義できます。今までは out 引数を使うか、戻り値を表すクラスを定義するとかしないといけなかったのですが、タプルを使うという選択肢が追加されてます。

TryParse をラップして戻り値で bool と int を返すようにしてみようと思います。

(bool ok, int result) ParseInt(string s)
{
  var ok = int.TryParse(s, out var r);
  return (ok, r);
}

戻り値を Task<(bool ok, int result)> のようにすると非同期メソッドでも複数の値を返すことができます。

因みにタプルは以下のように代入したり switch に渡したり色々できるので便利です。

// こんな感じで代入できる
var (ok, result) = ParseInt("100");
Console.WriteLine($"{ok}, {result}"); // true, 100

// switch 式に渡したりも
var x = ParseInt("100") switch
{
  (true, var r) => r, // パースに成功したら結果を返す
  (false, _) => -1, // 失敗時は -1
};

ラムダ式を変数に入れたい

地味にめんどくさかったんですよね。var が使えなくて。

var f = () => Console.WriteLine("Hello world"); // だめ
Action f = () => Console.WriteLine("Hello world"); // OK

引数が多くなってくるとめんどくささが上がります。

こんな時はメソッド内でもメソッドを定義できるようになったので、それを使うと同じようなことができます。

void SomeFunction()
{
  // キャプチャ変数を使わないほうが性能には優しい。static をつけると変数のキャプチャをしないことを明示できる。
  static void f() => Console.WriteLine("Hello world");

  f(); // 呼び出し
}

null のときだけ代入したい

今までは if(x == null) { x = "xxx"; } みたいにするか下の例に書くように、初見殺し的に書くしかなかったのですが ??= 演算子でさくっと出来るようになりました。便利!!

class Hoge
{
    private Command _oldCommand;
    // 初見殺し
    public Command OldCommand =>
        _oldCommand ?? (_oldCommand = new Command(() => Console.WriteLine("Before")));

    private Command _newCommand;
    // いいね
    public Command NewCommand => 
        _newCommand ??= new Command(() => Console.WriteLine("After"));
}

コンソールアプリの Main でも非同期処理したい

前は void Main() みたいに voidint じゃないとだめだったのですが async Task Main() のように async もいけるようになりました。

using System;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main()
        {
            Console.WriteLine("Foo");
            await Task.Delay(1000);
            Console.WriteLine("Bar");
        }
    }
}

今までは、以下のようにしてお茶を濁してたのでとても便利です。

using System;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main()
        {
            RunAsync().Wait();
        }

        static async Task RunAsync()
        {
            Console.WriteLine("Foo");
            await Task.Delay(1000);
            Console.WriteLine("Bar");
        }
    }
}

まとめ

ということで、思いついた機能をつらつらと書いていきました。
因みに、iPad でコンパイルせずに書いてるので typo があったり間違ったことを言ってたら教えてください。

ではでは。

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

AtCoder / C#でPopCount

PopCountとは

数値を2進数に変換したときに、立っているbitの数を数えることを言います。
例: popcount(12) = 2
12を2進数で表すと1100になりますから、1の数は2つありますので、popcount(12)=2です

最近でいうと、エイシング プログラミング コンテスト 2020のD問題で出てきました。

C#ではどうやるのか

ちょっと前までは、愚直に、bitにしてstringにして1の数を数えたり、bit演算子を駆使したりしてできました。
参考:ビットカウントする高速アルゴリズムをPythonで実装しながら詳しく解説してみる

しかし、.Net Core3.0には、ハードウェア命令を直接操作できる関数が導入され、またそれのラッパーとしてBitOperationsというクラスが追加されています。
ただし、これは、CLS準拠ではないので他のコンパイラには含まれていません。

AtCoderのC#(.NET Core 3.1.201)を使うと、.Net Core 3.0で実装された関数を使うことができます。

参考:
System.Runtime.Intrinsics.X86 Namespace
BitOperations クラス

実行例

using System.Numerics;   // ←これが必要です
using System;
public class MainClass
{
    public static void Main()
    {
        // 引数はuint、もしくはulongになるのでint やlongで扱ってるときはキャストする
        Console.WriteLine(BitOperations.PopCount(12));  // 2
        Console.WriteLine(BitOperations.PopCount(100000000));  // 12
        Console.WriteLine(BitOperations.PopCount(2049));  //2
    }
}

AtCoderに提出するときは、コンパイラに『C#(.NET Core 3.1.201)』を選ぶのをわすれないようにしましょう。

他の便利操作

BitOperations.LeadingZeroCount(UInt64)
先頭から0bitの数を数える

BitOperations.TrailingZeroCount(Int64)
末尾から0bitの数を数える

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

.NET 5.0 Previewのアナウンスブログをまとめた

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

Seleniumを使ってOutDocを自動出力したい

Seleniumを使ったOutDoc(オープンソースのOutSystemsドキュメント出力モジュール)を操作するコードを書いてみました。
ログインから生成されたドキュメントのスクリーンショットを取るところまで。
ただし、長くなったので、1ページずつ取得する画像を1枚にまとめる手順は別途。

確認環境

Personal Environment(Version 11.8.0 (Build 12006))
Service Studio(Version 11.7.15)
Visual Studio Code(Version 1.47.2)

あと、NuGetで、以下のSeleniumライブラリを入れています。
Selenium.WebDriver(Version 3.141.0)
Selenium.WebDriver.ChromeDriver(Version 84.0.4147.3001)
Selenium.Support(Version 3.141.0)

ChromeDriverを使う時は、Chromeとバージョンを揃えないと以下のようなエラーが発生しました。

型 'System.InvalidOperationException' のハンドルされていない例外が WebDriver.dll で発生しました: 'session not created: This version of ChromeDriver only supports Chrome version 84 (SessionNotCreated)'

サンプルコード

https://github.com/jyunji-watanabe/OutSystemsUtilities/tree/master/OutDocExporter

ビルドしたら、.exeと同じ場所にappsettings.jsonをコピーして、PEのURLとログインアカウント、パスワードを設定。

実行は、以下のようにモジュール名をパラメータにする。
.\OutDocExporter.exe HousesoftSampleReactive

OutDocでの画面遷移

ログインからドキュメント出力までは以下のように動きます。
なお、OutDocはTraditional Webという種類のモジュールであるため、各要素のセレクターにidを使うことができません。idが自動生成されるため。

  1. ログイン画面:ユーザー名とパスワードを入力してログイン
  2. ホーム画面:ログイン後のトップページ。トップメニューからeSpace一覧画面を開く
  3. eSpace一覧画面:モジュール名で検索した後、対象モジュールのリンクを開く
  4. ドキュメント生成画面:開くとドキュメントの準備が始まる。準備をAjax Refreshで待ち、終わると自動でドキュメントを開くボタンが表示されるのでクリックする
  5. ドキュメント画面:開いたら、スクリーンショットをとる

実装

ページオブジェクト

UIテストの文脈で進められているデザインパターン。
各ページ毎に操作用のクラスを分割し、ページ内の操作をクラス内に閉じ込めることで、読みやすく、変更に強くなる。
これから作るのはUIテストではないが、わかりやすさのために踏襲する。

1. ログイン画面

image.png

  • 実際にはChrome操作用のChromeDriverを使いますが、別のブラウザに変えても使えるようにベースクラスのRemoteWebDriverをコンストラクタのパラメータに使っています
  • 各ページオブジェクトクラスのコンストラクタでは、想定通りのページを開けるか確認し、問題があればオリジナルの例外クラスをスロー(ログイン画面ではTitleで)
  • SetCredentialメソッドで、パラメータをUsernameとPasswordのテキストボックスに設定している
    • driver.FindElement(By.CssSelector())の呼び出しは、CSSセレクターを使ってHTMLの特定要素を取り出す処理
    • Usernameの入力部品は「table.FindElement(By.CssSelector("tr:nth-of-type(3) input"))」で取得している。これは、tableタグの、3行目にあるinputタグという指定
    • inputタグを取得したら、.Clear()で入力済みの値をきれいにしてから、SendKeysでパラメータの値を設定
  • Loginボタンは、ボタンがページ内に一つしかないことから、「input[type=submit]」で指定できる
  • 画面遷移するメソッドは、戻り値で遷移先のページオブジェクトクラスを返す
LoginPage.cs
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;

public class Login 
{
    private const string PageTitle = "Login";
    private RemoteWebDriver driver;
    public Login(RemoteWebDriver driver)
    {
        this.driver = driver;
        if (this.driver.Title != Login.PageTitle)
        {
            throw new IllegalPageStateException(
                "ページタイトルが想定と異なります(想定:" + Login.PageTitle + "、実際:" + this.driver.Title + ")");
        }
    }

    public void SetCredential(string userName, string password)
    {
        // UserNameとPasswordを入力
        var table = driver.FindElement(By.CssSelector(".MainContent table table"));
        var userInput = table.FindElement(By.CssSelector("tr:nth-of-type(3) input"));
        userInput.Clear();
        userInput.SendKeys(userName);
        var passwordInput = table.FindElement(By.CssSelector("tr:nth-of-type(5) input"));
        passwordInput.Clear();
        passwordInput.SendKeys(password);
    }

    public HomeScreen LoginAndGoToHomeScreen()
    {
        driver.FindElement(By.CssSelector("input[type=submit]")).Click();
        return new HomeScreen(this.driver);
    }
}

2.ホーム画面

image.png

このページは中身がないので、赤枠のタブを開くだけの処理。

  • 「.Menu_TopMenus」はトップメニューのWeb Block内で、明示的に指定されているクラス名で変更の恐れが無いため使っている
  • トップメニュー内の各メニューはContainer Widget(=>divタグ)内にLink Widget(=>aタグ)という構成であることから「.Menu_TopMenus>div>a」でトップメニュー内のメニューリンクを指定できる。nth-of-type(2)は同じ階層にあるdivタグの2番め、すなわち左から2番めのメニューを指している
HomeScreen.cs
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;

public class HomeScreen
{
    // (中略)
    public ESpaceList MoveToESpaceList()
    {
        this.driver.FindElementByCssSelector(".Menu_TopMenus>div:nth-of-type(2)>a").Click();
        return new ESpaceList(this.driver);
    }
}

3.eSpace一覧画面

image.png

トップメニューで、「eSpaces」を選択すると開く画面。

  • コンストラクタで、トップメニューの正しいタブが開かれている(Active)になっていることをチェック
  • ①モジュール一覧はページングされているため、目標のモジュールが最初に表示されていないことがある → 検索ボックスにモジュール名を入力してSearchボタンをクリック
  • ②検索処理が遅いため、対象モジュールのリンクが表示されるまで待ち処理を入れてある
    • OpenQA.Selenium.Support.UI.WebDriverWaitを使う
    • コンストラクタでは、最長待ち時間に2分を設定しています。環境によって調整してください
    • WebDriverWait.Untilに待ち条件を指定するのですが、色々なサンプルにのっているExpectedConditionsを使った指定はdeprecatedとマークされています。ただ、その代替として挙げられていた匿名関数を使った方法だと待ち判定が発生するたびに例外(条件が満たされていない)が投げられるので、ExpectedConditionsで指定
    • ElementIsVisibleはセレクタで示した要素が表示されるまで待つという条件
  • ③モジュール名と一致するリンクは一つしかないはずなので、表示されたらクリック
ESpaceList.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI;

public class ESpaceList
{
    private const string TabTitle = "eSpaces";
    private RemoteWebDriver driver;
    public ESpaceList(RemoteWebDriver driver)
    {
        this.driver = driver;
        // トップメニューのアクティブなタブ内テキスト
        var activeTabTextInTheTopMenu = driver.FindElementByCssSelector(".Menu_TopMenuActive>a").Text;
        if (activeTabTextInTheTopMenu != ESpaceList.TabTitle)
        {
            throw new IllegalPageStateException(
                "選択されているタブが想定と異なります(想定:" + ESpaceList.TabTitle + "、実際:" + activeTabTextInTheTopMenu + ")");
        }
    }

    public ESpaceDesignFeedBack OpenESpace(string eSpaceName)
    {
        // 検索キーワードの設定
        var searchInput = this.driver.FindElementByCssSelector(".Filters input[type=text]");
        searchInput.Clear();
        searchInput.SendKeys(eSpaceName);
        // 検索ボタンクリック
        this.driver.FindElementByCssSelector(".Filters input[type=submit]").Click();
        // 検索結果の1ページ目にリンクテキストが、指定eSpace名であるLinkが表示される(=検索される)のを待ってクリック
        var waitForLinkWitheSpaceName = new WebDriverWait(this.driver, new System.TimeSpan(0, 2, 0));
        // 匿名関数で実装すると、探索のたびに例外を投げるので、deprecatedだが、いったんExpectedConditionsで実装しておく
        waitForLinkWitheSpaceName.Until(ExpectedConditions.ElementIsVisible(By.LinkText(eSpaceName)))
                                 .Click();
        // 対象モジュールのドキュメント生成ページが開く
        return new ESpaceDesignFeedBack(this.driver, eSpaceName);
    }
}

4.ドキュメント生成画面

image.png

画面を開くと、ローディングアイコンが表示されます。しばらくしてドキュメントの準備ができると「Open Documentation」というボタンが表示される。このボタンをクリックすると、目標のドキュメントが開きます。

  • ボタンの表示を待つ処理は、基本的に、上でLinkの表示を待ったときと同じ
  • セレクタの「.Button[value^=Open]」はButtonクラスがあり、かつvalue属性が「Open」で始まるもの、という指定
ESpaceDesignFeedBack.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.UI;

public class ESpaceDesignFeedBack
{
    // (中略)

    /// <summary>
    /// Open Documentationボタンが表示されるまで待ち、クリックする
    /// </summary>
    public void WaitForButtonAndClick()
    {
        // 最長2分間待つタイマー
        var waitForOpenDocumentButton = new WebDriverWait(this.driver, new System.TimeSpan(0, 2, 0));
        // 「Open Documentation」というvalueを持つボタンが表示されるのを待ち、表示されたらクリック
        waitForOpenDocumentButton.Until(ExpectedConditions.ElementIsVisible(By.CssSelector(".Button[value^=Open]")))
                                 .Click();
    }
}

スクリーンショット取得

以前は、FirefoxDriverを使うと、ページ全体のスクリーンショットを取れたようですが、今は仕様変更でだめでした。
よって、スクリーンショット撮影 → 縦方向にスクロールを繰り返してページ全体のスクリーンショット群を取得する処理にしています。

処理が長くなりすぎたので、画像を1枚にまとめるのは別の機会に。

参考記事

Chromeでフルサイズのスクリーンショットを撮るためのパッチ
を参考にしました。

OutDocの場合は、横スクロールが必要ないので、縦方向のみにしています。

コード

  • staticメソッドにしてあるので、呼び方は、ScreenShot.TakeWholePageAsScreenShot(driver)
  • ちょっとさぼって、出力先は「C:\work\ss」に固定。適宜修正してください
  • スクロールの判定には、ページ全体の高さとスクリーンショット1回分の高さをJavaScriptで取得して使っている
    • JavaScriptを実行したい場合は、WebDriverをOpenQA.Selenium.IJavaScriptExecutorインターフェースにキャストして、ExecuteScriptを呼び出す
    • スクロールさせるのもJavaScript
  • WebDriverにスクリーンショットを取らせる場合、OpenQA.Selenium.ITakesScreenShotインターフェースにキャストして、GetScreenShotでScreenshotオブジェクトを取得、さらにそのSaveAsFileで指定パスにファイルを保存
  • このロジックだと指定パスに日時_00001.png, 日時_00002.png……のようなファイルが出来上がる
ScreenShot.cs
using OpenQA.Selenium.Remote;
using OpenQA.Selenium;
public class ScreenShot
{
    public static string Path = "C:\\work\\ss";
    public static void TakeWholePageAsScreenShot(RemoteWebDriver driver)
    {
        // 仕様単純化のため、横スクロールは不要を前提とする
        var jsExecutor = driver as IJavaScriptExecutor;
        // JavaScriptを使って、スクロール回数の計算に使う値を取得
        var totalHeight = (long)jsExecutor.ExecuteScript("return document.body.scrollHeight;");
        var pageHeight = (long)jsExecutor.ExecuteScript("return window.innerHeight;");
        // スクロール制御用
        var scrolledHeight = (long)0;
        var currentImageCount = 1;  // ファイル名末尾に連番をつけるための変数。今何番目の画像かを示す
        var filePathPrefix = Path + "\\" + System.DateTime.Now.ToString("yyyyMMdd_HHmmss_");

        // 1ページ分ずつスクロールしながら、スクリーンショットを撮っていく
        var iTakesScreenshot = (ITakesScreenshot)driver;
        while(scrolledHeight < totalHeight)
        {
            jsExecutor.ExecuteScript($"window.scrollTo(0, {scrolledHeight});");
            iTakesScreenshot.GetScreenshot()
                            .SaveAsFile(filePathPrefix + currentImageCount.ToString().PadLeft(5, '0') + ".png");

            // ループ制御
            currentImageCount++;
            scrolledHeight += pageHeight;
        }
    }
}

メインプログラム

  • 設定ファイルから、URL、ユーザー名、パスワード、パラメータからモジュール名を取得
  • 処理本体では、順にページオブジェクトを使って画面遷移していき、最後でスクリーンショット
using System;
using System.IO;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Interactions;
using Microsoft.Extensions.Configuration;

class Program
{
    static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            System.Console.WriteLine("ドキュメントを出力するモジュール名を指定してください。");
            return;
        }
        var eSpaceName = args[0];
        if (Directory.Exists(ScreenShot.Path) == false)
        {
            System.Console.WriteLine($"出力先フォルダ{ScreenShot.Path}を作成するか、出力先フォルダを変更してください");
            return;
        }
        IConfiguration configuration = new ConfigurationBuilder()
                                            .AddJsonFile("appsettings.json", true, true)
                                            .Build();
        var url = configuration.GetSection("HostAddress").Value + "OutDoc";
        var userName = configuration.GetSection("UserName").Value;
        var password = configuration.GetSection("Password").Value;
        try
        {            
            DownloadDocument(url, userName, password, eSpaceName);
        }
        catch (System.Exception ex)
        {
            System.Console.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace);
        }
    }

    private static void DownloadDocument(string url, string userName, string password, string eSpaceName)
    {
        using (var driver = new ChromeDriver())
        {
            // ログインページヘ遷移し、ログインを実行する
            driver.Navigate().GoToUrl(url);
            var loginPage = new Login(driver);
            loginPage.SetCredential(userName, password);
            var homeScreen = loginPage.LoginAndGoToHomeScreen();
            // eSpacesページへ遷移する
            var eSpacesList = homeScreen.MoveToESpaceList();
            // eSpace名で検索して対象モジュールのみ表示した上で、クリックして開く(ドキュメントページへ)
            var eSpaceDesingFeedBack = eSpacesList.OpenESpace(eSpaceName);
            // 生成されたドキュメントを開く
            eSpaceDesingFeedBack.WaitForButtonAndClick();
            // スクリーンショットを取得
            ScreenShot.TakeWholePageAsScreenShot(driver);

            var debugdummy = "dummy";   // デバッグ用(ブラウザ表示した状態でブレークするため)ダミー行
        }
    }
}

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

.NET用テキストテーブルライブラリ「FluentTextTable」を公開しました!

「オブジェクトを手軽にテーブル状にテキスト出力したい!」

そんなことを思ったことはありませんか?

これは何?

FluentTextTableは、柔軟で簡単な「全角文字に対応した」.NET用のテキストテーブルライブラリです。デバッグや簡単なツールなどに利用できます。

つぎのような簡単なコードから・・・

var users = new[]
{
    new User {Id = 1, Name = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, Name = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

このようにコンソールにテーブル状に値を表示することができます。

basicforjp.png

複雑なテーブルの書式を、簡単かつ流暢(Fluent)に設定できます。

Build
    .TextTable<User>(builder =>
    {
        builder
            .Borders.Horizontals.AllStylesAs("-")
            .Borders.HeaderHorizontal.AllStylesAs("=")
            .Columns.Add(x => x.Id).HorizontalAlignmentAs(HorizontalAlignment.Right)
            .Columns.Add(x => x.Name).VerticalAlignmentAs(VerticalAlignment.Center)
            .Columns.Add(x => x.Birthday).VerticalAlignmentAs(VerticalAlignment.Bottom).FormatAs("{0:yyyy/MM/dd}")
            .Columns.Add(x => x.Occupations).FormatAs("- {0}");
    })
    .WriteLine(users);

formatted.png

つぎの書式のカスタマイズをサポートとしています。

  • 列指定(もしくは列自動生成)
  • オブジェクトのフォーマット
  • 水平・垂直アライメント
  • 罫線文字の変更
  • マージンとパディング

マークダウン形式での出力にもサポートしています。

Build
    .MarkdownTable<User>()
    .WriteLine(users);

markdown.png

クイックスタート

.NET Framework 4.0以上、.NET Standard 2.0以上をサポートしています。NuGetからインストールして利用してください。

> Install-Package FluentTextTable

出力対象となるクラスを定義します。

public class User
{
    public int Id { get; set; }
    public string EnglishName { get; set; }
    public string JapaneseName { get; set; }
    public DateTime Birthday;
}

Buildクラスを利用して出力対象クラス用のテーブルを作成します。

デフォルトではpublicなプロパティ・フィールドのすべてが出力の対象となります。

var table = Build.TextTable<User>();

行に該当するオブジェクトを生成して、出力します。

var users = new[]
{
    new User {Id = 1, EnglishName = "Bill Gates", JapaneseName = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, EnglishName = "Steven Jobs", JapaneseName = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

詳細な利用方法

Githubに利用方法をまとめています。

機会があればぜひご利用ください!

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

.NET用全角対応テキストテーブルライブラリ「FluentTextTable」を公開しました!

「オブジェクトを手軽にテーブル状にテキスト出力したい!」

そんなことを思ったことはありませんか?

これは何?

FluentTextTableは、柔軟で簡単な「全角文字に対応した」.NET用のテキストテーブルライブラリです。デバッグや簡単なツールなどに利用できます。

つぎのような簡単なコードから・・・

var users = new[]
{
    new User {Id = 1, Name = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, Name = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

このようにコンソールにテーブル状に値を表示することができます。

basicforjp.png

複雑なテーブルの書式を、簡単かつ流暢(Fluent)に設定できます。

Build
    .TextTable<User>(builder =>
    {
        builder
            .Borders.Horizontals.AllStylesAs("-")
            .Borders.HeaderHorizontal.AllStylesAs("=")
            .Columns.Add(x => x.Id).HorizontalAlignmentAs(HorizontalAlignment.Right)
            .Columns.Add(x => x.Name).VerticalAlignmentAs(VerticalAlignment.Center)
            .Columns.Add(x => x.Birthday).VerticalAlignmentAs(VerticalAlignment.Bottom).FormatAs("{0:yyyy/MM/dd}")
            .Columns.Add(x => x.Occupations).FormatAs("- {0}");
    })
    .WriteLine(users);

formatted.png

つぎの書式のカスタマイズをサポートとしています。

  • 列指定(もしくは列自動生成)
  • オブジェクトのフォーマット
  • 水平・垂直アライメント
  • 罫線文字の変更
  • マージンとパディング

マークダウン形式での出力にもサポートしています。

Build
    .MarkdownTable<User>()
    .WriteLine(users);

markdown.png

クイックスタート

.NET Framework 4.0以上、.NET Standard 2.0以上をサポートしています。NuGetからインストールして利用してください。

> Install-Package FluentTextTable

出力対象となるクラスを定義します。

public class User
{
    public int Id { get; set; }
    public string EnglishName { get; set; }
    public string JapaneseName { get; set; }
    public DateTime Birthday;
}

Buildクラスを利用して出力対象クラス用のテーブルを作成します。

デフォルトではpublicなプロパティ・フィールドのすべてが出力の対象となります。

var table = Build.TextTable<User>();

行に該当するオブジェクトを生成して、出力します。

var users = new[]
{
    new User {Id = 1, EnglishName = "Bill Gates", JapaneseName = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, EnglishName = "Steven Jobs", JapaneseName = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

詳細な利用方法

Githubに利用方法をまとめています。

機会があればぜひご利用ください!

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

全角対応テキストテーブルライブラリ「FluentTextTable」を公開しました!

「オブジェクトを手軽にテーブル状にテキスト出力したい!」

そんなことを思ったことはありませんか?

これは何?

FluentTextTableは、柔軟で簡単な「全角文字に対応した」.NET用のテキストテーブルライブラリです。デバッグや簡単なツールなどに利用できます。

つぎのような簡単なコードから・・・

var users = new[]
{
    new User {Id = 1, Name = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, Name = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

このようにコンソールにテーブル状に値を表示することができます。

basicforjp.png

複雑なテーブルの書式を、簡単かつ流暢(Fluent)に設定できます。

Build
    .TextTable<User>(builder =>
    {
        builder
            .Borders.Horizontals.AllStylesAs("-")
            .Borders.HeaderHorizontal.AllStylesAs("=")
            .Columns.Add(x => x.Id).HorizontalAlignmentAs(HorizontalAlignment.Right)
            .Columns.Add(x => x.Name).VerticalAlignmentAs(VerticalAlignment.Center)
            .Columns.Add(x => x.Birthday).VerticalAlignmentAs(VerticalAlignment.Bottom).FormatAs("{0:yyyy/MM/dd}")
            .Columns.Add(x => x.Occupations).FormatAs("- {0}");
    })
    .WriteLine(users);

formatted.png

つぎの書式のカスタマイズをサポートとしています。

  • 列指定(もしくは列自動生成)
  • オブジェクトのフォーマット
  • 水平・垂直アライメント
  • 罫線文字の変更
  • マージンとパディング

マークダウン形式での出力にもサポートしています。

Build
    .MarkdownTable<User>()
    .WriteLine(users);

markdown.png

クイックスタート

.NET Framework 4.0以上、.NET Standard 2.0以上をサポートしています。NuGetからインストールして利用してください。

> Install-Package FluentTextTable

出力対象となるクラスを定義します。

public class User
{
    public int Id { get; set; }
    public string EnglishName { get; set; }
    public string JapaneseName { get; set; }
    public DateTime Birthday;
}

Buildクラスを利用して出力対象クラス用のテーブルを作成します。

デフォルトではpublicなプロパティ・フィールドのすべてが出力の対象となります。

var table = Build.TextTable<User>();

行に該当するオブジェクトを生成して、出力します。

var users = new[]
{
    new User {Id = 1, EnglishName = "Bill Gates", JapaneseName = "ビル・ゲイツ", Birthday = DateTime.Parse("1955/10/28")},
    new User {Id = 2, EnglishName = "Steven Jobs", JapaneseName = "スティーブ・ジョブズ", Birthday = DateTime.Parse("1955/2/24")}
};
Build
    .TextTable<User>()
    .WriteLine(users);

詳細な利用方法

Githubに利用方法をまとめています。

機会があればぜひご利用ください!

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

UnityでもRoslynで構文解析やコード評価したい

はじめに

RoslynはC#の構文解析やコード評価を行えるライブラリでC# 6以降のコンパイラでも使われています。Roslynを利用すると構文解析された結果を利用できるので文字列比較や正規表現と比べると解析漏れをなくせます。

RoslynのインストールはNuGetで使えますがUnityだと重複するdllがありそのままでは導入できません。それをUPMで簡単に導入する方法が分かったのでまとめてみました。

Unityでは以下のプロダクトでRoslynが使われています。

環境構築

Unity2018.3以上

  • Package Managerから Add package from git URL... できるバージョンでは com.unity.code-analysis を追加する
  • Package Managerから Add package from git URL... できないバージョンでは Packages/manifest.json"com.unity.code-analysis": "0.1.2-preview", を追加する

インストールできると以下のようにPackage Managerに追加されます。

image.png

インストールだけではDLLを参照できないのでRoslynを使いたいスクリプトフォルダにアセンブリ定義を作り、以下の設定を変更します。

  • 一般 > リファレンスをオーバーライドをチェック
  • アセンブリ参照に Microsoft.CodeAnalysis で始まるdllを追加
  • エディタ拡張で使うならプラットフォームの Editor のみをチェック、アプリ内で使うならデフォルトの 任意のプラットフォーム のままでOK

image.png

Roslynのサンプル

Scripting API SamplesGetting Started C# Syntax Analysisを参考にサンプルを実行してみます。それぞれcode欄に実行または解析するコード、result欄にその結果を表示しています。また実行できるUnityプロジェクトは https://github.com/shiena/UnityRoslynSample にありメニューの Tools > Roslyn Sample を選択するとウインドウが開きます。

Evaluate a C# expression

コードを実行します。

string code = "1 + 2";
var result = CSharpScript.EvaluateAsync(code);

image.png

Evaluate a C# expression (strongly-typed)

ジェネリクスで結果の型を指定してコードを実行します。

string code = "1 + 2";
var result = CSharpScript.EvaluateAsync<int>(code);

image.png

Parameterize a script

クラス定義したパラメータをコードに適用して実行します。

public class Globals
{
    public int X;
    public int Y;
}
string code = "X+Y";
var globals = new Globals {X = 1, Y = 2};
var result = CSharpScript.EvaluateAsync<int>(code, globals: globals);

image.png

Query Methods

コードを解析してMainメソッドの最初の引数を出力します。

string code =
            @"using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);

var root = (CompilationUnitSyntax) tree.GetRoot();
var firstMember = root.Members[0];
var helloWorldDeclaration = (NamespaceDeclarationSyntax) firstMember;
var programDeclaration = (ClassDeclarationSyntax) helloWorldDeclaration.Members[0];
var mainDeclaration = (MethodDeclarationSyntax) programDeclaration.Members[0];
var argsParameter = mainDeclaration.ParameterList.Parameters[0];
var firstParameters = from methodDeclaration in root.DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
            where methodDeclaration.Identifier.ValueText == "Main"
            select methodDeclaration.ParameterList.Parameters.First();
var argsParameter2 = firstParameters.Single();

image.png

SyntaxWalkers

コードを解析してSystemまたはSystem.以外で始まるusingを出力します。

class UsingCollector : CSharpSyntaxWalker
{
    public readonly List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>();

    public override void VisitUsingDirective(UsingDirectiveSyntax node)
    {
        if (node.Name.ToString() != "System" &&
            !node.Name.ToString().StartsWith("System."))
        {
            this.Usings.Add(node);
        }
    }
}
string code =
            @"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;

    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;

        class Foo { }
    }

    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;

        class Bar { }
    }
}";

SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = (CompilationUnitSyntax) tree.GetRoot();

var collector = new UsingCollector();
collector.Visit(root);

image.png

参考リンク

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

ContentAlignmentとStringAlignmentの相互変換

背景

LabelのTextを自力でDrawしたかったんです。
OnPaintの中でDrawStringすればオッケです。
簡単でしょ?

詳細

なるべく、Labelコントロールのプロパティを尊重したいです。
特にTextAlignプロパティは重要でした。

ところで、Label.TextAlign は ContentAlignment 型のプロパティです。
えーと、こんな感じ

image

しかし、DrawStringで表示位置を制御するにはStringAlignment型のアトリビュート二つ、つまり

  • Alignment
  • LineAlignment

の組み合わせで、縦横の位置を指定しなければなりません。
つまりAlignmentは、この位置を表していて

image

LineAlignmentは、この位置を表していて

image

この組み合わせで、ContentAlignmentと同じ位置指定ができるようになっている。
ちゅー訳です。

NearとFarの位置関係は日本語ではの話、と思ってね

分析

ところで、ContentAlignment型はenumで以下の値を持っています。

TopLeft = 1
TopCenter = 2
TopRight = 4
MiddleLeft = 16
MiddleCenter = 32
MiddleRight = 64
BottomLeft = 256
BottomCenter = 512
BottomRight  =1024

StringAlignment型もenumで以下の値。

Near = 0
Center = 1
Far = 2



因みに、ContentAlignmentを図示すると以下の形。

image

enumの値は実際に立ったビットが2進数値としてどう評価されるかの値。


一方、StringAlignmentは下図。

image

enum値はただの、値を持った識別子。

変換 その1(あっち ⇒ こっち)

えーと、まずは分かり易そうな方から…

StringAlignmentはただの値、と云いながらもよ~く見てみると以下の対応関係が成り立つようになっています。

image

Left = Near なのでContentAlignment(の一部)とStringAlignmentの位置関係は、上の図のようになります。
で、着目すべきは赤い数字。

つまり、$2^{StringAlignment}$とすると、その結果が1だったり2だったり4だったりして、ContentAlignmentの対応位置にビットを立てる事ができるって訳です。


次は垂直方向の位置合わせです。
サクッと図にしてしまうと以下のような感じです。

image

Top / Middle / Bottom其々を4ビットのブロックとして考えると、StringAlignmentの値とブロックの位置関係を対応付ける事が出来ます。
つまり、水平位置の変換結果に垂直位置の変換値を掛け合わせる事で、ContentAlignmentに変換されます。
こう云う事―

ContentAlignment = Math.Pow(2, Alignment) * Math.Pow(16, LineAlignment);

わっかり難ーい!
と云う時には以下の様に考えても可。

$2^{Alignment}$ で求めた水平配置情報を―

  • 0ブロック左シフトすると、垂直方向にはTopに配置される
  • 1ブロック左シフトすると、垂直方向にはMiddleに配置される
  • 2ブロック左シフトすると、垂直方向にはBottomに配置される
ContentAlignment = Math.Pow(2, Alignment) << (4 * LineAlignment);

4ビット左シフトすると云う事は、(2の4乗)=16を掛けるってのと同義だからね。


さて―
簡単な方は片付いたけど、本来やりたかったのはContentAlignmentを二つのStringAlignemntに分割する方でした。

変換 その2(こっち ⇒ あっち)

あっちからこっちへの変換は数式でできたので、こっちからあっちへの逆変換も数式で出来る筈です。
数ⅡBを思い出しましょう!

冪状の逆関数は対数になります。
$2^n = x$ なら、$n = log_2 x$ と云う事です。

配置 冪乗 対数
left $2^0 = 1$ $0 = log_2 1$
center $2^1 = 2$ $1 = log_2 2$
right $2^2 = 4$ $2 = log_2 4$



さて、もう一回この図を持ってきて逆の見方で読み解くと-

image

もし ContentAlignment の値が $1$ ならば、$log_2 1 = 0$ で、(Topの)Nearです。
もし ContentAlignment の値が $2$ ならば、$log_2 2 = 1$ で、(Topの)Middleになります。
もし ContentAlignment の値が $4$ ならば、$log_2 4 = 2$ で、(Topの)Farに変換できます。

じゃぁ、垂直方向がTop以外の場合はどうなるかと云うと、この場合も先ほどとは逆に4ビット単位で右シフトしてあげれば良いですね。

ここも例に依って対数です。
一つのブロックが4ビット($=2^4=16$)単位ですので $log_{16} ContentAlignment$ とすれば、ビットがどのブロックに含まれているかを知る事が出来ます。

えーと、ここにきて更に公式ですけど…
C#のMathライブラリにおいて扱えるのは、常用対数か自然対数のどちらか1です。(対数の底が $10$ か $e$ かって事)
上に出てきた様に底を $2$とか $16$ にしたければ以下の変換公式を使う必要があります。

$log_x n = log_{10} n / log_{10} x$

ここら辺を考慮すると縦位置はー

LineAlignment = Math.Floor(Math.Log10(ContentAlignment) / Math.Log10(16))

横位置の方は縦位置に従って右シフトした上で対数変換すれば…

Alignment = Math.Log10(ContentAlignment >> (4 * LineAlignment)) / Math.log10(2)

で、縦横のStringAlignmentに変換できました。

じゃぁ、C#で表現してみましょう

多分、Extensionにした方が使い易い様な気がする…
ちょっと癖が出てしまいましたが、双方向ともStringFormatに対する拡張メソッドになります。

public static class ExtClass {
    public static ContentAlignment ToContentAlignment(this StringFormat Me) {
        return (ContentAlignment)((int)Math.Pow(2, (int)Me.Alignment) << (4 * (int)Me.LineAlignment));
    }

    public static void SetStringAlignment(this StringFormat Me, ContentAlignment ca) {
        int Valignment = (int)Math.Floor(Math.Log10((int)ca) / Math.Log10(16));
        int Halignment = (int)(Math.Log10((int)ca >> (4 * Valignment)) / Math.Log10(2));

        Me.LineAlignment = (StringAlignment)Valignment;
        Me.Alignment = (StringAlignment)Halignment;
    }
}
使い方
    StringFormat sf = new StringFormat();
    Console.WriteLine(sf.ToContentAlignment());            //  sfのAlignmentをContentAlignmentで取得

    sf.SetStringAlignment(ContentAlignment.BottomCenter);  //  ContentAlignemnetからsfにAlignmentをコピー
    Console.WriteLine(sf.ToContentAlignment());

てな感じで…


  1. .NET Frmeworkの場合 

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