Click here to Skip to main content
15,895,084 members
Articles / Desktop Programming / WPF

WPF and .NET 3.5 - Drawing Customized Controls and Custom UI Elements

Rate me:
Please Sign up or sign in to vote.
4.80/5 (24 votes)
10 Dec 2007CPOL8 min read 162.4K   3.9K   106  
Using Visual Studio 2008 for custom drawing using WPF and .NET 3.5; fun with Spirographs
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows;
using System.Collections.Generic;
using System.Windows.Media.Effects;
using System;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Reflection;

namespace WpfApplication1
{
    public enum DrawingMode
    { 
        ImageOnly,
        AnimateImage,
        AnimateImageAndCircles
    }

    public class GraphContext : Canvas
    {
        private delegate void AnimationDelegate();

        #region Declarations
        private List<Points> _points;
        private Spirograph _graph;
        private Point _firstPoint;
        private bool _changed;
        private int _animations;
        private System.Timers.Timer _timer;
        private DrawingContext _context;
        private Ellipse _fixedCircle;
        private Ellipse _movingCircle;
        private Ellipse _pen;
        private Line _spoke;
        #endregion

        #region Methods
        private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            try
            {
                AnimationDelegate animationMethod = new AnimationDelegate(this.AnimateGraphThreaded);

                this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, animationMethod);

                if (_animations == _points.Count - 1)
                {
                    _timer.Stop();
                    AnimationDelegate gapMethod = new AnimationDelegate(this.AnimateGraphGapThreaded);
                    this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, gapMethod);
                }
            }
            catch(TargetInvocationException){}
        }
        /// <summary>
        /// Sets the epicycloid so plotting points can be retrieved.
        /// </summary>
        /// <param name="graph">The graph.</param>
        internal void SetGraph(Spirograph graph)
        {
            if (_timer != null)
            {
                _timer.Stop();
            }

            // Clear residual animation objects
            this.Children.Remove(_fixedCircle);
            this.Children.Remove(_movingCircle);
            this.Children.Remove(_pen);
            this.Children.Remove(_spoke);
            _fixedCircle = null;
            _movingCircle = null;
            _pen = null;
            _spoke = null;

            _changed = true;
            _graph = graph;
            _points = _graph.GetPoints();

            // Remove the first point so no line is drawn from 0,0 to the first plot point
            _firstPoint = _points[0].ToPoint;
            _points.RemoveAt(0);
            
            this.InvalidateVisual();
        }
        /// <summary>
        /// Graph drawing implementation
        /// </summary>
        /// <param name="drawingContext">The drawing context.</param>
        private void Draw(DrawingContext drawingContext)
        {
            // Remove any current graph
            this.Children.Clear();

            // Plot the points
            if (_points != null && _points.Count > 0)
            {
                switch(this.DrawingMode)
                {
                    case DrawingMode.ImageOnly:
                        DrawStaticGraph(drawingContext);
                        break;

                    case DrawingMode.AnimateImage:
                    case DrawingMode.AnimateImageAndCircles:
                        AnimateGraph(drawingContext);
                        break;
                }
            }
        }
        /// <summary>
        /// Draws a static image of the graph.
        /// </summary>
        /// <param name="drawingContext">The drawing context.</param>
        private void DrawStaticGraph(DrawingContext drawingContext)
        {
            // PathGeometry is a nice alternative to drawingContext.DrawLine(...) as it 
            // allows the points to be rendered as an image that can be further manipulated
            PathGeometry geometry = new PathGeometry();
            
            // Add all points to the geometry
            foreach (Points pointXY in _points)
            {
                PathFigure figure = new PathFigure();
                figure.StartPoint = pointXY.FromPoint;
                figure.Segments.Add(new LineSegment(pointXY.ToPoint, true));
                geometry.Figures.Add(figure);
            }

            // Add the first point to close the gap from the graph's end point to graph's start point 
            PathFigure lastFigure = new PathFigure();
            lastFigure.StartPoint = _points[_points.Count - 1].FromPoint;
            lastFigure.Segments.Add(new LineSegment(_firstPoint, true));
            geometry.Figures.Add(lastFigure);

            // Create a new drawing and drawing group in order to apply a custom drawing effect
            GeometryDrawing drawing = new GeometryDrawing(this.Pen.Brush, this.Pen, geometry);
            DrawingGroup drawingGroup = new DrawingGroup();
            drawingGroup.Children.Add(drawing);

            // Clip the drawing so it doesn't extend outside the canvas boundaries
            drawingGroup.ClipGeometry = new RectangleGeometry(new Rect(0, 0, this.ActualWidth, this.ActualHeight));

            // Create a blur effect (i.e. "softness")
            BlurBitmapEffect blurEffect = new BlurBitmapEffect();
            blurEffect.Radius = Softness;
            drawingGroup.BitmapEffect = blurEffect;

            // Use an Image control and a DrawingImage to display the graph
            DrawingImage drawingImage = new DrawingImage(drawingGroup);

            // Freeze the DrawingImage for performance benefits.
            drawingImage.Freeze();

            Image image = new Image();
            image.Source = drawingImage;
            image.Stretch = Stretch.None;

            // Center the image on the canvas
            double x = this.ActualWidth / 2 - drawingImage.Width / 2;
            double y = this.ActualHeight / 2 - drawingImage.Height / 2;
            image.Margin = new Thickness(x, y, 1, 1);

            // Add the image to the UI
            this.Children.Add(image);
        }
        /// <summary>
        /// Inserts the animated point.
        /// </summary>
        /// <param name="fromX">From X.</param>
        /// <param name="fromY">From Y.</param>
        /// <param name="toX">To X.</param>
        /// <param name="toY">To Y.</param>
        private void InsertAnimatedPoint(Points pointXY)
        {
            // Create a line segment from the to and from points
            Line segmentAnimation = new Line();
            segmentAnimation.X1 = pointXY.FromPoint.X;
            segmentAnimation.Y1 = pointXY.FromPoint.Y;
            segmentAnimation.X2 = pointXY.ToPoint.X;
            segmentAnimation.Y2 = pointXY.ToPoint.Y;

            // Create a blur effect (i.e. "softness")
            BlurBitmapEffect blurEffect = new BlurBitmapEffect();
            blurEffect.Radius = Softness;

            // Apply the blur and draw the line
            segmentAnimation.Stroke = this.Pen.Brush;
            segmentAnimation.StrokeThickness = this.Pen.Thickness;
            segmentAnimation.BitmapEffect = blurEffect;
            this.Children.Insert(0, segmentAnimation);

            // Draw fixed and moving circle elements if animation is selected
            if (DrawingMode == DrawingMode.AnimateImageAndCircles && _context != null)
            {
                DrawFixedCircle();
                DrawMovingCircle(pointXY);
                DrawPen(pointXY);
                DrawSpoke(pointXY);
            }
        }
        /// <summary>
        /// Draws the fixed circle while animating.
        /// </summary>
        private void DrawFixedCircle()
        {
            // Only draw the fixed circle one time
            if (_fixedCircle == null)
            {
                // Create the circle
                _fixedCircle = new Ellipse();
                _fixedCircle.Width = _graph.InnerRadius * 2;
                _fixedCircle.Height = _graph.InnerRadius * 2;
                _fixedCircle.Stroke = new SolidColorBrush(Colors.White);
                _fixedCircle.StrokeThickness = 1;

                // Get the center x and y coordinates
                double x = this.ActualWidth / 2 - _graph.InnerRadius;
                double y = this.ActualHeight / 2 - _graph.InnerRadius;
                _fixedCircle.Margin = new Thickness(x, y, 1, 1);

                // Add the circle to the canvas
                this.Children.Add(_fixedCircle);
            }
        }
        /// <summary>
        /// Draws the moving circle while animating.
        /// </summary>
        /// <param name="pointXY">The point XY.</param>
        private void DrawMovingCircle(Points pointXY)
        {
            // Remove the moving circle from the UI element list since it mves each
            // time the graph is painted
            if (_movingCircle != null)
            {
                this.Children.Remove(_movingCircle);
            }

            // Create the circle
            _movingCircle = new Ellipse();
            _movingCircle.Width = Math.Abs(_graph.OuterRadius * 2);
            _movingCircle.Height = Math.Abs(_graph.OuterRadius * 2);
            _movingCircle.Stroke = new SolidColorBrush(Colors.White);
            _movingCircle.StrokeThickness = 1;

            // Get the center x and y coordinates
            double x = pointXY.CenterX - pointXY.Sign * _graph.OuterRadius;
            double y = pointXY.CenterY - pointXY.Sign * _graph.OuterRadius;
            _movingCircle.Margin = new Thickness(x, y, 1, 1);

            // Add the circle to the canvas
            this.Children.Add(_movingCircle);
        }
        /// <summary>
        /// Draws the pen point while animating.
        /// </summary>
        /// <param name="pointXY">The point XY.</param>
        private void DrawPen(Points pointXY)
        {
            // Remove the pene from the UI element list since it mves each
            // time the graph is painted
            if (_pen != null)
            {
                this.Children.Remove(_pen);
            }

            _pen = new Ellipse();
            _pen.Width = 5;
            _pen.Height = 5;
            _pen.Fill = new SolidColorBrush(Colors.White);

            _pen.Margin = new Thickness(pointXY.ToPoint.X - 2.5, pointXY.ToPoint.Y - 2.5, 1, 1);

            this.Children.Add(_pen);
        }
        /// <summary>
        /// Draws the spoke while animating.
        /// </summary>
        /// <param name="pointXY">The point XY.</param>
        private void DrawSpoke(Points pointXY) 
        {
            // Remove the spoke from the UI element list since it mves each
            // time the graph is painted
            if (_spoke != null)
            {
                this.Children.Remove(_spoke);
            }

            double direction = pointXY.Sign;

            // Determine if the spoke is inside our outside the moving circle
            if (pointXY.Position < 0)
            {
                direction *= -1;
            }

            // Get the X and Y coordinates of the circle's edge
            double x = pointXY.CenterX + direction * _graph.OuterRadius * Math.Cos(pointXY.Phi);
            double y = pointXY.CenterY - direction * _graph.OuterRadius * Math.Sin(pointXY.Phi);

            _spoke = new Line();
            _spoke.Name = "spoke";
            _spoke.X1 = x;
            _spoke.Y1 = y;
            _spoke.X2 = pointXY.ToPoint.X;
            _spoke.Y2 = pointXY.ToPoint.Y;
          
            // Apply the blur and draw the line
            _spoke.Stroke = new SolidColorBrush(Colors.White);
            _spoke.StrokeThickness = 1;

            this.Children.Insert(0, _spoke);
        }
        /// <summary>
        /// Animates the graph.
        /// </summary>
        /// <param name="drawingContext">The drawing context.</param>
        private void AnimateGraph(DrawingContext drawingContext)
        {
            // Start the timer to draw a new image every X seconds
            _context = drawingContext;
            _animations = 0;
            _timer = new System.Timers.Timer();
            _timer.Interval = Velocity;
            _timer.Elapsed += new System.Timers.ElapsedEventHandler(_timer_Elapsed);
            _timer.Start();
        }
        /// <summary>
        /// Threaded method for graph animation
        /// </summary>
        private void AnimateGraphThreaded()
        {
            try
            {
                InsertAnimatedPoint(_points[_animations++]);
            }
            catch (IndexOutOfRangeException) { }
        }
        /// <summary>
        /// Threaded method for graph animation completion (i.e. connect the start and end points)
        /// </summary>
        private void AnimateGraphGapThreaded()
        {
            try
            {
                Points points = new Points();
                points.FromPoint = _points[_points.Count - 1].FromPoint;
                points.ToPoint = _firstPoint;
                points.CenterX = _points[0].CenterX;
                points.CenterY = _points[0].CenterY;
                points.Phi = _points[0].Phi;
                points.Position = _points[0].Position;
                points.Sign = _points[0].Sign;

                InsertAnimatedPoint(points);
            }
            catch (IndexOutOfRangeException) { }
        }
        #endregion

        #region Overridden Methods
        /// <summary>
        /// Draws the graph
        /// </summary>
        /// <param name="drawingContext">The drawing instructions for a specific element. This context is provided to the layout system.</param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            if (_changed)
            {
                _changed = false;
                Draw(drawingContext);
            }
        }
        /// <summary>
        /// Redraws the graph since the display area's height and width changed
        /// </summary>
        /// <param name="sizeInfo">Details of the old and new size involved in the change.</param>
        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            _changed = true;
            this.InvalidateVisual();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the softness of the graph's line.
        /// </summary>
        /// <value>The drawing line softness.</value>
        public double Softness { get; set; }
        /// <summary>
        /// Gets or sets the pen.
        /// </summary>
        /// <value>The pen used for drawing.</value>
        public Pen Pen { get; set; }
        /// <summary>
        /// Gets or sets the pen's brush.
        /// </summary>
        /// <value>The pen's brush.</value>
        public Brush Brush { get; set; }
        /// <summary>
        /// Gets or sets the drawing mode.
        /// </summary>
        /// <value>The drawing mode.</value>
        public DrawingMode DrawingMode { get; set; }
        /// <summary>
        /// Gets or sets the velocity of the animation.
        /// </summary>
        /// <value>The velocity.</value>
        public double Velocity { get; set; }
        #endregion
    }
}

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 The Code Project Open License (CPOL)


Written By
Web Developer PageLabs
United States United States
I'm the founder of PageLabs, a web-based performance and SEO optimization site.

Give your site a boost in performance, even take a free speed test!

http://www.pagelabs.com

Comments and Discussions