During the development of a videogame skeleton, one of my biggest concerns was the drawing of a large interstellar map. Since it was just a skeleton, implementing 3D graphics with OpenGL or Direct3D was not an option. I soon discovered that GDI+ could very well fit to my needs. Next I had to face mainly two issues:
- While .NET panels reason in terms of "pixels", my map was based on some "light years" metrical units.
- I had to imagine an algorithm to easily paint a sphere (or spheroid) on that map
The included code shows how I solved those problems. It could be helpful to show how easy is to add graphics and shapes using GDI+ and .NET.
Conversion between pixel geometry and logical-units geometry
When we want to display something using GDI+, we must reason in terms of pixels. The
Graphics object can be obtained by calling the
CreateGraphics method from any inherited WinForm control. So, for instance, a
Panel is an optimal choice as a drawing surface, much better than the form itself. When a
Graphics object is initialized, it's geometry is based on the pixel displayed, being (0,0) the left-upper corner of it.
If I must draw a line, say, from the point (10,10) to the point (30,30) I must call:
Graphics panelGraphics = this.panel.CreateGraphics();
Since every method and property of GDI+ is based on pixels, drawing items in terms of some other units of measure implies a conversion. Also, when we use "real world" formulae, they must be adapted to the "pixel" logic. Instead of spreading the conversions throughout the code, a class is what is needed.
We could use the .NET library class
Graphics.PageScale to do so, since there are overloaded Graphics methods that accept float numbers (measure units) as input instead of integer numbers (pixel). But I preferred to write a class that has the same functions of
PageScale, but it's slightly different in concept. You do not have to compute the
PageScale constant, since it does that for you. Plus, you may use double precision.
GridView class provides the methods to pass from a "pixel" geometry to some other "logical units" geometry. So, when you have to draw a rectangle with a width of 4.5 cm and a height of 3.0 cm to a 400x500 panel, all you have to do is:
GridView with desired width of panel in logical units (if you want to display the 4.5 wide rectangle, a
logicalW parameter of 10.0 seems perfect)
- Pass your panel size in pixel
In the case above the
GridView will be constructed with:
GridView gv = new GridView(10.0,400,500);
Now, if you want to know how many pixels in this panel, a rectangle of 4.5 cm wide is, or if you want to know where the point (1.2cm,2.4cm) is, you may use:
int pixelWidth = gv.getPhysicalWidth(4.5);
int pX = gv.getPhysicalX(1.2);
int pY = gv.getPhysicalY(2.4);
GridView is initialized, the panel has now 2D cartesian axis (in the downloadable example, the length of them is 5.0 and the coordinates are displayed on top right).
An object of the class
Sphere can be initialized like below:
Sphere s1 = new Sphere(gv,r,this.centerX,this.centerY);
gv is the initialized
r is the sphere radius
centerX is the X coordinate of the center (in logical units)
centerY is the Y coordinate of the center (in logical units)
The algorithm for drawing the sphere is very simple, and it is based on drawing arcs in a rectangle whose dimensions shrink. The algorithm draws the arcs first from right to left, then from top to bottom.
private void drawArcs(Graphics g, Pen color, Rectangle r)
for (int j=r.Width; j>0; j-=10)
int left = r.Left+(r.Width-j)/2;
Rectangle rc = new Rectangle(left,r.Top,j,r.Height);
for (int j=r.Height; j>0; j-=10)
int top = r.Top+(r.Height-j)/2;
Rectangle rc = new Rectangle(r.Left,top,r.Width,j);
Modifying the number of loops (j-=10) you can obtain finer wires. You can also modify the width of the
Pen in order to get different results.
Point of Interest
When you draw something on a
Control, on a
Panel in this case, if you cover the control with another control (or another window), the contents just drawn disappears. For this reason, every new sphere that you draw is collected into an
ArrayList collection. Whenever the
Paint event is raised, I call the
paint method for every shape in the collection.