Click here to Skip to main content
15,891,738 members
Articles / Desktop Programming / WPF

Bullet Graphs - A Custom Control - WPF vs. Windows Forms

Rate me:
Please Sign up or sign in to vote.
4.88/5 (50 votes)
10 Oct 2008CPOL19 min read 184.5K   6.4K   163  
This article compares the development of a Line of Business control, the Bullet Graph, in both WPF and Windows Forms.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing.Design;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;


namespace WinFormsBulletGraph
{
    /// <summary>
    /// A bullet graph control that plots the graphical components of a bullet graph
    /// </summary>
    [DefaultBindingProperty("FeaturedMeasure")]
    public partial class BulletGraph : UserControl
    {
        /// <summary>
        /// Defines a single qualatative range within a bullet graph.
        /// </summary>
        [TypeConverter(typeof(ExpandableObjectConverter))]
        public class QualitativeRange
        {
            public int? Maximum {get; set;}

            public Color Color { get; set; }

            public override string ToString()
            {
                return "Maximum: " + (Maximum.HasValue ? Maximum.ToString() : "<null>") + ", Color: " + Color;
            }
        }

        private QualitativeRange[] qualitativeRanges = new QualitativeRange[] { };

        [Description("The qualitative ranges to display")]
        [Category("Bullet Graph")]
        public QualitativeRange[] QualitativeRanges
        {
            get { return qualitativeRanges; }
            set { qualitativeRanges = value; }
        }

        private FlowDirection flowDirection = FlowDirection.LeftToRight;

        [Description("The rendered direction of this graph")]
        [Category("Bullet Graph")]
        public FlowDirection FlowDirection
        {
            get { return flowDirection; }
            set { flowDirection = value; }
        }

        private int graphRange=100;

        [Description("The range/maximum of this graph")]
        [Category("Bullet Graph")]
        public int GraphRange
        {
            get { return graphRange; }
            set { graphRange = value; }
        }

        private int featuredMeasure = 77;

        [Description("The featured measure displayed on this graph")]
        [Category("Bullet Graph")]
        public int FeaturedMeasure
        {
            get { return featuredMeasure; }
            set {
                featuredMeasure = value;
                toolTip1.SetToolTip(this.featuredMeasureRect, "Featured Measure: " + Convert.ToString(featuredMeasure));
                this.Invalidate(this.featuredMeasureRect.ClientRectangle);
            }
        }

        /// <summary>
        /// private accessor for features measure, this method imposes the condition
        /// that featured measure should not exceed the range of the graph
        /// </summary>
        /// <returns></returns>
        private int GetFeaturedMeasure()
        {
            int measure = featuredMeasure;
                        
            if (graphRange>0 && measure > graphRange)
            {
                measure = graphRange;
            }
            if (graphRange<0 && measure < graphRange)
            {
                measure = graphRange;
            }

            return measure;
        }

        private int comparativeMeasure = 86;

        [Description("The comparative measure displayed on this graph")]
        [Category("Bullet Graph")]
        public int ComparativeMeasure
        {
            get { return comparativeMeasure; }
            set {
                comparativeMeasure = value;
                toolTip1.SetToolTip(this.comparativeMeasureRect, "Comparative Measure: " + Convert.ToString(comparativeMeasure));
            }
        }


        public BulletGraph()
        {
            InitializeComponent();

            bulletBackground.Paint += new PaintEventHandler(bulletBackground_Paint);
            this.Paint += new PaintEventHandler(BulletGraph_Paint);
  
        }

        /// <summary>
        /// override the paint method to plot the axes
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BulletGraph_Paint(object sender, PaintEventArgs e)
        {
            Rectangle rec = bulletBackground.ClientRectangle;
            rec.Offset(bulletBackground.Location);
            float ratio = (float)rec.Width / (float)graphRange;

            // determine the tick spacing
            float tickSpacing = graphRange>0 ? 1 : -1;
            float[] multipliers = new float[] { 2.5f, 2, 2 };
            int multiplierIndex = 0;
            while (Math.Abs((float)graphRange / tickSpacing) > 7)
            {
                tickSpacing *= multipliers[multiplierIndex % multipliers.Length];
                multiplierIndex++;
            }

            // plot the axes
            float tickPosition = 0;
            while (Math.Abs(tickPosition) <= Math.Abs((float)graphRange))
            {
                float xPos = rec.Left + tickPosition * ratio;

                // bottom tick marks
                PointF p1 = new PointF(xPos, rec.Bottom);
                PointF p2 = new PointF(xPos, rec.Bottom + 3);
                e.Graphics.DrawLine(Pens.Black, transform(p1, rec), transform(p2, rec));

                // upper tick marks
                p1 = new PointF(xPos, rec.Top);
                p2 = new PointF(xPos, rec.Top - 3);
                e.Graphics.DrawLine(Pens.Black, transform(p1, rec), transform(p2, rec));
                
                // labels
                Rectangle labelRec = transform(new Rectangle((int)xPos - 15, rec.Bottom + 4, 30, 15), rec);
                StringFormat drawFormat = new StringFormat();
                drawFormat.Alignment = StringAlignment.Center;
                e.Graphics.DrawString(Convert.ToString(tickPosition), this.Font, Brushes.Black, labelRec, drawFormat);

                tickPosition += tickSpacing;
            }


            // plot the featured measure
            float thirdOfHeight = (float)rec.Height / 3;
            int x = rec.Left;
            int y = (int) ((float)rec.Top + thirdOfHeight);
            int width = (int)((float)GetFeaturedMeasure() * ratio);
            int height = (int)thirdOfHeight;

            Rectangle controlRec = transform(new Rectangle(x, y, width, height), rec);

            featuredMeasureRect.Location = controlRec.Location;
            featuredMeasureRect.Size = controlRec.Size;

            // plot the comparative measure
            float sixthOfHeight = (float)rec.Height / 6;
            x = (int)((float)comparativeMeasure * ratio + rec.Left);
            y = (int)((float)rec.Top + sixthOfHeight);
            width = rec.Width / 80;
            height = (int)(sixthOfHeight*4);

            controlRec = transform(new Rectangle(x, y, width, height), rec);

            comparativeMeasureRect.Location = controlRec.Location;
            comparativeMeasureRect.Size = controlRec.Size;
        }

        PointF transform(PointF point, Rectangle container)
        {
            if (flowDirection == FlowDirection.LeftToRight)
                return point;
            else
            {
                float x = container.X * 2 + container.Width - point.X ;
                return new PointF(x, point.Y);
            }
        }

        Rectangle transform(Rectangle rec, Rectangle container)
        {
            if (flowDirection == FlowDirection.LeftToRight)
                return rec;
            else
            {
                int x = container.X * 2 +  container.Width - rec.X - rec.Width;
                return new Rectangle(x, rec.Y, rec.Width, rec.Height);
            }
        }

        RectangleF transform(RectangleF rec, Rectangle container)
        {
            if (flowDirection == FlowDirection.LeftToRight)
                return rec;
            else
            {
                float x = container.X * 2 + container.Width - rec.X - rec.Width;
                return new RectangleF(x, rec.Y, rec.Width, rec.Height);
            }
        }

        void bulletBackground_Paint(object sender, PaintEventArgs e)
        {
            if (qualitativeRanges == null)
                return;

            Rectangle rec = bulletBackground.ClientRectangle;
            float ratio = (float)rec.Width / (float)graphRange;

            float x = rec.Left;
            int previousValue = 0;
            foreach (QualitativeRange backgroundRegion in qualitativeRanges)
            {
                float x1 = x;
                float x2;
                if (backgroundRegion.Maximum.HasValue)
                {
                    x2 = x1 + (float)(backgroundRegion.Maximum.Value-previousValue) * ratio;
                    previousValue = backgroundRegion.Maximum.Value;
                }
                else
                {
                    x2 = rec.Right;
                }

                using (Brush brush = new SolidBrush(backgroundRegion.Color))
                {
                    e.Graphics.FillRectangle(brush, transform(new RectangleF(x1, rec.Top, (x2 - x1), rec.Height), rec));
                }

                x = x2;
            }
            
        }
    }
    
}

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
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.

I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.

I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.

Visit my blog - Colin Eberhardt's Adventures in .NET.

Follow me on Twitter - @ColinEberhardt

-

Comments and Discussions