Click here to Skip to main content
Click here to Skip to main content

Tagged as

Go to top

What can be simpler than graphical primitives? Part 3

, 8 Apr 2013
Rate this:
Please Sign up or sign in to vote.

Introduction

When an application is designed in a standard way on the basis of fixed elements, then users can look at such program only through the eyes of its developer and go only in his steps. When all the screen elements are movable and resizable, then an application turns into a very powerful instrument. Users can do all the things that were coded by the developer but users can do a lot more and each user can do all these things in the way he personally want them to be done.  Users can work with such application differently from the developer’s ideas; users can deal with the same task differently from each other, and even the same user can change at any moment the view of an application and work with it in many different ways.  Don’t forget that I am talking about already finished application which was distributed among the users. All those mentioned variations do not need any developer’s participation in the process of further changing; this is simply the main idea and the power of user-driven applications. To get all these possibilities, only one small change in the programs is needed: all the screen elements must be easily movable and resizable BY USER at any moment while an application is working!  Making any element movable and resizable becomes the basis of design.

You are looking at the third and also the last part of this article in which I continue to demonstrate and explain how some graphical primitives can be turned into movable and resizable. The set of examples for this article is only a small subset of examples from the book World of Movable Objects. Visually the examples accompanying this part of the article can look identical to what you see in the book but all these examples (with one exception) are significantly changed. The reason that there is one exception is simple: after all my attempts I have found out that this example was initially designed in such a way that I can’t improve it any more.

The main changes of the examples demonstrated in this part of the article are made in the covers of the elements and, as the result of such changes, some methods of their classes are changed. These are mostly the methods that are called at the starting moment of resizing. Nearly all the elements demonstrated in this part have curved borders so originally they were designed with the N-node covers while their new covers contain significantly lesser number of nodes. Also I began to use more often the technique of adhered mouse and you will see it in all further examples. I would recommend to compare in each case the new version of the cover with the old one which can be seen in similar class from the book.  The book with its accompanying project and several other very helpful documents and projects can be downloaded from the http://sourceforge.net/projects/movegraph/files/?source=directory  Such comparison of covers demonstrates not only the evolution of my views on cover design but also underlines that in each case there can be variants in design. From the users’ points of view, there is no difference in behaviour of elements with the old or the new version of cover so for users these improvements on developer’s kitchen do not matter at all. But I write this article for developers and any programmer may prefer to use one version or another. Throughout all years of my work I especially liked the situations when there were more than one possible solution; this gives developers the freedom of creation.

Some of the examples from this part can remind you the examples demonstrated in the previous parts of the article. Though I have divided the whole work into three parts, it is a single article and the division was made for the easiness of publication and the clarity of explanation.

The N-node covers are first demonstrated in the book with three types of elements: circles, rings, and rounded strips. In the second part of this article I explained the N-node covers for circles and rings and also demonstrated variants of significantly simplified covers for these elements. This simplification was based on a switch from the N-node covers to the ordinary covers consisting of only few nodes. Now let us do the same with the rounded strips. The variant of the rounded strip with the N-node cover can be found in the book; here is the new simplified version.

Rounded strips

  • File: Form_RoundedStrips.cs
  • Menu position: Medley – Rounded strips

Any object of the RoundedStrip class (figure 1) is defined by two points (ptC0 and ptC1) – centers of the circles – and the radius of those circles (m_radius).  There exists the minimal allowed distance between the centers of two circles –minLength. The distance between two centers is equal to the length of the straight part of the border so this restriction guarantees the existence of a straight part of border between two semicircles and thus prevents from squeezing a rounded strip into a single circle. There is also a limit on the minimal allowed radius of the circles (minRadius) so that no rounded strip can be turned into a line. Visually two rounded ends of the strip are indistinguishable from each other and for users there is no difference between any rounded strip and the same strip rotated for 180 degrees (rotation goes around the central point of an object), but dealing with such object at any moment requires the knowledge of the angle (m_angle) and it is always calculated as an angle of the line from ptC0 to ptC1.

public class RoundedStrip : GraphicalObject
{
    PointF ptC0, ptC1;          // centers of semicircles
    double m_angle;             // from ptC0 to ptC1
    float m_radius;
    SolidBrush m_brush;
    static int minRadius = 12;
    static int minLength = 20;    // straight part = distance (ptC0, ptC1)
    int delta = 3;

Text Box:

For forward moving and rotation of the rounded strips it would be enough to have a cover consisting of a single node as rounded strip is one of the shapes of the nodes. To organize the resizing of a rounded strip by any border point some nodes must be added to the cover. Straight and curved parts of the border are used to start different types of resizing.

The curved parts are used to change the length of a strip and when any point of the curved border is pressed then the mouse movement is allowed only along the main axis which goes through the centers of the circles. Such movement changes the distance between these two centers but it does not affect the width of the strip. When you try to move the straight part of the border, it moves only to or from the opposite side and the width of the strip is changed. Such movement does not affect the length of the straight part but the curves have to connect opposite straight borders so with the change of the distance between two straight borders the radius of the curves must also change and, as the result, the total length of the strip also changes.

In the second part of this article we used the circles with simple covers – the Circle_SimpleCover class. Cover for this class consists of two nodes, so we are familiar with the system of two circular coaxial nodes to organize the circle’s resizing. In case of a rounded strip, we have semicircles and exactly the same idea of resizing is used for such strips. There are five nodes in the cover of the RoundedStrip objects. The nodes overlap so all the needed movements are provided by organizing the right order of nodes in the cover. In the code below the special points of the border – the end points of the straight parts – are calculated by the RoundedStrip.CornerPoints() method.

// order of nodes:
//                  [0, 1]    strips on the straight sides
//                  [2]       inner big strip
//                  [3, 4]    circles
//
public override void DefineCover ()
{
    PointF [] pts = CornerPoints ();
    CoverNode [] nodes = new CoverNode [] {
          new CoverNode (0, pts [0], pts [1], delta), 
          new CoverNode (1, pts [2], pts [3], delta), 
          new CoverNode (2, ptC0, ptC1, m_radius - delta, Cursors .SizeAll), 
          new CoverNode (3, ptC0, m_radius + delta), 
          new CoverNode (4, ptC1, m_radius + delta) };
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

Four nodes of five in this cover provide the resizing. Though two of these nodes are big enough, they are mostly covered by another preceding node so only their narrow part along the border is visible (available) to the mover. Thus, along the whole length of border we receive a narrow strip that can be used for resizing; this narrow strip is combined of four nodes  On pressing any of these nodes, the cursor is placed exactly on the nearest point of the border line and the straight segment for further movement of the cursor is determined. One end of this segment is calculated on the base of the minimal size limits while the other end is declared to be somewhere far away.

public void StartResizing (Point ptMouse, int iNode)
{
    cornersBeforeResizing = CornerPoints ();
    PointF ptBase, ptCursor;
    double dist;
    switch (iNode)
    {
        case 0:     // straight border
            Auxi_Geometry .Distance_PointLine (ptMouse,
               cornersBeforeResizing[2], cornersBeforeResizing[3], out ptBase);
            angleBeam = m_angle + Math .PI / 2;
            ptInnerLimit = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
                                                        2 * minRadius);
            ptOuterLimit =Auxi_Geometry.PointToPoint (ptBase, angleBeam, 4000);
            ptCursor = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
                                                    2 * m_radius);
            break;
        … …
        case 4:     // semicircular border around the ptC1
            ptCursor = Auxi_Geometry .PointToPoint (ptC1,
                         Auxi_Geometry .Line_Angle (ptC1, ptMouse), m_radius);
            dist = Auxi_Geometry .Distance_PointLine (ptCursor,
               cornersBeforeResizing[1], cornersBeforeResizing[2], out ptBase);
            additionToLength = dist - Auxi_Geometry .Distance (ptC0, ptC1);
            angleBeam = m_angle;
            ptInnerLimit = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
                                             MinimumLength + additionToLength);
            ptOuterLimit =Auxi_Geometry.PointToPoint (ptBase, angleBeam, 4000);
            break;
    }
    Cursor .Position = form .PointToScreen (Point .Round (ptCursor));
}

Until the moment of the mouse release, the cursor can be moved only along the determined straight segment between two points (ptInnerLimit and ptOuterLimit) and the current mouse position at any moment is considered as the new position of the caught border. It is a classical example of using the technique of adhered mouse.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    bool bRet = false;
    double dist;
    if (catcher == MouseButtons .Left)
    {
        if (i != 2)
        {
            PointF ptBase, ptOnBeam;
            PointOfSegment typeOfNearest;
            Auxi_Geometry .Distance_PointSegment (ptM, ptInnerLimit,
                 ptOuterLimit, out ptBase, out typeOfNearest, out ptOnBeam);
            if (i == 0)
            {
                dist = Auxi_Geometry .Distance_PointLine (ptOnBeam,
                        cornersBeforeResizing [2], cornersBeforeResizing [3]);
                ptC0 = Auxi_Geometry .PointToPoint (cornersBeforeResizing [2],
                                            m_angle + Math .PI / 2, dist / 2);
                ptC1 = Auxi_Geometry .PointToPoint (cornersBeforeResizing [3],
                                            m_angle + Math .PI / 2, dist / 2);
                Radius = Convert .ToSingle (dist / 2);
            }
            … …
            else if (i == 4)
            {
                dist = Auxi_Geometry .Distance_PointLine (ptOnBeam,
                        cornersBeforeResizing [1], cornersBeforeResizing [2]);
                  ptC1 = Auxi_Geometry .PointToPoint (ptC0, m_angle,
                                                    dist - additionToLength);
                DefineCover ();
            }
            Cursor .Position = form .PointToScreen (Point .Round (ptOnBeam));
            bRet = true;
        … …

Arcs

Arc is going to be the next demonstrated graphical primitive and I want to show not one but two examples with the arcs because the covers are slightly different for thin and wide objects of such type. There is no strict definition for thin and wide arcs so I use my own simple rule to divide them: thin arcs are painted with a pen while for wide arcs some brush is used. Certainly, it is a very artificial division and you might prefer something different. Anyway, I want to demonstrate two possible variants and you make the decision about the preferable cover for similar objects in your programs.

  • File: Form_Arcs_Thin.cs
  • Menu position: Medley – Thin arcs

From the point of geometry, any thin arc is determined by four parameters: central point (m_center), radius of the circle (m_radius), angle to one end point (angleStart), and the sweep angle from this end point to another (angleSweep). Angles are used in nearly all the examples of this part and I want to remind that in all my programs the angles are used in the standard math way so positive angles are going counterclockwise.

public class Arc_Thin : GraphicalObject
{
    PointF m_center;
    float m_radius;
    double angleStart;
    double angleSweep;
    Pen m_pen;
    double minSweep_Degree = 10;
    double maxSweep_Degree = 350;
    static float minRadius = 20;

There is also a pen for drawing an arc (m_pen) and three restrictions on the sizes; these restrictions are determined by the set of allowed changes. The sweep angle of an arc can be changed by moving the end points. There are two restrictions on the sweep angle; one of them prevents from turning an arc into a point and there is a minimal sweep angle (minSweep_Degree = 10) while another does not allow to transform an arc into a closed circle and there is maximum sweep angle (maxSweep_Degree = 350). It is a standard practice to provide the forward movement and rotation of an object by any inner point and both movements are organized for the arcs according to this rule.

The only problematic thing was the change of the radius. There is no problem with the change itself, but from my point of view there is no obvious place to start such movement. At last I decided to start such resizing at the small area around the middle point of the arc, but there is still the problem of informing users about such possibility. In the similar example from the book you can see that this small area is painted with the wider pen; such change of the width gives a very clear signal that there is something special with this area. Another possibility is to use different color, for example, similar pen but darker. This might be a good solution when you have an arc of several pixels width, but if you use a thin pen of only one or two pixels then the short stretch of darker color does not attract any attention. In any case the mouse cursor over this special area of an arc is different from the cursor over neighboring areas and this change can inform users, but this difference is seen only when the cursor is moved to this area. There is no other signal about the special area around the middle point;

Text Box:  
Fig.2  Thin arcs

Maybe you can think out something else to visualize this small area of resizing.

Forward moving of an arc is provided by a pair of coaxial circular nodes with a small (several pixels) difference between their radii. It is like a pair of circular nodes used for the inner border of the rings demonstrated in the second part of this article. Because an arc includes only some part of the circular border, then the remaining part – the gap – must be cut out from the circular node used for moving. This technique was explained in the second part of the article: to cut out some part of the ordinary nodes, other node(s) with the Behaviour.Transparent parameter must be included into the cover. Figure 2 shows that, depending on the angle of the gap, this requires either one or two transparent nodes. When the gap angle is less than 180 degrees, a single transparent convex polygon is used; for bigger gaps there are two transparent rectangles.  

It is easier to work with the covers in which the number of nodes is determined at the moment of initialization, does not depend on the sizes or any other transformation of an object, and thus does not change throughout the life of an object. It is easier because from the very beginning the reaction on pressing one or another node depends only on the number of the node. From my point of view, the use of either one or two nodes for cutting the gap in the arc is a minor inconvenience. This inconvenience is so small that I didn’t think about avoiding it. Yet, there is an easy solution for using always two nodes regardless of the sweep angle; you will see this solution in one of the further examples – in the covers for pie slices which are similar to arcs in design and use.

Here is the code for the Arc_Thin.DefineCover() method. In order to shorten the code here in the text of an article, I skip the calculations of two transparent nodes for the case of the wide gap.

// order of nodes:
// [0] at one end (on angleStart)
// [1] at another end
// [2] in the middle of the arc
// [3] transparent; to cut out the inner circle
// [4] or [4,5] transparent; to cut out the gap
// last one     outer circle to cover the whole arc
//
public override void DefineCover ()
{
    float rOuter = m_radius + 3;
    float rInner = m_radius - 3;
    int nNodes = (Math .Abs (angleSweep) <= Math .PI) ? 7 : 6;
    CoverNode [] nodes = new CoverNode [nNodes];
    nodes [0] = new CoverNode (0, StartPoint, nrSmall);
    nodes [1] = new CoverNode (1, AnotherEnd, nrSmall);
    nodes [2] = new CoverNode (2, MiddlePoint, nrForMiddle);
    nodes [3] = new CoverNode (3, m_center, rInner, Behaviour .Transparent);
    if (Math .Abs (angleSweep) <= Math .PI)
    {
        //nNodes = 7;
        … …
        nodes [4] = new CoverNode (4, ptsA, Behaviour .Transparent);
        nodes [5] = new CoverNode (5, ptsB, Behaviour .Transparent);
    }
    else
    {
        //nNodes = 6;
        double sweepGap;
        if (angleSweep > 0)
        {
            sweepGap = angleSweep - 2 * Math .PI;
        }
        else
        {
            sweepGap = angleSweep + 2 * Math .PI;
        }
        double dist = rOuter * Math .Sqrt (2);
        PointF [] pts = new PointF [] { m_center, 
               Auxi_Geometry .PointToPoint (m_center, angleStart, dist), 
               Auxi_Geometry .PointToPoint (m_center,
                                            angleStart + sweepGap / 2, dist), 
               Auxi_Geometry .PointToPoint (m_center, angleStart + sweepGap,
                                            dist) };
        nodes [4] = new CoverNode (4, pts, Behaviour .Transparent);
    }
    nodes [nNodes - 1] = new CoverNode (nNodes - 1, m_center, rOuter,
                                        Cursors .SizeAll);
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

The resizing is started when any of the first three nodes is pressed. At this moment some calculations and preliminary movement of the cursor must be done, so at this moment the Arc_Thin.StartResizing() method is called. In each of these three cases the cursor is switched to the central point of the pressed node. From this moment and until the mouse release, the changing mouse position is used as the new location of the associated arc point. When the middle point of an arc is pressed (iCaughtNode == 2), then the pressed mouse is allowed to move only along the radial line; until the moment of release the allowed path is shown with an auxiliary thin line. While the radius of an arc is changed, angles to both end points do not change so these points also move along their radial lines. These two radial lines are also painted with a thin auxiliary pen. All three auxiliary lines are painted during the process of resizing and make the change of an arc much more obvious. When one of the end points is pressed, then the allowed path for the mouse is the circle. During the movement of an end point, part of the circle over the gap is also painted with an auxiliary line.

The part of the Arc_Thin.StartResizing() method for the case when the node over the middle point of an arc is pressed is really simple and short.

public void StartResizing (Point pt, int iCaughtNode)
{
    PointF ptCursor = MiddlePoint;  // anything
    … …
    else        // iCaughtNode == 2
    {
        ptCursor = MiddlePoint;
        double angleMiddle = angleStart + angleSweep / 2;
        ptInnerLimit = Auxi_Geometry .PointToPoint (m_center, angleMiddle,
                                                    minRadius);
        ptOuterLimit =Auxi_Geometry.PointToPoint (m_center, angleMiddle, 4000);
    }
    Cursor .Position = form .PointToScreen (Point .Round (ptCursor));
}

For the cases of the pressed node over one or another end point, the code is much longer, so I don’t want to include it here, but I want to give some explanation. (You can find similar explanation, maybe even more detailed, in the book and in one of my previous articles The Roads we Take which was published in February). There is a special problem of allowed (or restricted) movement along the circular line but the existence of this problem depends on the maximum allowed sweep angle. When the maximum allowed sweep angle is not big then the movable point – in our case it is the end point of an arc – can be easily kept within the allowed range. When the maximum allowed arc is very close to the full circle, then you can move the cursor fast enough to jump over the narrow gap. Calculated angle to the mouse signals that the cursor is inside the allowed range but it gives no indication that it happened as the result of closing the circle which is definitely wrong. To avoid such mistakes, the comparison of the current angle to the mouse with the allowed range is not enough; there must be some more sophisticated check. This check can be organized in different ways; here is my solution. I calculate the ranges for the first and the last quarter of the longest allowed arc and the direct jump from one of these quarters to another is prohibited.

  • File: Form_Arcs_Wide.cs
  • Menu position: Medley – Wide arcs

Text Box:  
Fig.3  Wide arcs

Wide arc (figure 3) looks like a ring with the cut out sector or several sectors if the sweep angle of the arc is small.

Because such arc reminds a ring then, to move its outer and inner borders, I use the same system of four circular nodes as was introduced with the rings in the previous part of this article. To cut out the needed part of the ring in order to transform it into an arc, I use the same one or two transparent nodes that were demonstrated in the previous example with the thin arcs. But this is not all that reminds one or another of the previous examples. I need something to organize the change of the sweep angle and for it I use the same strip nodes on the borders as were shown on the movable partitions of circles and rings in the previous part of this article.  Now all the needed parts (nodes) of the cover are declared and the main thing is to include them into the cover in correct order.

// order (and number) of nodes:
// [0]  strip   at one end (on angleStart)
// [1]  strip   at another end
// [2,3] or [2] transparent; to cut out the gap
// 1    circle  transparent; to cut out the inner circle
// 1    circle  to move the inner border
// 1    circle  to move the object
// 1    circle  to move the outer border
//
public override void DefineCover ()
{
    int jInside;
    int nNodes = (Math .Abs (angleSweep) <= Math .PI) ? 8 : 7;
    CoverNode [] nodes = new CoverNode [nNodes];
    nodes [0] = new CoverNode (0,
           Auxi_Geometry .PointToPoint (m_center, StartAngle, rOuter),
           Auxi_Geometry .PointToPoint (m_center, StartAngle, rInner), rStrip);
    nodes [1] = new CoverNode (1,
      Auxi_Geometry .PointToPoint (m_center, AnotherEndAngle, rOuter),
      Auxi_Geometry .PointToPoint (m_center, AnotherEndAngle, rInner), rStrip);
    if (Math .Abs (angleSweep) <= Math .PI)
    {
        // nNodes = 8;
        PointF ptA, ptB;
        double angleA, angleB;
        if (angleSweep >= 0)
        {
            angleB = angleStart;
            angleA = angleStart + angleSweep;
        }
        else
        {
            angleA = angleStart;
            angleB = angleStart + angleSweep;
        }
        ptA = Auxi_Geometry .PointToPoint (m_center, angleA, rOuter);
        ptB = Auxi_Geometry .PointToPoint (m_center, angleB, rOuter);
        PointF [] ptsA = new PointF [] {ptA, 
            Auxi_Geometry .PointToPoint (ptA, angleA + Math .PI / 2, rOuter), 
            Auxi_Geometry .PointToPoint (m_center, angleA + 3 * Math .PI / 4,
                                         rOuter * Math .Sqrt (2)), 
            Auxi_Geometry.PointToPoint (m_center, angleA - Math .PI, rOuter) };
        PointF [] ptsB = new PointF [] {ptB, 
             Auxi_Geometry .PointToPoint (m_center, angleB - Math .PI, rOuter), 
             Auxi_Geometry .PointToPoint (m_center, angleB - 3 * Math .PI / 4,
                                          rOuter * Math .Sqrt (2)), 
             Auxi_Geometry.PointToPoint (ptB, angleB - Math .PI / 2, rOuter) };
        nodes [2] = new CoverNode (3, ptsA, Behaviour .Transparent);
        nodes [3] = new CoverNode (4, ptsB, Behaviour .Transparent);
        jInside = 4;
    }
    else
    {
        // nNodes = 7;
        double sweepGap = (angleSweep > 0) ? (angleSweep - 2 * Math .PI)
                                           : (angleSweep + 2 * Math .PI);
        double dist = rOuter * Math .Sqrt (2);
        PointF [] pts = new PointF [] { m_center, 
            Auxi_Geometry .PointToPoint (m_center, angleStart, dist), 
            Auxi_Geometry .PointToPoint (m_center, angleStart + sweepGap / 2,
                                           dist), 
            Auxi_Geometry.PointToPoint (m_center, angleStart+sweepGap, dist) };
        nodes [2] = new CoverNode (4, pts, Behaviour .Transparent);
        jInside = 3;
    }
    nodes [jInside]     = new CoverNode (jInside, m_center, rInner - delta,
                                                  Behaviour .Transparent); 
    nodes [jInside + 1] = new CoverNode (jInside +1, m_center, rInner + delta); 
    nodes [jInside + 2] = new CoverNode (jInside + 2, m_center, rOuter - delta,
                                                      Cursors .SizeAll); 
    nodes [jInside + 3] = new CoverNode (jInside +3, m_center, rOuter + delta);
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

Movable outer and inner borders allow to change the width of the arc. By moving one border and then another it is possible to change radii and at the same time keep the same width of the arc. Is it possible to get the same result in some other way?  Well, for some time I thought about adding one more node exactly to fulfill this task. This would be a solution similar to the thin arcs only instead of the circular node on the middle point I would place a strip node along the radial line going through this middle point; this strip node would stretch from the inner border of the arc to the outer border. After some considerations I dropped this idea though it is not a problem to add such node. If you do not highlight the area of this node in one way or another then its existence is not obvious for users and the whole use of some secret place looks very artificial.  If there is some tip about the existence of this special area then it is much better.  If you want, you can add such node, visualize it, for example, with a darker or lighter color, and see how it works. Maybe you will like it.

Crescent

  • File: Form_Crescent.cs
  • Menu position: Medley – Crescent

Crescent is not among the most often used graphical elements but from time to time it is needed. We also see it throughout our life; at least those who look from time to time at the night sky. Warning: though the resizing of the crescent demonstrated below looks simple, it cannot be used for changing of natural illumination in your backyard.

Text Box:

The design of a crescent is not a problem at all: take a circle and cut out the bigger part of it by another circle of a bigger radius; figure 4 demonstrates the details of such construction. Auxiliary lines on the figure connect two horns and the centers of two circles with those horns. Several of the previous examples already demonstrated the cutting out of some areas so the bigger circular node must be a transparent one and precede another circular node in the cover. Such simple cover of only two nodes will provide the forward movement and rotation of a crescent but what about resizing?

I think that two types of resizing are needed: change of the size (distance between the horns) and change of the width. These movements are easily understandable by users and for both of them there are obvious points where such resizing can be started. To change the size, you press one or another horn and its movement is allowed along the line connecting the horns. To change the width, you press the border point in the widest part of an object and then make the crescent either wider or narrower. These four special points are covered by four small circular nodes (see figure 4) and in such a way we get the cover consisting of six circles. By default any circular node has the cursor in the shape of a hand and this fits very well with the idea of the first five nodes. Only for the last node covering the whole crescent we need to change this default value so that different shape will signal the possibility of moving the whole object.

public override void DefineCover ()
{
    // order of nodes:  ptA - ptD - ptB - ptC –
    //                  big Transparent circle (inner radius) –
    //                  smaller normal cirle (outer radius)
    CoverNode [] nodes = new CoverNode [] {
             new CoverNode (0, ptA, 4), 
             new CoverNode (1, ptD, 4), 
             new CoverNode (2, ptB, 4), 
             new CoverNode (3, ptC, 4),
             new CoverNode (4, ptCenterInner, Convert .ToSingle (rInnerBorder),
                                              Behaviour .Transparent), 
             new CoverNode (5, ptCenterOuter, Convert .ToSingle (rOuterBorder),
                                              Cursors .SizeAll) };
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

All nodes of the cover and auxiliary lines that are shown at figure 4 can be seen in the real Form_Crescent.cs when you press the small button to show cover. Letters to mark the special points and the numbers of the nodes (in red color) were artificially added to this figure and are not shown in the working application.

I always try to construct the covers in such a way that the resizing can start at any border point; this rule was demonstrated in the previous examples. But you don’t see it in the case of the crescent and the reason is very simple: I don’t understand myself what kind of resizing has to start at other border points. It is very easy to use in case of crescent a classical N-node cover and put a set of small overlapping circular nodes along both inner and outer borders, but how am I going to change the sizes if one of those nodes is pressed?  What size of the crescent has to be changed and how this change must correlate with the mouse movement if the resizing starts at any other border point?  For those four small nodes that you see at figure 4 I have a clear understanding of further changes if one of those nodes is pressed. When I’ll have the same clear understanding of the needed resizing started at any other border point, I’ll change the cover. If you have such understanding, you can change the cover yourself.

Here are several remarks about the currently implemented resizing and the existing restrictions on such resizing. When any of the first four nodes is pressed, the Crescent.StartResizing() method is called.

private void OnMouseDown (object sender, MouseEventArgs e)
{
    ptMouse_Down = e .Location;
    if (mover .Catch (e .Location, e .Button))
    {
        if (mover .CaughtSource is Crescent)
        {
            if (e .Button == MouseButtons .Left && mover .CaughtNode < 4)
            {
                mover .MouseTraced = false;
                crescent .StartResizing (e .Location, mover .CaughtNode);
                 mover .MouseTraced = true;
            }
            … …

At this moment the mouse cursor is moved to the central point of the pressed node, which means placing of the mouse exactly on the border. Then two boundary points (ptInner and ptOuter) for the mouse movement are calculated and until the moment of release the mouse cursor can move only along the straight line between these two points. Until the release of the mouse, its position is used as the location of the associated border point, so it is a classical use of the adhered mouse.

  • Two restrictions determine the range for moving of the point A (node number zero). By moving point A to the left (in case of figure 4), the crescent becomes wider but it must be always the crescent, so point A must be stopped somewhere before the line connecting two horns. This limit is defined by the minimum allowed overhanging of horns over point A – minHangWidth. By moving point A to the right, the crescent becomes narrower. At any moment there must be some space between the opposite nodes on points A and D because this area in the middle allows to move the whole crescent around the screen. There is a limit on minimal allowed width – minWidth – which is slightly bigger than the diameter of those small nodes.
  • public void StartResizing (Point ptMouse, int iNode)
    {
        ptHorns_base = Auxi_Geometry .PointToPoint (ptCenterInner, angle,
                                                    rInnerBorder - wHang);
        h = Auxi_Geometry .Distance (ptB, ptHorns_base);
        switch (iNode)
        {
            case 0:
            default:
                ptInner = Auxi_Geometry .PointToPoint (ptCenterInner, angle, 
                    Auxi_Geometry .Distance (ptCenterInner, ptHorns_base) +
                                                                minHangWidth);
                ptOuter = Auxi_Geometry .PointToPoint (ptCenterOuter, angle,
                                                   rOuterBorder - minWidth);
                break;
            … …
  • For the movement of point D (node number one) one limiting point of its movement is determined by the same minimal allowed width of crescent. Another end of the possible movement is determined by the maximum sweep angle of the outer border as it cannot be greater than 180 degrees.
  • public void StartResizing (Point ptMouse, int iNode)
    {
        … …
            case 1:
                Cursor .Position = form .PointToScreen (Point .Round (ptD));
                ptInner = Auxi_Geometry .PointToPoint (ptCenterInner, angle,
                                                   rInnerBorder + minWidth);
                ptOuter = Auxi_Geometry .PointToPoint (ptHorns_base, angle, h);
                break;
            … …
  • When you try to decrease the distance between two horns (point B with node number two and point C with node three) radii of both big circular nodes decrease and the minimal allowed distance is determined by the sweep angle of the outer border. There is no real limit in moving the horns in the opposite direction (away from each other) and it is calculated simply as being far away, much farther than you can move any horn.
  • public void StartResizing (Point ptMouse, int iNode)
    {
        … …
            case 2:
                Cursor .Position = form .PointToScreen (Point .Round (ptB));
                ptInner = Auxi_Geometry .PointToPoint (ptHorns_base,
                                      angle + Math .PI / 2, wHang + width); 
                ptOuter = Auxi_Geometry .PointToPoint (ptHorns_base,
                                                  angle + Math .PI / 2, 4000);
                break;
            case 3:
                Cursor .Position = form .PointToScreen (Point .Round (ptC));
                ptInner = Auxi_Geometry .PointToPoint (ptHorns_base,
                                      angle - Math .PI / 2, wHang + width);
                ptOuter = Auxi_Geometry .PointToPoint (ptHorns_base,
                                                  angle - Math .PI / 2, 4000);
                break;
        }
    }

Circles or their parts are often used in the programs and those elements can be used in different ways, so the next two examples deal with circles or their parts.

Sectors of circles

  • File: Form_CircleSector.cs
  • Menu position: Medley – Sector of a circle

I am sure that these sectors from figure 5 and their covers will remind you the wide arcs from one of the previous examples.

Text Box:  
Fig.5  Sectors of circles

Yes, there are similarities in view and cover design, but the allowed range for the sweep angle of these sectors is different and this difference causes some interesting changes in code. The sweep angle of these sectors can be changed between zero and 180 degrees and this is not my whim: indicators with an arrow moving inside the [0, 180] degree range are widely used in real devices and a lot of applications try to use similar screen objects to show the values of one or another parameter. In the real devices, there are always some marks and numbers around the semicircle; these additional marks are very helpful in getting the precise information from the moving arrow. It is not a problem at all to add such additional information around these sectors, but then everything will turn into complex object. I demonstrate a lot of complex objects in the book but I don’t want to turn these simple elements into complex objects; let us keep them really simple and deal only with the movements of these sectors.

public class CircleSector : GraphicalObject
{
    PointF m_center;
    float m_radius;
    double angleStart;
    double angleSweep;
    SolidBrush m_brush;
    bool bFixedStartingSide = false;
    Pen penFixedBorder;

The CircleSector class contains four standard fields to describe an object of such shape: central point (m_center), radius (m_radius), angle of one side (angleStart), and the sweep angle from this side to another (angleSweep). There is also a standard field for visualizing an object (m_brush), but in addition there are two fields which were not used in the previous similar classes.

In the real indicators the position of some value is fixed (usually it is zero value) and the moving arrow goes away from this fixed side and then returns back. In my simulation of the real devices there is a possibility to fix one side – the side of the angleStart – by setting the value of the bFixedStartingSide field. Another additional field – penFixedBorder – is used for visualizing this fixed side.

I deliberately made this pen wide (now it is three pixels wide, but you can change the width) because the use of this wide pen helps to solve one problem which is specific for this example. The sweep angle of any CircleSector object can be diminished to zero in which case this sector simply disappears from view. It is somewhere there on the screen but it is invisible. Not good situation at all, but if you declare the possibility of zero sweep angle then you have to think about such special situation. In the current example the wide line on the starting side is shown when the bFixedStartingSide value is set to true and this is regardless of the sweep angle. Thus, the sector can disappear, but the wide line still shows its location. If you don’t want to fix the starting side, you can switch ON the covers and the cover will show you the place of otherwise invisible sector. Then you can press the node, move one side from another, and in this way make the sector visible again. In the real application where the covers are never shown, you will have to think about something else to show such indicator with zero sweep angle.

// order of nodes:      [0]         strip on angleStart + angleSweep
//                      [1]         strip on angleStart (!)
//                      [2, 3]      Transparent rectangles along the sides
//                      [4]         smaller circle
//                      [5]         bigger circle
//
public override void DefineCover ()
{
    double angleA, angleB;
    if (angleSweep >= 0) 
    {
        angleA = angleStart + angleSweep;
        angleB = angleStart;
    }
    else
    {
        angleA = angleStart;
        angleB = angleStart + angleSweep;
    }
    double radPlus = m_radius + delta;
    PointF ptA = Auxi_Geometry .PointToPoint (m_center, angleA, radPlus);
    PointF ptB = Auxi_Geometry .PointToPoint (m_center, angleB, radPlus);
    PointF [] ptsA = new PointF [] {ptA, 
         Auxi_Geometry .PointToPoint (ptA, angleA + Math .PI / 2, radPlus), 
         Auxi_Geometry .PointToPoint (m_center, angleA + 3 * Math .PI / 4,
                                      radPlus * Math .Sqrt (2)), 
         Auxi_Geometry .PointToPoint (m_center, angleA - Math .PI, radPlus) };
    PointF [] ptsB = new PointF [] {ptB, 
         Auxi_Geometry .PointToPoint (m_center, angleB - Math .PI, radPlus), 
         Auxi_Geometry .PointToPoint (m_center, angleB - 3 * Math .PI / 4,
                                      radPlus * Math .Sqrt (2)), 
         Auxi_Geometry .PointToPoint (ptB, angleB - Math .PI / 2, radPlus) };
    CoverNode [] nodes = new CoverNode [6];
    nodes [0] = new CoverNode (0, m_center,
                 Auxi_Geometry.PointToPoint (m_center, angleStart + angleSweep,
                                             m_radius));
    nodes [1] = new CoverNode (1, m_center,
                 Auxi_Geometry.PointToPoint (m_center, angleStart, m_radius));
    nodes [2] = new CoverNode (2, ptsA, Behaviour .Transparent);
    nodes [3] = new CoverNode (3, ptsB, Behaviour .Transparent);
    nodes [4] = new CoverNode (4, m_center, m_radius - delta, Cursors.SizeAll); 
    nodes [5] = new CoverNode (5, m_center, m_radius + delta);
    if (bFixedStartingSide)
    {
        nodes [1] .Behaviour = Behaviour .Frozen;
      }
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

The cover for the CircleSector class is similar to the cover of the Arc_Wide class for the case of sweep angles not bigger than 180 degrees, but there is one change to which I want to attract your attention. This difference is in the placement of nodes zero and one. When an object is described by the angle of one side (angleStart) and the sweep angle from this side to another (angleSweep), then it is natural to place the first node of the cover – the node with zero number – on the starting side and this is done, for example, in the Arc_Wide class. In the Circle Sector class the reversed order of these two nodes is used and this is not by mistake but it was purposely done. In the Arc_Wide class I could do either way and both of them would work fine. In the CircleSector class the demonstrated order of nodes on the sides is the only one to be correct. Why?

In the CircleSector class the minimal allowed sweep angle is zero in which case two nodes on the sides take the same position. Mover analyses the nodes of any cover exactly in the same order in which they are included into the cover. When you fix one side of the CircleSector object then the side of the angleStart is fixed while another side is movable. The node of this movable side must be the first one in the cover; otherwise you will move this side on position of another, release it, and after it there will be no chances to increase this sector again because the unmovable node will cover the movable one. (If you want to design similar object in which any side can be fixed, then you need to change the code in such a way that the first node is always on movable side.)

When I want to fix one side of the sector, I change the behaviour of its node to Behaviour.Frozen. In the second part of this article I described all the values of the Behaviour enumeration. The Frozen value is used not too often but in some cases, like this one, it is very useful.

if (bFixedStartingSide)
{
    nodes [1] .Behaviour = Behaviour .Frozen;
}

While organizing any movement dealing with the angles, you have to deal with the problem which is caused by the origin of the angles and their calculations. For calculation of linear distance between two points, there is no ambiguity and the distance is either zero or positive. When an object is moved around the circle the solution is ambiguous and often depends on the starting angle and allowed movements. If the movement starts at 20 degrees and is over at 30 degrees then, with the high probability, an object was moved for 10 degrees. If the movement starts at 170 degrees and finishes at -170 degrees then what was the sweep angle of such movement?  It is easier if you are interested only in the current angle of an object or mouse cursor but what if you have to paint the sector between two positions?  This problem is similar to the problem of jumping over the gap of an arc which I mentioned in one of the previous examples.

With intervals of one or two years I try again and again to solve such problems by some calculations and comparison of angles only and each time I have a very complicated and not always correct code. For the CircleSector class I do not use the comparison of angles but check the relative positions of two points and one line. This is much easier and works without mistakes.

Suppose that you press one side of the sector in order to change its sweep angle. Another side will not change its position throughout such movement and its end point is one point of our base line (ptSeg_0). Maximum allowed sweep angle is 180 degrees and the end point of the movable side in such position gives another point of the base line (ptSeg_1).  Thus, the line through these two points is the boundary of the semicircle through which the movable side can go and all the possible end points of the movable side lie on one side of this line or on the line itself. When the sweep angle is equal 90 degrees, then the angle to the end point of the moving side is angleToIn and the point itself is ptIn.  The calculation of both values is done by the CircleSector.StartResizing() method which is called at the initial moment of resizing. I’ll remind once more that node number five allows to change the radius of the sector and just now we are interested in the remaining part of the method.

public void StartResizing (Point pt, int iNode)
{
    if (iNode == 5)
    {
        double angleBeam = Auxi_Geometry .Line_Angle (m_center, pt);
        PointF ptCursor =
                  Auxi_Geometry .PointToPoint (m_center, angleBeam, m_radius);
        ptInnerLimit =
                  Auxi_Geometry .PointToPoint (m_center, angleBeam, minRadius);
        ptOuterLimit = Auxi_Geometry .PointToPoint (m_center, angleBeam, 4000);
        Cursor .Position = form .PointToScreen (Point .Round (ptCursor));
    }
    else
    {
        angleStart = Auxi_Common .LimitedRadian (angleStart);
        if (iNode == 0)         // end line
        {
            ptSeg_0 =
                  Auxi_Geometry .PointToPoint (m_center, angleStart, m_radius);
            ptSeg_1 = Auxi_Geometry .PointToPoint (m_center,
                                              angleStart + Math .PI, m_radius);
            angleToIn = (angleInit >= 0) ? (angleStart + Math .PI / 2)
                                         : (angleStart - Math .PI / 2);
        }
        else         // start line
        {
            angleEnd = Auxi_Common .LimitedRadian (angleStart + angleSweep);
            ptSeg_0 =
                    Auxi_Geometry .PointToPoint (m_center, angleEnd, m_radius);
            ptSeg_1 = Auxi_Geometry .PointToPoint (m_center,
                                                angleEnd + Math .PI, m_radius);
            angleToIn = (angleInit >= 0) ? (angleEnd - Math .PI / 2)
                                         : (angleEnd + Math .PI / 2);
        }
        ptIn = Auxi_Geometry .PointToPoint (m_center, angleToIn, m_radius);
    }
}

Checking of the possible movement is done in the CircleSector.MoveNode() method. The proposed angle of the movable side is calculated from the mouse position (ptM), but the movable side can take the proposed angle only if two points – previously calculated point (ptIn) and the current mouse location (ptM) – are not on the opposite sides of the line going through the points ptSeg_0 and ptSeg_1.  Position of the mouse exactly on this line is allowed as the sweep angle of zero degree or 180 degrees are allowed. The Auxi_Geometry.OppositeSIdeOfLine() method analyses the positions of points ptM and ptIn in relation to the line going through the points ptSeg_0 and ptSeg_1.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    bool bRet = false;
    PointF ptNearest;
    PointOfSegment nearestType;
    if (catcher == MouseButtons .Left)
    {
        double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptM);
        if (i == 0)
        {
            if (!Auxi_Geometry.OppositeSideOfLine(ptSeg_0, ptSeg_1, ptM, ptIn))
            {
                angleSweep=Auxi_Common.LimitedRadian (angleMouse - angleStart);
                bRet = true;
            }
        }
        else if (i == 1)
        {
            if (!Auxi_Geometry.OppositeSideOfLine(ptSeg_0, ptSeg_1, ptM, ptIn))
            {
                angleSweep = Auxi_Common.LimitedRadian (angleEnd - angleMouse);
                angleStart = Auxi_Common.LimitedRadian (angleEnd - angleSweep);
                bRet = true;
            }
        }
        … …

Pies

  • File: Form_Pies.cs
  • Menu position: Medley – Pies

Text Box:  
Fig.6  Pies with individually movable slices

Figure 6 demonstrates two objects of the Pie class; each pie is a collection of the PieSlice elements.

public class Pie
{
    long m_id;
    List<PieSlice> m_slices = new List<PieSlice> ();

As seen from this short code, the Pie object itself is not derived from the classical GraphicalObject class, so it has no cover, it cannot be registered in the mover’s queue, and thus cannot be involved in the process of moving / resizing in that standard way which was demonstrated with all other objects in the previous examples. At the same time these pies behave like any other movable and resizable objects: they can be moved around the screen and rotated, their sizes can be changed, any movement is started by pressing one or another part of a pie with a mouse, so the mover is involved in the whole process and supervises it in the familiar way. How is it organized?

When all the slices of one pie have their apices in the same central point and have the same radius, then such pie looks like a multicolored circle (the right pie at figure 6). It is not a problem to organize simple enough cover for such circle and it was already discussed in the previous part of this article. The design of cover depends not only on the shape of an object but also on the required movements of an object and its parts. For any circle, even multicolored with the sliding partitions, the relative simplicity of the cover is based on the fact that there is a single radius for all the parts and there are no gaps between the neighbouring sectors. 

On the other hand, the left pie at figure 6 shows that each slice of a Pie object may have its own radius and can be individually moved out and positioned separately from others; these two requirements make the design of cover in a standard way very complicated. I am not saying that it is impossible to organize a traditional cover for a pie with such movement requirements. It is possible but it will be too complicated and tiresome. Instead, I prefer to use in this case the idea of siblings: to have a set of similar looking elements that can in some cases move independently, but in other cases the movement of any of them causes the identical movement of all the linked elements. There are several examples with siblings in the book but I decided to use in this article the example with the pies because the cover for each slice is similar to what was already discussed in this part of the article. Slices of a pie may be placed with gaps between them but normally these gaps are not too big and the covers of the neighbouring slices overlap making a mess of lines on the screen; to see the pure cover for a single PieSlice element you need to move this slice really far away from the central point of its pie (figure 7).

Text Box:  
Fig.7	Usually the covers of the slices overlap and demonstrate a mess on the screen.  To see the cover of a single PieSlice in details, you need to move this slice far away from its pie.

The cover of a single PieSlice element is similar to the cover of a circle sector from the previous example (compare figures 5 and 7); only the sweep angle of a slice is fixed at the moment of initialization, so there is no need in two nodes on the sides. There is also one additional circular node in the cover of the slices because a slice can be moved individually or cause the synchronous movement of all the siblings. To organize two different movements, the area of a slice is divided into two parts and these parts are covered by two different nodes; for better information of users about these differences in reaction, two parts of a slice are painted in slightly different colors.  By pressing with the left button the inner (darker) part of a slice, the movement of the whole pie is started, and it is regardless of whether at this moment the pressed slice is positioned next to its neighbours or it was already moved out earlier. By pressing the outer (lighter) part you can start the individual movement of this slice along its central line.

// order of nodes:      [0, 1]          transparent rectangles on the sides
//                      [2]             inner circle
//                      [3]             bigger circle - delta
//                      [4]             bigger circle + delta
//
public override void DefineCover ()
{
    CoverNode [] nodes = new CoverNode [5];
    double radPlus = m_radius + delta;
    double angleA, angleB;
      if (angleSweep >= 0)
    {
        angleB = angleStart;
        angleA = angleStart + angleSweep;
    }
    else
    {
        angleA = angleStart;
        angleB = angleStart + angleSweep;
    }
    PointF [] ptsA, ptsB;
    if (Math .Abs (angleSweep) <= Math .PI)
    {
        PointF ptA = Auxi_Geometry .PointToPoint (ptApex, angleA, radPlus);
        PointF ptB = Auxi_Geometry .PointToPoint (ptApex, angleB, radPlus);
        ptsA = new PointF [] {ptA, 
            Auxi_Geometry .PointToPoint (ptA, angleA + Math .PI / 2, radPlus), 
            Auxi_Geometry .PointToPoint (ptApex, angleA + 3 * Math .PI / 4,
                                         radPlus * Math .Sqrt (2)), 
            Auxi_Geometry .PointToPoint (ptApex, angleA - Math .PI, radPlus) };
        ptsB = new PointF [] {ptB, 
            Auxi_Geometry .PointToPoint (ptApex, angleB - Math .PI, radPlus), 
            Auxi_Geometry .PointToPoint (ptApex, angleB - 3 * Math .PI / 4,
                                         radPlus * Math .Sqrt (2)), 
            Auxi_Geometry .PointToPoint (ptB, angleB - Math.PI / 2, radPlus) };
    }
    else
    {
        float r2 = 2 * m_radius;
         ptsA = new PointF [] {ptApex, 
            Auxi_Geometry .PointToPoint (ptApex, angleStart, r2), 
            Auxi_Geometry .PointToPoint (ptApex,
                                 angleStart + angleSweep / 2 + Math .PI, r2), 
            Auxi_Geometry .PointToPoint (ptApex, angleStart + angleSweep, r2)};
        ptsB = new PointF [] {m_center, 
            Auxi_Geometry .PointToPoint (ptApex, angleStart, r2), 
            Auxi_Geometry .PointToPoint (ptApex,
                                 angleStart + angleSweep / 2 + Math .PI, r2), 
            Auxi_Geometry .PointToPoint (ptApex, angleStart + angleSweep, r2)};
    }
    nodes [0] = new CoverNode (0, ptsA, Behaviour .Transparent);
    nodes [1] = new CoverNode (1, ptsB, Behaviour .Transparent);
    float rInner = m_radius * coefInner;       
    nodes [2] = new CoverNode (2, ptApex, rInner, Cursors .SizeAll);
    nodes [3] = new CoverNode (3, ptApex, m_radius - delta, Cursors .Hand);
    nodes [4] = new CoverNode (4, ptApex, m_radius + delta, Cursors .Hand);
    cover = new Cover (nodes);
    cover .SetClearance (false);
}

One more change in the cover of a slice in comparison with the cover of a wide arc (see figure 3). In the wide arcs the number of nodes changes when the sweep angle crosses the boundary of 180 degrees; in the slices the number of nodes is always the same regardless of such angle. The code for the fixed number of nodes in the cover is easier, so, if you want, you can use the same technique for the wide arcs. When the sweep angle of a slice is greater than 180 degrees, then it is enough to have one transparent node along both sides. I still use two nodes (to have the same number of nodes for any slice), but the second of these two nodes is fictional. This node is absolutely the same as the previous one, so this second node is never used (touched) by mover. There can be another simple solution for this never used node: make it a small circular node far away of the screen.

What special features and methods are used to organize the PieSlice movements and how the synchronous movement of siblings works?

A slice of a pie looks similar to a sector of a circle so it has some identical fields like radius (m_radius), angle of one side (angleStart), sweep angle from this side to another (angleSweep), and the brush for painting a slice (m_brush). Well, there is also another lighter brush for painting the outer part of the slice (brushLight). Movement can be started at any inner point of both parts. The ratio between the inner and outer circles is set at such value (coefInner = 0.7f) that the areas of two parts are nearly equal and this makes the starting of both movements equally easy.

public class PieSlice : GraphicalObject
{
    PointF m_center;
    PointF ptApex;
    float m_radius;
    double angleStart;
    double angleSweep;
    SolidBrush m_brush;
    SolidBrush brushLight;
    List<pieslice> siblings = null;
    float coefInner = 0.7f;
    static int minRadius = 20;
    int delta = 3;
    int minDistanceCenterApex = 5;</pieslice>

Usually a pie is organized as a circle and the apex of each slice (ptApex) is positioned in the central point of the whole object (m_center). Any slice can be moved outside from central point; throughout such individual movement of a slice its apex can move only along the central line of this slice.

In addition to standard fields, there is a List of siblings – List<PieSlice> siblings – but this List is populated only for the pressed slice at the moment of pressing. For all other slices of the pie the List of siblings is empty and this prevents from organizing an infinitive loop of associated movements.

private void OnMouseDown (object sender, MouseEventArgs e)
{
    ptMouse_Down = e .Location;
    if (mover .Catch (e .Location, e .Button))
    {
        GraphicalObject grobj = mover .CaughtSource;
        if (grobj is PieSlice)
        {
            slicePressed = grobj as PieSlice;
            long idSlice = grobj .ID;
            long idPie = slicePressed .ParentID;
            for (int i = 0; i < pies .Count; i++)
            {
                if (idPie == pies [i] .ID)
                {
                    iPiePressed = i;
                    piePressed = pies [i];
                    break;
                }
            }
            List<PieSlice> siblings = new List<PieSlice> ();
            for (int i = 0; i < piePressed .Slices .Count; i++)
            {
                if (idSlice != piePressed .Slices [i] .ID)
                {
                    siblings .Add (piePressed .Slices [i]);
                    piePressed .Slices [i] .Siblings = null;
                }
                else
                {
                    iSlicePressed = i;
                }
            }
            slicePressed .Siblings = siblings;
            if (e .Button == MouseButtons .Left)
            {
                foreach (PieSlice slice in piePressed .Slices)
                {
                    slice .SaveInitialRadius ();
                }
                if (mover .CaughtNode == 3)
                  {
                    slicePressed .StartSliceMovement (e .Location);
                }
                else if (mover .CaughtNode == 4)
                {
                    slicePressed .StartResizing (e .Location);
                }
            }
            else if (e .Button == MouseButtons .Right)
            {
                foreach (PieSlice slice in piePressed .Slices)
                {
                    slice .StartRotation (e .Location);
                }
            }
        }
    }
    ContextMenuStrip = null;
}

When a slice is moved individually, its apex is moving along the radial line and there is a strict limit on one side of this movement: apex cannot move over the central point. There is no other limit for apex movement, but such outer limit is needed for calculations and it is defined as being far away and definitely outside the screen. When a slice is pressed with the left button somewhere in its outer part, then it is the starting moment for individual movement. The PieSlice.StartSliceMovement() is used to calculate the range for moving the pressed mouse; this range – [ptInnerLimit, ptOuterLimit] – is defined by the allowed movement for apex point and the initial position of the pressed mouse in relation to the apex.

public void StartSliceMovement (Point ptMouse)
{
    angleApex = Auxi_Common .LimitedRadian (angleStart + angleSweep / 2);
    angleApexMouse = Auxi_Geometry .Line_Angle (ptApex, ptMouse);
    distApexMouse = Auxi_Geometry .Distance (ptApex, ptMouse);
    ptInnerLimit = Auxi_Geometry .PointToPoint (m_center, angleApexMouse,
                                                distApexMouse);
    ptOuterLimit = Auxi_Geometry .PointToPoint (ptInnerLimit, angleApex, 4000);
}

When the inner part of the slice (node number two) is pressed and moved, then the PieSlice.MoveNode() method simply calls the PieSlice.Move() method.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    bool bRet = false;
    if (catcher == MouseButtons .Left)
    {
        PointF ptBase, ptNearest;
        PointOfSegment nearestType;
        if (i == 2)
        {
            // move all
            Move (dx, dy);
        }
        … …
// -------------------------------------------------        Move
public override void Move (int dx, int dy)
{
    m_center += new Size (dx, dy);
    ptApex += new Size (dx, dy);
    if (siblings != null)
    {
        foreach (PieSlice slice in siblings)
        {
            slice .Center = m_center;
        }
    }
}

For the pressed slice the List of siblings was already populated, so for each member of this List, which means for all other slices of the pie, the PieSlice.Center property is called. This property defines the new m_center value and synchronously changes the apex position. In this way a movement of the pressed slice causes the synchronous movement of all other slices and there is no change in the view of the whole pie; only the m_center field and the apex position are renewed for all the slices.

When the outer part of the slice (node number three) is pressed and moved, then only the apex point of this pressed slice is moved inside the allowed range [ptInnerLimit, ptOuterLimit] while other slices are not affected.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    … …
    else if (i == 3)        // move one slice
    {
        Auxi_Geometry .Distance_PointSegment (ptM, ptInnerLimit,
                 ptOuterLimit, out ptBase, out nearestType, out ptNearest);
        ptApex = Auxi_Geometry .PointToPoint (ptNearest,
                                 angleApexMouse + Math .PI, distApexMouse);
        Cursor .Position = form .PointToScreen (Point .Round (ptNearest));
        bRet = true;
    }
    … …

When the border of the slice (node number four) is pressed and moved, then the current mouse position is used for border placement and the slice radius is calculated from the cursor location. If the slice was previously moved outside from the central point of the pie then nothing else is done but if the apex of this slice is positioned in the central point of the pie then the radii of all the slices are changed with the same coefficient.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    … …
    else if (i == 4)        // change radius
    {
        Auxi_Geometry .Distance_PointSegment (ptM, ptInnerLimit,
                ptOuterLimit, out ptBase, out nearestType, out ptNearest);
        m_radius =
           Convert .ToSingle (Auxi_Geometry .Distance (ptApex, ptNearest));
        Cursor .Position = form .PointToScreen (Point .Round (ptNearest));
        bRet = true;
        if (m_center == ptApex) 
        {
            double coef = (double) radiusStart / m_radius;
            foreach (PieSlice sector in siblings)
            {
                sector .Zoom (coef);
            }
        }
    }
    … …

There is one more interesting object in the Form_Pie.cs. This is not a graphical primitive but a small group of the ElasticGroup class. Groups are not to be discussed in this article but they are described in details and with many examples in the book. In the current application such groups are used in this example and the next one. As all the objects in the user-driven applications, these groups are movable and their inner elements are individually movable / resizable. The change of the inner elements causes, if needed, the automatic change of the group sizes. You can change all the visibility parameters of the group by calling its tuning dialog; for this use the right button click anywhere inside the group.

A lot of holes and a bit more

  • File: Form_SetOfHoles.cs
  • Menu position: Medley – Set of holes

Text Box:  
Fig.8  Each area consists mainly of holes which must be filled with simple elements

The last example of this article differs from all the previous. First, there are main objects – big areas with holes – and different auxiliary objects (figure 8). In the current version the auxiliary elements are only circles and regular polygons, but it is not a problem to add more variants. These auxiliary objects definitely belong to the graphical primitives and this article is a good place for them. Among these auxiliary objects are simple circles which were shown in the second part of the article and different regular polygons which had to be shown in the first part but did not appear there. Well, I am going to show them here. But polygons, though they perfectly fit with the idea of this article, are only the auxiliary elements in this example. The main objects of this example consist mostly of the holes. These objects are movable but they have a lot of holes which are their most interesting parts. Can the holes be the main thing of an object? Well, in the childhood we liked to buy the fresh bagels or something that looked like bagel but with much bigger hole than in a standard bagel. It was like a bread in the form of a big ring but this thing was much tastier than any bread. So what was it that made the thing so tasty?  Was it the hole?

In this example we have the rectangular areas with holes and the auxiliary elements – the plugs. The idea is to select the needed plug, move it to the center of the hole, rotate the plug, and resize it in such a way that the plug fits well enough with the hole. If it happens, then both the plug and the covered hole are eliminated. The area disappears when all its holes are closed.

public class Plug : GraphicalObject
{
    Shape m_shape;
    PointF m_center;
    float m_radius;
    int nVertices;
    double m_angle;
    SolidBrush m_brush;

Text Box:  
Fig.9  Plugs and their covers

Any plug is described by its shape (m_shape), central point (m_center), radius (m_radius; for regular polygons it is the distance from central point to vertices), number of vertices (only for polygons), angle of the first vertex (only for polygons), and the brush used for painting (m_brush). The cover for any Plug object is simple and consists of only two nodes (figure 9). It is the theoretical minimum of nodes number as any plug must be movable and resizable. Such simple cover for a circle was already shown in the second part of this article. For regular polygons the idea of a simplest cover is the same: there are two nodes copying the shape of the border; the first node is slightly less than an object itself; the second node is slightly bigger.

public override void DefineCover ()
{
    CoverNode [] nodes = new CoverNode [2];
    if (m_shape == Shape .Circle)
    {
        nodes [0] = new CoverNode (0, m_center, m_radius - minDelta,
                                      Cursors .SizeAll); 
        nodes [1] = new CoverNode (1, m_center, m_radius + minDelta);
    }
    else
    {
        double delta = minDelta / Math .Cos (Math .PI / nVertices);
        PointF [] ptsInside = Auxi_Geometry .RegularPolygon (m_center,
                                        m_radius - delta, nVertices, m_angle);
        PointF [] ptsOutside = Auxi_Geometry .RegularPolygon (m_center,
                                        m_radius + delta, nVertices, m_angle);
        nodes [0] = new CoverNode (0, ptsInside);
        nodes [1] = new CoverNode (1, ptsOutside, Cursors .Hand);
    }
    cover = new Cover (nodes);
}

The whole inner node is used for moving the pressed plug so the plug can be moved and rotated by any inner point. The second node is used for resizing but only its narrow strip along the border of an object is opened for mover so the resizing is done by any border point. When any point inside this narrow strip of the second node is pressed, then the mouse cursor is moved to the nearest point of the border, the beam (angleBeam) for the allowed movement of the pressed mouse and two limiting points on this beam – ptInnerLimit and ptOuterLimit – are calculated.

public void StartResizing (Point ptMouse)
{
    PointF ptCursor;
    double angleBeam;
    if (m_shape == Shape .Circle)
    {
        angleBeam = Auxi_Geometry .Line_Angle (m_center, ptMouse);
        ptCursor = Auxi_Geometry .PointToPoint (m_center, angleBeam, m_radius);
        ptInnerLimit =
                 Auxi_Geometry .PointToPoint (m_center, angleBeam, minRadius);
    }
    else
    {
        PointF [] pts = Vertices;
        Auxi_Geometry.Distance_PointPolyline (ptMouse, pts,true, out ptCursor);
        scaling = m_radius / Auxi_Geometry.Distance (m_center, ptCursor);// >=1
        angleBeam = Auxi_Geometry .Line_Angle (m_center, ptCursor);
        ptInnerLimit = Auxi_Geometry .PointToPoint (m_center, angleBeam,
                                                    minRadius / scaling);
    }
    ptOuterLimit = Auxi_Geometry .PointToPoint (m_center, angleBeam, 4000);
    Cursor .Position = form .PointToScreen (Point .Round (ptCursor));
}

Until the moment of release, the mouse can be moved only along the line between the two calculated points and the current position of the mouse is used as the border placement.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                               MouseButtons catcher)
{
    bool bRet = false;
    if (catcher == MouseButtons .Left)
    {
        if (i == 0)
        {
            Move (dx, dy);
        }
        else
        {
            PointF ptBase, ptNearest;
            PointOfSegment typeOfNearest;
            Auxi_Geometry .Distance_PointSegment (ptM, ptInnerLimit,
                   ptOuterLimit, out ptBase, out typeOfNearest, out ptNearest);
            double dist = Auxi_Geometry .Distance (m_center, ptNearest);
            if (m_shape == Shape .Circle)
            {
                m_radius = Convert .ToSingle (dist);
            }
            else
            {
                m_radius = Convert .ToSingle (dist * scaling);
            }
            bRet = true;
            Cursor .Position = form .PointToScreen (Point .Round (ptNearest));
        }
    }
    … …

An area with the holes belongs to the AreaWithHoles class and this is the most interesting object in this example.

public class AreaWithHoles : GraphicalObject
{
    RectangleF rc;
    int nRow, nCol;
    List<Hole> holes = new List<Hole> ();
    SolidBrush brush;

Number of holes in an area can vary but the central points of the holes are positioned along rows and columns and there are some simple limitations to prevent the overlapping of holes. Holes and plugs have the same set of shapes (Shape enumeration) and the Hole class has the same fields for definition of its objects as the Plug class.

public class Hole
{
    Shape m_shape;
    PointF m_center;
    float m_radius;
    int nVertices;
    double m_angle;

Holes are not movable. They are defined at the moment when any new area is organized and their parameters are used in design of the area’s cover. The sizes of an area can be different but they are defined at the moment of initialization and are not changed later. Thus, an area is a non-resizable but movable rectangle and it would be enough to have for it a cover consisting of a single rectangular node if not that problem of holes.  The holes must provide the view through them and this is achieved by a simple painting. The holes must also provide the movement of the underlying objects and this is the classical case for using the transparent nodes. Each hole has a simple shape and any hole can be covered by a single node either circular or polygonal. Thus, we have a set of transparent nodes which precede the only non-transparent node in the cover.  If there are N holes in the area, then the cover for such area consists of N+1 nodes and only the last one of them is not transparent.  It is an unusual, very interesting in design, but simple enough cover.

public override void DefineCover ()
{
    CoverNode [] nodes = new CoverNode [holes .Count + 1];
    for (int i = 0; i < holes .Count; i++)
    {
        if (holes [i] .VerticesNumber == 0)
        {
            nodes [i] = new CoverNode (i, holes [i] .Center, holes [i] .Radius,
                                       Behaviour .Transparent);
        }
        else
        {
            nodes [i] = new CoverNode (i, holes [i] .Vertices,
                                       Behaviour .Transparent);
        }
    }
    nodes [holes .Count] = new CoverNode (holes .Count, rc);
    cover = new Cover (nodes);
}

From the point of design, it is an unusual cover and that was the only reason to demonstrate it here. It is a bit unusual and very interesting example of using the transparent nodes. From the point of moving, it is difficult to think out anything simpler. There is only one node to start any movement and this node can be used only for forward movement, so when this node is pressed then the AreaWithHoles.MoveNode() method must call the AreaWithHoles.Move() method.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    bool bRet = false;
    if (catcher == MouseButtons .Left)
    {
        Move (dx, dy);
        bRet = true;
    }
    return (bRet);
}

The AreaWithHoles.Move() method has to change the location of the area according to the mouse movement and the only additional thing is the synchronous change of the central points for all the holes of the area.

public override void Move (int dx, int dy)
{
    rc .X += dx;
    rc .Y += dy;
    SizeF size = new SizeF (dx, dy);
    for (int i = 0; i < holes .Count; i++)
    {
        holes [i] .Center += size;
    }
}

Conclusion to all three parts of this article

This article demonstrates only the moving and resizing of the simplest screen elements that we use in our programs. I began with the primitive but often used cases of covers, which are combined of several nodes, and demonstrated the moving of lines and polygons. For the curved borders, the N-node covers are used. Though such covers may consist of significant number of nodes, all these nodes behave in the same way so the MoveNode() methods for classes with such covers are also simple. The next very powerful technique is the use of the transparent nodes. They can turn into unbelievably simple the covers that can be developed in a standard way but require a lot of very tiresome work. They can also give simple solutions for the cases in which the use of standard cover design is problematic.

The objects demonstrated in this article are simple and I did it purposely. It’s like the use of differentiation and integration.  You learn to use them on some set of simple examples but there is no fixed list of their full use. Everything is based on your knowledge of several main rules, experience, and the ability to think and to solve the new problems. Exactly the same happens with the movability of the real screen objects. Some of them just copy the examples from this article and you can use them immediately. You can find a lot of other examples from different areas in the book and its associated project with the codes in the http://sourceforge.net/projects/movegraph/files/?source=directory. There are a lot of examples but they are not going to cover all your demands. Use the demonstrated technique and your brains.

I tried not to include more complicated objects into the examples of this article but the ElasticGroup class already found its way into it. Maybe I’ll write about this and the ArbitraryGroup class in the future article but both classes are described and demonstrated in details in the mentioned book. We often need to use complex objects which allow individual, synchronous, and related movements of their parts; design and work of movable complex objects is also described in the book.

Using of objects’ movability in your programs is like the using of math: all depends on you and your ability to think.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

SergeyAndreyev

United States United States
No Biography provided

Comments and Discussions

 
QuestionThe good one PinmemberAlex Beyderman11-Apr-13 1:12 
AnswerRe: The good one PinmemberSergeyAndreyev11-Apr-13 1:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140922.1 | Last Updated 8 Apr 2013
Article Copyright 2013 by SergeyAndreyev
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid