2015年11月27日金曜日

ProxySwitcher ver.1.0.0

さて、先日公開しましたSSIDProxy ver.1.1.1ですが、実はこっそりプロキシ設定機能の分離を行っておりました。ProxySwitcher.dllっていう新しいDLLが付属するようになったことが勘の良い方は気づいたかもしれません。
なぜ分離したかと言いますと、まあ、プログラムのモジュール化どうこうみたいな話はとりあえず置いておいて、私以外の他人がこれを直接いじれるようにするためです。 先日、ネットサーフィンと言いますか、いわゆるエゴサをしていたところ、VB.NETでプロキシ設定をいじることに苦労されている方がいるのを見かけました。私もこちらの記事で書いたように、一生懸命調べ、レジストリを解析し、やっとC#で設定できるようにこぎつけた経緯があり、この成果を多くの方に使ってもらえるよう、レジストリのデータ構造だけでなくやっぱりライブラリとして公開しようと思い至りました。

というわけで、SSIDProxyに組み込んでいたプロキシ切り替え機能の部分を抽出し、ライブラリとしました。それがProxySwitcherです。
(ちなみに、先日お話をしましたレジストリの変更通知を吐くライブラリもこれに組み込まれています。レジストリの変更通知だけを使いたい方もこのライブラリをどうぞ)

プロキシ設定の仕組み

復習ですが、Windowsのプロキシ設定にかかわるレジストリの項目は下記のようになっています。

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
┣Connections
┃┖DefaultConnectionSettings
┠AutoConfigURL
┠ProxyEnable
┠ProxyServer
┖ProxyOverride

システムはこのうちのDefaultConnectionSettingsに格納されているバイナリデータのみを読み込み、それをプロキシ設定として扱うようになっているようです。しかし、バイナリデータだけでは人間には中身がよくわからないからか、Internett Settings直下の各値にも同じデータを格納してくれるので、これを読み込むことで現在の設定を知ることも可能です。しかし、これはあくまでもダミーで、これを書き換えたところでシステムはそれをプロキシ設定として認識されないことに注意してください。
すなわち、プロキシ設定の読み出しにはDefaultConnectionSettingsを解読するのが素直で、書き込みの時にはDefaultConnectionSettingsとProxyEnableなどの値に適切な値を書き込む必要があるといえます。

ProxySwitcherの使い方

SystemLanSettingsクラス

上記のようなプロキシ設定を行うクラスがSystemLanSettingsクラスです。このクラスは以下のプロパティを持っています。
  • AutomaticallyDetectSettings
  • UseScript
  • ScriptAddress
  • UseProxy
  • Proxy
  • ProxyOverride
これらのプロパティを読み込み、書き込みをすることでシステムのプロキシ設定を読み込み、書き込みすることができます。また、このクラスはレジストリの変更を監視し、プロキシ設定が変化したらそれを通知する機能が付いています。その通知はINotifyPropertyChangedインターフェースによるプロパティ変更通知の実装で実現されているので、変更されたプロキシ設定に対応するプロパティの変更通知として飛んできます。また、レジストリ監視はWin32APIを使用しており、終了時に各種ハンドルを解放しなければならないので、IDisposableインターフェースを実装しております。インスタンスが不必要になったときはDisposeメソッドを呼び出す必要があります
多くのプロパティはstring型かbool型なのでわかりやすいかと思います。しかし、ProxyOverrideはプロキシの不使用をするアドレスを列挙したList<string>型になっているので注意してください。ローカルアドレスにもプロキシを使用しない場合は"<local>"というテキストをそのリストに含ませる必要があります。また、ProxyプロパティはProxySettings型になっております。

ProxySettingsクラス

プロキシ設定を保持するためのクラスです。WindowsはHTTP、HTTPS、FTP、Socksの4種類のプロトコルに対して個別にプロキシサーバーを設定することができます(使ったことないですが)。なので、ProxySettingsクラスはそれに合わせて
  • HttpProxy
  • HttpsProxy
  • FtpProxy
  • SocksProxy
の4つのプロパティを持っており、それぞれの値を設定できます。
このクラスは初期化時以外では値を書き換えることができない(immutableな)設計がされています。プロキシを設定した場合は適当に値を書き換えた上で新しいインスタンスを作ってあげてください。
ちなみに、コンストラクタのオーバーロードがいくつかありますが、そのうちでもstring型のテキストを受け取るものは、レジストリのProxyServerの値をそのまま受け取ってインスタンス化できるものになっています。また、ProxySettingsのインスタンスをToStringするとそのProxyServerのレジストリ値に書き込めるタイプのフォーマットになります。
なお、このクラスで扱われるそれぞれのプロキシ設定はProxy型になっています。

Proxyクラス

これはサーバーのアドレスとポートの2つの値を保持するためだけのクラスです。ProxySettingsクラスと同様にimmutableな設計をしています。string1つを受け取るコンストラクタは「URL:ポート番号」の書式のテキストを受け付けます。

LanSettingsクラス

今まではシステムのLAN設定を読み書きするためのクラスと、そのクラスに付随したクラスの説明をしましたが、単にLAN設定を保持するためのLanSettingsクラスもあります。このクラスのプロパティを読み書きしてもシステムのLAN設定が変化することはありませんが、SystemLanSettings.ToData()やSystemLanSettings.FromData()というstaticメソッドを介してシステムのLAN設定とLanSettingsインスタンスを相互変換できます。この場合、SystemLanSettingsクラスのインスタンスを作るのに比べてレジストリ監視に伴うオーバーヘッドを減らせますし、Dispose()メソッドを呼ぶ必要も無いので扱いやすいかと思います。

サンプルコード

static void Main(string[] args)
{
    LanSettings stg = SystemLanSettings.ToData();
    Console.WriteLine(stg);

    using(var sysstg = new SystemLanSettings()) {
        sysstg.PropertyChanged += (s, e) => Console.WriteLine($"\"{e.PropertyName}\" has changed.");

        Console.WriteLine("Watching LAN Settings...");
        Console.ReadLine();
    }

    Console.WriteLine("Stop Watching.");
    Console.WriteLine("Press Enter to exit...");
    Console.ReadLine();
}

このプログラムを実行するとこんな感じになります。


最初に現在の設定を表示しております。そのあとにインターネットオプションからいくらかプロキシ設定をいじってみたところ、ちゃんとレジストリ変更を認識して画面に表示してくれています。最後にEnterを押せば監視を終了してくれます。

おまけ:レジストリ監視

このライブラリはレジストリ監視機能を含んでいます。ProxySwitcher.Registry.Watcher名前空間内のRegistryWatcherがそれです。面倒くさいんでサンプルプログラムで使い方は察してください。

static void Main(string[] args)
{
    using(var watcher = new RegistryWatcher(@"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings")) {
        watcher.ValueChanged += (s, e) => {
            foreach(var info in e.ChangeInfo)
                Console.WriteLine(info);
        };
        watcher.SubKeyChanged += (s, e) => {
            foreach(var key in e.CreatedKeyNames)
                Console.WriteLine($"\"{key}\" has created.");
            foreach(var key in e.DeletedKeyNames)
                Console.WriteLine($"\"{key}\" has deleted.");
        };

        Console.WriteLine("Watching LAN Settings...");
        Console.ReadLine();
    }

    Console.WriteLine("Stop Watching.");
    Console.WriteLine("Press Enter to exit...");
    Console.ReadLine();
}

実行結果はこんな感じになります。


このクラスでは指定したキーの直下にある値の変更、指定したキーの直下へのキーの作成、削除を認識できます。これをうまく使って上記のSystemLanSettingsクラスはプロキシ設定の変更通知を行っています。ちゃんとそのキー下の値やキー名を記憶しておいて、変更があった時に素の差分を通知してくれるようになっています。

免責事項

このソフトウェアを使用することによって発生したいかなるトラブル(データ喪失、パソコン故障、データ流出等)についても、作成者は責任を負わないものといたします。特に、このソフトウェアは独自研究によるレジストリ編集を行っているため、その方法が完全ではなかったり、Windowsのバージョン・システム構成等の環境によって正常に動作しない可能性があります。場合によっては重大なトラブルを引き起こす可能性がありますので、そのようなリスクをご承知の上、ご利用ください。
なお、このソフトウェアを起動した時点で、免責事項を読んだかどうかにかかわらず、これらの免責事項に同意したものとみなします。
テンプレですが、どうぞご注意、ご理解くださいませ。

ライセンス

さて、このライブラリのライセンスをどうしようか考えましたが、SSIDProxyの開発に伴って作ったものということもあり、基本的にはSSIDProxyに準じ、それ以外に下記の条項を付け加えています。
  • このライブラリを改変してはならない。
  • 有償アプリケーションには使用してはならない。
  • 使用方法やバグに関するサポートをする責任を作者は負わない。
  • 完成したソフトウェアのどこか(ヘルプ、バージョン情報など)と、ReadMeなどのドキュメンテーションに私のライブラリを使用したことを明記すること。ただし、作者がこのライブラリを使用するときはその限りではない。
  • このライブラリを利用したアプリケーションをユーザーが配布するときに限って、作者に無断で再配布できるものとする。ただし、必要最小限のファイルに留めること。
今時クローズドソースも流行らないかもしれませんが、まあ、とりあえず現状はこのような形で。ニーズが出てきたら適宜ライセンスを変更してオープンソースにするかもしれません。

ダウンロード

SSIDProxyをダウンロードしてください。
SSIDProxy ver.1.1.1
そのうちの 
  • bin\ProxySwitcher.dll
  • bin\ja-JP\ProxySwitcher.resources.dll
の2つがProxySwitcherで必要なファイルです。プロジェクトからProxySwitcher.dllを参照をして使用してください。

2015年11月26日木曜日

SSIDProxy ver.1.1.1

さて、前回に引き続きSSIDProxyを更新いたしました。いちいち概要や使い方を何度も書くのもアレなので、初めてこのソフトについて知った方は前回の記事を読んでください。今回の更新は主にディテールのリファインやリファクタリングですが、表に出てくるような変更点としては下記のとおりです。

今回の変更

【新機能】
    • 他のアプリケーションによるプロキシ設定の変更を検知できるようになった。
    【機能変更】
    • 翻訳の改善
    • ポップアップ、タスクトレイアイコンのツールチップのメッセージを微妙に変更
    【バグ修正】
    • 一部環境でLAN設定の読み込み時に不正終了するバグの修正
    • 一部レジストリ設定の挙動模倣が不十分だったので修正
    • 自動設定用スクリプトの設定機能が不十分だったのを修正
    • ManagedNativeWifiはManaged Wifi APIの派生プロジェクトではありませんでした。バージョン情報等の謝辞を一部変更しています。
    まず、今回のバージョンからレジストリを監視し、他のアプリケーション(もちろんインターネットオプションからの変更を含む)によるプロキシの変更を検知し、ポップアップでお知らせできるようにしました。
    他のアプリケーションによってルールに違反した設定がされたとき、すぐさま元に戻すような機能を実装しよかと一瞬考えましたが、そのプロキシを設定した側のソフトで意図しない動作が起こる可能性や、もしくはSSIDProxyと同様のソフトが同時に動作していた場合、プロキシの設定の変更合戦になってしまうことを考えて、通知に留めました。SSIDProxyの保持しているルールに則った設定に戻したい場合は、アイコンを右クリックして今すぐルールを適用すればおkです。

    なお、上の箇条書きのところには書いておりませんが、内部的にルールの保存データのフォーマットが若干変更になっています。ver.1.1.0からのコンバーターは内蔵しているので、上書きインストールをすれば特に気にすることなく新データに移行してくれますが、多分次のバージョンでそのコンバーターが削除されるので、ver.1.1.1を介さずにver.1.1.0のデータを引き継ぐことはできなくなるかと思います。

    免責事項

    このソフトウェアを使用することによって発生したいかなるトラブル(データ喪失、パソコン故障、データ流出等)についても、作成者は責任を負わないものといたします。特に、このソフトウェアは独自研究によるレジストリ編集を行っているため、その方法が完全ではなかったり、Windowsのバージョン・システム構成等の環境によって正常に動作しない可能性があります。場合によっては重大なトラブルを引き起こす可能性がありますので、そのようなリスクをご承知の上、ご利用ください。
    なお、このソフトウェアを起動した時点で、免責事項を読んだかどうかにかかわらず、これらの免責事項に同意したものとみなします。
    テンプレですが、どうぞご注意、ご理解くださいませ。

    ダウンロード

    SSIDProxy ver.1.1.1

    2015年11月18日水曜日

    ListViewItemのイベントをMVVMスタイルのプログラムで受信する

    WPFでListViewを使うときというのは、たいてい、複数のプロパティを持つようなデータが複数あるときですよね。それゆえに、たいていItemsSourceに各々のデータに対応するViewModelをバインディングして、ListView.ViewにDisplayMemberBindingなどを設定したGridViewColumnを設定したGridViewを設定して使うことになると思います。
    内部的には、そのデータに対応するViewはListViewItemになっていて、ItemsSourceで指定したVMがそのDataContextに指定されています。そして、ListViewItemのプロパティならば、ListView.ItemContainerStyleを使ってスタイルを指定することでいじることができます。例えばこんな感じです。

    <ListView ItemsSource="{Binding Data}" >
        <!-- 中略 -->
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem" >
                <Setter Property="ContextMenu" >
                    <Setter.Value>
                        <ContextMenu>
                            <MenuItem Header="Open" Command="{Binding OpenCommand}" />
                            <MenuItem Header="Close" Command="{Binding CloseCommand}" />
                        </ContextMenu>
                    </Setter.Value>                            
                </Setter>
            </Style>
        </ListView.ItemContainerStyle>
    </ListView>
    

    このXAMLは、リストビューで表示している各項目に対してコンテキストメニューを表示させるようにしています。このスタイルはListViewItemに対して適用されているので、メニューにバインディングしたコマンドなど(上記の例ではOpenCommandやCloseCommand)は各アイテムのViewModelにバインドされます(上記の例ではData)。非常に素直な設計だと思います。データに対する操作を各データのVMが受け取るわけですからね。
    このほかにもListView.ContextMenuのほうにコンテキストメニューを登録し、SelectedItemなどをうまいこと使ってデータ処理をすることもできますが、この場合はヘッダーを右クリックしてもメニューが出てきてしまう点に問題があります(ListViewコントロール全域に対してコンテストメニューが登録されているわけですからね)。その観点からも、やはり上記のような実装が素直だと言えます。

    さて、ここまでは非常に王道なのですが、ListViewItemが発行するイベントを受信しようとすると、とたんに問題が難しくなります。
    前述の通り、ListViewItemは直接いじれないので、スタイルを介してプロパティ等をいじることになります。そして、プロパティと同様にEventSetterというセッターがあり、これを通すことでイベントの受信も可能になります。
    しかし、EventSetterは、そのイベントをコードビハインドに用意したイベントハンドラに飛ばします。コードビハインドで受信をしてしまうと、それをViewModelにきれいに飛ばすのがなかなか難しくなります。コードビハインドでDataContextを適当なViewModel型にキャストしメソッドを呼び出せば確かにViewからViewModelにイベントの発生を伝えることができますが、XAMLで表現したバインディング以外にデータの流れる経路ができてしまうことには非常に抵抗感があります。
    もちろんですが、スタイルにはi:Interaction.TriggersにEventTriggerを入れて使う、なんてこともできません。それができれば何も苦労はしないんですがねえ。

    というわけで、今回はいろいろ試行錯誤した結果、いかにListViewItemで発生したイベントを受信し、それをViewModelに伝えるかということを綴っていきたいと思います。

    結論から言いますが、ReactivePropertyを使いました。別にReactivePropertyがこれ用の機能を持っているというわけでもないんですが、一番使いやすかったので使いました。まずは、XAMLのほうから紹介します。

    <ListView ItemsSource="{Binding Data}" >
        <ListView.View>
            <GridView>
                <!-- 中略 -->
            </GridView>
        </ListView.View>
        <ListView.ItemContainerStyle>
            <Style TargetType="ListViewItem" >
                <!-- 中略 -->
            </Style>
        </ListView.ItemContainerStyle>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="MouseDoubleClick">
                <rp:EventToReactiveProperty ReactiveProperty="{Binding ListViewDoubleClicked}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </ListView>
    

    ListViewのほうにEventTriggerを指定し、ダブルクリックのイベントを受信しています。EventTriggerは多くのMVVMスタイルのXAMLで見かけるものなので特段説明はいらないかと思います。この時点では、たとえばヘッダー領域などを含んだ、ListView全体のダブルクリックを拾ってしまう点に注意する必要があります。
    さて、EventTriggerで指定したEventToReactivePropertyですが、これがReactivePropertyでサポートしている機能の1つで、イベントを直接ViewModelのReactiveProperty(IObservable<T>とINotifyPropertyChangedを実装したプロパティ)に飛ばすことができます。飛んでくるデータソースはRoutedEventArgs(もちろんイベントによってはそれを継承したEventArgsだったりする)で、ViewModelにEventArgsの処理を書くのは行儀が悪いので、作者の方のブログではReactiveConverter<T, U>を継承したクラスを作り、イベントをより実用的な型に変換しておりました。
    しかし、どうもReactiveConverter<T, U>を挟むと、イベントの送り主がAssosiateObject (この場合はListView)になってしまうようです。挟まなかった場合、ListViewItemのさらにいくつか子要素(TextBlockだったり、Borderだったりします)が送り主になるようです。ListViewItemの子要素ならば、DataContextはListViewItemのものになるはずですから、これをいいことに、DataContextに関連付けられているViewModelにダブルクリックのイベントを伝えることにしました。

    なお、上記のXAMLではちゃんと{Binding ListViewDoubleClicked}という形でイベントの受け取り先のReactivePropertyがバインディングされており、コードビハインドとViewModelの間につながりを持たせるということは行わずに済んでいます。

    public MainWindowViewModel()
    {
        ListViewDoubleClicked
            .Select(p => new { EventArgs = p, ViewModel = (p?.Source as System.Windows.FrameworkElement)?.DataContext as DataElementViewModel })
            .Where(p => p.ViewModel != null)
            .Do(p => p.EventArgs.Handled = true)    //HandledをtrueにするためにDoを挟む
            .Select(p => p.ViewModel)
            .Subscribe(p => p.DoubleClicked());
    }
    
    public ReactiveProperty<System.Windows.Input.MouseButtonEventArgs> ListViewDoubleClicked { get; } = new ReactiveProperty<System.Windows.Input.MouseButtonEventArgs>();
    

    つづいてViewModelの一部です。上記の通り、素のままではListViewが発行したすべてのダブルクリックイベントを受信してしまうので、Reactive Extensionsを使ってそれをフィルタリングしています。Reactive ExtensionsはLINQを使って時間的に流れてくるデータをフィルタリングや変換できる、とても強力なライブラリです。
    まずはSelectで受信したRoutedEventArgsと、そのViewModelを合成した匿名クラスを作っています。null条件演算子をふんだんに使い、指定のViewModelの型にできなかったときはnullになるように作っています。そして、次にWhereでそのnullになったものを弾き、生き残ったものに対してDoを挟んでEventArgsのHandledをtrueにする作業をしています。最後にViewModelをSelectし、SubscribeでViewModelにダブルクリックが行われたときに呼ぶべきメソッドを呼ぶようにしています。

    ViewModelでWPFのRoutedEventの処理をやっている時点で気持ち悪いって言えば気持ち悪いですが、うーん、コードビハインドからViewModelのメソッドを呼ぶのと比べてどっちが気持ち悪いんだろうな…。

    2015年11月16日月曜日

    C#でレジストリの変化を通知するイベントを作る

    レジストリの変化の通知を受け取りたいことがあると思います。C#にはRegistryKeyクラスなど、レジストリの読み書きや列挙等をするのに十分なクラスがありますが、変更通知だけは対応していません。これを行うにはWin32APIを直接叩く必要があります。

    基本的な実装 - Win32APIのラッピング

    さて、それっぽい関数をWin32APIから探すと、RegNotifyChangeKeyValue関数が見つかります。そして、その下のほうにC言語のサンプルコードが書いてあります。
    インデントが崩れていたり、コマンドライン引数のパース作業とかがあって微妙に読みにくかったりしますが、おおむね次の手順です。
    1. RegOpenKeyEx関数を使って変更通知許可を付加してレジストリキーをオープン
    2. CreateEvent関数を使ってイベントを作成
    3. RegNotifyChangeKeyValue関数を使ってレジストリの監視を開始
    4. WaitForSingleObject関数を使ってレジストリが変更されるまで待つ
    5. 変更監視が終わったらRegCloseKey関数CloseHandle関数を使ってリソースを解放する
    さて、レジストリを開いて、最終的に閉じる、イベントを作って最終的に閉じる、というのはいいかと思います。Win32APIでは、Create***()などの名前の関数はインスタンスをヒープ領域に確保しておりますので、後々にそのハンドルを使ってインスタンスを解放してやる必要があります。もちろんガベージコレクタなんてないですからね。その原則にのっとって、レジストリキーのハンドルと、イベントハンドルを最後に解放しております。

    ここで問題になるのが「イベント」と呼ばれるものです。これは何なのでしょう。C#のイベント機能とは違う感じですよね。この辺を詳しく説明しようとするとWin32APIのマルチスレッドの話になってすごく泥沼になってしまいますが、まあ、簡単に言うと、シグナル状態と非シグナル状態を手動で切り替えられる「オブジェクト」の一種ですね。オブジェクトと言うとなんかとても表現があいまいになってしましますが、ここで言うオブジェクトとは、スレッドやプロセス、ミューテックスやイベントなどを包含する概念です(すなわち、イベントとはスレッドやプロセスなどと並列する概念と言えます)。このオブジェクトにはシグナル状態と非シグナル状態というものがあり、例えばスレッドはスレッドが動作している状態が非シグナル状態、スレッドが終了したらシグナル状態に切り替わります。プロセスも動作中はシグナル状態で終了したら非シグナル状態になります。この非シグナル状態からシグナル状態に切り替わるのを待つことでで、複数のスレッドの同期を取ることができるようになるわけですね。そして、そのようにオブジェクトがシグナル状態から非シグナル状態に変化するのを待機するための関数がWaitForSingleObject関数なわけです。
    ここまで来るとだいたい予想が付くと思いますが、RegNotifyChangeKeyValue関数は、レジストリの変化が起きたらイベントを非シグナル状態に切り替えてくれる関数というわけですね。そして、オブジェクトが非シグナル状態になるまで制御を返さない関数がWaitForSingleObject関数というわけです。

    ここまで読むとわかるかと思いますが、レジストリが変化をするまで制御を返してくれなくなるので、このあたりの処理は別スレッドでやる必要があります。そして、監視を終了したいときにWaitForSingleObjectから制御を強制的に戻してやらなければならないという問題も生まれますね。
    これに関しては、もう1つ別のイベントを作ることで解決できます。WaitForMultipleObjects関数というものがあり、これを使えば複数のオブジェクトを同時に監視できます。この関数は第3引数に待機オプションを指定することができ、TRUEを指定するとすべてのオブジェクトがシグナル状態になったときに制御を返し、FALSEを指定するといずれか1つのオブジェクトがシグナル状態になったときに制御を返します。スレッドの同期などには前者が使えそうですが、今回は後者を使えば、レジストリの変化が発生していないときでも手動でもう一方のイベントをシグナル状態にすることで関数から制御を返してもらい監視を終了することができます。
    ちなみに、イベントを手動でシグナル状態にするにはSetEvent関数を使えばおkです。

    さて、ここで大体方針が定まりました。 このようにやっていこうと思います。
    1. RegOpenKeyEx関数を使って変更通知許可を付加してレジストリキーをオープン
    2. CreateEvent関数を使ってレジストリ監視用と終了用のイベントを作成
    3. RegNotifyChangeKeyValue関数を使ってレジストリの監視を開始
    4. 別スレッドからWaitForMultipleObjects関数を使ってレジストリが変更されるまで待つ
    5. レジストリが変更されたら適当に(C#の)イベントを発生させ、3.に戻って監視を再び続ける
    6. 変更監視を終えるときは、終了用のイベントを発生させ、スレッドが終了したのを確認したらRegCloseKey関数やCloseHandle関数を使ってリソースを解放する
    という流れになります。

    ここからが地獄のプログラミングのスタートです。C#でWin32APIを叩きまくるのは、何度やってもどうにも好きになれませんが、やらないわけにはいかないのでやっていきます…。

    void StartWatching(IntPtr RootKey, string subkey)
    {
        // キーを開く。
        int ErrorCode = Win32.RegOpenKeyEx(RootKey, subkey, 0, Win32.REGSAM.KEY_NOTIFY, out SubKey);
        if(ErrorCode != Win32.ERROR_SUCCESS)
            throw new Win32Exception(ErrorCode, nameof(Win32.RegOpenKeyEx));
    
        // イベントを作成する。
        RegEvent = Win32.CreateEvent(IntPtr.Zero, true, false, null);
        if(RegEvent == IntPtr.Zero)
            throw new Win32Exception(nameof(Win32.CreateEvent));
    
        // イベントを作成する。
        TerminateEvent = Win32.CreateEvent(IntPtr.Zero, true, false, null);
        if(TerminateEvent == IntPtr.Zero)
            throw new Win32Exception(nameof(Win32.CreateEvent));
    
        Win32.REG_NOTIFY_CHANGE Filter =
            Win32.REG_NOTIFY_CHANGE.NAME |
            Win32.REG_NOTIFY_CHANGE.ATTRIBUTES |
            Win32.REG_NOTIFY_CHANGE.LAST_SET |
            Win32.REG_NOTIFY_CHANGE.SECURITY;
    
        var events = new List<IntPtr>(new[] { RegEvent, TerminateEvent });
        var terminateIndex = events.IndexOf(TerminateEvent);
    
        WatchingTask = Task.Run(() => {
            while(true) {
                // レジストリエントリが変更されるかどうか、レジストリキーを監視する。
                ErrorCode = Win32.RegNotifyChangeKeyValue(SubKey, true, Filter, RegEvent, true);
                if(ErrorCode != Win32.ERROR_SUCCESS)
                    throw new Win32Exception(ErrorCode, nameof(Win32.RegNotifyChangeKeyValue));
    
                var waitret = Win32.WaitForMultipleObjects(events, false, Win32.INFINITE);
                if(waitret == Win32.WAIT_FAILED)
                    throw new Win32Exception(nameof(Win32.WaitForMultipleObjects));
    
                //イベントの発生が終了イベント起因だったらループ終了
                if(waitret == (Win32.WAIT_OBJECT_0 + terminateIndex))
                    break;
    
                RaiseRegistryChangedEvent();
            }
        });
    }
    

    このような形でどうでしょうか。DllImportした関数や、その他大勢の値などはWin32というクラスを作ってまとめてあります。

    internal static class Win32
    {
        [DllImport("advapi32.dll")]
        internal static extern int RegOpenKeyEx(IntPtr hKey, string lpSubKey, uint ulOptions, REGSAM samDesired, out IntPtr phkResult);
    
        [DllImport("advapi32.dll")]
        internal static extern int RegNotifyChangeKeyValue(IntPtr hKey, bool bWatchSubtree, REG_NOTIFY_CHANGE dwNotifyFilter, IntPtr hEvent, bool fAsynchronous);
    
        [DllImport("advapi32.dll")]
        internal static extern int RegCloseKey(IntPtr hKey);
    
        [DllImport("kernel32.dll")]
        internal static extern IntPtr CreateEvent(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName);
    
        [DllImport("kernel32.dll")]
        internal static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
    
        [DllImport("kernel32.dll", EntryPoint = "WaitForMultipleObjects")]
        private static extern uint _WaitForMultipleObjects(uint nCount, IntPtr lpHandles, bool fWaitAll, uint dwMilliseconds);
    
        [DllImport("kernel32.dll")]
        internal static extern bool SetEvent(IntPtr hEvent);
    
        [DllImport("kernel32.dll")]
        internal static extern bool ResetEvent(IntPtr hEvent);
    
        [DllImport("kernel32.dll")]
        internal static extern bool CloseHandle(IntPtr hObject);
    
        internal static uint WaitForMultipleObjects(IEnumerable<IntPtr> lpHandles, bool fWaitAll, uint dwMilliseconds)
        {
            IntPtr[] array = lpHandles.ToArray();
    
            // アンマネージ配列のメモリを確保
            IntPtr ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(IntPtr)) * array.Length);
            try {
                Marshal.Copy(array, 0, ptr, array.Length);
    
                return _WaitForMultipleObjects((uint)array.Length, ptr, fWaitAll, dwMilliseconds);
            }
            finally {
                // アンマネージ配列のメモリを解放
                Marshal.FreeCoTaskMem(ptr);
            }
        }
    
        [Flags]
        internal enum REGSAM : uint
        {
            KEY_ALL_ACCESS = 0xF003F,
            KEY_CREATE_LINK = 0x0020,
            KEY_CREATE_SUB_KEY = 0x0004,
            KEY_ENUMERATE_SUB_KEYS = 0x0008,
            KEY_EXECUTE = 0x20019,
            KEY_NOTIFY = 0x0010,
            KEY_QUERY_VALUE = 0x0001,
            KEY_READ = 0x20019,
            KEY_SET_VALUE = 0x0002,
            KEY_WRITE = 0x20006,
        }
    
        [Flags]
        internal enum REG_NOTIFY_CHANGE : uint
        {
            NAME = 1,
            ATTRIBUTES = 2,
            LAST_SET = 4,
            SECURITY = 8,
        }
    
        internal static readonly IntPtr HKEY_CLASSES_ROOT = new IntPtr(unchecked((int)0x80000000));
        internal static readonly IntPtr HKEY_CURRENT_USER = new IntPtr(unchecked((int)0x80000001));
        internal static readonly IntPtr HKEY_LOCAL_MACHINE = new IntPtr(unchecked((int)0x80000002));
        internal static readonly IntPtr HKEY_USERS = new IntPtr(unchecked((int)0x80000003));
        internal static readonly IntPtr HKEY_PERFORMANCE_DATA = new IntPtr(unchecked((int)0x80000004));
        internal static readonly IntPtr HKEY_PERFORMANCE_TEXT = new IntPtr(unchecked((int)0x80000050));
        internal static readonly IntPtr HKEY_PERFORMANCE_NLSTEXT = new IntPtr(unchecked((int)0x80000060));
        internal static readonly IntPtr HKEY_CURRENT_CONFIG = new IntPtr(unchecked((int)0x80000005));
        internal static readonly IntPtr HKEY_DYN_DATA = new IntPtr(unchecked((int)0x80000006));
        internal static readonly IntPtr HKEY_CURRENT_USER_LOCAL_SETTINGS = new IntPtr(unchecked((int)0x80000007));
    
        internal const int ERROR_SUCCESS = 0;
    
        internal const uint INFINITE = 0xFFFFFFFF;  // Infinite timeout
        
        internal const uint WAIT_TIMEOUT = 0x00000102;
        internal const uint WAIT_FAILED = 0xFFFFFFFF;
        internal const uint WAIT_OBJECT_0 = 0;
        internal const uint WAIT_ABANDONED_0 = 128;
    }
    

    WaitForMultipleObjects関数はオブジェクトのハンドルを並べた配列へのポインタを受け取りますが、C#などのマネージドな環境では仮想マシンによってメモリ上の配置が任意に変えられてしまいますので、一旦アンマネージ配列を確保してそっちにコピーしてから渡すようラッピングしています。それ以外の関数は基本的にそのままにしています(一部、フラグを組み合わせる引数はenumにしています)。

    終了処理はこのような形になります。

    bool disposed = false;
    
    /// <summary>
    /// リソースを破棄する
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    /// <summary>
    /// リソースを破棄する
    /// </summary>
    /// <param name="disposing">Dispose()メソッド中か</param>
    protected virtual void Dispose(bool disposing)
    {
        if(!disposed) {
            if(disposing) {
                // Free other state (managed objects).
            }
    
            // Free other state (unmanaged objects).
    
            //終了イベントを立てる
            if(!Win32.SetEvent(TerminateEvent))
                throw new Win32Exception(nameof(Win32.SetEvent));
    
            //終了待ち
            WatchingTask.Wait();
    
            // キーを閉じる。
            int ErrorCode = Win32.RegCloseKey(SubKey);
            if(ErrorCode != Win32.ERROR_SUCCESS)
                throw new Win32Exception(ErrorCode, nameof(Win32.RegCloseKey));
    
            // ハンドルを閉じる。
            if(!Win32.CloseHandle(RegEvent))
                throw new Win32Exception(nameof(Win32.CloseHandle));
    
            // ハンドルを閉じる。
            if(!Win32.CloseHandle(TerminateEvent))
                throw new Win32Exception(nameof(Win32.CloseHandle));
    
            disposed = true;
        }
    }
    
    /// <summary>
    /// デストラクタ
    /// </summary>
    ~RegistryWatcher()
    {
        Dispose(false);
    }
    

    いたって普通のDisposeパターンです。Disposeメソッドを呼び出してリソースを解放するのが基本ですが、やり忘れたときはデストラクタが呼ばれたときにアンマネージドリソースの解放を行うことでとりあえずしっかりと解放するようにしています。

    レジストリの変化の種類を調べる

    さて、RegNotifyChangeKeyValue関数ですが、上記で説明した通り、「変化が発生したらイベントをシグナル状態にする」という動作のみをします。そのため、どのキーや値がどう変化したかなどの情報は一切よこしてくれません。それではいささか不便なので、レジストリがどう変化したのかを調べ、それをEventArgsに含めようと思います。
    それをするためには、事前にレジストリを読み込んで保存しておき、変更通知が発生したときにもう一度読み込んで比較し、通知すれば良いという話になりますね。これはC#で実装できそうです。

    string[] SubKeyNames;
    Dictionary<string, object> Values;
    
    void LoadNowRegistryState()
    {
        SubKeyNames = TargetKey.GetSubKeyNames().OrderBy(p => p).ToArray();
        Values = new Dictionary<string, object>();
        foreach(var name in TargetKey.GetValueNames().OrderBy(p => p))
            Values[name] = TargetKey.GetValue(name);
    }
    
    private void RaiseRegistryChangedEvent()
    {
        string[] OldSubKeyNames = SubKeyNames;
        Dictionary<string, object> OldValues = Values;
    
        LoadNowRegistryState();
        
        //サブキーの変化を見る
        var CreatedKeys = SubKeyNames.Except(OldSubKeyNames);
        var DeletedKeys = OldSubKeyNames.Except(SubKeyNames);
    
        if(SubKeyChanged != null && (CreatedKeys.Any() || DeletedKeys.Any()))
            SubKeyChanged(this, new RegistrySubKeyChangedEventArgs(CreatedKeys, DeletedKeys));
    
        //値の変化を見る
        var CreatedValues = Values.Keys.Except(OldValues.Keys)
            .Select(p => new RegistryValueChangedInfo(p, RegistryValueChangeType.Created, null, Values[p]));
        var DeletedValues = OldValues.Keys.Except(Values.Keys)
            .Select(p => new RegistryValueChangedInfo(p, RegistryValueChangeType.Deleted, OldValues[p], null));
        var ChangedValues = OldValues.Keys.Intersect(Values.Keys)
            .Select(p => new { ValueName = p, Old = OldValues[p], New = Values[p] })
            .Where(p => p.Old is System.Collections.IEnumerable && p.New is System.Collections.IEnumerable ?  //列挙可能?
                    !((System.Collections.IEnumerable)p.Old).Cast<object>().SequenceEqual(((System.Collections.IEnumerable)p.New).Cast<object>()) :
                    !object.Equals(p.Old, p.New))
            .Select(p => new RegistryValueChangedInfo(p.ValueName, RegistryValueChangeType.Changed, p.Old, p.New));
    
        var MergedValues = CreatedValues.Concat(DeletedValues).Concat(ChangedValues).OrderBy(p => p.ValueName).ToArray();
    
        if(ValueChanged != null && MergedValues.Any())
            ValueChanged(this, new RegistryValueChangedEventArgs(MergedValues));
    }
    
    /// <summary>
    /// サブキーが変化した
    /// </summary>
    public event EventHandler<RegistrySubKeyChangedEventArgs> SubKeyChanged;
    
    /// <summary>
    /// 値が変化した
    /// </summary>
    public event EventHandler<RegistryValueChangedEventArgs> ValueChanged;
    

    このように、LoadNowRegistryStateメソッドで現在のレジストリのサブキー名および値を保存しておき、変化通知イベントがあるたびにそれを確認してどこが変わったかを見ています。このときの「値の変化があったかどうか」ということに関しては多くのトラップがあって、処理がとても面倒になります。
    1つ目のトラップは配列のEquals()メソッドは参照の等価性しか見ていないということでした。新旧2つの値はどのような型の値でもobject型の状態で保管してあるので、各クラスがオーバーライドしているはずのObject.Equals()メソッドを呼び出して値が等しいかを判定しようとしました。配列以外はそれでもいいのですが、値が配列だった場合(レジストリの値が「バイナリ値」だった場合byte[]型になります)、配列のEquals()メソッドは中身が同じでもインスタンスが違えばfalseを返してくるため意図した動作はしません。なので、IEnumerableを継承していたらLINQのSequenceEqualメソッドを使って配列の等価性を調べるという条件分けが必要になってきます(string型もIEnumerableを継承しているのでSequenceEqualを呼び出すことになりますが、まあ大きな問題では無いでしょう)
    そこで現れるのが2つ目のトラップで、.NETは値型の共変性、反変性が無いということでした。 非ジェネリックスのIEnumerable時代遅れなのでもう使いたくないんですがSequenceEqualメソッドでは使えない(定義されていない)のですが、byte[]は値型の配列なのでIEnumerableにキャストできてもIEnumerable<object>にはキャストできません。この辺りで一度はまってしまいました。結局、いったんIEnumerableにキャストしてからCast<object>()メソッドを呼び出してIEnumerable<object>に変換することで解決しています。冒頭にusing System.Collectionsを書かないようにするために名前空間付きでをIEnumerableを書いたので結構横長になってしまいました。

    まあ、そんな感じで以前に保存していた値との比較をし、値が変化すればその内容を報告するようなイベントを作り、呼び出すようにしました。


    このような形で、C#からのレジストリ監視が可能になりました。めでたしめでたし。
    えっ、このライブラリは公開してくれないのかって?うーん、まあそのうちね( ◠‿◠ )

    2015年11月12日木曜日

    WPFでタスクトレイ常駐型アプリを作る

    「Windowsでデスクトップアプリを作るならWPF」

    そんなWPFでも、弱点が無いわけではありません。その弱点の1つとして、タスクトレイアイコンの非対応が挙げられます。なので、何かしら、WPFとは違うフレームワークを使ってタスクトレイへのアイコン表示をやらなければならないわけですね。
    例えば、Windows Formsを使って表示する方法なんかがその代表例でしょう。 ただ、WPFアプリケーションにSystem.Windows.Formsの参照を追加することへの抵抗感や、WPFとWinFormsを相互運用する詳しい知識などが必要で、なかなかハードルの高いものがあります。

    そこで、それをラッピングしたライブラリが世の中にはあります。

    WPF NotifyIcon

    なかなか完成度の高いライブラリです。NuGetからインストールすることもできます。早速これを使ってみましょう。

    NotifyIconのXAMLデザイニング

    NuGetからWPF NotifyIconをダウンロードしてインストールしたら、早速タスクバーアイコンを表示してみましょう。もちろんXAMLからアイコンを表示させることができます。

    <Window x:Class="WpfNotifyIconTest.Views.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
            xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
            xmlns:v="clr-namespace:WpfNotifyIconTest.Views"
            xmlns:vm="clr-namespace:WpfNotifyIconTest.ViewModels"
            xmlns:tb="http://www.hardcodet.net/taskbar"
            Title="MainWindow" Height="350" Width="525">
        
        <Window.DataContext>
            <vm:MainWindowViewModel/>
        </Window.DataContext>
        
        <i:Interaction.Triggers>
        
            <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
            <i:EventTrigger EventName="ContentRendered">
                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
            </i:EventTrigger>
    
            <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
            <i:EventTrigger EventName="Closed">
                <l:DataContextDisposeAction/>
            </i:EventTrigger>
    
        </i:Interaction.Triggers>
    
        <tb:TaskbarIcon IconSource="/WpfNotifyIconTest;component/Images/icon.ico" />
    </Window>
    

    Livetを使っているので、プロジェクトテンプレートからi:Interaction.Triggers要素の付いたXAMLが勝手に出来上がってきています。
    まずは名前空間を指定します。xmlns:tb="http://www.hardcodet.net/taskbar"とでも置いておきましょう。そして、WindowのContentにTaskbarIconコントロールのインスタンスを生成しています。これだけでタスクバーにアイコンが現れます。アイコンは適当なアイコンを作って、そのパスを指定してあげています。
    今回作ったのはこの「あ」のアイコンです。どんどんアレンジしていきましょう。

    ツールチップを表示する

    <tb:TaskbarIcon IconSource="/WpfNotifyIconTest;component/Images/icon.ico" ToolTipText="「あ!」" />
    

    ToolTipTextプロパティはツールチップのテキストに対応しています。マウスをかざすと文字が出てきます。
    もちろん依存関係プロパティになっているのでバインディングすることも可能です。ちなみに、改行もちゃんとできます。素晴らしいです。

    WPF製のツールチップ

    WPF NotifyIconの威力はこんなもんじゃありません。  実は、ToolTipTextプロパティとは別にTrayToolTipプロパティがあって、ここには任意のUIElementが入れられます。

    <tb:TaskbarIcon IconSource="/WpfNotifyIconTest;component/Images/icon.ico" >
        <tb:TaskbarIcon.TrayToolTip>
            <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="1" >
                <Border.Background>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                        <GradientStop Color="White" Offset="0" />
                        <GradientStop Color="LightGray" Offset="1" />
                    </LinearGradientBrush>
                </Border.Background>
                <Border.BitmapEffect>
                    <DropShadowBitmapEffect Color="Gray" ShadowDepth="4" Softness="0.3"/>
                </Border.BitmapEffect>
                <TextBlock Text="あ!" FontSize="30" Margin="2,2,2,2" />
            </Border>
        </tb:TaskbarIcon.TrayToolTip>
    </tb:TaskbarIcon>
    

    自前でいい感じにツールチップっぽい枠を作ってテキストを表示しています。
    このように、巨大なツールチップがいとも簡単に完成してしまいました。もちろん今回はテキストしか表示しませんでしたが、Imageタグ等を使って画像を表示したりすることも容易いです。非WPFのタスクトレイアイコンにここまでWPFらしい機能が取り入れられているWPF NotifyIcon、すごいです。

    コンテキストメニュー

    さて、一通りツールチップをいじり倒した後で、次はコンテキストメニューです。右クリックで出てくるメニューです。

    <tb:TaskbarIcon IconSource="/WpfNotifyIconTest;component/Images/icon.ico" DoubleClickCommand="{Binding ConfigurationCommand}" >
        <tb:TaskbarIcon.TrayToolTip>
            <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="1" >
                <Border.Background>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                        <GradientStop Color="White" Offset="0" />
                        <GradientStop Color="LightGray" Offset="1" />
                    </LinearGradientBrush>
                </Border.Background>
                <Border.BitmapEffect>
                    <DropShadowBitmapEffect Color="Gray" ShadowDepth="4" Softness="0.3"/>
                </Border.BitmapEffect>
                <TextBlock Text="あ!" FontSize="30" Margin="2,2,2,2" />
            </Border>
        </tb:TaskbarIcon.TrayToolTip>
        <tb:TaskbarIcon.ContextMenu>
            <ContextMenu>
                <MenuItem Header="_Configuration" FontWeight="Bold" Command="{Binding ConfigurationCommand}" />
                <MenuItem Header="_Exit" Command="{Binding ExitCommand}" />
            </ContextMenu>
        </tb:TaskbarIcon.ContextMenu>
    </tb:TaskbarIcon>
    


    TaskbarIcon.ContextMenuにContextMenu要素を入れてあげれば大丈夫です。今回は、設定ウィンドウを出すConfigurationメニューを太字にして、ダブルクリックでこれが起動できるということを示してみました。TaskbarIconはダブルクリック時にDoubleClickCommandを実行してくれます。
    このあたりで、一通りのXAMLでできることは完成ですかね。

    バルーンチップを出す

    さて、タスクトレイアイコンから吹き出しが出て通知する機能をよく見たことがあると思います。
    これはWindows8.1でのバルーンチップです。Windows10からはこのように横から四角いのが出てくる形になっております。
    さて、これをWPF NotifyIconで出すこと自体はとても簡単です。TaskbarIconのShowBalloonTipメソッドを呼び出すだけです。すなわち、コードビハインドに全部書いてしまえば楽なのですが、 MVVMの体裁を維持したままコントロールのメソッドを呼び出すのはなかなか大変です。MVVMでやるにはMessengerパターンでViewModelからバルーンチップを出せるようにするのがよいでしょう。
    私は普段からWPFのMVVMライブラリとしてLivetを使っておりますので、LivetでMessengerパターンを実装する方法を説明していきます。他のライブラリを使っている人は適宜そちらに置き換えて考えてみてください。

    Livetでは、ViewModelクラスにMessengerプロパティがあります。これのRaiseメソッドにInteractionMessageクラスのインスタンスを引き渡すことでViewにメッセージを送れます。
    Livetには例えばメッセージボックスを表示させるためにInteractionMessageや、ウィンドウを閉じたり最大化したりするためのInteractionMessageが標準で用意されていますが、もちろんバルーンチップを表示するためのInteractionMessageは用意されていないので自前で用意する必要があります。

    /// <summary>
    /// バルーンチップを表示するための相互作用メッセージです
    /// </summary>
    public class BalloonTipMessage : InteractionMessage
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        public BalloonTipMessage() : base() { }
    
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="MessageKey">メッセージキー</param>
        public BalloonTipMessage(string MessageKey) : base(MessageKey) { }
    
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="Title">バルーンのタイトル</param>
        /// <param name="Message">バルーンのメッセージ</param>
        /// <param name="Icon">バルーンアイコン</param>
        /// <param name="MessageKey">メッセージキー</param>
        public BalloonTipMessage(string Title, string Message, BalloonIcon Icon, string MessageKey) : base(MessageKey)
        {
            this.Title = Title;
            this.Message = Message;
            this.Icon = Icon;
        }
    
        protected override Freezable CreateInstanceCore()
        {
            return new BalloonTipMessage(MessageKey);
        }
    
        /// <summary>
        /// バルーンのタイトル
        /// </summary>
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }
        public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(BalloonTipMessage), new PropertyMetadata(null));
    
        /// <summary>
        /// バルーンのメッセージ
        /// </summary>
        public string Message
        {
            get { return (string)GetValue(MessageProperty); }
            set { SetValue(MessageProperty, value); }
        }
        public static readonly DependencyProperty MessageProperty = DependencyProperty.Register(nameof(Message), typeof(string), typeof(BalloonTipMessage), new PropertyMetadata(null));
    
        /// <summary>
        /// バルーンアイコン
        /// </summary>
        public BalloonIcon Icon
        {
            get { return (BalloonIcon)GetValue(IconProperty); }
            set { SetValue(IconProperty, value); }
        }
        public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(BalloonIcon), typeof(BalloonTipMessage), new PropertyMetadata(BalloonIcon.None));
    }
    

    こんな感じでどうでしょうか。バルーンチップに引き渡す必要があるパラメーターはこのように依存関係プロパティで用意しておきます。そして、コンストラクタに数種類にオーバーロードを用意しておき、あとはFreezable.CreateInstanceCore()をオーバーライドしておくだけです。これは、自分のインスタンスを新しく作って返すだけで大丈夫です。

    さて、今度はこれをView側で受け取って何かしらコントロールを動かす仕組みを作ります。いわゆる「アクション」です。これはInteractionMessageより実装はとても楽です。

    public class BalloonTipMessageAction : InteractionMessageAction<TaskbarIcon>
    {
        protected override void InvokeAction(InteractionMessage message)
        {
            var msg = message as BalloonTipMessage;
    
            if(msg != null)
                this.AssociatedObject.ShowBalloonTip(msg.Title, msg.Message, msg.Icon);
        }
    }
    

    InteractionMessageAction<T>のTには、そのアクションを実行するコントロールの型を入れます。今回はTaskbarIconです。
    そして、InvokeActionメソッドをオーバーライドし、InteractionMessageがBalloonTipMessageのときのみアクションを実行します。TaskbarIconのインスタンスはthis.AssociatedObjectですので、このShowBalloonTipメソッドを呼び出せばオッケーです。
    ちなみに、ShowBalloonTipメソッドを呼び出すときには、内部的にWinFormsを使っている都合か、System.Drawingの参照を要求されます。ここは意地を張っても仕方ないので、泣く泣く参照を付けましょう。

    最後に、TaskbarIconにこのアクションを登録してあげます。

    <tb:TaskbarIcon IconSource="/WpfNotifyIconTest;component/Images/icon.ico" DoubleClickCommand="{Binding ConfigurationCommand}" >
        <tb:TaskbarIcon.TrayToolTip>
            <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="1" >
                <Border.Background>
                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                        <GradientStop Color="White" Offset="0" />
                        <GradientStop Color="LightGray" Offset="1" />
                    </LinearGradientBrush>
                </Border.Background>
                <Border.BitmapEffect>
                    <DropShadowBitmapEffect Color="Gray" ShadowDepth="4" Softness="0.3"/>
                </Border.BitmapEffect>
                <TextBlock Text="あ!" FontSize="30" Margin="2,2,2,2" />
            </Border>
        </tb:TaskbarIcon.TrayToolTip>
        <tb:TaskbarIcon.ContextMenu>
            <ContextMenu>
                <MenuItem Header="_Configuration" FontWeight="Bold" Command="{Binding ConfigurationCommand}" />
                <MenuItem Header="_Exit" Command="{Binding ExitCommand}" />
            </ContextMenu>
        </tb:TaskbarIcon.ContextMenu>
        <i:Interaction.Triggers>
            <l:InteractionMessageTrigger MessageKey="BalloonTip" Messenger="{Binding Messenger}">
                <m:BalloonTipMessageAction InvokeActionOnlyWhenWindowIsActive="False" />
            </l:InteractionMessageTrigger>
        </i:Interaction.Triggers>
    </tb:TaskbarIcon>
    

    最後の<i:Interaction.Triggers>というところです。ここで、このアクションのメッセージキーを設定し、BalloonTipMessageActionを設定してあげています。m:は、このアクションを実装した名前空間の参照です。そして、Livetの最新版の1.3では、アクションはウィンドウがアクティブな時しか実行されないようになっているので、非アクティブでも実行できるようにInvokeActionOnlyWhenWindowIsActiveをFalseにしておきます。前のバージョンでは一時期ハマりやすいポイントということでデフォルトでこれがFalseになってたようですが、それより前にデフォルトでTrueになっていた時代があったせいで混乱が起き、結局デフォルトでTrueになるように戻ったようです。

    ViewModelからバルーンチップを表示させるのはこのようにすればおkです。

    Messenger.Raise(new BalloonTipMessage("タイトル", "メッセージ内容", Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Info, "BalloonTip"));
    

    これでバルーンチップを表示する手順は終了です。

    実は、ツールチップの表示について、内部的に非WPFのToolTipTextプロパティに対してWPFのTrayToolTipプロパティがあるように、バルーンチップの表示でも非WPFのShowBalloonTipメソッドに対してWPFのShowCustomBalloonメソッドがあったりします。が、今回はこの話は割愛させていただきます(実は私もあまり触ってみたことが無いため)。興味のある方はぜひいじってみてください。

    MainWindowを消す

    さて、ここまでタスクトレイアイコンに求められる主なことを一通りやってきましたが、ここで気づいたと思います。

    _人人人人人人人人人人人人_
    > MainWindowいらなくね? <
     ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

    実は、このプログラムを実行するたびにMainWindowが表示されていました。
    アプリケーションにもよりますが、例えば常駐中は一切ウィンドウを出さず、設定画面など必要なときのみウィンドウを表示する、といった使い方は大いにあり得そうです。そのようなときに、このようなウィンドウは障害となってしまいます。しかし、TaskbarIconはXAML上で定義されているので、MainWindowクラスをインスタンス化しない限りタスクトレイにアイコンすら表示できなくなってしまいそうです。いったいどうすればいいでしょう。

    「MainWindowを非表示にする」もしくは「MainWindowを隠す」といったあたりの方針でいろいろ試してみましたが、現状の最適解としてはこんな感じですかね。

    <Window x:Class="WpfNotifyIconTest.Views.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
            xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
            xmlns:v="clr-namespace:WpfNotifyIconTest.Views"
            xmlns:m="clr-namespace:WpfNotifyIconTest.Views.Messaging"
            xmlns:vm="clr-namespace:WpfNotifyIconTest.ViewModels"
            xmlns:tb="http://www.hardcodet.net/taskbar"
            WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False" Width="0" Height="0"
            WindowStartupLocation="Manual" Top="-100" Left="-100" SourceInitialized="Window_SourceInitialized" >
    
        <!-- 中略 -->
    
    </Window>
    

    まず、WindowStyleをNoneにしています。これによって、一切タイトルバーや枠などの無いウィンドウになります。ResizeModeはNoResizeにして、隠したいウィンドウが大きいウィンドウサイズに変更されないようにしています。タスクバーに表示したくないので、ShowInTaskbarはFalseにしています。そして、ウィンドウの幅、高さをゼロにして、ウィンドウを画面の座標のマイナスの方向に吹っ飛ばしています。

    実は、これだけだと3つ問題があります。

    1つ目は、幅・高さともにゼロとはいえ、実は小さなサイズのウィンドウが存在してしまっていることです。適当にウィンドウを正の座標に持ってきてからよ~く観察すると、小さい点があることに気が付きます。
    デスクトップの背景に同化して気づいていませんでしたが、マウスで適当に範囲選択すると、小さいポツがデスクトップにあることに気が付きます。これが今回作ったウィンドウです。最初は、液晶のドット欠けか何かかなあと思っていましたが、場合によっては見えなかったりするのでなんだろうって思っていたら、このアプリケーションのウィンドウだったわけですね。
    マイナスの座標にウィンドウをもっていけば一応ごまかせますが、マルチディスプレイとかで、マイナスの座標にサブディスプレイを配置することは可能ですので、そのような場合は見えてくるかもしれません。そのとき、この小さなウィンドウだったら「え?液晶のドット欠けじゃね?」とごまかせます。これが1つ目の問題ですね。

    2つ目の問題は、Alt+Tabなどでこのウィンドウが見えてしまうことです。これに関しては解決することが可能です。MainWindowをツールウィンドウにすればいいのです。実は、上記のXAMLにSourceInitializedイベントのハンドラが登録されています。なので、MainWindow.のコードビハインドを見てみましょう。

    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    
        private void Window_SourceInitialized(object sender, EventArgs e)
        {
            var helper = new WindowInteropHelper(this);
            var exStyle = (Native.WS_EX)Native.GetWindowLontPtr(helper.Handle, (int)Native.GWL.EXSTYLE).ToInt32();
            exStyle |= Native.WS_EX.TOOLWINDOW;
            Native.SetWindowLongPtr(helper.Handle, (int)Native.GWL.EXSTYLE, new IntPtr((int)exStyle));
        }
    
        private class Native
        {
            [Flags]
            public enum WS_EX : int
            {
                TOOLWINDOW = 0x80,
            }
    
            public enum GWL : int
            {
                EXSTYLE = -20,
            }
    
            #region GetWindowLontPtr
    
            [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
            private static extern IntPtr _GetWindowLong(IntPtr hWnd, int nIndex);
    
            [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
            private static extern IntPtr _GetWindowLongPtr(IntPtr hWnd, int nIndex);
    
            public static IntPtr GetWindowLontPtr(IntPtr hWnd, int nIndex)
            {
                return IntPtr.Size == 8 ? _GetWindowLongPtr(hWnd, nIndex) : _GetWindowLong(hWnd, nIndex);
            }
    
            #endregion
    
            #region SetWindowLongPtr
    
            [DllImport("user32.dll", EntryPoint = "SetWindowLong")]
            private static extern int _SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
    
            [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
            private static extern IntPtr _SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
    
            public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
            {
                return IntPtr.Size == 8 ? _SetWindowLongPtr(hWnd, nIndex, dwNewLong) : new IntPtr(_SetWindowLong(hWnd, nIndex, dwNewLong.ToInt32()));
            }
    
            #endregion
        }
    }
    

    コードビハインドではネイティブWin32APIをたたいてこのウィンドウをToolWindowに設定しています。なぜXAMLのWindowStyleでToolWindowを指定しないのかというと、そこで設定してしまうとウィンドウのサイズが上記のような極小サイズにならないからです。×ボタンくらいのサイズになります。なので、コードビハインドで拡張ウィンドウスタイルでツールウィンドウを指定してあげています。
    拡張ウィンドウスタイルを取得するにはGetWindowLongPtr関数、設定するにはSetWindowLongPtr関数を呼び出せば良いのですが、よくよくwinuser.hを読むと、64bit環境では確かにGetWindowLongPtr関数を呼び出しているのですが、32bit環境ではGetWindowLong関数を呼んでいることがわかります。まあポインタのサイズが32bitと64bitで違うので仕方ないですね。なので、C#でこれを呼び出すときはIntPtr.Sizeをチェックすることで自らのアプリケーションが32bitか64bitかを判定し呼び分けています。
    これで、Alt+Tabでも画面が見えない状態で、かつドットサイズのウィンドウに保つことができました。

    最後の問題点は、起動直後にAlt+F4でこのソフトが終了できてしまうことです。別のアプリケーションに一度移動すれば、Alt+Tab等ででも切り替えることができなくなるので終了させてしまうことはなくなりますが、起動直後はアクティブなようでAlt+F4で終了できてしまいます。これについては、解決策を見つけることはできませんでした。いやいや、キーフックでもすりゃいいいのかもしれませんが…。


    ということで、一応2つほど(ドットサイズのウィンドウの存在、起動直後Alt+F4で終了できてしまう点)問題が残っていますが、ウィンドウを消すことができました。


    さて、ここまでで 、XAMLによるNotifyIconのデザイン、Messengerパターンによるバルーンチップ表示の手法、そしてMainWindowを消す話の3種類を見てきました。これで、WPFによる常駐型アプリケーションがそれなりに実用的な形で作ることができるのではないでしょうか。

    2015年11月10日火曜日

    WPFにおけるINotifyDataErrorInfoを使った値の検証(Validation)

    2017/2/20追記: 続編はこちら WPFにおけるINotifyDataErrorInfoを使った値の検証 属性版

    今回はこってこてのWPFの話をしたいと思います。

    WPFには値の検証機能があります。


    このように、認められた入力以外の入力があった場合、赤枠などにしてエラーを伝えてくれる機能です。ちょっといじればツールチップでエラーメッセージを表示したりすることもできますし、非常に明確にエラーの意を伝えられるので重宝しています。

    この値の検証ですが、実装の方法と言えばValudationRule抽象クラスを継承したクラスを作り、それをバインディングのValidationRulesプロパティに指定してあげるという方法が私の頭の中にありましたし、それ以外知りませんでした。WPFの値の検証で調べるとこれに関する記事はたくさん出てきますしね。

    しかし、この方法にはいろいろと問題点がありました。

    Viewで簡単に値の検証をできるのはいいのですが、ValidationRule.Validate()メソッドに渡されるのはそのコントロールにバインディングされている現在の値(とCultureInfo)だけなので、その現在の値のみを使った検証以外はできません。上記の画像のように「テキストボックスに数字のみが入力されたかをチェックする」などといった検証では問題がありませんが、例えば別のテキストボックスで入力された値も使って検証をするような場合はこの方法ではできません。
    また、値の検証はそのコントロール内でのみ完結してしまいます。 例えば、ダイアログボックスなどを実装する場合は、「正しい値が入力されていないうちにはOKボタンを押せなくする」などといった対応が必要になりますが、そもそも値の検証結果をViewModelで知る由もありません。コントロールのValidation.Errors添付プロパティに検証結果が入りますが、これをViewModelで読みだすのは至難の業でしょう。かといって、OKボタンにバインディングしているコマンドにValidationRule.Validate()メソッドで実装したものと同じ検証ロジックをコピペするのも良い実装とは言えません。

    このように、 ValidationRule抽象クラスを継承したクラスを作る方法は、作り方こそシンプルで、IValueConverterと似た方式で作ることができるという意味で分かりやすいものの、実用上に様々な課題があります。WPFではこれらの問題をクリアした値の検証システムが望まれるわけです。

    それが、INotifyDataErrorInfoインターフェースを実装したViewModelです。
    ViewModelです。
    ViewModelです。
    ViewModelです。

    はい。 ViewModelにエラー通知機能を実装するのです。そうです、それこそ、INotifyPropertyChangedインターフェースを実装したViewModelが、ViewModelのプロパティが変化したときにプロパティ変更イベントを発生させるように、INotifyDataErrorInfoインターフェースを実装したViewModelが、ViewModelのプロパティにエラーが発生したときにそれを通知することができるのです。ViewModelがエラー管理をするので、必要に応じて他のプロパティを参照できますし、OKボタンの制御も一緒にできてしまいます。とても素直で良い方法です。

    さて、ここでINotifyDataErrorInfoインターフェースで実装しなければいけないメソッド等を見ていきましょう。

    HasErrorsプロパティ

    bool HasErrors { get; }
    
    これはとてもシンプルなプロパティです。エラーがあるプロパティがあればTrue、なければFalseを返すプロパティです。すなわち、これを反転させれば、それがそっくりそのままOKボタンのIsEnabledになるわけですね。

    GetErrorsメソッド

    IEnumerable GetErrors(string propertyName)
    
    これはプロパティ名を渡すとエラーを返してくれるメソッドです。WPFの都合上か、非ジェネリックのIEnumerableが返却値になっています。1つのプロパティに複数のエラーがある場合があるので、このような実装になっています。通常はエラーテキストの集まりであるIEnumerable<string>でも返しておけば大丈夫です。
    なお、propertyNameが空かnullの場合はエンティティレベルでのエラーを返す必要があります。

    ErrorChangedイベント

    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
    
    このプロパティはエラーの状態が変わったとき(エラーが発生した、エラー内容が変わった、消えた)ときに発火するイベントです。DataErrorsChangedEventArgsクラスにはエラー状態が変わったプロパティ名が入っています。


    …とまあ、こんな程度です。これを実装したViewModelを作ってしまえばこっちのものなわけです。
    例えばこんな感じの実装でどうでしょう。

    public abstract class NotifyDataErrorInfoViewModel : ViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, HashSet<string>> Errors = new Dictionary<string, HashSet<string>>();
    
        /// <summary>
        /// エラーを追加するメソッド
        /// </summary>
        /// <param name="PropertyName">プロパティ名</param>
        /// <param name="ErrorText">エラー文</param>
        /// <returns>同じエラー文が無ければtrue、あればfalse</returns>
        protected bool AddError(string PropertyName, string ErrorText)
        {
            if(string.IsNullOrEmpty(PropertyName))
                throw new ArgumentException(nameof(PropertyName));
            if(string.IsNullOrEmpty(ErrorText))
                throw new ArgumentException(nameof(ErrorText));
    
            if(!Errors.ContainsKey(PropertyName))
                Errors[PropertyName] = new HashSet<string>();
    
            bool ret = Errors[PropertyName].Add(ErrorText);
            if(ret)
                RaiseErrorsChanged(PropertyName);
            return ret;
        }
    
        /// <summary>
        /// エラーを消去するメソッド
        /// </summary>
        /// <param name="PropertyName">プロパティ名</param>
        /// <returns>削除できればtrue、元から無ければfalse</returns>
        protected bool ResetError(string PropertyName)
        {
            if(string.IsNullOrEmpty(PropertyName))
                throw new ArgumentException(nameof(PropertyName));
    
            bool ret = Errors.Remove(PropertyName);
            if(ret)
                RaiseErrorsChanged(PropertyName);
            return ret;
        }
    
        /// <summary>
        /// エラーがあるかを取得するメソッド
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        /// <returns>エラー内容</returns>
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            return GetErrorsG(propertyName);
        }
    
        /// <summary>
        /// GetErrorsのジェネリック版
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        /// <returns>エラー内容</returns>
        public IEnumerable<string> GetErrorsG(string propertyName)
        {
            if(string.IsNullOrEmpty(propertyName))
                return Errors.Values.SelectMany(p => p).ToList().AsReadOnly();    //エンティティレベルでのエラー
            else if(Errors.ContainsKey(propertyName))
                return Errors[propertyName].ToList().AsReadOnly();
            else
                return Enumerable.Empty<string>();
        }
    
        /// <summary>
        /// エラーがあるかどうか
        /// </summary>
        public bool HasErrors => Errors.Any();
    
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
        /// <summary>
        /// エラー変更を発行するメソッド
        /// </summary>
        /// <param name="propertyName">プロパティ名</param>
        void RaiseErrorsChanged(string propertyName)
        {
            if(ErrorsChanged != null)
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
    


    C#6.0はnameof演算子があるので式木などを用いた手の込んだプロパティ名渡し用のオーバーロードなどはもう作らなくていいでしょう。また、HasErrorsプロパティはexpression-bodiedプロパティにしてみました。

    さて、実際のViewModelはこんな感じになります。

    public class MainWindowViewModel : NotifyDataErrorInfoViewModel
    {
        public void Initialize()
        {
            CheckDigitText();
            CheckNumberText();
        }
    
        void CheckNumberText()
        {
            if(!GetErrorsG(nameof(DigitText)).Any() && !string.IsNullOrEmpty(NumberText) &&
               NumberText.All(p => char.IsNumber(p)) && NumberText.Count() == int.Parse(DigitText))
                ResetError(nameof(NumberText));
            else
                AddError(nameof(NumberText), "This is not a correct number.");
        }
    
        void CheckDigitText()
        {
            if(!string.IsNullOrEmpty(DigitText) && DigitText.All(p => char.IsNumber(p)))
                ResetError(nameof(DigitText));
            else
                AddError(nameof(DigitText), "This is not a number.");
        }
    
        #region NumberText変更通知プロパティ
    
        public string NumberText
        {
            get { return _NumberText; }
            set
            {
                if(_NumberText == value)
                    return;
                _NumberText = value;
                RaisePropertyChanged(nameof(NumberText));
    
                CheckNumberText();
            }
        }
        private string _NumberText;
    
        #endregion
    
        #region DigitText変更通知プロパティ
    
        public string DigitText
        {
            get { return _DigitText; }
            set
            {
                if(_DigitText == value)
                    return;
                _DigitText = value;
                RaisePropertyChanged(nameof(DigitText));
    
                CheckDigitText();
                CheckNumberText();
            }
        }
        private string _DigitText;
    
        #endregion
    }
    

    NumberTextとDigitTextはそれぞれ値とその桁数を入力させるテキストボックスにバインディングするstring型のプロパティです。桁数が変わると値の正当性も変わってくるので、桁数が変更になったときはCheckNumberText()のほうも呼ぶようにしています。


    これは5桁を指定しているので値のほうの正当性も認められたということがわかる画面です。このように、複数のプロパティをまたいだ値の検証がとても簡単にできる仕組みです。

    これから、値検証が必要なViewを作るときは、どんどんこのINotifyDataErrorInfoを使っていきたいと思います。

    2015年11月9日月曜日

    SSIDProxy ver.1.1.0

    (2015/11/26追記)現在の最新版はver.1.1.1になっています。こちらからどうぞ。


    さて、前回の記事で紹介したレジストリをいじってプロキシ設定をする方法を実装し、さらにネットワーク接続状況が変わると自動的にSSIDに応じて設定したプロキシ設定に切り替えてくれるソフトを作りました。それが、SSIDProxyです。

    実は、近々閉鎖予定のウェブページにも公開しておりますが、2年以上前に作ったソフトということもあり、大幅にリファクタリングを行いました。というよりプロジェクトを新規で作り直すレベルで作り直しました。私自身、当時よりプログラミングのスキルが上がっているわけですし、C#やその他のライブラリも大きく進化しております。それらを含めて多少の改善と、あとは多少の機能追加も行いました。

    というわけで、このSSIDProxyについて説明していきます。

    概要

    このソフトはWindowsのプロキシの設定を無線LAN接続先のSSIDやMACアドレスによって自動的に変更するソフトです。例えばAndroidなんかは、Wi-Fiの設定の中にプロキシの設定があり、接続先によってプロキシを変更してくれますよね。しかし、WindowsではWi-Fiとプロキシの設定は別物です。それは不便で前時代的。
    そこでこのSSIDProxy。
    SSIDやMACアドレスに応じて適切なプロキシ設定に自動的に切り替えてくれます。たとえば、学校などではプロキシ環境下、家ではプロキシ無しの環境を往復するノートパソコンなどで、いちいち設定する煩わしさから解放してくれます。

    使い方

    ソフトを起動するとタスクトレイにアイコンが現れます。


    このアイコンを右クリックするかダブルクリックで設定画面を開きます。


    これが設定画面です。ここに「ルール」と呼ばれるものを設定することで、そのルールに従ったプロキシ設定をしてくれます。ルールは基本的にこの表の上へ行くほど優先度が高いですが、「デフォルト」は表のどこにあっても優先度は一番低くなります。複数のデフォルトのルールがある場合は、表の一番上側のものが適用されます。
    ルールやその優先順位等は、「追加」「編集」「削除」「上へ」「下へ」のボタンを用いて編集ができます。
    追加を押すと、下記のような画面が出てきます。


    無線LANのところで、SSIDかMACアドレスかデフォルトを設定します。現在接続しているネットワークがここで設定したSSID、もしくはMACアドレスに一致した場合、このプロキシ設定が適用されます。どれとも一致しなかった場合はデフォルトのものが適用されます。
    自動構成、プロキシサーバーのところはWindowsのインターネットオプションから行うプロキシ設定とほぼ同じデザインになっているため、難なく設定できるかと思います。

    これで設定ができました。あとは、接続先のネットワークが変更になると自動的にルールに従ってプロキシが切り替わります。
    また、強制的に現在のルールに従ってプロキシ設定を変更したい場合は、タスクトレイのアイコンを右クリックして「今すぐルールを適用する」をクリックすればOKです。
    ソフトウェア起動時にルールを適用したい場合、"--apply-with-start"をコマンドラインオプションに付けて起動すると起動直後にルールが適用されます。

    動作環境

    下記の条件をすべて満たしている必要があります。
    • .NET Framework 4.5.1以上
    • Windows Vista以上のWindows
    • Wi-Fiデバイスが付いており、正常に動作していること
     なお、動作確認はWindows 8.1 Pro with Media Centerで行っております。

    インストール・アンインストール方法

    インストーラは特別用意しておりませんので、適当なフォルダに解凍してお使いください。スタートアップに設定し、常駐させると便利です。起動時にルールを適用するには、"--apply-with-start"をコマンドラインオプションに付けてください。
    また、アンインストールはそのまま削除していただくだけで大丈夫です。

    使用ライブラリ

    このソフトウェアは下記のライブラリを使用しております。
     これらの有用なソフトウェアを公開してくださった方たちには深く感謝いたします。また、これらのライブラリのオリジナル部分に関しての著作権等の権利は作成者に帰属いたします。

    ※11/14追記:Managed Native Wifiの作者様より、派生ライブラリではない旨のご指摘をいただきましたので訂正いたしました。十分に確認しておらず誤ったことを記してしまい大変申し訳ございませんでした。なお、アプリケーションのバイナリについては次期バージョンより訂正させていただきます。

    免責事項

    このソフトウェアを使用することによって発生したいかなるトラブル(データ喪失、パソコン故障、データ流出等)についても、作成者は責任を負わないものといたします。特に、このソフトウェアは独自研究によるレジストリ編集を行っているため、その方法が完全ではなかったり、Windowsのバージョン・システム構成等の環境によって正常に動作しない可能性があります。場合によっては重大なトラブルを引き起こす可能性がありますので、そのようなリスクをご承知の上、ご利用ください。
    なお、このソフトウェアを起動した時点で、免責事項を読んだかどうかにかかわらず、これらの免責事項に同意したものとみなします。

    今バージョンでの追加機能

    • 日本語SSIDに対応
    • アクセスキー(アクセラレータキー)に対応
    • ルールの編集、順序変更機能を実装
    • その他細かなリファイン、ソースコードの全面的なリファクタリング 

    ダウンロード

    SSIDProxy ver.1.1.0

    プロキシ設定とレジストリ

    (この記事は以前にウェブページで公開した内容をブログに移行したものです)

    ノートパソコンの無線LANの接続先によってシステムのプロキシ設定を変えたいことってありませんか?そのためには、ソフトウェアからシステムのプロキシをいじる必要があります。 しかし、多分、インターネットオプションのプロキシ設定をいじるAPI的なものは無いです。いや、もしかしたらあるのかもしれませんが、情弱な私は見つけられませんでした。ですが、プロキシの設定がレジストリに書き込まれるという情報を得たので、どう書き込まれるのかレジストリを研究してみることにしました。

    注意:これは私の独自研究であり、正確性を保証するものではありません。この記事を信用してレジストリをいじっていかなる不具合や損害が起きても当方では一切責任を負いません。

    どこの設定の話?

    インターネットオプションの「LANの設定」の項目での設定の話です。FirefoxやOperaのプロキシの設定とは違います。IE以外のブラウザはたいてい独自にそのブラウザのみプロキシを履いてインターネットに接続する仕組みを持っているようですが、IEのプロキシ設定はWindowsシステムと共通のものとなっているようです。そして、事実上、様々なソフトがこの設定を使ってプロキシを認識しているようなので、プロキシ環境下でインターネット接続をする場合はここの設定が必要になる場合が非常に多いです。


    プロキシの設定で戦ったことがある人ならば誰もが見たことがあるであろうこのダイアログボックスです。今回は、このダイアログボックスで設定できるすべての項目(詳細設定ダイアログも含む)について調べていきます。

    設定は一体どこに書き込まれるのか?

    ズバリ、レジストリの
    HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\
    と、その子エントリだと思われます。実際、ここにもProxyEnableなどのそれらしいキーが散見されますね。しかし、おそらくそのProxyEnableなどは飾りで、これらのそれらしいキーを変更しても期待される動作はしないっぽいです。ProxyEnableなどのレジストリキーは見るからにしてわかりやすいのでそれを読み込んでプロキシ設定をするソフトもあるようですが、Windowsシステムの本命はここではないようです。

    では、その本命はどこかと言うと
    HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections\DefaultConnectionSettings
    です。プロキシ設定というか、インターネットオプションの「LANの設定」の項目はすべてこのキーの中に保管されています。その下に似たようなキー 「SavedLegacySettings」がありますが、名前からしてバックアップっぽいですよね。確証はありませんが。でも、こっちは基本いじらなく ても大丈夫っぽいです。

    環境によっては、DefaultConnectionSettingsとSavedLegacySettings以外のエントリがあるかもしれません。それらのエントリを見ると、そのWndowsに保存しているVPN設定名だったりするので、もしかしたらそのあたりの設定も入っているのかもしれませんが、今回はとりあえずDefaultConnectionSettingsのみに注目していきます。

    バイナリデータの構造

    で、そのDefaultConnectionSettingsについてなんですが、色々調べているとこの辺までたどり着いている人は結構いるっぽいんですが、結局バイナリ データということで敬遠して諦めてしまっている人が多いように見受けられました。しかし、バイナリデータの構造なんていうのは、古い時代?からプログラミ ングしている人が考えることはだいたい一緒です。着目すべきところに着目すれば、構造が自然と見えてきました。
    というわけで、レジストリ設定をポチポチ変えながらDefaultConnectionSettingsのどこが変化したかを追っていきました。その結果わかったバイナリデータの構造は下記の通りです。

     インデックス 型内容
    0DWORD0x46(ヘッダー?よくわからない)
    4DWORD内容が更新されるたびに値がインクリメントされているっぽい。
    8DWORDチェックボックスのフラグ。2^0のビットを0ビット目…2^31のビットを31ビット目と呼ぶと
    1ビット目:プロキシを使うかどうか(使うなら1)
    2ビット目:自動構成スクリプトを使うかどうか(使うなら1)
    3ビット目:設定を自動的に検出するかどうか(検出するなら1)
    12DWORDプロキシの長さ(インデックス16からの文字列の長さ)
    16BYTE配列プロキシをASCII文字列で記述している。
    全てのプロトコルが同じプロキシの場合、URL:ポートのフォーマット
    プロトコルで違う場合、http=URL:ポート;https=URL:ポート;ftp=URL:ポート;socks=URL:ポート
    といった具合の文字列になっている。
    16+プロキシ長DWORDプロキシを使用しないアドレスの長さ
    20+プロキシ長BYTE配列プロキシを使用しないアドレスがASCII文字列で記述してある。
    複数の場合は;で区切ってあり、「ローカルアドレスはプロキシサーバーを使用しない」にチェックが入れられている場合は、さらに<local>が付く。
    20+プロキシ長+不使用長DWORD自動構成スクリプトのアドレスの長さ
    24+プロキシ長+不使用長BYTE配列自動構成スクリプトのアドレスがASCII文字列で記述してある。


    ここまでわかっていれば、あとはレジストリ編集でLANの設定をレジストリ編集で行ったり、またはそこから読み出したりすることができます。文字列みたいにバイト数が定まらないデータを保存する際は、そのデータを記述する前にその長さを書き込んでやればいいっていうのは定石ですよね。
    この上記の表の最後にある「自動構成スクリプトのアドレス」よりも後にもデータがあるようですが、何の設定なのかはよくわかりませんでした。まあ、そこがわからなくてもプロキシ設定は全部マークできているのでノータッチにしておけば問題ないでしょう。あ、ちなみにWindowsが動いているIntelのCPUのアーキテクチャはDWORDなどの2バイト以上あるデータはリトルエンディアンで記述されるっていうのはいいですよね。
    あとは、この設定と連動してHKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion \Internet Settings\以下のProxyEnable、ProxyOverride、ProxyServerなどのレジストリキーをいい感じに編集しておけば、よりそれらしい挙動になると思います。プロキシ設定を読み込むソフトウェアによっては、これらのレジストリキーを参照しているものも少なくないので、DefaultConnectionSettingsと同時にこれらを書き換えておくのが安牌です。

    これらの一連の設定を行う機能をソフトウェアに組み込めば、この記事の冒頭に書いたニーズのソフトウェアとかが作れるわけです。(´◔౪◔)۶ヨッシャ!
    そして、それを実際に実装したソフトがSSIDProxyです。よろしくね!

    ※この記事はWindows 8 Pro with MediaCenter (64bit版)において実験的に調べたものです。他の環境に当てはまるかどうかは未検証ですし、不備や間違いがあるかもしれません。ご利用は自己責任でお願いいたします。