[C# WPF] なんとかしてWPFの描画を速くしたい「直線描画 」

2019年6月26日

最近WPFのパフォーマンスチューニングに勤しんでいます。

300,000個ほどのオブジェクトを描画するデスクトップアプリを作っている中で、役に立ったり効果のあった話をまとめていきます。

基本的には速度低下を招くよろしくない実装の確認や、対策の紹介などしていきます。

今回は「直線を描画するのにいちばん早い描き方」について検証します。

スポンサーリンク

環境

  • Visual Studio 2019 Community
  • .NET Framework 4.7.2

確認すること

10,000本の直線を描画する場合はどの描き方がいちばん早いのか?

検証(DrawingContext.DrawLine()の場合)

MainWindow.xaml

<Window x:Class="Sample_Performance_Line.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:Sample_Performance_Line"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        Loaded="Window_Loaded"
        >

    <Canvas>
        <local:View x:Name="xView"/>
        <Label x:Name="xLabel" Background="White"/>
    </Canvas>

</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;

namespace Sample_Performance_Line
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // 結果表示用
            xView.ResultLabel = xLabel;
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // 適当なシードで乱数を生成する(直線の始点と終点を適当に算出するのに使用)
            var randSH = new Random(17280489);
            var randSV = new Random(399594);
            var randEH = new Random(8793);
            var randEV = new Random(4498389);

            var points = new List<Tuple<Point, Point>>();
            for (var i = 0; i < 10000; ++i)
            {
                points.Add(new Tuple<Point, Point>(new Point(randSH.Next(800), randSV.Next(450)), new Point(randEH.Next(800), randEV.Next(450))));
            }

            xView.Points = points;
            xView.InvalidateVisual();
        }
    }
}

View.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Sample_Performance_Line
{
    class View : Control
    {
        public List<Tuple<Point, Point>> Points;
        public Label ResultLabel;

        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            if (Points == null || ResultLabel == null)
            {
                return;
            }

            var pen = new Pen(Brushes.Aqua, 1);

            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();

            foreach (Tuple<Point, Point> point in Points)
            {
                drawingContext.DrawLine(pen, point.Item1, point.Item2);
            }

            sw.Stop();
            ResultLabel.Content = "Result: " + sw.ElapsedMilliseconds.ToString() + " ms";
        }
    }
}

検証(DrawingContext.DrawGeometry()/LineTo()/線分ごとにGeometryを作る場合)

以降はView.csの描画部分だけ変更して検証しました。

描画処理の差分だけ記載しておきます。

View.cs

foreach (Tuple<Point, Point> point in Points)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(point.Item1, false, false);
        context.LineTo(point.Item2, true, false);
    }
    drawingContext.DrawGeometry(null, pen, geometry);
}

検証(DrawingContext.DrawGeometry()/LineTo()/ひとつのGeometryで描画する場合)

View.cs

var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
    foreach (Tuple<Point, Point> point in Points)
    {
        context.BeginFigure(point.Item1, false, false);
        context.LineTo(point.Item2, true, false);
    }
}
drawingContext.DrawGeometry(null, pen, geometry);

検証(DrawingContext.DrawGeometry()/PolyLineTo()/線分ごとにGeometryを作る場合)

View.cs

foreach (Tuple<Point, Point> point in Points)
{
    var geometry = new StreamGeometry();
    using (var context = geometry.Open())
    {
        context.BeginFigure(point.Item1, false, false);
        context.PolyLineTo(new Point[] { point.Item2 }, true, false);
    }
    drawingContext.DrawGeometry(null, pen, geometry);
}

検証(DrawingContext.DrawGeometry()/PolyLineTo()/ひとつのGeometryで描画する場合)

View.cs

var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
    foreach (Tuple<Point, Point> point in Points)
    {
        context.BeginFigure(point.Item1, false, false);
        context.PolyLineTo(new Point[] { point.Item2 }, true, false);
    }
}
drawingContext.DrawGeometry(null, pen, geometry);

まとめ

試した結果の要点は以下のようになりました。

  • 直線を描くだけならDrawLine()を使おう
  • Geometryを使って描くときは、ひとつのGeometryにまとめて描こう

おしまい。

スポンサーリンク