- 投稿日:2020-09-18T22:07:56+09:00
VSを使わずC#をコンパイルする方法
検証環境
・Windows10 64bit 1909 build 18363.1082
・DotnetFramework x64 v4.0.30319参考(というかほとんどパクリ…)
https://qiita.com/toshirot/items/dcf7809007730d835cfc
手順
①下記のサンプルcsファイルを作成
test.csusing 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
- 投稿日:2020-09-18T20:18:46+09:00
ハッシュ値で画像の類似度を判定する
概要
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がどっちなのかは分かりません…。
画像のハッシュ値を求める
コード
// 画像の読み込み 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 = 10402329587663758416pHashがPerceptual Hashでのハッシュ値です。
CoenM.ImageSharp.ImageHashではulongですが、アルゴリズム本来の意味では64bitのハッシュが求まります。画像間の類似度を求める
この2つの画像のハッシュ値それぞれを求め、類似度を求めます。
コード
// 画像の読み込み 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.625pHash1が左の画像のハッシュ値、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 = 5958916077812472516pHash、aHash、dHashともに使い方は同じです。
ハッシュアルゴリズムをインスタンス化する際に使いたいアルゴリズムを選んでください。アルゴリズムごとの類似度サンプル(以下コード省略)
いろんなケースでアルゴリズムごとに類似度を求めてみます。
類似度は100で完全一致です。
一致と判断するのはおおむね90台後半にしたほうがいいです。一部だけ異なる画像の比較
実行結果
pHashSimilarity = 90.625 aHashSimilarity = 67.1875 dHashSimilarity = 92.1875異なる部分が小さければある程度類似画像とみなしてくれます。
が、しきい値を下げすぎることはお勧めしません。
この例で一致と判断したいのであれば、異なっていない箇所だけ切り出して比較したほうがいいです。
(異なっている箇所が分かる場合だけですが)異なる画像の比較
実行結果
pHashSimilarity = 50 aHashSimilarity = 59.375 dHashSimilarity = 54.6875全然違う画像と比較した場合の類似度は50前後です。
類似度0は全く逆の画像(aHashならネガポジ反転とか)なので、ある意味元の画像と近いです。リサイズされた画像の比較
実行結果
pHashSimilarity = 100 aHashSimilarity = 100 dHashSimilarity = 100これらのアルゴリズムは、同じサイズに縮小してからハッシュ値を求めるというものなので、リサイズされた画像やアスペクト比が異なる画像との比較もできます。
ウォーターマークありなしの比較
実行結果
pHashSimilarity = 96.875 aHashSimilarity = 62.5 dHashSimilarity = 90.625ウォーターマークありなしもある程度類似画像とみなしてくれます。
aHashは類似度が低いですね。圧縮ノイズで劣化した画像との比較
実行結果
pHashSimilarity = 96.875 aHashSimilarity = 100 dHashSimilarity = 100圧縮ノイズがあっても類似画像とみなしてくれます。
一部切り出した画像との比較
実行結果
pHashSimilarity = 53.125 aHashSimilarity = 70.3125 dHashSimilarity = 59.375一部切り出した画像は基本的に異なる画像とみなされます。
アルゴリズムによって多少異なりますが、ハッシュ値は縦横8x8といった、ごく画像にリサイズしてから計算します。
一部切り出されていると縮小した際に全く異なった結果になるため、類似度が低くなります。
余白が広くなっていたり狭くなっていたりしても同様です。色味が補正された画像の比較
実行結果
pHashSimilarity = 96.875 aHashSimilarity = 82.8125 dHashSimilarity = 93.75程度によりますが、色味が補正されていても類似画像とみなしてくれます。
これもaHashは類似度が低いですね。人間の目にはよく似ているように見える画像の比較
実行結果
pHashSimilarity = 78.125 aHashSimilarity = 87.5 dHashSimilarity = 75aHashは比較的高いように見えますが、類似度90未満は信用しないほうがいいです。
こういう画像の比較は機械学習等の手段を検討すべきです。一見同じに見えるが、背景の透過率が異なっている画像の比較
実行結果
pHashSimilarity = 43.75 aHashSimilarity = 51.5625 dHashSimilarity = 46.875左の画像はPNGで右の画像はJPEGです。
フォーマットの違いは重要ではありません。
左の画像は背景が透明で、右の画像は白色になっています。
CoenM.ImageHash.HashAlgorithmsでは透過度も含めてハッシュ値を求めているため、異なる画像とみなされます。
透過度を無視したければ、ハッシュ値を求める前に透過情報をなくしておく必要があります。雑感
- サンプルのように、用途によって最適なアルゴリズムは異なります。正確性を重視したい場合はpHashがよさそうです。
- 今回は余白の多いイラストで試しましたが、写真などの余白がない(情報量が多い)画像であればもっと顕著な結果が出ます。
- どの類似度以上なら一致と判断するかどうかは実際に比較してチューニングが必要です。情報量が多く、ノイズが少ない画像であれば類似度98あたりをしきい値にするといいと思います。
- 投稿日:2020-09-18T19:26:00+09:00
WPFで使いまわしができる「ListBox.SelectedItemsを取得するメソッド」
背景
以前 こちら でListBox.SelectedItemsの中身を取り出す方法を書きました。しかし ListBox が複数になるとコード量が多く邪魔だったので、中身を取得する専用のメソッドくんを考えました。
準備
WPFでリストを複数(今回は二つ)作る。
c#でリスト項目をセット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 を押すと
選択中の項目が取り出せました。ボタン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; } }
- 投稿日:2020-09-18T18:47:37+09:00
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)を使用しています。
- 必要なら、
PlayerSettings
でScripting Runtime Version
を4.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
- デモ用シーンです。
最新プラグインへの更新
- 公式サイトのダウンロードから最新版を取ってきて
Assets/Plugins
へ導入してください。- Androidについては、こちらの記事(Qiita)を参考にしてください。
基本的な使い方
- データベース
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の外側で行われる文字列ベースのバインドです。
その他
- ご指摘やご提案、あるいはご質問などを歓迎します。
- 常識的なことも理解していないので、何か間違えているような場合はご助言いただけると助かります。
- より詳しい使い方については、使用例を記事にしたいと考えています。
- 投稿日:2020-09-18T17:34:29+09:00
WPF や UWP のコントロールは見た目で選ぶのではなく機能で選ぼう
昔にもブログで書いたような気がするけど、最近また思ったので Qiita のほうにも書いておきます!
WPF/UWP のコントロールの特徴
見た目は完全に置き換え可能なように作られているということです。なので、見た目じゃなくて純粋に機能で選んだほうがいいという特徴があります。
例えば
先日、ReactiveProperty のサンプルで TodoMVC のような Todo リストアプリを作ってみたのですが、画面左上に一括チェックや一括チェック解除を行うチェックボックスがありました。
これは見た目はチェックボックスなのですが、どちらかというと押すと一括チェックや一括チェック解除の機能を実行して、見た目がたまたまチェックボックスみたいなボタンに近いかなと思いました。
なので、この部分は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>
Button
のTemplate
を差し替えてCheckBox
の見た目にしています。あとCheckBox
自体は動いてほしくないのでIsHitTestVisible
にFalse
を設定して押しても何もおきないようにしました。
これは、コントロールが見えてるだけの状態にすることが出来て稀に使う便利な機能なので存在を覚えておくといいと思います。このボタンを押すと
CompleteAllCommand
に紐づいてる処理が動いて Todo のステータスが変わります。ステータスが変わった結果IsCompletedAllItems
の値が変わってCheckBox
のIsChecked
が更新されて見た目上はチェックがついたり外れたりします。まとめ
ということで WPF/UWP は、コントロールの見た目を完全に置き換えることが出来るので、見た目は飾りと思って機能でコントロールを選択するようにしましょう。
ではでは。
- 投稿日:2020-09-18T13:20:53+09:00
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 も整理したので手動作業が結構減ったのでリリースやドキュメントの更新などが楽になりました。
- 投稿日:2020-09-18T11:05:23+09:00
複数 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.csusing 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 は回収されてそうです。
次に新しい UI スレッドで表示させるほうで同じ手順でやってみます。
増えてる…。という感じになります。こちらの記事にも同じようなことが書いてあります。
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(); }無事消えてくれました。
辛い点
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(); }気合入れて Windows API 叩けば WindowStyle を None に指定するよりも本物の動きに似せることはできます。例えば以下のようにすると子 Window が閉じるまでは移動も最大化も最小化も閉じることもできない感じになります。
MainWindow.xaml.csusing 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; } } }Windows のメッセージとか調べたりしないといけないけど、マルチスレッド プログラミングの苦しみに比べたら幸福度が高いです。
重たい処理
他に UI が固まる可能性のある処理ですが UI スレッドで重たい処理を行うケースがあります。
それについては可能な限り async/await を使って非同期でやるなり、どうしても重たい処理だけ別スレッドでやるという風にして UI スレッドを止めることが無いようにしてください。こうすることで、多分かなりの部分で単一の UI スレッドで出来るようになると思います。
まとめ
WPF で複数の UI スレッドは本当に最後の手段にとっておいて、できる限りやらないですむ方法を探る方がトータルで幸せになると個人的には感じています。
特に普通のマルチスレッド プログラミングの難しさに加えて
- Dispatcher をシャットダウンしないとメモリリークする
- 別の UI スレッドでやったイベントが伝搬してきたら死ぬ
などのハマりどころがあるので、泣きたくなります。なんかメモリリークしてるから Window 開いたりスレッド作ってそうなところを全部チェックしてくれ。すべてのルートで Dispatcher がシャットダウンされてるか確認してくださいと言われたら個人的には泣きたくなります。
あと、ReactiveProperty は UI スレッドが単一であるという前提のもので作ってるので ReactiveProperty を使う場合は UI スレッドを複数作らないでください。多分つらくなります。
自動で UI スレッドにイベントをディスパッチしてるのですが、このディスパッチ先はアプリが最初からもってる UI スレッドになるので、他の UI スレッドのことなんか知らないってなります。
- 投稿日:2020-09-18T10:51:40+09:00
フライトシミュレータのSDKとか必要そうなサイト
目次
Microsoft Flight Simulator (MSFS2020)
Prepar3D (P3D)Microsoft Flight Simulator (MSFS2020)
オフィシャルサイト
https://www.xbox.com/ja-JP/games/microsoft-flight-simulator
SDK概要
https://docs.microsoft.com/en-us/previous-versions/microsoft-esp/cc526948(v=msdn.10)
Event IDs
https://docs.microsoft.com/en-us/previous-versions/microsoft-esp/cc526980(v=msdn.10)
Simulation Variables
https://docs.microsoft.com/en-us/previous-versions/microsoft-esp/cc526981(v=msdn.10)
physin
https://pypi.org/project/SimConnect/
physin git
https://github.com/hankhank10/MSFS2020-cockpit-companion
Prepar3D (P3D)
オフィシャルサイト
SDK概要
http://www.prepar3d.com/SDKv4/LearningCenter.php
Event IDs
http://www.prepar3d.com/SDKv4/LearningCenter.php
Simulation Variables