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

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

仮想化しないでも速いCanvasの作り方

XAML Advent Calendar 2014 - Qiita

こちら向けの記事です。1日遅刻しました。ごめんなさい。

あちこちで書いて回っておりますがPanel.Childrenがとても重いのです。 遅い原因はこちら。 http://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/Panel.cs,1001

同じ階層のVisualはVisualTreeに結合した順番でしか表示できないので、ChildrenやPanel.ZIndexが変更される都度、Panel.ZIndexを使ってこのRecomputeZStateが走ります。 レイアウトパスの直前に1回だけしてくれたらいいのにと思うかもしれませんが、残念ながらそのような作りにはなっていないのが現状です。

とすると、FrameworkElementに独自のChildrenを実装してやるしか手がない。ということになります。 幸いなことに、XAMLで記述する場合も [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] という属性をつけて、IList<UIElement>を実装してやればいいので、結構簡単なんです。

<Window x:Class="FastCanvas.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:FastCanvas"
        Title="MainWindow">
    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
        <local:Canvas x:Name="_canvas" Width="3200" Height="32000"></local:Canvas>
    </ScrollViewer>
</Window>

今日の主役は仮想化しないで高速なCanvasなので、XAMLはこのくらいです。 XAMLの話の予定でしたのにすみません。 一応、100000個くらいのシェイプをゾロッと配置してみます。

using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace 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 rectangle = new Rectangle() { Width = 32, Height = 32, Fill = Brushes.Aquamarine, RadiusY = 12, RadiusX = 12 };
                    FastCanvas.Canvas.SetLocation(rectangle, new Point(x * 32, y * 32));
                    _canvas.Children.Add(rectangle);
                }
            }
        }
    }
}

そして、これが問題のCanvas

using System.Windows;
using System.Windows.Media;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace FastCanvas
{
    public class Canvas : FrameworkElement
    {
        public static Point GetLocation(DependencyObject obj)
        {
            return (Point)obj.GetValue(LocationProperty);
        }

        public static void SetLocation(DependencyObject obj, Point value)
        {
            obj.SetValue(LocationProperty, value);
        }
        public static readonly DependencyProperty LocationProperty =
            DependencyProperty.RegisterAttached("Location", typeof(Point), typeof(Canvas),
            new FrameworkPropertyMetadata(default(Point),FrameworkPropertyMetadataOptions.AffectsArrange));

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public ObservableCollection<UIElement> Children{get; private set;}

        public Canvas()
        {
            Children = new ObservableCollection<UIElement>();

            Children.CollectionChanged += Children_CollectionChanged;
        }

        void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if(e.OldItems != null)
            {
                foreach (UIElement oldItem in e.OldItems)
                {
                    RemoveVisualChild(oldItem);
                }
            }
            if(e.NewItems != null)
            {
                foreach (UIElement newItem in e.NewItems)
                {
                    AddVisualChild(newItem);
                }
            }
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return Children.Count;
            }
        }

        protected override Visual GetVisualChild(int index)
        {
            return Children[index];
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            foreach(var child in Children)
            {
                var location = GetLocation(child);

                child.Arrange(new Rect(location, child.DesiredSize));
            }

            return base.ArrangeOverride(finalSize);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            foreach (var child in Children)
            {
                var fe = child as FrameworkElement;

                if( fe != null )
                {
                    child.Measure(new Size(fe.Width,fe.Height));
                }
            }

            return base.MeasureOverride(availableSize);
        }
    }
}

100000のシェイプを配置しても、Panel.Children.Addにあるようなもっさり感はありません。 ZIndexが不要ならこの程度で十分行けてしまうという程度で考えていただけると良いかなと思います。