午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

ListBoxで棒グラフ、MultiBindingを使ってListBox幅と要素幅を連動

ListBoxで棒グラフ…ちょっと何言ってるか分からないですね…
イメージ 1
MultiBingingを使って
ListBoxの幅に合わせて要素の幅を変更している
上半分のListBoxは失敗例で
下半分が期待通りにできたListBox
 
Bindingソース1 ListBoxのActualWidth
Bindingソース2 DataContext、ここでは0.5とか0.8
Bindingターゲット ListBoxの要素のWidth
 
 
MainWindow.xaml

f:id:gogowaten:20191213163756p:plain

<Window x:Class="_20190321_ListBoxで棒グラフ_比率で伸縮.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20190321_ListBoxで棒グラフ_比率で伸縮"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400">
  <Window.Resources>
    <local:MyConverter x:Key="myConv"/>
    <local:MyMultiConverter x:Key="myMultiConv"/>
  </Window.Resources>
  <Grid>
    <StackPanel>
      <ListBox Name="MyListBox1" ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel>
              <TextBlock Text="{Binding}"/>
              <Border Background="MediumOrchid" Height="10"
                      Width="{Binding}"/>
              <!--以下はエラーになる、ConvererParameterにBingingは使えない-->
              <!--<Border Grid.Column="0" Background="MediumAquamarine" Height="10"
                Width="{
                  Binding ConverterParameter={Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBox},
                  Path=ActualWidth},
                  Converter={StaticResource myConv}
                }"/>-->

            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>

      <ListBox Name="MyListBox2" ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel>
              <TextBlock Text="{Binding}"/>
              <Border Background="MediumAquamarine" Height="10">
                <Border.Width>
                  <MultiBinding Converter="{StaticResource myMultiConv}">
                    <Binding ElementName="MyListBox2" Path="ActualWidth"/>
                    <Binding/>
                  </MultiBinding>
                </Border.Width>
              </Border>
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>

    </StackPanel>
  </Grid>
</Window>
 
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace _20190321_ListBoxで棒グラフ_比率で伸縮
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            //ContentRenderedイベントでデータバインディングするのは
                    //Bindingソースには実際に表示されたListBoxの横幅を使うから
            ContentRendered += MainWindow_ContentRendered;

        }

        //ContentRenderedイベントでデータバインディング
        private void MainWindow_ContentRendered(object sender, EventArgs e)
        {
            List<double> myData = new List<double> { 0.5, 0.8, 0.3, 1.0 };
            DataContext = myData;//データバインディング
        }
    }
    //未使用
    public class MyConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double width = (double)parameter;
            double rate = (double)value;
            return (width * rate) - 20;//-20はパディングみたいなもの、適当に
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    //使用、2つのソースからターゲットに送る値に変換
    //MultiBindingのConverterで使う
    public class MyMultiConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double width = (double)values[0];
            double rate = (double)values[1];
            //return (width * rate) - 20;//-20はパディングみたいなもの、適当に
            return (width - 16) * rate;//こっちのほうがいい
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

}
 
 
 
イメージ 3
棒グラフはListBoxのItemTemplateに設定したBorderを使って表現
そのBorderのWidthにMultiBindingを使っている
 
 
失敗例

f:id:gogowaten:20191213163903p:plain

同じくBorderのWidthにBindingで
ConverterのConverterParameterにListboxのActualWidthを使おうとしているのが
25、26行目なんだけど
これはエラーになる
イメージ 5
ConverterParameterにDependencyProperty以外を使っているのが良くないみたい、だけどどうすればいいのかわからん
 
よくなさそうなところ
"ConverterParameter={Binding RelativeSource"
でぐぐったら
 
wpfバインディングConverterParameter - コードログ
https://codeday.me/jp/qa/20181201/36915.html
ConverterParameterプロパティは依存関係プロパティではないため、バインドできません。
しかし、代替の解決策があります。通常のBindingの代わりにmulti-value converterを使用することができます:
言われてみれば、なるほどねえってなるけど、全然思いつかなかったよ
 
 
 
 
実際に使うときはこんな感じ
MainWindow.xaml

f:id:gogowaten:20191213163918p:plain

<Window x:Class="_20190321_Listboxで棒グラフ2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20190321_Listboxで棒グラフ2"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="400">
  <Window.Resources>
    <local:MyMultiConverter x:Key="myMultiConv"/>
  </Window.Resources>
  <Grid>
    <StackPanel>
      <ListBox Name="MyListBox" ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel>
              <TextBlock Text="{Binding Value}"/>
              <Border Background="MediumOrchid" Height="10" HorizontalAlignment="Left">
                <Border.Width>
                  <MultiBinding Converter="{StaticResource myMultiConv}">
                    <Binding ElementName="MyListBox" Path="ActualWidth"/>
                    <Binding Path="Rate"/>
                  </MultiBinding>
                </Border.Width>
              </Border>
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </StackPanel>
  </Grid>
</Window>
これはさっきのと基本は変わりなし
BorderのHorizontalAlignmentにleftを指定したのと
18行目、BindingのPathに数値表示のTextBlockにValue
23行目、BorderのPathにRateを指定、これは幅の割合
 
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace _20190321_Listboxで棒グラフ2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();


            //ContentRenderedイベントでデータバインディングするのは
            //Bindingソースには実際に表示されたListBoxの横幅を使うから
            ContentRendered += MainWindow_ContentRendered;

        }
        //ContentRenderedイベントでデータバインディング
        private void MainWindow_ContentRendered(object sender, EventArgs e)
        {
            List<double> dList = new List<double>() { 1100, 2340, 330, 5328 };
            double max = dList.Max();
            List<MyData> myDatas = new List<MyData>();
            foreach (var item in dList)
            {
                myDatas.Add(new MyData(item, max));
            }
            DataContext = myDatas;//データバインディング
        }
    }
    //使用、2つのソースからターゲットに送る値に変換
    //MultiBindingのConverterで使う
    public class MyMultiConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            double width = (double)values[0];
            double rate = (double)values[1];
            //return (width * rate) - 20;//-20はパディングみたいなもの、適当に
            return (width - 16) * rate;//こっちのほうがいい
        }
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    public class MyData
    {
        public double Value { get; set; }//表示する値
        public double Rate { get; set; }//割合、比率用

        public MyData(double value, double max)
        {
            Value = value;
            Rate = value / max;
        }
    }
}
データバインディングに使う値を実際の値と、表示幅割合指定用の値、この2つに分けた
 
実行すると
イメージ 7
伸ばす
イメージ 11
いいねえ
 
縮めると
イメージ 8
一番小さい値の330の棒グラフは消えてしまった
これは
イメージ 9
Converterの45行目で幅を-20しているから
じゃあなくせばいいかとすると
イメージ 10
最大幅になる要素がListBoxと同じ幅になって、常にスクロールバーが表示されてしまう
これは最大でもListBox幅の0.99倍とかにするか、最低幅を1にすればいいかも
 
最後に-20するんじゃなくて元の幅から-16程度したものの割合にすれば
イメージ 13
50行目をやめて51行目
割合が0.0にならない限り0にはならないから
当たり前なんだよなあ、うっかり
でもこれで
イメージ 12
ちょうど良くなった
 
 
 
ここまでできてみれば難しくないんだけど、最初はGrid.ColumnDefinitionsでなんとかしようとしたり遠回りしてた
あとはWPFにもグラフ表示するコントロールもあったはずで、それを使ったほうが速いかもしれないけど、最近ListBoxばかり使っていたからListBoxから離れられなかったw
 
 
 
 
ギットハブ
今回のアプリのダウンロード先
 
 
 
 
 
 
 
関連記事
2019/3/5は16日前
WPFのListBoxでいろいろ、Binding、見た目の変更、横リスト ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15893148.html
ListBox.ItemTemplateを使って要素の表示を変更