WPF でグラフのイメージを作成・表示

WPF 上でグラフを作成・表示するプログラムです。
System.Windows.Media.Geometry クラスの派生クラスでグラフの図形情報を作成し、System.Windows.Media.DrawingVisual オブジェクトへ System.Windows.Media.DrawingContext.DrawGeometry メソッドを用いて図形情報を描画します。描画を行った DrawingVisual オブジェクトは System.Windows.Media.Imaging.RenderTargetBitmap オブジェクトの Render メソッドを用いてビットマップ化を行い、WPF の Image 要素を用いて画面上に表示します。

実際に表示を行った画面は次のようになります。

関数グラフ表示の例
関数グラフ

黒い線で描かれているのは Y = X^3 – 9X のグラフで、赤い線で描かれているのは Y = X^2 – 3X のグラフです。

それでは、プログラムです。
「WPF アプリケーション」なプロジェクトを作成します(プロジェクト名は FunctionGraph02 としています)。
最初にグラフィック描画関係から。
プロジェクトに「Utility」フォルダを追加します。
まずは、システムから画面の DPI 設定を取得するコードから。
Utility フォルダに「DeviceHelper」クラスを作成します。

using System;
using System.Runtime.InteropServices;
using System.Windows.Controls;

namespace FunctionGraph02.Utility
{
    // 以下の DPI 設定の取得は MSDN よくある質問 フォーラム「RenderTargetBitmap の使い方」より抜粋
    // http://social.msdn.microsoft.com/Forums/ja-JP/df0c59a1-f7c0-4591-9285-eeabc252a608/rendertargetbitmap-?forum=wpffaqja
    internal class DeviceHelper
    {
        /// <summary>
        /// システムから画面の DPI 設定を取得します。
        /// </summary>
        /// <param name="orientation"></param>
        /// <returns></returns>
        public static Int32 PixelsPerInch(Orientation orientation)
        {
            Int32 capIndex = (orientation == Orientation.Horizontal) ? 0x58 : 90;
            using (DCSafeHandle handle = UnsafeNativeMethods.CreateDC("DISPLAY"))
            {
                return (handle.IsInvalid ? 0x60 : UnsafeNativeMethods.GetDeviceCaps(handle, capIndex));
            }
        }
    }

    internal sealed class DCSafeHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
    {
        private DCSafeHandle() : base(true) { }

        protected override Boolean ReleaseHandle()
        {
            return UnsafeNativeMethods.DeleteDC(base.handle);
        }
    }

    [System.Security.SuppressUnmanagedCodeSecurity]
    internal static class UnsafeNativeMethods
    {
        [DllImport("gdi32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern Boolean DeleteDC(IntPtr hDC);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern Int32 GetDeviceCaps(DCSafeHandle hDC, Int32 nIndex);

        [DllImport("gdi32.dll", EntryPoint = "CreateDC", CharSet = CharSet.Auto)]
        public static extern DCSafeHandle IntCreateDC(String lpszDriver,
            String lpszDeviceName, String lpszOutput, IntPtr devMode);

        public static DCSafeHandle CreateDC(String lpszDriver)
        {
            return UnsafeNativeMethods.IntCreateDC(lpszDriver, null, null, IntPtr.Zero);
        }
    }
}

コメントにあるとおり、このコードは MSDN よくある質問 フォーラム「RenderTargetBitmap の使い方」より抜粋しています。

次に「Coordinate」クラスを作成します。

namespace FunctionGraph02.Utility
{
    /// <summary>
    /// 座標を示すクラス
    /// (System.Windows.Point は構造体)
    /// </summary>
    class Coordinate
    {
        public Coordinate() { }
        public Coordinate(double x, double y)
        {
            X = x;
            Y = y;
        }

        /// <summary>
        /// X 座標
        /// </summary>
        public double X { get; set; }
        /// <summary>
        /// Y 座標
        /// </summary>
        public double Y { get; set; }
    }
}

このクラスは、座標を示すクラスです。System.Windows.Point は構造体のため、クラスのものを作っています。

次に「ChartData」クラスを作成します。

using System.Windows.Media;

namespace FunctionGraph02.Utility
{
    class ChartData
    {
        public ChartData()
        {
            BorderColor = Colors.Black;
            FillColor = Colors.Gray;
        }

        /// <summary>
        /// グラフの線の色
        /// </summary>
        public Color BorderColor { get; set; }

        /// <summary>
        /// グラフの塗りつぶし
        /// </summary>
        public bool IsFill { get; set; }

        /// <summary>
        /// グラフの塗りつぶし色
        /// </summary>
        public Color FillColor { get; set; }

        /// <summary>
        /// グラフ描画を描く値の配列
        /// </summary>
        public object[] Datas { get; set; }
    }
}

このクラスは、描くグラフの情報を保持するクラスです。塗りつぶし系は今回の関数グラフの例では利用しませんが、棒グラフなどで利用できます。

次に「ChartBase」クラスを作成します。

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

namespace FunctionGraph02.Utility
{
    abstract class ChartBase
    {
        private const double SCALE_TICK_LEN = 5;

        public ChartBase(int canvasWidth, int canvasHeight)
        {
            ZoomRatioX = ZoomRatioY = 1;
            IsMaxAxesFromDatas = true;
            MinAxisX = MinAxisY = 0;
            MaxAxisX = MaxAxisY = 1;
            AxisXNicValStart = AxisYNickValStart = 1;
            AxisXNickValIncrements = AxisYNickValIncrements = 1;
            AxisXScaleStart = AxisYScaleStart = 1;
            AxisXScaleIncrements = AxisYScaleIncrements = 1;
            FrameColor = Colors.Gray;

            var dpi = (Double)DeviceHelper.PixelsPerInch(Orientation.Horizontal);
            _canvas = new RenderTargetBitmap(canvasWidth, canvasHeight, dpi, dpi, PixelFormats.Default);
        }

        private RenderTargetBitmap _canvas;
        /// <summary>
        /// グラフを描くカンバスを取得します。
        /// </summary>
        public RenderTargetBitmap Canvas
        {
            get { return _canvas; }
        }

        /// <summary>
        /// グラフのX軸拡大縮小の比率
        /// </summary>
        public double ZoomRatioX { get; set; }

        /// <summary>
        /// グラフのY軸拡大縮小の比率
        /// </summary>
        public double ZoomRatioY { get; set; }

        /// <summary>
        /// X 軸長の最大値からの余白値
        /// </summary>
        public int MarginX { get; set; }

        /// <summary>
        /// Y 軸長の最大値からの余白値
        /// </summary>
        public int MarginY { get; set; }

        /// <summary>
        /// グラフ表示場所の移動量(X 座標)
        /// </summary>
        public int TranslateX { get; set; }
        /// <summary>
        /// グラフ表示場所の移動量(Y 座標)
        /// </summary>
        public int TranslateY { get; set; }

        /// <summary>
        /// X 軸, Y 軸の最大値をデータ値から設定するか
        /// </summary>
        public bool IsMaxAxesFromDatas { get; set; }

        /// <summary>
        /// Y 軸の最小値
        /// </summary>
        public int MinAxisY { get; set; }

        /// <summary>
        /// Y 軸の最大値
        /// </summary>
        public int MaxAxisY { get; set; }

        /// <summary>
        /// X 軸の最小値
        /// </summary>
        public int MinAxisX { get; set; }

        /// <summary>
        /// X 軸の最大値
        /// </summary>
        public int MaxAxisX { get; set; }

        /// <summary>
        /// Y 軸の標題
        /// </summary>
        public string AxisYHeader { get; set; }
        /// <summary>
        /// X 軸の標題
        /// </summary>
        public string AxisXHeader { get; set; }

        /// <summary>
        /// Y 軸値表示を行うか
        /// </summary>
        public bool IsAxisYNickVal { get; set; }
        /// <summary>
        /// Y 軸値表示の開始値
        /// </summary>
        public int AxisYNickValStart { get; set; }
        /// <summary>
        /// Y 軸値表示の増分値
        /// </summary>
        public int AxisYNickValIncrements { get; set; }
        /// <summary>
        /// X 軸値表示を行うか
        /// </summary>
        public bool IsAxisXNickVal { get; set; }
        /// <summary>
        /// X 軸値表示の開始値
        /// </summary>
        public int AxisXNicValStart { get; set; }
        /// <summary>
        /// X 軸値表示の増分値
        /// </summary>
        public int AxisXNickValIncrements { get; set; }

        /// <summary>
        /// X 軸目盛り表示を行うか
        /// </summary>
        public bool IsAxisXScale { get; set; }

        /// <summary>
        /// X 軸目盛り表示の最大化
        /// </summary>
        public bool IsAxisXScaleLenMax { get; set; }

        /// <summary>
        /// X 軸目盛り表示の開始値
        /// </summary>
        public int AxisXScaleStart { get; set; }
        /// <summary>
        /// X 軸目盛り表示の増分値
        /// </summary>
        public int AxisXScaleIncrements { get; set; }

        /// <summary>
        /// Y 軸目盛り表示を行うか
        /// </summary>
        public bool IsAxisYScale { get; set; }

        /// <summary>
        /// Y 軸目盛り表示の最大化
        /// </summary>
        public bool IsAxisYScaleLenMax { get; set; }

        /// <summary>
        /// Y 軸目盛り表示の開始値
        /// </summary>
        public int AxisYScaleStart { get; set; }
        /// <summary>
        /// Y 軸目盛り表示の増分値
        /// </summary>
        public int AxisYScaleIncrements { get; set; }

        /// <summary>
        /// 枠線と目盛りの色
        /// </summary>
        public Color FrameColor { get; set; }

        /// <summary>
        /// グラフを描画します。
        /// </summary>
        /// <param name="datas"></param>
        public void DrawGraph(ChartData[] chartDatas)
        {
            var seriesGeometries = new Geometry[chartDatas.Length];
            for (var i = 0; i < chartDatas.Length; ++i)
            {
                seriesGeometries[i] = getSeriesGeometry(chartDatas[i].Datas);
            }

            if (IsMaxAxesFromDatas)
            {
                Rect rect;
                int currentX;
                int currentY;
                for (var i = 0; i < chartDatas.Length; ++i)
                {
                    rect = seriesGeometries[i].GetRenderBounds(new Pen(new SolidColorBrush(Colors.Black), 1));
                    currentX = (int)(rect.Width / ZoomRatioX) + MarginX;
                    currentY = (int)(rect.Height / ZoomRatioY) + MarginY;
                    MaxAxisX = MaxAxisX < currentX ? currentX : MaxAxisX;
                    MaxAxisY = MaxAxisY < currentY ? currentY : MaxAxisY;
                }
            }
            var frameGeometry = getFrameGeometry();

            var dv = new DrawingVisual();
            var dc = dv.RenderOpen();
            // グラフ描画位置の移動量をセット
            var transTrans = new TranslateTransform(TranslateX, TranslateY);
            dc.PushTransform(transTrans);
            // Y 軸の中心で反転をセット(左上原点補正)
            var scaleTrans = new ScaleTransform();
            scaleTrans.CenterY = MaxAxisY * ZoomRatioY / 2;
            scaleTrans.ScaleY = -1;
            dc.PushTransform(scaleTrans);
            // フレームを描画
            dc.DrawGeometry(null, new Pen(new SolidColorBrush(FrameColor), 1), frameGeometry);
            
            for (var i = 0; i < chartDatas.Length; ++i)
            {
                // 塗りつぶし設定
                SolidColorBrush fillBrush = chartDatas[i].IsFill ? new SolidColorBrush(chartDatas[i].FillColor) : null;
                // グラフを描画
                dc.DrawGeometry(fillBrush, new Pen(new SolidColorBrush(chartDatas[i].BorderColor), 1), seriesGeometries[i]);
            }
            // 反転設定を解除
            dc.Pop();
            // X軸, Y軸の標題
            Axes(dc);
            dc.Pop();
            dc.Close();
            // カンバスへ描画
            ((RenderTargetBitmap)_canvas).Render(dv);
        }

        /// <summary>
        /// カンバスの描画をリセット
        /// </summary>
        public void Reset()
        {
            _canvas.Clear();
        }

        /// <summary>
        /// データ値のグラフの幾何学図形を取得します。
        /// </summary>
        /// <param name="datas"></param>
        /// <returns></returns>
        protected abstract Geometry getSeriesGeometry(object[] datas);

        private Geometry getFrameGeometry()
        {
            var zoomedMinX = MinAxisX * ZoomRatioX;
            var zoomedMaxX = MaxAxisX * ZoomRatioX;
            var zoomedMinY = MinAxisY * ZoomRatioY;
            var zoomedMaxY = MaxAxisY * ZoomRatioY;
            PathFigure frameFigure;
            frameFigure = new PathFigure();
            var points = new Point[4] { new Point(zoomedMaxX, zoomedMinY), new Point(zoomedMaxX, zoomedMaxY),
                new Point(zoomedMinX, zoomedMaxY), new Point(zoomedMinX, zoomedMinY) };
            frameFigure.StartPoint = new Point(zoomedMinX, zoomedMinY);
            LineSegment lineSeg;
            var segCollect = new PathSegmentCollection();
            for (var i = 0; i < points.Length; ++i)
            {
                lineSeg = new LineSegment();
                lineSeg.Point = points[i];
                lineSeg.IsStroked = true;
                segCollect.Add(lineSeg);
            }
            frameFigure.Segments = segCollect;
            var figCollect = new PathFigureCollection();
            figCollect.Add(frameFigure);

            // Y axis scale
            if (IsAxisYScale)
            {
                drawScale(figCollect, Axis.Y, new Int32Rect((int)MinAxisX, (int)MinAxisY, (int)MaxAxisX, (int)MaxAxisY), AxisYScaleStart, AxisYScaleIncrements);
            }
            // X axis scale
            if (IsAxisXScale)
            {
                drawScale(figCollect, Axis.X, new Int32Rect((int)MinAxisX, (int)MinAxisY, (int)MaxAxisX, (int)MaxAxisY), AxisXScaleStart, AxisXScaleIncrements);
            }

            var frameGeometry = new PathGeometry();
            frameGeometry.Figures = figCollect;

            return frameGeometry;
        }

        // 目盛り描画
        private void drawScale(PathFigureCollection figCollect, Axis axis, Int32Rect rect, int start, int increments)
        {
            PathFigure scaleFigure;
            LineSegment scaleSeg;
            PathSegmentCollection scaleCollect;
            var minX = rect.X * ZoomRatioX;
            var minY = rect.Y * ZoomRatioY;
            var max = axis == Axis.X ? rect.Width : rect.Height;
            var zoomed = 0d;

            // 0軸
            scaleFigure = new PathFigure();
            scaleFigure.StartPoint = axis == Axis.Y ? new Point(0, minY) : new Point(minX, 0);
            scaleSeg = axis == Axis.Y ? new LineSegment(new Point(0, max * ZoomRatioY), true) :
                new LineSegment(new Point(max * ZoomRatioX, 0), true);
            scaleCollect = new PathSegmentCollection();
            scaleCollect.Add(scaleSeg);
            scaleFigure.Segments = scaleCollect;
            figCollect.Add(scaleFigure);

            // 目盛りの長さ
            var tickLen = axis == Axis.Y ? minX + SCALE_TICK_LEN : minY + SCALE_TICK_LEN;
            if (axis == Axis.Y && IsAxisYScaleLenMax)
            {
                tickLen = rect.Width * ZoomRatioX;
            }
            else if (axis == Axis.X && IsAxisXScaleLenMax)
            {
                tickLen = rect.Height * ZoomRatioY;
            }
            for (var i = start; i < max; i += increments)
            {
                scaleFigure = new PathFigure();
                zoomed = axis == Axis.Y ? i * ZoomRatioY : i * ZoomRatioX;
                scaleFigure.StartPoint = axis == Axis.Y ? new Point(minX, zoomed) : new Point(zoomed, minY);
                scaleSeg = axis == Axis.Y ? new LineSegment(new Point(tickLen, zoomed), true) :
                    new LineSegment(new Point(zoomed, tickLen), true);
                scaleCollect = new PathSegmentCollection();
                scaleCollect.Add(scaleSeg);
                scaleFigure.Segments = scaleCollect;
                figCollect.Add(scaleFigure);
            }
        }

        private void Axes(DrawingContext dc)
        {
            FormattedText text;
            // Y軸目盛値表示
            Point point;
            double minX = 0;
            if (IsAxisYNickVal)
            {
                foreach (var n in getNicks(AxisYNickValStart, AxisYNickValIncrements, MaxAxisY))
                {
                    text = getFormattedText(n.ToString());
                    point = getNickPoint(text, n, MaxAxisY, Axis.Y, ZoomRatioX, ZoomRatioY);
                    point.X += MinAxisX * ZoomRatioX;
                    dc.DrawText(text, point);
                    if (point.X < minX) minX = point.X;
                }
            }
            // Y軸標題
            text = getFormattedText(AxisYHeader);
            point = getPoint(text, minX, ZoomRatioY);
            // Y軸標題の中心で反時計回りに90度回転させる
            dc.PushTransform(new RotateTransform(-90, point.X + text.Width / 2, point.Y + text.Height / 2));
            dc.DrawText(text, point);
            dc.Pop();
            // X軸目盛値表示
            if (IsAxisXNickVal)
            {
                foreach (var n in getNicks(AxisXNicValStart, AxisXNickValIncrements, MaxAxisX))
                {
                    text = getFormattedText(n.ToString());
                    point = getNickPoint(text, n, MaxAxisY, Axis.X, ZoomRatioX, ZoomRatioY);
                    point.Y -= MinAxisY * ZoomRatioY;
                    dc.DrawText(text, point);
                }
            }
            // X軸標題
            text = getFormattedText(AxisXHeader);
            dc.DrawText(text, getPoint(text, 1));
        }

        private static int[] getNicks(int start, int increment, int maxValue)
        {
            var temp = new List<int>();
            for (var i = start; i < maxValue; i += increment)
            {
                temp.Add(i);
            }

            return temp.ToArray();
        }

        private static FormattedText getFormattedText(string text)
        {
            var yAxisText = new FormattedText(text,
                new CultureInfo("ja-jp"),
                FlowDirection.LeftToRight,
                new Typeface(new FontFamily("Meiryo UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
                12, new SolidColorBrush(Colors.Black));

            return yAxisText;
        }

        /// <summary>
        /// 目盛り値表示の座標計算
        /// </summary>
        /// <param name="text"></param>
        /// <param name="nickNum">表示する目盛り値(軸上の位置)</param>
        /// <param name="height"></param>
        /// <param name="axis"></param>
        /// <param name="zoomRatioX"></param>
        /// <param name="zoomRatioY"></param>
        /// <returns></returns>
        private static Point getNickPoint(FormattedText text, int nickNum, int height, Axis axis, double zoomRatioX, double zoomRatioY)
        {
            var result = new Point();
            if (axis == Axis.X)
            {   // X軸の場合
                result.X = nickNum * zoomRatioX - text.Width / 2;
                result.Y = height * zoomRatioY;
            }
            else
            {   // Y軸の場合
                result.X = -text.Width - 10; // 10 は位置の微調整値
                result.Y = -nickNum * zoomRatioY + height * zoomRatioY - text.Height / 2 - 2; // 2 は位置の微調整値
            }

            return result;
        }

        /// <summary>
        /// X軸標題の座標計算
        /// </summary>
        /// <param name="text"></param>
        /// <param name="lineNo">表示行位置</param>
        /// <returns></returns>
        private Point getPoint(FormattedText text, int lineNo)
        {
            var x = (MinAxisX * ZoomRatioX + MaxAxisX * ZoomRatioX) / 2d - text.Width / 2d;
            var y = MaxAxisY * ZoomRatioY - MinAxisY * ZoomRatioY + text.Height * lineNo + 10; // 10 は位置の微調整値

            return new Point(x, y);
        }

        /// <summary>
        /// Y軸標題の座標計算
        /// </summary>
        /// <param name="text"></param>
        /// <param name="maxX"></param>
        /// <param name="maxY"></param>
        /// <param name="nickX">Y軸目盛り値表示の最小X座標</param>
        /// <returns></returns>
        private Point getPoint(FormattedText text, double nickX, double zoomRatioY)
        {
            var x = nickX - text.Width;
            var y = (MaxAxisY * zoomRatioY - MinAxisY * ZoomRatioY) / 2d - text.Height / 2d;

            return new Point(x, y);
        }

        private enum Axis
        {
            Y,
            X
        }
    }
}

このクラスがグラフ作成・表示の基底クラスになります。
getSeriesGeometry 抽象メソッドの引数(グラフを描く元データ)は、object の配列としています。

次に「FunctionGraph」クラスを作成します。FunctionGraph クラスは、ChartBase クラスを継承し、getSeriesGeometry メソッドを実装します。

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

namespace FunctionGraph02.Utility
{
    class FunctionGraph : ChartBase
    {
        public FunctionGraph(int canvasWidth, int canvasHeight) : base(canvasWidth, canvasHeight) { }

        protected override Geometry getSeriesGeometry(object[] vals)
        {
            if (vals.GetType() != typeof(Coordinate[]))
            {
                throw new ArgumentException("引数の型が Coordinate[] ではありません。");
            }

            PathFigure figure;
            figure = new PathFigure();
            figure.StartPoint = new Point(((Coordinate)vals[0]).X * ZoomRatioX, ((Coordinate)vals[0]).Y * ZoomRatioY);
            LineSegment lineSeg;
            var segCollect = new PathSegmentCollection();
            Coordinate item;
            for (var i = 1; i < vals.Length; ++i)
            {
                lineSeg = new LineSegment();
                item = (Coordinate)vals[i];
                lineSeg.Point = new Point(item.X * ZoomRatioX, item.Y * ZoomRatioY);
                lineSeg.IsStroked = true;
                segCollect.Add(lineSeg);
            }
            figure.Segments = segCollect;

            var figCollect = new PathFigureCollection();
            figCollect.Add(figure);

            var seriesGeometry = new PathGeometry();
            seriesGeometry.Figures = figCollect;

            return seriesGeometry;
        }
    }
}

getSeriesGeometry メソッドは座標の配列を受け取り、グラフの図形情報を返すようにしています。

ここまででグラフィック描画関係ができたので、次はビューモデルです。
MVVM パターンなので、お決まりのコマンド処理とプロパティチェンジイベントの発火を提供する ViewModelBase クラスは別記事の ViewModelBase で書いたものを使っています。別プロジェクトにして作成した DLL を参照設定で追加するなり、プロジェクト内にソースを取り入れるなりしてください(次のコードは DLL を参照する前提で書いています)。
プロジェクトに「ViewModels」フォルダを追加し、ViewModels フォルダに「MainViewModel」クラスを作成します。

using System;
using System.Windows.Input;
using System.Windows.Media;

using MakCraft.ViewModels;

using FunctionGraph02.Utility;

namespace FunctionGraph02.ViewModels
{
    class MainViewModel : ViewModelBase
    {
        private const int IMAGE_WIDTH = 350;
        private const int IMAGE_HEIGHT = 380;
        private FunctionGraph _chart;

        public MainViewModel()
        {
            _chart = new FunctionGraph(IMAGE_WIDTH, IMAGE_HEIGHT);
            ImageSource = _chart.Canvas;
            ImageWidth = IMAGE_WIDTH;
            ImageHeight = IMAGE_HEIGHT;

            _chart.ZoomRatioX = 14.0;
            _chart.ZoomRatioY = 5.0;
            _chart.TranslateX = 180;
            _chart.TranslateY = 20;
            _chart.IsMaxAxesFromDatas = false;
            _chart.MinAxisX = -8;
            _chart.MaxAxisX = 8;
            _chart.MinAxisY = -30;
            _chart.MaxAxisY = 30;
            _chart.AxisXHeader = "入力値";
            _chart.AxisYHeader = "出力値";
            _chart.IsAxisXNickVal = true;
            _chart.IsAxisYNickVal = true;
            _chart.AxisXNicValStart = -6;
            _chart.AxisYNickValStart = -25;
            _chart.AxisXNickValIncrements = 2;
            _chart.AxisYNickValIncrements = 5;
            _chart.FrameColor = Colors.LightGray;
            _chart.IsAxisXScale = _chart.IsAxisYScale = true;
            _chart.IsAxisXScaleLenMax = _chart.IsAxisYScaleLenMax = true;
            _chart.AxisXScaleStart = -6;
            _chart.AxisYScaleStart = -25;
            _chart.AxisXScaleIncrements = 2;
            _chart.AxisYScaleIncrements = 5;
        }

        private ImageSource _imageSource;
        public ImageSource ImageSource
        {
            get { return _imageSource; }
            set
            {
                _imageSource = value;
                base.RaisePropertyChanged(() => ImageSource);
            }
        }

        private double _imageWidth;
        public double ImageWidth
        {
            get { return _imageWidth; }
            set
            {
                _imageWidth = value;
                base.RaisePropertyChanged(() => ImageWidth);
            }
        }

        private double _imageHeight;
        public double ImageHeight
        {
            get { return _imageHeight; }
            set
            {
                _imageHeight = value;
                base.RaisePropertyChanged(() => ImageHeight);
            }
        }

        private void drawChartExecute()
        {
            var chartData = new ChartData[] {
                new ChartData { Datas = computeFunction(), BorderColor = Colors.Black },
                new ChartData { Datas = computeFunction2(), BorderColor = Colors.Red },
            };
            _chart.DrawGraph(chartData);
        }
        private ICommand _drawChartCommand;
        public ICommand DrawChartCommand
        {
            get
            {
                if (_drawChartCommand == null)
                {
                    _drawChartCommand = new RelayCommand(drawChartExecute);
                }
                return _drawChartCommand;
            }
        }

        // Y = X^3 - 9X
        private Coordinate[] computeFunction()
        {
            var minX = -4.0d;
            var maxX = 4.0d;
            var interval = 1;
            var intervalRate = 10d;
            var length = (int)((maxX - minX) / (interval / intervalRate)) + 1;
            var result = new Coordinate[length];
            var x = (int)(minX * intervalRate);
            double y;
            for (var i = 0; i < length; ++i)
            {
                y = Math.Pow((x / intervalRate), 3) - 9 * (x / intervalRate);
                result[i] = new Coordinate(x / intervalRate, y);
                x += interval;
            }

            return result;
        }

        // Y = X^2 - 3X
        private Coordinate[] computeFunction2()
        {
            var minX = -4.0d;
            var maxX = 7.0d;
            var interval = 1;
            var intervalRate = 10d;
            var length = (int)((maxX - minX) / (interval / intervalRate)) + 1;
            var result = new Coordinate[length];
            var x = (int)(minX * intervalRate);
            double y;
            for (var i = 0; i < length; ++i)
            {
                y = Math.Pow((x / intervalRate), 2) - 3 * (x / intervalRate);
                result[i] = new Coordinate(x / intervalRate, y);
                x += interval;
            }

            return result;
        }
    }
}

_chart.ZoomRatioX で X 軸方向に14倍、_chart.ZoomRatioY で Y 軸方向に5倍にし(そのままだと軸方法の変化量1.0 = 1ピクセルになってしまうので)、_chart.TranslateX で X 軸方向に180、_chart.TranslateY で Y 軸方向に20移動させています。
computeFunction メソッド及び computeFunction2 メソッドでグラフ化する座標の配列を作成します。座標の配列は、drawChartExecute メソッドで ChartData クラスのインスタンスにグラフの色と共にセットしています。

最後にビューの MainWindow.xaml です。

<Window x:Class="FunctionGraph02.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:FunctionGraph02.ViewModels"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>

    <StackPanel>
        <TextBlock Text="関数グラフのテスト" FontSize="18" Margin="8" HorizontalAlignment="Center" />

        <Button
            Command="{Binding DrawChartCommand}"
            Content="グラフ描画" Padding="8 4" MinWidth="100" Margin="8" HorizontalAlignment="Right" />

        <Border BorderBrush="BurlyWood"  BorderThickness="1">
            <Image
                Source="{Binding ImageSource}" Width="{Binding ImageWidth}" Height="{Binding ImageWidth}" />
        </Border>
    </StackPanel>
</Window>

以上で「グラフ描画」ボタンをクリックすると関数グラフが表示されます。
次回は、「グラフの塗りつぶし」を使う例として棒グラフの例を書く予定です。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です