じょいのーと

プログラミング関連のアレコレを書いています

ピクセル単位スクロール可能な仮想化ListBox

お仕事でList系のControlの描画コストが高すぎてパフォーマンスが出ないと悩んでた部分が、趣味コード弄ってる時に解決できる事が分かったのでメモ。

WPF仮想化の基本

基本的にListBox/ListViewは初期状態で仮想化がONになっています。
なので、基本的にはそのまま何も追加のコードを書かずに仮想化されます。

しかし、デフォルトの仮想化はアイテム単位での仮想化を行っている関係で、ListBoxのアイテムが中途半端な状態(半分だけ見えてるなど)を許しません。

UI的にそれが許される場合は良いのですが、1つ1つのItemが大きい場合やスクロールした事が分かりづらい外観の場合には致命的にUXが悪化します。

そのため、Web上で見つかる多くの資料では以下のようなXAMLを書けと書いてあります。

<ListBox ItemsSource="{Binding Collection}"
         ScrollViewer.CanContentScroll="False"/>

ListBoxが内部的に所有しているScrollViewerコントロールのCanContentScroll(Content単位スクロールフラグ)をFalseにしろ、という記述を追加してます。

これでピクセル単位のスクロールが可能になるんですが、CanContentScrollをFalseにした場合、仮想化はOFFになります。つまり、超重いということ。

これは内部的な仕組みとして、コンテンツ単位で仮想化している為に回避できない巨大なデメリットでした。

WPF仮想化の基本(.Net Framework 4.5版)

実は上記コードは.Net Framework 4.0の世界では正しいのですが、現在のWPFの状況には合ってません。

実は.Net Framework 4.5から、ListBoxが内部的に持つ仮想化パネルのスクロールオプションをPixel単位に切り替えるプロパティが追加されてます。

これにより、ピクセル単位スクロール+仮想化ONが簡単に実現可能になりました。

2014年10月現在、ListBoxでピクセル単位スクロールをしたいならば、以下のように記述するのが最善です。

<ListBox ItemsSource="{Binding Collection}"
         VirtualizingPanel.ScrollUnit="Pixel"/>

注意点

基本的にWPFを扱っている時には明示的に仮想化しなくても勝手に仮想化してくれるんですが、以下のような場合には仮想化されないので気をつけてください。

1.仮想化したいListBoxがStackPanelの上に乗っている

StackPanelやWrapPanel、ListBox等の上にListBoxが乗っかっている場合には仮想化は強制的にOFFになります。

GridやBorder等の固定的なコントロールの上に乗っている必要があります。

2.CanContentScroll="False"している

VirtualizingPanel.ScrollUnit="Pixel"を指定しても、以下のようにすると仮想化"だけ"OFFになります。

<ListBox ItemsSource="{Binding Collection}"
         VirtualizingPanel.ScrollUnit="Pixel"
         ScrollViewer.CanContentScroll="False"/><!-- 念のため は ダメ、ゼッタイ -->

念のため、とか言いながら記述すると仮想化がOFFになります。