【WPF(XAML)C#4】DataGridの使い方

セクション4は初心者なら必ず設定に苦戦するであろうDataGridの紹介です。本当はListViewもこのセクションでやろうかと思いましたがDataGridは設定する内容が多く、非常に長くなってしまったのでセクションを分ける事にしました。とはいえ、ListViewは一般的にはDataGrid程、使用頻度は高くないので今回はDataGridの運用方法を集中的に覚えましょう。

また、このページはコードが多いので複数のウィンドウで開いて頂き、コードと見比べながら見ていく事をオススメ致します。(これはPC向け案件。スマホの方はすみません><。)

DataGrid

下の画像を見てください。

このお天気一覧はDataGridで作られたものです。DataGridとは表のような機能でタイトルの所はちょっと手抜きしてますが、見た目的には業務の成果物に近い感じではないでしょうか?実はこの時点で結構設定いじってます。

設定を何もいれないと下記のような悲惨な状態になるので見てみてください。

Σ(・□・;)めっちゃエクセルのような表だし、内部のプログラム変数も一緒に表示されてるし、ヤバい!

既に実績のあるプロジェクトなら共通設定ファイルみたいな所でDataGridのデフォルトStyleを定義している事が多いので最初から整形されているのですが例えばソース個別に独自にStyleを定義する時はデフォルトStyleが適用されないので、どうしたらちゃんと整形されたDataGridになるのか長時間悩みます

今回はヤバい表っぽいものからどうやって整形を行えたか流れを追って見ていきたいと思います。

DataGridの最低限の構成とは?

いかにも表らしい表だった設定を入れる前のソースは以下のようになります。(共通部分は省略しました)ただし、天気アイコンだけは数値のまま見せるのもアレだったので適用しています。こちらも解説しますね。

<DataGrid x:Name="WeatherGrid"
          Grid.Column="1"
          ItemsSource="{Binding gridSource}"
          Margin="0,10"
          >
    <DataGrid.Resources>
        <Style x:Key="WeatherIconStyle" TargetType="{x:Type Image}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding weather}" Value="1">
                    <Setter Property="Source" Value="../Image/16_2_19.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="2">
                    <Setter Property="Source" Value="../Image/cloudy.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="3">
                    <Setter Property="Source" Value="../Image/rainy.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="4">
                    <Setter Property="Source" Value="../Image/sunder.png"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
        <DataTemplate x:Key="WeatherTemplate">
            <StackPanel>
                <Image Style="{DynamicResource WeatherIconStyle}" Width="25"/>
            </StackPanel>
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn Header="都道府県" Width="60" Binding="{Binding prefLabelPref}"/>
        <DataGridTextColumn Header="市区町村" Width="90" Binding="{Binding municipalityLabel}"/>
        <DataGridTemplateColumn Header="天気" CellTemplate="{StaticResource WeatherTemplate}" Width="50"/>
        <DataGridTextColumn Header="気温" Width="40" Binding="{Binding temperature, StringFormat={}{0:N0}℃, TargetNullValue=不明}"/>
        <DataGridTextColumn Header="降水確率" Width="50" Binding="{Binding rainyPercent, StringFormat={}{0:N0}%, TargetNullValue=不明}"/>
    </DataGrid.Columns>
</DataGrid>

上記コードの中で色が変わっている所がDataGridの記述です。これは天気関連以外では最低限の部分です。これだけでも結構長いですね…。

そして下はC#側のソースです。コードを見ると都道府県のデータは直設定していますが実務の場合は、きちんと都市の天気取得APIか何かを呼んで取得したデータをforeachして設定していくのが一般的ですね。

namespace TestApp.main
{
    /// <summary>
    /// Window1.xaml の相互作用ロジック
    /// </summary>
    public partial class DataGridWindow : Window
    {
        DataGridWindowViewModel MyVM = new DataGridWindowViewModel();

        public DataGridWindow()
        {
            InitializeComponent();

            DataContext = MyVM;
        }

        private void DataGridWindow_Loaded(object sender, RoutedEventArgs e)
        {
            // 市区町村を設定
            var setItem = new DataGridPracticeDto()
            {
                prefLabelPref = "北海道",
                municipalityLabel = "札幌市",
                weather = 1,
                temperature = 28,
                rainyPercent = 10
            };

            MyVM.gridSource.Add(setItem);

       ~中略~

            setItem = new DataGridPracticeDto()
            {
                prefLabelPref = "広島県",
                municipalityLabel = "広島市",
                weather = 1,
                temperature = 29,
                rainyPercent = 50
            };

            MyVM.gridSource.Add(setItem);
        }

        private void WindowClose_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }

    public class DataGridWindowViewModel : BindableBase
    {
        private ObservableCollection<DataGridPracticeDto> _gridSource = new ObservableCollection<DataGridPracticeDto>();
        public ObservableCollection<DataGridPracticeDto> gridSource
        {
            get => _gridSource;
            set => SetProperty(ref _gridSource, value);
        }
    }

    public class DataGridPracticeDto : BindableBase
    {
        /// <summary>
        /// 都道府県
        /// </summary>
        public string prefLabelPref { get; set; }

        /// <summary>
        /// 市区町村
        /// </summary>
        public string municipalityLabel { get; set; }

        /// <summary>
        /// 天気
        /// 1 = 晴れ/2 = 曇/3 = 雨/4 = 雷
        /// </summary>
        public int weather { get; set; }

        /// <summary>
        /// 気温
        /// </summary>
        public int? temperature { get; set; }

        /// <summary>
        /// 降水確率
        /// </summary>
        public double? rainyPercent { get; set; }
    }
}

天気アイコンはどうやって表示させたの?

コードを見ればおわかりだと思いますがcs側で設定しているお天気のプロパティはint型の数値です。ですので普通に考えれば「アイコンなんて表示できなくない?」となるのですがXAMLには渡されたパラメータの値で対象のクラス(この例では画像を表示させるImageクラス)のプロパティの値を変更させる事が可能です。(XAML版のif文と考えてもらえばOKです)

これ以降の説明は上のXAML側のコードを別ウィンドウに表示して下の説明を見てくださるとわかりやすいと思います。

まず実際表示している<DataGridTemplateColumn Header=天気” CellTemplate=”{StaticResource WeatherTemplate}”ですがこのDataGridTemplateColumnは「自分で表示の仕方を定義します」というプロパティです。ちなみにDataGridTextColumnは「文字」を表示するプロパティです。で、定義した部分は少し上のWeatherTemplateの内容を表示しています。

さらに見ていきましょう。前回の記事のお天気マークはImageに直接Sourceで画像の場所をしていましたが今回はStyle=”{DynamicResource WeatherIconStyle}” を定義しているだけです。これはImageに設定するプロパティは別の場所(今回ならWeatherIconStyle)で設定しますという意味です。ちなみに下のコードのように別出しせずに直接Styleを定義することも可能です。

<DataTemplate x:Key="WeatherTemplate">
    <StackPanel>
        <Image Width="25">
            <Image.Style>
                <Style TargetType="{x:Type Image}">
                    <Setter Property="Source" Value="../Image/16_2_19.png"/>
                </Style>
            </Image.Style>
        </Image>
    </StackPanel>
</DataTemplate>

ただ、コードを見てもらえばわかりますが、個別にStyleを定義しちゃうと間延びしてしまい、とても可読性が悪くなります。本来Styleの別出しは同じ設定を適用させたい時に使う方が多いですが可読性のために例え一つであってもStyleを別定義にする事をオススメします。

上記StyleコードでTriggersを使用していますがこちらがif文のような分岐をさせる命令になります。今回はDataTriggerしか使っていませんがDataTriggerはViewModelのデータを参照したい時に使う文です。逆に表示・非表示を設定するVisibility等を参照する時はTriggerを使用します。あとは見て頂ければなんとなく想像できると思いますがViewModel内のweatherプロパティの値が1であれば晴れのソースを参照します。という意味の構文になっています。こうしてアイコン表示を実現しています。

最初の画像の完成されたDataGridのXAML

ここまで終わりましたので設定を色々と追加したDataGridのソースをお見せしますね。元と追加した所は色をつけてあります。

<DataGrid x:Name="WeatherGrid"
          Grid.Column="1"
          ItemsSource="{Binding gridSource}"
          Margin="0,10"
          
          AutoGenerateColumns="False"
          CanUserAddRows="False"
          CanUserReorderColumns="False"
          GridLinesVisibility="None"
          IsReadOnly="True"
          HeadersVisibility="Column"
          AlternatingRowBackground="Beige"
          AlternationCount="2"
          BorderThickness="0"
          >
    <DataGrid.Resources>
        <Style TargetType="{x:Type DataGridColumnHeader}">
            <Setter Property="Background" Value="Aqua"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
        </Style>
        <Style TargetType="{x:Type DataGridRowHeader}">
            <Setter Property="FontSize" Value="12"/>
        </Style>
        <Style x:Key="WeatherIconStyle" TargetType="{x:Type Image}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding weather}" Value="1">
                    <Setter Property="Source" Value="../Image/16_2_19.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="2">
                    <Setter Property="Source" Value="../Image/cloudy.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="3">
                    <Setter Property="Source" Value="../Image/rainy.png"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding weather}" Value="4">
                    <Setter Property="Source" Value="../Image/sunder.png"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
        <DataTemplate x:Key="WeatherTemplate">
            <StackPanel>
                <Image Style="{DynamicResource WeatherIconStyle}" Width="25"/>
            </StackPanel>
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.RowStyle>
        <Style TargetType="{x:Type DataGridRow}">
            <Setter Property="Height" Value="30"/>
        </Style>
    </DataGrid.RowStyle>
    <DataGrid.CellStyle>
        <Style TargetType="{x:Type DataGridCell}">
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
    </DataGrid.CellStyle>
    <DataGrid.Columns>
        <DataGridTextColumn Header="都道府県" Width="60" Binding="{Binding prefLabelPref}"/>
        <DataGridTextColumn Header="市区町村" Width="90" Binding="{Binding municipalityLabel}"/>
        <DataGridTemplateColumn Header="天気" CanUserSort="False" CellTemplate="{StaticResource WeatherTemplate}" Width="50"/>
        <DataGridTextColumn Header="気温" Width="40" Binding="{Binding temperature, StringFormat={}{0:N0}℃, TargetNullValue=不明}"/>
        <DataGridTextColumn Header="降水確率" Width="50" Binding="{Binding rainyPercent, StringFormat={}{0:N0}%, TargetNullValue=不明}"/>
    </DataGrid.Columns>
</DataGrid>

どうですか?元のコードから比べると倍以上に増えていますよね。DataGridはある程度きちんと理解しておかないとデザインを設定する時にドツボにはまりますのでしっかり覚えましょう。(経験者談)

各設定パラメータ解説

それではどの部分がどう影響したか、前のウィンドウと修正後のウィンドウを比較して解説したいと思います。

まず横に伸びてるこのプロパティ列は明らかに邪魔なので消します。これはAutoGenerateColumnsの設定で表示されています。これをFalseにしましょう。なんでこんなパラメータのデフォルトがTrueなんだろう(;’∀’)

次に以下の画像で設定内容を書きましたのでご確認ください。

画像に直接影響しない設定内容は下に記載しました。複数のDataGridを使う時はデフォルトをいじらなければいけないものは共通Styleにしてしまうのが一般的ですね。ちなみに枠は入れたいけど色だけ変更したい場合はBorderBrushを使えばOKです。

  • CanUserReorderColumns : Trueだと行の順番をドラッグ&ドロップで変えることができる。
  • IsReadOnly : FalseだとCellの内容を自由に変更する事ができてしまう
  • CanUserSort : 行の並び順を「降順・昇順・何もしない」の3段階に切り替える。

DataGridという親クラスの中の内訳

ここがDataGridのデザインを変更する上で一番大切なのですがDataGridは各々の場所でクラスが異なります。これをちゃんと理解できてないと、よく「あれ?どうやったらヘッダのデザインが変更できるんだろう?」とか「線が邪魔なので消したい…」の解決策が思い浮かばないです。

画像の通り、DataGridという親クラスの子供に更に4つのクラスにタイプが分かれています

上のコードにあったようにこの4つのタイプに対して、それぞれ適切に設定を入れないと「セルは文字位置とか変わるのにヘッダーが変わらないなぁ…という課題を長時間ひきづってしまいますのでDataGridを使うなら必ず覚えましょう。(これも経験者談)

ItemsSource

最後の解説になりますが、DataGridはList構造になっているのでListを参照したい時はItemsSource=”{Binding gridSource}”と定義します。で、C#側はListではなく、ObservableCollectionを使用していますが使い方はListと変わりありません。これも詳細は他サイトを見てほしいのですが、大事な一点があってセクション1でやったINotifyPropertyChangedも同時に行ってくれるみたいなのでView側への反映も行ってくれるListであると理解してもらえれば問題ないと思います。

こちらもItemsSourceで参照させたい時のクラスにはObservableCollectionを使用し、そうでない時はListを使うというルールを作っておけば間違いないです。

まとめ

いかがでしたでしょうか?大事な所+設定する内容が多すぎて非常に長い記事になってしまいました。

ただ、それだけDataGridはXAML初心者がつまづきやすいポイントとなりますので共通で使っているDataGridのスタイルはあるか?を先輩や有識者に確認し、共通で使っているものを確認した結果、どうしても自分で定義したものを使わなければいけない時は定義するStyleにBaseOnを使うと共通の設定を反映しつつ、自分でカスタマイズしたい設定を追加する事が可能なのでこれも知識として覚えておいてください。

<Style x:Key="OriginalDataGridStyle" TargetType="{x:Type DataGrid}" BasedOn="{StaticResource 共通で使用しているスタイル}">

みたいなコードです。

以上でDataGridの回は終わりです。次こそListView/ListBoxをやりたいと思います。