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

VSを使わずC#をコンパイルする方法

検証環境

・Windows10 64bit 1909 build 18363.1082
・DotnetFramework x64 v4.0.30319

参考(というかほとんどパクリ…)

https://qiita.com/toshirot/items/dcf7809007730d835cfc

手順

①下記のサンプルcsファイルを作成

test.cs
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Windows.Forms;
using System.Drawing;

class Neko : Form {

  [STAThread]
  public static void Main() {
    GeneratedCodeAttribute generatedCodeAttribute = new GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator","10.0.0.0);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run( new Neko());
  }

  Neko(){
    Text = "Button Click Sample";
    ClientSize = new Size(200, 200);
    Button btn1 = new Button();
    TextBox txb1 = new TextBox();
    Label lb1 = new Label();
    btn1.Location = new Point(50, 50);
    txb1.Location = new Point(100,100);
    lb1.Location = new Point(150,150);

    btn1.Text = "Click!";
    btn1.Click += btn1_Click;

    lb1.Text = "test";

    Controls.AddRange(new Control[] { btn1 });
    Controls.AddRange(new Control[] { txb1 });
    Controls.AddRange(new Control[] { lb1 });
  }

  void btn1_Click(object sender, System.EventArgs e) {
    MessageBox.Show("こんにちはにゃー", "挨拶");
  }
}

②コマンドプロントを起動し下記コードを実行

実行
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe D:\test\test.cs

③cmdのカレントディレクトリに"test.exe"が吐き出されるので実行する。
image.png

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

ハッシュ値で画像の類似度を判定する

概要

2つの画像のハッシュ値をそれぞれ求め、ハッシュ値間の類似度を求めます。
完全に一致しているかどうか、ではなく類似度が分かるので、しきい値を適切に設定するとあいまいな画像一致判定ができます。
本稿はC#を前提として記述していますが、使用しているアルゴリズムは他の言語でも実装があるようなので、一定の参考になると思います。

前提

前述のとおり本稿はC#を前提として記述しています。
このアルゴリズムを使うには、CoenM.ImageSharp.ImageHashをNuGetでインストールし、以下のnamespaceを追加してください。

using CoenM.ImageHash;
using CoenM.ImageHash.HashAlgorithms;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

ライブラリ

ハッシュ値を求めるアルゴリズム

CoenM.ImageSharp.ImageHashでは、pHash(Perceptual Hash)、aHash(Average Hash)、dHash(Difference Hash)の3つのアルゴリズムを使用できます。

pHashとaHashは以下の記事に詳しいです。
同一画像を判定するためのハッシュ化アルゴリズム

以下は、pHashとaHashの開発者であるDr.Neal Krawetz氏のブログのエントリから各アルゴリズムの評価を意訳したものです。

  • pHashは遅いが、正確性では最高のパフォーマンスを発揮する。
  • aHashは高速だが、正確性に欠ける。一致すべき画像の取りこぼしは少ないが、一致すべきでない画像と一致するケースが多い。
  • dHashはaHashと同等の速度で、正確性もかなり高い。

※ブログのコメントに「pHash別に遅くないじゃん。二次元のDCTをO(n^4)でやってるから遅い。O(n^3)でやればpHashも全然速いじゃん。」みたいな指摘がありますが、CoenM.ImageSharp.ImageHashがどっちなのかは分かりません…。

画像のハッシュ値を求める

この画像のハッシュ値を求めます。(©いらすとや)
neko_kaburu_man.png

コード

    // 画像の読み込み
    Image<Rgba32> image = Image.Load<Rgba32>(@"neko_kaburu_man.png");
    // ハッシュアルゴリズムのインスタンス化(この例ではpHash)
    IImageHash pHashArgorithm = new PerceptualHash();
    // ハッシュ値を求める
    ulong pHash = pHashArgorithm.Hash(image);
    // 出力
    Console.WriteLine($"pHash = {pHash}");

実行結果

pHash = 10402329587663758416

pHashがPerceptual Hashでのハッシュ値です。
CoenM.ImageSharp.ImageHashではulongですが、アルゴリズム本来の意味では64bitのハッシュが求まります。

画像間の類似度を求める

この2つの画像のハッシュ値それぞれを求め、類似度を求めます。
neko_kaburu_man.pngneko_kaburu_woman.png

コード

    // 画像の読み込み
    Image<Rgba32> image1 = Image.Load<Rgba32>(@"neko_kaburu_man.png");
    Image<Rgba32> image2 = Image.Load<Rgba32>(@"neko_kaburu_woman.png");

    // ハッシュアルゴリズムのインスタンス化(この例ではpHash)
    IImageHash pHashArgorithm = new PerceptualHash();

    // ハッシュを求める
    ulong pHash1 = pHashArgorithm.Hash(image1);
    ulong pHash2 = pHashArgorithm.Hash(image2);

    // ハッシュ値間の類似度を求める
    double pHashSimilarity = CompareHash.Similarity(pHash1 , pHash2);

    // 出力
    Console.WriteLine($"pHash1 = {pHash1}");
    Console.WriteLine($"pHash2 = {pHash2}");
    Console.WriteLine($"pHashSimilarity = {pHashSimilarity}");

実行結果

pHash1 = 10402329587663758416
pHash2 = 10401203732853990480
pHashSimilarity = 90.625

pHash1が左の画像のハッシュ値、pHash2が右の画像のハッシュ値です。
pHashSimilarityが類似度です。
類似度について詳しくは後述します。

いろんなアルゴリズムでハッシュ値を求める

コード

    // 画像の読み込み
    Image<Rgba32> image = Image.Load<Rgba32>(@"neko_kaburu_man.png");
    // ハッシュアルゴリズムのインスタンス化
    IImageHash pHashArgorithm = new PerceptualHash();
    IImageHash aHashArgorithm = new AverageHash();
    IImageHash dHashArgorithm = new DifferenceHash();
    // ハッシュ値を求める
    ulong pHash = pHashArgorithm.Hash(image);
    ulong aHash = aHashArgorithm.Hash(image);
    ulong dHash = dHashArgorithm.Hash(image);
    // 出力
    Console.WriteLine($"pHash = {pHash}");
    Console.WriteLine($"aHash = {aHash}");
    Console.WriteLine($"dHash = {dHash}");

実行結果

pHash = 10402329587663758416
aHash = 11590717229577021695
dHash = 5958916077812472516

pHash、aHash、dHashともに使い方は同じです。
ハッシュアルゴリズムをインスタンス化する際に使いたいアルゴリズムを選んでください。

アルゴリズムごとの類似度サンプル(以下コード省略)

いろんなケースでアルゴリズムごとに類似度を求めてみます。
類似度は100で完全一致です。
一致と判断するのはおおむね90台後半にしたほうがいいです。

一部だけ異なる画像の比較

neko_kaburu_man.pngneko_kaburu_woman.png

実行結果

pHashSimilarity = 90.625
aHashSimilarity = 67.1875
dHashSimilarity = 92.1875

異なる部分が小さければある程度類似画像とみなしてくれます。
が、しきい値を下げすぎることはお勧めしません。
この例で一致と判断したいのであれば、異なっていない箇所だけ切り出して比較したほうがいいです。
(異なっている箇所が分かる場合だけですが)

異なる画像の比較

neko_kaburu_man.pngpose_dance_ukareru_woman.png

実行結果

pHashSimilarity = 50
aHashSimilarity = 59.375
dHashSimilarity = 54.6875

全然違う画像と比較した場合の類似度は50前後です。
類似度0は全く逆の画像(aHashならネガポジ反転とか)なので、ある意味元の画像と近いです。

リサイズされた画像の比較

neko_kaburu_man.pngneko_kaburu_man_resized.png

実行結果

pHashSimilarity = 100
aHashSimilarity = 100
dHashSimilarity = 100

これらのアルゴリズムは、同じサイズに縮小してからハッシュ値を求めるというものなので、リサイズされた画像やアスペクト比が異なる画像との比較もできます。

ウォーターマークありなしの比較

neko_kaburu_man.pngneko_kaburu_man_watermarked.png

実行結果

pHashSimilarity = 96.875
aHashSimilarity = 62.5
dHashSimilarity = 90.625

ウォーターマークありなしもある程度類似画像とみなしてくれます。
aHashは類似度が低いですね。

圧縮ノイズで劣化した画像との比較

neko_kaburu_man_good.jpgneko_kaburu_man_bad.jpg

実行結果

pHashSimilarity = 96.875
aHashSimilarity = 100
dHashSimilarity = 100

圧縮ノイズがあっても類似画像とみなしてくれます。

一部切り出した画像との比較

neko_kaburu_man.pngneko_kaburu_man_cropped.png

実行結果

pHashSimilarity = 53.125
aHashSimilarity = 70.3125
dHashSimilarity = 59.375

一部切り出した画像は基本的に異なる画像とみなされます。
アルゴリズムによって多少異なりますが、ハッシュ値は縦横8x8といった、ごく画像にリサイズしてから計算します。
一部切り出されていると縮小した際に全く異なった結果になるため、類似度が低くなります。
余白が広くなっていたり狭くなっていたりしても同様です。

色味が補正された画像の比較

neko_kaburu_man.pngneko_kaburu_man_retoned.png

実行結果

pHashSimilarity = 96.875
aHashSimilarity = 82.8125
dHashSimilarity = 93.75

程度によりますが、色味が補正されていても類似画像とみなしてくれます。
これもaHashは類似度が低いですね。

人間の目にはよく似ているように見える画像の比較

pose_dance_ukareru_man.pngpose_dance_ukareru_woman.png

実行結果

pHashSimilarity = 78.125
aHashSimilarity = 87.5
dHashSimilarity = 75

aHashは比較的高いように見えますが、類似度90未満は信用しないほうがいいです。
こういう画像の比較は機械学習等の手段を検討すべきです。

一見同じに見えるが、背景の透過率が異なっている画像の比較

neko_kaburu_man.pngneko_kaburu_man_good.jpg

実行結果

pHashSimilarity = 43.75
aHashSimilarity = 51.5625
dHashSimilarity = 46.875

左の画像はPNGで右の画像はJPEGです。
フォーマットの違いは重要ではありません。
左の画像は背景が透明で、右の画像は白色になっています。
CoenM.ImageHash.HashAlgorithmsでは透過度も含めてハッシュ値を求めているため、異なる画像とみなされます。
透過度を無視したければ、ハッシュ値を求める前に透過情報をなくしておく必要があります。

雑感

  • サンプルのように、用途によって最適なアルゴリズムは異なります。正確性を重視したい場合はpHashがよさそうです。
  • 今回は余白の多いイラストで試しましたが、写真などの余白がない(情報量が多い)画像であればもっと顕著な結果が出ます。
  • どの類似度以上なら一致と判断するかどうかは実際に比較してチューニングが必要です。情報量が多く、ノイズが少ない画像であれば類似度98あたりをしきい値にするといいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPFで使いまわしができる「ListBox.SelectedItemsを取得するメソッド」

背景

以前 こちら でListBox.SelectedItemsの中身を取り出す方法を書きました。しかし ListBox が複数になるとコード量が多く邪魔だったので、中身を取得する専用のメソッドくんを考えました。

準備

WPFでリストを複数(今回は二つ)作る。
c#でリスト項目をセット

image.png

MainWindow.xaml
<Window>
    <Grid>
        <!-- 上下に2分割 -->
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 上半分 -->
        <Grid Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="25"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="25"/>
            </Grid.RowDefinitions>

            <Label Grid.Row="0" Content="リストボックス1"/>
            <ListBox Grid.Row="1" Name="ExampleList1" Margin="10" SelectionMode="Extended" ScrollViewer.VerticalScrollBarVisibility="Auto">
                <!-- SelectionMode="Extended" :ctrl,shift+クリックで複数選択可能 -->
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Id, StringFormat=IDは{0} :}"/>
                            <TextBlock Text="{Binding Name, StringFormat= Nameは{0} :}"/>
                            <TextBlock Text="{Binding Age, StringFormat= Ageは{0}}"/>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
            <Button Grid.Row="2" Name="Btn1" Content="ボタン1" Margin="50,0,50,0" Click="Btn1_Click"/>
        </Grid>

        <!-- 下半分 -->
        <Grid Grid.Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="25"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="25"/>
            </Grid.RowDefinitions>

            <Label Grid.Row="0" Content="リストボックス2" Grid.ColumnSpan="2"/>
            <ListBox Grid.Row="1" Name="ExampleList2" Margin="10" SelectionMode="Extended" ScrollViewer.VerticalScrollBarVisibility="Auto" Grid.ColumnSpan="2">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Id, StringFormat=IDは{0} :}"/>
                            <TextBlock Text="{Binding Name, StringFormat= Nameは{0} :}"/>
                            <TextBlock Text="{Binding Age, StringFormat= Ageは{0}}"/>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
            <Button Grid.Row="2" Name="Btn2" Content="ボタン2" Margin="50,0,50,0" Click="Btn2_Click" Grid.ColumnSpan="2"/>
        </Grid>
    </Grid>
</Window>
MainWindow.xaml.cs
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    List<ExampleClass> list = new List<ExampleClass>();
    public MainWindow()
    {
        InitializeComponent();

        list.Add(new ExampleClass() { Id = 0, Name = "aaa", Age = 10 });
        list.Add(new ExampleClass() { Id = 1, Name = "bbb", Age = 20 });
        list.Add(new ExampleClass() { Id = 2, Name = "ccc", Age = 30 });
        list.Add(new ExampleClass() { Id = 3, Name = "ddd", Age = 40 });
        list.Add(new ExampleClass() { Id = 4, Name = "eee", Age = 50 });
        ExampleList1.ItemsSource = list;
        ExampleList2.ItemsSource = list;
    }

    private void Btn1_Click(object sender, RoutedEventArgs e)
    {
        //リストボックス1の選択アイテムを取り出す
    }

    private void Btn2_Click(object sender, RoutedEventArgs e)
    {
        //リストボックス2の選択アイテムを取り出す
    }
}
class ExampleClass
{
    public int Id { get; set; }
    public string Name { get; set; }
    public byte Age { get; set; }
}

リストボックスの中身を取得するメソッドくん

今回の本題です。このメソッドを、リスト選択アイテムを知りたい場所で呼び出します。

MainWindow.xaml.cs
    //! SelectedItemsの中身を取り出すメソッド
    private List<ExampleClass> GetSelectedItems(ListBox listBox)
    {
        List<ExampleClass> selItems = new List<ExampleClass>();
        foreach (var oneItemLine in listBox.SelectedItems)
        {
            ExampleClass item = oneItemLine as ExampleClass;
            selItems.Add(item);
        }
        return selItems;
    }



各ボタンから呼び出します。
対象のコントロールを探す方法は こちら を参考にしました。

MainWindow.xaml.cs
    //! リストボックス1の選択中アイテムを表示する
    private void Btn1_Click(object sender, RoutedEventArgs e)
    {
        // 操作するコントロール
        string controlName = "ExampleList1";

        // リストボックスを探す
        object controlObj = FindName(controlName);
        ListBox listBox = (ListBox)controlObj;

        // 選択項目が0 => メソッドを出る
        if (listBox.SelectedItems.Count == 0)
            return;

        // 選択中のアイテムを取得する
        List<ExampleClass> selItems = GetSelectedItems(listBox);

        // メッセージボックスに表示する内容
        string message = "";
        foreach (var line in selItems)
        {
            message = message + string.Format("\r\nId:「{0}」 Nmae:「{1}」 Age:「{2}」", line.Id, line.Name, line.Age);
        }
        message = string.Format($"{controlName} で選択中の項目は\r\n{message}\r\n\r\nです");

        MessageBox.Show(message);
    }

    //! リストボックス2の選択中アイテムを表示する
    private void Btn2_Click(object sender, RoutedEventArgs e)
    {
        // リスト1と大体同じ。ContorolNameを変えるのみ。
    }

ボタン1 を押すと
image.png
選択中の項目が取り出せました。ボタン2でも同じです。
でもボタン1と2のコードもだいたい同じだからまとめたいですね。まとめた最終的なコードを最後に載せておきます。

最終的なコード全体

ExampleClassは変わっていないので省略します。

MainWindow.xaml.cs
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
    List<ExampleClass> list = new List<ExampleClass>();
    public MainWindow()
    {
        InitializeComponent();

        list.Add(new ExampleClass() { Id = 0, Name = "aaa", Age = 10 });
        list.Add(new ExampleClass() { Id = 1, Name = "bbb", Age = 20 });
        list.Add(new ExampleClass() { Id = 2, Name = "ccc", Age = 30 });
        list.Add(new ExampleClass() { Id = 3, Name = "ddd", Age = 40 });
        list.Add(new ExampleClass() { Id = 4, Name = "eee", Age = 50 });
        ExampleList1.ItemsSource = list;
        ExampleList2.ItemsSource = list;
    }

    //! リストボックス1の選択中アイテムを表示する
    private void Btn1_Click(object sender, RoutedEventArgs e)
    {
        // 操作するコントロール
        string controlName = "ExampleList1";

        ItemShow(controlName);
    }

    //! リストボックス2の選択中アイテムを表示する
    private void Btn2_Click(object sender, RoutedEventArgs e)
    {
        // 操作するコントロール
        string controlName = "ExampleList2";

        ItemShow(controlName);
    }

    // SelectedItemsの中身を取り出す~メッセージ表示メソッド
    private void ItemShow(string controlName)
    {
        // リストボックスを探す
        object controlObj = FindName(controlName);
        ListBox listBox = (ListBox)controlObj;

        // 選択項目が0 => メソッドを出る
        if (listBox.SelectedItems.Count == 0)
            return;

        // 選択中のアイテムを取得する
        List<ExampleClass> selItems = GetSelectedItems(listBox);

        // メッセージボックスに表示する内容
        string message = "";
        foreach (var line in selItems)
        {
            message = message + string.Format("\r\nId:「{0}」 Nmae:「{1}」 Age:「{2}」", line.Id, line.Name, line.Age);
        }
        message = string.Format($"{controlName} で選択中の項目は\r\n{message}\r\n\r\nです");

        MessageBox.Show(message);
    }

    //! SelectedItemsの中身を取り出すメソッド
    private List<ExampleClass> GetSelectedItems(ListBox listBox)
    {
        List<ExampleClass> selItems = new List<ExampleClass>();
        foreach (var oneItemLine in listBox.SelectedItems)
        {
            ExampleClass item = oneItemLine as ExampleClass;
            selItems.Add(item);
        }
        return selItems;
    }
}

image.png
image.png
image.png
アイテムはクリックした順で取得します。

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

SQLiteUnityKit拡張ライブラリ

SQLiteUnityKit を下敷きにしたライブラリ

  • 数多あるSQLiteUnityKitの改修のひとつです。
  • リポジトリ (GitHub)

特徴

  • 日本語が使えるようにしました。
  • トランザクション処理に対応しました。
  • バインド変数に対応しました。
  • 既存DBを上書きしないようにしました。(かといって、マージもしません。)
  • オープンやクローズ、リソースの開放などといった低レベル処理は隠蔽して、抽象化レベルの高い処理だけを表に出すようにしました。
  • できるだけ例外は内部で捉えて、リソースの未解放を避けて動き続けるように務めました。

前提

環境

  • Unity 2019.4.10f1 (LTS)
  • Unity 2018.4.26f1 (LTS)
  • Unity 2017.4 (LTS)
  • SQlite 3.33.0
  • C# 6
    • 文字列補完(string interpolation)を使用しています。
    • 必要なら、PlayerSettingsScripting Runtime Version4.xに設定してください。

SQLite

  • SQLiteは、SQLのサブセットが使えるスタンドアローンなデータベース管理システムです。
    • Windows、MacOS、Android、iOSなどに対応しています。
  • 公式サイト

SQLiteUnityKit

  • SQLiteUnityKitは、UnityからSQLiteを使用するためのフレームワークです。
  • リポジトリ (GitHub)

導入と概要

  • リポジトリからAssetsをプロジェクトへ導入してください。

概要

  • Assets/Plugins/sqlite3/
    • 各プラットフォーム向けのSQLiteプラグインです。(iOSはOS側でサポートがあります。)
  • Assets/Scripts/
    • SQLiteUnity.cs
      • 必須部分です。
    • "SQLiteUnityUtility.cs"
      • 拡張ユーティリティクラスです。お好みでどうぞ。
      • トランザクションでも擬似的なバインドが使えるようになっています。
    • "Test.cs"
      • デモ用スクリプトです。
  • Assets/Prefabs/Console.prefab
    • デモ用プレハブです。
  • Assets/Scenes/SQLite_Test.unity
    • デモ用シーンです。

最新プラグインへの更新

基本的な使い方

  • データベース
    • public class SQLite : IDisposable
    • 新規生成 (初期化クエリ) (既にあれば単に使う、元があればコピーして使う)
      • public SQLite (string dbName, string query = null)
    • 単文を実行
      • public void ExecuteNonQuery (string query, SQLiteRow param = null)
    • 単文を実行して結果を返す
      • public SQLiteTable ExecuteQuery (string query, SQLiteRow param = null)
    • 単文の変数を差し替えながら順に実行
      • public void ExecuteNonQuery (string query, SQLiteTable param)
      • 同じSQL文を、パラメータを変えながら繰り返し実行します。
    • 複文を一括実行し、誤りがあれば巻き戻す
      • public bool TransactionQueries<T> (T query) where T : IEnumerable<string>
      • public bool TransactionQueries (string query)
      • 複数行を配列やリストで渡すか、単一文字列として渡すか、という違いです。
      • 冒頭と末尾にBEGIN,COMMITが勝手に付きます。
  • 行列データ
    • public class SQLiteTable
    • クエリで返されるデータで、列の定義と行データの集合です。
  • 行データ / バインドパラメータ
    • public class SQLiteRow : Dictionary<string, object>
    • 1行分のデータで、列データの集合です。バインドパラメータを渡すときにも使います。
  • 拡張バインド (トランザクション用)
    • public static string SQLiteBind (this string query, SQLiteRow param)
    • sqliteの外側で行われる文字列ベースのバインドです。

その他

  • ご指摘やご提案、あるいはご質問などを歓迎します。
    • 常識的なことも理解していないので、何か間違えているような場合はご助言いただけると助かります。
  • より詳しい使い方については、使用例を記事にしたいと考えています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPF や UWP のコントロールは見た目で選ぶのではなく機能で選ぼう

昔にもブログで書いたような気がするけど、最近また思ったので Qiita のほうにも書いておきます!

WPF/UWP のコントロールの特徴

見た目は完全に置き換え可能なように作られているということです。なので、見た目じゃなくて純粋に機能で選んだほうがいいという特徴があります。

例えば

先日、ReactiveProperty のサンプルで TodoMVC のような Todo リストアプリを作ってみたのですが、画面左上に一括チェックや一括チェック解除を行うチェックボックスがありました。

image.png

これは見た目はチェックボックスなのですが、どちらかというと押すと一括チェックや一括チェック解除の機能を実行して、見た目がたまたまチェックボックスみたいなボタンに近いかなと思いました。
なので、この部分は Button コントロールになっています。

該当部分の XAML は、こんな感じになっています。

<Button Command="{Binding CompleteAllCommand}"
        Focusable="False"
        Margin="5">
    <Button.Template>
        <ControlTemplate TargetType="{x:Type Button}">
            <Grid Background="White">
                <Border Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}">
                    <CheckBox IsHitTestVisible="False" 
                          IsChecked="{Binding IsCompletedAllItems.Value, Mode=OneWay}" 
                          Focusable="False"
                          HorizontalAlignment="Center"
                          VerticalAlignment="Center"/>
                </Border>
            </Grid>
        </ControlTemplate>
    </Button.Template>
</Button>

ButtonTemplate を差し替えて CheckBox の見た目にしています。あと CheckBox 自体は動いてほしくないので IsHitTestVisibleFalse を設定して押しても何もおきないようにしました。
これは、コントロールが見えてるだけの状態にすることが出来て稀に使う便利な機能なので存在を覚えておくといいと思います。

このボタンを押すと CompleteAllCommand に紐づいてる処理が動いて Todo のステータスが変わります。ステータスが変わった結果 IsCompletedAllItems の値が変わって CheckBoxIsChecked が更新されて見た目上はチェックがついたり外れたりします。

まとめ

ということで WPF/UWP は、コントロールの見た目を完全に置き換えることが出来るので、見た目は飾りと思って機能でコントロールを選択するようにしましょう。
ではでは。

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

ReactiveProperty v7.4.0 出てます

7.3 は特に記事書いてないので、ここでは 7.3 と 7.4 で追加されたものを書いていこうと思います。

ToReactivePropertySlimAsSynchronized 拡張メソッド (7.3 で追加)

INotifyPropertyChanged インターフェースを実装したクラスのプロパティと同期する ReactivePropertySlim<T> を生成する ToReactivePropertySlimAsSynchronized 拡張メソッドを追加しました。

以下のような感じです。scheduler の設定と ignoreValidationErrorValue が無い以外は ToReactivePropertyAsSynchronized と同じです。

var p = new Person { Name = "hoge" };
var rp = p.ToReactivePropertySlimAsSynchronized(x => x.Name);

ReactivePropertyScheduler.SetDefaultSchedulerFactory メソッド (7.4 で追加)

今までは ReactiveProperty や ReactiveCollection がイベントをディスパッチするために使う IScheduler のインスタンスの指定方法は 2 種類しかありませんでした。

  • ReactivePropertyScheduler.SetDefault(...) でグローバルに設定
  • コンストラクタかファクトリーメソッドの scheduler 引数で明示的に指定

今回追加した ReactivePropertyScheduler.SetDefaultSchedulerFactory は、ReactiveProperty や ReactiveCollection が生成されるタイミングで IScheduler を生成する処理を指定できます。なので WPF だと App クラスの Startup イベントで以下のような設定をしておくと、インスタンス生成時の Dispatcher を使ってイベントをディスパッチするようにできます。

private void Application_Startup(object sender, StartupEventArgs e)
{
    ReactivePropertyScheduler.SetDefaultSchedulerFactory(() =>
        new DispatcherScheduler(Dispatcher.CurrentDispatcher));
}

ViewModel などが、必ず自分が紐づく UI スレッド上で作られるという前提があるなら、この方法で複数 UI スレッドがあっても動くようにはなると思います。

個人的には複数の UI スレッドを WPF で作るのは、やらないですむならやらないことをお勧めしますが、今までは ReactiveProperty を使ってると割と詰んでいたけど、一応使えるようにしたという感じです。

まとめ

GitHub Actions も整理したので手動作業が結構減ったのでリリースやドキュメントの更新などが楽になりました。

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

複数 UI スレッドを WPF でやる前にやってほしいこと

最初に結論

WPF で複数 UI スレッドやろうと思った人はやらないでください…。
何か複数 UI スレッドで回避しようとしている問題に、他の回避方法があるならそっちを検討してください。
「おっ?UI スレッドもう 1 つ作ったら解決じゃない?」くらいの軽い気持ちでやると、注意深くプログラムを組まないと長時間稼働してたら死んだり、何かの拍子に死ぬような厄介な問題が起きることが多い印象です。

本文

WPF というか、ほとんどの UI を持ってるプラットフォームは単一の UI スレッドがあって、そこでイベント(メッセージ)を処理していくループがあると思います。

私が知ってる中では UWP が Window ごとに独立した UI スレッドを持っています。

複数の UI スレッドがあると何がうれしいの?

重たい処理やモーダルなダイアログを出しても別の UI スレッドで動いてる人には影響を与えません。
おそらく UWP は Window ごとに UI スレッドがあるほうが UI のフリーズが起こりにくいのでこのようにしてるのかなぁと思います。

WPF だとどうなる?

単一の UI スレッドなので UI スレッドがブロックされるとアプリの全 Window が固まります。
試してみましょう。WPF アプリのプロジェクトを作って MainWindow に以下のようにボタンを置いてみます。

MainWindow.xaml
<Window
    x:Class="WpfApp5.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <StackPanel>
        <Button Click="ModalButton_Click" Content="Modal" />
        <Button Click="NonModalButton_Click" Content="NonModal" />
        <Button Click="FileDialogButton_Click" Content="FileDialog" />
    </StackPanel>
</Window>

そして、コードビハインドに以下のようにクリックイベントを実装していきます。

MainWindow.xaml.cs
using Microsoft.Win32;
using System.Windows;

namespace WpfApp5
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ModalButton_Click(object sender, RoutedEventArgs e)
        {
            new MainWindow().ShowDialog();
        }

        private void NonModalButton_Click(object sender, RoutedEventArgs e)
        {
            new MainWindow().Show();
        }

        private void FileDialogButton_Click(object sender, RoutedEventArgs e)
        {
            new OpenFileDialog().ShowDialog();
        }
    }
}

ボタンを押すたびに Window が増えたりファイルを開くダイアログが出るのですが ModalButton_Click の処理が走ると、新しく表示された Window 以外操作できなくなります。これは UI スレッドがブロックされたというよりは ShowDialog で表示された Window 以外を触れなくするという動きですね。

沢山 Window を出しているようなアプリで、何処かで Window を ShowDialog しただけで他の Window が触れなくなると困るケースはあるかなと思います。

回避方法 (個人的にはお勧めしない)

WPF の Window は Single Thread Apartment だと別スレッドでも使えるので、こうすると ShowDialog しても大丈夫です。

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var t = new Thread(_ => new MainWindow().ShowDialog());
    t.SetApartmentState(ApartmentState.STA); // 必須
    t.Start();
}

これをすると、新しい Window は別スレッドで動いてくれるので、そこから先でどんな重い処理をしたりスレッドをブロックしても元の Window の動きが阻害されることはありません。

ただ、この方法だと Dispatcher を終了させないとメモリリークしてしまうという問題が起きたりします。
軽く見てみましょう。VS 2019 でデバッグ実行してメモリのスナップショットをとります。その後、普通に Window を 3 つ表示させて全部閉じた後 GC を走らせてメモリのスナップショットをとります。

MainWindow クラスのインスタンス数の Diff をとってみると 0 ですね。ちゃんと閉じた Window は回収されてそうです。

image.png

次に新しい UI スレッドで表示させるほうで同じ手順でやってみます。
増えてる…。

image.png

という感じになります。こちらの記事にも同じようなことが書いてあります。

http://grabacr.net/archives/1851

では、対処法にならって対処してみましょう。

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var t = new Thread(_ =>
    {
        var w = new MainWindow();
        w.Closed += (_, __) =>
        {
            // Window が閉じたら Dispatcher を終了
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
        };
        w.ShowDialog();
    });
    t.SetApartmentState(ApartmentState.STA); // 必須
    t.Start();
}

無事消えてくれました。

image.png

辛い点

UI スレッド以外からコレクションの変更イベントが飛んでくると WPF の DataGrid や ListBox なんかは簡単に死にます。なので、別スレッドに所属する Window が同一のコレクションを参照して画面に出してると厄介です。

そもそも、標準提供されている ObservableCollection<T> 自体がスレッドセーフじゃないので、何も考えずに複数スレッドから操作してると、たまにうまく動かないとかがあったりして死にます。

なのでコレクションを複製して、それぞれが良い感じに同期をとるとかそういうことをやってコレクションの変更イベントを自分が表示されてる Window の UI スレッドで発行するような仕組みを作らないと辛そうです。

自分は可能であれば複数スレッドが協調作業しないといけない状態を作りたくないので嫌です。

別の対処法

デフォルトの ShowDialog で思った以上に Window にロックがかかるのが嫌な場合は自分で動かさないようにする Window を制御してしまうとかっていう手がありますね。例えばこうすると子 Window が閉じるまで親 Window の UI は触れない。

private void ModalButton_Click(object sender, RoutedEventArgs e)
{
    var originalWindowStyle = WindowStyle;
    WindowStyle = WindowStyle.None;
    IsEnabled = false;
    var w = new MainWindow { Owner = this };
    w.Closed += (_, __) =>
    {
        WindowStyle = originalWindowStyle;
        IsEnabled = true;
    };
    w.Show();
}

image.png

気合入れて Windows API 叩けば WindowStyle を None に指定するよりも本物の動きに似せることはできます。例えば以下のようにすると子 Window が閉じるまでは移動も最大化も最小化も閉じることもできない感じになります。

MainWindow.xaml.cs
using System;
using System.Windows;
using System.Windows.Interop;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        private bool _isLock;
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ShowButton_Click(object sender, RoutedEventArgs e)
        {
            _isLock = true;
            IsEnabled = false;
            var w = new MainWindow { Owner = this };
            w.Closed += (_, __) =>
            {
                _isLock = false;
                IsEnabled = true;
            };
            w.Show();
        }

        const int WM_SYSCOMMAND = 0x0112;
        const int SC_MOVE = 0xF010;
        const int SC_MASK = 0xFFF0;
        const int SC_MAXIMIZE = 0xF030;
        const int SC_MINIMIZE = 0xF020;
        const int WM_CLOSE = 0x0010;

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            var source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
            source.AddHook(WndProc);
        }

        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            // _isLock が true の間は閉じたり最大化したり最小化したり移動させない
            if (msg == WM_CLOSE)
            {
                handled = _isLock;
            }

            if (msg == WM_SYSCOMMAND)
            {
                var sc = wParam.ToInt32() & SC_MASK;
                if (sc is SC_MOVE or SC_MAXIMIZE or SC_MINIMIZE) // C# 9.0 の書き方なので、それより前の場合は == と || 使って書いて
                {
                    handled = _isLock;
                }
            }
            return IntPtr.Zero;
        }
    }
}

image.png

Windows のメッセージとか調べたりしないといけないけど、マルチスレッド プログラミングの苦しみに比べたら幸福度が高いです。

重たい処理

他に UI が固まる可能性のある処理ですが UI スレッドで重たい処理を行うケースがあります。
それについては可能な限り async/await を使って非同期でやるなり、どうしても重たい処理だけ別スレッドでやるという風にして UI スレッドを止めることが無いようにしてください。

こうすることで、多分かなりの部分で単一の UI スレッドで出来るようになると思います。

まとめ

WPF で複数の UI スレッドは本当に最後の手段にとっておいて、できる限りやらないですむ方法を探る方がトータルで幸せになると個人的には感じています。
特に普通のマルチスレッド プログラミングの難しさに加えて

  • Dispatcher をシャットダウンしないとメモリリークする
  • 別の UI スレッドでやったイベントが伝搬してきたら死ぬ

などのハマりどころがあるので、泣きたくなります。なんかメモリリークしてるから Window 開いたりスレッド作ってそうなところを全部チェックしてくれ。すべてのルートで Dispatcher がシャットダウンされてるか確認してくださいと言われたら個人的には泣きたくなります。

あと、ReactiveProperty は UI スレッドが単一であるという前提のもので作ってるので ReactiveProperty を使う場合は UI スレッドを複数作らないでください。多分つらくなります。

自動で UI スレッドにイベントをディスパッチしてるのですが、このディスパッチ先はアプリが最初からもってる UI スレッドになるので、他の UI スレッドのことなんか知らないってなります。

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

フライトシミュレータのSDKとか必要そうなサイト

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