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; double m_angle; float m_radius;
SolidBrush m_brush;
static int minRadius = 12;
static int minLength = 20; int delta = 3;
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.
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: 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: 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;
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.
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)
{
… …
nodes [4] = new CoverNode (4, ptsA, Behaviour .Transparent);
nodes [5] = new CoverNode (5, ptsB, Behaviour .Transparent);
}
else
{
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; … …
else {
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
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.
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)
{
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
{
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.
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 ()
{
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.
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.
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) {
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 {
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
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).
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.
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 (dx, dy);
}
… …
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) {
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) {
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
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;
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); 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.