2015年12月19日土曜日

WPF用縦書きテキストブロック Tategaki ver.2.1.1

(2024/05/11追記)
Google等からこのページに直接来られる方が多いようですが、最新版はver.3.2.2になっていますので、こちらからどうぞ。

先日Nugetデビューしたので、ついでにTategakiもNugetにアップロードしました。
バージョンを少し進めていますが、これはまあコードの一部をnameofにしてC#6.0化したり、XAML名前空間をURLで指定できるようにしたためです。

Glyphs特有の癖と言いますか、どうしても横書きのTextBlock等と同じように使えないところをどうにかしたいなとは思ってはいるのですが、なかなか上手くいかないですね…。例えば、フォント名→フォントファイルの変換なんかは独自に私が実装していますので、どうしてもTextBlockの抽象化ほどきれいにいっていなかったりします。早くWPF側で縦書きに対応してほしいのですが、なんていうか、もうMicrosoft内ではWPFを時代遅れとみている風潮すらありそうな気がして、あまり期待はしていません…。


Tategakiの使い方

そういえば、Tategakiをいかにして作るかみたいな話はしましたが、使い方はサンプルコードに頼りっぱなしで全然解説していなかったなーって思いましたので、ここで少し解説しておきます。

Nugetからダウンロード

まずは適当なWPFプロジェクトを作りNugetからTategakiを落としてきます。



これで必要な参照は追加されます。

XAMLでの配置

まずはXAMLでTategakiに使う名前空間を設定します。
xmlns:tg="http://schemas.eh500-kintarou.com/Tategaki"
そしたら、コントロールを適当に配置します。
<Grid>
    <tg:TategakiText Text="羅生門" VerticalAlignment="Center" FontSize="36" FontWeight="Bold" FontFamily="MS ゴシック" />
</Grid>
これで完成です。どうです?簡単でしょう。
ちなみに、自動的に改行される複数行に渡ったテキストはTategakiMultilineを使います。
<tg:TategakiMultiline Text="{Binding ElementName=textBox1, Path=Text, UpdateSourceTrigger=PropertyChanged}" />
どうです?簡単でしょう。

ちなみに、縦書きでテキストを作る場合、右から左へスタックしていくかと思いますが、WPFのStackPanelではそういうことはできません(たぶん)。なので、私は左右反転させたテキストStackPanelで左から右へスタックし、 そのStackPanel全体をもう一度左右反転させる、なんてことをやってそれっぽいコードを書いています。初めから内容がわかってるんだったら後ろの行から順に左からスタックしていくのとかもぜんぜんありでしょうが…。

てな感じで、適当にコントロールを配置するとこんな感じのアプリケーションが出来上がります。



ちゃんとフォントファイルから縦書き用の文字のインデックスを取ってきているので、カッコや句読点など横書きと縦書きで位置が変わるようなものでも正確に表示できていることがわかるかと思います。

ダウンロード


2015年12月17日木曜日

ListView Extensions ver.1.0.0-beta1

WPFのListViewって結構使うのに根性いりません?

ちょっとしたデータの可視化程度ならば適当な実装でもさほど困らないかもしれませんが、ListViewらしくヘッダーをクリックしてソートしたり、右クリックやダブルクリック、選択したアイテムの取得などの実装をしようとし始めるととたんに面倒になってくるのがWPFのListViewです。
そこで、ListViewでよくやることについて、手が届きそうで届かない痒い所を中心にView、ViewModel、Modelのすべてにおいてサポートをするライブラリを作ったら格段とListViewの使い勝手が上がるのではないか?という発想になりました。

というわけで、こんなクラスを実装し、ライブラリ化しました。下記の図の青色のクラスです。


順を追って説明していきます。

Model

Modelの中心的存在となるのはSortableObservableCollection<T>です。
ここで重要なのは「Sortable」です。Sortedじゃないんですね。ListViewに表示するデータはソートされていなくてもいいですし、ソートされていてもいいです。必要に応じて並び替えることができるということでSortableという名前にしました。
このコレクションは、ISortableObservableCollection<T>インターフェースを継承しています。
/// <summary>
/// ソート可能な変更通知コレクションのインターフェースです。
/// </summary>
/// <typeparam name="T">要素の型</typeparam>
public interface ISortableObservableCollection<T> : IList<T>, INotifyPropertyChanged, INotifyCollectionChanged
{
    /// <summary>
    /// 自身のソート状態からして適切な位置にアイテムを追加します。
    /// ソートされていなかったら末尾に追加されます。
    /// もしも追加するアイテムと同じ大きさのアイテムがあった場合、すでにあるものの最後に挿入されます。
    /// </summary>
    /// <param name="item">追加するアイテム</param>
    void AddAsSorted(T item);

    /// <summary>
    /// 指定したインデックスが示す位置にある項目を、コレクション内の新しい場所に移動します。
    /// </summary>
    /// <param name="oldIndex">移動する項目の場所を指定する、0から始まるインデックス。</param>
    /// <param name="newIndex">項目の新しい場所を指定する、0から始まるインデックス。</param>
    void Move(int oldIndex, int newIndex);

    /// <summary>
    /// 自身をソートします。
    /// </summary>
    /// <param name="propertyName">ソートするプロパティ名</param>
    /// <param name="direction">ソートする方向</param>
    void Sort(string propertyName, SortingDirection direction);

    /// <summary>
    /// 現在のソート条件
    /// </summary>
    SortingCondition SortingCondition { get; }
}
IList<T>とINotifyCollectionChangedを継承しているのは当然ですね。あとは、SortingConditionというプロパティを持つのでINotifyPropertyChangedも継承しています。

さて、このインターフェースが独自に実装を支持しているのは、見ての通り3つのメソッドと1つのプロパティです。
AddAsSortedは見ての通り、ソート状態を保ったままアイテムを追加するものです。もしもコレクションがソートされていなければ末尾に追加されます。
MoveはObservableCollectionなどと同じくアイテムを移動させるものです。
Sortはソートを実行するメソッドです。T型のプロパティ名をstringで指定して、SortingDirectionで指定した向きでソートを実行します。
public enum SortingDirection
{
    None,
    Ascending,
    Descending,
}
いたってシンプルです。Ascendingは昇順、Descendingは降順です。SortメソッドにNoneを渡すと例外を吐きます。

SortingConditionですが、これは現在のソート条件を示すプロパティです。
public class SortingCondition : IEquatable<SortingCondition>
{
    /// <summary>
    /// ソート条件なしとして初期化します
    /// </summary>
    public SortingCondition()
    {
        PropertyName = string.Empty;
        Direction = SortingDirection.None;
    }

    /// <summary>
    /// ソート条件なしとして初期化します
    /// </summary>
    public SortingCondition(string propertyName, SortingDirection direction)
    {
        if(direction == SortingDirection.None) {
            PropertyName = string.Empty;
            Direction = SortingDirection.None;
        } else {
            if(string.IsNullOrEmpty(propertyName))
                throw new ArgumentNullException(nameof(propertyName));

            PropertyName = propertyName;
            Direction = direction;
        }
    }

    /// <summary>
    /// プロパティ名
    /// </summary>
    public string PropertyName { get; }

    /// <summary>
    /// ソート方向
    /// </summary>
    public SortingDirection Direction { get; }

    /// <summary>
    /// ソート条件がなしかを取得します。
    /// </summary>
    public bool IsNone => Direction == SortingDirection.None;

    #region IEquatable

    public override bool Equals(object obj)
    {
        return base.Equals(obj as SortingCondition);
    }

    public override int GetHashCode()
    {
        return PropertyName.GetHashCode();
    }

    public bool Equals(SortingCondition other)
    {
        if(other != null)
            return this.PropertyName == other.PropertyName && this.Direction == other.Direction;
        else
            return false;
    }

    #endregion

    public override string ToString()
    {
        if(IsNone)
            return "None";
        else
            return $"{PropertyName}, {Direction}";
    }

    /// <summary>
    /// ソート条件なし
    /// </summary>
    public readonly static SortingCondition None = new SortingCondition();
}
平たく言えばプロパティ名とソート方向を保持するクラスです。等価評価やソート条件なしを示すstaticフィールドなどが用意されており、ちょっとコードがごちゃごちゃしていますが、基本的にはソート条件を示すだけです。Sortメソッドを呼び出すことでソート条件が変化し、SortingConditionプロパティが変化します。また、アイテムの追加、移動、変更等でソート条件を満たさなくなった時も同様にこのプロパティは変化します。

概ねこれがModelの中身です。
SortableObservableCollectionは中でObservableCollectionのフィールドを持っており、コレクションの変更通知自体はObservableCollectionに任せています。しかし、ObservableCollectionはスレッドセーフではないので、そのあたりのニーズがある場合は適当なスレッドセーフな変更通知コレクションを実装したライブラリなどと組み合わせて独自のISortableObservableCollectionを実装するとよいです。このライブラリではSortableCollection抽象クラスを提供しており、これはソートにかかわるメソッドやプロパティのみが実装されたものなので、これを上手く継承するとそういったものが比較的楽に作れるかと思います。

ViewModel

ViewModelは基本的にListViewViewModel<TViewModel,TModel>クラスを使うことになります。
コンストラクタを見れば使い方が想像つきやすいかと思います。
public ListViewViewModel(ISortableObservableCollection<TModel> source, Func<TModel, TViewModel> converter, Dispatcher dispatcher)
まずはsourceです。これはModelで用意したSortableObservableCollectionを指定すれば良いです。そのコレクションの変更を監視し、それに追従して自身のコレクション(読み取り専用)を変更します。
ところで、ViewModelはViewの都合を吸収するものですから、Modelで使ってたコレクションの要素の型とは違うものになることが多いです。なので、その変換をするメソッドをconverterに渡します。そうすることで、ModelをViewModelに変換した状態でListViewViewModelに保持することができるようになります。
最後に、ListViewコントロールに対してコレクションの変更通知をするときはUIスレッドで行う必要があるため、この引数でUIのDispatcherを引き渡してあげる必要があります。これにより、ListViewViewModelではたとえUIスレッド外でSortableObservableCollectionが変更されたとしてもちゃんとUIスレッド上でCollectionChangedイベントを発生させてくれます。

ちなみに、コンストラクタはもう1つあり、そちらではそのDispatcherの実行優先度を指定することができます。指定しなかった場合(上記のコンストラクタを使った場合)はDispatcherPriority.Normalになります。

最後に、このコンストラクタを呼び出す例を示しましょう。
People = new ListViewViewModel<PersonViewModel, Person>(model.People, m => new PersonViewModel(m), DispatcherHelper.UIDispatcher);
modelのPeopleプロパティ(SortableObsevableCollection<Person>型)に対して、それに対応するListViewViewModelを作っています。変換はPersonViewModelのコンストラクタにPerson型を与えることで実現しています。逆に言えば、コレクションの要素のViewModelはこのような実装をすることをお勧めします。


また、ViewModelにはSortableObservableCollectionに対応したコレクション操作以外に、ListViewの他の機能に対応するプロパティもいくつか持っています。

コレクションの操作

例えば、ListViewを使う場面として、何か適当なデータを複数セットする場合などがあると思います。その場合、データの追加、削除、移動などといった作業が付きまといます。


この画像を見ると、上から3つの項目が選択されています。選択されている項目があるのでRemoveボタンは有効です。また、一番上の項目が選択されているためこれをさらに上へ移動することはできないのでUpボタンは無効化されていますが、下への移動はできるのでDownボタンは有効になっています。
このようなデータの削除、移動は選択状態さえわかれば実行の可否がわかりますし、各要素のデータの内容知っている必要もありません。項目を消し去ったりそのまま動かせばいいだけです。なので、ListViewViewModelでは、その動作を実現するコマンドを用意してあります。
/// <summary>
/// 選択中の項目を削除するコマンド
/// </summary>
public ICommand RemoveSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveUpSelectedItemCommand { get; }

/// <summary>
/// 選択中の項目を上へ移動するコマンド
/// </summary>
public ICommand MoveDownSelectedItemCommand { get; }
ちなみにですが、見ての通り追加はコマンドでは用意されていません。追加されるデータの内容がわかりませんからね。また、追加は基本的にいつでも可能なものです。もしでないときがあるのならば、それはListViewの状態から決まるものではないと考えられます。なのでこのライブラリではそれに対応するコマンドはサポート外です。適当にコマンドを作るなりメソッドをバインディングするなりしてSortableObservableCollectionに直接Addしてください。

ソート

ソートは流れが面倒なので詳しくはまとめて後述します。
ViewModelとしては、ソート指示を受け取るSortByPropertyCommandと、現在のソート条件を示すSortingConditionプロパティを持っています。

選択

ListViewで複数選択する場合はSelectedItemsプロパティをバインディングする必要があります。
実はこのプロパティは読み取り専用依存関係プロパティなので普通にバインディングできないのですが、まあとりあえずそれについては後述します。
SelectedItemsをバインディングする先として、ListViewViewModelではSelectedItemsSetterプロパティを用意しています。ですが、これは非ジェネリックスのIList型なのでとても使いにくいです。なので、この内容をミラーリングしたジェネリックのコレクション、しかも変更通知までできるようにしたものとして、SelectedItemsプロパティを用意してあります。もしも選択状態を知りたければこれを読み込めば大丈夫です。

ちなみに、選択をViewModel側から変更したいときのためにSelectItem、 UnselectItem、ToggleItemSelectionなどのメソッドも用意されています。また選択されているか同化を判別するためにIsSelectedItemなどのメソッドもあります。また、選択を反転するコマンドとしてToggleSelectionCommandも用意されています。

View

Viewのサポートは上記のようなクラスの提供とは少し異なります。なんといっても使うのはListViewですから、例えばListViewViewModelをバインディングするだけですべてが実現できるようなコントロールは用意していません。必要に応じて、必要なものをバインディングしてください。

まずは下準備として、XAMLに名前空間の定義をします。
xmlns:lv="http://schemas.eh500-kintarou.com/ListViewExtensions"
名前空間はURLでまとめてあります。
XAMLの書き方は機能別に記述します。

ソート


ListViewのヘッダーをクリックしたらソートがされるべきです。ですが、残念ながらListViewには勝手にソートしてくれる機能はありません。なので、そのような機能をこのライブラリがサポートすることで解決しています。

全体の流れてとしては下記の図のようになります。



View:ヘッダーがクリックされる
↓コマンド呼び出し
ViewModel
↓Sortメソッド呼び出し
Model::自身をソートし、ソート条件を更新する
↓コレクション・プロパティ監視
ViewModel:自身のコレクションとソート条件をModelと同期
↓バインディング
View:コレクションの反映、ソート済みのヘッダーの▲▼印の表示

てな具合ですかね。ViewからModelまでを往復します。
ViewModel→Model→ViewModelの流れはListViewViewModelクラスとSortableObservableCollectionクラスがいい感じに勝手に処理をしてくれますが、ViewとViewModelのつながりはXAMLに記述しなければなりません。例えば、こんな感じになります。
<ListView ItemsSource="{Binding People}" >
    <ListView.Resources>
        <lv:SortingConditionConverter x:Key="ConditionToDirectionConverter" />
    </ListView.Resources>
    <ListView.View>
        <GridView>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Name}">
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Name" >
                    <lv:SortedHeader Content="Name" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Name'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="150" DisplayMemberBinding="{Binding Pronunciation}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Pronunciation" >
                    <lv:SortedHeader Content="Pronunciation" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Pronunciation'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="70" DisplayMemberBinding="{Binding Age}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Age" >
                    <lv:SortedHeader Content="Age" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Age'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="120" DisplayMemberBinding="{Binding Birthday}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Birthday" >
                    <lv:SortedHeader Content="Birthday" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Birthday'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
            <GridViewColumn Width="100" DisplayMemberBinding="{Binding Height}" >
                <GridViewColumnHeader Command="{Binding People.SortByPropertyCommand}" CommandParameter="Height_cm" >
                    <lv:SortedHeader Content="Height" SortingDirection="{Binding People.SortingCondition, Mode=OneWay, Converter={StaticResource ConditionToDirectionConverter}, ConverterParameter='Height_cm'}" />
                </GridViewColumnHeader>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>
まずはGridViewColumnでバインディングするプロパティ名を指定します。この辺まではListViewの一般的な使い方かと思います。ですが、これはあくまでも"列"の設定ですので、列に対応したヘッダーの設定を別口でする必要があります。ということで、GridViewColumnHeaderを指定してあげます。これが列のヘッダーに対応する要素ですので、クリック時のコマンドをListViewViewModelのSortByPropertyCommandにバインディングしてあげることで、ヘッダーのクリックを伝えることができるようになります。CommandParameterにはソートするときに使うプロパティ名を指定する必要があるので注意してください。

また、GridViewColumnHeaderはそのContentが表示内容になるわけですが(すなわち任意のコントロールを表示内容として配置できます)、その表示内容にはこのライブラリが提供している SortedHeaderコントロールを使うといいです。これは、Contentに加えてソートを示す▲や▼などの図形を同時に表示することができるコントロールです。SortingDirectionプロパティにNoneを指定すれば非表示、Ascending/Descendingで▲/▼になります。
この状態をListViewから反映させるには、SortingConditionをバインディングするといいです。ただし、SortingConditionはソートに使われたプロパティ名とその方向を保持しているクラスですので、適当なValueConverterを介してそれを各ヘッダーのSortingDirectionに変換してやる必要があります。それがSortingConditionConverterになります。パラメーターにプロパティ名を与えれば、そのプロパティ名に一致するSortingConditionの場合はそのSortingDirection、そうでない場合はSortingDirection.Noneを返すようになっています。

これで、ヘッダーを押すとソートされ▲▼が付くというListViewが実現できるようになりました。

選択のバインディング

さて、上でListViewのSelectedItemsプロパティは読み取り専用なので直接バインディングはできないということを紹介しました。
じゃあどうするか。トリガーアクションを使います。

このライブラリではListViewSelectedItemsActionというアクションを持っており、このアクションが実行されると、SelectedItemsをSourceにコピーしてくれます。イベントトリガーを使って、SelectionChangedイベントが発生するたびにこのアクションを実行しても良いですが、選択項目が変更されるとどうもSelectedItemsにアイテムが追加されたり削除されたりするだけで、SelectedItems自体の参照は変わらないようです。むしろ、ViewModel側から選択操作をする場合は、このSelectedItemsの参照が無ければできませんから(これのAddメソッドを呼び出すことで実現している)、インスタンスが出来上がったらいち早く呼び出してやる必要があります。

というわけで、あなたが普段使っているMVVMライブラリのメッセンジャー機能を使ってこのようにListViewViewModelをインスタンス化した直後にSelectedItemsをミラーリングしてあげてください。私が普段使っているLivetではこうなります。
People = new ListViewViewModel<PersonViewModel, Person>(model.People, m => new PersonViewModel(m), DispatcherHelper.UIDispatcher);
Messenger.Raise(new InteractionMessage("SelectedItemsMirroring"));
XAMLではこのようにします。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SelectedItemsMirroring" >
        <lv:ListViewSelectedItemsAction Source="{Binding People.SelectedItemsSetter}" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
こうすることで、ListViewViewModelが作られた直後にSelectedItemsがミラーリングされ、選択項目の取得や設定が可能になります。図にするとこんな感じになります。



ちなみにですが、 ContentRenderedとかのイベントを拾ってあげればわざわざViewModelからメッセージ送らなくていいんじゃないの?との考えもあるかもしれませんが、タイミングによってはContentRendered発生後にListViewViewModelをインスタンス化することがありますので、上手く拾えない可能性が結構あります。変なところで悩まないためにもこの方法が今のところベストかなーと思っています。何かいい対策があればいいのですが…。

ListViewの項目のダブルクリック

さて、ファイルビュアーみたいなものを想定した場合、項目がダブルクリックされたときにその要素のViewModelに対してコマンドを発火させるなりメソッドを呼び出すなりしてやりたくなることがあるかと思います。
これが案外めんどくさいんですね。詳細については以前の記事で紹介しましたが、この記事の機能もこのライブラリに組み込まれています。さらに、前回の記事ではコマンドをバインディングするだけでしたが、メソッドも直接バインディングできるようになっています。
<ListView ItemsSource="{Binding People}" >
    <!-- 中略 -->
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <!--<Setter Property="lvap:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />-->
            <Setter Property="lv:DoubleClickBehavior.MethodTarget" Value="{Binding}" />
            <Setter Property="lv:DoubleClickBehavior.MethodName" Value="DoubleClicked" />
        </Style>
    </ListView.ItemContainerStyle>
</ListView>
こんな感じですね。コメントアウトしてあるのはコマンドをバインディングする方法で、コメントアウトしていないほうがメソッドを直接バインディングするほうです。

ライセンス

以下の各項目をお守りください
  • このライブラリを利用する方は自己責任でお願いします。いかなる問題が起きても作者は責任を負いません。
  • このソフトを悪用しないでください。
  • このソフトウェアを無断で単体での転載、再配布しないでください。ただし、このライブラリを参照しているソフトウェアと一緒に配布する場合を除きます。
  • 作者は使用方法やバグに関するサポートをする義務を負いません。
  • 有償アプリケーションには使用してはならない。
  • 完成したソフトウェアのどこか(ヘルプ、バージョン情報など)と、ReadMeなどのドキュメンテーションに私のライブラリを使用したことを明記すること。ただし、作者(私)がこのライブラリを自分のソフトで使用するときはその限りではない。

公開

Nugetに初挑戦してみました。どうぞ。
https://www.nuget.org/packages/ListViewExtensions/
なお、 プレリリース版扱いですので、Nugetでは検索時にプレリリース版もヒットするように設定する必要があるので注意してください。

また、こちらにサンプルプログラムを用意しています。どうぞ。
ListViewExtensionsSample ver.1.0.0-beta1

2015年12月8日火曜日

ListViewItemのイベントをMVVMスタイルのプログラムで受信する - 添付プロパティ編

さて、以前にReactive Propertyを使って苦し紛れにListViewItemのイベントを受信する方法を紹介しました。しかし、その方法は本当にかなり苦し紛れで、それらしいスタイルは曲がりなりにも美しいとは言えませんでした。

しかし、今回は別の方法でついにそれっぽい書き方をすることに成功しました。 その方法はずばり、添付プロパティを使う方法です。
WPFには添付プロパティという機構があり、本来そのコントロールが持っているプロパティではない任意のプロパティを作成し、コントロールに添付することができます。Grid.Rowなどのプロパティがその一つで、例えばTextBlockは当然Grid以外にも配置されますからGridの何行目何列目に位置するなんていうプロパティは持っていませんが、添付プロパティを使うことでそういう情報を付加できるようになるものです。

というわけで、添付プロパティで適当なコマンドをListViewItemに添付してしまえば、そこで上手くやることでそのコマンドを発火させることができるのではないかというアプローチになります。

public static class DoubleClickBehavior
{
    public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
        Regex.Replace(nameof(CommandProperty), "Property$", ""),    //末尾のPropertyを消す
        typeof(ICommand),
        typeof(DoubleClickBehavior),
        new FrameworkPropertyMetadata(null, (sender, e)=> {
            Control ctrl = sender as Control;

            if(ctrl != null) {
                ICommand oldCommand = (ICommand)e.OldValue;
                ICommand newCommand = (ICommand)e.NewValue;

                if((oldCommand != null) && (newCommand == null))    //購読を停止
                    ctrl.MouseDoubleClick -= Control_MouseDoubleClick_ForCommand;
                if((oldCommand == null) && (newCommand != null))    //購読を開始
                    ctrl.MouseDoubleClick += Control_MouseDoubleClick_ForCommand; ;
            }
        }));

    public static void SetCommand(DependencyObject obj, ICommand value)
    {
        obj.SetValue(CommandProperty, value);
    }

    public static ICommand GetCommand(DependencyObject obj)
    {
        return (ICommand)obj.GetValue(CommandProperty);
    }

    private static void Control_MouseDoubleClick_ForCommand(object sender, MouseButtonEventArgs e)
    {
        ICommand com = GetCommand((Control)sender);

        if(com.CanExecute(e))
            com.Execute(e);
    }
}

こんな感じです。添付プロパティはDependencyProperty.RegisterAttached()メソッドを使って作った****Propertyという名前のプロパティで、セッターやゲッターはGet****()、Set****()という名前で作るというお作法になっています。そしていずれもstaticです。

ここでミソなのは、添付プロパティのコンストラクタに与えるFrameworkPropertyMetadataクラスは、コンストラクタに値の変更時にその通知を受け取るハンドラを渡すオーバーロードがあるということです。もちろんその通知のsenderは添付プロパティが取り付けられたコントロールになるので、それに対してイベントの購読を行えば、イベントを拾って添付プロパティに投げることが可能になります。

XAMLではこんな感じになります。

<ListView ItemsSource="{Binding Items}" >
    <ListView.View>
        <GridView>
            <!-- 中略 -->
        </GridView>
    </ListView.View>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="b:DoubleClickBehavior.Command" Value="{Binding DoubleClickCommand}" />
        </Style>
    </ListView.ItemContainerStyle>
</ListView>

いたって普通にItemsContainerStyleでプロパティを設定しています。しかしこれが添付プロパティで、上記の通りこれに値をバインディングすることでイベントを購読できるようになります。

それにしてもこのアイディアを知ったときは目から鱗だったなあ。

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