The Colored Doughnuts
Up to now, with the exception of the Skyscrapers class (which came from another application) the samples have been specially designed to explain the whole mechanism as simply as possible. In reality, you'll probably want to add contours to more complicated objects, allowing them to both move and rotate, so here's a more complicated example. One financial plot description shows a picture of several coaxial rings colored by sectors (see Figure 2
), which served as another exercise to check whether the design of moveable/resizable graphics is complete. You'll find the description for class DoughnutSet in the file DoughnutSet.cs
, and all the code samples for working with this class in the file Form_DoughnutSet.cs
|Figure 2. A DoughnutSet Object: A doughnut set consists of a set of coaxial rings containing an arbitrary number of colored sections.|
Each ring consists of a number of sectors (two is the minimum). To distinguish the sectors, each has its own color. You can show values as numbers or as percentages, and you can change the color, text, borders, or font for each sector (you can actually change all viewing parameters; however that's outside the scope of this article). You are free to add features to set these parameters specifically, but for the purposes of this application, the parameters are mostly selected at random:
public class DoughnutSet :
List<DoughnutRing> rings =
new List<DoughnutRing> ();
Nodes are located at the points where the sectors' borders cross with the inner or outer border of each ring (see Figure 2
). The pairs of nodes on each border are connected to each other, but there are no connections between the pairs, so the contour consists of an unlimited number of disjoint sets.
|Author's Note: You can see the best illustration of this strange contour in the main form of the application. Switch ON the contours using the small button in the corner and move the picture of the DoughnutSet object to the same location where you can see Y(x) plotting. The DoughnutSet picture will be blocked from view by that plotting, but its contour will be in perfect view above it.
Clicking the left mouse button on any DoughnutSet node lets you move it along the radius, thus changing the width of the ring. Pressing the right mouse button on any node regardless of whether it belongs to an inner or outer circle of the ring starts rotation of that ring. Grabbing any connection (pressing and holding the left mouse button) moves the entire set of rings. There are several limitations on the individual node movements (minimum radius for the inner border of the smallest ring, minimum distance between rings, minimum width of the ring, etc.), which affect the MoveContourPoint()
There is a moveable panel on the form with several controls that allow you to add or delete rings, change the number of sectors in any ring, and update the values for any ring.
method is relatively simple (see Listing 1
), but a bit longer than any previous code example; however, these rules should help you to understand the code:
- Nodes are calculated beginning from the inner ring.
- Each new pair of nodes represents the line on the border of two sectors.
- All even nodes are on the inner borders of the rings; odd nodes are on the outer borders.
Moving the whole object (the Move()
method) involves moving the centers of all the rings and the title:
public override void Move
(int cx, int cy)
Size size = new Size (cx, cy);
ptCenter += size;
foreach (DoughnutRing ring in rings)
ring .Center = ptCenter;
title .Move (cx, cy);
method for the DoughnutSet class is more complicated; forward movement (movement along its radius) of any node must take into consideration all the previously mentioned limitations. Forward movement changes the width of the ring. The code for forward movement is too long to include here, but here's a brief overview. To move any individual node you must:
- Calculate three generic Lists containing: the number of sectors in each DoughnutRing, their inner radii, and outer radii.
- Check which ring and which side of the ring the node belongs to.
- Calculate the new radius according to the mouse movement.
- Check if the node can really move to this radius according to all the limitations (there is a special situation for the inner ring and another for the outer border of the biggest ring.
The code for rotation is much shorter, because all the rings work under the same rule. First, calculate the angle of the current mouse position (first in radians, and then transfer it into degrees). If this angle differs from the starting degree of the corresponding sector, it changes the starting angle of the ring and you must redefine the whole contour:
else if (catcher == MouseButtons.Right)
double angleNew_Radian = -Math .Atan2
(ptM .Y - ptCenter .Y,
ptM .X - ptCenter .X);
double angleNew_Deg =
180 / Math .PI;
if (Math .Abs (angleNew_Deg -
fStart_Deg[iSectorInRing]) > 1.0)
int nAddAngle = Convert.ToInt32
(angleNew_Deg - fStart_Deg[iSectorInRing]);
rings [jRing].StartingAngle =
rings [jRing] .StartingAngle,
For rotation, it's more accurate to base calculations on the mouse position rather than shifts; the code uses the mouse position ptM
; however, the longer part of the same method that controls forward movement bases the calculations on shifts in position (cx
That's all the code you need to write, even for this complicated object. All this complexity is handled in the DefineContour()
and two movement methods:
private void OnMouseDown
(object sender, MouseEventArgs mea)
private void OnMouseUp (object sender,
Movers .ReleaseMover ();
private void OnMouseMove (object sender,
Pay extra attention to the OnMouseDown()
method. Because you are working with both types of movements, you have to pass the pressed button as a second parameter to Movers.CatchMover()