第六回まどべんよっかいち勉強会 LTスライド

第六回まどべんよっかいち勉強会お疲れ様でした。
「DotNetOpenAuthをうまいこと使う」というタイトルで、LTさせて頂きました。スライド置いておきます。

ASP.NET MVC4の新規プロジェクトで使用される「DotNetOpenAuth」ライブラリと、「Facebook C# SDK」を連携させるネタです。オリジナルはUsing OAuth Providers with MVC 4 By Tom FitzMackenですが、ポイントはほぼ一か所なので、LTネタとしては良かったと思います。

良くないのは、LTとして長すぎだったと言う事か orz すいません、時間が押していたのに。精進します。

オリジナルスライドはこちら(アニメーションを多用しているので、オリジナルを見た方が良いです)

なお、時間が無かったので補足しませんでしたが、通常のウェブサイト向け(ASP.NETウェブサイト)のOAuth運用では、アクセストークンを直接取得するのではなく、代わりに「コード」と呼ばれる、ログイン毎に一意の暗号コードを受け取り、暗号コードからアクセストークンを取得します。

The Login Flow for Web (without JavaScript SDK)

そのため、LTで紹介した方法(直接アクセストークンを取得する)は、最後にWindows Phone向けの事を絡めましたが、元々Windows Phoneのようなクライアントアプリケーション向けの処理方法です。

… と言う事を喋る余裕もありませんでしたw

COMのアパートメント (6) アパートメントの種類はどのように決まるのか

ずいぶんご無沙汰になってしまった。未だにCoInitializeExとかRPC_E_CHANGED_MODEで検索してくる方が絶えないので、少しだけ続きを書く。

前回、アパートメントの種類は、ライブラリの呼び出し元(正確にはスレッドを生成したコードの設計者)が知っているはずと述べた。だが、現実には、STAとMTAのどちらを使用すべきか、はっきり分かっていない開発者が多いと思う。
また、STAは重く、MTAにすれば軽いと、「まことしやかに」ささやかれていたりもする。もっと印象的なのは、「マルチスレッド」だからというものだ。

CoInitializeExでSTAかMTAを指定する事の他に、スレッドにアパートメントを指定する「意味づけ」がある。以下にこれらをまとめる。

  • STA – シングルスレッドアパートメント
    あるスレッドがCoInitializeExでSTAに設定した場合、そのスレッドはウインドウのメッセージループを持つと仮定する。つまり、そのスレッドが実行するコードの中核には、GetMessage・TranslateMessage・DispatchMessageによるメッセージループが存在する(しなければならない)。
    これは、極々一般的なWin32アプリケーションのメインスレッドの要件となる。同時に、例えワーカースレッドであっても、その中核がメッセージループで構成されているならば、STAとして設定する必要がある。
    もし、無理やりMTAとする場合、非常に難しい問題を自力で回避しなければならず(ウインドウメッセージとインターフェイス呼び出しの手動マーシャリング)、それが不可能な場合もある。通常はそのような事をする意味はない。
  • MTA – マルチスレッドアパートメント
    あるスレッドがCoInitializeExでMTAに設定した場合、そのスレッドにSTAのようなメッセージループの要件は不要となる。しかし、これは同時に、ウインドウメッセージとの連携は「全くない」と仮定する事と同じである。
    自分がウインドウメッセージと関連が無い(それどころか、ウインドウと関連が無い)と決めてかかるのは危険だ。そのスレッドが、あらゆるユーザーインターフェイスを、「直接的」ないし「間接的」に操作していないと確信できるなら、MTAを使用する事が出来る。これは、特に自分が書いていないサードパーティのコードを流用する場合は難しい。
  • メインSTA
    これはプロセスのメインスレッドがSTAに設定されている場合に、特にその状態を言う。コードの中には、メインスレッドでのみ操作可能な、特別な位置づけのAPIや変数などが存在する場合がある(つまり、例えSTAでも、ワーカースレッドからは操作してはならない、など)。これらを「メインSTAに紐づけられている」、と擬似的に考える。

スレッドにSTAを設定すると、スレッド毎に独立したSTAに所属する。つまり、スレッド=STAとなる。複数のスレッド(メインスレッドやワーカースレッド)が、それぞれCoInitializeExでSTAに設定されると、STAの部屋がそのスレッドの個数用意され、それぞれのスレッドがそれぞれのSTA部屋に入ることになる。

これに対して、スレッドをMTAに設定すると、プロセス当たりたった一つのMTA部屋に、MTA設定されたスレッドが同居する。STAはn個存在する可能性があるが、MTAは常に一つとなる。そして、STAのうちメインスレッドが入ったSTAにのみ、メインSTAという名前がついていると考えればよい。

で、これが何の役に立つのかと言う事だが、スレッド同士が会話したい場合(あるスレッドが、別のスレッドにあるCOMのインスタンスのメソッドを呼び出したりする)、この擬似的な「部屋」によって挙動が変わることになる。

Apartments

例えば、STAスレッド同士が会話する場合、相手のスレッドは別のSTA部屋に存在する(一つのスレッドには一つのSTA部屋が割り当てられるから、必ず別のSTA部屋になる)。すると、会話(メソッド呼び出し)は、ファサードとなるインターフェイスを経由しなければならない。COMの抽象化された世界では、メソッド呼び出しが直接成立しているように見えるが、実際には部屋が遠いので、秘書が電話でやりとりしているようなイメージだ。

物理的にはどうだろうか。それぞれのスレッドは両方ともウインドウメッセージのループを持っている。また、ウインドウメッセージの動作を邪魔すると困るので、お互いのスレッドに関与する場合はPostThreadMessageを使って、メッセージキューに内容を放り込むのが良いだろう(秘書に伝言する)。
時期的に都合がよくなる(ウインドウメッセージに同期)と、そのメッセージが取り出されて(秘書が社長に伝達)、「送り先のスレッド」で処理が実行される。返信も同様だ。元のスレッドのメッセージキューに結果が放り込まれ、元のスレッドの都合が良い時にとり出され、「送り元のスレッド」で処理が継続する。

この挙動が、一般的にWin32でワーカースレッドを生成して、時間のかかる処理をウインドウ処理から外だしした時と殆ど同じに見えるだろうか? 俯瞰して眺めるなら、STAとして設定することで、面倒なスレッド間メッセージングを隠ぺいして、RPCライクに呼び出し出来るようになると言う事だ。

だから、STAは「重い」のだ。

しかし、STAが重いからという理由だけでは、MTAに設定することは難しい。MTA同士の呼び出しでは、秘書も電話も存在しない。MTA部屋は一つしかない。その部屋の中で、社長が隣り合う席で直接話し合うようなイメージだ。ネイティブコードにおけるstd_callによって、直接メソッドが呼び出される。通常のプログラミングにおける「メソッド呼び出し」と完全に等価だ。それはすなわち、ウインドウメッセージやウインドウとの同期を一切行なわないと言う事だ。

仮にメッセージループを持つスレッドが、別のスレッドで生成されたCOMインスタンスのメソッドを直接呼び出した場合、メソッドは呼び出し元のスレッドで実行される。その結果、インスタンスが保持するウインドウハンドル(当然、別のスレッドに紐づいている)を、呼び出し元のスレッドが直接使用してしまうかもしれない。動作は予測不可能なものになる。

また、COMコンポーネントには、アパートメント属性の指定が無いものがある。このコンポーネントは、STAで生成される事を想定しているため、コンポーネント内でスレッド競合を特に考慮していない。MTAで生成すると、COMのインフラによって自動的にファサード(秘書通話に相当)が生成され、スレッド競合が回避される。

そのため、仮にだが、アパートメントを無視したような呼び出しを実行した場合(アパートメントを適当に設定すれば、結果的にそうなる可能性がある)、つまり、STAで生成されたこのようなインスタンスが、別のSTA/MTAからファサードの介在なく直接呼び出されたり、このインスタンスがコールバックすると、ウインドウメッセージの同期破綻はもちろん、ウインドウAPIの無効な呼び出し、スレッド競合による変数の破壊などの深刻な問題が発生する。

#以上の説明には、アパートメント設定の問題の他にも、プログラミング上の問題が含まれているが、そちらについては
#長くなるので、別の機会に。

この事を理解すれば、意味も分からずアパートメントを適当に設定するというのが、いかに危険か分かると思う。背景の理解は大変だが、判断の基準はシンプルで難しくない。

  • スレッドで行われる操作が、ウインドウやウインドウメッセージに及ぶ可能性がある場合(特にメッセージループを内包する場合)は、STAとして設定する。
  • それ以外であればMTAとすればパフォーマンスが向上する可能性があるが、心配ならSTAとして設定してもよい。
  • スレッドを生成したら、即アパートメントを設定する事。
  • COMインスタンスを操作する可能性が(直接的にも、間接的にも)完全にゼロである場合は、アパートメントを設定しなくて良い(CoInitializeExを呼び出す必要はない)。

.NETでコードを書いているのであれば、想像してほしい。Windows FormsならControl.Invoke、WPFならDispatcherを使いたくなる状況かどうかと同じ判断を、アパートメントに対しても行えば良い。これは、結局前回最後に書いた通り、これらのフレームワークのウィザードが生成するコードが「STAThread」とマークされ、メソッド呼び出しにマーシャリングを必要としていることに符丁するわけだ。

パフォーマンスが気になるというのなら、何が問題でSTAは遅いのか、と言う事を考えると答えが得られる。つまり、メソッド呼び出し(メッセージループにメッセージをポストしてやり取りする)にコストがかかることが問題なのであり、頻繁な呼び出しを避ければ良いのだ。プロパティを何度も参照したり、細かいメソッドを何度も呼び出したりすることを避けることで、一般的な使い方であればそうそう困ることはない。

COMコンポーネント側を設計するのであれば、頻繁なメソッド呼び出しを避けることができるように、拡張されたインターフェイスを設計しておくとよい。例えば、すべてのプロパティの値をまとめて取得・設定出来るようなメソッドや、バルク実行できるメソッド等が考えられる。

そして、COMインスタンスの操作を全く行わないのであれば、CoInitializeExを呼び出す必要はない。アパートメントの設定によって、STAならばウインドウメッセージキューが生成されてしまうし、MTAなら図中のスタックビルダーやインフラの準備でスレッドローカルストレージを消費するだろう。

例えば、素のWin32アプリケーションを作る場合、通常はCoInitializeExを呼び出したりはしない。しかし、ウインドウがActiveXコントロールを内包したり、WMI COMインターフェイスを使ったりするなど、直接的・間接的にCOMインスタンスを操作する可能性が生じれば、CoInitializeExでアパートメントを設定しなければならない。しかも、スレッドのエントリポイント(メインSTAなら、WinMain)の出来るだけ早い段階で、だ。

そして、ここが難しいところだが、アパートメントを設定しないのであれば、「間接的にも使われていない」と言う事を確認しておくことが重要だ(だから、心配ならSTAに設定する事をお勧めする)。

.NETのワーカースレッドで、スレッドのアパートメントを殆ど意識しないのは、COMインスタンスを.NETで操作する機会が(一般的には)あまり無いからだ。必要なければ、アパートメントを設定する必要もない。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

猫でもわかるExpression Design

Windows Phoneアプリのアイコンを作るのに、VS2012ImageLibraryをいじって手を抜こうとしていたのだが、余計に面倒なことになってきたので、久しぶりにExpression Designを触った。某人からも「手ごろなお絵かきツールない?」と言われていたので、ここは一つチュートリアルを作らねばならないなと。


名称未設定1で、今日のお題はコレ→

分かってしまえば、「3分」で作れます。特にBlendを良く触る人は、「2分」で行けます。

用意するもの:
Microsoft Expression Design 4

3でも2でもあまり違いはありません。1は不明。Designはプロダクトから外れてしまったのですが、幸いなことに正式版が無償で公開されています!!

Microsoft Expression Design 4 (English)

このページは英語版ですが、下のほうに日本語版へのリンクがあるので、そこからダウンロードして下さい。すばらしい!


WS000000では、Designを起動します。

Designのファイルメニューの新規作成で、ドキュメントを作成します。

WS000001この新規作成ダイアログは、ドキュメントのサイズを指定できるようになっています。上の例では1024px×1024pxとしましたが、Designは「ベクター」ベースです。つまり、このサイズはあくまで指標であって、出力時にはスケーラブルに変更する事が出来ます。つまり、Designはペイント系ではなく、Illustratorのようなドロー系です。

ちなみに、Blend同様、数値入力ボックスの上でマウスをドラッグすると、手打ちしなくても値を上下出来ます。クリックすれば手打ちできます。これ良く考えてあるよね。但し、マウスボタンの調子が悪いと発狂するので、新調しましょう。


WS000002とりあえず、何も描かれていない状態です。

分かりにくいのですが、左下に「44%」と表示されています。これはキャンバスの拡大率です。もし、四角形がウインドウに収まっていないのであれば、ここを変更して丁度収まるようにして下さい。
(数値入力ボックスはドラッグできるんですよー?覚えてますか?)

WS000003まず、画角一杯の四角形を描きます。左のツールバーから、四角いアイコンをクリックし、出てきたサブメニューから「四角形」を選択します。

WS000004その後、キャンバスの左上隅から、右下隅までドラッグします。

キャンバスと同じ大きさなので分かりにくいですが、赤線の四角形が出来ました(赤線はこの図形が選択されていることを示します)。

WS000005やってみればわかりますが、キャンバスの四隅にはマウスポインタが「スナッピング」されるので、四隅の選択は非常に簡単です。ぎりぎりを狙って四苦八苦しなくても済みます。

WS000006ここで、右側のプロパティの「外観」に表示されている、色パレット(なんか綺麗にグラデーションしている面)で適当な色をクリックして選んでみて下さい。現在選択されているオブジェクト(つまり、今描いた四角形)の色が変わります。

WS000007もう色々触ってみたくなったカモ?w まぁ、ちょっと我慢して続きを。今回作るアイコンの背景はグラデーションさせたいので、実際にやってみます。ちょっと小さいのですが、基本パレット内のグラデーションパレット(左右に白黒グラデーションしているパレット)を選択します。

WS000008すると、色パレットの下にグラデーションバーが表示されます。ここも見た目と裏腹に非常に高機能なのですが、今回は簡単に2色のグラデーションをちゃっちゃと設定します。グラデーションバーの左側の下に、小さい四角のセレクタがあります。例では黒色ですね。これをクリックします。

WS000009すると、色パレットは、グラデーションバーのクリックした個所の色を指定出来るようになります。そこで、青っぽい色を選択します。この例のように、青が選択できない、青じゃない色しか出ていない場合は、すぐ右側にある虹色の縦のバーから青色付近をクリックすれば、選択できるようになります。

WS000010グラデーションバーの色の変化と同じように、四角形のグラデーションも変化したと思います。今度は右側の四角のセレクタ(白色)をクリックして、同様にちょっと濃さの違う青色を選択します。

WS000012背景完成!と行きたいところですが、グラデーションの方向が違いますね。色パレット右下の、トランスフォームアイコン(形が良く分からないので、スクリーンショットを参考に)をクリックし、回転角度をいじってください。

ここでは90°にしてみました。
(数値入力ボックスはドラッグできるんですよー!?覚えてますか!?)


WS000013これで、当初のもくろみ通りの背景が完成。次はルーペを作ります。ルーペの肝は何といってもレンズの部分でしょう。中抜きの円をどうやって描くかです。心配無用、超簡単。

ツールバーから、楕円を選択します。

そして、円を描くのですが、まずは中抜きの円の外形の大きさにします。四角形と同じようにドラッグで描けますが、そのままでは文字通り「楕円」になってしまいます。ここでは真円にしたいので、シフトキーを押しながらドラッグして下さい。XY軸が1:1の真円として描画されます。

もし、円までグラデーションになってしまったら、円を描いた時点で(つまり円が選択されている状態で)、右側の色パレットの基本パレットから白色を選択します。

WS000014そして、もう一つ円を描いてしまいましょう。コピペ(Ctrl-C、Ctrl-V)でも良いですし、新たに円を描いても良いです。

円の四隅に小さい四角があります。名称が良く分かりませんが、一般的には「アンカー」でしょう。このアンカーをドラッグして、中抜き円の内径に近づけます(シフトキーと併用ですよ!1:1になります)。

図形などのオブジェクトを操作する(移動や変形など)場合は、左側ツールバーの一番上の矢印アイコンを選択しておくと、やりやすくなります。

WS000015さらに円自体をドラッグして、最初の円と同心円付近に持ってきます。

WS000030ありがちなのは、外形が大きい円の「下」に小さい円がもぐりこんでしまって操作出来ないと言う場合です。WordとかExcelで図形を描画した事があるなら分かると思いますが、同じように修正できます。

円を右クリックして「整列」「背面移動」で、図形の順序を変更して下さい。

WS000016さて、準備が出来たら、大小2つの円を同時に選択します。シフトキーを押しながら、二つの図形をクリックします。両方とも赤線で囲われるはずです。

WS000017そして、「オブジェクト」メニューの「パス演算」、「背面マイナス前面」を選択します。

WS000031何が起こったか分かりますか?WPFで言うなら、GeometryCombineMode.Excludeですよー? 内径の大きさとして準備した図形で「引き算」を行ったわけです。これでルーペは出来たも同然。

ちなみに、このパス演算を使用すれば、三日月とか、どこかにありそうなマークとか、簡単に作れますね!
今回の場合、背景がグラデーションではなく単色であれば、内側の円の色を背景と同じにするだけでも行けますが、パス演算は強力なので、紹介しました。


WS000018後はルーペの柄の部分です。もう大体の操作方法は分かったと思いますが、一応やっておきます。まず、四角形ツールで長方形を描きます。

WS000033次にこの長方形を斜めに傾けます。長方形が選択されている状態で、下段中央付近に「回転角度」という数値入力ボックスがあります。これが正に選択しているオブジェクトの角度を表します。

例では(ドラッグして設定したので)45.1°になっていますが、手打ちすれば45°になりますよ。もうそろそろOK?

WS000021で、長方形の幅とか端処理をいじって、それらしくします。四角形の四隅を丸くするには、右のプロパティから「四角形の編集」「角の半径」という数値入力ボックスをいじります。ドラッグでぐわっと変えて、見ながら調節すれば良いでしょう。

WS000022いくぞー合体ー(ry

WS000023おぉ、完成だ! ….って、なんか変?

良く見ると、柄の長方形に縁取りが。

これは、オブジェクトの「ストローク」というやつです。図形を描くと、「ストローク(縁取り)」と「内面の塗りつぶし」の2つの属性が必要になります。今まで描いてきた四角形と円は、どちらも内面の塗りつぶし色だけを指定したため、縁取りがデフォルトのままになっているのです。

対処としては2通り考えられます。縁取りを塗りつぶしと同色にするか、縁取りを消すか、です。単純色なら同色でも問題ありませんが、今回のようにグラデーションを適用したり、テクスチャハッチングとかすると、模様が合わなくなって目立ってしまう可能性もあります。まぁ、そんな事に悩む頃には、どうすれば良いかも分かるでしょう。今は深入りせず、縁取りを消してしまいましょう。

WS000034図形を選択して、色パレットのタブを「ストローク」に変えます。

WS000035そして、基本色パレットから「なし」をクリックして、ストロークを無しにします。

これでOK。色パレット下のコンボボックスに「ストロークなし」と表示されていますね? ここをいじるとストロークの色だけじゃなく、模様とかテクスチャとか。まぁ、後で遊んでみて下さい。

ちなみに、塗りつぶしの色を「なし」にも出来ますよ。ストロークにのみ色を指定すれば、塗りつぶさない図形とか、簡単ですね?


WS000027さあ、完成です (^o^)/~~

WPFやっている人なら、ここまでの時点で「こりゃまんまWPFじゃないか」と思うかもしれません。たぶんそうです。というか、このDesignをWPFで実装しない理由はないでしょう。

WS000028さて、最後にこれをPNGで出力します。ファイルメニューの「エクスポート」で、フォーマットをPNG、画像サイズを256×256にして、パスやファイル名を指定してエクスポート。最初に書いたように、Designはベクターベース(WPFなら当たり前)なので、指定したサイズで綺麗に拡大・縮小されますよ。

WS000029ちなみに、PNGにすればベクターデータはすべて失われてしまいます。ちゃんとDesignのフォーマットでも保存しておきましょう(拡張子はdesign)。エクスポートではなく、普通に名前を付けて保存でOKです。

そして、PNGなら当然、透明色も反映されますよ。半透明や複雑な中抜き形状の図形を、アルファチャネルありで出力できます。図形や文字のアンチエイリアスも反映されます。


DesignとPaint.NETがあれば、とりあえずお絵かきに困ることはないと思いますよ。

ToggleSwitchを分離ストレージに関連付ける

WPFのトグルスイッチは、主に設定に使用する。
これが沢山あると、

ToggleSwitch

XAMLもそうだが、トグルイベントのハンドラもそれに合わせて書かなければならない。設定項目が増えるほど、このハンドラコードをチマチマと追加しなければならず、単純ミスも多くなる。
そこで、これをビヘイビアで何とかしようという話。

アプリケーションが複雑ではない場合、設定情報を単純に分離ストレージに格納すると思う。IsolatedStorageSettings.ApplicationSettingsコレクションを使用すると、出し入れは簡単になる。
ToggleSwitchの状態は、「オン」と「オフ」だけなので、ApplicationSettingsには単純にブール値で格納すれば良い。
そこで:

public sealed class AppSettingsBehavior : Behavior<ToggleSwitch>
{
    public static DependencyProperty SettingNameProperty =
        DependencyProperty.Register(
            "SettingName",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata(null));

    public static DependencyProperty OnTextProperty =
        DependencyProperty.Register(
            "OnText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("On"));

    public static DependencyProperty OffTextProperty =
        DependencyProperty.Register(
            "OffText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("Off"));

    public AppSettingsBehavior()
    {
    }

    public string SettingName
    {
        get
        {
            return (string)this.GetValue(SettingNameProperty);
        }
        set
        {
            this.SetValue(SettingNameProperty, value);
        }
    }

    public string OnText
    {
        get
        {
            return (string)this.GetValue(OnTextProperty);
        }
        set
        {
            this.SetValue(OnTextProperty, value);
        }
    }

    public string OffText
    {
        get
        {
            return (string)this.GetValue(OffTextProperty);
        }
        set
        {
            this.SetValue(OffTextProperty, value);
        }
    }

    protected override void OnAttached()
    {
        bool value;
        if (IsolatedStorageSettings.ApplicationSettings.TryGetValue(this.SettingName, out value) == false)
        {
            this.AssociatedObject.IsChecked = false;
        }
        else
        {
            this.AssociatedObject.IsChecked = value;
        }

        UpdateContent();

        this.AssociatedObject.Checked += AssociatedObject_Checked;
        this.AssociatedObject.Unchecked += AssociatedObject_Unchecked;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.Checked -= AssociatedObject_Checked;
        this.AssociatedObject.Unchecked -= AssociatedObject_Unchecked;
    }

    private void UpdateContent()
    {
        this.AssociatedObject.Content =
            (this.AssociatedObject.IsChecked ?? false) ? this.OnText : this.OffText;
    }

    private void AssociatedObject_Checked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = true;
        UpdateContent();
    }

    private void AssociatedObject_Unchecked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = false;
        UpdateContent();
    }
}

というようなビヘイビアを作っておいて、

<ListBox Grid.Row="1" Margin="0,0,0,0" Padding="0,0,0,0"
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem"
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListBox.ItemContainerStyle>

    <toolkit:ToggleSwitch Header="正規表現を使用する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsRegexMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>

    <toolkit:ToggleSwitch Header="大小文字を区別する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsCaseSensitiveMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>
</ListBox>

と書くだけで、次々とToggleSwitch項目を簡単に増やすことができる。SettingNameには、分離ストレージに格納するキーの名前を、OnTextとOffTextはスイッチ左側に表示される状態を示すテキストを指定する。これらは依存関係プロパティにしてあるので、バインディングで多国語化することもできる。

Metro UIのボタンエフェクトをWindows Phoneに適用する

Windows Phoneで遊び始めている。
Windows Phone自体もそうだが、WPFも殆ど触ったことがないので、とても苦しいw
取り合えず、簡単なアプリを実際に公開してみて感触をつかみつつ、WPF理解への足掛かりにしたいなと思っている。

で、早速製作中なのだが、Metro UI(名称がぽしゃったので、何と呼べばいいのか困るなぁ)に使用するユーザーインターフェイスのボタン、

WP8MetroUI

のマウスカーソルのあたりをタップした時に、ボタンの隅が押されて変形したような挙動でフィードバックがある、アレをやりたいと思ったのだが、どうも簡単に出来ないようだ。
で、WPFの変形の基礎とか、コントロールのカスタマイズの方法など、色々調べて以下のコードを書いた。

(ここまで紆余曲折の末、約2日 orz 直前にGeometry/Path/RenderTargetBitmapだけいじっていたのが幸いした。でないと、座標がdoubleというだけでも悶絶していたかもしれない…)

public sealed class TiltBehavior : Behavior&lt;UIElement&gt;
{
    private PlaneProjection projection_;

<pre><code>public TiltBehavior()
{
    this.Depth = 30.0;
    this.Tracking = true;
}

public double Depth
{
    get;
    set;
}

public bool Tracking
{
    get;
    set;
}

protected override void OnAttached()
{
    this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    }
}

protected override void OnDetaching()
{
    this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    }
}

private static void Apply(Size size, Point point, PlaneProjection projection, double depth)
{
    // コントロールのサイズからノーマライズした割合を得る
    var normalizePoint = new Point(
        point.X / size.Width,
        point.Y / size.Height);

    // 0~1の範囲外を切り捨てる
    var satulatePoint = new Point(
        (normalizePoint.X &amp;gt; 1.0) ? 1.0 : ((normalizePoint.X &amp;lt; 0.0) ? 0.0 : normalizePoint.X),
        (normalizePoint.Y &amp;gt; 1.0) ? 1.0 : ((normalizePoint.Y &amp;lt; 0.0) ? 0.0 : normalizePoint.Y));

    // 中心位置からの割合を得る
    var originPoint = new Point(
        satulatePoint.X * 2.0 - 1.0,
        satulatePoint.Y * 2.0 - 1.0);

    // 絶対位置
    var absolutePoint = new Point(
        Math.Abs(originPoint.X),
        Math.Abs(originPoint.Y));

    // 中心からの位置関係
    var directionX = originPoint.X &amp;gt;= 0.0;
    var directionY = originPoint.Y &amp;gt;= 0.0;

    // タップされた位置に応じて、回転軸位置を固定する(0又は1)
    projection.CenterOfRotationX = directionX ? 0.0 : 1.0;
    projection.CenterOfRotationY = directionY ? 0.0 : 1.0;

    // 辺ではなく、中心をタップした場合にも、フィードバックを得る
    // (辺をタップした場合は0に近づく事で影響を避ける)
    var distance = (absolutePoint.X &amp;gt; absolutePoint.Y) ? absolutePoint.X : absolutePoint.Y;
        projection.GlobalOffsetZ =
        (1.0 - distance) *
        0.5 *       // 中心位置でのZ座標
        (-depth);

    // Rotationは角度なので、計算して算出
    projection.RotationY =
        Math.Atan2(depth * (0.0 - originPoint.X) * 0.5, size.Width) /       // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
    projection.RotationX =
        Math.Atan2(depth * originPoint.Y * 0.5, size.Height) /      // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
}

private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.AssociatedObject.Projection == null)
    {
        this.AssociatedObject.CaptureMouse();

        projection_ = new PlaneProjection();
        this.AssociatedObject.Projection = projection_;

        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
{
    if (projection_ != null)
    {
        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void Uncapture()
{
    if (object.ReferenceEquals(this.AssociatedObject.Projection, projection_) == true)
    {
        this.AssociatedObject.Projection = null;
        projection_ = null;

        this.AssociatedObject.ReleaseMouseCapture();
    }
}

private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Uncapture();
}

private void AssociatedObject_LostMouseCapture(object sender, MouseEventArgs e)
{
    Uncapture();
}
</code></pre>

}

何しろWPFは完全に初心者なので、変なことをやっていたり、思想から外れる設計なのかもしれないのであしからず。これはUIElementクラスに適用できるビヘイビアクラスで、プロジェクトに入れておいて、ページのXAMLで以下のような感じで使う。

&lt;ListBox x:Name=&quot;MainListBox&quot; Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot;&gt;
    &lt;ListBox.ItemTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;StackPanel Margin=&quot;8,0,8,8&quot;&gt;
                &lt;TextBlock Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Name}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;TextBlock Margin=&quot;8,8,0,8&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Description}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;i:Interaction.Behaviors&gt;   &lt;!-- ココ --&gt;
                    &lt;Behaviors:TiltBehavior /&gt;
                &lt;/i:Interaction.Behaviors&gt;
            &lt;/StackPanel&gt;
        &lt;/DataTemplate&gt;
    &lt;/ListBox.ItemTemplate&gt;
&lt;/ListBox&gt;

ListBoxにコレクションをバインディングし、その要素毎にStackPanelで表示する。そのStackPanelにビヘイビアの指定を行うと、対応するクラスのビヘイビアが呼び出される。名前空間「i」は、System.Windows.Interactivityで、これはWindows Phone以外ではアセンブリが違うかもしれないが存在すると思う。最初にxmlns:iで宣言しておくこと。

なお、Buttonに適用するとうまく動かない。何故かは、これから悩むところ (^^;; StackPanelのクリックを検出する方向で逃げたほうがいいのか、Buttonで真面目にやるほうがいいのか、それすら分からないのが困ったものだ…