views:

312

answers:

1

I am creating a rectangle as a custom control object that is placed within a Panel control. The control rectangle is created and evidenced by the Panel's Paint event:

void myPanel_Paint(object sender, PaintEventArgs e)
{
 Graphics g = e.Graphics;

 foreach(ControlItem item in controls)
  g.DrawRectangle(new Pen(Brushes.Blue), new Rectangle(item.Location, item.Size));

 g.Dispose();
}

In order to remove the foreach above, and to improve rendering performance, I am trying to give the custom rectangle control a similar Paint event (so it isn't confused with the standard Paint event, I am calling my event Render).

Custom base class constructor:

   { controlBase = new Bitmap(50, 25);  /*Create a default bitmap*/ }

Custom base class CreateGraphics:

   { return Graphics.FromImage(controlBase); }

Obviously, I require an event handler for rendering/painting:

public class RenderEventArgs : PaintEventArgs
{
 public RenderEventArgs(Graphics g, Rectangle rect) :
     base(g, rect)
 { }
}

However, the following is not providing the expected results:

void item_Render(object sender, RenderEventArgs e)
{
 Graphics g = e.Graphics;
 g.DrawRectangle(new Pen(Brushes.Blue),
  new Rectangle(((myBaseClass)sender).Location, ((myBaseClass)sender).Size));
 g.Dispose();
}

So, I'm trying to figure out what I am missing. The Panel that contains the rectangle objects sets the size and location. I'm not sure if I have all the information here, so if you have any questions, please feel free to ask.

Thanks...

Edit: @Henk Holterman - I raise the OnRender in my derived Constructor:

public ControlItem()
 : base()
{
 Graphics g = CreateGraphics();
 Pen p = new Pen(Color.Black, 2.5f);

 BackColor = Color.Beige;

 g.DrawRectangle(p, Bounds);
 OnRender(new RenderEventArgs(g, Bounds));

 g.Dispose();
}

I am not sure that I am raising this in the right place(s). When the parent Panel control sets Location:

public Point Location
{ 
 get { return myRectangle.Location; }
 set 
 { 
  myRectangle.Location = value;
  DrawBase();
 }
}

...where...

private void DrawBase()
{
 Graphics g = CreateGraphics();
 g.DrawImage(controlBase, Location);
 g.Dispose();
}
+2  A: 

Abstracting inner items with Render states is nice for organizational purposes, but doesn't necessarily improve performance. Also, when using the Graphics object, you can only draw on a Graphics object that has been initialized with a context, whether a screen/display, bitmap, or other output media (like Metafiles). It is unusual to initialize a Graphics object in a constructor if you're not making use of off-screen contexts.

To improve your drawing code, you should start by minimizing the areas that are being drawn. A good heuristic to keep in mind is "the lower the number of pixels drawn = the faster my display can be".

With that in mind, you should look at using the ClipRectangle property in the PaintEventArgs object. This tells you the region of the Graphics object that needs to be updated. From here, use whatever Graphics object is given to you to do your drawing update work.

Also, if you need to redraw one of your inner elements, you should just invalidate the area that needs to be redrawn. This, combined with the use of the ClipRectangle when redrawing, will keep down the amount of actual display work being performed.

PaintEventArgs.ClipRectangle Property (System.Windows.Forms) @ MSDN

I've written a sample that you can pick apart to see all of the above in action. While not the best possible (for example, some things can be recycled, like Pens and Brushes), it may give you some better ideas on how to approach your drawing.

Note that this sample doesn't use Events. I'm sure you can figure out how to alter it to use events if you like. I omitted them because foreach()/enumeration is not necessarily a drawback if you aren't redrawing everything every time you need an update. If your drawn figures are so complex that you want to use events, you may be better off creating new user controls. Also, you will need to check overlaps for each inner item whether you use events or foreach().

First, I created a C# Forms application called DefinedRectangles. Then, I added a DisplayRect class to represent rectangle outlines.

Here is the source code for the Form. I added a MouseDoubleClick handler to the form to allow changes to the state of the rectangles. Rectangles can be solid/dashed depending on the last state, and will flip back and forth as their inside area is double-clicked.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace DefinedRectangles
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            innerItems = new List<DisplayRect>();
            innerItems.Add(new DisplayRect(new Rectangle(0, 0, 50, 50), Color.Blue, true));
            innerItems.Add(new DisplayRect(new Rectangle(76, 0, 100, 50), Color.Green, false));
            innerItems.Add(new DisplayRect(new Rectangle(0, 76, 50, 100), Color.Pink, false));
            innerItems.Add(new DisplayRect(new Rectangle(101, 101, 75, 75), Color.Orange, true));
        }

        List<DisplayRect> innerItems;

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            foreach(DisplayRect dispItem in innerItems)
            {
                dispItem.OnPaint(this, e);
            }
        }

        private void Form1_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            foreach (DisplayRect dispItem in innerItems)
            {
                dispItem.OnHitTest(this, e);
            }
        }
    }
}

Here is the source code for the DisplayRect class. Note that when we need to invalidate, we have to adjust our update rectangle to make up for quirks between rectangle borders and rectangle areas.

using System;
using System.Collections.Generic;
using System.Text;

using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace DefinedRectangles
{
    public class DisplayRect
    {
        public DisplayRect(Rectangle dispArea, Color dispColor, bool dashed)
        {
            m_area = dispArea;
            m_areaColor = dispColor;
            m_solidLines = !dashed;
        }

        Rectangle m_area;
        Color m_areaColor;
        bool m_solidLines;

        public Rectangle Bounds { get { return m_area; } }

        public void OnPaint(object sender, PaintEventArgs e)
        {
            if (!m_area.IntersectsWith(e.ClipRectangle)) { return; }

            Graphics g = e.Graphics;
            using (Pen p = new Pen(m_areaColor))
            {
                if (m_solidLines)
                {
                    p.DashStyle = DashStyle.Solid;
                }
                else
                {
                    p.DashStyle = DashStyle.Dot;
                }
                // This could be improved to just the border lines that need to be redrawn
                g.DrawRectangle(p, m_area);
            }
        }

        public void OnHitTest(object sender, MouseEventArgs e)
        {
            // Invalidation Rectangles don't include the outside bounds, while pen-drawn rectangles do.
            // We'll inflate the rectangle by 1 to make up for this issue so we can handle the hit region properly.
            Rectangle r = m_area;
            r.Inflate(1, 1);
            if (r.Contains(e.X, e.Y))
            {
                m_solidLines = !m_solidLines;
                Control C = (Control)sender;
                C.Invalidate(r);
            }
        }
    }
}
meklarian
WOW!!! Thank you very much for this insight. Much appreciated.
dboarman
No problem. Hope this helps. Also, try modifying the example rectangles so that some of them overlap. You can see that all the rectangles in overlapping regions will change if a hit is detected in those areas. From there, you may also need to add z-ordering if this is not a desired behavior.
meklarian