sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF 使用したファイルの履歴メニューを作る

今回は、使用したファイルなどの履歴メニューをシンプルに実装してみたサンプルの記事です。

だいたいのアプリケーションのメニューを見てみると、最近使ったファイルを再度使用するための履歴メニューがあります。VisualStudioCode だとこんな感じです。

f:id:shikaku_sh:20200416185600p:plain:w500
前に使ったファイルが選択できる

作ってみた(動作の雰囲気)

f:id:shikaku_sh:20200416185736g:plain:w500
サンプルの雰囲気

指定した個数(ここでは10個)の履歴を管理します。新しいファイルパスを追加すると一番古いデータから削除していきます。意外とどう実装するか微妙に思った次第。

いわゆる、FIFO (First In, First out) の先入れ先出し方式です。

動作の例だとリストを更新すると、メニューの一番下から切り替わるようになっているけど、これも逆転するように並びを変更しました。(IValueConverter で)

画面

とりあえず、サンプルの見た目は次のように作成しました。

ポイントは、メニューアイテムの履歴要素を Files から生成しています。Click イベントは、そのまま実行していますが、実際は Command で受けてあげたほうがよいと思います。

サンプルプログラムは Initialized イベントで生成してみたパターンも公開しています。

<Window x:Class="HistoryMenuSample.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:HistoryMenuSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Menu Grid.Row="0"
              IsMainMenu="True">
            <MenuItem Header="File">
                <MenuItem Header="最近使用したファイルを開く" 
                          ItemsSource="{Binding Files}"
                          Click="MenuItem_Click"/>
            </MenuItem>
        </Menu>

        <Grid Grid.Row="1">
            <StackPanel>
                <Button Margin="10" Padding="10" 
                        HorizontalAlignment="Center" VerticalAlignment="Center" 
                        Content="新しい要素を追加" Click="Button_Click" />
            </StackPanel>
        </Grid>
        
    </Grid>
</Window>

メニューの生成コード

Files プロパティとバインディングしたメニューを表示します。なので、下コードの GetHistoryMenu は、バインディング部分を珍しく(例外らしく)コーディングで書いています。

const int _HistorySize = 10;

public ObservableFixedQueue<string> Files { get; set; } = new ObservableFixedQueue<string>(_HistorySize);

public MainWindow()
{
    InitializeComponent();

    this.DataContext = this;

    // データを詰める
    for (var i = 0; i < _HistorySize; i++)
    {
        var item = Properties.Settings.Default.FilePaths[i];

        Files.Enqueue(item);
    }
}

private void MenuItem_Click(object sender, RoutedEventArgs e)
{
    var menuItem = e.OriginalSource as MenuItem;
    MessageBox.Show(String.Format($"「{menuItem.Header}」をクリックしました。"));
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    var text = $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm-ss")} に追加したデータ";
    
    Files.Enqueue(text);

    // 履歴を保存
    Properties.Settings.Default.FilePaths = Files.ToStringCollection();
    Properties.Settings.Default.Save();
}

ここで、ポイントになっているのはファイルパスの履歴を管理するリストを ObservableQueue コレクションで管理していることです。コレクションは、(10個まで持てる)ファイルパスのテキストを追加し、いらないテキストを削除し、画面に更新を通知する仕組みです。

具体的に実装した ObservableQueue は、ObservableCollection のようにバインディングの通知ができるコレクションです。コレクションの特徴としては、名前のとおり Queue の動きをするように実装したクラスでもあります。

さらに、Queue の中でも固定長で10個だけ記録する特徴が必要です。10個よりも大きいと不都合です。ObservableQueue クラスを ObservableFixedQueue クラスに継承して実装しています。

コレクションの実装例

汎用性のレベルからすると FixedQueue を Observable に継承してもよかったかもしれないです。

並びを反転させる

単純なコンバーターで並びを反転させることができます。(フィルタリングなどの見た目に関わるものは、ここで設定したほうが楽だと思います)

ItemsSource="{Binding Files, Converter={StaticResource ReverseConverter}}"
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;

namespace HistoryMenuSample
{
    /// <summary>
    /// <see cref="ReverseConverter"/> クラスは、カスタム ロジックをバインディングに適用する方法を提供します。
    /// <para>
    /// リストの並びを逆転させます。
    /// </para>
    /// </summary>
    public class ReverseConverter : IValueConverter
    {
        #region Fields

        private ObservableCollection<object> _Items = new ObservableCollection<object>();

        #endregion

        #region Public Methods

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var rawItems = value as IEnumerable<object>;

            if (rawItems == null) return null;

            if (rawItems is INotifyCollectionChanged)
            {
                (value as INotifyCollectionChanged).CollectionChanged += (sender, args) => 
                {
                    Update(rawItems);
                };
            }

            Update(rawItems);

            return _Items;
        }

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

        #endregion

        #region Private Methods

        private void Update(IEnumerable<object> items)
        {
            _Items.Clear();

            foreach (var item in items.Reverse())
            {
                _Items.Add(item);
            }
        }

        #endregion
    }
}

サンプル

テストプログラムは GitHub の「Samples」に公開しています。今回のプログラムは「HistoryMenuSample」です。

履歴は Settings.settings を利用して次回起動時にもバックアップする仕様にしてみました。

user.config ファイル

Settings.settings のファイルは下記に保存されています。

  • %APPDATA%..\Local\HistoryMenuSample

参考

文学テクスト入門 (ちくま学芸文庫)

文学テクスト入門 (ちくま学芸文庫)

  • 作者:前田 愛
  • 発売日: 1993/09/01
  • メディア: 文庫