Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

What can be simpler than graphical primitives? Part 1

0.00/5 (No votes)
5 Mar 2013 1  
This article is about the moving and resizing of different graphical primitives.

Introduction

When all the screen elements are fixed, you can look at the program only through the eyes of its developer and go only in his steps. When all the screen elements are movable and resizable, you can do all the things that were coded by the developer but you can do all those things the way you want them to be done. This is the power of user-driven applications. To get all those possibilities, you need only one small change in the program: all the screen elements must be easily movable and resizable by you at any moment.

A lot of interesting programs can be based on very simple elements; for example, to organize a Family tree it is enough to use straight lines and rectangles while very interesting and informative graphs can be based on circles and rings. Graphical primitives which are used in absolutely different applications are the same and the list of these primitives is not too long: small colored spots, straight lines, rectangles, polygons, circles, rings, crescents, rounded strips, arcs, and…  Maybe I’ll remember something else later but at the moment I think this will be enough for my article. I also think that it will be too much for a single article so I am going to divide it into two or three parts.

Drawing of all those mentioned primitives is not a problem at all. In my programs all the objects are movable regardless of their complexity and shape, so this article is about the moving and resizing of different graphical primitives. For some of them the standard algorithms are used and the whole process of their moving and resizing looks easy. Objects with the curved borders and holes may require special technique. Nearly all these things are described in the book World of Movable Objects and all the codes can be found in its accompanying program. Some of the examples (also with all the codes) can be found in another book Easy Tasks which is organized as an exercise-book with explanations. Both books with their accompanying projects can be downloaded from the http://sourceforge.net/projects/movegraph/files/?source=directory

The program to accompany this article is designed as simple as possible. At the same time it is an ordinary user-driven application so it works according to all the rules of such applications.

  • All the elements are movable.
  • All the parameters of visibility are easily controlled by the users.
  • The users’ commands on moving / resizing of objects or on changing the visibility parameters are implemented exactly as they are; no additions or expanded interpretation by developer are allowed.
  • All the parameters are saved and restored.
  • The above mentioned rules are implemented at all the levels beginning from the main form and up to the farthest corners.

Thus, you can change the position, size, and view of any element you see on the screen. Forward movement of an element is done by the left button and can be started at any inner point of an element; rotation is done in the similar way but by the right button. Resizing can be started by the left button at any border point. Movement of some special (usually obvious) points changes the configuration of the touched object. Commands for tuning are included into context menus which are called either by the right click, or by the double click of the left button, or in both ways.

The basic ideas of turning any screen object into movable / resizable are described at the beginning of the book World of Movable Objects (read chapter 1 – Requirements, ideas, algorithm). The whole process can be described in several words, but the devil (or the magic?) is in the details. An object is covered by a set of nodes. Three shapes of nodes are used: circles, rounded strips, and convex polygons. There are no limitations on the sizes of nodes or their placement. Each node is used for reconfiguring, resizing, or forward movement of an object. In this way each and all the screen objects are turned into movable / resizable, and together such objects allow to develop absolutely different programs – user-driven applications. But the basis of all these new things is an easy to use instrument of turning any object into movable, and the set of different shapes with which we work is very limited…

Now we can start. The plan for Part 1 of this article is perfectly described by the well known child rhyme:

“One’s none;
Two’s some;
Three’s many;
Four’s a penny;
Five’s a little hundred.”

Points

  • File: Form_Spots.cs
  • Menu position: Spots

The smallest screen element is a point. It is so small that it is even difficult to see. To make the process of moving the points easier, I often mark them with small colored spots; the needed point is the central point of such a spot (figure 1).

Text Box:  
Fig.1  Form_Spots.cs

The real point has only one parameter – its location. A representation of the point as a spot is described by the central point, radius, and color of the spot.

public class SpotCC : GraphicalObject
{
    Form form;
    Mover supervisor;
    PointF m_center;
    int m_radius = 5;
    Color m_color = Color .Red;
    static int radMin = 3;
    static int radMax = 12;

This SpotCC class is nearly identical to the Spot class from EasyTasks. The CC part in the name of the SpotCC class is an abbreviation for centered cursor; this is the only and the main difference between two classes. When a colored spot is pressed for moving, there are two ways to go on with this moving.

  • You can calculate the initial difference (dxShift, dyShift) between the mouse cursor and the central point of the spot and use this shift throughout the whole process of movement to calculate the central point based on the cursor location. This technique is used in the Spot class from EasyTasks.
  • The colored spot is usually so small that a mandatory move of the cursor for several pixels is not even detected by users. Thus, at the starting moment of movement the cursor can be switched to the central point of the pressed spot and then throughout the whole movement the cursor position can be used as the central point of this colored spot. This technique is used in the SpotCC class.

Movement of any object is done by the mouse. From the initial moment when an object is pressed by the mouse cursor, any mouse movement is transformed into the movement of this object. There can be different restrictions on the objects’ movements (the chapter Movement restrictions is one of the biggest in the book) and there can be two types of reactions on such restrictions.

  • An object is stopped because of the existing limitation on required movement, but the mouse cursor continues to move.  If you change, for example, the direction of mouse movement in such a way that the existing restriction does not prevent the movement of the object any more then the object starts moving again. There is still the same correlation between the movements of the mouse and the object but their relative position now differs from what was at the beginning of movement. During the first years of my work with the movable objects I used only this logic of the link between the mouse and the object caught for movement so this logic is demonstrated in many examples of the book.
  • When an object is stopped by any restriction on its movement, the mouse cursor is also stopped and does not move farther on. Throughout the whole process of movement the mouse cursor retains the same relative position; I call it the technique of adhered mouse.  Now I use this logic more and more. Even if I use an example similar to the one demonstrated in the book, chances are high that in the book the first type of logic was used while now I use the second type of logic in the case of some restriction. Certainly, a small change in the code of the original class of objects has to be made.

When the first type of logic is used then any move of the mouse for (dx, dy) pixels results in equal move of the caught object. This is correct when there are no restrictions. When for any reason such move of an object is not allowed then an object does not move at all though the cursor has already moved.

For the second type of logic a small trick has to be used. Suppose that you press the colored spot not on its central point. As I mentioned before, at this moment the cursor is moved to the central point. But any cursor movement is transformed into the movement of the caught spot. If nothing is done, then the cursor will be moved to the central point but the colored spot will move away from the cursor on exactly the same shift. To avoid this, the link between the cursor (the mover) and the caught object must be temporarily cut for this initial mouse shift. This is done inside the SpotCC.StartMoving() method.

private void OnMouseDown (object sender, MouseEventArgs e)
{
    ptMouse_Down = e .Location;
    if (mover .Catch (e .Location, e .Button))
    {
        GraphicalObject grobj = mover .CaughtSource;
        if (e .Button == MouseButtons .Left)
        {
            if (grobj is SpotCC)
            {
                (grobj as SpotCC) .StartMoving ();
            }
        }
    }
}

// -------------------------------------------------        StartMoving
public void StartMoving ()
{
    supervisor .MouseTraced = false;
    Cursor .Position = form .PointToScreen (Point .Round (m_center));
    supervisor .MouseTraced = true;
}

In further examples you will see that the same temporary disconnection between a mover and the caught object is used when some restriction does not allow the movement of the caught object and the cursor has to be returned to the previous location.

Segment of a straight line

Segment of a straight line is such a simple object.  What can be interesting there? I am going to demonstrate not even one but two classes of such elements, so there are some interesting features.

  • File: Form_StraightSegment_ArbitraryAngle.cs
  • Menu position: Straight segment – Arbitrary angle

Text Box:  
Fig.2  An arbitrary length and angle can be set by moving the end points of a segment

Let us start with the segments belonging to the StraightSegment_ArbitraryAngle class. A segment of a straight line looks very simple – it is a line between two end points, so this is all that is needed for its description: two points and a pen.

public class StraightSegment_ArbitraryAngle : GraphicalObject
{
    Form form;
    Mover supervisor;
    PointF ptA, ptB;
    Pen m_pen;
    static int minLen = 16;
    double compensation;

By moving any end point, the length and the angle of the line can be changed. To avoid the problem of an accidentally disappearing object, I added the limit on minimal length. A segment can be moved around the screen and rotated (around its middle point) by any inner point. To provide all the needed movements, a simple cover of two circles and one strip is used.  Two small circular nodes cover the end points while the whole length of a segment is covered by one rounded strip.

public override void DefineCover ()
{
    float radius = Math .Max (3, (m_pen .Width + 1) / 2);
    cover = new Cover (new CoverNode [] {new CoverNode (0, ptA, radius), 
                                         new CoverNode (1, ptB, radius),
                                         new CoverNode (2, ptA, ptB, radius)});
}

When a circular node at one or another end of a segment is pressed by mouse then the cursor is moved exactly to the associated end point. This is done in the same way as in the SpotCC class, but here is a small difference. Because there are two end points, then the number of the caught node must be sent as a parameter to the StraightSegment_ArbitraryAngle.StartResizing() method.

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 StraightSegment_ArbitraryAngle)
        {
            StraightSegment_ArbitraryAngle line =
                                 grobj as StraightSegment_ArbitraryAngle;
            if (e .Button == MouseButtons .Left)
            {
                line .StartResizing (mover .CaughtNode);
            }
            else if (e .Button == MouseButtons .Right)
            {
                line .StartRotation (e .Location);
            }
        }
        Invalidate ();
    }
}

Depending on the number of the caught node, the cursor is positioned on one or another end point.

public void StartResizing (int iNode)
{
    if (iNode == 0)
    {
        //supervisor .MouseTraced = false;
        Cursor .Position = form .PointToScreen (Point .Round (ptA));
        //supervisor .MouseTraced = true;
    }
    else if (iNode == 1)
    {
        //supervisor .MouseTraced = false;
        Cursor .Position = form .PointToScreen (Point .Round (ptB));
        //supervisor .MouseTraced = true;
    }
}

An end point can be moved anywhere around the screen. Whenever the cursor goes, the associated end point goes with it. There is only one restriction: no segment can become shorter than the allowed minimal length, so when you try to turn the caught segment into a single point or simply to make it too short something must be done. When you try to move the caught end point too close to another end point, the caught end point stops and the cursor must be returned back to it. Here is the part of the StraightSegment_ArbitraryAngle.MoveNode() method for the case of the node with number zero – the node over ptA; the code for circular node over ptB is similar.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                   MouseButtons catcher)
{
    bool bRet = false;
    if (catcher == MouseButtons .Left)
    {
        switch (i)
        {
            case 0:
                if (Auxi_Geometry .Distance (ptM, ptB) >= minLen)
                {
                    Point_A = ptM;
                    bRet = true;
                }
                else
                {
                    //supervisor .MouseTraced = false;
                    Cursor.Position = form .PointToScreen (Point.Round (ptA));
                    //supervisor .MouseTraced = true;
                }
                break;
            … …

There are commented lines both in the StartResizing() and MoveNode() methods.  These comments can be easily deleted but in general they are needed as working lines of code so I don’t want to erase them and I want to explain why they are not needed in the case of the StraightSegment_ArbitraryAngle class.

The MoveNode() method is used in any class of the movable objects to define the movement of the caught object as a reaction on the mouse movement. Among the parameters of this method you can see the (dx, dy) shift of the mouse cursor from the previous position and the exact position of the mouse cursor ptM. While dealing with rotation, I prefer to use the exact position of the mouse – ptM. While dealing with the forward movement, I preferred to base my calculations on the mouse shift (dx, dy). When the move of an object is calculated from the (dx, dy) of the cursor and the cursor has to be returned back to the previous position without any change of the object’s position, then the temporary disconnection of the object from the mover is a mandatory thing; you will see this in further examples. When the position of the point is determined by the cursor position ptM and the cursor is returned to the position of this point then such disconnection is not needed. It is not needed in this particular (and rare) case of forward movement but it is needed in general; I preferred to comment the lines than to delete them. Now I use more and more the technique of adhered mouse and for such case the calculations on the basis of ptM are preferable. Thus, even for forward movement there are more examples when the temporary disconnection between the mover and the caught object is not needed any more. You have to understand the condition for its use and organize such disconnection only when it is needed.

Rotation of a segment is really easy. From two end points of the segment – ptA and ptB – the length of the segment and its angle – it is the angle from ptA to ptB - are calculated. Throughout the rotation, the middle point and the length of a segment do not change, so it is not a problem to calculate both end points for any particular angle.

At the initial moment of rotation, which starts when the line is pressed with the right button, the angle from the middle point of the segment to the mouse cursor is calculated and then the compensation angle as the difference between this angle and the line angle.

public void StartRotation (Point ptMouse)
{
    double angleMouse = Auxi_Geometry .Line_Angle (MiddlePoint, ptMouse);
    compensation = Auxi_Common .LimitedRadian (angleMouse - Angle);
}

This compensation does not change throughout the whole process of rotation so for any mouse position the angle of the line is easily calculated. With the known length, middle point, and angle of the segment, its end points are calculated without problem.

public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                       MouseButtons catcher)
{
    bool bRet = false;
    if (catcher == MouseButtons .Left)
    {
        … …
    }
    else if (catcher == MouseButtons .Right)
    {
        double angleMouse = Auxi_Geometry .Line_Angle (MiddlePoint, ptM);
        Angle = angleMouse - compensation;
        DefineCover ();
        bRet = true;
    }
    return (bRet);
}

The straight segments of the next example have slightly different rules of their change. The rotation is organized in exactly the same way around the middle point of the pressed segment, but when you try to move any end point, it is allowed to move only along the line. For better demonstration of this feature, there are auxiliary lines to show the possible way of movement for the end points.

  • File: Form_StraightSegment_OnLine.cs
  • Menu position: Straight segment – On the line

This Form_StraightSegment_OnLine.cs looks similar to the previous one, but each segment of a straight line has an auxiliary thin line which goes from one border of the form to another. These lines help to see the special features of the SegmentOfLine class. The new segments can be moved around the screen and rotated in exactly the same way as was done with segments from the previous example and the only difference is in moving the end points: they can be moved only along the line. There are also limits both on minimal and maximum length of segments, so when any end point is grabbed for moving, two points on the line are calculated (pt0_possible and pt1_possible) and the caught end point can be moved only between these points.

Text Box:  
Fig.3  The end points can be moved only along the line, but the line itself can be moved around the screen and rotated

public void StartResizing (int iNode)

{
    if (iNode == 0)
    {
        Cursor .Position = form .PointToScreen (Point .Round (ptA));
        pt0_possible = Auxi_Geometry .PointToPoint (ptB, Angle + Math .PI, maxLen);
        pt1_possible = Auxi_Geometry .PointToPoint (ptB, Angle + Math .PI, minLen);
    }
    else if (iNode == 1)
    {
        Cursor .Position = form .PointToScreen (Point .Round (ptB));
        pt0_possible = Auxi_Geometry .PointToPoint (ptA, Angle, maxLen);
        pt1_possible = Auxi_Geometry .PointToPoint (ptA, Angle, minLen);
    }
}

When an end point is moved by mouse, then the mouse cursor moves somewhere along the line. Depending on the new cursor location, the nearest point on the line (ptBase) and the nearest point inside the allowed range (ptNearest) are calculated. If the nearest point on the line is inside the allowed range, then the end point is moved to the new location; otherwise it is not moved at all.  In any situation the cursor is moved into the position of the end point.

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 typeOfNearest;
        switch (i)
        {
            case 0:
                Auxi_Geometry .Distance_PointSegment (ptM, pt0_possible,
                   pt1_possible, out ptBase, out typeOfNearest, out ptNearest);
                if (ptBase == ptNearest)
                {
                    Point_A = ptNearest;
                    bRet = true;
                }
                Cursor .Position = form .PointToScreen (Point .Round (ptA));
                break;
            … …

We are moving along the digits: one, two, three…  If this is the number of special points in a figure, then we already looked at colored spots (one point) and line segments (two points), so the next one is going to be a triangle.

Triangles

  • File: Form_Triangles.cs
  • Menu position: Triangles

Any triangle is described by three points of its vertices and the color.

public class Triangle : GraphicalObject
{
    PointF [] pts;
    SolidBrush brush;
    int radiusNode;
    PointF ptCenter;
    Pen penBorder = new Pen (Color .DarkGray, 3);
    static double minDistOpposite = 8;

Text Box:  
Fig.4  Triangles

For the first time in this article we are going to deal with figures in which the border and the main area are easily distinguishable and are used for starting different movements. The main area of any triangle is used to start the forward movement of the whole figure (left button press) or rotation (right button). Three vertices can be used to change the configuration of the pressed triangle while all other parts of the border are used for resizing. To emphasize the different reactions on pressing the border or the main area of a figure, I added the special drawing of the border with a wide enough pen of different color (penBorder). The rotation goes around the point of bisector’s crossing; an auxiliary pen shows this point by drawing all three bisectors (figure 4).

Three different types of movements are started by the left button (reconfiguring, resizing, and forward movement), so they must be provided by different nodes of the cover. Usually the smaller nodes must precede the bigger ones in the cover, so we have such order of nodes: circular nodes on vertices, strip nodes along the border segments, and the polygonal node to cover the whole area of a figure.

// 3 circles + 3 strips + polygon
public override void DefineCover ()
{
    CoverNode [] nodes = new CoverNode [7];
    for (int i = 0; i < 3; i++)
    {
        nodes [i] = new CoverNode (i, pts [i], radiusNode);
    }
    for (int i = 0; i < 3; i++)
    {
        nodes [i + 3] = new CoverNode (i + 3, pts [i], pts [(i + 1) % 3]);
    }
    nodes [6] = new CoverNode (6, pts);
    cover = new Cover (nodes);
}

The unrestricted reconfiguring or resizing of a triangle can produce some strange results. For example, in the book World of Movable Objects there is the Form_Triangles.cs in which similar triangles can be reconfigured without any restrictions; as a result, a triangle can be turned into a single line (all three vertices are on the same line) and even this line can disappear from view. There is still a triangle somewhere on the screen, but it is invisible and you do not know where to press in order to move some vertex and make the figure visible again. To avoid such situations in the current example, I added the special border drawing into the Triangle class and also set the minimal allowed distance between a vertex and opposite side of triangle (minDistOpposite).

Any movement of a Triangle object starts when an object is pressed with a mouse. Some preliminary actions and calculations may be needed and they are done in the special methods of the Triangle class.

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 Triangle)
        {
            Triangle trio = grobj as Triangle;
            if (e .Button == MouseButtons .Left)
            {
                NodeShape shape = mover .CaughtNodeShape;
                if (shape == NodeShape .Circle)
                {
                    trio .StartReconfiguration (mover .CaughtNode);
                }
                else if (shape == NodeShape .Strip)
                {
                    trio .StartResizing (mover .CaughtNode, e .Location);
                }
            }
            else if (e .Button == MouseButtons .Right)
            {
                trio .StartRotation (e .Location);
            }
        }
        Invalidate ();
    }
    ContextMenuStrip = null;
}

The easiest reaction is on pressing a circular node above one of the vertices. At this moment the mouse cursor is moved exactly on this covered vertex and throughout the full further movement the position of the cursor is considered as the new position of this vertex. Certainly, if this new position for the vertex is allowed, but this is checked in the Triangle.MoveNode() method to which I’ll return a bit later..

public void StartReconfiguration (int iNode)
{
    Cursor.Position = form .PointToScreen (Point .Round (pts [iNode]));
}

The resizing is started when one of the strip nodes along the border is pressed. During the resizing, the cursor can be moved only along the straight line which goes from the point of bisectors’ crossing (ptCenter) and this point on the border (ptOnBorder). Throughout the time of resizing this auxiliary line is shown on the screen. One end of this line (ptNearestToCenter) is not exactly on the point of bisectors’ crossing but is slightly aside because there is a limitation on the squeeze of triangle; another end of the line (ptFarAway) is far away and is definitely outside the screen.  At the same initial moment of resizing the distances (double [] radii) and the angles from the point of bisectors’ crossing to three vertices (double [] angles) are calculated.

public void StartResizing (int iNode, Point ptMouse)
{
    double [] dist = new double [3];
    for (int i = 0; i < 3; i++)
    {
        dist [i] = Auxi_Geometry .Distance_PointLine (pts [i],
                                       pts [(i + 1) % 3], pts [(i + 2) % 3]);
    }
    double distMin = Math .Min (Math .Min (dist [0], dist [1]), dist [2]);
    double coefToNearest = minDistOpposite / distMin;
    for (int i = 0; i < 3; i++)
    {
        radii [i] = Auxi_Geometry .Distance (ptCenter, pts [i]);
        angles [i] = Auxi_Geometry .Line_Angle (ptCenter, pts [i]);
    }
    PointF ptOnBorder;
    Auxi_Geometry .Line_Crossing (ptCenter, ptMouse, pts [iNode % 3],
                                      pts [(iNode + 1) % 3], out ptOnBorder);
    Cursor .Position = form .PointToScreen (Point .Round (ptOnBorder));
    distInitial = Auxi_Geometry .Distance (ptCenter, ptOnBorder);
    ptNearestToCenter = Auxi_Geometry .PointOnLine (ptCenter, ptOnBorder,
                                                    coefToNearest);
    ptFarAway = Auxi_Geometry .PointToPoint (ptCenter,
                       Auxi_Geometry .Line_Angle (ptCenter, ptOnBorder), 4000);
    bInResizing = true;
}

Start of the forward movement does not need any calculations but the start of rotation requires the calculation of distances from the point of bisectors’ crossing to three vertices (double [] radii) and the compensation angles for those vertices (double [] compensation).

public void StartRotation (Point ptMouse)
{
    double angleMouse = Auxi_Geometry .Line_Angle (ptCenter, ptMouse);
    for (int i = 0; i < 3; i++)
    {
        radii [i] = Auxi_Geometry .Distance (ptCenter, pts [i]);
        compensation [i] = Auxi_Common .LimitedRadian (angleMouse –
                               Auxi_Geometry .Line_Angle (ptCenter, pts [i]));
    }
}

All possible movements are described in the Triangle.MoveNode() method.

  • If the polygonal node is moved by the left button, then it is a simple forward movement without any change of triangle’s configuration.
  • public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                       MouseButtons catcher)
    {
        bool bRet = false;
        if (catcher == MouseButtons .Left)
        {
            if (i == 6)
            {
                Move (dx, dy);
            }
            … …
    // -------------------------------------------------        Move
    public override void Move (int dx, int dy)
    {
        SizeF size = new SizeF (dx, dy);
        for (int i = 0; i < pts .Length; i++)
        {
            pts [i] += size;
        }
        ptCenter += size;
    }
  • If a circular node is moved, then in normal situation the caught vertex is moved to the new mouse position. If for some reason this position is not allowed, then the cursor is simply returned to the current position of the pressed vertex.
  • public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                       MouseButtons catcher)
    {
        bool bRet = false;
        if (catcher == MouseButtons .Left)
        {
            … …
            else if (i < 3)
            {
                int iA = (i + 1) % 3;
                int iB = (i + 2) % 3;
                if (Auxi_Geometry .Distance_PointLine (ptM, pts [iA], pts [iB]) >
                                                                 minDistOpposite &&
                    Auxi_Geometry.SameSideOfLine (pts[iA], pts[iB], ptM, pts[i]))
                {
                    pts [i] = ptM;
                    ptCenter = Auxi_Geometry .TriangleBisectorsCrossing (pts);
                    bRet = true;
                }
                else
                {
                    Cursor.Position = form .PointToScreen (Point .Round (pts [i]));
                }
            }
            … …
  • Throughout the resizing, the cursor is moved along the auxiliary line, the scaling coefficient is calculated, and all three vertices are moved according to this coefficient but without changing the angles.
  • public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                       MouseButtons catcher)
    {
        bool bRet = false;
        if (catcher == MouseButtons .Left)
        {
            … …
            else
            {
                PointF ptBase, ptNearest;
                PointOfSegment typeOfNearest;
                Auxi_Geometry .Distance_PointSegment (ptM, ptNearestToCenter,
                         ptFarAway, out ptBase, out typeOfNearest, out ptNearest);
                Cursor .Position = form .PointToScreen (Point .Round (ptNearest));
                double coef =
                      Auxi_Geometry .Distance (ptCenter, ptNearest) / distInitial;
                for (int j = 0; j < 3; j++)
                {
                    pts [j] = Auxi_Geometry .PointToPoint (ptCenter, angles [j],
                                                           radii [j] * coef);
                }
            }
        … …
  • Rotation goes in a standard way. For any position of the mouse cursor its angle is easily calculated; after it the known distances to all three vertices and their compensation angles give the new positions of the vertices.
  • public override bool MoveNode (int i, int dx, int dy, Point ptM,
                                       MouseButtons catcher)
    {
        bool bRet = false;
        if (catcher == MouseButtons .Left)
        {
            … …
        }
        else if (catcher == MouseButtons .Right)
        {
            double angleMouse = Auxi_Geometry .Line_Angle (ptCenter, ptM);
            for (int j = 0; j < 3; j++)
            {
                pts [j] = Auxi_Geometry .PointToPoint (ptCenter,
                      Auxi_Common .LimitedRadian (angleMouse - compensation [j]),
                                                  radii [j]);
            }
            DefineCover ();
            bRet = true;
        }
        return (bRet);
    }

What else can be found in the Form_Rectangles.cs? The rules of user-driven applications require that all visualization parameters can be easily changed by user at any moment and that all the changes must be saved in order to provide exactly the same view when the application is used the next time. In the Form_Triangles.cs the change of the visualization parameters is provided via the commands of two context menus. One menu can be called on the information area and three commands allow to change its font, color of the text, and the background color. Another menu can be called on any triangle and allows to change its color and also to change the order of triangles on the screen. Each movable object occupies its personal level and the commands allow to change the order of these levels.

One, two, three, four, …

Rectangle is the most often used shape of the screen elements and there are different variants to move and resize the rectangles; all depends on particular task and application. It can be a big surprise but I am going to skip the case of rectangles in this article. Different types of rectangles are demonstrated by the examples of the book and I do not want to copy them into this article. Some of those cases, for example, rectangles with the fixed ratio of the sides can be organized now in a better way, but I think that you can use it as a small exercise (certainly, if you are interested).

There are more variants of polygons than rectangles, but here I am going to write only about one type of them – the chatoyant polygons. The name highlights not the way they are moved or resized but only their painting technique. It’s not the main feature of the Polygon_Chatoyant class but it allows to distinguish them from other polygons.

  • File: Form_ChatoyantPolygons.cs
  • Menu position: Polygons

Figure 5 shows the default view of the Form_ChatoyantPolytgons.cs. Those polygons in view belong to the Polygon_Chatoyant class and their covers are similar to what was shown with triangles in the previous example. Each polygon is described by a set of vertices and one central point plus the colors for all these points. There is also an additional field to decide about the drawing of auxiliary lines from the central point to the vertices on the border.

public class Polygon_Chatoyant : GraphicalObject
{
    PointF m_center;
    PointF [] ptVertices;
    Color clrCenter;
    Color [] clrVertices;
    bool bAuxiLines;

Text Box:  
Fig.5  Polygons of the Polygon_Chatoyant class

Next figure with the special drawing of cover (figure 6) makes the explanation of the cover much easier.

  • The central point is the place for a circular node.
  • Each vertex is also covered by a circular node.
  • Every two consecutive vertices are connected by a strip node; the last vertex is connected with the first one, so each vertex is connected with two neighbors.
  • Each pair of two consecutive vertices and the central point form a polygonal (triangular) node.

Text Box:  
Fig.6  Chatoyant polygon and its cover

The cover of the Polygon_Chatoyant class uses the nodes of all three possible types to organize moving, resizing, and reconfiguring of the objects. The easiest way to initialize such an object is to organize it in the form of a regular polygon, but it can be done also in other ways. There is no requirement for polygon to be convex and the central point can be placed anywhere. You can take any set of points (N >= 3) and declare them to be the vertices; you can also declare an arbitrary point to be a central point. When the Polygon_Chatoyant object is initialized in the form of a regular polygon, then the central point is really the central, but generally this is only the point of rotation for the whole object and nothing more; this “central” point can be moved anywhere and can be either inside the polygon or outside. When such object is initially organized in the form of a regular polygon, then the strip nodes cover the perimeter of an object; the union of the polygons (triangles) covers the whole area of an object. Later, when any point associated with a circular node can be moved around the screen to an arbitrary place, a polygon quickly changes its shape and the strips are not on the perimeter any more, but an object can be still moved, reconfigured, and zoomed in the same way. In the further explanation, whenever I write central point, it means the point, which was originally central at the moment of initialization, but can be now anywhere; the same thing about vertices.

  • Circular nodes are used for individual movements of the points with which they are associated regardless of whether it is a vertex or a center. This is the way for reconfiguring a polygon; though often born as regular polygons, these objects can be quickly turned into very strange figures.
  • Strip nodes are used for zooming.
  • Polygonal nodes (triangles) are used for moving the whole objects.

For originally regular N-gon, the number of nodes is 3 * N + 1, so a polygon with 12 vertices has 37 nodes. It looks like there must be a lot of code writing for such object, especially in the MoveNode() method, but in reality it is not so, because the nodes of the same group have similar behaviour. As often done with the covers containing different types of nodes, the smaller nodes are included into the cover before the bigger ones, so here is the order of nodes for the Polygon_Chatoyant class:

  1. N circular nodes on the vertices.
  2. One circular node on the center.
  3. N strip nodes along the border; the strips connect the neighboring vertices.
  4. N triangular nodes for the inner area.
public override void DefineCover ()
{
    CoverNode [] nodes = new CoverNode [3 * VerticesNum + 1];
    for (int i = 0; i < VerticesNum; i++)
    {
        nodes [i] = new CoverNode (i, ptVertices [i], 6);
    }
    nodes [VerticesNum] = new CoverNode (VerticesNum, m_center, 6);
    int k0 = VerticesNum + 1;
    for (int i = 0; i < VerticesNum; i++)
    {
        nodes [k0 + i] = new CoverNode (k0 + i, ptVertices [i],
                                       ptVertices [(i + 1) % VerticesNum]);
    }
    k0 = 2 * VerticesNum + 1;
    for (int i = 0; i < VerticesNum; i++)
    {
        PointF [] pts = new PointF [3] { ptVertices [i],
                          ptVertices [(i + 1) % VerticesNum], m_center };
        nodes [k0 + i] = new CoverNode (k0 + i, pts);
    }
    cover = new Cover (nodes);
}

Objects of the Polygon_Chatoyant class can be moved forward, reconfigured, resized, and rotated in similar way as was demonstrated with the triangles in the previous example. Three of the mentioned movements need a bit of preliminary actions; the corresponding methods of the Polygon_Chatoyant class are called from inside the OnMousedown() method of the form when a polygon is caught by mover.  These three methods need the number of the caught node and/or the mouse position at the starting moment.

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 Polygon_Chatoyant)
        {
            Polygon_Chatoyant poly = grobj as Polygon_Chatoyant;
            if (e .Button == MouseButtons .Left)
            {
                NodeShape shape = mover .CaughtNodeShape;
                if (shape == NodeShape .Circle)
                {
                    poly .StartReconfiguration (mover .CaughtNode);
                }
                else if (shape == NodeShape .Strip)
                {
                    poly .StartResizing (mover .CaughtNode, e .Location);
                }
            }
            else if (e .Button == MouseButtons .Right)
            {
                poly .StartRotation (e .Location);
            }
        }
    }
    ContextMenuStrip = null;
}

From the point of movement, there is no difference between triangles from the previous example and polygons, but there is a significant difference in the work of two forms. The Form_ChatoyantPolygons.cs is designed according to all the rules of user-driven applications. Not only the view and place of each polygon are decided by users but also the number of polygons and their order on the screen can be changed at any moment. While adding a new polygon, you can set all the colors. The only thing that I did not include into this example is the change of colors for existing polygons. This thing is demonstrated in the application accompanying the book; for example, look at the Form_ObjectsOnTabs.cs.

This is the first part of my article about the most often used graphical primitives and I included into this part only the primitives consisting of the straight lines. In the next part I am going to write about the primitives with the curved borders. Because I use only the nodes of three shapes – circles, rounded strips, and convex polygons – then the moving of objects with the curved borders required the use of special technique. The technique is really simple in use and for years I used it without any change at all. Then I understood that for the most often used objects with the curved borders – circles and rings – covers can be even simpler than for triangles or rectangles. If you are familiar with the N-node covers and don’t want to wait for the second part of this article, you can look at the chapter Simpler covers in the book World of Movable Objects.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here