JsonValueのメモalue

導入方法

C#用のJSONパーサーに何が良いのか分からないがMS謹製のSystem.Jsonを使って見る。他にDynamicJsonも良さそうだが開発者自身がSystem.Json禅譲と言っているのでパスした。

System.Jsonは元々SliverLight用のライブラリだったようだが.Net用にもNuGetでインストールして利用できる。下のページにあるようにVisualStudioのNuGetコンソールから叩けば良い。正式版が出ていないようなので-Versionオプションまで含める必要がある

NuGet Gallery | System.Json 4.5.0

他にも別名のJsonValueというものもあるようだがこちらも古いまま宙ぶらりんになっている模様。もし最新の開発状況などご存知なら教えて下さい。

JsonArrayのLINQが効かない

これが本題。JsonArrayはJSONの配列部分を取得した時に、JsonValueの実装サブクラスのインスタンスとして得られる。これはIEnumerableを継承しており、通常通りLINQのSelectやらWhereやらで操作できるはずなのだが、インテリセンスが効かないし、コーディングしてもコンパイルエラーが出る。(list1の箇所)

using System.Json;

namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {

            var json = JsonObject.Parse(@"{ ""Items"" : [ {""Name"":""いちじく""} , {""Name"":""にんじん""} , {""Name"":""さんしょ""} ]}");
            var array = (JsonArray)json["Items"];

            var list1 = array.Select(x => x["Name"].ReadAs<string>());     //コンパイルエラー
            var list2 = ((IEnumerable<JsonValue>)array).Select(x => x["Name"].ReadAs<string>());
            var list3 = array.Select<JsonValue,string>(x => x["Name"].ReadAs<string>());
            var list4 = array.AsJsonArray().Select(x => x["Name"].ReadAs<string>());
            var list5 = json["Items"].AsJsonArray().Select(x => x.ReadAs<string>());

            list4.ToList().ForEach(Console.WriteLine);
        }
    }

    static class JsonValueExtention {
        public static IEnumerable<JsonValue> AsJsonArray(this JsonValue json) { return (IEnumerable<JsonValue>)json; }
    }
}

どうも原因は、JsonArrayはIEnumerableだけでなく、親クラス経由でIEnumerable< KeyValuePair >も継承しているので、どちらのIEnumerableのLINQ操作なのか曖昧なことが原因のようだ。実際にlist2のコードのようにIEnuerableに明示的にキャストすると意図通り動いてくれる。ただ少々面倒くさいし、可読性が落ちる。list3のようにSelect拡張メソッドに明示的に型推論のヒントを与えることでも解決できるが、こちらも少々煩雑。
どうしてもワンライナーのメソッドチェーンで書きたければ、list4のようにキャスト用の拡張メソッドを用意してやるのがスマートかもしれない。list5のようにより短く書くこともできる。
なお、System.Json名前空間のJsonValueLinqExtentionsクラスにToJsonArray拡張メソッドがあるが、これはどうも目的が全く違いもののようなので自作が必要だ。

備忘録

個人的に直ぐ忘れて毎回調べていること、トラブルにはまったときに思い出す注意点を備忘録代わりにかいておく。

デバッグ中にローカル変数ウィンドウなどで変数の中身を見せるための記述

DebuggerDisplay属性を使う
デバッグ時にプロパティ値やフィールド値を簡単に確認できるようにするには?[C#、VB] - @IT

VisualStateManagerがうまく動かない場合

どうもTemplateの中でVisualStateManagerを使わないとだめみたい(未検証)

アニメーションやトリガーが効かない

例えば

TemplateBindingとTemplateParentの違い

"{TemplateBinding Property=Background}"と"{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Background}"はよく似ているが厳密には機能が違う。詳しくは下に説明がある。MSDNの解説では駄目だな。

WPF TemplateBinding vs RelativeSource TemplatedParent - Stack Overflow

元々どちらもテンプレート定義の中から、テンプレートが適用されるコントロール(親)のプロパティにバインディングしたいときに使う。いろいろ説明があるようだけど、端的にわかりやすいのは2Wayバインディングがしたいときには後者の書き方じゃ無いとだめみたいだ。

ListBoxの選択状態の変更

以前からの疑問が解消したのでメモ。

背景知識の説明

まずWPFでのListBox(というか親クラスのItemsControl)とリスト(IListとか)の関係を整理する。ListBox.ItemsSourceにリストをぶち込むとListBoxにリスト項目の一覧が表示される。単純明快。

ただし、舞台裏では少々複雑なことが行われている。ItemsSouceに渡したリストが自動的にItemCollectionに変換され、これがListBoxで参照される。ItemsCollectionはリストのラッパーでListBoxやItemsCollectionに不可欠な選択状態(現在選択中の項目への参照)やソート結果(並び順の状態)を保持するために使われる。つまり内部ではListBoxが直接リストを参照していないことになる。お陰で特殊なクラスから派生する必要も無く普通のIListや配列を渡すだけで、WPFのリッチな機能(ソートやグループ化)が使えるのだ。なのでこれらの機能をコントロールする場合にはItemCollectionオブジェクトの取得が必要になる。これにはCollectionViewSource.GetDefaultViewメソッドで取得できる。

余談ながらCollectionViewSourceクラスとの関係を言うと、ItemsCollectionはXAML上では使えずC#などコードからの利用が前提となる。このためXAML上でItemCollectionを生成してListBox.ItemsSourceに食わせるのにCollectionViewSourceを使うことになる。

ListBoxの選択状態

それで本題。以前から分からなかったことが一つあった。ListBoxのように複数選択するタイプのItemsControlでの選択状態の制御だ。ListBox上で選択された要素が知りたければListBox.SelectedItemsプロパティでリストとして取得できる。

ただ選択状態をコードで制御するのがよく分からない。読取り専用なので設定できない。単一項目版のSelectedItemプロパティの方は設定もできるのだが、複数項目選択の変更に困る。ListBoxの内部では状態を持っているのは間違いないのだが、ListBoxのAPIにはそれらしいものが見当たらない。ソート機能のようにItemsCollectionが担当しているかと想像したがここにも該当するものが無い。ListBox専用の特殊な子クラスがあるのかと思っていたがこれも見当たらない。これが長らくの謎だった。

チェックボックスリスト

別件でチェックボックスリストのサンプルを探していた。するとこんなのがあった。
WPF/コンポーネント/コントロール/チェックボックスリストボックス - プログラミング図書館・本館 - アットウィキ

ここではItemsTemplateの中のチェックボックスコントロールをListBoxの選択状態にバインディングしている。なるほど。バインディング先は(Selector.IsSelected)となっている。これはSelectorクラス(ListBoxの親クラス)が持っている添付プロパティだ。

謎の解明

添付プロパティの詳細説明は省略するが、任意のクラス(正確にはDependencyObjectのサブクラス)に後付けできる仮想的なプロパティのようなものと言えば良いか。具体的にはDockPanelの表示位置を指定する際に使うDockPanel.Dockというプロパティが添付プロパティだ。Dockプロパティ自体はDockPanelが実際に保有しているが、貼り付け先のオブジェクトに関連づけて持たせる事ができるので、さも貼り付け先のオブジェクトが持っているかのようにXAML上では扱える。

なので選択状態を持っているのは誰だという疑問はぐるっと回ってやはりListBoxが持っていたのであった。なるほど。上の説明になっているか分からないけどちょっとサンプルコードを書いておく。

 <ListBox Name="MyListBox" SelectionMode="Multiple" >
     <ListBoxItem Content="いち" Selector.IsSelected="True" />
     <ListBoxItem Content="にぃ" />
     <ListBoxItem Content="さん" Selector.IsSelected="True"  />
     <ListBoxItem Content="よん" />
 </ListBox>

Selector.IsSelectedを設定した項目だけ選択状態にめでたくなっているのが分かる。

なおC#コードからは次のように書ける。

Selector.SetIsSelected((DependencyObject) MyListBox.Items[1], true);

背景が透けているウィンドウを作る

今回はウィンドウの背景色を透明にして、デスクトップが透ける効果のあるウィンドウデザインを紹介します。タイトルバーを消すと(というか消さざるを得ないのですが)ガジェットのような雰囲気が出ます。

Windowコントロールの設定

ウィンドウ背景の透過効果を有効にするには次のWindowコントロールのプロパティ設定が必要です。

  1. AllowsTransparency を True にする
  2. WindowStyle を None にする(必須)
  3. Background を Transparentにする(あるいはOpacityを1.0未満に設定するなど透明度を設定する)

また、ウィンドウの外形や背景効果をカスタマイズするために背景用の要素を配置します。今回は角丸の四角形にするためRectangleを使いました。お好みでPathを使って星型にするなどできますのでカスタマイズの自由度は高いと思います。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" 
        Loaded="Window_Loaded" MouseRightButtonDown="Window_MouseRightButtonDown"
        SizeToContent="WidthAndHeight" Background="Transparent"
        WindowStyle="None" AllowsTransparency="True" >
    
    <Grid>
        
        <!--ウィンドウのドラッグ移動用のThumb-->
        <Thumb Name="WindowBackground" DragDelta="WindowBackground_DragDelta">
            <Thumb.Template>
                <ControlTemplate>
                    <!--ウィンドウ背景のデザイン-->
                    <Rectangle RadiusX="20" RadiusY="20"
                               Fill="Black" Opacity="0.5"/>
                </ControlTemplate>
            </Thumb.Template>
        </Thumb>
        
        <!--通常のウィンドウコンテンツ-->
        <Grid>
            <TextBlock Name="TimeDisplay" Foreground="LightGreen"
                       FontFamily="Century" FontSize="40" 
                       HorizontalAlignment="Center"
                       VerticalAlignment="Center" 
                       Margin="50,30"
                       Text="12:34:56">
                <TextBlock.Effect>
                    <DropShadowEffect />
                </TextBlock.Effect>
            </TextBlock>
        </Grid>

    </Grid>
</Window>

ウィンドウ操作

上の設定でタイトルバーが(表示したくても)表示されなくなります。このためウィンドウの移動や閉じるなどの標準操作ができなくなりますので個別に代替手段を提供してやる必要があります。
今回はドラッグでウィンドウ移動をできるように背景図形を兼ねたThumbコントロールを入れています。また右クリックでウィンドウを閉じてアプリを終了していますが、本来であれば右クリックメニューや閉じるボタンがある方が適切かもしれません。

        //ウィンドウのドラッグでウィンドウ移動
        private void WindowBackground_DragDelta(object sender, DragDeltaEventArgs e) {
            Left += e.HorizontalChange;
            Top += e.VerticalChange;
        }

        //右クリックでアプリケーション終了
        private void Window_MouseRightButtonDown(object sender, MouseButtonEventArgs e) {
            this.Close();
        } 
    }

サンプルの作成

今回の完成イメージはガジェット風のデジタル時計に仕立ててみました。本題では無いので少々大雑把です。

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            //時計更新処理のスレッドをテキトーに作る
            var t = new Thread(() => {
                while (true) {
                    Dispatcher.Invoke((Action)(() => TimeDisplay.Text = DateTime.Now.ToLongTimeString()), null);
                    Thread.Sleep(1000);
                }
            });
            Closing += (_, __) => t.Abort();    //ウィンドウ閉鎖時に更新スレッドを停止する
            t.Start();
        }
別スレッドからのコントロールプロパティの更新処理

時計表示を1秒おきに更新するよう簡単にスレッドを立ち上げています。アニメーション効果を入れるなら、アニメーションの完了イベントを利用しても良いと思います。

なお、WPFやWinFormでコントロールのプロパティ変更などはメインスレッドでのみ許されています。WPFで別スレッドから更新する場合にはDispatcherクラスのInvokeやBeginInvokeメソッドを使えば良いです。

Delegate型の引数でのラムダ式の利用

Invokeメソッドの第一引数はDelegate型です。ラムダ式や匿名メソッド〔delegate(〜){〜}〕が直接使えません。次のようなコンパイルエラーが出ます。

ラムダ式 はデリゲート型ではないため、型 'System.Delegate' に変換できません。

この場合には適当なデリゲート型に明示的にキャストすれば良いようです。つまりラムダ式からDelegete型には暗黙の型変換をしてくれないということのようです。
ControlクラスのInvokeメソッドで匿名メソッドを使うには?[2.0のみ、C#] - @IT

Closingイベント

ウィンドウを閉じた際に時計更新用のスレッドを停止させないとアプリケーションが起動したままになってしまいます。停止の処理は1行だけで単純なのでこのようなイベントハンドラの登録にしてみました。

リサイズハンドルをAdornerで実装する

今回はパワーポイントのようなオブジェクトのサイズ変更可能なリサイズハンドルを作ってみます。

Adonerの利用方法と実装のサンプル

まず、サイズ変更対象のオブジェクトの四隅にドラッグ可能なコントロール(リサイズ・ハンドル)を作ります。WPFではこのような用途にAdornerという仕組みが用意されています。ただしAdornerの実装サンプルを調べてみてもあまりヒットしません。またXAMLではなくコードベースのものしか見当たりません。

ResizingAdorner のサンプル | Microsoft Docs
http://denisvuyka.wordpress.com/2007/10/15/wpf-simple-adorner-usage-with-drag-and-resize-operations/

そもそもAdorner自体がXAMLで使うようになっていないので仕方がないと言えるのですが、装飾部分までもコードで書くのではカスタマイズが大変です。まず装飾部分をXAML上でテンプレートで定義できるようにAdornerを拡張するところから始めます。

AdornedByクラスの準備

まずTemplate添付プロパティを作ります。添付プロパティの説明は割愛しますが既に馴染みがあると思います。

AdornedByの使い方

AdornedByを使って今回のリサイズハンドルの表示をXAMLで実装してみます。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        
        <!--リサイズハンドル用のテンプレート定義-->
        <ControlTemplate TargetType="Thumb" x:Key="ResizeHandleTemplate">
            <Ellipse Width="10" Height="10" Margin="-3"
                     Stroke="DimGray" Fill="LightSteelBlue"/>
        </ControlTemplate>
        
        <!--装飾用のテンプレート定義-->
        <ControlTemplate x:Key="AdornerTemplate">
            <Grid>
                <Thumb Name="ResizeThumb_LT" 
                       HorizontalAlignment="Left" VerticalAlignment="Top"
                       Template="{StaticResource ResizeHandleTemplate}"
                       DragDelta="ResizeThumb_DragDelta"/>
                <Thumb Name="ResizeThumb_RT" 
                       HorizontalAlignment="Right" VerticalAlignment="Top"
                       Template="{StaticResource ResizeHandleTemplate}"
                       DragDelta="ResizeThumb_DragDelta"/>
                <Thumb Name="ResizeThumb_LB" 
                       HorizontalAlignment="Left" VerticalAlignment="Bottom"
                       Template="{StaticResource ResizeHandleTemplate}"
                       DragDelta="ResizeThumb_DragDelta"/>
                <Thumb Name="ResizeThumb_RB" 
                       HorizontalAlignment="Right" VerticalAlignment="Bottom"
                       Template="{StaticResource ResizeHandleTemplate}"
                       DragDelta="ResizeThumb_DragDelta"/>
            </Grid>
        </ControlTemplate>

    </Window.Resources>
    
    <Canvas>
        <!--単純な利用:装飾対象には添付プロパティを設定する-->
        <Button Name="Target"
                Canvas.Left="100" Canvas.Top="20"
                Width="100" Height="40"
                Content="サイズ変更可" 
                my:AdornedBy.Template="{StaticResource AdornerTemplate}"/>

        <!--フォーカスで装飾を有効にする場合-->
        <Button Name="Target2"
                Canvas.Left="100" Canvas.Top="80"
                Width="100" Height="40"
                Content="フォーカスで">
            <Button.Style>
                <Style TargetType="Button">
                    <Style.Triggers>
                        <Trigger Property="IsFocused" Value="True">
                            <Setter Property="my:AdornedBy.Template"
                                    Value="{StaticResource AdornerTemplate}"/>
                        </Trigger>
                    </Style.Triggers> 
                </Style>
            </Button.Style>
        </Button>
                
    </Canvas >
    
</Window>

ThumbコントロールはMarginを負値にすることで少し外側に配置しています。また二つ目のボタンではフォーカス時のみリサイズハンドルを表示しています。

リサイズハンドルのイベント処理です。

        //リサイズハンドルのイベント処理
        private void ResizeThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) {

            var thumb = sender as Thumb;
            if (thumb == null) return;

            //サイズ変更の対象要素を取得する
            var adored = AdornedBy.GetAdornedElementFromTemplateChild(thumb) as FrameworkElement;
            if (adored == null) return;

            //サイズ変更処理(横)
            if (thumb.Name == "ResizeThumb_LT" || thumb.Name == "ResizeThumb_LB") {
                Canvas.SetLeft(adored, Canvas.GetLeft(adored) + e.HorizontalChange);
                adored.Width = Math.Max(20, adored.Width - e.HorizontalChange);
            } else {
                adored.Width = Math.Max(20, adored.Width + e.HorizontalChange);
            }

            //サイズ変更処理(たて)
            if (thumb.Name == "ResizeThumb_LT" || thumb.Name == "ResizeThumb_RT") {
                Canvas.SetTop(adored, Canvas.GetTop(adored) + e.VerticalChange);
                adored.Height = Math.Max(20, adored.Height - e.VerticalChange);
            } else {
                adored.Height = Math.Max(20, adored.Height + e.VerticalChange);
            }
            e.Handled = true;
        }

iPhone風のボタン付きリストボックスを作る

前回にListBoxのリストアイテムを枠いっぱいに引き伸ばす方法を紹介しました。今回はこれを使ってiPhone風の右端にボタンがあるリストボックス(下図)を作ってみます。

リストアイテムをリストボックス幅にフィットさせる

前回紹介したようにListBox.HorizontalContentAlignment = Strechを設定すれば枠いっぱいに引き伸ばされます。

ただ一点不具合があります。リストアイテムのテキストが長い場合にはリストボックス幅よりも大きくなってしまいます。この結果、右端のボタンがリストボックスからはみ出てしまい表示されなくなります。これを防ぐために、ScrollViewer.HorizontalScrollBarVisibility = Disabled を入れます。横スクロールが無効になり、スクロールバーが表示されなくなるのと同時に、リストアイテムはリストボックス幅に縮められます。

        <ListBox ItemTemplate="{StaticResource ListDataTemplate}"
                 ItemsSource="{StaticResource ListData}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 HorizontalContentAlignment="Stretch" />

レイアウト設定

丸ボタンを右端に配置し、余ったスペースをTextBlockに割り当てるためにDockPanelを使いました。テキスト以外の部品が増えるようならGridパネルなどの方が分かりやすくなるかも知れません。

<Window x:Class="wpf110421IconInListBox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="200">

    <Window.Resources>

        <!--雰囲気を出すためのスタイル定義(適当)-->
        <LinearGradientBrush x:Key="BackgroundBrush1" 
                             EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="White" Offset="0" />
            <GradientStop Color="CadetBlue" Offset="1" />
        </LinearGradientBrush>
        <LinearGradientBrush x:Key="BackgroundBrush2" EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="#FF96CBFF" Offset="0" />
            <GradientStop Color="#FF0B2B4B" Offset="1" />
        </LinearGradientBrush>
        <!--雰囲気を出すためのスタイル定義(丸ボタン)-->
        <ControlTemplate x:Key="BallButtonTemplate" TargetType="Button">
            <Grid>
                <Ellipse Fill="{StaticResource BackgroundBrush2}" />
                <Ellipse Margin="1" Stroke="LightSteelBlue" StrokeThickness="2" />
                <TextBlock Text="&gt;" 
                           FontSize="9" Foreground="White" 
                           HorizontalAlignment="Center" 
                           VerticalAlignment="Center" />
            </Grid>
        </ControlTemplate>


        <!--リストボックス用のテンプレート-->
        <DataTemplate x:Key="ListDataTemplate">
            <Grid>
                <!--リストアイテムの背景-->
                <Rectangle RadiusX="5" RadiusY="5" 
                           Fill="{StaticResource BackgroundBrush1}" />
                <DockPanel Margin="4">
                    <!--丸ボタン:右寄せ-->
                    <Button DockPanel.Dock="Right" 
                            Width="20" Height="20"
                            Template="{StaticResource BallButtonTemplate}" />
                    <!--リストアイテムのテキスト表示-->
                    <TextBlock Text="{Binding}" VerticalAlignment="Center" />
                </DockPanel>
            </Grid>
        </DataTemplate>

        <!--リストボックス用のデータ-->
        <x:Array xmlns:sys="clr-namespace:System;assembly=mscorlib"
                 x:Key="ListData" Type="sys:String">
            <sys:String></sys:String>
            <sys:String>いい</sys:String>
            <sys:String>ううう</sys:String>
            <sys:String>あいうえおかきくけこさしすせそ</sys:String>
        </x:Array>
        
    </Window.Resources>

    
    <Grid>
        <!--データテンプレートの幅をスクロール領域全幅にフィットさせる-->
        <ListBox ItemTemplate="{StaticResource ListDataTemplate}"
                 ItemsSource="{StaticResource ListData}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 HorizontalContentAlignment="Stretch" />

    </Grid>
    
</Window>

雰囲気を出すため冒頭で背景グラデーションやボタンのスタイルを定義していますがテキトーです。この辺を無視すればずいぶんシンプルに実現できていると感じると思います。

動作させて、ウィンドウ枠をいろいろ変えてみるとリストボックスの大きさに合わせて丸ボタンが追従しているのが分かります。また高さをすぼめて縦スクロールバーが出てきてもうまく避けています。

参考:その他の強引な方法

今回はListBoxのプロパティで上手く解決しましたが、データテンプレートの幅を親スクロール領域の幅に強引にバインディングする方法もあります。知っていると何かの際に役に立つかも知れませんので簡単に紹介します。

        <DataTemplate x:Key="ListDataTemplate">
            <Grid Margin="-2,0"
                  Width="{Binding RelativeSource={RelativeSource AncestorType=ScrollViewer}, Path=ViewportWidth}" >
                      :
                    (省略)

親要素のScrollViewerオブジェクトを見つけてきて、データテンプレート直下のパネルのWidthにバインディングしています。またこのままだと僅かに大きくなってしまい横スクロールバーが出てしまいますので、Marginに負値を入れて一回り小さなサイズにしています。