- 投稿日:2020-02-20T22:47:06+09:00
[xaml/C#]ユーザーコントロールのプロパティの値の妥当性検証と値の矯正のやり方(と、躓いた箇所とその回避策)
もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4fやりたいこと
まずは、自前のユーザーコントロールを作成して、それがもつプロパティに値をバインドして、ユーザーコントロールを使う側(画面側)から操作する、また、ユーザーコントロール側からなにか値を受け取ったりする、ということをしたい。
で、やりたいことは、
使う側からプロパティを介して値をセットするときに、もしユーザーコントロール側が持っている「正しい値の範囲」からセットされた値が逸脱していた場合に、ユーザーコントロール側でそれを正しい値の範囲に丸めたい。かつ、その丸めた値を使う側のプロパティの値にも反映させたい。イメージとしては、このような感じ。①から④のようなことをしたい。
実験用画面のイメージ
下記のような、画面(MainWindow)に、簡単なユーザーコントロール(SimpleUserControl)を含むものを作って試す。
実験のイメージとしては、
- 画面左のButtonを押すと、画面が持っているstringのプロパティの文字列のケツに"A"が加えられる。(上の図の①)
- それがユーザーコントロール側のプロパティにも反映され、ユーザーコントロール上のテキストBoxに表示される。(上の図の②)
- セットされた文字列が不正な値(5文字以上)になると、ユーザーコントロール側で丸める処理を行う。(上の図の③)
- 丸めた値が画面側のプロパティにも反映される。(上の図の④)
としようと考えたが、試してみた結果、上の④が、思ったようにいかなかった。
実験コード
画面側コード
MainWindow.xaml<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp1" mc:Ignorable="d" Title="MainWindow" Height="250" Width="400" x:Name="root"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Button Content="Button" Click="Button_Click"/> <local:SimpleUserControl Grid.Column="1" MyTextProp="{Binding DispText, ElementName=root, Mode=TwoWay}" /> </Grid> </Window>MainWindow.xaml.csusing System; using System.ComponentModel; using System.Windows; namespace WpfApp1 { public partial class MainWindow : Window, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string propertyName) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // userControlにバインドして文字列を渡すためのプロパティ public string DispText { get { return _dispText; } set { Console.WriteLine("DispText = {0}", value); _dispText = value; OnPropertyChanged(nameof(DispText)); } } private string _dispText = string.Empty; public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { try { // "A"を付け足していく DispText += "A"; } catch (Exception ex) { MessageBox.Show(ex.Message); } } } }ユーザーコントロール側コード
SimpleUserControl.xaml<UserControl x:Class="WpfApp1.SimpleUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Name="root"> <Grid Width="150" Height="50"> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"/> <ColumnDefinition Width="50"/> </Grid.ColumnDefinitions> <TextBox x:Name="MyTxt" Text="{Binding MyTextProp, ElementName=root, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="MyTxt_TextChanged" /> <Button Grid.Column="1" Content="ボタン" /> </Grid> </UserControl>SimpleUserControl.xaml.csusing System; using System.Reflection; using System.Windows; using System.Windows.Controls; namespace WpfApp1 { public partial class SimpleUserControl : UserControl { public string MyTextProp { get { return (string)GetValue(MyTextProperty); } set { SetValue(MyTextProperty, value); } } public static readonly DependencyProperty MyTextProperty = DependencyProperty.Register( nameof(MyTextProp), // プロパティ名 typeof(string), // プロパティの型 typeof(SimpleUserControl), // プロパティを所有する型=このクラスの名前 new PropertyMetadata("", // 初期値 new PropertyChangedCallback(StringChanged), // プロパティが変わった時のハンドラ new CoerceValueCallback(CoerceStringValue)), // 値の矯正のためのハンドラ new ValidateValueCallback(ValidateStringValue)); // 値の妥当性確認のためのハンドラ // 値の変化 private static void StringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { Console.WriteLine(MethodBase.GetCurrentMethod().Name + " old : " + e.OldValue + " new : " + e.NewValue); // 値がかわらないとここは通らない。(プロパティに値を入れても、同じ値だとここは通らない) } // 値の矯正 private static object CoerceStringValue(DependencyObject d, object baseValue) { Console.WriteLine(MethodBase.GetCurrentMethod().Name + " value : " + (string)baseValue); var txt = (string)baseValue; return (txt.Length <= 5) ? txt : string.Empty; // 5文字以上なら空文字に矯正する(※が、画面側には伝わらない!) } // 値の妥当性確認 private static bool ValidateStringValue(object value) { var txt = (string)value; Console.WriteLine(MethodBase.GetCurrentMethod().Name + " value : " + txt); if (txt == null) return false; // nullのときは異常(falseをreturnすると、ArgumentExceptionを返してくれる) if (txt.Length >= 5) throw new InvalidCastException(); // 5文字以上なら自分の好きな例外をスローしてやる return true; // それ以外はOKとする(setされた値になる) } // コンストラクタ public SimpleUserControl() { InitializeComponent(); } // テキストが変化したときのイベント private void MyTxt_TextChanged(object sender, TextChangedEventArgs e) { Console.WriteLine(MethodBase.GetCurrentMethod().Name); // (仮にここでMyTextPropを書き換えたとしても画面側(DispText)には伝わらない!!!!) } } }やり方
ユーザーコントロール側では、画面側に向けたインターフェースとして
DependencyProperty
を使う。(これはもうそういうものだとして覚える)その
DependencyProperty
には、セットされた値に対して
- 検証(値が正しい値なのかどうか)
- 矯正(値を正しい値に書き換える(強制??))
- 変化検出(値が変わったことを検出する)
ということができるような仕組みが用意されている。それぞれ、
- ValidateValueCallback
- CoerceValueCallback
- PropertyChangedCallback
というもの。上のサンプルでは、
SimpleUserControl.xaml.cs
の中で定義している依存関係プロパティMyTextProperty
を作るときに使われている。(使い方は実験コードのコメントの通り)うまくいかなかった点
上のほうの図の中の、
- ①値をセットする
- ②セットされた値がユーザーコントロールのプロパティに伝わる
- ③セットされた値が不正だった場合に値を丸める(範囲内の値に直す)
- ④丸められた値が画面側のプロパティにも反映される
のうち、①②③まではうまくいった。(③は、途中まで?うまくいった)が、④がうまくいかなかった。
上で挙げた仕組みのどこかで値を丸める(=サンプル中の
MyTextProp
を丸める)と、MyTextProp
の値は意図通り丸めることができるのだが、それが画面側のプロパティ(=サンプル中のDispText
)に反映されてくれない。通常は、上のほうの図のようにバインドしたときは、ユーザーコントロール側で
MyTextProp
の値に何か入れてやると、画面側のDispText
も一緒に変化してくれる。が、今回の場合はそうはならなかった。色々試したところ、
- ユーザーコントロールの依存関係プロパティにバインドした画面側のプロパティは、
そのプロパティの値を操作したときの
「検証/矯正/変化検出」の仕組み(メソッド)の中では書き換えることができないっぽい。(変化検出というのは、PropertyChangedCallbackで設定したメソッドもだが、試したところ、TextBoxの
TextChanged
イベントハンドラでも同様だった。)さらに試しに、画面とユーザーコントロールに、
DispText
とMyTextProp
とは関係ないプロパティと依存関係プロパティをもうひとつずつ作ってバインドし、上のようなMyTextProp
の変化の流れの中で値を書き換えてやると、うまく画面側のプロパティにも書き換えた値が反映された。まとめ
うまくまとめられないが、
- ユーザーコントロールの依存関係プロパティの「検証/矯正/変化検出」の仕組みでは、それにバインドしたユーザーコントロールを使う側のプロパティまでは
矯正(強制?)
を行うことはできない。(っぽい)その後
結果、上記のように丸められなかったので、実験コードの
ValidateStringValue()
メソッドに書いた処理のように、
異常な値の場合は例外を吐くようにして、画面側で「異常な値」をsetしたところにお知らせする ようにした。
(これが正しいやり方なのかどうかはわからないが...)参考
妥当性検証など
https://blog.okazuki.jp/entry/2014/08/17/220810[C#][WPF]DependencyObjectって その2
http://blogs.wankuma.com/kazuki/archive/2008/01/29/119892.aspx追記(ValidateValueCallbackの中でfalseを返した時の動きについて)
実際に動かして試したところ、
ValidateValueCallback
の中でreturn falseすると、
ArgumentException例外を投げてくれるわけではなくプロパティの値が初期値に戻る(PropertyMetadataの第一引数)ような動きをしている。
その際、デバッグ出力の欄にはSystem.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='AAAAAAA' BindingExpression:Path=DispText; DataItem='MainWindow' (Name='root'); target element is 'SimpleUserControl' (Name='root'); target property is 'MyTextProp' (type 'String')
というMsgが出ている。
例外は投げないが、異常値なので初期値に戻すということ?詳細は調べきれてない。追記(ごりごりの逃げの方法)
どうしても親画面側のプロパティの値を一緒に丸めてしまいたいときは、下記のようにして無理やりできるのはできた。
(親のプロパティ名を指定してしまっているので、依存性が出来てしまってUserControlの意味なしになる)逃げ.csprivate static void StringChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { BindingExpression beb = BindingOperations.GetBindingExpression(d, MyTextProperty); if (beb != null) { if (((string)e.NewValue).Length >= 5) { (beb.DataItem as MainWindow).DispText = (string)""; } } }もう他でそのUserControlを絶対使わなくて、どうしても今すぐそういうことがしたいときはこれで逃げれるかも。
- 投稿日:2020-02-20T22:43:26+09:00
Azureみたいなウィザードっぽい入力フォームを作りたい Phase2 確認画面実装編(knockout.js使いこなせない)
前の記事の続き。
フォームに入力した内容を、最後に確認画面で出すための実装に挑戦した。
↓こんな感じで入力すると
こうなる。名前はいま食べてるお菓子からそのまま流用した。
入力内容の連携にはknockout.jsを使った。data-bindってやつ。
knockout.jsはダウンロードして、フォルダ構成が以下になるように配置。
- index.html(本記事のソースをコピペしただけのHTMLファイル)
- AdminLTE-3.0.2(フォルダ)
- css
- img
- js
- plugins
- knockout-3.5.1(フォルダ)
- knockout-3.5.1.js
ソースは以下。(クリックするとソースが表示されます)
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <title>テストページ</title> <!-- Font Awesome Icons --> <link rel="stylesheet" href="AdminLTE-3.0.2/plugins/fontawesome-free/css/all.min.css"> <!-- Theme style --> <link rel="stylesheet" href="AdminLTE-3.0.2/css/adminlte.min.css"> <!-- Google Font: Source Sans Pro --> <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet"> </head> <body> <div class="split-input-form container-fluid"> <div class="Title">分割入力フォーム</div> <div class="docking"> <div class="docking-body p-3"> <ul class="nav nav-tabs"> <li class="nav-item"> <a href="#tab1-content" class="nav-link" data-toggle="tab">基本</a> </li> <li class="nav-item"> <a href="#tab2-content" class="nav-link" data-toggle="tab">住所</a> </li> <li class="nav-item"> <a href="#tab3-content" class="nav-link" data-toggle="tab">連絡先</a> </li> <li class="nav-item"> <a href="#tab4-content" class="nav-link" data-toggle="tab">確認</a> </li> </ul> <div class="tab-content"> <div id="tab1-content" class="tab-pane active"> <div> <span>基本の情報を入力します。</span> </div> <br /> <div class="font-weight-bold" style="margin: 10px 0px;">基本情報</div> <div> <span>あなたの名前と年齢を入力します。</span> </div> <br /> <div class="row form-group"> <label class="col-xl-1 d-flex align-items-center"> 名前 </label> <input type="text" data-bind="value: inputName, valueUpdate: 'input'" class="col-xl-11 form-control" id="InputName" placeholder="名前を入力" /> </div> <br /> <div class="row form-group"> <label class="col-xl-1 d-flex align-items-center"> 年齢 </label> <input type="text" data-bind="value: inputAge, valueUpdate: 'input'" class="col-xl-11 form-control" id="InputAge" placeholder="年齢を入力" /> </div> </div> <div id="tab2-content" class="tab-pane"> <div> <span>あなたの住所を入力します。</span> </div> <br /> <!-- general form elements --> <div class="row form-group"> <label for="selectPrefectureLabel" class="col-xl-1 d-flex align-items-center"> 都道府県 </label> <select class="form-control" data-bind="options: prefItems, value: selectedPref" id="selectPrefecture"></select> </div> <br /> <div class="form-group"> <label for="selectAreaLabel" class="col-xl-1 d-flex align-items-center"> 地域 </label> <div class="custom-control custom-radio"> <input class="custom-control-input" data-bind="checked: inputArea" type="radio" id="AreaRadioWest" name="AreaRadio" checked="" value="どちらかというと西"> <label for="AreaRadioWest" class="custom-control-label"> どちらかというと西 </label> </div> <div class="custom-control custom-radio"> <input class="custom-control-input" data-bind="checked: inputArea" type="radio" id="AreaRadioEast" name="AreaRadio" value="どちらかというと東"> <label for="AreaRadioEast" class="custom-control-label"> どちらかというと東 </label> </div> </div> </div> <div id="tab3-content" class="tab-pane"> <div> <span>希望する連絡手段を選んでください。(複数選択可能)</span> </div> <br /> <div class="form-group"> <div class="custom-control custom-checkbox"> <input class="custom-control-input" data-bind="checked: inputContact" type="checkbox" id="customboxMobile" value="携帯電話" /> <label for="customboxMobile" class="custom-control-label"> 携帯電話 </label> </div> <div class="custom-control custom-checkbox"> <input class="custom-control-input" data-bind="checked: inputContact" type="checkbox" id="customboxPhone" value="固定電話" /> <label for="customboxPhone" class="custom-control-label"> 固定電話 </label> </div> <div class="custom-control custom-checkbox"> <input class="custom-control-input" data-bind="checked: inputContact" type="checkbox" id="customboxFax" value="FAX" /> <label for="customboxFax" class="custom-control-label"> FAX </label> </div> </div> </div> <div id="tab4-content" class="tab-pane"> <div> <span>入力した内容を確認します。以下でよろしいですか?</span> </div> <br /> <div class="form-group"> <div class="row"> <label class="col-xl-1 d-flex align-items-center"> 名前 </label> <p class="col-xl-11" id="InputNameResult" data-bind="text: inputName" /> </div> <br /> <div class="row"> <label class="col-xl-1 d-flex align-items-center"> 年齢 </label> <p class="col-xl-11" id="InputAgeResult" data-bind="text: inputAge" /> </div> <br /> <div class="row"> <label class="col-xl-1 d-flex align-items-center"> 都道府県 </label> <p class="col-xl-11" id="InputPrefResult" data-bind="text: selectedPref" /> </div> <br /> <div class="row"> <label class="col-xl-1 d-flex align-items-center"> 地域 </label> <p class="col-xl-11" id="InputAreaResult" data-bind="text: inputArea" /> </div> <br /> <div class="row"> <label class="col-xl-1 d-flex align-items-center"> 連絡先 </label> <p class="col-xl-11" id="InputContactResult" data-bind="text: inputContact" /> </div> </div> </div> </div> </div> </div> </div> <!-- jQuery --> <script src="AdminLTE-3.0.2/plugins/jquery/jquery.min.js"></script> <!-- Bootstrap 4 --> <script src="AdminLTE-3.0.2/plugins/bootstrap/js/bootstrap.bundle.min.js"></script> <!-- AdminLTE App --> <script src="AdminLTE-3.0.2/js/adminlte.min.js"></script> <!-- knockout.js --> <script src="knockout-3.5.1/knockout-3.5.1.js"></script> <script> $(function () { var viewModel = { // // 初期値は空文字 // inputName: ko.observable(''), inputAge: ko.observable(''), prefItems : ['北海道', '本州', '四国', '九州'], selectedPref: ko.observable('北海道'), inputArea: ko.observable(''), inputContact: ko.observableArray() }; ko.applyBindings(viewModel); }); </script> </body> </html>knockout.js、まだよくわかってないけどいい感じ。
今は入力フォームを監視して、入力された値を確認画面にそのまま反映してるだけで、まだ氷山の一角って感じ。
例えば、「基本」タブの「名前」を「姓」「名」って分ける修正をするときに、ViewModelだけ更新すればUIも動的に変わってくれるって使い方ができそう。
ていうか、それが主目的のライブラリなんだっけか・・・・?もうちょい勉強して、拡張しやすいコードに更新したいですね。
でも、今は先に進むことを考えよう・・・・
- 投稿日:2020-02-20T21:48:33+09:00
アニメーションが終了してから プロパティーの値を変更したくても変わらない
FillBehavior = "Stop"
を 追加する。sample<DoubleAnimation Storyboard.TargetName="hoge" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="00:00:00.300" FillBehavior = "Stop" />
- 投稿日:2020-02-20T19:30:35+09:00
Unity API実装で値ちゃんと受け取れてるか確認
↓C#の頭いいやつが作ったリフレクションで変数群取れるやつ用意する
ObjectDumper.csusing System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Text; public class ObjectDumper { private int _level; private readonly int _indentSize; private readonly StringBuilder _stringBuilder; private readonly List<int> _hashListOfFoundElements; private ObjectDumper(int indentSize) { _indentSize = indentSize; _stringBuilder = new StringBuilder(); _hashListOfFoundElements = new List<int>(); } public static string Dump(object element) { return Dump(element, 2); } public static string Dump(object element, int indentSize) { var instance = new ObjectDumper(indentSize); return instance.DumpElement(element); } private string DumpElement(object element) { if (element == null || element is ValueType || element is string) { Write(FormatValue(element)); } else { var objectType = element.GetType(); if (!typeof(IEnumerable).IsAssignableFrom(objectType)) { Write("{{{0}}}", objectType.FullName); _hashListOfFoundElements.Add(element.GetHashCode()); _level++; } var enumerableElement = element as IEnumerable; if (enumerableElement != null) { foreach (object item in enumerableElement) { if (item is IEnumerable && !(item is string)) { _level++; DumpElement(item); _level--; } else { if (!AlreadyTouched(item)) DumpElement(item); else Write("{{{0}}} <-- bidirectional reference found", item.GetType().FullName); } } } else { MemberInfo[] members = element.GetType().GetMembers(BindingFlags.Public | BindingFlags.Instance); foreach (var memberInfo in members) { var fieldInfo = memberInfo as FieldInfo; var propertyInfo = memberInfo as PropertyInfo; if (fieldInfo == null && propertyInfo == null) continue; var type = fieldInfo != null ? fieldInfo.FieldType : propertyInfo.PropertyType; object value = fieldInfo != null ? fieldInfo.GetValue(element) : propertyInfo.GetValue(element, null); if (type.IsValueType || type == typeof(string)) { Write("{0}: {1}", memberInfo.Name, FormatValue(value)); } else { var isEnumerable = typeof(IEnumerable).IsAssignableFrom(type); Write("{0}: {1}", memberInfo.Name, isEnumerable ? "..." : "{ }"); var alreadyTouched = !isEnumerable && AlreadyTouched(value); _level++; if (!alreadyTouched) DumpElement(value); else Write("{{{0}}} <-- bidirectional reference found", value.GetType().FullName); _level--; } } } if (!typeof(IEnumerable).IsAssignableFrom(objectType)) { _level--; } } return _stringBuilder.ToString(); } private bool AlreadyTouched(object value) { if (value == null) return false; var hash = value.GetHashCode(); for (var i = 0; i < _hashListOfFoundElements.Count; i++) { if (_hashListOfFoundElements[i] == hash) return true; } return false; } private void Write(string value, params object[] args) { var space = new string(' ', _level * _indentSize); if (args != null) value = string.Format(value, args); _stringBuilder.AppendLine(space + value); } private string FormatValue(object o) { if (o == null) return ("null"); if (o is DateTime) return (((DateTime)o).ToShortDateString()); if (o is string) return string.Format("\"{0}\"", o); if (o is char && (char)o == '\0') return string.Empty; if (o is ValueType) return (o.ToString()); if (o is IEnumerable) return ("..."); return ("{ }"); } }使い方
Example.cs//データ表示したい型をuserに代入するとstringに変換される var dump = ObjectDumper.Dump(user);使用例
TestAPI.csusing NetworkAPI; using UnityEngine; using UnityEngine.UI; public class TestAPI : MonoBehaviour { Text test; // Start is called before the first frame update void Start() { test = this.gameObject.GetComponent<Text>(); //API送るなにか作った想定 APIManager.Api(hoge, GetRanking_Success, GetRanking_Failure); } private void GetRanking_Failure(NetworkStatus status, int bizerror, string bizerrormessage) { test.text = "失敗\nサーバーのURL接続の設定合ってますか?\n"; } private void GetRanking_Success(object data) { //受け取りたい型として変換 GetRankingResponseForm getData = data as GetRankingResponseForm; //受け取った型をstringに変換 string dump = ObjectDumper.Dump(getData); //表示 test.text = "成功\n"+dump; } }だいたいこんな感じでそれぞれのAPIごとにテストシーン作ってくといいかも
- 投稿日:2020-02-20T18:43:44+09:00
【Unity(C#)】サンプルから理解するUniRx
めちゃくちゃ難しかった
世の中には大量にUniRxに関する
最強にわかりやすい資料が転がっているのですが、
どうやら私は人より完全に理解した気になるのが早いらしく、
悪い勘違いをしたまま、完全に理解した気になっていたようです。現時点でもその可能性は否定できませんが、
実際のサンプルを用いて人に説明するつもりで学んでいけば、
より理解が深まるのではないかと思い、メモを残すことにしました。実際のサンプル
実際のサンプルがこちらです。
右の黒い柱のようなオブジェクト(この記事内では"壁"とします)に
球体(この記事内では"ボール"とします)が衝突すると
ライトの色がボールの色と同じ色に変化します。この程度ならUniRxを使わずとも簡単に実装可能なので
行われている処理をイメージしやすいと思いサンプルに選びました。UniRxを利用
では実際にUniRxを使った場合はどうなるのか見ていきます。
図の通りです。
と言いたいところですが
自分でも嫌になるくらいごちゃごちゃしてしまったので、
コードを追って説明する上での補足として見るぐらいがいいと思います。では実際にコードを追っていきます。
コード
using UniRx; using UnityEngine; using System; /// <summary> /// 壁にアタッチ /// </summary> public class CollisionNotify : MonoBehaviour { //下のコメントアウトしたプロパティを簡潔に書いたらこうなる //IObservableは外部のクラスで監視されるためpublicで公開しておく public IObservable<Color> colorObservable => colorSubject; //public IObservable<Color> triggerObservable //{ // get { return colorSubject; } //} //何かしら起きたことをお知らせする機能(SubjectのIObserver)はこのクラス内で使用するので外部に公開する必要はない readonly Subject<Color> colorSubject = new Subject<Color>(); //オブジェクトの衝突時、メッセージを発行する void OnCollisionEnter(Collision collision) { Color otherObjColor = collision.gameObject.GetComponent<MeshRenderer>().material.color; colorSubject.OnNext(otherObjColor); } }
CollisionNotify
クラス内ではIObservable
,Subject
,OnNext
が書かれています。しかし、実際にObserverパターンの中の具体的な処理を担っているのは
ここではSubject
,OnNext
のみです。
IObservable
は外部に公開しており、
他のクラス内でやってほしい処理を登録する役割を担います。
次に先ほど具体的な処理を担っていなかった
IObservable
に対して別クラスにて役割を与えます。using UnityEngine; using UniRx; /// <summary> /// 適当なオブジェクトにアタッチ /// </summary> public class LightColorChanger : MonoBehaviour { [SerializeField] CollisionNotify collisionNotify; [SerializeField] Light directionalLight; void Start() { //OnNextメッセージを受け取ったら実行(≒OnNextメッセージが飛んでくるまで監視される) collisionNotify.colorObservable .Subscribe(collisionObjectColor => { directionalLight.color = collisionObjectColor; Debug.Log("色変わったよ!"); }) .AddTo(this); } }コード内のコメントにもある通り、
発行された値(OnNextの引数に指定した値)を受け取って、
Subscribe内の処理を実行します。なので
Subscribe(collisionObjectColor => {})
のcollisionObjectColor
にはcolorSubject.OnNext(otherObjColor);
で
発行したメッセージの中身のotherObjColor
が入っています。つまり、
①オブジェクトが壁にぶつかる
②メッセージが発行される
③メッセージの中の値を受け取る
④登録した処理が実行される(値を利用できる)
となります。ここまでのメモを振り返って改めて最初の図を見ると、
最初に見た時よりはマシに見えてきました。Subjectは外部のクラスに公開しない
Subject
をreadonly
にして、OnNext
を発行するクラスを制限していました。CollisionNotifyクラスreadonly Subject<Color> colorSubject = new Subject<Color>();設計の観点から、メッセージを発行するクラスは、
不用意に外部のクラスに公開するのはよろしくないそうです。これは別にprivateでも問題ないのですが、
"外では使わないよ"というのを強調するために使っています。UniRx.Triggers
先ほどの図で説明した一連の流れをストリームソースと呼びます。
ただ、Unityには既にいろんなコールバックイベント(Button押したら...衝突したら...とか)が存在しています。
そこで、既存のコールバックイベントを活用してストリームソースを作りたい。。。
という願いに答えてくれる機能が既にあります。
例えば、先ほど例に挙げた、
OnCollisionEnterを検知しOnNextメッセージを発行するサンプル
は下記のように書き換えできます。
using UniRx.Triggers; //これ必要 using UniRx; using UnityEngine; public class UseOnCollisionEnterAsObservable : MonoBehaviour { [SerializeField] Light directionalLight; void Start() { this.OnCollisionEnterAsObservable() .Subscribe(collisionObject => { ColorChange(collisionObject); Debug.Log("色変わったよ!"); }) .AddTo(this); } void ColorChange(Collision collision) { directionalLight.color = collision.gameObject.GetComponent<MeshRenderer>().material.color; } }いろんなのがある(特にUI関連)ので実際に触ってみると便利さがわかるかと思います。
【参考リンク】:UniRx.Triggers
UniRxって結局何
Q. UniRxって結局何がすごいの?
という疑問。
その答えとしては、いろいろあるのでしょうが、A. MVPパターンを簡単に実装できる
が一番大きな利点でしょうか。(特にお仕事で利用する場合は)
それに伴ってまた疑問となるのが、
Q. MVPパターンって何がすごいの?
ですが、
A. 規模が大きくなっても比較的楽に拡張ができる
が答えでしょうか。
(私もあまり詳しくないので、もっと他に理由があれば教えてください)【参考リンク】:Unityで学ぶMVPパターン ~ UniRxを使って体力Barを作成する ~
最後に
もうちょっと踏み込んだ理解をしようと試みたんですが、
今の自分ではまだ難しかったです。これから学習する方の入門書の入門書となれば幸いです。
今後はそもそもの理解度を深めつつ、Hot、Cold等の話もインプットして自分なりのメモを残そうと思います。
参考リンク
- 投稿日:2020-02-20T12:09:05+09:00
XmlSerializerで複数種類のエレメントのListを定義したい
XmlSerializerで複数種類のエレメントのListを定義したい
設定ファイルなんかで Listを使うんだけど、設定内容はほとんど一緒なのに動作がちょっとだけ違うみたいな場合
plantumlclass Config{ string ConnectionString List<Supplier> Suppliers } class Supplier{ string Name string Query string AdditionalParameter string getItemName(SqlDataReader reader) } class SealCountSupplier{ string OptionalInfo override string getItemName(SqlDataReader reader) } Config *-right- Supplier Supplier <|-- SealCountSupplierこんな形にして
<?xml version="1.0" encoding="utf-8"?> <Config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <ConnectionString>hogehoge...</ConnectionString> <Suppliers> <Supplier> <Name>Nissin</Name> <Query>select * from...</Query> <AdditionalParameter /> </Supplier> <SealCountSupplier> <Name>Maruchan</Name> <Query>select * from...</Query> <AdditionalParameter /> <OptionalParameter>100</OptionalParameter> </SealCountSupplier> </Suppliers> </Config>こんな定義にしたい。
でも普通に
XmlSerialiser.Deserialize()
するとSealCountSupplier
が復元されない。そんなあなたに XmlArrayItemAttribute
XmlSerializer がシリアル化された配列で配置できる派生型を指定する属性を表します。
XmlArrayItemAttribute クラス (System.Xml.Serialization) | Microsoft Docspublic class Config { public string ConnectionString{get;set;} public Config() { } [XmlArrayItem(Type = typeof(Supplier))] [XmlArrayItem(Type = typeof(SealCountSupplier))] public List<Supplier> Suppliers = new List<Supplier>(); }こうじゃ。
- 投稿日:2020-02-20T11:43:53+09:00
Unity2018までのInputManagerを拡張していたら車輪の再発明になっていた話
そもそもの話
- 元々はUnity2018.2.8f1で利用するためのInputManager拡張スクリプトだった(特にXInputを使うゲームパッド用)。
- エディタ拡張の練習がてら拡張をしようとしたときにInputSystemが発表された。
- InputSystemはUnity2019.1から利用可能
とどのつまり、元々再発明をしようとして作ったわけではないです。
Unity2018をまだ利用している方がいらっしゃれば参考にしていただければと思います。Unity2019.1から利用可能なInputSystemについての解説は一切行いませんのでご注意ください!
開発環境
Windows10 Home
Unity2018.2.8f1→Unity2018.4.17f1
VisualStudio 2017 Community→VisualStudio 2019 professional上記環境(特に後者)以外での動作は一切保証してません。
ソースコード
載せているコード外でいくつかバグが発覚したため不具合調整中です
エディタ拡張表示部分
InputManagerEX.csvoid Display() { switch (selected) { #region 操作名 case 0: inputCodeCount = EditorGUILayout.IntField("操作の数", inputCodeCount); using (var scrollView = new EditorGUILayout.ScrollViewScope(new Vector2(0, scroll))) { for (int i = 0; i < inputCodeCount; i++) { try { inputCode[i] = EditorGUILayout.TextField("操作の名前", inputCode[i]); } catch { inputCode.Add(""); inputCode[i] = EditorGUILayout.TextField("操作の名前", inputCode[i]); } } scroll = scrollView.scrollPosition.y; } //横並びの開始 EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Refresh")) { Debug.Log("画面更新開始"); try { LoadEnum(); Debug.Log("画面更新完了"); } catch (System.Exception e) { Debug.LogError(e.Message); throw; } } if (GUILayout.Button("Export")) { Debug.Log("出力開始"); try { CreateEnum(); ClearDictionary(); Debug.Log("出力完了"); } catch (System.Exception e) { Debug.LogError(e.Message); throw; } } //横並び終了 EditorGUILayout.EndHorizontal(); break; #endregion case 1: try { using (var scrollView = new EditorGUILayout.ScrollViewScope(new Vector2(0, scroll))) { //更新されるたびに初期化する Dictionary<InputCode, KeyCode> keyListDic = new Dictionary<InputCode, KeyCode>(); Dictionary<InputCode, ButtonList> buttonListDic = new Dictionary<InputCode, ButtonList>(); //表示部分更新処理 for (int i = 0; i < (int)InputCode.END; i++) { EditorGUILayout.BeginHorizontal(); #region 操作 EditorGUILayout.BeginVertical(); //Enumのポップアップ表示 GUILayout.Label("操作の名前"); //string型でinputListsと同時に生成などしてるので問題ない GUILayout.Label(inputCode[i]); EditorGUILayout.EndVertical(); #endregion #region ボタン EditorGUILayout.BeginVertical(); GUILayout.Label("ボタンの名前"); buttons[i] = (ButtonList)EditorGUILayout.EnumPopup(buttons[i]); if ((32 < (int)buttons[i]) && ((int)buttons[i] < 41) && !inputManager.CodeAtTriggerDictionary.ContainsKey(inputs[i])) inputManager.SetTrigger(inputs[i], inputManager.GetTriggerAtButton(buttons[i])); else if ((0 < buttons[i]) && ((int)buttons[i] < 33) && !inputManager.CodeDictionary.ContainsKey(inputs[i])) inputManager.SetButton(inputs[i], inputManager.GetKeyCodeAtButton(buttons[i])); else if (!inputManager.CodeDictionaryOnKeyBoard.ContainsKey(inputs[i])) inputManager.SetKeyCode(inputs[i], inputManager.GetKeyCodeAtButton(buttons[i])); EditorGUILayout.EndVertical(); #endregion #region キー EditorGUILayout.BeginVertical(); if (inputManager.CodeAtTriggerDictionary.ContainsKey(inputs[i]) && inputManager.TriggerAtButtonDictionary.ContainsKey(buttons[i])) { GUILayout.Label("トリガー"); GUILayout.Label(inputManager.GetTriggerAtButton(buttons[i]).ToString()); } else { GUILayout.Label("キーコード"); GUILayout.Label(inputManager.GetKeyCodeAtButton(buttons[i]).ToString()); } EditorGUILayout.EndVertical(); #endregion EditorGUILayout.EndHorizontal(); } #region メッセージ EditorGUILayout.BeginVertical(); for (int i = 0; i < (int)InputCode.END; i++) { int count = 0; for (int j = 0; j < (int)InputCode.END; j++) { if ((buttons[i] == buttons[j]) && (i != j)) { count++; } } EditorGUILayout.BeginHorizontal(); if (count > 0) { GUILayout.Label("Attention!!:同一のボタンが割り当てられています!『対象の操作名:" + inputs[i] + ", 対象のボタン:" + buttons[i] + ", 重複している個数:" + count + "』"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (buttons[i] == ButtonList.None) { GUILayout.Label("Attention!!:ボタンが割り当てられていません!『対象の操作名:" + inputs[i] + "』"); } EditorGUILayout.EndHorizontal(); GUILayout.Space(10); } EditorGUILayout.EndVertical(); #endregion scroll = scrollView.scrollPosition.y; } } catch (System.Exception e) { Debug.LogError(e.Message); throw; } break; } }解説
switch文で表示を切り替えています。
1番 InputCode
こちらはswitch文での1番。利用する操作の数と操作の名前を入力。
最終的にExportのボタンを押すことで列挙型を含めた.csファイルを生成しています。2番 CodeJoin
操作とキーコード(コントローラーのボタン)を結び付ける部分です。
同一のボタンが割り当てられている場合とボタンが割り当てられていないときにアテンションします。なお、ボタンの名前を保存する列挙型(ButtonCode.cs)はキーコードとの連動部分でDictionaryでの設定が必要になるために今回はGUIでの設定の実装を見送りました。
実際の処理部分
長いので小分けにします
インスタンス生成部分
InputManager.cs/// <summary> /// インスタンス取得 /// </summary> /// <returns>実体</returns> public static InputManager GetInstance { get { if (inputManager == null) { //GameObjectの生成 myObject = new GameObject("InputManagerGO"); //InputManagerのAdd myObject.AddComponent<InputManager>(); inputManager = myObject.GetComponent<InputManager>(); //初期化 inputManager.Initialize(); } return inputManager; } } public static void DeleteObjct() { #if UNITY_EDITOR DestroyImmediate(myObject); #endif } /// <summary> /// 再生直後に一回だけ実行 /// </summary> void Awake() { #if UNITY_STANDALONE //破壊不可 DontDestroyOnLoad(gameObject); #endif }解説
シングルトンパターンで作製しています。
最初に呼び出されたときに初期化を行い、Awakeメソッドを利用することでアプリケーション開始の最初のフレームでDontDestroyOnLoadオブジェクトとして設定しています。また、InputCode.csを生成する際、ゲームオブジェクトである「InputManagerGO」がアセットの再読み込みを行うAssetDatabase.Refreshメソッドを呼び出すと「InputManagerGO」が再生成されてしまいます。すると、アセットの再読み込みの度に仕事をしない「InputManagerGO」が増えてしまいます。
InputCode.cs生成部分
InputManagerEX.cs/// <summary> /// InputCodeの生成 /// </summary> void CreateEnum() { string path = Application.dataPath + "\\Resouces\\XInputAssistManager\\Scripts\\InputCode.cs"; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("namespace XInputAssistManager"); stringBuilder.AppendLine("{"); stringBuilder.AppendLine(" public enum InputCode"); stringBuilder.AppendLine(" {"); //リストの中身を入れる for (int i = 0; i < inputCodeCount; i++) { stringBuilder.Append(" "); stringBuilder.Append(inputCode[i]); stringBuilder.AppendLine(","); } stringBuilder.Append(" "); stringBuilder.Append("END"); stringBuilder.AppendLine(","); stringBuilder.AppendLine(" }"); stringBuilder.AppendLine("}"); // 文字コードを指定 Encoding enc = Encoding.GetEncoding("UTF-8"); // ファイルを開く StreamWriter writer = new StreamWriter(path, false, enc); // テキストを書き込む writer.Write(stringBuilder.ToString()); // ファイルを閉じる writer.Close(); Debug.Log(path + "に出力"); InputManager.DeleteObjct(); AssetDatabase.Refresh(); try { LoadEnum(); } catch (System.Exception e) { Debug.LogError(e.Message); throw; } }そこで読み込む直前でDestroyImmediateメソッド(をラッパーしたメソッド)を呼び出すことで古い「InputManagerGO」を削除しています。
注意
利用する場合、ProjectSettingsのInputのAxisに"LT(number)"(かっこは不要)をJoyStickAxisの9th axisで追加しなければなりません。
同様に、ProjectSettingsのInputのAxisに"RT(number)"をJoyStickAxisの10th axisで追加しなければなりません。
(numberはコントローラーの数ですので、InputのJoyNumでコントローラーの番号を指定してください)詳しいことはGitHubに
プロジェクトとunitypackageを置いておきますので、そちらで確認してください。参考にさせていただいた記事
- 投稿日:2020-02-20T01:45:08+09:00
C#でDinic法を実装して最大二部マッチング問題、最大フロー問題を解く
はじめに
atcoderの過去問を解いている際に、最大二部マッチングに帰着できる問題に遭遇した。
C - 2D Plane 2N Points
(なお、この問題自体はグラフ理論の知識無しでも工夫すれば解ける問題だったので、気になった方は考えてみてほしい)競技プログラミングでは最大二部マッチング問題は頻出らしいのだが、自分は初心者に毛が生えた程度の実力なので、最大二部マッチング問題であることには気付いたものの対応するデータ構造、アルゴリズムを用意していなかった。
典型問題だしググれば出てくるだろうと思ったのだが、やはり競技プログラミングでC#erは圧倒的に少数派らしく、コピペで使用できそうなソースコードを見つけることができなかったので、自分で実装したものを残しておくことにした。
最大二部マッチング問題は、最大フロー問題の特殊な場合として扱える。以下の記事が参考になる。
実世界で超頻出!二部マッチング (輸送問題、ネットワークフロー問題)の解法を総整理!今回使用するDinic法というものは最大フロー問題を高速に解くことができるアルゴリズムで、Ford-Fulkerson法を改善したもの。アルゴリズムの動作原理は以下の記事が詳しい。
tkw's diary - Dinic法ソースコード
using System; using System.Collections.Generic; using System.Linq; namespace Hoge { class Dinic { public Dinic(int node_size) { V = node_size; G = Enumerable.Repeat(new List<Edge>(), V).ToList(); level = Enumerable.Repeat(0, V).ToList(); iter = Enumerable.Repeat(0, V).ToList(); } class Edge { public Edge(int to, int cap, int rev) { To = to; Cap = cap; Rev = rev; } public int To { get; set; } public int Cap { get; set; } public int Rev { get; set; } } List<List<Edge>> G; int V; List<int> level; List<int> iter; public void AddEdge(int from,int to,int cap) { G[from].Add(new Edge(to, cap, G[to].Count)); G[to].Add(new Edge(from, 0, G[to].Count - 1)); } public int MaxFlow(int s,int t) { int flow = 0; while (true) { BFS(s); if (level[t] < 0) { return flow; } iter = Enumerable.Repeat(0, V).ToList() ; var f = DFS(s, t, 1000000007); while (f > 0) { flow += f; f = DFS(s, t, 1000000007); } } } void BFS(int s) { level = Enumerable.Repeat(-1, V).ToList(); level[s] = 0; var que = new Queue<int>(); que.Enqueue(s); while (que.Count != 0) { var v = que.Dequeue(); for(int i = 0; i < G[v].Count; i++) { var e = G[v][i]; if(e.Cap>0 && level[e.To]< 0) { level[e.To] = level[v] + 1; que.Enqueue(e.To); } } } } int DFS(int v,int t,int f) { if (v == t) return f; for(int i = iter[v]; i < G[v].Count; i++) { iter[v] = i; var e = G[v][i]; if(e.Cap>0 && level[v] < level[e.To]) { var d = DFS(e.To, t, Math.Min(f, e.Cap)); if (d > 0) { e.Cap -= d; G[e.To][e.Rev].Cap += d; return d; } } } return 0; } } }使用法
有向グラフの辺の数だけAddEdgeを呼び出し、辺の始点、終点、重みを与えてやればよいです。MaxFlowが最大フローを返します。
注意点
このソースコードは辺の重みがint型の範囲内のものでしか使用できません。辺の重みがlong型やdouble型の問題に利用したい場合は、EdgeクラスのCapプロパティに対応する部分を書き換える必要があります。C#のジェネリックは数値型のみというような制約が現状ではできないようです。にしてももうちょいうまいやり方なかったのか。decimalとか使えばいいのか?わからん。
また、深さ優先探索で十分に大きい値として用いている1000000007も、場合によっては修正してやる必要があります。
最後に
C#は競プロでの利用者少ないし速度もそんな早くないしでなかなかつらいんだけど、やっぱり好きな言語なのでこれからも使っていきたいです。この記事がC#erの助けになったら嬉しいな。
間違い、改善点等ありましたらコメント等でご指摘いただけると幸いです。