Click here to Skip to main content
15,885,435 members
Articles / Operating Systems / Windows

WPF Chart Control With Pan, Zoom and More

Rate me:
Please Sign up or sign in to vote.
4.92/5 (42 votes)
10 Dec 2012Public Domain10 min read 386.5K   10.9K   174  
Chart Control for Microsoft .NET 3.0/WPF with pan, zoom, and offline rendering to the clipboard for custom sizes.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Microsoft.Windows.Media;
using Microsoft.WindowsAPICodePack.DirectX.Direct2D1;
using Microsoft.WindowsAPICodePack.DirectX.DirectWrite;

using DWrite = Microsoft.WindowsAPICodePack.DirectX.DirectWrite;
using Microsoft.WindowsAPICodePack.DirectX.DXGI;
using Microsoft.WindowsAPICodePack.DirectX.WindowsImagingComponent;
using System.Windows.Interop;

namespace WpfD2DSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        // CodePack D2D
        D2DFactory d2dFactory;
        DWriteFactory dwriteFactory;
        RenderTarget renderTarget;
        TextFormat textFormat;

        // State management
        private bool m_isAnimating;
        private bool m_recreateSurfaceOnResize = true;
        Point m_lastMousePosition;
        bool m_hasCapture;

        // Maintained simply to detect changes in the interop back buffer
        IntPtr m_pIDXGISurfacePreviousNoRef;

        // (fake) data to plot
        int frameCount = 0;
        RectF graphVisibleArea;
        float scale = 1.01f;
        Point2F[] dataPoints;
        
        public MainWindow()
        {
            InitializeComponent();

            host.Loaded += new RoutedEventHandler(host_Loaded);
            host.SizeChanged += new SizeChangedEventHandler(host_SizeChanged);
            host.MouseMove += new MouseEventHandler(host_MouseMove);

            InitializeData();
        }

        #region Callbacks
        void host_Loaded(object sender, RoutedEventArgs e)
        {
            // Create the D2D Factory
            d2dFactory = D2DFactory.CreateFactory(D2DFactoryType.SingleThreaded);

            // Create the DWrite Factory
            dwriteFactory = DWriteFactory.CreateFactory();

            // The text format
            textFormat = dwriteFactory.CreateTextFormat("Bodoni MT", 24, DWrite.FontWeight.Normal, DWrite.FontStyle.Italic, DWrite.FontStretch.Normal);

            InteropImage.HWNDOwner = (new System.Windows.Interop.WindowInteropHelper(this)).Handle;
            InteropImage.OnRender = this.DoRender;

            // Start rendering now!
            InteropImage.RequestRender();
        }

        void host_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // Potentially update the interop surface size
            if (m_recreateSurfaceOnResize)
            {
                UpdateSize();
            }
        }

        private void ResetVisibleArea(object sender, RoutedEventArgs e)
        {
            graphVisibleArea = new RectF(0, 0, 1, 1);
            InteropImage.RequestRender();
        }

        private void ToggleScaleAnimation(object sender, RoutedEventArgs e)
        {
            if (m_isAnimating)
            {
                System.Windows.Media.CompositionTarget.Rendering -= new EventHandler(AnimatedScaleRenderCallback);
                m_isAnimating = false;
                ((Button)sender).Content = "Start Scale Animation";
            }
            else
            {
                System.Windows.Media.CompositionTarget.Rendering += new EventHandler(AnimatedScaleRenderCallback);
                m_isAnimating = true;
                ((Button)sender).Content = "Stop Scale Animation";
            }
        }

        private void host_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if ((Keyboard.IsKeyDown(Key.LeftCtrl) ||
                Keyboard.IsKeyDown(Key.RightCtrl)) &&
                host.CaptureMouse())
            {
                m_hasCapture = true;
                m_lastMousePosition = e.GetPosition(host);
            }
        }

        void host_MouseMove(object sender, MouseEventArgs e)
        {
            // Are we in capture/pan mode?
            if (m_hasCapture && Object.ReferenceEquals(Mouse.Captured, host))
            {
                Point curMousePosition = e.GetPosition(host);
                Vector delta = curMousePosition - m_lastMousePosition;
                m_lastMousePosition = curMousePosition;

                if (renderTarget != null)
                {
                    float graphDeltaX = (float)(delta.X / host.ActualWidth * graphVisibleArea.Width);
                    float graphDeltaY = (float)(delta.Y / host.ActualHeight * graphVisibleArea.Height);

                    graphVisibleArea.Left -= graphDeltaX;
                    graphVisibleArea.Right -= graphDeltaX;
                    graphVisibleArea.Top -= graphDeltaY;
                    graphVisibleArea.Bottom -= graphDeltaY;

                    InteropImage.RequestRender();
                }
            }
            else
            {
                Point mousePoint = e.GetPosition(host);
                Point2F dataPoint = DataPointFromMousePoint(mousePoint);

                int foundIdx = Array.BinarySearch(dataPoints, dataPoint, new PointComparer());

                float window = (float)(2.0 / host.ActualWidth * graphVisibleArea.Width);

                if (foundIdx < 0)
                {
                    int i = ~foundIdx;

                    for (; i < dataPoints.Length && dataPoints[i].X <= dataPoint.X + window; i++)
                    {
                        if ((dataPoints[i].X <= dataPoint.X + window) &&
                            (dataPoints[i].Y >= dataPoint.Y - window) &&
                            (dataPoints[i].Y <= dataPoint.Y + window))
                        {
                            foundIdx = i;
                            break;
                        }
                    }

                    if (foundIdx < 0)
                    {
                        i = ~foundIdx - 1;

                        for (; i >= 0 && dataPoints[i].X >= dataPoint.X + window; i--)
                        {
                            if ((dataPoints[i].X >= dataPoint.X + window) &&
                                (dataPoints[i].Y >= dataPoint.Y - window) &&
                                (dataPoints[i].Y <= dataPoint.Y + window))
                            {
                                foundIdx = i;
                                break;
                            }
                        }
                    }
                }

                if (foundIdx >= 0 && foundIdx < dataPoints.Length)
                {
                    Point2F foundPoint = dataPoints[foundIdx];

                    Point foundPointInCanvasSpace = MousePointFromDataPoint(foundPoint);

                    Canvas.SetLeft(Highlight, foundPointInCanvasSpace.X - 4);
                    Canvas.SetTop(Highlight, foundPointInCanvasSpace.Y - 4);
                    Highlight.Visibility = System.Windows.Visibility.Visible;

                    Details.DataContext = new { Point = new Point(foundPoint.X, 1 - foundPoint.Y), };

                    if (foundPointInCanvasSpace.X > host.ActualWidth / 2)
                    {
                        Details.ClearValue(Canvas.LeftProperty);
                        Canvas.SetRight(Details, host.ActualWidth - foundPointInCanvasSpace.X + 10);
                    }
                    else
                    {
                        Details.ClearValue(Canvas.RightProperty);
                        Canvas.SetLeft(Details, foundPointInCanvasSpace.X + 10);
                    }

                    if (foundPointInCanvasSpace.Y > host.ActualHeight / 2)
                    {
                        Details.ClearValue(Canvas.TopProperty);
                        Canvas.SetBottom(Details, host.ActualHeight - foundPointInCanvasSpace.Y + 10);
                    }
                    else
                    {
                        Details.ClearValue(Canvas.BottomProperty);
                        Canvas.SetTop(Details, foundPointInCanvasSpace.Y + 10);
                    }

                    Details.Visibility = System.Windows.Visibility.Visible;
                }
                else
                {
                    Highlight.Visibility = System.Windows.Visibility.Hidden;
                    Details.Visibility = System.Windows.Visibility.Hidden;
                }
            }
        }

        private void host_MouseUp(object sender, MouseButtonEventArgs e)
        {
            if (Object.ReferenceEquals(Mouse.Captured, host))
            {
                host.ReleaseMouseCapture();
                m_hasCapture = false;
            }
        }

        private void host_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Point curMousePosition = e.GetPosition(host);
            Point2F centerPoint = DataPointFromMousePoint(curMousePosition);

            float wheelScale = 1.1f;

            if (e.Delta > 0)
            {
                wheelScale = 1 / wheelScale;
            }

            float width = graphVisibleArea.Width;
            float height = graphVisibleArea.Height;
            graphVisibleArea.Left = (graphVisibleArea.Left - centerPoint.X) * wheelScale + centerPoint.X;
            graphVisibleArea.Top = (graphVisibleArea.Top - centerPoint.Y) * wheelScale + centerPoint.Y;
            graphVisibleArea.Right = graphVisibleArea.Left + width * wheelScale;
            graphVisibleArea.Bottom = graphVisibleArea.Top + height * wheelScale;

            InteropImage.RequestRender();
        }

        private void ScaleStretch_Checked(object sender, RoutedEventArgs e)
        {
            m_recreateSurfaceOnResize = true;
            UpdateSize();
        }

        private void ScaleStretch_Unchecked(object sender, RoutedEventArgs e)
        {
            m_recreateSurfaceOnResize = false;
        }

        void AnimatedScaleRenderCallback(object sender, EventArgs e)
        {
            if (++frameCount % 100 == 0)
            {
                scale = 1 / scale;
            }

            float width = graphVisibleArea.Width;
            float height = graphVisibleArea.Height;
            graphVisibleArea.Left = (graphVisibleArea.Left - 0.75f) * scale + 0.75f;
            graphVisibleArea.Top = (graphVisibleArea.Top - 0.75f) * scale + 0.75f;
            graphVisibleArea.Right = graphVisibleArea.Left + width * scale;
            graphVisibleArea.Bottom = graphVisibleArea.Top + height * scale;

            InteropImage.RequestRender();
        }
        #endregion Callbacks

        #region Helpers

        void UpdateSize()
        {
            // TODO: handle non-96 DPI
            uint surfWidth = (uint)(host.ActualWidth < 0 ? 0 : Math.Ceiling(host.ActualWidth));
            uint surfHeight = (uint)(host.ActualHeight < 0 ? 0 : Math.Ceiling(host.ActualHeight));

            InteropImage.SetPixelSize(surfWidth, surfHeight);
        }

        private void InitializeData()
        {
            dataPoints = new Point2F[100000];

            Random r = new Random();

            for (int i = 0; i < dataPoints.Length; i++)
            {
                float x = (float)r.NextDouble();
                dataPoints[i] = new Point2F(x, 1.0F - ((float)(Math.Pow(x, 2)) + x * (x * ((float)r.NextDouble() / 2.0f - 0.5f))));
            }

            // Sort for easy future lookup
            dataPoints = dataPoints.OrderBy((p) => { return p.X * 10000 + p.Y; }).ToArray();

            graphVisibleArea = new RectF(0, 0, 1, 1);
        }

        private Point MousePointFromDataPoint(Point2F dataPoint)
        {
            return new Point((float)((dataPoint.X - graphVisibleArea.Left) / graphVisibleArea.Width * host.ActualWidth),
                             (float)((dataPoint.Y - graphVisibleArea.Top) / graphVisibleArea.Height * host.ActualHeight));
        }


        private Point2F DataPointFromMousePoint(Point mousePos)
        {
            return new Point2F((float)(mousePos.X / host.ActualWidth * graphVisibleArea.Width + graphVisibleArea.Left),
                               (float)(mousePos.Y / host.ActualHeight * graphVisibleArea.Height + graphVisibleArea.Top));
        }
        
        private class PointComparer : IComparer<Point2F>
        {
            #region IComparer<Point2F> Members

            public int Compare(Point2F x, Point2F y)
            {
                return (x.X * 10000 + x.Y).CompareTo(y.X * 10000 + y.Y);
            }

            #endregion
        }

        #endregion Helpers

        #region D2D

        void PaintGrid(RenderTarget target)
        {
            double logW = Math.Log10(graphVisibleArea.Width);
            double logH = Math.Log10(graphVisibleArea.Height);

            SolidColorBrush spGridBrush = target.CreateSolidColorBrush(new ColorF(0.33f, 0.34f, 0.36f, 1.0f));
            SolidColorBrush blackBrush = renderTarget.CreateSolidColorBrush(new ColorF(0, 0, 0, 1));

            for (int i = 1; i <= 2; i++)
            {
                if (i == 2)
                {
                    spGridBrush.Opacity = -(float)((logW - 100) % 1) * 0.9f + 0.1f;
                }

                double xStep = Math.Pow(10, Math.Ceiling(logW - i));

                for (double x = graphVisibleArea.Left - Math.IEEERemainder(graphVisibleArea.Left, xStep); x < graphVisibleArea.Right; x += xStep)
                {
                    float scaledX = (float)((x - graphVisibleArea.Left) / graphVisibleArea.Width * (float)target.PixelSize.Width);
                    target.FillRectangle(new RectF(scaledX - 1, 0, scaledX + 1, renderTarget.PixelSize.Height), spGridBrush);

                    if (i == 1)
                    {
                        renderTarget.DrawText(
                                      String.Format("{0:G4}", x),
                                      textFormat,
                                      new RectF(scaledX - 15, target.PixelSize.Height - textFormat.FontSize - 2, scaledX + 45, target.PixelSize.Height), blackBrush);
                    }
                }

                if (i == 2)
                {
                    spGridBrush.Opacity = -(float)((logH - 100) % 1) * 0.9f + 0.1f;
                }

                double yStep = Math.Pow(10, Math.Ceiling(logH - i));

                for (double y = graphVisibleArea.Top - Math.IEEERemainder(graphVisibleArea.Top, xStep); y < graphVisibleArea.Bottom; y += yStep)
                {
                    float scaledY = (float)((y - graphVisibleArea.Top) / graphVisibleArea.Height * (float)target.PixelSize.Height);
                    target.FillRectangle(new RectF(0, scaledY - 1, renderTarget.PixelSize.Width, scaledY + 1), spGridBrush);

                    if (i == 1)
                    {
                        renderTarget.DrawText(
                                      String.Format("{0:G4}", (1.0f - y)),
                                      textFormat,
                                      new RectF(0, scaledY - textFormat.FontSize / 2.0f,
                                                60, scaledY + textFormat.FontSize / 2.0f), blackBrush);
                    }
                }
            }
        }

        void DoRender(IntPtr pIDXGISurface)
        {
            if (pIDXGISurface != m_pIDXGISurfacePreviousNoRef)
            {
                m_pIDXGISurfacePreviousNoRef = pIDXGISurface;

                // Create the render target
                Surface dxgiSurface = Surface.FromNativeSurface(pIDXGISurface);
                SurfaceDescription sd = dxgiSurface.Description;

                RenderTargetProperties rtp =
                    new RenderTargetProperties(
                        RenderTargetType.Default,
                        new PixelFormat(Format.Unknown, AlphaMode.Premultiplied),
                        96,
                        96,
                        RenderTargetUsage.None,
                        Microsoft.WindowsAPICodePack.DirectX.Direct3D.FeatureLevel.Default);

                try
                {
                    renderTarget = d2dFactory.CreateDxgiSurfaceRenderTarget(dxgiSurface, rtp);
                }
                catch (Exception)
                {
                    return;
                }

                // Clear the surface to transparent
                renderTarget.BeginDraw();
                renderTarget.Clear(new ColorF(1, 1, 1, 0));
                renderTarget.EndDraw();
            }

            SolidColorBrush spBrush = renderTarget.CreateSolidColorBrush(new ColorF(1, 1, 1, 1.0f));

            renderTarget.BeginDraw();

            renderTarget.Clear(new ColorF(1, 1, 1, 0));

            PaintGrid(renderTarget);

            float scaleX = (float)renderTarget.PixelSize.Width / graphVisibleArea.Width;
            float scaleY = (float)renderTarget.PixelSize.Height / graphVisibleArea.Height;
            float opacity = Math.Max(renderTarget.PixelSize.Width * renderTarget.PixelSize.Height / (25.0f * (dataPoints.Length * graphVisibleArea.Width * graphVisibleArea.Height)), 0) * 0.8f + 0.2F;

            for (int i = 0; i < dataPoints.Length; i++)
            {
                Point2F point = dataPoints[i];

                if (point.X >= graphVisibleArea.Left && point.X <= graphVisibleArea.Right &&
                    point.Y >= graphVisibleArea.Top && point.Y <= graphVisibleArea.Bottom)
                {
                    spBrush.Color = new ColorF(point.Y, point.X, Math.Abs((point.X - 0.5f) * (point.Y - 0.5f) * 4), opacity);

                    float left = (point.X - graphVisibleArea.Left) * scaleX - 1.5f;
                    float top = (point.Y - graphVisibleArea.Top) * scaleY - 1.5f;

                    // Subtract Y from 1.0f to invert so the origin is bottom/left.
                    renderTarget.FillRectangle(
                        new Microsoft.WindowsAPICodePack.DirectX.Direct2D1.RectF(left, top, left + 3, top + 3),
                        spBrush);
                }
            }

            renderTarget.EndDraw();
        }

        #endregion D2D
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Founder Cheesy Design
Taiwan Taiwan
John graduated from the University of South Australia in 1997 with a Bachelor of Electronic Engineering Degree, and since then he has worked on hardware and software in many fields including Aerospace, Defence, and Medical giving him over 10 of years experience in C++ and C# programming. In 2009 John Started his own contracting company doing business between Taiwan and Australia.

Comments and Discussions