WPFでItemsControl を使ったページャー【1,2,3,…,20】

コンピュータ

WPFのItemsControlでページャーを作ります。

ソースコード

ファイル名:PagerDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:Helpers\RoutedCommandHelper.cs

using System.Windows;
using System.Windows.Input;

namespace PagerDemo;
public static class RoutedCommandHelper
{
    public static RoutedUICommand Create(
        Window window,
        string? name,
        Action<ExecutedRoutedEventArgs> execute,
        Func<bool>? canExecute = null,
        Key? key = null,
        ModifierKeys modifiers = ModifierKeys.None)
    {
        var cmd = name == null
            ? new RoutedUICommand()
            : new RoutedUICommand(name, name, window.GetType());

        ExecutedRoutedEventHandler exec = (_, e) =>
            execute(e);

        CanExecuteRoutedEventHandler can = (_, e) =>
            e.CanExecute = canExecute?.Invoke() ?? true;

        window.CommandBindings.Add(
            new CommandBinding(cmd, exec, can));

        if (key != null)
        {
            window.InputBindings.Add(
                new KeyBinding(cmd, key.Value, modifiers));
        }

        return cmd;
    }
}

ファイル名:Helpers\ViewModelBase.cs

// 汎用的な ViewModel 基底クラス実装。
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace PagerDemo;
public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

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

ファイル名:MainViewState.cs


using System.Collections.ObjectModel;

namespace PagerDemo;
public class MainViewState : ViewModelBase
{
    public ObservableCollection<PagerItem> PagerItems { get; } = [];
    public int PagerItemsMax { get; set;} = 0;
    public int PagerItemsCurrent { get; set;} = 0;
}

ファイル名:MainWindow.xaml.cs

using System.Windows;
using System.Windows.Input;

namespace PagerDemo;

public partial class MainWindow : Window
{
    public MainViewState State { get; } = new();
    public RoutedUICommand NavigateCommand { get; private set; }
    public MainWindow()
    {
        InitializeComponent();

        NavigateCommand = RoutedCommandHelper.Create(
            window: this,
            name: "Navigate",
            execute: Navigate);
        
        DataContext = this;

        this.Loaded += (sender, e) =>
        {
            // 初期値
            SetCurrentValue(1, 20);
        };
    }
    void SetCurrentValue(int value, int max)
    {
        State.PagerItems.Clear();
        State.PagerItemsMax = max;
        State.PagerItemsCurrent = value;

        if (value <= 0 || max <= 0) return;

        // --- 先頭 ---
        State.PagerItems.Add(new PagerItem
        {
            DisplayName = "1",
            Value = 1
        });

        if (max == 1) return;

        // ページ数が少ない場合は全部表示
        if (max <= 6)
        {
            for (int i = 2; i <= max; i++)
            {
                State.PagerItems.Add(new PagerItem
                {
                    DisplayName = i.ToString(),
                    Value = i
                });
            }
            return;
        }

        // -----------------------------
        // 中央表示範囲の計算(最大5)
        // -----------------------------

        const int windowSize = 5;

        int start = value - 1;
        int end   = value + 4;

        // 左端補正
        if (start < 2)
        {
            start = 2;
            end = start + windowSize - 1;
        }

        // 右端補正
        if (end > max - 1)
        {
            end = max - 1;
            start = end - windowSize + 1;
        }

        // --- 中央ページ ---
        for (int i = start; i <= end; i++)
        {
            State.PagerItems.Add(new PagerItem
            {
                DisplayName = i.ToString(),
                Value = i
            });
        }

        // --- 末尾 ---
        State.PagerItems.Add(new PagerItem
        {
            DisplayName = max.ToString(),
            Value = max
        });

        // -----------------------------
        // 「...」補正
        // -----------------------------

        // 先頭側
        if (State.PagerItems[1].Value != 2)
        {
            State.PagerItems[1].DisplayName = "...";
        }

        // 末尾側
        int lastMiddleIndex = State.PagerItems.Count - 2;
        if (State.PagerItems[lastMiddleIndex].Value != max - 1)
        {
            State.PagerItems[lastMiddleIndex].DisplayName = "...";
        }
    }
    void Navigate(ExecutedRoutedEventArgs e)
    {
        var x = e.Parameter as PagerItem;
        if (x is null) return;
        
        //System.Diagnostics.Debug.Print($"{x.Value}");
        SetCurrentValue(x.Value, State.PagerItemsMax);
    }
}

ファイル名:PagerItem.cs

// ページャーの項目を表すクラス
namespace PagerDemo;
public sealed class PagerItem
{
    public string DisplayName { get; set; } = "";
    public int Value { get; set; } = 0;
}

ファイル名:MainWindow.xaml

<Window x:Class="PagerDemo.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:PagerDemo"
        mc:Ignorable="d"
        Title="PagerDemo" Height="200" Width="400">
    <Grid Margin="8">
        <ItemsControl
            ItemsSource="{Binding State.PagerItems}"
            HorizontalAlignment="Center"
            VerticalAlignment="Top"
            Height="24">

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal"/>
                    <!-- ItemsControlの項目を水平方向に配置 -->
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <!-- ItemsControlのItemを水平方向に配置 -->

                        <!-- 区切り -->
                        <TextBlock Text="  "
                            Margin="3,0"
                            VerticalAlignment="Center" />

                        <!-- クリック可能なページャー要素 -->
                        <Button Content="{Binding DisplayName}"
                                FontSize="18"
                                Padding="2,0"
                                Margin="0"
                                Background="Transparent"
                                BorderThickness="0"
                                Cursor="Hand"
                                VerticalAlignment="Center"
                                Command="{Binding DataContext.NavigateCommand,
                                                    RelativeSource={RelativeSource AncestorType=ItemsControl}}"
                                CommandParameter="{Binding}" />
                                <!-- Command ItemsControlをソースに指定 -->
                                <!-- CommandParameter BreadcrumbItemとバインド -->
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>                
        </ItemsControl>
    </Grid>
</Window>

実行例

スクリーンショット

それらしく作ったつもりですが、「…」周りと、全体の件数がの制御がコントロールしきれていません。とりあえず動作に支障はなさそうです。

コメント