現在、プロジェクトの関係で初めて WPF を触っています。「GUI とデータ(および関連する処理)の分離と言う事にかなり気を使って設計されてるんだなぁ」と言う感動と「XAML の書き方がよく分からない!」と言う絶望が織り交じったファーストインプレッション。Binding には何が書けるのか、どう書いたらどことバインディングできるのか、については一度きちんと体系立てて纏めておかないと行き当たりばったりのコードになりそうでちょっと(私が)危ない感じでした。
さて。今、サムネイルビューワみたいな部品を作っているのですが、ここに表示されるものが 1000 個を超えると言う事も有り得るため消費メモリやパフォーマンス(表示速度)について少し真面目に考えておく必要があります。取りあえずざっとググってみると ListView や ListBox には VirtualizingStackPanel なるものが存在するようで、これを適切に設定しておくと「表示されているもののみが生成される」と言う動作になるそうです。
ListBoxItemが足りなくなったらListBoxItemを生成し、ListBoxItemを使い回せる場合は使い回していることがわかります。リサイクルされてますね。
とんち絵で表すと
【WPF】VirtualizingStackPanel Recyclingはコンテナがリサイクルされる。 | 創造的プログラミングと粘土細工
VirtualizingStackPanel で画面上に表示されているデータのみ動的に生成して、残りのデータに関しては画像ファイルへのパスなどメタ情報のみを保持しておく事で消費メモリはかなり抑えられそうな予感がします。後は、「現在、どのデータ(画像)を生成して表示する必要があるのか」を知る方法についてですが、WPF: Data Virtualization - CodeProject と言う参考になりそうな記事が見つかりました。
この記事で何をやっているかと言うと「配列 (IList) っぽいクラスを作成して、[] 演算子 (this[int index]) をハックする」と言うもののようです。上記を参考にして、似たようなクラスを作成してみました。
using System; using System.Collections; using System.Collections.Generic; // IList っぽいクラスの Count, this[int index] メソッドがコールされた時に該当する // データを返すためのインターフェース。表示用の画像データ以外のメタデータ等は、 // このインターフェースを実装するクラスで保持する。 public interface IItemsProvider<T> { int ProvideItemsCount(); T ProvideItem(int index); } // ハリボテ IList<T> public class VirtualizingListProxy<T> : IList<T>, IList { public VirtualizingListProxy(IItemsProvider<T> provider) { _provider = provider; } public int Count { get { return _provider.ProvideItemsCount(); } } public T this[int index] { get { return _provider.ProvideItem(index); } } public IEnumerator<T> GetEnumerator() { for (int i = 0; i < Count; ++i) yield return this[i]; } // その他 IList<T>, IList に必要なメソッドを取りあえず定義だけする // ...(省略)... }
ListView (の VirtualizingStackPanel) が画像を表示するためにデータを取得しようと [] 演算子をコールすると、ハリボテ IList
ここで問題となったのは、WPF: Data Virtualization - CodeProject では「要素の追加や削除は考慮していない」と言う点でした。現在やろうとしているサムネイルビューワは要素の追加や削除も有り得るので、何らかの修正を行う必要があります。
取りあえずやってみたのは、「要素の追加や削除はハリボテ IList
using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; public interface IItemsProvider<T> : INotifyCollectionChanged { int ProvideItemsCount(); T ProvideItem(int index); } // ハリボテ IList<T> public class VirtualizingListProxy<T> : IList<T>, IList, INotifyCollectionChanged { public VirtualizingListProxy(IItemsProvider<T> provider) { _provider = provider; _provider.CollectionChanged -= new NotifyCollectionChangedEventHandler(CollectionChangedProxy); _provider.CollectionChanged += new NotifyCollectionChangedEventHandler(CollectionChangedProxy); } public int Count { get { return _provider.ProvideItemsCount(); } } public T this[int index] { get { return _provider.ProvideItem(index); } } public IEnumerator<T> GetEnumerator() { for (int i = 0; i < Count; ++i) yield return this[i]; } // その他 IList<T>, IList に必要なメソッドを取りあえず定義だけする // ...(省略)... // IItemsProvider<T> の CollectionChanged イベントを受け取り、 // それをそのまま伝播するためのイベントハンドラ public event NotifyCollectionChangedEventHandler CollectionChanged; protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { var handler = CollectionChanged; if (handler != null) handler(this, e); } }
これは割とうまくいきそうだったのですが、諸々の問題で動作が不安定になってしまったので取りあえず保留としています。何となく ObservableCollection
もう一つの問題は、画像を表示するために WrapPanel を使用しているのですが、どうやら WrapPanel を使用すると VirtualizingMode がうまく機能しないようです(いきなり全インデックスに対してアクセスが発生する)。こっちの問題はまだまったく触ってないのですが、Virtualizing WrapPanel - CodeProject のような記事も見つかったので、この辺りも参考に検討していく予定です。