2015/12/18

MemoryStreamのデータ

MemoryStreamといえばStream系の処理をするときのバッファー役ですが、これからのデータの取り出し方について。

1. 前置き

実際のコードを見た方が手っ取り早いので、随時Reference SourceのMemoryStreamのソースを参照していきます。

MemoryStreamはメモリ上の領域をバッキングストアとしたStreamで、そのためのバイト配列を内部に持っています。それがプライベートフィールドの_bufferです。その中の実際のデータの位置は_origin_lengthで示されます。つまり、_bufferの全部がデータだとは限らず、その一部に格納される構造になっています。

2. MemoryStream.ToArrayメソッド

MemoryStreamに何らかのデータを詰めたとして、それをStreamとして受け渡すのではなく、直接取り出したい場合、オーソドックスなのはMemoryStream.ToArrayメソッドです。

例えば、こんな感じ。
この例ではDataContractSerializerでオブジェクトをシリアライズしたデータをMemoryStreamに詰め、ToArrayメソッドで取り出したバイト配列をEncoding.GetStringメソッドに渡しています。

このToArrayメソッドのソースを見ると、データの長さ(_length - _origin)と同じ長さのバイト配列を用意し、これにBuffer.InternalBlockCopyメソッド(たぶんBuffer.BlockCopyと同じ)で_buffer中のデータをコピーした後、この配列を返しています。

これは安全な方法ですが、処理の最後にデータを取り出したいだけの場合にはコピーが無駄だったりします。

3. MemoryStream.GetBufferメソッド

これが何とかならないかなと思っていたとき、ヒントをいただいた気がしたのでよく調べたらMemoryStream.GetBufferメソッドが存在しました。このGetBufferメソッドのソースを見ると一目瞭然ですが、_bufferの参照をそのまま返しています。ただし、この中のデータの位置は自分で指定する必要があります。

例えば、開始位置を0に決め打ちすれば、こんな感じになります。
問題は開始位置、すなわち_originが0ではない場合で、その場合はズレが生じます。かつ、_originを外部から参照できるプロパティがありません。一応Lengthプロパティのソースを見ると、これは_length - _originの値なので、_originが参照できさえすればそのまま使えるのですが。

一応、この例のようにMemoryStreamのコンストラクターのうち元となるバイト配列のないオーバーロードを使ったときは、_originは0になるようなので、そこに注意すれば問題はなさそうですが、一抹の不安が残ります。

また、publiclyVisibleを指定できるコンストラクターでこれをfalseにした場合(_exposableがfalseの場合)、GetBufferメソッドは例外を吐きます。

4. MemoryStream.TryGetBufferメソッド

ではどうするかというところで、メソッド群を見直すとMemoryStream.TryGetBufferメソッドが存在しました。これはoutでArraySegment<byte>を返します。

このTryGetBufferメソッドのソースを見ると、ArraySegmentのコンストラクターのoffsetに_originの値を、countに_length - _originの値を指定しています。したがって、返されたArraySegmentのOffsetとCountプロパティでデータの位置を確定できるわけです。

これを使うと以下のようにできます。
これでコンストラクターに左右されず、安全にコピーなしでデータを取り出すことができるようになりました。

5. まとめ

MemoryStreamの振る舞いはどのコンストラクターでどのように指定するかで細かく変わってくるので、GetBufferで済むか、TryGetBufferで安全策を取るか、別にToArrayで構わないか、結局はケースバイケースなのですが、どんなオプションがあるか知っておくと少し安心できます。

2015/11/28

Windows 10 1511のバージョン判定

Windows 10のビルド10586というか、Threshold2というか、1151のバージョンをコード的にどう確認するかについて、以下の記事を参考にレジストリ情報を追加してみた。
レジストリを読んでいるだけだが、新しいReleaseIdに「1511」の値が出ている。BuildLabの方には「th2」の文字が入っている。その後ろにある「151112-1900」は2015/11/12 1900というリリース日時を示しているのかもしれない。

これを元に実用的な処理を組むにはあやふや過ぎるので、あくまで参考程度で。

2015/11/14

WPFのPer-Monitor DPIサポート(その1)

WPF自体のPer-Monitor DPIサポートについて。


従来、WPF自体にはPer-Monitor DPIのサポートはなく、Win32 APIを駆使する必要があったわけですが、.NET Framework 4.6.1の後のバージョンでようやくサポートが入るようです。
このエントリ中にあるサーベイに登録すればプレビュー版を試せるとのことで、登録しておいたところ、昨日招待メールが届いたのでダウンロードして試してみました。

ダウンロードできるのは.NET Framework 4.6.1のプレビュー版のようで、それを利用するサンプルコードは既にGitHubで公開されています。
このサンプルコードの範囲内で簡単にまとめると、以下が追加されています。
  • System.Windows名前空間
    • DpiScaleInfo構造体(DPI情報を格納する)

  • System.Windows.Media.VisualTreeHelperクラス
    • public static DpiScaleInfo GetDpi(Visual visual)メソッド
    • public static void SetRootDpi(Visual visual, DpiScaleInfo dpiInfo)メソッド

  • System.Windows.Windowクラス
    • protected override void OnDpiChanged(DpiScaleInfo oldDpiScaleInfo, DpiScaleInfo newDpiScaleInfo)メソッド

  • System.Windows.Controls.Imageクラス
    • protected override void OnDpiChanged(DpiScaleInfo oldDpiScaleInfo, DpiScaleInfo newDpiScaleInfo)メソッド
    • public event RoutedEventHandler DpiChangedイベントハンドラー
さらにコメントを見ると、大元のSystem.Windows.Media.VisualクラスにOnDpiChangedメソッドとDpiChangedイベントハンドラーが追加されるようです。

つまり、他の色々なイベント処理と同じように、各コントロールでDPI変化イベントを捉えて処理できるようになる、ということです。当然コントロールによって予め処理が作り込まれているものもあって、Windowクラスではモニターをまたがって移動したときに自動的にサイズ変更がされるようになっていました(位置はWM_DPICHANGED準拠)。さらに必要な処理があれば、OnDpiChangedをオーバーライドするなりDpiChangedに登録するなりして入れ込めばいいわけですね。

どんなものが来るのかなと思ってましたが、自然な拡張のようでとりあえず安心しました。

[追記1]

DpiChangedイベントハンドラーはRoutedEventHandlerとなっているとおり、引数はただのRoutedEventArgsなので、DPI情報を直接取得はできません。これは少しだけ面倒なので、以下のような専用のDpiChangedEventArgsを使うようにしてはどうかとフィードバックしてみました。

[追記2]

フィードバックへの返信で、このサポートが入るのは4.6.1ではなく、その後のバージョンだと訂正されたので、修正しました。

Managed Native Wifiライブラリ

WLAN Profile Viewerのモデルのコア部分を追加修正してライブラリとして公開しました。
名前のManagedとNativeが矛盾しているような感じもしますが、Native WifiというWin32 APIのマネージドラッパーという性格上、これが一番ストレートな気がしたので。

同種のものは既にManaged Wifiがありますが、これが大枠だけ提供するのに対して、このManaged Native Wifiはすぐ使えるメソッドを提供しています。非同期処理になるものについてはTAPでasync/awaitベースのメソッドも用意しています。

一応、ほぼ同じことはNetshでも可能ですが、反応がやはり遅いのと、処理の結果を非同期に受け取ることができないので(例えば、無線LANに接続するようリクエストはできても、接続できたか否かの結果は直接受け取れない)、使い勝手は劣ります。

1. 機能

現在の機能は以下のようなものです。

・利用可能なネットワーク、BSSネットワークの一覧取得。
・無線LANを(自動走査を待たずに)すぐ走査させる。
・無線プロファイルの一覧取得、追加、削除、順番の変更。
・無線LANへの接続、切断。

アプリのサブ機能でちょっと無線LANを利用したい、というぐらいのカジュアルな用途なら大体カバーできるんじゃないかと思います。というか、自分が昔ほしかった。

ただし、無線プロファイルの追加はプロファイルを構成するXMLを条件に合わせて自分で組み立てないといけないので、実は簡単ではないです。

2. 使用例

例として、現在利用可能な無線LANの中に、それに対応した無線プロファイルが存在していれば、そのうち最も信号状態のよい無線LANに接続するものです。

NativeWifiクラスのEnumerateAvailableNetworksメソッドは利用可能な無線LANの情報を収めたAvailableNetworkPackオブジェクトを列挙します。このProfileNameプロパティに、その無線LANに対応した無線プロファイルが存在していればそのプロファイル名が入っています。

まずこのProfileNameの有無でフィルターした後、同じくSignalQualityプロパティに信号状態が0-100の範囲で入っているので(大きいほどよい)、これの逆順で並び替え、最大のものを取得します。

これが取得できているかチェックした後、ConnectNetworkAsyncメソッドで非同期に接続を試みます。引数はAvailableNetworkPackオブジェクトから無線インターフェイスのID、プロファイル名、BSSの種別をそれぞれ渡します。最後はタイムアウト時間で、ここでは10秒を指定しています。この時間までに接続できればTrue、できなければFalseが戻り値として返ります。実際のところ、信号状態がよければ瞬時に繋がります。

こんな感じでさほど複雑ではないですが、WLAN Profile Viewerの中にもNativeWifiクラスの使用例があるので参考にしてください。

3. 課題
  • エラー処理: Native Wifiで起こり得るエラーについてMSDNでは網羅されてないので、実際どんなエラーが起きてどう処理すればいいのか、まだ煮詰まってません。

  • 取得する情報: Native Wifiで取得できる情報は多岐に渡るので、現在はその一部を拾い出している状態です。ここは使用シナリオに応じて検討の要あり。

  • Wi-Fi Direct: Windows 8以降、Native WifiにWi-Fi Direct関係の関数が追加されていますが、そもそもデスクトップアプリからのWi-Fi Directの使用方法がよく分かってないので、手つかずの状態。

2015/10/05

ThinkPad X230のパームレスト

ThinkPad X230のパームレストの互換性について。

1. 修理上がり

先日、X230をLCD関係の修理に出したら、キーボードとパームレストにも問題が見つかったとのことで、一緒に交換されて帰ってきたのは有り難いのだけど……。
ThinkPad X230: Palmrest

一見どこもおかしいところはないように見えて、自分でも「何か違う気がするけど、まあいいか」と思ってやり過ごしたのだけど、X230を持っている人なら気付くかもしれない。

そう、これパームレスト(キーボードから手前の筐体)がX220用のものになっている。X230のものはキーボードに向かって下がる部分が直線的に折れた平面になっているが、これはなだらかな曲面になっている。

そのくせこれだけ見ると特別おかしくは見えないのは、元々X230が機構的にはX220のキーボードを変更したマイナーチェンジモデルだから。細かく見れば、クリックボタンの横になる部分に、X220のクリックボタンにある曲線のラインにつながる微かな曲線が入っているが、ぱっと見には分からない。

で、そうと分かった後も、機能的には問題ないし、「これはこれで面白いから、このままでいいか」と思っていた。初めのうちは。

そのうちキーボードの右端手前と左端手前が妙にぺこぺこ上下に動くことに気付いて、キーボードを取り付け直したり、果ては両面テープで留めたりしたりして、これはどうもおかしいと思い始めて分解写真を見比べているうちに気付いた。X230とX220のパームレストには互換性があるように見えて、そうではないことを。

2. パームレストの違い

結局、本来のX230用のパームレストを送ってもらって自分で交換することにしたので、そのときに比較してみた。上がX220用で、下がX230用。
ThinkPad X230: Palmrest Comparison

注目すべきはキーボードとパームレストの境目になる部分にある穴で、(角の部分を除いて)左からキーボードの爪が嵌まる穴、キーボードにかかった液体の排水口、爪の穴、クリックボタン部分を挟んで、爪の穴、排水口、爪の穴がある(爪の穴は計4箇所)。この構成と位置は同じだが、よく見ると穴の大きさが違う。
Thinkpad X230: Palmrest Comparison

排水口はほぼ同じ大きさだが、爪の穴は上のX220用は縦幅が広いのに対して、下のX230用は縦幅が狭い。

このこと自体はX230が出た当初から言われていて、X230にX220以前のキーボードを装着しようとしたときの物理的障害になっている。
X220以前のキーボードは金属板の縁が上に立ち上がった浅いバスタブ状になっていて(排水口に面した部分だけ開いている)、爪部分も同じなので高さがある。これに対してX230のキーボードはバスタブを止めたので、金属板がそのまま真っ直ぐ爪になっている。
Thinkpad X230: Keyboard
ThinkPad X230: Keyboard

したがって、X230用のパームレストの爪の穴は縦幅の狭い穴でよく、これにX220以前のキーボードの爪を嵌めるには穴を上に削って大きくする必要がある(あえてその方法を選ぶなら)。逆に、X220用のパームレストの爪の穴にはX230のキーボードの爪はそのまま余裕で通る。

それ以上深く意識してなかったが、そこが実は落とし穴で、キーボードの固定ネジは中央部の2箇所しかないので、左右の端は爪だけで固定される形になっている。キーボードを取り付けるときは初めに奥に差し込んで奥側の金属板をキーボードベゼルに引っ掛けた後、手前に引いて手前側の爪をパームレストに嵌める構造になっている。このとき奥側に引っ掛けた部分でキーボードがわずかに反るテンションが掛かっているので、手前側の爪もぴったり嵌まっていれば強固に固定されるが、ここに遊びがあると手前側が浮き気味になって、タイピング時にぺこぺこ動いてしまう。

したがって、もしX230にX220のパームレストを流用する必要が出たときは、キーボードをきっちり固定するためには爪の穴を小さくする工作が必要、という話なのだが、この情報がいつか誰かの役に立つことがあるのかどうか。

3. おまけ

キーボードの爪がバスタブ状でなくなったことで液体が爪の穴から浸入しやすくなったのでは、と思うかもしれないが、そこは抜かりなかった。パームレストを裏から見ると、
Thinkpad X230: Palmrest Comparison

上のX220用では爪の穴がそのまま抜けているのに対して、下のX230用では爪の穴の奥がシールで閉囲されていて、液体がそのまま中に入らないようになっている。

とはいえ、この排水機構自体、とくに密閉されているわけでもなく隙間も多いので、気休め程度ではあるけど。

2015/09/24

ReadyNASのファン交換

ReadyNAS Ultra 2のファンを初めて掃除して以来、時々掃除してきたが、ついに掃除しても軸音がするようになったのでファン交換を決意した。

1. 失敗のファン

Ultra 2の購入時にファンがうるさかったときのために交換用ファンも買っていたが、元のファンが十分静かだったためにお蔵入りになっていた。この出番がついに来た、というわけで、それに交換すれば終わりの予定だった。

そのファンはオウルテック扱いの山洋電気製SF9-F1で、静音的には鉄板のものだったが……一応温度を見ているとCPU温度がUltra 2では未踏の80度を超え(RAIDar Protocolによる上限は80度)、冷却力不足が露呈した。さすがに回転数が800RPM固定では無理があったらしい。「こんなこともあろうかと……」が実は保険になってなかったという。

仕方ないので、9cmで回転数可変のファンということでENERMAXのPWMのUCTB9Pを買ってきたが、これが完全な外れ。たぶん不良個体だと思うが、軸音が元からひどくて話にならない。初ENERMAXだったのだが、不幸な出会いだった。

2. 元のファン

気を取り直して元のファンを改めてチェックすると、このDeltaのAFB0912HHは日本国内の一般の取り扱いはないらしい。ならばと大体同じ筐体の312のファンを見てみると、同じ型番のものだった。
ReadyNAS Ultra 2 and 312: Fans

左がUltra 2のもの、右が312のもので、型番は全く同じ。312本体のコネクタも当然Ultra 2と同じ3ピンのもの。
ReadyNAS 312: Fan Connector Pin

調べてみると4ベイのモデルも同じファンらしいので、ReadyNASの2ベイと4ベイのシリーズでずっと使っているものっぽい。312のファンもこれまでのところ良好なので、これが手に入れば話は早いのだが、サブタイプが複数あって該当するものが分からないし、送料もそれなりにかかる。

3. Noctuaのファン

他に探すとして、3ピンということは4ピンのPWMと違って電圧で回転数を制御していることになる。SF9-F1で回転数が固定だったのは電圧にかかわらず回転が一定になるようにできているのだと思う。そういう場合を除けば選択肢は広い。

一方、静音的には通常時800RPM程度がターゲットなので、ほとんどが定格で1000RPM台半ばのファンのスペックを見たところで参考にはならず、結局は勘になる。回転数の変わる範囲内で、軸音(直接の風切り音以外の音)が出る回転数があるかは実際に回してみないと分からないし。

ということで、一応回転数可変という点を考慮して、NoctuaのNF-B9 redux(非PWM)を購入。回転数変更用の抵抗など付属品を省いたモデルだが、不要なので。

左下が元のDelta、右下が山洋電気、左上がENERMAX、右上がNoctua。
ReadyNAS Ultra2: Replacement Fans

ファンの羽、コアの支柱ともバラエティに富んでいるが、通常時無音を目指すレベルの静音ではあまり関係なかったりする。

NF-B9の結果は当たりで、通常時は十分静かだし、回転数が上がっても気が付くような軸音は発しない。ファンを探す旅も3つ目で終わった。願わくば、次に交換が必要になったときにも市場に存在していることを。

2015/08/23

ReactivePropertyと無線LAN

ReactivePropertyと無線LANという組み合わせでアプリを作りました。

1. WLAN Profile Viewer

先に簡単にアプリの紹介を。無線LANプロファイルを管理するためのWindowsデスクトップアプリです。ストレートにWLAN Profile Viewerという名前にしました。

Windows 8以降、OS標準の無線LANのUIは現在電波の入っている無線LANを入口にしたものになりました。これは初めて接続するときだけ接続先の無線LANを選んで、後は自動的に接続するようOSにお任せにしてしまう前提であればシンプルでいいですが、ユーザーが自分でコントロールしようとすると必ずしも使い勝手のいいものではないです。今時コーヒーショップで無線LANを見ると20も30も電波が飛んでますし。

このアプリは既に作成された無線LANプロファイルを電波状態を含めて一覧表示し、そこから接続と切断ができます。これにプロファイルの順番の変更(ただしWindows 10では意味なし)と削除も併せて最低限一通りの管理ができるので、OS標準のUIよりは多少使い勝手がいいと思います。

正直いえばReactivePropertyの練習用に作っていたものですが、意外と実用性がありそうなのでアプリに仕立てました。詳しくはレポジトリで。
2. ReactivePropertyのプロパティ

ReactiveProperty自体については開発者の@neueccさん、@xin9leさん、@okazukiさんがたくさん書かれてますので、そちらを読んでいただければいいのですが、「そもそもReactivePropertyのプロパティって何?」という点について、「そこからか?」と言われそうですが、自分は何だろうと思ったので簡単に書きます。

これは最初に@neueccさんが書かれてます。正確にはそちらを。
ざっくりと自分のイメージでいえば以下のような感じです。
まずプロパティはデータを入れておく入れ物といっていいと思いますが、そのためには内部に値を保持していて、それに随時アクセスできる必要があります。一方、IObservableのチェーンではイベントなどで値が流れていくわけですが、常に値があるとは限らないので、そのままではデータの入れ物にはなりません。

そこで内部のlatestValueフィールドに値を常に保持するようにし、それにValueプロパティを通してアクセスできるようにしたのがReactiveProperty、のベースなのだと思います。したがって、プロパティを利用する立場からは、相手は一義的にはValueプロパティで、latestValueフィールドはそのバッキングストア、それらを提供するコンテナがReactivePropertyと見なすことができ、それをIObservableのチェーンに挟み込むことで(発端でも終端でも構いませんが)、両者をうまく融合させたと。

というわけで、ReactiveProperty自体はValueプロパティのコンテナなので、基本的にReadonlyなプロパティとして生成すればいいわけです。なお、ReadOnlyReactivePropertyはそのValueプロパティがReadonlyという意味で、Readonlyの対象が違うのですが、初めは混同してました。

3. ObserveElementXXXXX

ObserveElementXXXXXはコレクション要素のプロパティ変化をIObservableにする拡張メソッド群です。自分がコレクション要素のプロパティ変化をReactivePropertyで捉えるにはどうすればいいかと書いてたら、@xin9leさんと@okazukiさんがあれよあれよという間に作り上げて、すげーと思いました。

この使用例として、要素になるものとして以下のMemberViewModelがあるとします。この中のIsLongとIsSelectedはModelと同期、あるいはViewにバインドされて随時変わるものと考えてください。
これを要素とするObservableCollectionを持つMainWindowViewModelは以下のとおり。
この中でCLRプロパティであるIsLongにObserveElementPropertyを、ReactivePropertyであるIsSelectedにObserveElementObservablePropertyをそれぞれ繋げています。これらの戻り値の型はPropertyPackで、このValueに元のプロパティの値、Instanceにその要素のインスタンスが入っているので、まずValueを見てtrueのものだけ通し、その後で要素のインスタンスをメソッドに渡しています。

コレクション要素の変化を追うのは割と面倒ですが、それがこれだけでできます。なお、同じ結果は、適当なタイミングでコレクションをループして走査することでも得られますが、そうすると当然ループのコストがかかります。

また、プロパティ変化をある条件でフィルターした要素を集計したいときは、FilteredReadOnlyObservableCollectionが利用できます。この例は以下のようなものです。
ReactivePropertyであるIsAllLongを生成するのに、上の方法ではIsLongにObserveElementPropertyを繋げ、その後で元のコレクションをループさせて判定しているので、当然ループのコストがかかります。対して、下の方法ではIsLongがfalseである要素のコレクションをFilteredReadOnlyObservableCollectionで生成し、このCollectionChangedイベントを捉えて、元のコレクションとFilteredReadOnlyObservableCollectionのCountから判定しています。

ただ、これが使えるのは対象の、変化するプロパティがCLRプロパティの場合で、ReactivePropertyを対象とするときは別の方法を考える必要があります。

単純には、ObserveElementObservablePropertyを繋げ、そこから拾い出した要素を外のコレクションに保持し、その要素数を使えばいい気がしますが、これだけだと元のコレクションから要素が削除された場合に追い切れません(ObserveElementObservablePropertyは現在コレクションにある要素を対象とするので)。これを考慮した場合は以下のようなものが考えられます。
ReactivePropertyであるIsAnySelectedを生成するのに、上の方法では元のコレクションをループさせて判定しています。対して、下の方法では、いきなり膨らみましたが、条件を満たす要素をListに保持するようにし、現在コレクションにある(新たに追加された場合も含む)要素をObserveElementObservablePropertyで、要素が削除された場合とコレクションがクリアされた場合をCollectionChangedAsObservableで追うようにした上で、ListのCountで判定しています。

一応これを汎用の拡張メソッドにしてみたものが、他の例と併せてReactivePropertyTestプロジェクトにあるので、興味があれば見てください。ちなみに、NewItemsをチェックしているルートは実際はObserveElementObservablePropertyが先に捉えるので不要ということに後から気づきました。

しかし、変化の頻度が少なく要素数も特に多くなければループさせる方法でも十分なので、実際ここまでやることはないかもしれません。

4. まとめ

このアプリではM-V-VM間を専らRxとReactivePropertyで結ぶようにした結果、その部分のコードがとても少なく、すっきりとなりました。

したがって、Rxをガンガン使うようなアプリであれば、ReactivePropertyも併用すればその威力はさらに増すと思います。Rx自体の学習コストが決して低くないのがあれですが、ReactivePropertyのソースにはRxの高等テクニックが詰まっているので、時々見ると発見があります。

最後に一つ。RxとReactivePropertyを駆使すればアプリ内を融通無碍に繋ぐことができますが、これと非同期が組み合わさると実行コンテキストがよく分からない状態になることがあります。何か動作が妙……というときは実行コンテキストを確かめてみるのも手です。

2015/07/27

VisualStateManagerのVisualStateを確認する

WPFのコントロールの遷移をVisualStateManagerでデザインしていると、現在のVisualStateを直接確認したいときがあります。

VisualStateGroupが一つだけなら同じVisualStateGroup内のVisualStateは排他なので結果を見て判断することも難しくないですが、VisualStateGroupが複数になると異なるVisualStateGroup間のVisualStateは併存するので、複数のVisualStateの効果が重なることになり、何がどうなっているか判断に迷うことがあります。というか、かなり迷いました。

そこで、デバッグ用に現在のVisualStateを出力する添付プロパティを書いてみました。
このキモは、VisualStateGroupは対象のコントロール自体ではなく、たいていその中のGridなどに定義されるので、子要素を下りながら探す必要があることです。なお、VisualStateManagerではなくトリガーに依っている場合は、当然何も出てきません。

VisualStateの変化を捉える方法が見つからなかったのでIntervalで指定された秒ごとに永久ループで出力しますが、そこはあくまでデバッグ用ということで。

これをToggleButtonに使った例。
このToggleButtonにはCommonStatesとCheckStatesのVisualStateGroupがあり、これらがToggleButtonと継承元のButtonBaseとさらに元のControlのChangeVisualStateの重ね掛けで変化するというややこしいものですが、一番下に加えたVisualStateMonitorの添付プロパティは以下のように出力します。
// 初期状態
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
// カーソルを上に移動
Element: TestToggleButton -> Group: CommonStates -> State: MouseOver
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
// クリック
Element: TestToggleButton -> Group: CommonStates -> State: MouseOver
Element: TestToggleButton -> Group: CheckStates -> State: Checked
// カーソルを上から外す
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Checked
// 再度クリックしてカーソルを外す
Element: TestToggleButton -> Group: CommonStates -> State: Normal
Element: TestToggleButton -> Group: CheckStates -> State: Unchecked
これが分かれば簡単になるというものでもないですが、直接確認しながら作業できるのは随分違うと思います。実はもっと簡単にできる方法があるのかもしれませんが、とりあえず。

[追記] 添付プロパティの作成について

添付プロパティの構文の話ですが、依存関係プロパティの場合、PropertyMetadataのPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)のdはそのプロパティが定義されたクラスのインスタンスなので、これをキャストして処理をインスタンスメンバーに繋げることができます。一方、添付プロパティの場合、dはそのプロパティが添付されたFrameworkElementのインスタンスになるので、添付プロパティが定義されたクラスのインスタンスメンバーにそのままアクセスはできません。

したがって、FrameworkElementのインスタンスの処理はおおよそ以下に分かれます。
  1. 静的メソッドの中で処理を完結させる。デリゲートを駆使すればある程度複雑なこともできるが、状態をフィールドなどに保存できないので、行き届いた状態管理は難しい。

  2. 静的メソッドの中で、状態を静的フィールドなどに保存しながら処理する。アプリ中の一カ所でしか使われない場合や、静的な値が共有されても構わない場合に限られる。

  3. 添付プロパティの型をそのプロパティが定義されたクラスにすると、初期化時にe.NewValueにそのクラスのインスタンスが入ってくるので、それをキャストしてインスタンスメンバーに繋げる。WindowChromeで使われている方法で、複雑な処理も行いやすい。
あくまで構文上のテクニックですが、結構悩むところなので。

2015/06/25

OneDriveとデスクトップアプリ(Windows 8.1限定)

OneDriveをWindows 8.1に同期して使っているとき、OneDrive中のファイルをデスクトップアプリから扱う方法に関して。

1. オンラインのみのファイルの判別

Windows 8.1ではOneDrive中のファイルを「オフラインで利用可能」か「オンラインのみ」に設定できるが、「オンラインのみ」にした場合、これをデスクトップアプリのSystem.IO.FileStreamで開こうとすると、例外(System.IO.IOException)が飛ぶ。

例えば、以下のうち下の利用可能性を「オンラインのみ」にしたファイルがそれに当たる。


同じフォルダをコマンドプロンプトから「dir /a」で表示させたもの。「オンラインのみ」のファイルはサイズが括弧で囲まれている。


「オンラインのみ」のファイルはファイルエントリだけがローカルにあって、データ本体はクラウド上にあるものが必要に応じて流れてくるものなので、その状態では開けないのは別に不思議ではない。

問題はそれを事前にどうやって判別するかで、WinRTではStorageFile.IsAvailableStorageFile.AttributesのLocallyIncompleteを見ればいい。一方、デスクトップアプリではそのものずばりの情報が出てこなくて時間を取られたが、答えは簡単だった。
このファイル属性のうち、Oのオフライン属性がそれで、OneDriveで「オンラインのみ」のファイルにはこれが付く。System.IO.FileAttributesのOfflineの項を見ると、"The file is offline. The data of the file is not immediately available."とあるので(OneDriveとは一言も出てこないが)、これらしい。

ということで、以下はテストコードとその結果。
見ての通り、「オンラインのみ」のファイルかどうかはオフライン属性の有無で判別できることが確認できる(オンラインとオフラインの用語がややこしい)。ちなみに、これらのファイルに付いている属性は、初めにエクスプローラーで属性を表示させたものと当然ながら一致している。

2. オンラインのみのファイルへのアクセス

オーソドックスにはOneDriveのAPIを使えばいい話だが、MSDNに以下のエントリがあった。
このサンプルコードをふむふむと読み始めて、途中で意味が呑み込めてきて……驚愕。

デスクトップアプリからWinRTのAPIを使うお約束をすっ飛ばし、デスクトップアプリでも使えるとされていないStorageFileクラスを、WinRTのアセンブリを動的ロードしてリフレクションして実行するラッパー、とは!

いや、Microsoftの人が書いているのだからできるのだろうが、何という力業。積極的に使いたいかは別にして、そういうのもあるのか……と。

3. Windows 10での変更

Windows 10ではまたOneDriveに変更があるらしく、現在のInsider Previewでは「オンラインのみ」の設定はなくなり、すべて「オフラインで利用可能」と同じく、データ本体がローカルにコピーされるようになっている。したがって、「オンラインのみ」のファイルを扱う方法はWindows 8.1で最後になるらしい。いい加減安定させた方がいいと思うが。

2015/06/20

水晶雫グラフィグ

水晶雫ちゃんの3回目の誕生日に合わせて、イラストレーターの桐野霞先生が水晶雫グラフィグを公開されていたので、早速作ってみました。
グラフィグというのは独特の四角いスタイルにデフォルメされた、キャラクターペーパークラフトのフォーマットのようなもの、だと思います。

1. 出来上がり

PDFをキンコーズでプリントアウトしたものから数時間の作業後、完成したお姿。
Suisho Shizuku Graphig

カラーレーザープリンターによる出力ということで、印刷物と変わらない美麗さでした。
Suisho Shizuku Graphig

グラフィグなので、とくに後ろから見ると箱箱しています。
Suisho Shizuku Graphig

首は回せるので、少し動きは付けられます。
Suisho Shizuku Graphig

これだけデフォルメしても雫ちゃんのイメージは保たれているのはさすがです。部品を作っている間はカクカクしてるなと思っていたのが、全部合わせた途端、なるほどなるほど、これはこれで、と顕現する感じです。

2. 参考

作り方に複雑なことはないですが、参考になりそうなことを。
  • キンコーズでの出力は、マットで210g/m2ぐらいの紙を使用。強度的には厚い方がいいですが、厚すぎると狙った位置できれいに折るのが難しくなります。結果的にはこれ以上厚いと難しくなる感じ。費用は、PC利用を除いたプリントアウトだけだと一枚50円強で、1セット2枚で100円強でした。一応、ミスに備えて予備の分もプリントアウトしておくとベター(自分は使った)。

  • 頭部の頬の部分の折り線が少しズレてるので、先に顎の部分を折り、机の角で箱に組みながら折る位置を順々に出していくといいです。この際、差し込むベロの部分は切り落として、のりしろと接着剤だけで固定するようにした方が調整はしやすいです。ただ、頭の側面は髪ですっぽり隠れるので、あまり神経質になる必要はないです。

  • 足の上下をぴったり接着したら、上下の長さが少し違っていて切り込まないといけなくなったので、仮組みをよくやった方がいいです。
まあしかし、手軽にできるのがペーパークラフトのいいところだと思うので、あくまで気楽にどうぞ。

2015/06/18

WinRTでの設定の保存(続)

PasswordVaultを使った設定の保存を確認しているうち、アプリをアンインストールしてもPasswordVaultに保存された情報は消えずに残り、再度アプリをインストールすると読み出せることに気づきました。

残るといっても暗号化されているので大過ないといえばないですが、一応、暗号化したファイルをLocalFolder/RoamingFolderに保存する(LocalFolder/RoamingFolderはアンインストール時に削除される)ものも追加していたら全体がややこしくなってきたので、まとめ直しました。
これを使った設定クラスは、例えば以下のようになります。
以下の属性で保存先と暗号化の有無を指定します。
  • RoamingAttribute : ローミングする。
  • CryptVaultAttibute : PasswordVaultに保存する。
  • CryptFileAttribute : 暗号化したファイルをLocalFolder/RoamingFolderに保存する。
PasswordVaultはローミングされるので、CryptVaultAttibuteの場合はRoamingAttributeは影響しません。また、CryptVaultAttibuteはCryptFileAttributeより優先されます。

型は、直接扱えないものはシリアライズして保存するので制限はない、はずですが、シリアライズ特有の問題は起こり得るので、それぞれ対処を。各プロパティの初回アクセス時にリフレクションを使ってどう扱うかを決めますが、結果をキャッシュしたものを次回から使うので、コストはそんなには高くないと思います。

まあ実際はここまでやらずとも、DictionaryにまとめてシリアライズしてRoamingFolderに保存一本でもいいんですが、あるものは使ってみたいという性分なので。

[追記] LocalSettings/RoamingSettingsでサポートされる型

LocalSettings/RoamingSettingsはIDictionaryになっていて、この値の型はObjectですが、実際に保存できるのはLocalSettingsの説明によればWindows Runtime base data typesとされています。

このリストの型をテストしてみたところ(voidを除く)、なぜかUriだけはサポートされてない型という例外が出て駄目でした。一方、ここにはないbyte[]でも保存できるので、どんな型でもシリアライズしてbyte[]にするか、そのままbyte[]に変換すれば、直接出し入れは可能です。

[修正]

例のシングルトンインスタンスの生成方法を修正。C# 6.0の理解が足りてませんでした。

2015/06/07

WinRTでの資格情報の保存

WinRTで資格情報、パスワードであったりアクセストークンであったり、に限らず、秘密を要する情報を安全に保存するにはWindows.Security.Credentialsが使えます。
ざっくりまとめると、
  • リソース名(Resource)、ユーザー名(UserName)、パスワード(Password)から成るPasswordCredentialオブジェクトをPasswordVaultクラスのメソッドを使って書き込み・読み出しする。
  • リソース名は、Twitterのようなサービス名、あるいは複数のユーザー情報をまとめるためのグループ名のようなもの。とくに分類する必要がなければ何でもいい。
  • ユーザー名とパスワードは、そのままの意味で使ってもいいし、要はDictionaryにおけるKeyとValueの組み合わせと同じなので、別の意味を持たせてもいい。
  • 一つのアプリで使えるPasswordCredentialの上限は10(後述)。
これをどう使うかというと、
  • 保存するときは、リソース名、ユーザー名、パスワードでPasswordCredentialを作成して、PasswordVault.Addメソッドを使う。

  • 保存したPasswordCredentialを取得するときは、
    • リソース名とユーザー名が分かっていればPasswordVault.Retrieveメソッドを使う。指定したPasswordCredentialが存在しなければ例外(System.Exception)が飛ぶので、要try-catch。
    • リソース名でまとめて取得するにはPasswordVault.FindAllByResourceメソッド、ユーザー名でまとめて取得するにはPasswordVault.FindAllByUserNameメソッドを使う。これらも存在しなければ例外が飛ぶので、要try-catch。
    • 全部まとめて取得するにはPasswordVault.RetrieveAllメソッドを使う。これは存在しなくても例外は飛ばないので、実はこれからフィルターして探し出せばtry-catchなしで済む。
    • なお、まとめて取得したPasswordCredentialはパスワードが空なので、パスワードを見るにはその前にPasswordCredential.RetrievePasswordメソッドを実行する要あり。

  • 保存したPasswordCredentialのパスワードを修正するときは、同じリソース名、ユーザー名でPasswordCredentialを作成してPasswordVault.Addメソッドを使えば上書きされる(PasswordCredentialを取得して修正しても、保存したものには反映されない)。

  • 保存したPasswordCredentialを削除するときは、一旦取得してからPasswordVault.Removeメソッドを使う。
これらをまとめてみたもの。
ここからGet/Setメソッドを静的メソッドにして、WinRTでの設定を保存するクラスでプロパティのバッキングストアに使えるようにすれば、秘密を要する情報も設定クラスでまとめて管理できるようになるかなと思います。

ちなみに、PasswordCredentialの上限はPasswordVault.Addメソッドの説明には10と書かれてますが、10より少ない状態から一気に作成すれば10を超えて作成できたりしたので、上限をチェックするアルゴリズムに現状は穴があるっぽいです。

それはともかく、量が多い場合は暗号化したファイルをLocalFolderなりRoamingFolderに保存しろという話で、そういう例もあります。

2015/04/29

VBのループ中のローカル変数

今更という気はしますが、VBで引っ掛かってしまったので、記録として。

VBとC#は兄弟言語ですが、C#から見ると引っ掛かりやすい点があって、ぱっと思いつく限り以下のようなものがあります。
  • Nothingの意味(必ずしもnullではない)
  • 配列のコンストラクタの要素数
  • 整数と実数の自動変換(とくに除算時)
  • オーバーロード解決の優先順
これらとは別に、あまり意識してなかった違いとして、メソッド中のローカル変数の初期値の扱いがあります。C#ではローカル変数を宣言後、値を与えないまま使おうとするとエラーとなってビルドできないのに対し、VBでは値を与えなくてもその型の既定値を使う形でビルドできます。

以下のメソッド中のローカル変数valueについて、LocalVariableCase0では初期値としてFalseを代入しているのに対し、LocalVariableCase1では初期値を代入していませんが、Boolean型の既定値がFalseなので同じ結果になります。
ここまでは問題ありませんが、これをループにすると違いが出ます。

以下のLocalVariableCase2ではループが回る度にvalueはFalseに初期化されますが、LocalVariableCase3ではループしても前回のループで与えられたTrueが残ってしまいます。
ここで試しにLocalVariableCase3のループ中の処理を別メソッドに切り出すと、valueのスコープはそのメソッド中に限定されてLocalVariableCase2と同じ結果になります。
つまり、同一メソッド内のループ中であればループの度にローカル変数を新たに宣言したつもりでも、そうはならず前回の値が維持されます。

これは少しトリッキーというか、予想とは違っていて驚いたわけですが、そういえばVBを勉強し始めたときに変数に初期値を与えておかないと予期しない動作になって危ないと読んだような記憶がありますが、すっかり忘れてました。

というか、何でもかんでも初期値を与えるのもカーゴカルトみたいで無駄だなと思って削っていたら引っ掛かってしまったわけですが、また忘れそうなので書いておきます。

[追記] IL

これだけでは何がどうなっているか明瞭でないので、LocalVariableCase3のILをIL DASMで見ると、こうなっています。


これも今更ですが、ローカル変数はVBでのメソッド中の位置に関わらずILでは冒頭で宣言される形になっています。そこでVBでもローカル変数を冒頭で宣言するように変えたメソッドを作り、そのILを見たのが以下です。


見ての通り、ILは全く同じになります。つまり、ローカル変数の宣言はその位置で変数が初めて確保されることを意味しないので、初期値にリセットするにはきちんと代入しないとダメということですね。

2015/04/15

WinRTでの設定の保存

WinRTでの設定については、どこに、どのタイミングで保存するか考える必要があるわけですが、
  1. 設定が変更される都度、保存するようにした方が確実。
  2. 永続的にするにはApplicationDataのLocalSettingsかRoamingSettingsに保存するのが便利。
  3. 設定用クラスの設定用プロパティのアクセサー内でこれらにアクセスするようにすると管理がすっきりする。
  4. そもそもLocalSettingsかRoamingSettingsを設定用プロパティのバッキングストアにしてしまえばいい。
ということになって、@tmytさんがそういう例を出されてます。
この方法をベースに自作列挙型や自作クラスも保存できるように考えて、以下のようになりました。まずは設定用の基本クラス。
自作列挙型は基になる型に変換した上でそれを保存するように、自作クラスはDataContract属性を付ける前提でJSONにシリアライズした形で保存するようにしています。

これを継承した設定用クラスの例。
頻繁に参照されるプロパティでコストが気になる場合はアクセサー内でキャッシュするようにすればいいかと思います。

とくに目新しいことはないですが、一つの定型的方法として。

[修正]

GetValueメソッド中で自作クラスの値がまだ存在しなかった場合の処理を修正。

2015/02/22

WPFでカラープロファイルを添付プロパティで取得する

2/21のめとべや東京#7での@veigrさんのセッション「カラーマネジメントシステムの概要とカラマネプログラミング初歩」はもやっと感じていたカラーマネジメントの基礎がようやく理解でき、かつ実践的なコードもあるという、自分的にとても有意義なものでした。
セッション中でも触れられていたWPFの関連APIの説明も参考になります。Microsoftからの説明があまりないので、こういう実際の動作まで調べたものは有り難いです。
WPFに関しては、モニターのカラープロファイルを取得する部分を除けば標準APIの枠内で対応でき、さほどハードルは高くないので(Windowsフォトビューアーのように同じ画像内で同時に違うカラープロファイルを適用するなどという変態的なことを目指さなければ)、画像の色を真面目に表示するアプリを作る場合は必見だと思います。

で、モニターのカラープロファイルについては、マルチモニター対応のためには各Windowが現在属しているモニターのものが必要になるので、これをWindowの添付プロパティで取得するものを書いてみました。
この添付プロパティを持つクラスはColorProfilePropertyですが、
  • Freezableを継承しているのは、バインディングターゲットとなれるようにするため。DependencyObjectだとバインディングソースにしかなれないので、バインディングを張るときの自由度が落ちる。
  • 添付プロパティの値を自分自身のインスタンスとしているのは、このクラス中の依存関係プロパティを複数、自由に参照できるようにするため。"AttachedProperty"という名前にとくに意味はない。
モニターのカラープロファイルのファイルパスを取得するメソッドは以下のとおりです。
private string GetColorProfilePath(Visual sourceVisual)
{
  var source = PresentationSource.FromVisual(sourceVisual) as HwndSource;
  if (source == null)
    return null;

  var monitorHandle = NativeMethod.MonitorFromWindow(
    source.Handle,
    NativeMethod.MONITOR_DEFAULTTO.MONITOR_DEFAULTTONEAREST);

  var monitorInfo = new NativeMethod.MONITORINFOEX
  {
    cbSize = (uint)Marshal.SizeOf(typeof(NativeMethod.MONITORINFOEX))
  };

  if (!NativeMethod.GetMonitorInfo(monitorHandle, ref monitorInfo))
    return null;

  IntPtr deviceContext = IntPtr.Zero;

  try
  {
    deviceContext = NativeMethod.CreateDC(
      monitorInfo.szDevice,
      monitorInfo.szDevice,
      null,
      IntPtr.Zero);

    if (deviceContext == IntPtr.Zero)
      return null;

    // First, get the length of file path.
    var lpcbName = 0U;
    NativeMethod.GetICMProfile(deviceContext, ref lpcbName, null);

    // Second, get the file path using StringBuilder which has the same length. 
    var sb = new StringBuilder((int)lpcbName);
    NativeMethod.GetICMProfile(deviceContext, ref lpcbName, sb);

    return sb.ToString();
  }
  finally
  {
    if (deviceContext != IntPtr.Zero)
      NativeMethod.DeleteDC(deviceContext);
  }
}
この中でもコアなのはGetICMProfile関数を使う部分ですが、ファイルパスを格納するlpszFilename(第3引数)のサイズを決めるのに少しややこしいことをしていて、初めにこれにnullを入れて実行するとlpcbName(第2引数)にlpszFilenameのサイズが入ってくるので、これを使ってStringBuilderを用意し、lpszFilenameに入れて再度実行することで取得しています(@veigrさんのサンプルのとおり)。

2回実行するのはどうも……という場合は、lpcbNameを通常あり得るパスの最大長である260に決め打ちしても動きます。
var lpcbName = 260U;
var sb = new StringBuilder((int)lpcbName);
NativeMethod.GetICMProfile(deviceContext, ref lpcbName, sb);
万一サイズが足りなかった場合を考えて戻り値を取って処理を重ねることもできます……細かい話ですが。

サンプルアプリを実行すると、カラープロファイルのファイルパスが表示されます。

モニター間をまたいでWindowを移動かリサイズするとファイルパスが変わり、画像がColorConvertedBitmapで再描画されます。ただ、この処理が結構重いので、滑らかに切り替わるというわけでもないです。まあ移動かリサイズを止めたタイミングで反映させるようにすれば、目立たないだろうと思いますが。

実はこれまでセカンダリモニターのカラープロファイルは適当で済ませてましたが、きちんと設定して試してみるとプライマリモニターとはっきり分かる違いがあるのを発見して、意外と馬鹿にならないです。

2015/02/12

Exifの日付だけを修正する

先日、新しいカメラを持ってイベントに行って写真を撮ってきた。まではよかったが、PCに取り込む段になって写真の日付が1年前になっていることに気づいた。原因は容易に想像できるとおり、初めにカメラの時計を設定したときに年を間違えていた。

当然、画像ファイルのExifに記録された日付も1年前になっていたが、Exifの情報は色々な写真管理の基礎となるので、これはいかにもまずい。

そこで既存ツールを試してみたが、Exifのサムネイルが維持されなかったりして、ツールを確かめて回るのも手間。それなら自分でできないかと思い、ただし、真っ当にExifを編集しようとするとファイルサイズが変わるので、年を1年直すだけでそれは負けた気がする、ということで直接ファイルのバイナリを修正しようと考えた。

Exifの規格では日時は"YYYY:MM:DD HH:MM:SS"のフォーマットのASCII文字列と決まっているので、そんな無理な話でもないはず。実際、バイナリエディタで簡単に見ることができたので、ゴーアヘッド。

1. バイト配列用のIndexOfとReplaceメソッド

まずはバイト配列に含まれる他のバイト配列の位置を調べて置換するメソッドが必要なので(StringにおけるString.IndexOfとString.Replaceメソッドに相当)、書いてみた。

主眼は位置を調べるメソッドの方で、複数用(SequenceIndicesOf)と単数用(SequenceIndexOf)にそれぞれ引数がbyte[]とIEnumerable<byte>のオーバーロードを作った。
ルートとしては、
  1. 複数用byte[]メソッド+単数用byte[]メソッド
  2. 複数用byte[]メソッド+単数用IEnumerable<byte>メソッド
  3. 複数用IEnumerable<byte>メソッド
の3通りあるが、SequenceReplaceメソッド中の呼び出し元の引数がbyte[]なので、このままだと1のルートが走る。

折角なので引数にAsEnumerable<byte>()を付けてルートを変えつつ、最終的なコードで300のJPGファイル(計1.06GiB)を処理したところ、所要時間は以下のようになった。
  1. 21.6秒
  2. 46.7秒
  3. 36.1秒
byte[]で通した1のルートが基本にして最速という結果だが、2のルートが2倍以上遅いのはSkipが時間を食っているのではないかと思う(実行の度に先頭からループが回るわけで)。自分的に期待したのは3のルートだが、1のルートには全く及ばず。

なお、最終的なコードでは置換数=検索数の上限を指定するmaxCount引数を使ってないが、実はヒット回数は各ファイル3回でかつ位置は先頭部分と分かっているので、これに3を指定すれば画像データ部分を無駄に検索する必要がなくなって所要時間はたいして変わらなくなる、というオチ。

[修正]

複数用IEnumerable<byte>メソッドのmaxCountの処理にバグがあったので修正した。複数用byte[]メソッドは問題なかったが、比較のためこれに合わせた。なお、数字を挙げた所要時間を計った際にはmaxCountを使ってないので、影響はない。

2. Exifの日付部分を置換するメソッド

このSequenceReplace拡張メソッドを使ってJPGファイルのExifの日付部分を置換するメソッド(とそのコンソールアプリ)。

各ファイルについて、以下の処理をする。
  1. バイト配列として読み込む
  2. 一応、真っ当にExifの日時の文字列を読み出す(System.Photo.DateTakenにあるパスを使う)
  3. 元の日時をDateTimeに変換し、修正したDateTimeを生成して、これを規格に従った文字列にする
  4. 両方の文字列をASCIIの文字コードでバイト配列に変換する
  5. これらを置換したバイト配列を生成する
  6. バイト配列を別ファイルに書き込む
3. まとめ

以上、実用での使用時間は1分以内だったというコーディングだが、バイト配列の置換というのはユーティリティメソッドとしていかにもありそうなので、より効率的な方法があるような気がする。

2015/01/31

無線LANのSSIDをC#から取得する

無線LANのSSIDをC#からAPIで取得するコードを書いてみた。

1. 背景

アプリから無線LANのSSIDを取得したいときがあるが、標準の.NET FrameworkにもWinRTにもそれが可能なAPIは用意されていない。C++にはNative Wifi APIがあるが、このマネージド実装のManaged Wifi APIは開発が止まって久しく、かといってP/Invokeの宣言を起こすのも大変そうなので敬遠していた。実用上はNetshの出力をパースすれば大体足りるし。

それが、ふとMSDNでこんなエントリを見かけた。
「おおっ」と思いつつダウンロードしたが、実体部分はC++のライブラリで、それをC#から使うものだった(タイトルどおりではある)。悔しかったので少し探したところ、幾つかサンプルが挙げられていた。
これらとMSDNを合わせ見た結果、必要なピースは揃ってそうだったので書いてみた。
2. 利用可能なネットワークのSSIDを取得する

手順としては以下のようになる。
  1. WlanOpenHandleでハンドルを取得する。このdwClientVersionはOSによって変わるが、Vista以降は2。
  2. WlanEnumInterfacesでPCにある無線LANインターフェイス(無線LANアダプター)の情報を取得する。
  3. WlanGetAvailableNetworkListで各インターフェイスから見える無線LANネットワークの情報を示すWLAN_AVAILABLE_NETWORKを取得する。これは各インターフェイスの感度にも左右されるが、インターフェイスが複数あれば当然、単数でも試した限りでは重複があり得る。
  4. WLAN_AVAILABLE_NETWORKからSSIDを取得する。
public static IEnumerable<string> GetAvailableNetworkSsids()
{
  var clientHandle = IntPtr.Zero;
  var interfaceList = IntPtr.Zero;
  var availableNetworkList = IntPtr.Zero;

  try
  {
    uint negotiatedVersion;
    if (WlanOpenHandle(
      2, // Client version for Windows Vista and Windows Server 2008
      IntPtr.Zero,
      out negotiatedVersion,
      out clientHandle) != ERROR_SUCCESS)
      yield break;

    if (WlanEnumInterfaces(
      clientHandle,
      IntPtr.Zero,
      out interfaceList) != ERROR_SUCCESS)
      yield break;

    var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);

    Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);

    foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
    {
      if (WlanGetAvailableNetworkList(
        clientHandle,
        interfaceInfo.InterfaceGuid,
        WLAN_AVAILABLE_NETWORK_INCLUDE_ALL_MANUAL_HIDDEN_PROFILES,
        IntPtr.Zero,
        out availableNetworkList) != ERROR_SUCCESS)
        continue;

      var networkList = new WLAN_AVAILABLE_NETWORK_LIST(availableNetworkList);

      foreach (var network in networkList.Network)
      {
        Debug.WriteLine("Interface: {0}, SSID: {1}, Quality: {2}",
          interfaceInfo.strInterfaceDescription,
          network.dot11Ssid,
          network.wlanSignalQuality);

        yield return network.dot11Ssid.ToString();
      }
    }
  }
  finally
  {
    if (availableNetworkList != IntPtr.Zero)
      WlanFreeMemory(availableNetworkList);

    if (interfaceList != IntPtr.Zero)
      WlanFreeMemory(interfaceList);

    if (clientHandle != IntPtr.Zero)
      WlanCloseHandle(clientHandle, IntPtr.Zero);
  }
}
P/Invokeの宣言はレポジトリの方を参照。

3. 接続中のネットワークのSSIDを取得する

手順は2.までは上と同じ。
  1. WlanOpenHandleでハンドルを取得する。
  2. WlanEnumInterfacesでPCにある無線LANインターフェイスの情報を取得する。
  3. WlanQueryInterfaceで各インターフェイスの現在の接続状況を示すWLAN_CONNECTION_ATTRIBUTESを取得する。このためにはOpCodeにwlan_intf_opcode_current_connectionを指定する。
  4. WLAN_CONNECTION_ATTRIBUTESのisStateで接続中かどうか判別できるので、接続中ならwlanAssociationAttributesのWLAN_ASSOCIATION_ATTRIBUTESからSSIDを取得する。
public static IEnumerable<string> GetConnectedNetworkSsids()
{
  var clientHandle = IntPtr.Zero;
  var interfaceList = IntPtr.Zero;
  var queryData = IntPtr.Zero;

  try
  {
    uint negotiatedVersion;
    if (WlanOpenHandle(
      2, // Client version for Windows Vista and Windows Server 2008
      IntPtr.Zero,
      out negotiatedVersion,
      out clientHandle) != ERROR_SUCCESS)
      yield break;

    if (WlanEnumInterfaces(
      clientHandle,
      IntPtr.Zero,
      out interfaceList) != ERROR_SUCCESS)
      yield break;

    var interfaceInfoList = new WLAN_INTERFACE_INFO_LIST(interfaceList);

    Debug.WriteLine("Interface count: {0}", interfaceInfoList.dwNumberOfItems);

    foreach (var interfaceInfo in interfaceInfoList.InterfaceInfo)
    {
      uint dataSize;
      if (WlanQueryInterface(
        clientHandle,
        interfaceInfo.InterfaceGuid,
        WLAN_INTF_OPCODE.wlan_intf_opcode_current_connection,
        IntPtr.Zero,
        out dataSize,
        ref queryData,
        IntPtr.Zero) != ERROR_SUCCESS) // If not connected to a network, ERROR_INVALID_STATE will be returned.
        continue;

      var connection = (WLAN_CONNECTION_ATTRIBUTES)Marshal.PtrToStructure(queryData, typeof(WLAN_CONNECTION_ATTRIBUTES));
      if (connection.isState != WLAN_INTERFACE_STATE.wlan_interface_state_connected)
        continue;

      var association = connection.wlanAssociationAttributes;

      Debug.WriteLine("Interface: {0}, SSID: {1}, BSSID: {2}, Signal: {3}",
        interfaceInfo.strInterfaceDescription,
        association.dot11Ssid,
        association.dot11Bssid,
        association.wlanSignalQuality);

      yield return association.dot11Ssid.ToString();
    }
  }
  finally
  {
    if (queryData != IntPtr.Zero)
      WlanFreeMemory(queryData);

    if (interfaceList != IntPtr.Zero)
      WlanFreeMemory(interfaceList);

    if (clientHandle != IntPtr.Zero)
      WlanCloseHandle(clientHandle, IntPtr.Zero);
  }
}
なお、3.のWlanQueryInterfaceはそのインターフェイスが接続中でないときはERROR_INVALID_STATEを返してくる。

4. まとめ

Windows 7とWindows 8.1、ついでにWindows 10 TPで確認したところ問題なさそうだったので、とりあえずこれで行けると思う。

しかしAPIで取得できるとコマンド出力をパースするよりわずかでも速いし、気持ちすっきりするのがいい。まあP/Invokeのマーシャリングは結構泥臭いけど。

[追記] UTF-8のSSID

SSIDというとASCII文字列と思われがちだが、IEEE 802.11の規格を読むとSSIDは0-32のOctet stringとされていて、平たくいうと長さ0-32のバイト配列とされている。これは現在はUTF-8も使えることになっていて、当然日本語を使うこともできる(それで駄洒落を作るのが少しネタになっていた)。

Native Wifi APIでSSIDの情報を格納するのはDOT11_SSIDだが、この説明でも"The SSID that is specified by the ucSSID member is not a null-terminated ASCII string."と釘が刺されている。

このDOT11_SSIDに対応するP/Invoke用の構造体を初め以下のように書いていた。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
  public uint uSSIDLength;

  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
  public string ucSSID;

  public override string ToString()
  {
    if ((ucSSID == null) || (ucSSID.Length < (int)uSSIDLength))
      return null;

    return ucSSID.Substring(0, (int)uSSIDLength);
  }
}
これには問題があって、uSSIDLengthの示す長さを勘違いしていることもさることながら、実際に試してみるとUnmanagedType.ByValTStrでは正しくマーシャリングされず、「の」が「縺ョ」のように文字化けを起こす。これを回避するには、ucSSIDをまずはバイト配列にマーシャリングした上で、これから文字列に変換すればうまく行った。
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] ucSSID;

public override string ToString()
{
  return Encoding.UTF8.GetString(ucSSID, 0, (int)uSSIDLength);
}
さらに、規格に忠実にバイト配列としても取り出せるようにまとめ直せば、こんな感じ。
[StructLayout(LayoutKind.Sequential)]
private struct DOT11_SSID
{
  public uint uSSIDLength;

  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
  public byte[] ucSSID;

  public byte[] ToBytes()
  {
    return (ucSSID != null)
      ? ucSSID.Take((int)uSSIDLength).ToArray()
      : null;
  }

  public override string ToString()
  {
    return (ucSSID != null)
      ? Encoding.UTF8.GetString(ToSsidBytes())
      : null;
  }
}
ちなみに、Windows 8.1のNetshの出力を見るとUnmanagedType.ByValTStrを使ったときと同じように文字化けする。一方、設定のネットワークから見ると文字化けしないので、NetshのUTF-8対応が遅れているようで、Windows 10 TPでも解消してないらしい。
ということで、UTF-8対応ができるという点が、現状APIによる方法が明確に優れている点になる。

[追記] ライブラリ公開

ライブラリとしてまとめたので、コードをなるべく合わせるよう修正した。

2015/01/28

Windows 10 TPのインストール時のエラー

Windows 10 TPのインストール時に遭遇したエラーについての記録。

1. エラーの状況

最近のWindowsのインストールで問題が出ることはあまりないですが、久々に謎なエラーに遭遇しました。前提として、UEFI Onlyに設定したThinkPad X230でUEFI用に作成したUSBメモリから起動してのインストールです。

状況としては、
  1. Build 9926の日本語版のISOファイルからUSBメモリを作成し、これから起動しようとしたところ、「お使いのPCに必要なメディアドライバーがありません。」というエラーが表示されて進めない。


  2. IntelのIRSTのドライバーファイルを読み込ませてみたが、変化なし。たぶんこのメッセージどおりの問題ではない。
  3. ISOファイルのハッシュ値を確認したが、ダウンロードサイトに示されたとおり。
  4. USBメモリを作成し直してみたが、エラーは変わらず。そもそもUEFI用のUSBメモリの作成方法は簡単で(後述)、間違う余地は少ない。
  5. 英語版のISOファイルからUSBメモリを作成してみたが、"A media driver your computer needs is missing."という同内容のエラーが出る。


  6. 単純な問題ではない気配がしてきたので、ISOファイルをDVD-Rに焼いてインストールしたところ、問題なく終了。問題はUSBメモリの起動にある。
  7. 残しておいたBuild 9879のISOファイルからUSBメモリを作成してみたが、エラーは変わらず。Build 9926特有の問題ではないが、しかし以前は問題なかったはず。
  8. ふとUSBメモリを差すコネクタをX230の向かって左側から右側に変えたところ、エラーは出ず、ハードウェアと関係があることが判明。正確には左側2箇所のコネクタ(USB.3.0)のどちらでもエラーが出るが、右側1箇所のコネクタ(USB2.0)では出ない。なお、左側でも通常時は問題ないし、左側で作成したUSBメモリで右側から起動もできる。
  9. 確認のため、USBメモリをそれまでのPicoDrive F3 32GBから、Express RC8 25GBとJetFlash 760 16GB(いずれもUSB3.0)に変えてみたが、エラーの出る条件は変わらず。USBメモリ側の問題ではない。
  10. さらに確認のため、USBメモリをPicoBoost 8GB(USB 2.0)に変えてみたところ、左右どちらでもエラーは出ず。エラーが出るのはUSB3.0の場合のみ。
  11. 念のため、Windows 8.1 Enterprise評価版のISOファイルを使ってみたところ、USB3.0のUSBメモリでも左右どちらでもエラーは出ず。エラーが出るのはWindows 10 TPの場合と判明。
以上をまとめると、
  • X230の左側のUSBコネクタ(USB3.0)
  • USB 3.0のUSBメモリから
  • Windows 10 TPのインストーラーを起動
しようとした場合にこのエラーが出る、ということになります。

真っ先に疑われるのはX230のUSBコネクタの劣化あるいは埃詰まりで、通常時には問題ないものがインストール時に顕在化するというのは昔からあることですが、
  • 製造から1年足らずで、2箇所のコネクタに同じ問題が出るものか。
  • USB3.0用のメス側の接点はベロの先端に、かつX230の場合は下向きに付いているので、埃は付着しにくい。
ということがあって、保留です。

一つ明らかなのは、Windows 10 TPのインストーラーはUSB3.0の認識に何か変更があったっぽい、ということですが、まあ問題に遭遇でもしない限り無駄知識ですけど。

[追記1]

コメントいただいた情報によればX1 CarbonのUSB3.0コネクタでも同じエラーが出るそうで、かつX230の右側のコネクタはUSB2.0なので、エラーが出る条件としてはUSB3.0がやはりキーのようです。

[追記2]

Microsoft CommunityのWindows Insider Programにポストしました。
[追記3]

Insider PreviewのBuild 10074のISOではこのエラーは出なくなりました(日本語版、英語版とも)。問題は解決されたようです。

2. UEFI用のUSBメモリの作成

なお、UEFIではブートに関するノウハウが従来と変わりました。パーティションがアクティブであること、専用のブートセクタであることは必要なくなりました。代わりにパーティションがFAT32であることが必須です。

したがってブータブルなUSBメモリを作成するには、とにかくFAT32でフォーマットして、ISOファイル(x64版であること)の中身をコピーするだけです。特別なツールやDiskPartを使う場面は基本的にないと思います。

この点に関して、Microsoftの高橋さんが書かれている方法のうちアクティブとブートセクタの部分は、従来の場合と両対応にするために必要なことであって、UEFIだけ考える場合は必要ないと思います。

2015/01/26

Windows 10 TPのバージョン判定(続)

Windows 10 TPのバージョン判定を新しいBuild 9926で試してみると、

マニフェストファイルにWindows 10のcompatibilityの記述あり。

同なし。

VerifyVersionInfoがGetVersion/GetVersionExと同じ呪縛にかかった模様。意図された動作かどうか、現状分からないが。

最後まで残るとすれば、個人的な予想ではWMIではないかと。WMIはシステム管理にも使われるので、アプリ対策の都合だけで変えにくいと思うので。

2015/01/11

Venue 8 Proと+port

OTGのUSBコネクタで充電とデータ通信が同時にできる+portの話。

1. 顛末

Venue 8 Proは充電とUSBを同時使用できないと書いた後、純正のDell Micro USB Dongle for Data and Chargingを国際発送してくれる店を探してみたが、これが見つからなかったので、結局Mobile Design Labsの+portをオーダーした。本体29ドルに送料10ドルを合わせて39ドル。

これはKickStarterで資金調達して製品化されたもので、動画でもVenue 8 Proでデモしているように、純正を除けば唯一の確実な選択肢、と思われた。


が、実はその数日前に発表されたBuffaloの「Android用充電機能付きUSBハブ」BSH4AMB03BK/Nの対応機種にVenue 8 Proが含まれているのを後になってから知り、さらにACASISのHO27の店頭販売が始まった

12月前半まではモバイルショップを回っても影も形もなかったのに、一気に状況が変わってきた。ということで、このタイミングで+portを買うこともなかったが、来たものは来たものとして使っていく所存。

2. 充電時間

+portはVenue 8 Proを念頭に置いて作られたものらしいから大丈夫だろうとは思ったものの、一応充電時間を比較してみた。
Venue 8 Pro and Plusport

接続方法は以下のとおり。
  1. +portのMicro USBコネクタにVenue 8 Proの付属ACアダプターからのMicro USBケーブルを接続
  2. +portのMicro USBケーブルをVenue 8 ProのMicro USBコネクタに接続
  3. +portのUSBコネクタにLogitecのUSB有線アダプターのLAN-GTJU3を接続
文字にするとややこしいが、実際はそうでもない。ちなみに、+portからACアダプターのケーブルを抜くと+portのUSBコネクタに差したデバイスとの接続が失われるので、+portの動作自体にACアダプターからの電源供給が必要な模様。

比較するのは以下の3ケース。
  • Case 1: 通常どおり付属ACアダプターからのケーブルをVenue 8 Proに直結。負荷として無線LANで接続した状態でYouTubeの動画を流し続ける。

  • Case 2: +portにACアダプターからのケーブルだけ接続した状態で、+portのケーブルをVenue 8 Proに接続し、充電が開始された後、+portのUSBコネクタに有線LANアダプターを接続。負荷として有線LANで接続した状態で(無線LANはオフ)同じくYouTubeの動画を流し続ける。

  • Case 3: +portにACアダプターからのケーブルと有線LANアダプターを両方接続した状態で、+portのケーブルをVenue 8 Proに接続。負荷はCase 2と同じ。
Case 1だけ無線LANだが、そこは仕方ない。Case 2は、これが+portの説明書で示された手順で、かつOTGケーブルには電源を先に接続して充電を開始させた後でデータ通信に切り替えてしまうというテクニックがあるので、それが関係あるか確かめるため。実用上はCase 3で済めば、それに越したことはない。

充電状態の記録には簡単なアプリを作った。
ただ、参照元のデータがリアルタイムに更新されない(時々思い出したようにジャンプする。これは通知領域の電源表示も同様)ことが分かったので、細かい動きは気にしないということで。

結果は以下のとおり。
10%から100%までの充電時間には一応差はある。
  • Case 1: 216分
  • Case 2: 239分
  • Case 3: 236分
が、グラフを見て明らかなとおり、有意な差はないと考えていいと思う。なお、Case 2の終盤が直線なのは値が大きくジャンプしただけで、細かく値が取れていれば他と同様のカーブになったと思う。

結論として、+portにACアダプターからのケーブルとUSBデバイスを両方接続した状態を固定して、+portのケーブルをVenue 8 Proに抜き差しするだけで問題ないと言えよう。

3. ガジェットとして

外装の成形や組み立ての精度は高くないが、機能が果たせればそこは問わない。
Plusport
Plusport
Plusport

ただし、このケーブルの固定方法はよろしくない。
Plusport

外装のチューブが固定されておらず、シールド線が剥き出し。これはそのうち断線するおそれが高いので、何らかの補強策が必要。

まあ壊れたらBuffaloの製品があると考えれば、既に気は楽。