WPFでDataGridの実装

今回はC#でWPFアプリケーションの制作にポンコツ2人組が挑戦してみました。 APIから取得したデータをDataGridに表示するアプリケーションです。 開発環境はVisualStudio2019で、ライブラリはNewtonsoft.Jsonを使用しています。C#初心者が制作したものなので いろいろとおかしい部分が多いと思います。プログラムを流用する際は適宜修正してください。

※この記事は2023/08/28時点の情報です。

App.xaml
ブール値から Visibility 列挙値への変換や、その逆の変換を行うコンバーター(BooleanToVisibilityConverter)と IValueConverterインターフェースを実装したクラス(InverseBooleanConverter(ソースは下の方にあります))を Application.Resourcesに追加しています。

<Application x:Class="WpfApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApp1"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
        <local:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
    </Application.Resources>
</Application>

App.xaml.cs
このソースは特に何もしていません。デフォルトのままです。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace WpfApp1
{
    /// 
    /// Interaction logic for App.xaml
    /// 
    public partial class App : Application
    {
    }
}

MainWindow.xaml
データグリッドに表示するデータを取得するためにAPIにリクエストを送るボタンと、 データグリッドで選択した行のデータをテキストエリアに表示するボタン、そしてデータグリッドを配置しています。 データグリッドの行をクリックするごとに選択状態がON・OFFされます。

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:av="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="av" x:Class="WpfApp1.MainWindow"
        Title="API Data Display" Height="450" Width="800">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="491*"/>
            <ColumnDefinition Width="309*"/>
        </Grid.ColumnDefinitions>

        <Button Content="APIにリクエスト" Command="{Binding LoadDataCommand}" IsEnabled="{Binding IsLoading, Converter={StaticResource InverseBooleanConverter}}" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="150,15,0,0" Width="87"/>
        <Button Content="選択されているデータを出力" Click="LogSelectedData_Click" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="450,15,0,0" Grid.ColumnSpan="2" Width="142"/>
        <TextBlock x:Name="logTextBlock" Margin="10,10,360,10" FontSize="16"/>
        <DataGrid x:Name="dataGrid" IsReadOnly="True" ItemsSource="{Binding DataItems}" AutoGenerateColumns="False" CanUserAddRows="False" HorizontalAlignment="Left" Width="500" Grid.ColumnSpan="2" Margin="150,40,0,20">
            <DataGrid.Resources>
                <!-- ヘッダースタイルをカスタマイズ -->
                <Style TargetType="DataGridColumnHeader">
                    <Setter Property="FontSize" Value="30"/>
                </Style>
                <local:BooleanToIconConverter x:Key="BooleanToIconConverter"/>
                <!-- コンバーターをリソースに追加 -->
            </DataGrid.Resources>
            <DataGrid.RowStyle>
                <Style TargetType="DataGridRow">
                    <Setter Property="Height" Value="50"/>
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.CellStyle>
                <Style TargetType="DataGridCell">
                    <Setter Property="FontSize" Value="40"/>
                </Style>
            </DataGrid.CellStyle>
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="Select">
                    <DataGridTemplateColumn.CellStyle>
                        <Style TargetType="DataGridCell">
                            <EventSetter Event="PreviewMouseDown" Handler="Cell_PreviewMouseDown"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsSelected}" Value="True">
                                    <Setter Property="Background" Value="Yellow"/>
                                    <Setter Property="Foreground" Value="Black"/>
                                    <Setter Property="BorderThickness" Value="0"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </DataGridTemplateColumn.CellStyle>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Image Source="{Binding IsSelected, Converter={StaticResource BooleanToIconConverter}}" Width="50" Height="50"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="400">
                    <DataGridTextColumn.CellStyle>
                        <Style TargetType="DataGridCell">
                            <EventSetter Event="PreviewMouseDown" Handler="Cell_PreviewMouseDown"/>
                            <Setter Property="FontSize" Value="40"/>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsSelected}" Value="True">
                                    <Setter Property="Background" Value="Yellow"/>
                                    <Setter Property="Foreground" Value="Black"/>
                                    <Setter Property="BorderThickness" Value="0"/>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </DataGridTextColumn.CellStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <TextBlock Text="Loading..." Visibility="{Binding IsLoading, Converter={StaticResource BooleanToVisibilityConverter}}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="400,0,0,0"/>
    </Grid>
</Window>

MainWindow.xaml.cs
データグリッドの行をチェックした時の処理と、選択した行のデータをテキストエリアに出力する処理を実装しています。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// 
    /// Interaction logic for MainWindow.xaml
    /// 
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Cell_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (sender is DataGridCell cell)
            {
                if (cell.DataContext is ApiDataItem dataItem)
                {
                    dataItem.IsSelected = !dataItem.IsSelected; // チェックボックスと連動
                }
            }
        }

        private void LogSelectedData_Click(object sender, RoutedEventArgs e)
        {
            string logText = "";
            foreach (ApiDataItem selectedItem in dataGrid.Items)
            {
                if (selectedItem.IsSelected)
                {
                    logText += $"{selectedItem.Name}\n";
                }
            }
            logTextBlock.Text = logText; // TextBlock にログを設定
        }

    }
}

BooleanToIconConverter.cs
データグリッドの選択行のレ点チェック画像をチェックあり・なしの画像で切り替えています。

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media.Imaging;

namespace WpfApp1
{
    public class BooleanToIconConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool boolValue)
            {
                string iconName = boolValue ? "checked_icon.png" : "unchecked_icon.png";
                return new BitmapImage(new Uri($"/WpfApp1;component/image/{iconName}", UriKind.RelativeOrAbsolute));
            }
            return null;
        }

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

InverseBooleanConverter.cs
IValueConverterインターフェースを実装したクラスです。 プロパティ→画面表示・画面表示→プロパティの双方向の変換を記述するようです。 ここでは前者のみ実装し、後者はエラーとしています。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows;

namespace WpfApp1
{
    public class InverseBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool boolValue)
            {
                return !boolValue;
            }
            return DependencyProperty.UnsetValue;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

ApiDataItem.cs
APIから取得したデータを格納するクラス(ApiDataItem)、APIにリクエストを送信して取得したデータを 画面と連携させるためのクラス(ViewModel)、画面のAPIにリクエストボタンをクリックした際に トリガーとなるICommandを実装したクラス(RelayCommand)です。

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace WpfApp1
{
    public class ApiDataItem : INotifyPropertyChanged
    {
        private bool _isSelected;
        private string? _name;

        public bool IsSelected
        {
            get => _isSelected;
            set
            {
                if (_isSelected != value)
                {
                    _isSelected = value;
                    OnPropertyChanged();
                }
            }
        }

        public string? Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged();
                }
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class ViewModel : INotifyPropertyChanged
    {
        private ObservableCollection<ApiDataItem>? _dataItems;
        private bool _isLoading;
        private RelayCommand? _loadDataCommand;

        public ObservableCollection<ApiDataItem>? DataItems
        {
            get { return _dataItems; }
            set
            {
                _dataItems = value;
                OnPropertyChanged();
            }
        }

        public bool IsLoading
        {
            get { return _isLoading; }
            set
            {
                _isLoading = value;
                OnPropertyChanged();
            }
        }

        public RelayCommand? LoadDataCommand
        {
            get
            {
                return _loadDataCommand ??= new RelayCommand(async () =>
                {
                    try
                    {
                        IsLoading = true;
                        using var httpClient = new HttpClient();
                        var response = await httpClient.GetAsync("http://localhost:8080/api/sample");

                        if (response.IsSuccessStatusCode)
                        {
                            var jsonResponse = await response.Content.ReadAsStringAsync();
                            DataItems = JsonConvert.DeserializeObject<ObservableCollection<ApiDataItem>>(jsonResponse);
                        }
                        else
                        {
                            // Handle API error
                        }
                    }
                    catch (Exception ex)
                    {
                        // Handle exception
                        Console.WriteLine(ex.ToString());
                    }
                    finally
                    {
                        IsLoading = false;
                    }
                });
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class RelayCommand : ICommand
    {
        private readonly Action? _action;

        public RelayCommand(Action? action)
        {
            _action = action;
        }

        public bool CanExecute(object? parameter)
        {
            return true;
        }

        public void Execute(object? parameter)
        {
            _action?.Invoke();
        }

        public event EventHandler? CanExecuteChanged;
    }
}

これらのソースはnamespace(WpfApp1)の直下に配置しており、 画像ファイル(checked_icon.pngとunchecked_icon.png)はnamespace(WpfApp1)直下のimageフォルダにリソースとして登録しています。 (画像ファイルやAPIのソースは当記事に掲載していませんのでご自身でご用意ください。)

以下がアプリケーションを実行した画面です。

C#のWPFでDataGridアプリケーション

管理人情報