LoginSignup
0
0

WPF リストのチェック機能の実装方法について

Posted at

はじめに

WPF でなにかしらのリストをチェックできるような機能を実装する際の手法について備忘録も兼ねて記します。

MVVM フレームワークとして Prism を使用しています。

0. Model, ViewModel

Item.cs
public class Item : BindableBase
{
    private string name;
    public string Name
    {
        get { return name; }
        set { SetProperty(ref name, value); }
    }

    private decimal price;
    public decimal Price
    {
        get { return price; }
        set { SetProperty(ref price, value); }
    }
}

こんな Model があるとして、この Model を表示してチェックできるようにしていきます。
まず、この状態ではチェックの状態を保持するプロパティが存在しないしないためこの Model をラップしたクラスを作成します。

CheckableItem.cs
public class CheckableItem : BindableBase
{
    private bool isChecked;
    public bool IsChecked
    {
        get { return isChecked; }
        set { SetProperty(ref isChecked, value); }
    }

    public Item Item { get; }

    public CheckableItem(Item item)
    {
        Item = item;
    }
}

複数のクラスに対応できるように下記のようなクラスにしておくと便利です

Checkable.cs
public class Checkable<T> : BindableBase
{
    private bool isChecked;
    public bool IsChecked
    {
        get { return isChecked; }
        set { SetProperty(ref isChecked, value); }
    }

    public T Source { get; }

    public Checkable(T source)
    {
        Source = source;
    }
}

そして MainWindow の ViewModel

MainWindowViewModel.cs
public class MainWindowViewModel : BindableBase
{
    public List<Item> Goods { get; }

    public List<CheckableItem> CheckableGoods { get; }

    public MainWindowViewModel()
    {
        Goods = Enumerable.Range(1, 6).Select(i =>
        {
            return new Item()
            {
                Name = $"商品{i}",
                Price = i * 1000,
            };
        }).ToList();

        CheckableGoods = Goods.Select(x => new CheckableItem(x)).ToList();
    }
}

ここでは Item クラスのリストを初期化して CheckableItem のリストに変換しています
あとは View で表示できるようにするだけです。

1. ItemsControl を使う

MainWindow.xaml
<ItemsControl ItemsSource="{Binding CheckableGoods}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CheckBox IsChecked="{Binding IsChecked}">
                <TextBlock Text="{Binding Item.Name}"/>
            </CheckBox>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

CheckListSample_001.gif

シンプルに ItemsControl で表示するとこんな感じになります。
これを以下のようにもう少しカスタマイズすることもできます。

MainWindow.xaml
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
    <ItemsControl ItemsSource="{Binding CheckableGoods}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border x:Name="border" Padding="2">
                    <CheckBox IsChecked="{Binding IsChecked}" HorizontalContentAlignment="Stretch">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Text="{Binding Item.Name}"/>
                            <TextBlock Grid.Column="1" Text="{Binding Item.Price}"/>
                        </Grid>
                        <CheckBox.Style>
                            <Style TargetType="CheckBox">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding IsMouseOver, ElementName=border}" Value="True">
                                        <Setter Property="Foreground" Value="White"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </CheckBox.Style>
                    </CheckBox>
                    <Border.Style>
                        <Style TargetType="Border">
                            <Style.Triggers>
                                <Trigger Property="IsMouseOver" Value="True">
                                    <Setter Property="Background" Value="Black"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </Border.Style>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

CheckListSample_002.gif

CheckBox の上に Border が入っているのはマウスをのせた際に背景色を変更するためで、その見た目の切り替えは Border.StyleTriggerCheckBox.StyleDataTrigger で行っています。

2. DataGrid を使う

MainWindow.xaml
<DataGrid  ItemsSource="{Binding CheckableGoods}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridCheckBoxColumn Header="IsChecked" Binding="{Binding IsChecked}"/>
        <DataGridTextColumn Header="Name" Width="*" Binding="{Binding Item.Name}" IsReadOnly="True"/>
        <DataGridTextColumn Header="Price" Width="*" Binding="{Binding Item.Price}" IsReadOnly="True"/>
    </DataGrid.Columns>
</DataGrid>

CheckListSample_003.gif

まず必要最低限の実装をするとこんな感じになります。
この状態だと

  • チェックボックスをクリックした時しかチェックできない(行のどこをクリックした場合でもチェックしたい)
  • チェックボックスをクリックするために一度選択状態にしないといけない

などの問題が発生します。

DataGrid で選択状態にしないとチェックボックスが機能しない問題はDataGridCheckBoxColumn でなく DataGridTemplateColumn を使用して CheckBox を配置する方法でも解消することができます。

行のどこをクリックした場合でもチェックできるようにするには、クリックイベントを拾ってクリックした行の DataContextIsChecked プロパティにアクセスすればよさそうです。
この処理は View のコードビハインドに直接記述すると汎用性が低くなるので Behavior を使用します。

CheckDataGirdBahavior.cs
public class CheckDataGirdBahavior : Behavior<DataGrid>
{
    public string IsCheckedPropertyName
    {
        get { return (string)GetValue(IsCheckedPropertyNameProperty); }
        set { SetValue(IsCheckedPropertyNameProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsCheckedPropertyName.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsCheckedPropertyNameProperty =
        DependencyProperty.Register("IsCheckedPropertyName", typeof(string), typeof(CheckDataGirdBahavior), new PropertyMetadata("IsChecked"));

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseLeftButtonDown += DataGrid_PreviewMouseLeftButtonDown;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseLeftButtonDown -= DataGrid_PreviewMouseLeftButtonDown;
    }

    private void DataGrid_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (e.Source is not DependencyObject source)
        {
            return;
        }
        var window = Window.GetWindow(source);
        var pos = e.GetPosition(window);
        var hitTestResult = VisualTreeHelper.HitTest(window, pos);
        if (hitTestResult is null)
        {
            return;
        }
        var visualHit = hitTestResult.VisualHit;
        while (visualHit != null)
        {
            if (visualHit is DataGridRow)
            {
                break;
            }
            visualHit = VisualTreeHelper.GetParent(visualHit);
        }
        if (visualHit is not DataGridRow target)
        {
            return;
        }
        var dataContext = target.DataContext;
        var type = dataContext.GetType();
        try
        {
            var prop = type.GetProperty(IsCheckedPropertyName);
            bool isChecked = (bool)prop.GetValue(dataContext);
            prop.SetValue(target.DataContext, !isChecked);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

やっていることは単純で PreviewMouseLeftButtonDown イベントでクリック位置の DataGridRow を取得してリフレクションで IsChecked プロパティの値を反転させています。
また、 IsCheckedPropertyName という依存関係プロパティを定義することでプロパティ名が IsChecked 以外の場合にも対応できるようにしています。

あとはこれを View に適用するだけです。

MainWindow.xaml
<DataGrid  ItemsSource="{Binding CheckableGoods}" AutoGenerateColumns="False" IsReadOnly="True">
    <i:Interaction.Behaviors>
        <be:CheckDataGirdBahavior/>
    </i:Interaction.Behaviors>
    <DataGrid.Columns>
        <DataGridCheckBoxColumn Header="IsChecked" Binding="{Binding IsChecked}"/>
        <DataGridTextColumn Header="Name" Width="*" Binding="{Binding Item.Name}"/>
        <DataGridTextColumn Header="Price" Width="*" Binding="{Binding Item.Price}"/>
    </DataGrid.Columns>
</DataGrid>

もとのチェックボックスが機能しないように IsReadOnly を True にしてください。

CheckListSample_004.gif

3. ListView を使う

MainWindow.xaml
<ListView ItemsSource="{Binding CheckableGoods}">
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="IsChecked" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <CheckBox IsChecked="{Binding IsChecked}" HorizontalAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Item.Name}" TextAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Price" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Item.Price}" TextAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

CheckListSample_005.gif

こちらは選択状態にしなくてもチェックボックスは機能しますが、
チェックボックスをクリックした時しかチェックできません。
なのでまた Behavior で対応します。

CheckListViewBahavior.cs
public class CheckListViewBahavior : Behavior<ListView>
{
    public string IsCheckedPropertyName
    {
        get { return (string)GetValue(IsCheckedPropertyNameProperty); }
        set { SetValue(IsCheckedPropertyNameProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsCheckedPropertyName.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsCheckedPropertyNameProperty =
        DependencyProperty.Register("IsCheckedPropertyName", typeof(string), typeof(CheckListViewBahavior), new PropertyMetadata("IsChecked"));


    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseLeftButtonDown += ListView_PreviewMouseLeftButtonDown;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewMouseLeftButtonDown -= ListView_PreviewMouseLeftButtonDown;
    }

    private void ListView_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        if (e.Source is not DependencyObject source)
        {
            return;
        }
        var window = Window.GetWindow(source);
        var pos = e.GetPosition(window);
        var hitTestResult = VisualTreeHelper.HitTest(window, pos);
        if (hitTestResult is null)
        {
            return;
        }
        var visualHit = hitTestResult.VisualHit;
        while (visualHit != null)
        {
            if (visualHit is ListViewItem)
            {
                break;
            }
            visualHit = VisualTreeHelper.GetParent(visualHit);
        }
        if (visualHit is not ListViewItem target)
        {
            return;
        }
        var dataContext = target.DataContext;
        var type = dataContext.GetType();
        try
        {
            var prop = type.GetProperty(IsCheckedPropertyName);
            bool isChecked = (bool)prop.GetValue(dataContext);
            prop.SetValue(target.DataContext, !isChecked);
        }
        catch (Exception)
        {
            throw;
        }
    }
}

これもやっていることは、取得する対象が ListViewItem になっているという点以外は DataGrid の時と同じです。
あとは同様に View に適用します。

MainWindow.xaml
<ListView ItemsSource="{Binding CheckableGoods}">
    <i:Interaction.Behaviors>
        <be:CheckListViewBahavior/>
    </i:Interaction.Behaviors>
    <ListView.ItemContainerStyle>
        <Style TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListView.ItemContainerStyle>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="IsChecked" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <Grid>
                            <CheckBox IsChecked="{Binding IsChecked}" HorizontalAlignment="Center"/>
                            <TextBlock/>
                        </Grid>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Name" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Item.Name}" TextAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
            <GridViewColumn Header="Price" Width="100">
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Item.Price}" TextAlignment="Center"/>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>
</ListView>

CheckListSample_006.gif

ListView には DataGrid のような IsReadOnly プロパティがありません。
なので Grid の中に CheckBoxTextBlock を重ねて CheckBox がクリックされないようにしています。
この実装方法はパワープレイ気味というか、あまり正攻法ではない気がするので、他にいい方法があれば教えてください。

終わりに

チェックリストの実装方法を3種類ほど書きましたが、特に要件がなければ ItemsControl を使用するのが一番いいと思います。
次点で、ヘッダーが必要とかであれば DataGrid を使用するのがよさそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0