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).
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 ();
}
}
}
}
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
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)
{
Cursor .Position = form .PointToScreen (Point .Round (ptA));
}
else if (iNode == 1)
{
Cursor .Position = form .PointToScreen (Point .Round (ptB));
}
}
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
{
Cursor.Position = form .PointToScreen (Point.Round (ptA));
}
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.
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;
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.
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);
}
… …
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;
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.
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:
- N circular nodes on the vertices.
- One circular node on the center.
- N strip nodes along the border; the strips
connect the neighboring vertices.
- 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.