C#+WPFチューニング戦記

C#とWPFで高速なコードと最適なシステムを書くためにやってきたいろいろな事を書いてみます。.NET Frameworkのソースコードを読み解きましょう。なお、ここに書かれているのは個人の見解であって何らかの団体や企業の見解を代表するものではありません。

簡素な仮想化パネル

こんなXAML

<Window x:Class="Test.FastCanvas.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Test.FastCanvas"
        Title="MainWindow" Height="350" Width="525">
    <ScrollViewer x:Name="scroller" HorizontalScrollBarVisibility="Visible">
        <local:FastCanvas x:Name="canvas" />
    </ScrollViewer>
</Window>

こんなコード(usingは省略してます)

namespace Test.FastCanvas
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            for (int y = 0; y < 1000; y++)
            {
                for (int x = 0; x < 100; x++)
                {
                    var element = new Ellipse() { Width = 32, Height = 32, Fill = Brushes.LightPink };
                    element.MouseEnter += (s,e) =>{ ((Ellipse)s).Fill = Brushes.Blue; };
                    element.MouseLeave += (s, e) => { ((Ellipse)s).Fill = Brushes.LightPink; };
                    Canvas.SetLeft(element, x * 32);
                    Canvas.SetTop(element, y * 32);
                    canvas.AddElement(element);
                }
            }
            canvas.Height = 32 * 1000;
            canvas.Width = 32 * 100;
            scroller.ScrollChanged += scroller_ScrollChanged;
        }

        void scroller_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            var rect = new Rect(
                scroller.HorizontalOffset, 
                scroller.VerticalOffset, 
                scroller.ViewportWidth, 
                scroller.ViewportHeight);

            canvas.SetViewport(rect);
        }
    }
}

こんなコントロール(usingは省略してます)

namespace Test.FastCanvas
{
    public class FastCanvas : Canvas
    {
        private HashSet<UIElement> _virtualChildren = new HashSet<UIElement>();

        public void AddElement(UIElement element)
        {
            _virtualChildren.Add(element);
        }

        public void RemoveElement(UIElement element)
        {
            _virtualChildren.Remove(element);
        }

        public void SetViewport(Rect rect)
        {
            foreach(FrameworkElement child in _virtualChildren)
            {
                var childRect = new Rect(Canvas.GetLeft(child), Canvas.GetTop(child), child.Width, child.Height);
                if (!rect.IntersectsWith(childRect))
                {
                    if (Children.Contains(child))
                        Children.Remove(child);
                }
                else
                {
                    if (!Children.Contains(child))
                        Children.Add(child);
                }
            }
        }
    }
}

分かり易く仮想化を説明する教材として作ったものです。
ScrollChangedを使うと古風なので(あとレイアウトパスが過剰に走って遅いので)、データバインディングでViewportを設定できるようにするとMVVM風ですよね。マルチバリューコンバーターの出番です。

起動時間と、マウスヒットテストは仮想化の有無だけで相当違います。それを体感するためのサンプルと思っていただけたら。
ちなみに、同じコードでFastCanvasをCanvasに入れ替え、ScrollChangedのイベントをやめるだけで、仮想化自体の威力は体感できると思います。