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

Xamarin.Forms + Entity Framework Core SQLiteで組みたい場合に最初にやること

環境

  • Windows10 Pro (20H2) 64bit
  • Visual Studio 2019 16.8.4
  • Xamarin 16.8.000.261

要約

たまーにXamarin触るんですけど、大体EFCoreとSQLite周りでハマります。
主にMigration出来るようになるまでの手順が分からなくなります。
そろそろプロジェクト作る都度ネットで色々調べて初期設定する状態から卒業したいので、
自分用メモ兼ねて最初の手順をまとめておきます。

手順

Xamarin.Formsのプロジェクトを立てる

image.png
ここは詰まるポイント無し。Visual Studioさんに全投げ。

ModelとDbContext用のプロジェクトを立てる

データベースに保存するテーブル用クラスと、アクセス用DbContextをまとめるDLL用のプロジェクトを作ります。
.NET Standardのクラスライブラリで良いです。
image.png
image.png

Migration用にコンソールアプリのプロジェクトを追加する

Migration実行するときには.NET Coreで動くプロジェクトが必要です。
Xamarinは動かせないので、Migrationをするためだけのダミープロジェクトを追加します。
image.png
image.png

Nugetから必要なパッケージをインストール

Mictosoft.EntityFramework.Core.Sqliteと、Mictosoft.EntityFramework.Core.ToolsをNugetからインストールします。

  • Mictosoft.EntityFramework.Core.Sqlite
    image.png

  • Mictosoft.EntityFramework.Core.Tools
    image.png

Migration用プロジェクトからDLL用プロジェクトを参照設定

image.png
こうやって

image.png
こう。ここも詰まるポイント無しです。

ModelとDbContextを作成する

先に作ったDLL用プロジェクトにModelとDbContextを追加します。
ざっくり書いていきます。

Model\Bookshelf.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace XamaEFCSqlite.Common.Model
{
    /// <summary>
    /// 本棚
    /// </summary>
    public class Bookshelf
    {
        /// <summary>
        /// ID
        /// </summary>
        public Guid BookshelfID { get; set; }

        /// <summary>
        /// 場所
        /// </summary>
        public string Location { get; set; }

        /// <summary>
        /// 容量
        /// </summary>
        public int Capacity { get; set; }

        public IList<Book> Books { get; set; }

        public Bookshelf()
        {
            BookshelfID = Guid.NewGuid();
            Location = string.Empty;
            Capacity = 0;
            Books = new List<Book>();
        }
    }
}
Model\Book.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace XamaEFCSqlite.Common.Model
{
    /// <summary>
    /// 本
    /// </summary>
    public class Book
    {
        /// <summary>
        /// 本ID
        /// </summary>
        public Guid BookID { get; set; }

        /// <summary>
        /// タイトル
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// ページ数
        /// </summary>
        public int PageCount { get; set; }

        public Book()
        {
            BookID = Guid.NewGuid();
            Title = string.Empty;
            PageCount = 0;
        }
    }
}
StoreDbContext.cs
using Microsoft.EntityFrameworkCore;
using System;
using XamaEFCSqlite.Common.Model;

namespace XamaEFCSqlite.Common
{
    public class StoreDbContext : DbContext
    {
        public DbSet<Bookshelf> Bookshelves { get; set; }
        public DbSet<Book> Books { get; set; }
    }
}

とりあえずモデル2つとコンテキストを作りました。

DbContextに細工

MigrationをするためにはDbContextにパラメータなしのコンストラクタと、データベースプロバイダーを構成する手段が必要です。
「データベースプロバイダーを構成する手段」で手っ取り早いのは、OnConfigureをoverrideすることっぽいので、実装していきます。

StoreDbContext.cs
using Microsoft.Data.Sqlite;            //ここを追加
using Microsoft.EntityFrameworkCore;
using System;
using XamaEFCSqlite.Common.Model;

namespace XamaEFCSqlite.Common
{
    public class StoreDbContext : DbContext
    {
        public DbSet<Bookshelf> Bookshelves { get; set; }
        public DbSet<Book> Books { get; set; }

        //--- ここから追加 ---
        const string dbName = "sample.db";
        string dbPath;

        public StoreDbContext() : this(string.Empty) { }

        public StoreDbContext(string DatabasePath)
        {
            dbPath = DatabasePath;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);

            string path = System.IO.Path.Combine(dbPath, dbName);
            SqliteConnectionStringBuilder builder = new SqliteConnectionStringBuilder()
            {
                DataSource = path,
                ForeignKeys = true
            };
            optionsBuilder.UseSqlite(builder.ConnectionString);
        }
        //--- ここまで追加 ---
    }
}

いきなりごっそり増えましたね。
ここは正直有識者がいらっしゃったら是非相談したいポイントです。

SQLiteのプロバイダーを指定するため、OnConfiguring内でUseSqliteしています。
この時、UseSqliteの引数として接続文字列を渡す必要があります。

ファイル名は定数[dbName]に指定してありますが、ファイルを保存しておく場所は実行するOS(AndroidとかiOSとか)によって変わります
その為、コンストラクタでファイルの場所だけ受け取れるようにしておいて、接続文字列を組み立てる際に完全パスに変えています。

そしてMigrationが実行できるように、パラメーターを受け取らないコンストラクタも定義します。
実際のコードの中でパラメーター無しコンストラクタを呼び出すと、ファイルの場所指定が空白のままOnConfiguring→UseSqliteされるので結構ヤバいことになります。変な所にSqliteDB作られるとか、アクセス不可でエラーになるとか……

なので、実際にコード書くときは[Obsolate]付けるとかで対処した方が安全かもしれませんね。
ちなみにMigration実行時はパスとか関係ありません。

ともあれ、これで準備が整いました。

はじめてのMigration

「最初にやること」としては前セクションまでで終わってたりしますが、せっかくなのでMigrationのコマンドもおさらいしておきます。
Migrationクラスを作成するコマンドは

dotnet ef migrations add <Migration名> -s <Migration用プロジェクト名> -p <DLLプロジェクト名>

こんな感じです。今回の構成の場合は

dotnet ef migrations add InitialCreate -s XamaEFCSqlite.MigrationTool -p XamaEFCSqlite.Common

こうですね。
[dotnet ef]コマンドが見つからない的なエラーが出た場合はこの辺とか見てインストールしましょう。
参照設定ちゃんとしてるのに[Could not load assembly 'なんとか'. Ensure it is referenced by the startup project 'かんとか'.]見たいなエラーが出る場合は、一度リビルドすると良いかもしれません。

結果確認!

image.png
出来ました。以降変更がある場合はさっきのコマンドを使ってMigrationクラスを作っていく形になります。

まとめ

いつも困るところをまとめてみました。
一番最初にやる処理ってプロジェクトごとに1回しかやらないので結構忘れがちです。
自分用メモとしてもQiitaは有効だと思うので今後も書いていきます。

//後は文章ちゃんとまとめる能力が欲しい。
//記事作ると大体クソ長になるのやめたい。

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

【Unity】角度を扱うときはfloatじゃなくて専用のAngle構造体を用意すると捗る

角度の扱いづらさ

角度を普通にfloatやdoubleで扱おうとすると、次のような問題が発生して扱いづらいです。

  • 度数法(degree)なのか弧度法(radian)なのかわからない
  • 360°以上になると角度値は違うけど見た目は同じになってしまいややこしくなる
    • 360°と0°では角度値は違えど見た目は変わらない
    • 180°と-180°、45°と405°なども同様。見た目は同じだけど内部値が全く違うので意図しない動きになったりする

これらの問題を専用の変換メソッド等を用意して解決しても良いのですが、グローバルなメソッドにすれば角度以外のfloat値に対して適切ではないメソッドが呼べてしまうし、必要な部分だけにスコープを限定したメソッドとして定義すれば再利用性が失われてしまいます

それに、「角度」のように何らかの意味のある数値として扱う場合は、プリミティブ型をそのまま代用するのではなく、専用のオブジェクトとして定義したほうが、専用の操作をオブジェクト内に隠蔽することができるため、可読性や保守性に富みます。

というわけで、角度を表すAngle構造体を作りました。

角度を表すAngle構造体

作成したAngle構造体の機能を紹介します。
今すぐコードが見たい方はこちら

インスタンスの生成を行うファクトリ

度数法と弧度法を混同させないため、コンストラクタは隠蔽しています。
その代わりに各種ファクトリメソッドを用意しています。

Angle.FromDegreeファクトリメソッド

度数法の値からAngle構造体のインスタンスを取得します。

Angle angle = Angle.FromDegree(60);

周回数を指定することもできます。

Angle angle = Angle.FromDegree(1, 60); //360°+60°

Angle.FromRadianファクトリメソッド

弧度法の値からAngle構造体のインスタンスを取得します。

Angle angle = Angle.FromRadian(UnityEngine.Mathf.PI);

こちらも同様に、周回数を指定することもできます。

Angle angle = Angle.FromRadian(-2, UnityEngine.Mathf.PI); //-4π+π

Angle.Zeroファクトリプロパティ

角度が0のAngle構造体のインスタンスを取得します。

Angle angle = Angle.Zero; //0°

Angle.Roundファクトリプロパティ

角度が360°のAngle構造体のインスタンスを取得します。

Angle angle = Angle.Round; //360°

各種変換を行うメソッド

各種変換メソッドを提供します。
イミュータブルな設計とするため、変換メソッドを実行しても元のインスタンスは変更されず、新しいインスタンスを返すようになっています。

Normalizeメソッド

角度を-180°<θ<=180°の範囲で正規化します。

Normalize.png

例えば、225°の角度は-180°<θ<180°の間には入っていないため、-135°に正規化されます。

Angle angle = Angle.FromDegree(225).Normalize(); //-135°

Normalize1.png

同様に、-450°は-90°に正規化されます。

Angle angle = Angle.FromDegree(-450).Normalize(); //-90°

Normalize2.png

PositiveNormalizeメソッド

角度を0°<=θ<360°の範囲で正規化します。

PositiveNormalize.png

例えば、-135°を0°<=θ<360°の範囲に正規化すると、225°となります。

Angle angle = Angle.FromDegree(-135).PositiveNormalize(); //225°

PositiveNormalize1.png

同様に、-450°は270°に正規化されます。

Angle angle = Angle.FromDegree(-450).PositiveNormalize(); //270°

PositiveNormalize2.png

Reverseメソッド

Reverseメソッドは、次のように見た目上の角度を変更せずに、方向のみを反転させます。

Angle angle = Angle.FromDegree(90).Reverse(); //-270°

Reverse1.png

Angle angle = Angle.FromDegree(-450).Reverse(); //630°

Reverse2.png

SignReverseメソッド

SignReverseメソッドは、角度の符号を単純に反転させます。

Angle angle = Angle.FromDegree(90).SignReverse(); //-90°

SignReverse.png

Absoluteメソッド

Absoluteメソッドは、SignReverseメソッドの正の方向への片道切符バージョンです。

Angle angle = Angle.FromDegree(90).Absolute(); //90°
Angle angle = Angle.FromDegree(-90).Absolute(); //90°

情報の取得を行うプロパティ

Angle構造体から角度情報を取得することができます。

TotalDegreeプロパティ

角度値を度数法で取得します。

float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).TotalDegree; //-450f

TotalRadianプロパティ

角度値を弧度法で取得します。

float rad1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalRadian; //π
float rad2 = Angle.FromDegree(-1, -90).TotalRadian; //-5π/4

NormalizedDegreeプロパティ

Normalizeした角度値を度数法で取得します。

float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).NormalizedDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).NormalizedDegree; //-90f

NormalizedRadianプロパティ

NormalizedDegreeプロパティの弧度法バージョン。

PositiveNormalizedDegreeプロパティ

NormalizedDegreeプロパティのPositiveNormalizeしたバージョン。

PositiveNoramlizedRadianプロパティ

PositiveNormalizedDegreeプロパティの弧度法バージョン。

Lapプロパティ

角度が何周しているかを取得します。

int lap1 = Angle.FromDegree(180).Lap; //0
int lap2 = Angle.FromDegree(360).Lap; //1
int lap3 = Angle.FromDegree(-730).Lap; //-2

IsCircledプロパティ

角度が1周以上回っているかどうかを取得します。

bool circled1 = Angle.FromDegree(180).IsCircled; //false
bool circled2 = Angle.FromDegree(360).IsCircled; //true
bool circled3 = Angle.FromDegree(-730).IsCircled; //true

IsTrueCircleプロパティ

角度が360°の倍数かどうかを取得します。

bool trueCircle1 = Angle.FromDegree(180).IsTrueCircled; //false
bool trueCircle2 = Angle.FromDegree(360).IsTrueCircled; //true
bool trueCircle3 = Angle.FromDegree(-730).IsTrueCircled; //false

IsPositiveプロパティ

正の方向への角度かどうか取得します。

bool circled1 = Angle.FromDegree(180).IsPositive; //true
bool circled2 = Angle.FromDegree(360).IsPositive; //true
bool circled3 = Angle.FromDegree(-730).IsPositive; //false

演算子

各種演算子をオーバーロードしており、プリミティブ型と同じように各種演算をすることができます。

+-演算子

角度を加算/減算します。

var plusAngle = Angle.FromDegree(120) + Angle.Round; //480°
var minusAngle = Angle.FromDegree(45) - Angle.Round; //-315°

*/演算子

角度を実数で乗算/除算します。

var multiAngle = Angle.FromDegree(120) * -3; //-360°
var divideAngle = Angle.FromDegree(120) / 4; //30°

==,!=,<,<=,>,>=演算子

角度の大きさを比較します。

var b1 = Angle.FromDegree(90) == Angle.FromDegree(90); //true 
var b2 = Angle.FromDegree(90) != Angle.FromDegree(450); //true 
var b3 = Angle.FromDegree(90) < Angle.FromDegree(45); //false
var b4 = Angle.FromDegree(90) <= Angle.FromDegree(90); //true 
var b5 = Angle.FromDegree(90) > Angle.FromDegree(45); //true 
var b6 = Angle.FromDegree(90) >= Angle.FromDegree(45); //true 

インターフェイス実装

次の2つのインターフェイスを実装しています。

IEquatable<Angle>インターフェイス

等価性比較のためにIEquatable<Angle>インターフェイスを実装しています。

==演算子があるのにわざわざIEquatable<Angle>インターフェイスを実装するメリットはこの記事が参考になりますが、要約すると次のようになります。

  • IEquatable<Angle>がないとobject版のEquals(object obj)が呼ばれることになる。値型をobjectにキャストするとボックス化が発生するのでオーバーヘッドがかかってしまう
  • 構造体のobject版のEquals(object obj)メソッドの既定の動作は、すべてのフィールドの等価性を比較すること。これが==演算子の比較内容と異なると、==演算子の結果とEqualsメソッドの結果に相違が生じ、混乱を招いてしまう
    • しかも、等価性比較はリフレクションを用いて行われる模様なので、速度が圧倒的に遅い

このような理由から、構造体の場合は基本的にIEquatable<T>インターフェイスを実装したほうが良いようです。

IComparable<Angle>インターフェイス

LINQのOrderByメソッドやMax,Minメソッドを利用できるようにするためにIComparable<Angle>インターフェイスを実装しています。

オーバーライド

object型に定義されている次のメソッドをオーバーライドしています。

ToStringメソッド

周回数と残りの角度を返します。

string str = Angle.FromDegree(2, 45).ToString(); //2x + 45°

ToStringをオーバーライドしていると、VisualStudioのデータヒントにも同様のフォーマットで表示されるので便利です。
image.png

Equals(object o)メソッド

IEquatable<Angle>インターフェイスのEqualsメソッドも実装したのですが、objectクラスのEqualsメソッドもオーバーライドしています。
理由としては、前述の通りEquals(object o)メソッドの既定の動作が全フィールドの等価性比較であること、しかもそれがリフレクションによる比較であるためです。
Angle構造体は内部で持つ角度値を単純に比較するだけで良いので、リフレクションを使う既定の動作をオーバーライドして封印します。

ちなみに、VisualStudioでは==演算子をオーバーロードするとEqualsメソッドとGetHashCodeメソッドもオーバーライドしなさいと警告が出ます。
==演算子をオーバーロードしている→自前で等価性比較処理が書けている→既定のEqualsを使う必要がない→だったらオーバーライドしろ、ということですね。
image.png

GetHashCodeメソッド

GetHashCodeメソッドはDictionaryのキーとして使うときに使われるようです。

A hash code is a numeric value that is used to insert and identify an object in a hash-based collection such as the Dictionary class, the Hashtable class, or a type derived from the DictionaryBase class. The GetHashCode method provides this hash code for algorithms that need quick checks of object equality.

https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0

これをオーバーライドしないとDictionaryのキーとして使ったときに正しく動作しない可能性がある模様。

VisualStudioがオーバーライドをおすすめしてくれたのでオーバーライドします。
しかも有能VisualStudioが中身も自動で実装してくれます。

ぶっちゃけあまり詳しくない。

ソースコード本体

上記機能を備えたAngle構造体のソースコードです。
そのままコピペで使えます。

[追記]
編集リクエスト頂きライセンスを明記しました。
改変等自由ですので是非ご利用ください。

/*
Angle.cs

Copyright (c) 2021 yutorisan

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System;
using UnityEngine;

namespace UnityUtility
{
    /// <summary>
    /// 角度
    /// </summary>
    public readonly struct Angle : IEquatable<Angle>, IComparable<Angle>
    {
        /// <summary>
        /// 正規化していない角度の累積値
        /// </summary>
        private readonly float m_totalDegree;

        /// <summary>
        /// 角度を度数法で指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        private Angle(float angle) => m_totalDegree = ArithmeticCheck(() => angle);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        /// <exception cref="OverflowException"/>
        private Angle(int lap, float angle) => m_totalDegree = ArithmeticCheck(() => checked(360 * lap + angle));

        /// <summary>
        /// 度数法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(float degree) => new Angle(degree);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(int lap, float degree) => new Angle(lap, degree);
        /// <summary>
        /// 弧度法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(float radian) => new Angle(RadToDeg(radian));
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(int lap, float radian) => new Angle(lap, RadToDeg(radian));
        /// <summary>
        /// 角度0°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Zero => new Angle(0);
        /// <summary>
        /// 角度360°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Round => new Angle(360);

        public bool Equals(Angle other) => m_totalDegree == other.m_totalDegree;

        public override int GetHashCode() => -1748791360 + m_totalDegree.GetHashCode();

        public override string ToString() => $"{Lap}x + {m_totalDegree - Lap * 360}°";

        public override bool Equals(object obj)
        {
            if (obj is Angle angle) return Equals(angle);
            else return false;
        }

        public int CompareTo(Angle other) => m_totalDegree.CompareTo(other.m_totalDegree);

        /// <summary>
        /// 正規化された角度(-180° &lt; degree &lt;= 180°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Normalize() => new Angle(NormalizedDegree);

        /// <summary>
        /// 正の値で正規化された角度(0° &lt;= degree &lt; 360°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle PositiveNormalize() => new Angle(PositiveNormalizedDegree);

        /// <summary>
        /// 方向を反転させた角度を取得します。
        /// 例:90°→-270°, -450°→630°
        /// </summary>
        /// <returns></returns>
        public Angle Reverse()
        {
            //ゼロならゼロ
            if (this == Zero) return Zero;
            //真円の場合は真逆にする
            if (IsTrueCircle) return new Angle(-Lap, 0);
            if (IsCircled)
            { //1周以上している
                if (IsPositive)
                { //360~
                    return new Angle(-Lap, NormalizedDegree - 360);
                }
                else
                { //~-360
                    return new Angle(-Lap, NormalizedDegree + 360);
                }
            }
            else
            { //1周していない
                if (IsPositive)
                { //0~360
                    return new Angle(m_totalDegree - 360);
                }
                else
                { //-360~0
                    return new Angle(m_totalDegree + 360);
                }
            }
        }
        /// <summary>
        /// 符号を反転させた角度を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle SignReverse() => new Angle(-m_totalDegree);
        /// <summary>
        /// 角度の絶対値を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Absolute() => IsPositive ? this : SignReverse();

        /// <summary>
        /// 正規化していない角度値を取得します。
        /// </summary>
        public float TotalDegree => m_totalDegree;
        /// <summary>
        /// 正規化していない角度値をラジアンで取得します。
        /// </summary>
        public float TotalRadian => DegToRad(TotalDegree);
        /// <summary>
        /// 正規化された角度値(-180 &lt; angle &lt;= 180)を取得します。
        /// </summary>
        public float NormalizedDegree
        {
            get
            {
                float lapExcludedDegree = m_totalDegree - (Lap * 360);
                if (lapExcludedDegree > 180) return lapExcludedDegree - 360;
                if (lapExcludedDegree <= -180) return lapExcludedDegree + 360;
                return lapExcludedDegree;
            }
        }
        /// <summary>
        /// 正規化された角度値をラジアン(-π &lt; rad &lt; π)で取得します。
        /// </summary>
        public float NormalizedRadian => DegToRad(NormalizedDegree);
        /// <summary>
        /// 正規化された角度値(0 &lt;= angle &lt; 360)を取得します。
        /// </summary>
        public float PositiveNormalizedDegree
        {
            get
            {
                var normalized = NormalizedDegree;
                return normalized >= 0 ? normalized : normalized + 360;
            }
        }

        /// <summary>
        /// 正規化された角度値をラジアン(0 &lt;= rad &lt; 2π)で取得します。
        /// </summary>
        public float PositiveNormalizedRadian => DegToRad(PositiveNormalizedDegree);
        /// <summary>
        /// 角度が何周しているかを取得します。
        /// 例:370°→1周, -1085°→-3周
        /// </summary>
        public int Lap => ((int)m_totalDegree) / 360;
        /// <summary>
        /// 1周以上しているかどうか(360°以上、もしくは-360°以下かどうか)を取得します。
        /// </summary>
        public bool IsCircled => Lap != 0;
        /// <summary>
        /// 360の倍数の角度であるかどうかを取得します。
        /// </summary>
        public bool IsTrueCircle => IsCircled && m_totalDegree % 360 == 0;
        /// <summary>
        /// 正の角度かどうかを取得します。
        /// </summary>
        public bool IsPositive => m_totalDegree >= 0;

        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator +(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree + right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator -(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree - right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator *(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree * right));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator /(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree / right));
        public static bool operator ==(Angle left, Angle right) => left.m_totalDegree == right.m_totalDegree;
        public static bool operator !=(Angle left, Angle right) => left.m_totalDegree != right.m_totalDegree;
        public static bool operator >(Angle left, Angle right) => left.m_totalDegree > right.m_totalDegree;
        public static bool operator <(Angle left, Angle right) => left.m_totalDegree < right.m_totalDegree;
        public static bool operator >=(Angle left, Angle right) => left.m_totalDegree >= right.m_totalDegree;
        public static bool operator <=(Angle left, Angle right) => left.m_totalDegree <= right.m_totalDegree;

        /// <summary>
        /// 演算結果が数値であることを確かめる
        /// </summary>
        /// <param name="func"></param>
        /// <returns></returns>
        private static float ArithmeticCheck(Func<float> func)
        {
            var ans = func();
            if (float.IsInfinity(ans)) throw new NotFiniteNumberException("演算の結果、角度が正の無限大または負の無限大になりました");
            if (float.IsNaN(ans)) throw new NotFiniteNumberException("演算の結果、角度がNaNになりました");
            return ans;
        }
        private static float RadToDeg(float rad) => rad * 180 / Mathf.PI;
        private static float DegToRad(float deg) => deg * (Mathf.PI / 180);
    }
}


単体テスト

Angle構造体はこれから長く使い続けられそうなので、きちんと単体テストを行いました。
次の記事を参考にしながら、xUnitChainning Assertionを使ってテストコードを記述しました。
xUnit.net でユニットテストを始める

以下がテストコードです。
IComparable<Angle>インターフェイスを実装したことにより、OrderByによる並べ替えも成功しています。

例外処理も可能な限りチェックしたかったのですが、Angle構造体は内部に単精度浮動小数点型(float)を使っているので、どうしても最大値・最小値の扱いが難しいです(例えばfloat.MaxValue+1000などとしても、floatの有効数字は7桁なので1000が丸め込まれてしまう)。
そのため明確にNaNもしくはInfinityまたはNegativeInfinityになった場合のみ例外の発生をチェックしました。
実際、3.40282347×10^38°なんて角度を使うことはほぼありえないため目をつむります。floatの仕様に依存すると言えば言い訳になるかも。

using System;
using UnityUtility;
using UnityEngine;
using Xunit;
using System.Linq;
using System.Collections.Generic;

namespace AngleStructUnitTest
{
    public class UnitTest1
    {
        [Fact]
        public void CreateInstance()
        {
            Angle.FromDegree(180).Is(Angle.FromRadian(Mathf.PI));
            Angle.FromRadian(-4 * Mathf.PI).Is(Angle.FromDegree(-720));
            Angle.FromDegree(-1, -180).Is(Angle.FromDegree(-360 + -180));
            Angle.FromRadian(-10, Mathf.PI).Is(Angle.FromDegree(-3600 + 180));
            Angle.Zero.Is(Angle.FromRadian(0));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NaN));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NegativeInfinity));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.PositiveInfinity));
        }

        [Fact]
        public void Normalize()
        {
            Angle.Zero.Normalize().Is(Angle.Zero);
            Angle.FromDegree(180).Normalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).Normalize().Is(Angle.FromDegree(-90));
            Angle.FromDegree(360).Normalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(360 * 4 + 20).Normalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-360 * 80 + 20).Normalize().Is(Angle.FromDegree(20));
        }

        [Fact]
        public void PositiveNormalize()
        {
            Angle.FromDegree(0).PositiveNormalize().Is(Angle.Zero);
            Angle.FromDegree(180).PositiveNormalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(360).PositiveNormalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(380).PositiveNormalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 - 90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 * 5 + 90).PositiveNormalize().Is(Angle.FromDegree(90));
        }

        [Fact]
        public void Reverse()
        {
            Angle.Zero.Reverse().Is(Angle.Zero);
            Angle.FromDegree(45).Reverse().Is(Angle.FromDegree(-315));
            Angle.FromDegree(-90).Reverse().Is(Angle.FromDegree(270));
            Angle.FromDegree(180).Reverse().Is(Angle.FromDegree(-180));
            Angle.FromDegree(360).Reverse().Is(Angle.FromDegree(-360));
            Angle.FromDegree(359).Reverse().Is(Angle.FromDegree(-1));
            Angle.FromDegree(361).Reverse().Is(Angle.FromDegree(-1, -359));
            Angle.FromDegree(-450).Reverse().Is(Angle.FromDegree(360 + 270));
            Angle.FromDegree(2, 90).Reverse().Is(Angle.FromDegree(-2, -270));
        }

        [Fact]
        public void SignReverse()
        {
            Angle.Zero.SignReverse().Is(Angle.Zero);
            Angle.FromDegree(60).SignReverse().Is(Angle.FromDegree(-60));
            Angle.FromDegree(-120).SignReverse().Is(Angle.FromDegree(120));
            Angle.FromDegree(-2, 60).SignReverse().Is(Angle.FromDegree(2, -60));
        }

        [Fact]
        public void Absolute()
        {
            Angle.Zero.Absolute().Is(Angle.Zero);
            Angle.FromDegree(60).Absolute().Is(Angle.FromDegree(60));
            Angle.FromDegree(-120).Absolute().Is(Angle.FromDegree(120));
            Angle.FromDegree(-4, 60).Absolute().Is(Angle.FromDegree(4, -60));
            Angle.FromDegree(4, -60).Absolute().Is(Angle.FromDegree(4, -60));
        }

        [Fact]
        public void StandardMethods()
        {
            Angle.FromDegree(3, 270).ToString().Is("3x + 270°");
            Angle.FromDegree(90).Equals(Angle.FromDegree(1, 90).Normalize()).IsTrue();
            Angle.FromDegree(45).Equals(45).IsFalse();

            object o = Angle.FromDegree(135);
            Angle.FromDegree(135).Equals(o).IsTrue();
            Angle.Round.Equals(null).IsFalse();
        }

        [Fact]
        public void Operator()
        {
            (Angle.FromDegree(45) + Angle.FromDegree(90)).Is(Angle.FromDegree(135));
            (Angle.FromDegree(30) - Angle.FromDegree(90)).Is(Angle.FromDegree(-60));
            (Angle.FromDegree(90) * 4.5f).Is(Angle.FromDegree(90 * 4.5f));
            (Angle.FromDegree(45) * -1).Reverse().Is(Angle.FromDegree(315));
            (Angle.FromDegree(90) * 0).Is(Angle.Zero);
            (Angle.FromDegree(4, 90) / 2).Is(Angle.FromDegree(2, 45));
            (Angle.FromDegree(5, 10).Normalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(-5, 10).PositiveNormalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(90).Reverse() != Angle.FromDegree(-90)).IsTrue();
            (Angle.FromDegree(45) > Angle.FromDegree(90)).IsFalse();
            (Angle.FromDegree(-1, 0) > Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(-1, 0) >= Angle.FromDegree(-360)).IsTrue();
            (Angle.FromDegree(-1, 20) < Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(45)).IsTrue();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(90)).IsTrue();

            Assert.Throws<NotFiniteNumberException>(() => Angle.FromDegree(float.MaxValue) + Angle.FromDegree(float.MaxValue));
            Assert.Throws<NotFiniteNumberException>(() => Angle.Zero - Angle.FromDegree(float.MaxValue) * 2);
            Assert.Throws<NotFiniteNumberException>(() => Angle.Round / 0);

        }

        [Fact]
        private void Getter()
        {
            Angle.FromDegree(2, 90).TotalDegree.Is(810);
            Angle.FromDegree(2, 90).Normalize().TotalRadian.Is(Mathf.PI / 2);
            Angle.Zero.NormalizedDegree.Is(0);
            Angle.FromDegree(2, 90).NormalizedDegree.Is(90);
            Angle.FromDegree(-1, -90).NormalizedRadian.Is(-1 * Mathf.PI / 2);
            Angle.Zero.PositiveNormalizedDegree.Is(0);
            Angle.FromDegree(1, 90).Reverse().PositiveNormalizedDegree.Is(90);
            Angle.FromDegree(-2, 90).PositiveNormalizedRadian.Is(Mathf.PI / 2);
            Angle.FromDegree(3, 90).Lap.Is(3);
            Angle.FromDegree(360).Lap.Is(1);
            Angle.FromDegree(-180).Lap.Is(0);
            Angle.FromDegree(-750).Lap.Is(-2);
            Angle.FromDegree(-360).IsCircled.IsTrue();
            Angle.FromDegree(1, -1).IsCircled.IsFalse();
            Angle.FromDegree(1).IsPositive.IsTrue();
            (Angle.FromDegree(1) * -1).IsPositive.IsFalse();
            Angle.Round.IsTrueCircle.IsTrue();
            Angle.Zero.IsTrueCircle.IsFalse();
            Angle.FromDegree(180).IsTrueCircle.IsFalse();
            Angle.FromDegree(-720).IsTrueCircle.IsTrue();
        }

        [Fact]
        private void Compare()
        {
            var collection = new List<Angle>
            {
                Angle.FromRadian(MathF.PI / 2),
                Angle.FromDegree(45),
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.Round
            };

            collection.OrderBy(a => a).SequenceEqual(new List<Angle>
            {
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.FromDegree(45),
                Angle.FromRadian(MathF.PI / 2),
                Angle.Round
            }).IsTrue();
            collection.Max().Is(Angle.Round);
            collection.Min().Is(Angle.FromDegree(-90));
        }
    }
}

テスト結果

すべて合格。
image.png

最後に

角度の扱いは簡単なようで地味に厄介でした。
その厄介な部分を全部隠蔽してきれいな部分だけを外部に公開したこのAngle構造体、少し使ってみたのですが普通に便利です。(自画自賛)
みなさんも自己責任でよかったらどうぞ。

以下GitHubでも公開しています。
https://github.com/yutorisan/UnityUtility/blob/develop/UnityUnility/Structs/Angle.cs

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

wixsharpでインストーラーを作る

はじめに

Windows向けインストーラーの形式として、MSIというものが存在する。
インストーラー作成ツールとしてメジャーなものには、VSのインストールプロジェクト、あるいはInstallShieldなどがあるが、
このMSIを基盤としている(NSISInno Setupは別の仕組み)。

このMSIを作るための手法として、Wix Toolsetというものが存在する。
XMLで宣言的にコンポーネント等を設定して、MSIを作るというものだ。

しかしこのwix、再利用がしにくい部分がある、記述が冗長になりやすい、補完が難しい等、作りこんでいくと多少苦しくなる時がある。
そのようなwixをC#で記述することによって、IDE(vsやvscode)の補完や、処理の再利用などをしやすくした WixSharp なるものが存在する。

今回は、そのWixSharpをvscodeで記述するための最初のステップや、注意するべき点について記述したいと思う。

なお、WixSharpを使うに当たりMSIとWixの関連領域に踏み込むことは不可避なので、これらも並行して学んでいくと良いと思う。

WixSharpとは

WixSharpとは、C#でMSIパッケージの生成をできるようにしたライブラリで、ほとんどの機能はWixのラッパーとなる。
また、Wixに備わっているDTF(Deployment Tool Foundation)を利用することによって、winformsやWPFでのUI記述や、.NETによるカスタムアクションの定義等を容易に行う事が可能になる。
もともとCS-Scriptという、Roslyn以前にC#をスクリプトで使おうというプロジェクトの一つの活用事例として作られたものであるが、実際はCS-Scriptなしでも単体のライブラリとして存在している。

今回は執筆時点での最新版である1.15.0をベースに書いていく。

前提条件

本記事の中で必要なものは以下の通り

  • Windowsマシン
    • そもそもMSIがWindows専用の機構であるため
  • .NET Framework SDK
    • WixSharpは.NET Framework用のライブラリしか提供してないため
  • Windows 10 SDK
    • インストールコンポーネントの中の"MSI Tools"というものがあるので、それを有効にする
  • Wix v3
    • https://wixtoolset.org/releases/ より、最新安定版をDLする
    • *-binaries.zip*.exeがあるが、片方はバイナリのみ、片方はインストーラーとなり、今回はどちらでも構わない
  • Visual Studio Code + C#拡張
    • エディタとして本記事で使う
  • Visual Studio + .NET開発 サポート
    • 同じくC#拡張のため
    • 最低限Visual Studio Build Toolでも可

使用するWixの準備

WixSharpはその性質上、Wixの実行バイナリ(candle.exe等)がどうしても必要になる。そのため、Wixの実行バイナリがあるフォルダがどこにあるかを知る必要がある。
定義するための手法は以下のようなものがあり、いずれも設定されたディレクトリ直下にcandle.exe等があることが期待される。
上から順に優先される。

  1. MSI生成時にWixSharp.Compiler.WixLocationに設定する
  2. コマンドライン引数で/WIXBIN:をプレフィックスに持つものを探し、その後ろのパスを使用する
    • MSBuildで使う用
  3. 環境変数のWIXSHARP_WIXDIRを取得
    • エラーメッセージ等でも示されているので、環境変数で設定する場合はこの方法が一番いいかも
  4. 環境変数のWixLocationを取得
  5. 環境変数のWIXを取得し、その後ろにbinフォルダを追加する
    • WIXをMSI経由でインストールした場合、環境変数WIXは自動的に設定される
  6. %ProgramFiles%の配下にあるWixインストールディレクトリを検索し、あればそこにbinを追加する
  7. ..\..\Wix_bin\bin..\..\..\..\Wix_bin\bin..\..\..\..\..\Wix_bin\binを検索し、あれば採用する
    • サンプル用なので実際には期待しない方が良い

MSIからWixをインストールした場合、大抵の場合はWIX環境変数の部分で引っかかると思う

MSI生成プロジェクトの準備

新規プロジェクトの生成

さて、Wix実行ファイルの場所特定ができたら、次はMSIを生成するプロジェクトの作成である。
といっても、生成するのは普通のC#のコンソールプロジェクトで、dotnet new console辺りで作ればいいだけである。
ただし、WixSharpのライブラリは.NET Framework製なので、後でTargetFrameworkを.NET Framework系のものに変えておこう(net461とかnet35とか)

nugetパッケージの追加

プロジェクトの生成が完了したら、WixSharpのnugetパッケージを追加することになる。
WixSharpには以下のように複数のnugetパッケージが存在するため、自分のプロジェクトに合わせて参照するものを変えること。

  • WixSharp.bin
    • ライブラリ部分のパッケージ
  • WixSharp
    • WixSharp.bin + MSBuild時に更新が入った場合、自動的にプログラムが実行されるようになる→ビルドするだけでMSIができるようになる
  • WixSharp.Lab
    • WixSharp.binに比べて実験的な機能が含まれている
    • WixSharp.binと共存可
    • 1.15.0時点では画面生成用のクラスが含まれている(WixSharp.Controls)

実装

プロジェクトの準備ができたら、ソースを編集していく。
おおまかには以下のような流れとなる

  1. WixSharp.Projectインスタンスの生成
  2. ProductCode、UpgradeCode、言語設定等の主要属性の設定
  3. 以下を再帰的に行う
    1. WixSharp.Dirの生成
      • インストールルート以外は、全てディレクトリ単体の名前を指定する
    2. 生成したディレクトリ内にWixSharp.Fileオブジェクト等をnewして追加
      • ファイルの他、レジストリやサービス登録等もここで追加する
    3. 内部に更にディレクトリがある場合、WixSharp.Dirを生成して繰り返す
  4. カスタムアクションがあればそれを定義して、Projectインスタンスに追加する

この他にも、MSIのネイティブUIを定義できたり、Windows FormsやWPFを使って画面を作成することもできるが、長くなるのでこの記事では取り上げない。

主に決めるべき主要属性

wixsharpでは、wixに関するプロパティが細かい所まで設定できるが、その中でも最初に考慮した方が良いものを挙げていく。
下で紹介されている他にも、成果物出力先決定などに使われるOutDir等の設定があるが、あまり重要ではないため省略する。
全て見たい場合は、WixSharpのコードを見ること

UpgradeCode

MSIでいう所の UpgradeCodeプロパティを設定する。
よほどのことが無い限り、MSIのUpgradeCodeは変わらないはずなので、とりあえず設定しておく方がいい。

ProductCode

MSIでいう所の ProductCodeプロパティ を設定する。
ProductCodeプロパティはメジャー/マイナーアップグレードの主要な判断材料となるため、よく考えて設定する、しないを決定すること。
また、WixSharpにおいては、各コンポーネント(ファイル等)のGUIDはProductCodeを基点に一意に決定されるため、値の取り扱いについては十分注意すること

GUID

これはWixSharp独自のプロパティで、設定すると後述するProductCodeやUpgradeCodeが手動設定されていない場合の基点となる。
WixSharpがUpgradeCode、ProductCodeがどのように決定するかは以下の図を参照。

UpgradeCode決定手順

ProductCodeの決定の仕方

MajorUpgrade

メジャーアップグレードで、ダウングレードを許すか、どのタイミングで既存プロダクトの削除を行うかを設定する。
メジャーアップグレードをしたいならば必ず設定しておくこと。
それぞれのフィールドの意味は以下で、WixのMajorUpgradeエレメントに対応する

  • bool? AllowDowngrades(default=false)
    • 古いバージョンへのダウングレードを検知したとき、インストールをブロックするかどうか
  • bool? AllowSameVersionUpgrades(default=true)
    • バージョン番号が同じ場合でも、メジャーアップグレードを許可するか
  • bool? Disallow(default=false)
    • メジャーアップグレードを禁止する
  • string DisallowUpgradeErrorMessage
    • メジャーアップグレードを検知したときに、エラー画面とともに表示されるメッセージ
  • bool? IgnoreRemoveFailure(default=false)
    • メジャーアップグレードの時にアンインストールを実行して失敗したとき、アップグレードを続行するかどうか
    • 続行したときに何が起こるかは個別の事情が絡むため網羅的にいいにくいので、実際どうなるかを確かめてから設定すること
  • bool? MigrateFeatures(default=true)
    • Featuresを継承するかどうか
  • string RemoveFeatures
    • 指定したFeatureがインストールされている時、それらを再インストールしない
  • WixSharp.UpgradeSchedule? Schedule(default=afterInstallValidate)
    • メジャーアップグレードの中で、アンインストールをどのタイミングで行うか設定する
    • 大体はデフォルトのままで問題ないが、アップグレードが途中で失敗して中断した場合、元バージョンもろとも消え去ってしまうため、可能性がある場合は注意すること

MajorUpgradeStrategy

メジャーアップグレードをどのバージョンからどのバージョンまでで有効にするか等を決定する。
Wixでいう所の、Upgradeエレメントに対応する

Version

ここでバージョンを設定すると、ProductVersionプロパティが設定され、作成するMSIパッケージのバージョンになる。
ただし、MSIのルールとして、[major].[minor].[build](各構成要素は全て整数)のように形式が決まっているので注意。
詳しくは MSIのドキュメントを参照

MSIにおいてバージョン番号は、メジャー/マイナー/スモールアップデートに関わる所なので気を付けること。

UI

Wixが標準で用意しているUIのどれを使うかを選択する。使えるのは以下で、WixSharp.WUIというenumクラスの値を指定する。
ここで設定すると、UIRefにIDを指定したものが追加される。
それぞれのダイアログの解説については、Wixのものをそのまま使っているだけなので、wixtoolsetのWixUIダイアログの解説を参照。

ここではテンプレートから選ぶような感じになるが、アプリ独自要素(シリアルとか)をインストールUIで設定させたい場合もあるだろう。そのような時はカスタムダイアログを作ることになると思うが、これが書くとまた長いので、公式のサンプルソースへのリンクを紹介するだけに留めたい。サンプルでは、WinFormsやWPFのダイアログをUIシーケンスの中に挟み込んでいる。

Properties

MSIのユーザー定義プロパティを追加する。
AddPropertiesメソッドで追加が可能。
なお、ここで定義できる値は、全てインストール実行前に値が決定されるものとなることに注意。
具体的に言うと、例えばあるプロパティの値に[PROPERTY_A]\ABCのような、他のプロパティに依存した値を付けても、実際に使う時にはプロパティ指定部分が空になったり、意図した値が反映されない場合がある(インストールディレクトリの指定等)。
そのような場合は、後述するカスタムアクションの一種であるWixSharp.SetPropertyActionを使用する必要がある。このアクションを使う時も、アクションの実行順に注意しないと、意図した結果にならない場合があることに注意。

SourceBaseDir

ファイル等のコンポーネントを指定する際、ソースに相対パスを指定した時の基点となるフォルダを指定する。
設定しないときは、カレントディレクトリが使われる。

コンポーネントの追加

プロジェクトの属性が決まったら、各種コンポーネント(ファイル、レジストリ等)を追加していく。
基本的にインストールルートとなるWixSharp.Dirを作成し、そこにどんどんコンポーネントや配下のフォルダを作成していく形になる。
作成の時はLinq to XMLのように、コンストラクタで追加していってもいいし、インスタンスを作成した後追加していってもいい。

ソースでいうと

// `WixSharp.Project`に対する便利拡張メソッドを使用するため
// using WixSharp.CommonTasks;
var project = new WixSharp.Project("ProjectName");
#region "projectオブジェクトへのプロパティ設定等"
#endregion
// コンストラクタ時に追加
var dir = new Dir(new WixSharp.Id("DirID"), "%ProgramFiles%Hoge", 
    new WixSharp.File(new WixSharp.Id("File1"), "path/to/source1")
    );
// 後から追加
// ここで設定するIdはWixSharp上のIDで、MSIのコンポーネントに設定するGUIDのIDは別途設定されることに注意
dir.Add(new WixSharp.File(new WixSharp.Id("File2"), "path/to/source2"));
// プロジェクトインスタンスに追加
project.Add(dir); 

のような感じになる。レジストリを追加するときは、WixSharp.Fileの代わりにWixSharp.RegKeyを使う。
なお、インストールはしないが、カスタムアクションなどで使用するファイルがある場合は、WixSharp.Binaryを使って追加する。これはDirに所属している必要は無い。

Feature

例えばWindows SDKをインストールする時、インストールするコンポーネントを選択する画面が出てくると思うが、
あれを実現しているのがFeatureという機能となる。
特に指定しない限り、WixSharpは全てのコンポーネントを同じFeatureに属するものとして処理するが、
例えばインストール時にコンポーネントを選択させたい場合は、このFeatureを使うことになる。
基本的には、WixSharp.Featureオブジェクトを最初に生成しておき、各WixSharp.File等のコンポーネントを生成する時、Featureフィールドがあると思うので、そこに設定すればいい。
サンプルコードはWixSharpのリポジトリにあるので、そこを参照すること

カスタムアクションの追加

コンポーネントが追加し終わったら、カスタムアクションを追加していく。
カスタムアクションは全てWixSharp.Actionから派生しており、共通属性として以下のものを持つ

  • string Name
    • カスタムアクションの名前
    • 他から参照する時に使われるので、少なくともプロジェクトの中ではユニークであること
  • WixSharp.Conditon Condition
    • カスタムアクションを実行するかしないか
    • 文法は MSIのもの がそのまま使えるが、代表的なものは、WixSharp.Conditionの静的フィールドとして定義されている
  • WixSharp.Execute Execute
  • bool? Impersonate
    • ユーザー権限(UAC無し)で実行するか(=true)、システム権限(UAC有)で実行するか(=false)で、デフォルトはtrue
    • falseで実行したいときは、更にExecuteを"deferred"にする必要がある
  • WixSharp.Return Return
    • カスタムアクションの実行結果をどう処理するか(戻り値が0かどうかで成功/失敗を判断)
    • 通常はcheck(デフォルト)かignore
  • WixSharp.Sequence
    • インストールシーケンスのうち、どのシーケンスに所属するかを指定する
    • 大体InstallExecuteSequence(インストール開始後)か、InstallUISequence(MSIのUI画面)になる
    • 代表的なものはWixSharp.Sequenceに静的フィールドとして存在する
  • WixSharp.StepWixSharp.When
    • カスタムアクションをどの時点(Step)の前後どちらで(When)実行するかを指定する(相対指定)
    • 代表的なものはWixSharp.Stepに静的フィールドが存在するが、自分のカスタムアクションのIDを指定することも可
  • WixSharp.SequenceNumber
    • 相対指定ではなく、絶対的な数値の順番で指定する
    • StepWhen設定とは排他
    • 何を指定すべきかは、MSIの仕組みを知っていないと厳しいため、上級者向け

カスタムアクションの種類は、WixSharpにおいては以下のものが公式として存在する。

  • BinaryFileAction
    • MSIに組み込んだ"Binary"ファイルを取り出して、実行可能ファイルとして実行する
  • ManagedAction
    • DTF(Deployment Tools Foundation)というwix提供の機能を利用して、C#の静的メソッドをカスタムアクションとして実行する
    • 作り方は後述
  • ElevatedManagedAction
    • 上記ManagedActionを管理者権限で実行する
    • 実際は、上記ManagedActionにexecute=deferredimpersonate=noが設定されたものになる
  • InstalledFileAction
    • インストールする(されているファイル)を、IDをキーにして実行する
  • PathFileAction
    • システムに存在するEXEを指定して実行する
  • SetPropertyAction
    • インストーラー実行時にプロパティを設定する
    • インストール中に値が変わるプロパティを基に更に他のプロパティを設定したい場合に使う
    • カスタムアクション実行時点のプロパティ値を使用するため、実行順に注意すること

他にWixとしては、ネイティブDLLの中の関数を実行するということも可能だが、WixSharpではデフォルトでやり方が用意されているわけではない。

ManagedActionの作り方

C#でManagedActionを使う場合、いくつかの手順を踏む必要がある。
まず最初に、Microsoft.Deployment.WindowsInstaller.dllを、カスタムアクションを仕込むプロジェクトへ参照追加する必要がある。
カスタムアクションを入れるプロジェクトは、現在wixsharpを使用しているプロジェクトでもいいし、また別途プロジェクトを用意しても構わない。
これは、Wixをインストールするか、zipアーカイブを展開すると、candle.exe等があるフォルダに同梱されている。
これを直接Reference等で参照する。

次に、実行するメソッドを定義するわけだが、

  • 所属するクラスがpublic
  • 型がpublic static ActionResult [メソッド名](Microsoft.Deployment.WindowsInstaller.Session)であること
  • 属性にMicrosoft.Deployment.WindowsInstaller.CustomActionがあること

を満たせばOK。例として下記。

CustomAction.cs
using Microsoft.Deployment.WindowsInstaller;
public class CustomActions
{
  // WixSharp側で参照するための名前を設定する
  // デフォルトはメソッド名がそのまま使われる
  [CustomAction("Method1")]
  public static ActionResult Method1(Session session)
  {
    // 例外等エラーが起きてcatchしないと、制御が効かずにそのまま終わる可能性があるので注意
    try
    {
      // MSIのログに出力
      session.Log("in customaction");
      // MSIプロパティを設定する
      // Execute=immediateの時に使えるが、他の場合は無視される
      session["A_PROPERTY"] = "ABCDE";
      // MSIプロパティを取得する
      // Execute=immediateの時に使えるが、他の時はnullになる
      string propb = session["B_PROPERTY"];
      // CustomActionDataを取得する
      // Execute=deferredの時に、ここに値が入り、そうでないときはnull
      string caData = session.CustomActionData;

      return ActionResult.Success;
    }
    catch(Exception e)
    {
      session.Log("error: {0}", e);
      return ActionResult.Failure;
    }
  }
}

上記を満たしたメソッドを用意した上で(仮にCustomActions.Method1とする)、ManagedActionを以下のように追加する。

project.AddAction(new WixSharp.ManagedAction("Method1")
{
    // カスタムアクションメソッドが入っているアセンブリを指定する
    // %this%は、このプロジェクトのアセンブリを意味する特別な文字列(デフォルトは%this%)
    ActionAssembly = "%this%",
    Condition = Condition.NOT_Installed,
    Execute = Execute.immediate,
    Impersonate = true,
    // ActionAssemblyに、DTFとシステムアセンブリ以外の依存がある場合、ここにもれなく記述する
    RefAssemblies = new string[],
    Return = Return.check,
    Step = Step.AppSearch,
    // Execute=deferredの時、ここにプロパティ名をカンマ区切りで入れる必要がある
    // こうすると、CustomActionDataに"A_PROPERTY=[A_PROPERTYの値];B_PROPERTY=[B_PROPERTYの値]"のように、値が格納される
    UsesProperties = "A_PROPERTY,B_PROPERTY",
});

注意点として、ManagedActionを使用すると、その時点でMSIのインストールに.NET Framework依存ができてしまうため、ManagedActionが使用しているフレームワークバージョンは確実に満たせるように、開発要件を定めておくこと

MSIの生成

さて、全てのプロジェクト設定が終わったら、最後にWixSharp.Project.BuildMsi(string path = null)を実行する。
出力されるMSIファイルは、指定が無ければカレントディレクトリの"[プロダクト名].msi"という名前で出力される。
また、同時に生成されたwxsファイルが、"wix/[プロジェクト名].g.wxs"として出力される。
こちらは、生成結果の確認等に使うと良いだろう。

また、MSIを作らずにwxsの確認だけ行いたい場合は、WixSharp.Project.BuildWxs(string path = null)を実行する。
こちらはWixのツールを実行しないので、BuildMsiでエラーが出た時にwxsの中身を確認したい場合に使うと良いと思う。

終わりに

ここまでWixSharpの入り口として記事を書いたが、MSIネイティブやWindows Formsを画面に使えたり、あるいはMSIを実行するためのブートストラッパーの作成ができるなど、ここでは紹介していない機能が色々あるので、機会があればまたその辺りの記事を書いてみたい。

参考リンク

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

[初投稿]

自己紹介

現在就活真っ只中の京都大学大学院修士一年(M1)です。
ゲーム業界を目指しています。ポートフォリオの作成が義務感にかられて、なかなか進まずでしたが、友人にこんなゲーム作って欲しいんだけど、と言ってもらって作っている際にとても楽しくて、今はゲーム業界が第一志望です。自身のアウトプットの場としてQiitaにゆるくあげていけたらなと考えています。

よろしくお願いします。

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

C#でAtCoder Beginners Selection(ABC081B - Shift only)

準備

C#でAtCoderデビューのための準備
のあとで AtCoder Beginners Selection をやってみました。

問題分

ABC081B - Shift only
https://atcoder.jp/contests/abs/tasks/abc081_b

提出結果

using System;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        int n = int.Parse(Console.ReadLine());
        string[] value = Console.ReadLine().Split(' ');
        var a = Enumerable.Repeat(0, n).ToArray();
        for (int i = 0; i < n; i++)
        {
            a[i] = int.Parse(value[i]);
        }
        int kaisuu = 0;
        bool owari = false;
        while (owari == false)
        {
            for (int i = 0; i < n; i++)
            {
                int amari = a[i] % 2;
                if (amari == 0)
                {
                    a[i] = a[i] / 2;
                }
                else
                {
                    owari = true;
                    break;
                }
            }
            if (!owari) kaisuu++;
        }
        Console.WriteLine($"{kaisuu}");
    }
}

テスト実行

image.png
image.png
image.png

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

[Unity]AssemblyDefinitionを跨いだ共通クラスの作成

前提
・Assetをインポートしすぎて60個くらいAssemblyDefinitionできちまったぜ!てへっ!
・グローバルなDebugクラスはHighBridRender(ver8.2.0)で定義されててこっちで定義するとビルドエラー!うへっ!
 ⇒やめてほしい
・AssemblyDefinitionに共通クラスを追加するの面倒だな(今後も含めて)
・ECSとか何万個のオブジェクトをゴリゴリ動かすから少しでも負荷になることは避けたい…ンゴ。

やることは大まかに以下の通り。
<手順>
1.クラスライブラリ(DLL)を作成。
 ⇒VisualStudioで適当にプロジェクト作ってビルドでおk
2.クラスライブラリをAssets/Pluginsのどこかにおく。
 ⇒Assets/Plugins/DLLとかネストしてておk
3.どこででも1.で作成したクラスライブラリが使えるようになる。
 ⇒\(^o^)/やた

<メリット>
・AssemblyDefinitionにわざわざ追加しなくていい。
 [Plugins]⇒[各AssemblyDefinition]⇒[Assembly-CSharp]の順で参照されるらしい。詳しくは知らんが動く。
 #最初はクラスライブラリをUnityのクラスライブラリ群に突っ込んだりしていろいろ試してた。
 [Plugins]にAssemblyDefinition作って置けばいいんじゃね?っていってもできんかった。
 ワイももっと楽がしたかった

<デメリット>
・クラスライブラリのビルド忘れとかがある場合がある。
・クラスライブラリから[Assembly-CSharp]とか[各AssemblyDefinition]の参照ができない。
 ⇒1回目はプレイボタンをエラーなく押すと動くけど、2回目以降は参照がおかしいとUnity様に怒られる。
  バグだと思っているが直るの待つのは愚策なので参照できないと割り切って作る。
  クラスライブラリなので処理を独立させましょう。

<例>
DebugLog用のオーバーラップクラスでいい感じに作れるんじゃないかと思われる。

DebugS.cs
// 1.共通クラスライブラリ(DLL)
//  ネットのどこかにあるDebugLogのオーバーラップクラス
using UnityEngine;
public static class DebugS
{
#if !ENABLE_DEBUG_MODE
    private const string CONDITIONAL_TEXT = "UNITY_EDITOR";
#else
    private const string CONDITIONAL_TEXT = "ENABLE_DEBUG_MODE";
#endif
    /* 略 */
    [Conditional(CONDITIONAL_TEXT)]
    public static void Log(object message, LogLevel level)
    {
        if ((int)Ground.In.Value >= (int)level) UnityEngine.Debug.Log(message);
    }
    /* 略 */
}
namespace Ground
{
    public enum LogLevel { None,Low,Mid,High}
    public class In {public static LogLevel Value = LogLevel.None;}
}

使用例(上記DLLを[Assets/Plugins/DLL]とかに配置後)

Hogehoge.cs
// UnityのどこかのDebugLog出力ソースコード
// 参照設定やUsingは不要(でーん!)
protected override void Initialize()
{
    if (!LoadThisData(SettingsSavePath))
    {
        DebugS.LogError($"Fail Load {SettingsSavePath}");
    }
    if (AutoSaveFlag)
    {
        StartAutoSave(DuloGames.UI.UIManager.Instance);
        DebugS.Log("AutoSave", Ground.LogLevel.High);
    }
    else DebugS.Log("Not AutoSave", Ground.LogLevel.High);
}
// ログ出力レベルの変更
void ChangeDebugLoglevel(Ground.LogLevel level)
{
    Ground.In.Value = level;
}

Unity標準のDebugクラスがオープンソースコードだったら喜んでたけど探してもなかったのであきらめた
(オーバーラップしたかった)

これが一番っょぃと思います。

元のDebugぶっ壊せてもユーザ環境壊せんから仕方ないな、これで勘弁してやろう。
俺より強いやつに会いに行く。(いまここ)

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

interfaceについて!!Unity日記#1

始めに!!

・皆さん初めまして!!とある学校で専門生してるamanon(仮名)と申します!
 Unityを勉強してる皆様と情報共有できたらいいな~と思って初めてみました!

・Unity初心者さんや勉強中な人向けに記事を書いていきますのでよろしくお願いします!!

・第一回目はinterfaceです!(1回目にやる内容じゃないですが...(-_-;))
 とても便利なので頑張って行きましょうー!!

interfaceとは?

・クラスを作った人(作成側)とそのクラスを利用する側(利用者)でお約束事が出来る機能です

interfaceの用途

・コードの疎結合化 

・実装の中身は利用者側は知らなくていい!

・メソッドの中身は利用者側が仕様を決めれることが出来ます!

まとめてみましたが全然理解しなくて大丈夫ですよ!!
言葉だけじゃ伝わらない部分もあるのでソース交えながらやっていきましょう!!

使い方

public interface I_Damageble()
{
 //OK!!
  public  void Damage(int Value);

  //エラー
  public  void Damage(int Value)
 {
   helth-=Value;
  }
}

・今回はI_Damagebleというinterface(作成側)を作りDamageという関数を持たせました!
・interfaceはあくまでも機能だけ定義する事が出来て中身は作る事が出来ません!少し矛盾してるというか変ですよね.
・じゃあ関数なのに中身はどうやって実装するんだ?って思いますよね!そうです、中身は利用者側が実装しなければなりません!作っていきます!

public class Damageble:MonoBehaviour,I_Damageble <-ここに利用するinterface名
{
  //HP
  private int helth=0;
  //ダメージ関数
  public void  Damage(int Value)
  {
   helth-=Value;
  }
}

・Damagebleクラス(利用者)にI_Damageble(作成者)を継承させたらI_Damagebleの機能が使える事が出来ます!
・実際に継承させI_DamagebleのDamage関数の中身を実装してみました!

void OnCollisonEnter(Collison other)
{ 
  //ohter(当たった情報)からIDamagebleを取得します!
  //無い場合nullが返ってきます!
  var id=other.gameobject.Getcopoment<IDamageble>();

  //nullの場合に処理を実行するとIDamagebleの情報が無いのでエラー
  // idにデーターが入った場合のみ実行する,nullの場合は処理しない
    if (id!=null)
    {
       id.Damage(10);
    }
}

・あとはこのDamage関数を当たり判定の関数の中で呼んであげればやる事は終わりです!
・FPSでプレイヤーが建物に当たった時にダメージ処理が実行されたらおかしいですよね。なのでnull処理は必ず入れてください。

ここまで

interfaceの使い方を紹介しましたがどこが便利やねん!普通にダメージ関数を作った方が楽やんけ!!
と思った方もいらっしゃるかと思いますがこれから紹介していきます!!

void OnCollisonEnter(Collison other)
{ 
    if(other.gameobject.tag=="Enemy")
    {
      Damage(10);
    }
}

・おなじみの当たり判定処理ですね!!FPSやTPSなどでは問題ないですが宇宙区間で戦うシューティングを思い浮かべてみてください!

・もし岩や流星群、飛行機の破片が当たった時にダメージ処理したい場合それぞれの
 オブジェクトに"Enemy"というタグを設定しますか?しないですよね(;^ω^)だって敵じゃないですので!

・じゃそれぞれにタグを設定しますか?

void OnCollisonEnter(Collison other)
{ 
    if(other.gameobject.tag=="Enemy")
    {
      Damage(10);
    }
    if(other.gameobject.tag=="ryusei")
    {
      Damage(10);
    }
    if(other.gameobject.tag=="hahenn")
    {
      Damage(10);
    }

    if(other.gameobject.tag=="iwa")
    {
      Damage(10);
    }
}

・いい案ではありますがhit時ダメージ処理をしたいオブジェクトが増えるたびにコードも増えてバグの原因になりかねません。
・チーム制作なんかでやっているとヒューマンエラーにもつながります。

解決法

・ここで便利になってくるのがinterfeceです!

//流星クラス
public class ryusei:MonoBehaviour,I_Damageble 
{
   public void Damage(int value)
   {
   }
}

//岩クラス
public class iwa:MonoBehaviour,I_Damageble 
{
   public void Damage(int value)
   {
   }
}

//欠片クラス
public class kakera:MonoBehaviour,I_Damageble 
{
   public void Damage(int value)
   {
   }
}

//プレイヤークラス
public class Player:MonoBehaviour
{
   void OnCollisonEnter(Collison other)
  { 
  //これだけで欠片、岩、流星が当たった時の処理が出来ます
  var id=other.gameobject.Getcopoment<IDamageble>();
    if (id!=null)
    {
       id.Damage(10);
    }
  }
}

・それぞれのクラスにinterfaceを背負わせる事で当たったときそのオブジェクトは
interfaceを背負ってるか背負ってないかでダメージ処理が出来るので
わざわざ文字列で判定をとらなくていいですしコードも少なくて済みます!とても便利ですよね!!!

以上です!!お疲れ様でした!

おわりに

ここまで目を通してくださりありがとうございました!当たり判定をいっぱい取る場合には有効な手法ですので使ってみてください!!

初めての試みで不慣れですがまた定期的に書いていこうと思います!よろしくお願いします

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

SlackでUNCパスのリンクをクリックして直接ファイルが開けるようになるツール「Liwing」

はじめに

SlackでUNCパスのリンクをクリックして直接ファイルが開けるようになるツール「Liwing」の紹介です。
例えば、\\servername\test.xlsx というExcelファイルパスのリンクを Slack に投稿すると、そのリンクをクリックするだけで Excel を起動してそのファイルを開きます。

ツールの使い方

インストール方法

以下の GitHub ページの How to install と書かれれている所から LiwingSetup.msi というインストーラをダウンロードしてインストールしてください。C#で開発したWindows専用のツールです。
LiwingのGitHubページ

Slackにリンクを投稿する方法

例えば、\\servername\test.xlsx のファイルパスのリンクを作成したい場合、そのファイルを選択して、エクスプローラーのコンテストメニューの [送る] - [Liwing] というメニューを実行します。
(Liwingをインストールすることで、[送る]メニューが追加されます)
image.png
上記を実行すると、クリップボードに lw://file/servername/test.xlsx というカスタムURLスキームという技術を用いた形式のURLがコピーされます。

そのURLをSlackに投稿すると、下図のようにリンク形式で表示します。
image.png

対象のファイルを開く方法

上記の「lw:」から始まるリンクをクリックすると、下図のダイアログが表示されます。
ここで[リンクを開く]を選択すれば、対象のファイルを開きます。ファイルがxlsxの拡張子であれば、Excelを起動します(拡張子ごとの既定のアプリケーションで起動します)。
image.png

便利な使い方

チーム全員でLiwingをインストール

「lw:」から始まるカスタムURLスキームのリンクを開くためには、Liwingのインストールが必要です。
従って、ネットワーク上のUNCパスにファイルを格納しているチームの場合、チーム全員がLiwingをインストールしておくと便利です。

複数ファイルのURLをコピー

エクスプローラーで複数ファイルを選択して [送る] - [Liwing] メニューを実行すれば、複数ファイルのURLをクリップボードにコピーします。

htmlファイルに利用

Slackに貼り付けるだけでなく、htmlファイルのリンクにも利用できます。
通常はブラウザからUNCパスのファイルを直接開くことはできませんが、以下のようにLiwingでコピーしたURLをリンク先パスに書くことで、ブラウザからUNCパスのファイルを開くリンクを作成できます。
(例) <a href="lw://file/servername/test.xlsx">test.xlsx</a>

ローカルのファイルを開くリンクに利用

UNCパスだけでなく、ローカルのパス(C:ドライブなど)のリンクにも利用できます。
従って、GitHubのリポジトリをクローンするローカルのパスをチーム全員で統一すれば、Slackから対象ドキュメントを直接開くリンクが作成できます。
(例) チーム全員がLドライブに対象リポジトリをクローンしていた場合、以下のリンクで対象ファイルを開く
lw://file/L:/docs/DesignDocuments/FeatureDesign.md

ツールの実現に用いた技術

Windowsには、URI スキームという概念があり、特定のアプリはブラウザからでもリンクをクリックすることで起動できます。
さらに、上記をカスタマイズするために、カスタム URL スキーム という概念があり、それを用いることで任意のデスクトップアプリを起動できるようになります。
技術の詳細は以下の記事に記載しています。
ブラウザ上のリンクから任意のデスクトップアプリのファイルを開けると超便利

まとめ

SlackでUNCパスのリンクをクリックして直接ファイルが開けるようになるツールを紹介しました。

ちなみに私は、普段はエンジニアリングマネージャーとして、チームの皆で楽しく開発する施策を色々実施しています。詳しくは以下を参照ください。
1年以上かけて生産性倍増+成長し続けるチームになった施策を全部公開

Twitterでも開発に役立つ情報を発信しています → @kojimadev

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

C#でAtCoder Beginners Selection(ABC081A - Placing Marbles)

準備

C#でAtCoderデビューのための準備
のあとで AtCoder Beginners Selection をやってみました。

問題文

ABC081A - Placing Marbles
https://atcoder.jp/contests/abs/tasks/abc081_a

提出結果

using System;

class Program
{
    static void Main(string[] args)
    {
        string input = Console.ReadLine();
        int okCount = 0;
        for(int i = 0; i < 3; i++)
        {
            char c = input[i];
            if (c == '1') okCount++;
        }
        Console.WriteLine(okCount);
    }
}

テスト実行

image.png
image.png

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

C#でAtCoder Beginners Selection(ABC086A - Product)

準備

C#でAtCoderデビューのための準備
のあとで AtCoder Beginners Selection をやってみました。

問題文

ABC086A - Product
https://atcoder.jp/contests/abs/tasks/abc086_a

提出結果

using System;
class Program
{
    static void Main(string[] args)
    {
        string[] input = Console.ReadLine().Split(' ');
        int a = int.Parse(input[0]);
        int b = int.Parse(input[1]);
        int amari = (a * b) % 2;
        if (amari == 0) Console.WriteLine("Even");
        if (amari == 1) Console.WriteLine("Odd");
    }
}

テスト実行

image.png
image.png

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