JavaScriptでマイクロ秒単位の定期処理を実行する(npm fractional-timer)
JavaScriptで1ミリ秒より短いインターバルタイマー処理を提供するモジュール fractional-timer
のご紹介。
Link: Flickr PAGE - CC BY-SA 2.0
実際のところ、精度はよくありませんので、クリティカルな用途には向きません。 単純な処理をなるべく高速にタイマーで実行したいけど、標準タイマーの1ミリ秒では遅すぎる・・・といった時に使える感じ。 処理が重い場合は、呼び出し間隔は長くなります。
Chrome と Node.Js で、正しく動いていることを確認していますが、 FirefoxやEdgeでは、まともに動いていなかったので、ご注意下さい。
使い方
使い方は遅延時間に実数を指定できる点を除けば、標準のsetInterval
/clearInterval
と同じです。
遅延時間は標準と同じくミリ秒単位で指定します(0.1=100マイクロ秒)。
以下のように呼び出すことで、タイマーの数と遅延時間等を調整して、定期処理を実行します。
(function() { "use strict"; var ft = require("fractional-timer"); var ftid = ft.setInterval( function(){ ft.clearInterval(ftid); }, 0.001);// 1 microsecond }());
インストール
npmでインストールすればNode.jsから使えます。
> npm install fraction-timer
ブラウザのWEBアプリでは、browserify
でrequire
するか、
script
タグで読み込んで、FractionalTimer
クラスの静的メソッドsetInterval
/clearInterval
を直接使ってくださいね。
リポジトリ
その他、能書き
やっていることは単純です。複数のインターバルタイマーで一つの処理を呼び出しているだけです。
例えば、1マイクロ秒で実行したい場合、1ミリ秒のタイマーを1000個使って呼び出すということです。
実際には、指定された間隔から算出される単位時間あたりの実行回数を計算し、タイマーの数と間隔を自動的に算出、ベストエフォートで呼び出します。
「MZ-700フルJavaScriptエミュレータ」で、エミュレーションをワーカースレッドの定期処理で実装していて、当初は単一のインターバルタイマーで、最も高速になるように調整していたのですが、ある時タイマーが二重に動いてしまったことがあって、速度がなんと2倍程度に跳ね上がっていたのです。
「うわあ!世紀の大発見!!!」と思ったけれど、単一スレッド内ではシーケンシャル動作が保証されているので、当然の挙動なんですよね。
ただ、こういうことをやってる人があまりいないところを見ると、需要はないみたいなんですけどね(笑)
.NETのアプリケーション設定を編集可能なXMLに保存する
.NETのプロジェクトでは「アプリケーション構成ファイル」というファイルに設定情報を書いておけます。 でもあれは実行時に書き換えるような利用方法は想定されていません。 インストーラーでセットアップすると、アプリケーションフォルダに置かれるため、読み取り専用で、編集するにも管理者権限が必要です(また、書き換えたつもりでも、何かのタイミングで元に戻るということも・・・どういう機構なのか知らないのですが・・・)
そこで、ここでは、編集可能なXMLファイルをアプリケーションの動的な設定情報として利用するためのサンプルコードをご紹介。 基本クラスは、お持ち帰り可能です。ご自由にご利用ください。
- はじめに - 機能仕様
- XML設定クラス定義例:SampleSettingsクラス
- 設定ファイルの読み込み処理
- XMLファイルの例:SampleSettings.xml
- XML設定ファイルの基本クラス:XmlSettingFileクラス
- 設定ファイルはWindowsの特殊フォルダ以下へ
- 参考サイト
売り上げランキング: 16,889
1. はじめに - 機能仕様
ここでやりたいことをザックリ以下に箇条書き。
- アプリ起動時に特定のフォルダに保存されたXMLの設定ファイルを読み込む。
- ファイルがない場合は、アプリ起動時に既定の内容で設定ファイルを作成する。
- 設定情報はクラスのインスタンスとしてプログラムからアクセスできる。
- 最低限プリミティブ型が正しく読み書きできること。
設定読み込み時の値のチェックはしていません。 バリデーション機能を持たせたメタクラスを導入すれば可能ですが、大げさになり過ぎるので、またおいおい。
2. XML設定クラス定義例:SampleSettingsクラス
以下がXML設定情報を保持するクラス。
設定ファイルに保存したい値をメンバとして定義したシンプルなクラスです。
XMLシリアライズを利用しているのでパブリックなデフォルトコンストラクタが必要です。
基本クラスの XmlSettingFile
クラス は、下で説明します。
/// <summary> /// XMLアプリケーション設定サンプル /// </summary> public class SampleSettings : Utils.XmlSettingFile { /// <summary> /// 既定の設定情報を生成。 /// (デフォルトコンストラクタは必ず必要) /// </summary> public SampleSettings() { } //以下のメンバーはXMLに保存される public string Name { get; set; } = "Koji Takami"; public string Email { get; set; } = "vzg03566@gmail.com"; public int FeatureOption { get; set; } = 0; public bool DebugOption { get; set; } = false; //保存したくないメンバーは以下のように宣言 [System.Xml.Serialization.XmlIgnore] public string NotSaved; }
3. 設定ファイルの読み込み処理
上のクラスのインスタンスをXMLファイルから読み込むには、XmlSettingFile.Load
を使用します。
このメソッドには、保存場所のルートを決めるスペシャルフォルダの列挙子と、既定値を保持する設定情報のインスタンスを指定します。
以下のコードでは、設定ファイルを読み込んで、SampleSettingクラスのインスタンスを返します。
public partial class App : Application { static public SampleSettings sampleSettings = new SampleSettings(); public App() { try { sampleSettings = Utils.XmlSettingFile.Load( Environment.SpecialFolder.CommonApplicationData, new SampleSettings()) as SampleSettings; } catch (Exception ex) { Console.WriteLine("Error:{0}", ex.Message); } finally { sampleSettings.Save(); } } }
- 上のコードで読み書きされる設定ファイルは、「C:\ProgramData\(CompanyName)\(ProductName)\(ProductVersion)\SampleSetting.xml」です。ここで、(CompanyName)、(ProductName)、(ProductVersion) はアセンブリのファイルバージョン情報の設定値です。プロジェクトの初期状態ではCompanyNameが空白ですが、その部分抜きで正しいパスに生成されます。
- 読み込み時にファイルがなければ、既定値のインスタンスを使用して作成されます。
- ここでは、アプリケーションクラスのコンストラクタで読み出していますが、別のタイミングでも問題はありません。
- 読み込んだ直後に保存しているのは、開発中に追加項目を反映したりObsoleteな設定項目をXMLから削除するためです。これがなくてもファイルがなければ作成されます。このSaveは、必ずしも必要ではありません。
4. XMLファイルの例:SampleSettings.xml
最初の実行時に生成されたファイルの中身は以下のようになります。 書き換えて読み込んだら、ちゃんと設定値が反映されます。それが目的ですからね。
<?xml version="1.0" encoding="utf-8"?> <SampleSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Takami Koji</Name> <Email>vzg03566@gmail.com</Email> <FeatureOption>0</FeatureOption> <DebugOption>false</DebugOption> </SampleSettings>
5. XML設定ファイルの基本クラス:XmlSettingFileクラス
以下が、このエントリーの御本尊。 派生クラスのメンバーをXMLファイルへシリアライズ/デシリアライズするクラス。 (名前空間は適当に変えてください)
ファイル名の決定や、アクセス権の設定などで長くなっていますが、やってることは意外にシンプル。XmlSerializerのお陰様。
using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.AccessControl; using System.Security.Principal; using System.Text; using System.Xml.Serialization; namespace StudyDotNet.Utils { public class XmlSettingFile { /// <summary> /// 設定ファイルを読み込む /// </summary> /// <param name="folderId"> /// スペシャルフォルダを指定する</param> /// <param name="initial"> /// ファイルがない場合の既定値を持つ /// インスタンス</param> /// <param name="filename"> /// ファイル名。 /// 省略時はクラス名を採用する</param> /// <returns></returns> public static XmlSettingFile Load( Environment.SpecialFolder folderId, XmlSettingFile initial, string filename = null) { //ファイル名を生成 string pathname = CreatePathname( folderId, initial, filename); //設定ファイルがなければ既定の値で生成する if (!File.Exists(pathname)) { Save(pathname, initial); AllowUsersReadAndWrite(pathname); } //設定ファイルを読み込む return Read(pathname, initial.GetType()); } /// <summary> /// 設定ファイル名。 /// 実際に読み込んだもの。 /// 書き戻す場合にも利用する。 /// </summary> [XmlIgnore] public string Filename { get; private set; } /// <summary> /// 設定ファイルへ書き戻す。 /// プログラムから設定値を変更した場合などに使用。 /// </summary> public void Save() { Save(Filename, this); } #region StaticPrivate /// <summary> /// 与えられた情報からファイル名を生成する /// </summary> /// <param name="folderId"></param> /// <param name="initial"></param> /// <param name="filename"></param> /// <returns></returns> static private string CreatePathname( Environment.SpecialFolder folderId, XmlSettingFile initial, string filename) { //ファイル名が指定されていないならクラス名 if(filename == null) { filename = initial.GetType().Name; } //拡張子がXMLでない場合はXMLに変更する if(Path.GetExtension(filename) == "") { filename = Path.ChangeExtension(filename, "xml"); } //ファイル名を決定 string pathname = Path.Combine( CreateFolder(folderId), filename); return pathname; } /// <summary> /// スペシャルフォルダ以下のパスを生成 /// </summary> /// <param name="folderId"></param> /// <returns></returns> static private string CreateFolder( Environment.SpecialFolder folderId) { //アセンブリのファイルバージョン情報を利用してフォルダを決定 var verinfo = FileVersionInfo.GetVersionInfo( Assembly.GetExecutingAssembly().Location); // 空白の項目はパスに含まれない。 // 例えば CompanyName が空の場合、 // @"C:\ProgramData\ProductName\1.0.0.0" などとなる string path = Path.Combine(new string[] { Environment.GetFolderPath(folderId), verinfo.CompanyName, verinfo.ProductName, verinfo.ProductVersion, }); //フォルダがないなら生成する if(!Directory.Exists(path)) { Directory.CreateDirectory(path); } return path; } /// <summary> /// 設定を設定ファイルに保存 /// </summary> /// <param name="filename"></param> /// <param name="setting"></param> static private void Save( string filename, XmlSettingFile setting) { var writer = new StreamWriter( filename, false, new UTF8Encoding(false)); var xml = new XmlSerializer(setting.GetType()); xml.Serialize(writer, setting); writer.Close(); } /// <summary> /// 設定ファイルを読み込む /// </summary> /// <param name="filename">設定ファイル</param> /// <param name="type">設定データの型</param> /// <returns>type型のインスタンス。 /// ファイルない場合はnullを返す。</returns> static private XmlSettingFile Read( string filename, Type type) { var reader = new StreamReader( filename, new UTF8Encoding(false)); var xml = new XmlSerializer(type); var setting = xml.Deserialize(reader) as XmlSettingFile; reader.Close(); if(setting != null) { setting.Filename = filename; } return setting; } /// <summary> /// 指定ファイルにUsersグループのユーザーの読み書き権限を与える /// </summary> /// <param name="filename"></param> static private void AllowUsersReadAndWrite(string filename) { FileSystemAccessRule rule = new FileSystemAccessRule( new NTAccount("Users"), FileSystemRights.Write | FileSystemRights.Read, AccessControlType.Allow); FileSecurity security = File.GetAccessControl(filename); security.AddAccessRule(rule); File.SetAccessControl(filename, security); } #endregion } }
6. 設定ファイルはWindowsの特殊フォルダ以下へ
参考のため、アプリケーションの設定ファイルを保存するために適当なスペシャルフォルダを抜き出してみました。 下表はDOBON.NET プログラミング道:特殊ディレクトリのパスを取得するからの抜粋です。
Windows / .NET的に設定ファイルは、以下のようなフォルダ以下に、アプリケーションの企業名、製品名、バージョンなどのフォルダを掘って、保存するのが望ましいようです。
Environment.SpecialFolder列挙体のメンバ | 具体例(参考) | |
---|---|---|
1 | CommonApplicationData | C:\ProgramData |
2 | ApplicationData | C:\Users\(UserName)\AppData\Roaming |
3 | LocalApplicationData | C:\Users\(UserName)\AppData\Local |
4 | UserProfile | C:\Users\(UserName) |
6 | MyDocuments | C:\Users\(UserName)\Documents |
5 | CommonDocuments | C:\Users\Public\Documents |
全ユーザーに共通する設定なら1が良いと思います。ユーザー別なら2か3、4もあり。ただしローミングが必要なら2であるべきですね。
5と6については非推奨。Documents
に設定ファイルを置くのはちょっと違うかもしれないですからね。
7. 参考サイト
OpenCVの画像処理をお手軽に ― OpenCVフィルター処理ライブラリ cvImagePipeline のご紹介
OpenCVの画像処理をお手軽にできるC++向けフィルター処理ライブラリ cvImagePipeline のご紹介。
画像処理の流れや各モジュールのパラメータの指定をXMLで記述できますので、画像処理の処理順の変更やパラメーター調整作業がはかどります。 複数のフィルターを組み合わせて独自のフィルターも作れます。
背景除去のサンプルプログラム
以下の画像は、ライブラリに含まれているサンプルプログラムの実行中画面です。
単純な背景除去の実装実験を行っている画面です。 入力画像や前処理中の画像などを、6分割した画面に統合して表示しています。 (カメラ入力や画面の分割、Window表示もそれぞれ、このライブラリのフィルターです。)
左上から順に、
- キャプチャ画像(の鏡像)
- 1をグレイスケールに変換
- 2に対して、ヒストグラム均一化とガウシアンブラーを適用
- 3の過去18000フレーム分を平均
- 3と4の差分(絶対値差分)
- 5の二値化
このあと、6の二値化画像に対して、膨張(dilate)と縮小(erode)を適当に繰り返してから元画像(1の画像)をマスクすれば背景と前景に分離できそうです。
処理は全てsample.xml
(↓)に記述しており、参考として下に掲載しているC++のコード(capture.cpp
)で読み込んで処理させています。
このためビルド無しで処理を変更可能です。(XMLを読み込む処理は複数のフィルターをまとめるImgProcSet
の機能です)
sample.xml
- 背景除去するXML
<cvImagePipeline name="testProcessor"> <Processor class="VideoCapture" name="cap"> <Property name="deviceIndex" value="0"/> </Processor> <Processor class="Flipper" name="fripHoriz"> <Property name="flipDir" value="1"/> </Processor> <Processor class="ImagePoint" name="raw"/> <Processor class="ColorConverter" name="grayscale"/> <Processor class="EqualizeHist" name="equalizeHist"/> <Processor class="GaussianBlur" name="blur"/> <Processor class="DepthTo32F" name="depth32F"/> <Processor class="ImagePoint" name="pp"/> <Processor class="RunningAvg" name="background"> <Property name="averageCount" value="18000"/> </Processor> <Processor class="AbsDiff" name="diff" autoBind="false"> <Input to="src1" from="pp"/> <Input to="src2" from="background"/> </Processor> <Processor class="Convert" name="to8UC"> <Property name="rtype" value="0"/> <Property name="alpha" value="255"/> <Property name="beta" value="0"/> </Processor> <Processor class="Threshold" name="binary"> <Property name="type" value="CV_THRESH_BINARY"/> <!-- CV_THRESH_BINARY CV_THRESH_BINARY_INV CV_THRESH_TRUNC CV_THRESH_TOZERO CV_THRESH_TOZERO_INV --> <Property name="otsu" value="1"/> <Property name="thresh" value="50"/> <Property name="maxval" value="255"/> </Processor> <Processor class="FitInGrid" name="integratedImage" autoBind="false"> <Property name="width" value="960"/> <Property name="height" value="480"/> <Property name="cols" value="3"/> <Property name="rows" value="2"/> <Property name="interleave" value="0"/> <Input to="0" from="raw"/> <Input to="1" from="grayscale"/> <Input to="2" from="blur"/> <Input to="3" from="background"/> <Input to="4" from="diff"/> <Input to="5" from="binary"/> </Processor> <Processor class="ImageWindow" name="window"> <Property name="windowName" value="cvImagePipeline"/> <Property name="showFPS" value="1"/> </Processor> </cvImagePipeline>
capture.cpp
- XMLを処理するプログラム
#include "stdafx.h" #if defined(_MSC_VER) #include <windows.h> #else #include <unistd.h> #define Sleep(millisec) usleep(millisec * 1000) #endif #include <opencv2/opencv.hpp> #include "cvImagePipeline.h" using namespace cvImagePipeline; using namespace cvImagePipeline::Filter; #if defined(_MSC_VER) int _tmain(int argc, _TCHAR* argv[]) #else int main(int argc, char* argv[]) #endif { cvInitSystem(argc, argv); ImgProcSet processors; std::string xml_filename("sample.xml"); if (argc > 1) { xml_filename = argv[1]; } if (!processors.loadXml(xml_filename)) { std::cerr << "ファイル読み込み失敗 ファイル名:" << xml_filename << std::endl; return -1; } Sleep(2000); while(true) { processors.execute(); int ch = cvWaitKeyEx(1); if (ch == '\x1b') { fprintf(stderr, "exit.\n"); break; } } cvDestroyAllWindows(); return 0; }
リポジトリ
上の例も含めて、その他詳細は下記リポジトリのREADMEを参照してください。
実を言うと、このリポジトリ、1年以上放置しているんですねえ。 しかし今でも Clone してくださる方が月に数人いらっしゃるようです。 READMEが古くなっていて申し訳ないですから、これを機にキチンとメンテしようと思ってはいますが・・・。
そういや以下の記事でもシレッと使っていますねw
今後の課題
今考えつく今後の課題は以下の様なことです。放置している場合ではないなあ。
- OpenCV 3.0以上での動作確認。
- Windows 10 での動作確認。
- 基本フィルタと他のサンプルフィルタの分離。
- 追加実装のしやすさを追求。
- XMLをXAML的文法に変更(記述量を少なくできそう)。
- XMLのビジュアルな編集。
- デバッグ機能の充実。
- 実行中のフィルタのバイパスやパラメータの変更機能。
- Pythonから利用できるインタフェース。C++はやっぱり敷居が高いかも。
- 妙に凝った変な名称のフィルタを改名するw
まだ間に合うXAMLの基本
「XAMLはなんだか複雑だ」と思ってました。
しかし、あることに気が付いてから「なかなかシンプルなんじゃない?」と思えるようになりました。
ちゃんと知っている方にとっては、当たり前のことかと思いますが、その「ちょっとしたこと」を出発点に、1段掘り下げて調べた結果を書いておきます。
photo credit: Doug Kline Star Wars Celebration IV - X-Wing fighter (back) via photopin (license)
- XAML要素でインスタンスが生成される
- 属性でパブリックプロパティを設定する
- プロパティへクラスオブジェクトを代入する
- 静的クラスのプロパティへオブジェクトを代入する
- そもそも子要素は何処に格納されるのか
1. XAML要素でインスタンスが生成される
XAMLの要素名はクラス名で、実行時には、そのクラスのデフォルトコンストラクタでインスタンスが生成される。
クラス名であることは知っていましたが、デフォルトコンストラクタで生成されるという認識がありませんでした。 だから実行時のオブジェクトの状態を正確に把握できていなかったんですわ。
MainWindow.xaml(MVVM的に真っ当にMessageBoxを表示する - 銀の弾丸より)
<Window x:Class="StudyDotNet.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" xmlns:local="clr-namespace:StudyDotNet" xmlns:vm="clr-namespace:StudyDotNet.ViewModels" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:tr="clr-namespace:StudyDotNet.Triggers" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> <i:Interaction.Triggers> <tr:MessageBoxTrigger> <tr:MessageBoxAction /> </tr:MessageBoxTrigger> </i:Interaction.Triggers> <Grid> <Button x:Name="button" Content="Button" Command="{Binding SampleCommand}" HorizontalAlignment="Left" Margin="50,50,0,0" VerticalAlignment="Top" Width="75"/> </Grid> </Window>
takamints.hatenablog.jp
上のXAMLでは、次のクラスオブジェクトが生成されています。 (ドットを含む要素名と、子要素の所在については後述します)
- MainWindowViewModel
- MessageBoxTrigger
- MessageBoxAction
- Grid
- Button
確かにビューモデルやボタンなどは、実行時に生成されていて、コードビハインドからアクセスできますよね。 でも、デフォルトコンストラクタとの関連は想像していなかったんです。 このような、標準の部品に関しては、.NETフレームワーク内で特別なことが行われているのだろう・・・などと思ってました。
でも、自作クラスも生成されてるってことは・・・という段になって、やっと気付いた。
思った以上にシンプルですね。そもそもフレームワークをそんなガチガチに作るわけ無いですし。
2. 属性でパブリックプロパティを設定する
売り上げランキング: 190,062
要素の属性はそのクラスオブジェクトのパブリックプロパティへの代入です。 デフォルトコンストラクタで生成されたのち、プロパティが設定されます。
属性値として直接記述できるのは、文字列とか数値といったプリミティブな型に制限されると思います。 バインディングの解決は、少し複雑なことがなされているのかもしれませんね。
3. プロパティへクラスオブジェクトを代入する
ところで、XAMLでデータコンテキストを生成しているところ(以下)。
<Window x:Class="StudyDotNet.MainWindow" ~中略~ > <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> ・ ・ ・ </Window>
この Window.DataContext要素のように、親エレメント.プロパティ名
という要素は、親要素のインスタンスプロパティへの代入なんですね。
つまりここでは、MainWindow
クラスの基本クラスであるWindow
のDataContext
プロパティへ、MainWindowViewModel
のインスタンスを設定(Property Set)しているということです。
4. 静的クラスのプロパティへオブジェクトを代入する
以下のInteraction.Triggers
も似ていますが、ドットの前がInteractionとなっていて、親要素ではありません。
これは、System.Windows.Interactivity.Interaction
というスタティッククラスで、そのTriggers
というプロパティにMessageBoxTrigger
を追加しています。
<Window x:Class="StudyDotNet.MainWindow" ~中略~ > ・ ・ ・ <i:Interaction.Triggers> <tr:MessageBoxTrigger> <tr:MessageBoxAction /> </tr:MessageBoxTrigger> </i:Interaction.Triggers> ・ ・ ・ </Window>
ところが Interaction
クラスにそのものスバリのTriggers
というプロパティはありません。
かわりにTriggerCollection GetTriggers(DependencyObject obj)
というメソッドがありますので、XAMLのパーサーがうまくやってくれているのだと思います(詳細不明)。
以下のように、コードビハインドからthis
を与えて返されるコレクションに、ちゃんとMessageBoxTrigger
のインスタンスが入っていました。
using System; using System.Windows; using System.Windows.Interactivity; namespace StudyDotNet { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); var triggers = Interaction.GetTriggers(this); foreach(var trigger in triggers) { Console.WriteLine(trigger.GetType().Name); } } } }
ちなみにInitializeComponent
の前では何も表示されなかったので、XAMLはInitializeComponent
で処理されているのでしょう。
5. そもそも子要素は何処に格納されるのか
XAMLのパーサーは、子要素のインスタンスを親要素の何処に格納するの?という疑問。 コレクションであることは間違いなさそうなのですが。
XAMLの以下の部分などですね。
<i:Interaction.Triggers> <tr:MessageBoxTrigger> <tr:MessageBoxAction /> </tr:MessageBoxTrigger> </i:Interaction.Triggers>
「XAMLのパーサーが、MessageBoxTriggerのインスタンスに、MessageBoxActionのインスタンスを保持させている」ということは明らか。
実際にアクションはイベントトリガーのActionsというコレクションプロパティに保持されており、 このプロパティは、MessageBoxTriggerの基本クラスであるTriggerBaseに実装されています。 でも、XAMLだけを見る限りは、そんな事実はわからない。
ということで、System.Windows.Interactivity.TriggerBaseの説明を見てみると、以下のようになっています。
[ContentPropertyAttribute("Actions")] public abstract class TriggerBase : DependencyObject, IAttachedObject
なるほど、ContentPropertyAttributeで「XMLのコンテントはプロパティ名 Actions に」と読めますね。 XAMLのパーサーはこの宣言に従って子要素を親要素に格納している。
ということで、Interaction.Triggersの中身を、以下のように冗長に書き換えても同じように動作します。
<i:Interaction.Triggers> <tr:MessageBoxTrigger> <tr:MessageBoxTrigger.Actions> <tr:MessageBoxAction /> </tr:MessageBoxTrigger.Actions> </tr:MessageBoxTrigger> </i:Interaction.Triggers>
まとめと所感
このように、曖昧に済ませていたことを深掘りしてみて、かなりスッキリ気分爽快。 XAMLで何でもできる気がしてきました。
基本を押さえるのはホントに大切。 知識やスキルを習得するときの効率性に直結しますね。
いくつになってもお勉強です。
MVVM的に真っ当にMessageBoxを表示する
photo credit: waterj2 DSCF0629 via photopin (license)
MVVM的に正しくモーダルなメッセージボックスを表示するサンプルコードを示します。
WPFでのMVVMパターンとしては、ビュー以外からメッセージボックスを直接表示するのは良くないらしいです。
メッセージボックスで親ウィンドウを指定しないとモーダルにならないのですが「ビュー以外からメインウィンドウを逆方向へ参照するのは悪手」ということです。
じゃあ、どうすればよいかっていうと「メッセンジャーパターン」を使いましょうとのことでした。
「メッセンジャーパターン」て?
メッセンジャーパターンは「VMからViewを操作する方法」のことらしいです。 以下のようなカラクリだと理解しました。
- 「メッセンジャー」というオブジェクトからイベントを発行して、
- あらかじめビューに実装されたイベントトリガーが、このイベントを受けて、
- イベントトリガーが保有するトリガーアクションが処理をする(イベントの引数も渡される)。
上に書いた点を自分なりに整理して、ややこしいことを抜きで System.Windows.MessageBox と同じように使えるコードをご紹介します。 GitHub Gistにも置いていますのでご自由にご利用ください。
コードの概要
ここで、
- メッセンジャーは単なるクラス。
- イベントトリガーは、System.Windows.Interactibity.EventTriggerの派生クラス。
- トリガーアクションは、System.Windows.Interactibity.TriggerAction
の派生クラス。
※ System.Windows.Interactibityは、プロジェクトから参照されていないかもしれません。 参照マネージャの左のツリーから[アセンブリ]/[拡張]を開いてチェックを付ければOKです。
1.メッセンジャーとイベントトリガー(これが本体)
内部クラスを含めて3つのクラスを定義しています。System.Windows.MessageBoxと同じように使えるようにしたら長くなりました。
- MessageBox - メッセンジャー。スタティックなShowメソッドでシングルトンのインスタンスからイベントを発行。
- MessageBox.Action トリガーアクションの抽象基底クラス。実際にメッセージを表示するクラスの基本クラスです。
- MessageBoxTrigger - イベントトリガークラス。MessageBoxからのイベントを受け取って、トリガーアクションを実行します。
MessageBoxTrigger.cs
using System; using System.Linq; using System.Windows; using System.Windows.Interactivity; namespace StudyDotNet.Triggers { /// <summary> /// MVVM的メッセージボックスを表示するためのメッセンジャー。 /// /// MessageBox.Showメソッドで、イベントトリガーを起動する。 /// /// 実際の表示は、イベントトリガーから実行される /// トリガーアクションに実装される。 /// </summary> public class MessageBox { /// <summary> /// MessageBoxTriggerを起動するイベント。 /// </summary> public event EventHandler<EventArgs> ShowMessageBox; /// <summary> /// MessageBoxTriggerを起動するイベントの名前。 /// </summary> public static string EventName { get { return "ShowMessageBox"; } } /// <summary> /// このクラスはシングルトン。 /// </summary> public static MessageBox Instance { get; private set; } = new MessageBox(); private MessageBox() { } /// <summary> /// MessageBoxTriggerを起動するイベントの引数。 /// トリガーアクションへ渡されて処理される。 /// </summary> public class EventArgs : System.EventArgs { public string Text { get; set; } public string Title { get; set; } public MessageBoxButton Button { get; set; } public MessageBoxImage Icon { get; set; } /// <summary> /// メッセージボックスの結果を受け取るコールバック /// </summary> public Action<MessageBoxResult> NotifyResult { get; set; } } /// <summary> /// MVVM的メッセージボックスを表示。 /// 実際にはイベントを発行してイベントトリガーを起動する。 /// </summary> /// <param name="messageBoxText"></param> /// <param name="title"></param> /// <param name="button"></param> /// <param name="icon"></param> /// <returns></returns> public static MessageBoxResult Show( string messageBoxText, string title = null, MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.Information) { //メッセージボックスの結果 MessageBoxResult messageBoxResult = MessageBoxResult.Cancel; //イベントを発行する Instance.ShowMessageBox?.Invoke( Instance, new MessageBox.EventArgs() { Text = messageBoxText, Title = title, Button = button, Icon = icon, //コールバックで結果を受け取る NotifyResult = result => { messageBoxResult = result; } }); //メッセージボックスの結果を返す return messageBoxResult; } /// <summary> /// メッセージを表示するトリガーアクション実装用の抽象基底クラス。 /// /// 派生クラスでShowMessageを実装する。 /// </summary> public abstract class Action : TriggerAction<DependencyObject> { /// <summary> /// アクションの実態 /// </summary> /// <param name="parameter"></param> protected override void Invoke(object parameter) { //イベント引数の種別を検査 var messageBoxArgs = parameter as MessageBox.EventArgs; if(messageBoxArgs == null) { return; } //メッセージボックスの表示結果を取得 MessageBoxResult result = ShowMessage( messageBoxArgs.Text, messageBoxArgs.Title, messageBoxArgs.Button, messageBoxArgs.Icon); //コールバックで結果を通知 messageBoxArgs.NotifyResult?.Invoke(result); } /// <summary> /// メッセージボックスを表示する抽象メソッド。 /// </summary> /// <param name="text"></param> /// <param name="title"></param> /// <param name="button"></param> /// <param name="icon"></param> /// <returns></returns> abstract protected MessageBoxResult ShowMessage( string text, string title, MessageBoxButton button, MessageBoxImage icon); } } /// <summary> /// MVVM的メッセージボックスを表示するためのイベントトリガー。 /// /// MessageBox メッセンジャーの発行するイベントで起動される。 /// </summary> public class MessageBoxTrigger : System.Windows.Interactivity.EventTrigger { public MessageBoxTrigger() : base(MessageBox.EventName) { SourceObject = MessageBox.Instance; } } }
2.トリガーアクションの実装例
MesssageBoxのトリガーから実行されるトリガーアクションクラスの実装例です。
MessageBox.Actionから派生した、MessageBoxActionクラスを実装しています。 メッセージボックスを表示して、その結果を返すクラスです。
このクラスから使っている MessageBox クラスは、System.Windows のものということに注意してください。
MessageBoxAction.cs
using System.Windows; namespace StudyDotNet.Triggers { /// <summary> /// メッセージボックスを表示するトリガーアクション /// </summary> public class MessageBoxAction : MessageBox.Action { protected override MessageBoxResult ShowMessage( string text, string title, MessageBoxButton button, MessageBoxImage icon) { var owner = Application.Current.Windows .OfType<Window>().SingleOrDefault( x => x.IsActive); if(owner == null) { owner = Window.GetWindow(AssociatedObject); } return System.Windows.MessageBox.Show( owner, text, (title != null ? title : Application.Current.MainWindow.Title), button, icon); } } }
追記(2017-03-29): メッセージボックスのオーナーウィンドウに、アクティブウィンドウを指定するようにしました。メッセージボックスが出ている場合はnullになりうるのでその場合は従来通りのウィンドウとしています。(c# - Refer to active Window in WPF? - Stack Overflow)
3.メインウィンドウ(MainWindow.xaml)の記述例
上のメッセージトリガーとトリガーアクションクラスは以下のように、Xamlに Interaction.Trigger 要素を追加します。
XMLの名前空間、xmlns:i
と xmlns:tr
の宣言にも注意。i
は、System.Windows.Interactibityを使用するためで、
tr
は、上記のトリガー/アクションを使用するためのものです。
<Window x:Class="StudyDotNet.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" xmlns:local="clr-namespace:StudyDotNet" xmlns:vm="clr-namespace:StudyDotNet.ViewModels" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:tr="clr-namespace:StudyDotNet.Triggers" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> <!-- ここから --> <i:Interaction.Triggers> <tr:MessageBoxTrigger> <tr:MessageBoxAction /> </tr:MessageBoxTrigger> </i:Interaction.Triggers> <!-- ここまで --> <Grid> <Button x:Name="button" Content="Button" Command="{Binding SampleCommand}" HorizontalAlignment="Left" Margin="50,50,0,0" VerticalAlignment="Top" Width="75"/> </Grid> </Window>
4.メッセージボックスを表示する
使う側からは単純に、MessageBox.Showを呼び出すだけです。 System.Windows.MessageBoxとゴッチャにならないように注意は必要。
using System; using System.Windows.Input; using StudyDotNet.Triggers; namespace StudyDotNet.Commands { public class SampleCommand : ICommand { public event EventHandler CanExecuteChanged; public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { MessageBox.Show("MVVMバンザイ!"); } } }
いくつになってもお勉強
これによって、より少ない変更で、従来のメッセージボックス使用部分をメッセンジャーパターンに移行できます。
ユニットテストは、固定の応答を返すスタブで対応。
また、別の表示方法に切り替えるのも容易です。
例えば独自のウィンドウで表示したり、 ポップアップせずメインウィンドウ内にメッセージを配置したり、 履歴を参照する機能を追加したりと、広がりがあります。
ということで、やはりビュー以外からは、表示内容やUIに直接の関わりを持たないほうが良いのでしょう。
いくつになってもお勉強です。