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
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
どうしてもワンライナーのメソッドチェーンで書きたければ、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
ただし、舞台裏では少々複雑なことが行われている。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コントロールのプロパティ設定が必要です。
- AllowsTransparency を True にする
- WindowStyle を None にする(必須)
- 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添付プロパティを作ります。添付プロパティの説明は割愛しますが既に馴染みがあると思います。とか
今回の実装ではAdornerdBy.Template=〜を付けた要素を装飾対象になります。またTemplateにはControlTemplateを渡しますがこれが装飾部分の表示に使われます。
namespace WpfApplication1 { public class AdornedBy : Adorner { //添付プロパティの実装 public static readonly DependencyProperty TemplateProperty = DependencyProperty.RegisterAttached("Template", typeof(ControlTemplate), typeof(AdornedBy), new FrameworkPropertyMetadata(null, OnTemplateChanged)); public static ControlTemplate GetTemplate(DependencyObject obj) { return (ControlTemplate)obj.GetValue(TemplateProperty); } public static void SetTemplate(DependencyObject obj, ControlTemplate value) { obj.SetValue(TemplateProperty, value); } //テンプレート描画用のControlオブジェクトへの参照 private FrameworkElement _Content; //コンストラクタ public AdornedBy(UIElement adornedElement) : base(adornedElement) { } //添付プロパティTemplateの設定時に初期化処理を行う private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var adorned = d as FrameworkElement; var me = new AdornedBy(adorned); //装飾層に登録する if (adorned.IsInitialized) { me.AddToAdornerLayer(); }else{ //初期化中の場合は登録処理を遅延させる adorned.Loaded += (_, __) => me.AddToAdornerLayer(); } //子Controlオブジェクトを生成して設定されたテンプレートを設定する var t = e.NewValue as ControlTemplate; var ctrl = new Control { Template = t }; me._Content = ctrl; me.AddVisualChild(ctrl); me.AddLogicalChild(ctrl); me.InvalidateVisual(); } //装飾層に登録する private void AddToAdornerLayer(){ AdornerLayer layer = AdornerLayer.GetAdornerLayer(AdornedElement); if (layer == null) throw new InvalidOperationException("XAML tree must have at lest one AdornerDecorator."); //既存の装飾を除去 Adorner[] registed = layer.GetAdorners(AdornedElement); if (registed != null) { foreach (var ad in registed) { if (ad is AdornedBy) layer.Remove(ad); } } //装飾を登録 layer.Add(this); } //テンプレート中の要素から装飾対象を取得するヘルパーメソッド public static UIElement GetAdornedElementFromTemplateChild(FrameworkElement contained) { var tp = contained.TemplatedParent as FrameworkElement; if (tp == null || tp.GetType() != typeof(Control)) return null; var me = tp.Parent as AdornedBy; if (me == null) return null; return me.AdornedElement; } //サイズ計測処理の実装 (テンプレートの大きさを装飾対象に一致させる) protected override Size MeasureOverride(Size constraint) { return AdornedElement.DesiredSize; } //配置処理の実装 (テンプレートの位置を装飾対象に一致させる) protected override Size ArrangeOverride(Size finalSize) { _Content.Arrange(new Rect(AdornedElement.DesiredSize)); return AdornedElement.DesiredSize; } //描画されるために不可欠なので実装をしておく protected override int VisualChildrenCount { get { return 1; } } protected override Visual GetVisualChild(int index) { return _Content; } } }
添付プロパティが設定された時にAdornerの初期化処理をします。具体的にはAdonerを継承したAdornedByクラスのインスタンスを生成し、装飾表示用のレイヤーを探して登録します。またAdornedByの直下にControlクラスのインスタンスを作りTemplateの値を設定します。構造は次のような感じになります。
Window ├ 装飾層(AdornerDecorator) │ └ AdornerdBy (Adorner) │ └ Control │ └ ControlTemplate (Teplateの中身) └ 装飾対象
適切に装飾を表示させるために、いくつかのメソッドをオーバーライドしています。
MeasureOverride / ArrangeOverride
レイアウト機構に関係するもので装飾表示のサイズや位置合わせに関係します。
MeasureOverrideは装飾が要求するサイズを回答して、サイズを装飾対象と一致させます。ArrangeOverrideは表示位置の調整に関係しますが今回は特別なことはしていません。
GetVisulaChild / VisualChildrenCount
AdornerクラスがFrameworkElementクラスを継承していないので実装しないと子要素が適切に描画されないようです。
またAddLogicalChildを呼び出していますが、これはControl.Parentで値を取得するために必要です。なお今回は踏み込みませんがWPFにはビジュアルツリーと論理ツリーがあります。ビジュアルツリーは描画処理に、論理ツリーはParentやTemplatedParentあたりに関係します。
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=">" 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に負値を入れて一回り小さなサイズにしています。