Professional Documents
Culture Documents
World of Movable Objects
World of Movable Objects
Sergey Andreyev
Acknowledgments
The main algorithm for moving the screen objects and many examples which you can find in this book were developed at
the time when the author was a freelance scientist, had no obligations to any company or institution, and had a brilliant
opportunity to concentrate entirely on his ideas that resulted in the theory of movable objects and user-driven applications.
For more than a year I spent all the time in the library of Victoria University in Wellington surrounded by innumerable
books of its collection. The ideas and thoughts of many scientists from the past were not only hidden under the covers of
those books but filled the air of the library and turned it into a very special place to work on new ideas. The glass wall of
the library showed everlastingly changeable but always magnificent Wellington Bay and that was the best inspiration I
needed. (Though I have to admit that from time to time it was really hard to turn back from the outside view to the screen
of my computer.)
Significant part of the first version of this book was written later while the author worked as a senior researcher in the
Department of Mathematical Modelling in the Heat and Mass Transfer Institute of the Belarussian Academy of Sciences.
To work on that first version, I took a four month leave from the institute and found the best possible retreat and place for
thinking at the house of my old friends – Dave Glyer and Vicki Bier. Even my ability to ask strange questions at any hour
seven days a week did not destroy our friendship.
The first version of this book was released in November 2010. Next year and a half, whenever I thought that something
significant had to be added to the book, I would give such writing a higher priority than my every day work. The head of
my department – Stanislav Shabunya –looked at those changes of priorities with infinitive patience. At those periods when
I was not lost somewhere among the pages of this book, our discussions with Dr.Shabunya resulted in several interesting
scientific applications and some of them can be found in the Demo program accompanying this book.
I am also thankful to those “specialists” on interface design who, without even looking at the working user-driven
applications, continue to insist that the movability of the screen elements is nonsense and simply a heresy. (I am not sure
that those people would be happy to see their names mentioned in such a context, so let these specialists be anonymous in
this text.) Anyway, I am really thankful to these people because their aggressive aversion to the new ideas pushed me into
further thinking about better examples for demonstration, about better technique of turning all the screen objects into
movable and resizable, and about better explanation.
World of Movable Objects 1 (978) Contents
Contents
Introduction 5
About the structure of this book 8
Preliminary remarks 13
Three technical notes 16
Requirements, ideas, and algorithm 18
Basic requirements 18
Algorithm 19
Safe and unsafe moving and resizing 23
From algorithm to working programs 24
First acquaintance with nodes and covers 27
Node types 27
Solitary lines 33
Rectangles 38
Standard case - independent moving of borders 38
Standard case but with a possibility of disappearance 44
Rectangles with a single moving border 47
One movable side which can turn out a rectangle 49
Symmetrically changeable rectangles 51
Rectangles with the fixed ratio of the sides 53
Rotation 57
Circles 57
Polyline 61
Rectangles 65
Short conclusion to this chapter 70
Texts 71
Text_Horizontal – the simplest class of movable texts 71
Information on request 75
From ClosableInfo class to InfoOnRequest class 79
Back to unclosable information 80
Rotatable texts 80
Texts moved individually and by sample 86
Comments to points 92
Polygons 96
Regular polygons 96
Regular polygons that can disappear 103
Convex polygons 105
Triangles 108
Chatoyant polygons 113
Move, resize, reconfigure, and a bit more 113
Creating new polygons. Two versions of design. 122
Dorothy’s house 129
Curved borders. N-node covers 133
Circles, rings, and rounded strips 133
Circles 135
Rings 137
Strips 140
Arcs 148
Thin arcs 148
Wide arcs 150
Transparent nodes 152
Rings 153
Regular polygons with circular holes 156
Convex polygons with regular polygonal holes 158
Crescent 164
Sector of a circle 168
Non-resizable sectors 168
World of Movable Objects 2 (978) Contents
Groups 402
Ordinary panels 402
Ordinary GroupBox 405
Non-resizable groups 406
Resizable groups with dynamic layout 412
Dominant and subordinate controls 417
Elastic group 424
Interesting aspects of visibility 434
On movability 437
Tuning of the ElasticGroup objects 440
The basis of total tuning 441
Temporary groups 443
Arbitrary groups 447
Tuning of the ArbitraryGroup objects 454
Objects on tab control 456
Siblings 478
Rectangles 478
Circle composed of sectors 482
Slices of a pie 488
ElasticGroup class vs. dominant – subordinates relation 498
One more class with dominant – subordinates relation 508
Reference book on controls and groups 512
Short conclusion to this chapter 516
Useful objects 517
More filename viewers 517
Several words at the end of part one 528
User-driven applications 529
Selection of years 533
Personal data 539
Variations on the theme of “Settings” 550
Block diagram 562
Applications for science and engineering 582
The iron logic of movability 583
The Plot class 586
Movability of plots and subordinates 591
Visibility of plotting areas 600
Identification 602
Visibility of scales and comments 606
Tuning of plots, scales, and comments 607
Faster tuning of the plots 616
Analyser of functions 619
Typical design of scientific applications 626
DataRefinement application 634
Simple data viewers for TXT and BIN files 655
More about using texts 661
Graph manual definition 666
Design of tuning forms 674
Short conclusion to this chapter 684
Data visualization 685
Bar charts 686
Pie charts 691
Ring sets 694
Variety of possibilities among the variety of plots 696
The same design ideas at all levels 703
Back to the plots 708
Some interesting possibilities 714
Visualization of covers 714
One trick with controls 718
Controls with comments (general case) 723
A set of nearly independent controls 730
World of Movable Objects 4 (978) Contents
Introduction
Years ago, when I understood the cause of long stagnation in the development of scientific applications, I began to work on
the solving of this problem. The initial task was absolutely obvious to me at that moment and was very limited – I needed
movable plots in my applications. But from the very first moment when I saw the movement of a single plotting area I
understood that I had done something extraordinary. I knew that I had done something great but I did not understand how
big it was. At that moment I had no idea that by designing such an algorithm I opened a door into absolutely new world –
the world of movable objects. I was quickly improving the algorithm and using it with different objects in different tasks
and the exact understanding that this was really another infinitive world came somewhere two or three months later. For the
next several years I worked like an explorer of the uncharted areas; each new expedition in any direction unveiled such
things about which I did not even think at the beginning.*
The book has a standard linear structure with the chapters going one after another. Any next chapter can use the
explanations from the previous part of the book and introduces something new. At the same time, very few real algorithms
have a linear structure; the structure of a tree is much more common in programming than anything else. Each new piece
opens the way for a new branch or even several branches of ideas. It would be nice to have the book with the same tree
structure, but I do not know how to write such a book, so I continue to write this one in a traditional linear way.
This book is about two things:
• The design of movable / resizable objects.
• The development of user-driven applications.
Two theories can be looked at as the independent things because:
• The design of movable objects is not influenced in any way by the afterthoughts of where these objects are going
to be used.
• The design of user-driven applications is independent of the real algorithm for constructing movable objects.
At the same time there is a very strong relation between two things because user-driven applications can be designed only
on the basis of movable / resizable objects and all the extraordinary features of such applications are the results of their
construction exclusively and entirely of movable objects. Movable / resizable objects can be used by themselves in the
existing applications of the standard type, but only invention of an algorithm of turning an arbitrary object into movable /
resizable allowed to design an absolutely new type of programs – user-driven applications.
Often enough when I say that I have thought out an algorithm of making movable any screen object, a lot of people are a bit
(or strongly) surprised: “What are you talking about? Is there anything new in it? We have seen objects moving around the
screen for years and years”. Certainly, they saw; as the demonstration of moving objects has the history of several decades.
Only all those objects were and are moved according to scenario written by their developers. Those screen objects move
not as users want them to move but as developers ordered them. Users can do nothing about such moving except watching.
The thing I was working on for years and which I am going to describe here is absolutely different: it is the moving of
screen objects by the users of programs. This book is about the development of such screen objects, movements of which
are not predicted by designer of an application. It is not about a film developed on a predetermined scenario. It is about an
absolutely new type of programs – user-driven applications. In these programs all objects, from the simplest to the most
complicated and consisting of many independent or related parts are designed to be moved and resized only by USERS.
Objects are designed with these special features and then the whole control of WHAT, WHEN, and HOW is going to appear
on the screen is given to users.
I would like to mention beforehand that the overall behaviour of user-driven applications is so different from whatever you
experienced before that you can feel a shock or at least a great amusement at the first try. From my point of view, such
reaction from the people who are introduced to the user-driven applications for the first time will be absolutely natural. A
lot of people had the same feeling of a shock when, after years of work under DOS, they tried the Windows system for the
first time. By the way, the only visual difference of the Windows system from familiar DOS was the existence of several
movable / resizable windows (in comparison with a single one and unmovable) and the possibility of moving icons across
the screen. That was all! And even that was a shock. From the users’ point of view, the step from the currently used
programs to the user-driven applications is much bigger than that old step from DOS to Windows. In user-driven
applications EVERYTHING is movable and resizable and all changes are done not according to some predefined scenario
but by users themselves.
*
The whole situation is perfectly described by Clifford Simak in his marvelous story “The Big Front Yard”.
World of Movable Objects 6 (978) Introduction
Movability of elements is not simply an extra feature that is added to well known objects in order to improve their
behaviour. Technically (from the programming point of view), it is an addition of new feature, but it turned out to be not
simply a small addition to the row of other features. The movability of objects which are well known for years changes the
way of using these objects. The development of interface at the best possible level is still the requirement which is not
going to be eliminated, but at the same time each user receives a chance to change the view easily to whatever he wants.
The preferable look of an application, depending on the current task and many other things, can be different at one or
another moment even for the same user.
Throughout the whole history of programming we have a basic rule which was never doubted since the beginning and up till
now and, as a result, turned into an axiom: any application is used according to the developer’s understanding of the task
and the scenario that was coded at the stage of the design. After reading this, you will definitely announce a statement:
“Certainly. How else can it be?” Well, for centuries there was a general view, which eventually turned into an axiom, that
the Sun was going around the Earth. There were no doubts about it. Yet, it turned out to be not correct.
40 years ago the majority of programs were aimed at solving some scientific or engineering tasks and the overwhelming
majority of those programs were written in FORTRAN. I think that not too many readers of this book can explain or even
remember the origin of this name; I would remind that it stands for FORmula TRANslation. The main purpose of the
language was to translate the equations and algorithms into the intermediate notation which was, in its turn, translated into
the inner machine codes. The main goal of the language was to deal with formulas! Computers were big calculators and
nothing else. It was not strange at all that whatever commands were there for visualizing of data and results, those
commands were intermixed with commands for calculations. At that time nobody asked the questions about such
development of programs. The author of a program tried to write a set of instructions to turn formulas into the final results;
it would be nice to see some intermediate results if the calculations were long and complicated. The calculations were often
very long and complicated, so few extra operations for showing out intermediate values were incorporated into the block of
calculations just at the places where those values were obtained.
Years later much better visualization was achieved both with the hardware improvement and the design of new languages.
At some moment there came the understanding that calculations and visualization had to be separated. They were separated
from the point of programming, but the same person – developer – was (and still is) responsible for everything. A
developer knows all the insides of calculations and he decides what, when, and how to show. Eventually this developers’
dictate came into conflict with the wide variety of users’ requests for visualization; the adaptive interface was thought out to
solve the problem. Many forms of adaptive interface were proposed (the dynamic layout is only one of its popular
branches), but all those numerous solutions are only softening the problems but not solving them. The main defect of the
adaptive interface is in its own base: the designer puts into the code the list of available choices for each and all situations he
can think about. Users have no chances to step out of the designer’s view and understanding of any situation. It is the dead
end of programs’ evolution under the ideas which were proposed around 30 years ago.
With the movability of all the parts from the tiny elements to the most complex objects, there is another way of application
design, when a program continues to be absolutely correct from the point of fulfilling its main purpose (whatsoever this
purpose is), but at the same time does not work according to the predefined scenario and does not even need such a
scenario. To do such a thing, an application has to be developed not as a program in which whatever can be done with it has
to be thought out by the developer beforehand and hard coded; instead an application is turned into an instrument of solving
problems in particular area. An instrument has no fixed list of things that can be done with it but only an idea of how it can
be used; then an instrument is developed according to this idea. User of an instrument has full control of it; only user
decides when, how, and for what purpose it must be used. Exactly the same thing happens with programs that are turned
into instruments.
I call the programs, based on movable / resizable objects, user-driven applications. When you get a car, you get an
instrument of transportation. Its manual contains some suggestions on maintenance, but there is no fixed list of destinations
for this car. You are the driver, you decide about the place to go and the way to go. This is the meaning of the term user-
driven application: you run the program and make all the decisions about its use; a designer only provides you with an
instrument which allows you to drive.
The first half of this book is about the design of movable objects. I have already mentioned the first misunderstanding of
the importance of this task; misunderstanding based on not realizing the difference between the moving according to the
predefined scenario (it is simply an animation) and the moving of objects according to user’s wish. But when I explain the
obvious difference between these two things, I often hear another statement. “Everyone can move and resize windows at
any moment and in any way he wants, so what is the novelty of your approach?” The answer is simple but a bit longer than
on the first question.
World of Movable Objects 7 (978) Introduction
Rectangular windows are the basic screen elements of the Windows operating system.* You can easily move all these
windows, resize them, overlap them, or put them side by side. At any moment you can reorganize the whole system of
screen windows to whatever you really need. It was not this way at the beginning of the computer era; it became the law
after Windows conquered the world. This is axiom 1 in modern day programming design: On the upper level, all objects
are movable and resizable. To make these features obvious and easy to use, windows have title bars by which they can be
moved and borders by which they can be resized. Being movable and resizable are standard features of all windows and
only for special purposes these features can be eliminated.
Usually the goal of switching on the computer is not to move some rectangular windows around the screen but to do
something special in applications which are represented by those windows. You start an application you need, you step
onto the inner level, and then everything changes. Here, inside the applications, you are doing the real work you are
interested in, and at the same time you are stripped of all the flexibility of the upper level – you can do only what the
designer of the program allows you to do. The design can be excellent or horrible, his skills can influence the effectiveness
of your work in different ways, but still it is awkward that users are absolutely deprived of any control of the situation.
Have you ever questioned the cause of this abrupt change? If you have, then you belong to the tiny percentage of those who
did. And I would guess that the answer was: “Just because. These are the rules.”
Unfortunately, these ARE the rules, but rules are always based on something. The huge difference between the levels is that
on the upper level there is only one type of objects – windows, while on the inner level there are two different types:
controls inheriting a lot from windows and graphical objects that have no inheritance from them and that are absolutely
different. The addition of these graphical objects changes the whole inner world of the applications. (In reality, whatever
you see at the screen is a graphical object, but as it was declared in the famous book [1] decades ago: “All animals are
equal, but some animals are more equal than others”. Controls are those “more equal” elements on the screen and their
behaviour is absolutely different from the behaviour of ordinary graphical objects.)
The inheritance of controls from windows is not always obvious as controls often do not look like windows. Controls have
no title bars, so there is no indication that they can be moved; usually there are no borders that indicate the possibility of
resizing. But programmers can easily use these features of all the controls and from time to time they do use them, for
example, via anchoring and docking. The most important thing is not how controls can be moved and resized, but that for
them moving and resizing can be organized, though I have to mention that the programmers use this moving and resizing of
controls as their secret weapon and never give users direct access to it. The designer decides what will be good for users in
one or another situation, and, for example, when user changes the size of the window, then the controls inside can change
their size and position but only according to the decisions previously coded by the developer.
Graphical objects are of an absolutely different origin than controls and, by default, they are neither movable nor resizable.
There are ways to make things look different than what they are in reality (programmers are even paid for their knowledge
of such tricks). One technique that programmers use is to paint on top of a control: any panel is a control, so it is resizable
by default; with the help of anchoring / docking features, it is fairly easy to make an impression as if you have a resizable
graphics which is changing its sizes according to the resizing of the form (dialog). Simply paint on top of a panel and make
this panel a subject of anchoring / docking. By default, panels have no visible borders, and if the background color of the
panel is the same as the background of its parent form, then there is no way to distinguish between painting in the form or
on the panel which resides on it. Certainly, such “resizing” of graphics is very limited, but in some cases it is just enough;
all depends on the purpose of application. Another solution for resizing of rectangular graphical objects is the use of bitmap
operations, but in most cases it cannot be used because of quality problems, especially for enlarging images. Both of these
tricky solutions (painting on a panel or using bitmap operations) have one common defect – they can be used only with the
rectangular objects.
If any limited area is populated with two different types of tenants (in our case – controls and graphical objects) which
prefer to live under different rules, then the only way to organize their peaceful residence and avoid any mess is to force
them to live under ONE law. Because graphics are neither movable nor resizable, the easiest solution is to ignore these
controls’ features as if they do not exist. That is why so few applications allow users to move around any inner parts. Thus
we have axiom 2: On the inner level, objects are usually neither movable nor resizable. Interestingly, the combined use of
these two axioms creates this absolutely paradoxical situation:
• On the upper level which is not so important for real work any user has an absolute control of all components and any
changes are done easily.
*
There are other multi-window operating systems which also use rectangular windows as the basic element. When I write
about moving of the windows, it is applied not only to the particular Windows system from Microsoft, but to the whole
class of multi-window operating systems. So, in further text, I will not add “and other similar operating systems” every
time when I mention Windows.
World of Movable Objects 8 (978) Introduction
• On the inner level which is much more important for any user because the real tasks are solved here users have nearly
no control at all. If they do have some control, then it is very limited and is always organized indirectly through some
additional windows or features.
Axioms that I mentioned were never declared as axioms in a strict mathematical way; at the same time I have never seen,
read, or heard about even a single attempt to look at this awkward situation any other way than as an axiom and to design
any kind of application on a different foundation. Programmers received these undeclared axioms from Microsoft and have
worked under these rules for years without questioning them. If you project the same rules on your everyday life, it would
be like this: you are free to move around the city or country, but somebody tells you where to put each piece of furniture
inside your house. Would you question such a situation?
When I began to publish articles about my algorithm of movability and about user-driven applications, it was nearly
impossible to convince any reviewer that the movability of all the screen objects is the thing that must be implemented as
quickly as possible. At that time, we had only the realm of personal computers to discuss and that kingdom was absolutely
ruled by adaptive interface and dynamic layout. Under such ruling, the movability of screen objects is not needed at all
(“smart” CEOs think so), and it was not of big surprise to me when reviewers one after another declared such movability a
heresy. Now we see absolutely different situation on neighbouring territories (cellular phones, smartphones, tablets, and so
on); it is impossible to ignore the processes which go there, but some “specialists” continue to insist that the future of
interface for PCs is with dynamic layout.
Mobile phone systems began to spread at nearly the same time as PCs. Soon mobile phones also used the same idea of
menus and submenus but small screen sizes and low resolution put a constraint on its use. Throughout the last years we see
technical revolution in small devices and this caused a real revolution in their interfaces. Now technical characteristics of
small devices bring them much closer to personal computers, but their designers are smart enough not to copy the interface
from PC. Whenever possible, the touchscreens are used or programs that simulate the touch screens. The system of
available commands is not too wide and the main instrument – a finger – is not too accurate but good enough to organize the
direct manipulation of all the screen elements. User can click an object to open the associated program, scroll the surface,
change the view scale, or push an object in the direction of its needed move. It is definitely a small subset of what can be
done on PCs, but there is one common feature in all available actions: users deal directly with the screen objects and this
became the main interface idea on all portable devices. On all portable devices … but not on personal computers!
In general the screens of our PCs are not touch screens but this is not even needed. If all the screen objects in our programs
are movable then with the help of ordinary mouse we turn all the screens into the touch screens of extremely high resolution
where we can directly manipulate all the screen elements of any size and origin. This changes our work with all the
programs and this is going to be a real revolution on PCs.
Let us return to the PC world with which you are familiar.
So, in the world of programs we have a situation when users have to do their most important work inside the applications
while being deprived of the real control of these applications; the work can go on only according to the previously
developed scenarios. (The questions of whether any form of adaptive interface can really solve the problems are discussed
at the beginning of the second part of this book.) I realized years ago that immovability of all elements inside the
applications became the main problem in design of many programs but especially in complicated ones. My goal was to find
a general solution which would allow to move and resize objects of an arbitrary shape. I had been looking for the general
solution and I found it. This book is about the algorithm, main solutions, and some results.
Chapter starts with an application with which everyone is familiar – Calculator. How to transform an
existing application into user-driven? How much efforts are needed? What changes have to be done on the
programmers’ side? Is it difficult for users to switch from the old style programs to the new one?
Though the discussion of one example of this chapter appears closer to the end of the book, but the example
itself – Book by chapters and examples – can be very helpful throughout the reading of this book.
One more example – Family Tree – has an outstanding series of small preliminary examples (12 of them!) to
demonstrate all steps and ideas of design. According to my estimation, this example generated the highest
interest from readers of my articles and book.
The last example in the book includes graphical elements of all possible shapes and the ideas of the best
group design (as I understand them) which were demonstrated throughout the book but also adds some new
features which were not used before.
Chapter 22 Getting rid of controls
This chapter was added at the beginning of 2016 and was placed at the end of the book because… it is the
worst place for this chapter. In reality the results of this chapter can be (and must be) used in nearly all
previous examples.
This chapter demonstrates the graphical analogues for the most popular controls: push buttons, labels,
numeric up-down controls, combo boxes, check boxes, and groups of radio buttons. There is not a single
control in all the examples of this chapter and the best place for this chapter would be not at the end of the
book but much earlier somewhere in the first part. Unfortunately, the appearance of this chapter in the first
part would not only require the use of these graphical “controls” in the majority of the book examples but
might require the restructuring of the whole book and I am simply not ready to start it right now (May 2016).
All new classes of this chapter are designed with the use of adhered mouse technique. There is also a good
example of tuning form design.
In September 2016 the revised and improved version of this chapter was included into the new book
Elements of Total Movability. After it I tried to make similar changes in the current chapter but I think that
the new book contains better version.
In November 2016 the Elements of Total Movability was rewritten once more and the accompanying
program was also improved. I try to install those improvements into the examples of this big book but can
do it only partly. It became absolutely clear to me that new results demand the serious change of the whole
book. This is going to be the next step.
Book includes the summary of rules for design of movable objects and development of user-driven applications. There are
also references and links to other programs and documents which were developed for better explanation of these items. In
addition, there are four appendices.
Appendix A Examples (forms) used in accompanying application
There are two tables in this appendix. First table includes all examples from Demo application in
alphabetical order. For each example, there is a small figure, short description, and the names of classes
from MoveGraphLibrary.dll which are used in current example.
Second table includes the same type of information but about the forms from MoveGraphLibrary.dll.
These tables are very helpful in obtaining some short information about each form and in navigating to the
place in the book where the needed form is discussed in details, but the number of examples is so big (more
than 180) that even these tables occupy 40 pages. There is a separate file –
BookExamplesInOrder_Figures.doc – which includes only the small figures of all examples (forms) in the
same order in which those examples appear in the book.
Appendix B Use of classes from MoveGraphLibrary.dll
Includes the table of classes from MoveGraphLibrary.dll with the names of examples from the Demo
program where these classes are used. For each example, there is the page number where this example is
discussed.
Appendix C Classes designed for Demo application
There are so many examples in this application that readers are going to be lost among them. Two lists help
to navigate through those examples. One list contains the names of examples and classes which were
especially designed for these examples (shapes of objects, names of classes, and their main features).
Another list gives the same information but in different order: for each popular shape, there are names of
classes and their features.
World of Movable Objects 12 (978) Introduction
While working on the new version of this book, I tried to enforce more strict system of class names and thus
I had to rename a lot of previously designed classes. It can be a bit confusing for users who are familiar with
previous versions of Demo application when they try to find the same classes in the new version and do not
see them. For such readers, I prepared the list of renamed classes.
Appendix D Classes renaming in Demo application
List shows the renamed classes of objects. Maybe there were also two or three renamed examples, but I did
my best not to do it.
To run an application accompanying this book only two files are needed: WorldOfMoveableObjects.exe and
MoveGraphLibrary.dll.
Some examples need one or two auxiliary files. For example, a small application with a labyrinth for dachshund uses two
images of this dog; those images are stored in two files in the …\WorldOfMoveableObjects\bin\Release subdirectory.
Application can work without those files but it is more interesting with them. Some engineering applications from
chapter 18 cannot demonstrate anything without data source files; those files are also included into subdirectories.
Many examples from Demo application are designed in a very simple way in order only to explain one or another feature.
Some examples are the simplified versions of real applications and there are also several examples of working applications
without any changes at all or with minimal changes. Codes for all parts of Demo application are available and you are free
to use any parts of them in your own programs.
World of Movable Objects 13 (978) Introduction
Preliminary remarks
When any well known object gets new features, users should be aware of these new things. The goal of my work is to
develop an easy to use algorithm for turning ANY screen object into movable and resizable. All these innumerable objects
were developed at the best level by their designers, and I do not want in any way to interfere in their design. The
appearance of objects must be exactly the same as it was thought out by their designers. But if there is no visual indication
of the new features, then how users will understand that objects on the screen are movable and resizable? Users need to
know this fact! Try not to exclaim any astonishment or indignation on reading the previous sentence. I only want to remind
you that there is no indication that windows on the upper level can be moved and resized; everyone should know this fact
and this knowledge is enough to navigate throughout the Windows or similar systems. There were no such things before the
Windows era, so everyone who was going to use it for the first time had to be told beforehand that all those windows could
be moved and resized and how he was going to do it. Some people are old enough to remember what was before Windows
and at the same time are not old enough to forget it; the younger generation may think that they are already born with the
knowledge of movability of windows, but I have a feeling that even they have to be informed about this fact at one moment
or another.
After this reminder, I think that the fact that users of user-driven applications have to be informed at least once about the
movability of each and all objects is not an outrageous one.
Now, if you are aware that all the objects on the screen are movable, then how are you going to move them? Mouse seems
to be an obvious and the best instrument because there are programs in which one or another element is moved and this is
always done with a mouse. Again, the moving of windows is the first example which comes to one’s mind. But there is a
big difference between the moving of windows on the upper level and my algorithm for all kinds of elements. Each
window has a title bar by which it is moved. This decision seems to be good enough because all windows have the standard
rectangular shape. I propose the algorithm which works for the objects of an arbitrary shape, so my vision is different:
objects must be moved by any inner point. If any object is moved by any inner point, then there is no need at all for a
special indication of its movability. You simply should know this fact and that is enough.
If there is no indication of the movability of an object, then how are we going to decide whether an object is movable or
not? We do not need to do it: all objects must be movable. I hope that this time you contained your indignation. Have
you ever had doubts about the movability of any particular window? So all the objects in the Demo application
WorldOfMoveableObjects are movable; if any object in this application is not movable, then it is done purposely for better
comparison and explanation.
The movability of all elements is the basis of user-driven applications. I have heard the complaints about the underlined
statement but only from the people who never tried the applications based on movable / resizable elements. (The famous
outcry that “driving the car faster than 15 miles per hour is dangerous for animals and people and thus must be forbidden”,
that outcry came only from those who never tried the car but not from those who had ever made a single car trip.)
There are a lot of different objects in our applications; these objects are supposed to be involved in different types of
movements. If there is no indication of any movement, then how users are going to understand in what movements an
object can be involved? This is one of the problems which are mentioned in the examples which I am going to demonstrate
and discuss in further chapters.
Figure I.1 demonstrates the first view which you see on starting the WorldOfMoveableObjects application. All shown
objects are movable and all of them, except texts, are resizable. All objects which you see in this form are discussed in
details in one or another chapter of the book. I think that a quick look across this picture from one object to another may
give you a better understanding of what you are going to find further on. It is like a scheme at the entrance of a big
museum. Only instead of the “Do not touch the exhibits”, you have “Any object can be moved by any inner point”, so I do
not need to repeat it for each of them.
Chatoyant polygon in the middle Configuration of this polygon can be changed by moving the central point or any end
point of any segment of the perimeter. By changing the configuration you can literally
turn the figure inside out. All other points of the perimeter, except the apices, can be
used for scaling the polygon. The polygon can be rotated (right mouse press) by any
inner point. Class ChatoyantPolygon is discussed in the chapter Polygons.
Group in the top right corner This group consists of controls paired with comments. Any control is moved by the
frame; its comment moves synchronously. Comments can be moved independently and
placed anywhere. Class CommentedControl is discussed in the chapter Control +
graphical text. A set of elements constitutes the group. The frame of the group adjusts
to all changes of inner elements and is always shown around them. The title of the
World of Movable Objects 14 (978) Introduction
group can be moved left and right between two sides of the group. Class
ElasticGroup is discussed in the chapter Groups and is used in many examples.
Fig.I.1 The first view which you see on starting the application
Plot in the bottom left corner The main plotting area is resized by borders and corners. The scales can be moved
individually. There must be an obvious conformity between the scales and the main
plotting area, so vertical scale can be moved only left and right while horizontal scale can
be moved only up and down. Comment belongs to the CommentToRect class; this
class is discussed in the chapter Complex objects. Classes Scale and Plot are
discussed in the chapter Applications for science and engineering.
Ring Ring can be resized by any point of the outer or inner circle; resizing of rings is discussed
in the chapter Curved borders. N-node covers. Ring cover uses node of a special type
which is discussed in the chapter Transparent nodes. This ring has sliding partitions
which allow to change sector angles. Rings with sliding partitions are first discussed in
the chapter Finishing strokes on graphical primitives.
Information at the bottom This text can be moved but not rotated. Class Text_Horizontal is discussed in the
chapter Texts.
Words across the form These words can be moved and rotated by any point. Class Text_Rotatable is
discussed in the chapter Texts.
House House can be resized by all four sides and by all four corners of its rectangular part. The
roof top can be moved not only up or down but also to the sides (no requirement for the
roof symmetry). Class House_Simple is not used in other examples, but there are
similar classes of houses in the chapter Medley of examples.
World of Movable Objects 15 (978) Introduction
There are two big differences between using objects of the same classes in the Form_Main.cs and further on. First, you
can move the objects in the Form_Main.cs and you can resize them but you cannot tune them. Second, the results of these
changes (new positions and sizes) are not saved for later use in this form. Design of programs without these two things is
against the rules of user-driven applications, but I decided not to introduce them here. Everything will come at a proper
time.
At figure I.1 there is a group with short information about the book, among other facts you can see that there are more than
180 examples in the book and accompanying program. The picture of the main form shows only a short menu with six
positions and this looks like the only way to all those examples. Yes, there is a whole system of submenus, so the way to
some of the forms (examples) goes through two or three menu levels; this is not very suitable if you want to go from one
example to another. Another and more serious problem is simply in the number of those examples; it would be impossible
or at least very difficult for users (readers of the book) to remember the exact way to one or another needed form. In order
to make the search for the needed examples easier, I added one more example (seriously, I am not joking) to the already
huge program. It is impossible to open in the working application all submenus simultaneously in order to get the whole
picture of all the possibilities, so I made it possible in the form of a block diagram which duplicates the view of all
submenus as if they are all opened at the same time. I will write about this example in the chapter User-driven applications,
but here is the view of the Form_BlockDiagram.cs (figure I.2). Certainly, this form is used not only to show the
information about the program in a better way: by double clicking on the appropriate line of any submenu you can call an
associated example, so it is another access to all of them. Choose whatever you prefer. This form is under the
Miscellaneous menu position; at figure I.2 it is in the top right corner.
Fig.I.2 Form_BlockDiagram.cs shows the view of all submenus as if they are all opened simultaneously. Any example
of this program can be called by a double click on its line in a submenu.
Starting from version 6.15, I added one more way to the examples of the book. It also looks like a block diagram
(figure I.3), but in this variant each block corresponds to some chapter of the book (the title works as a tip), while the inner
lines give links (through double click) to the examples which are used in the text of each chapter. This example can be
called through the second submenu position under Miscellaneous. The example Form_BookByChapters.cs is described in
the chapter Medley of examples; unfortunately, this happens closer to the end of the book. I think that using of this example
can be very helpful throughout the whole time of reading this book, so here are several words about the possible use of it.
The book is not small, its reading can take some time, and there are around 180 examples in the accompanying program.
Any part of the book contains clear information about the way to the currently discussed example, but this view gives a
better and much easier access to nearly all of them: simply look for the needed chapter and double click the inner line which
describes an example you want to look at. There is an easy way to change the visibility parameters of any block (through its
World of Movable Objects 16 (978) Introduction
context menu), so change the visibility parameters for the needed chapter and the next time it will remind you where you
have stopped reading the last time.
Fig. I.3 Each block in the Form_BookByChapters.cs corresponds to a chapter of the book and gives links to its examples
All blocks at figure I.3 are movable, so you can organize the whole view in any way you want.
In October 2012 I added an exercise-book Easy Tasks to be used together with this book. I cannot say exactly at which
moment users can start working on those exercises; each user has to decide it for himself. All exercises deal with the simple
graphical objects which are mostly discussed in the first chapters of the current book, but each exercise has to be developed
according to all the rules of user-driven applications. Into that exercise-book, I have included my versions of solving those
exercises, but it does not mean that your solutions must be similar. You can try to solve some of those exercises after
reading first several chapters of this book; if you see that you do not know yet how to design one thing or another, you can
return to those exercises later. The main thing is to get the feeling of user-driven applications and to use the rules of such
applications in your design not artificially but automatically.
Basic requirements
“In science, finding the right formulation of a problem is often the key to solving it…” [2]. As a designer of very
complicated systems, I know exactly what features the system of movable / resizable elements must have to be useful for
my work.
• I need an easy way to declare the objects in any form (dialog) movable and resizable; just touch an object and it
becomes movable. Such transformation was already demonstrated years ago [3]; maybe I can do the same?
• Easiness of transformation does not mean that it can be applied only to primitive objects. The configuration of objects
can be at any level of complexity; changing of configuration might be influenced by a lot of different things. For
example, some objects may allow any changes, others may need fixing of several parameters (sizes); parts of the
objects may generate restrictions on changes of other parts. But in any case the whole variety of possible
reconfigurations must be easy to understand and implement.
• These features – movable and resizable – must be added to an object like extra “invisible” features. They will not
destroy any image, but it will be obvious that objects are movable and resizable. The best thing would be simply to
know about these features without any additional indication. In some cases I may need some instrument for indication
of these new possibilities, but usually I would prefer to go on without any extra lines or marks.
• These features should not be an “all or nothing” case. Objects of the same classes can be used with or without these
new features; the decision of declaring an object movable / unmovable or changeable / fixed can be done by user while
an application is running.
• The whole technique of dealing with the movable / resizable elements must be extremely simple: press and move, press
and reconfigure, and even press and rotate, which is not organized for windows but can be very useful for many
graphical objects. If it is useful, it must be organized without any limitations.
Some of these “nice to have” features look like they conflict with each other (simple but with all the possibilities you can
imagine) and even provide alternatives (not visible and obvious), but I am not writing a book about the future of
programming in 2025. The biggest secret is that all these things are already designed and work now; a programmer can
apply these features to any object.
I constantly work with the C# language, so all the programs which I am going to mention here are written in this language.
All the code examples are written in C# and I am going to use some terms from this language. But the algorithm and the
designed classes are not linked only to C# – they can be easily developed with other instruments; I simply prefer to use C#.
All examples in this book are from the WorldOfMoveableObjects application; all the codes of this project are available
(see information in the section Programs and Documents).
The easiest way to move any object around the screen is to drag it, so only three mouse events are used for the whole
process: MouseDown, MouseMove, and MouseUp. No additional keyboard pressing is used for moving / resizing
anywhere throughout my applications. If an element is involved in forward moving, resizing, and reconfiguring, then
starting one of these processes is determined only by the place where an object is pressed by a mouse. A lot of elements are
involved both in forward movement and rotation; the distinction between them is made on the pressed button: I use left
button for forward movement, resizing, and reconfiguring, while rotation is done by the right button.
Applications are populated both with graphical objects and controls. Graphical objects can be of an arbitrary shape; the
proposed technique is especially designed for turning into movable / resizable the objects of any shape. Standard controls
have rectangular shape; this simplifies some problems. Very important things for turning objects into movable / resizable
can be automated in the case of controls. I will write about controls and groups of controls in the special chapters but
throughout the whole explanation, beginning from the simplest cases, you will see the movable controls and groups of
controls in the same forms where some features of moving / resizing the graphical objects are demonstrated. Now let us
turn to the algorithm.
World of Movable Objects 19 (978) Chapter 1 Requirements, ideas, and algorithm
Algorithm
My idea of making graphical objects movable and resizable is based on covering an object by a set of sensitive areas which
are used to start one or another type of movement. These simple sensitive areas are called nodes and belong to the
CoverNode class. The nodes are usually invisible, so there is no direct visual indication about this new but very important
feature of the screen objects – their movability. The nodes can be visualized and all the examples from the first several
chapters include an easy instrument of such visualization. This visualization of nodes is needed only for initial explanation
of the basic design; nodes visibility makes cover construction more obvious. Later, it is enough for users to know that all
the objects are movable and resizable; after understanding and accepting this fact, users do not need any more reminders.
Movement of any node is transferred into the real movement of the screen object with which it is associated. Four different
types of movement are considered.
• If the size of an object is not changed and an object is moved without any change of relative positions of its parts
and without rotation, then it is forward movement.
• If all the parts of an object are increased or decreased with the same coefficient, but the general shape is not
changed, then it is resizing.
• If movement of any part changes the relative positions of the parts and the general shape, then it is reconfiguring.
• If the general shape is not changed, but the whole object is turned from its original position, then it is rotation.
The first three movements (forward movement of the whole object, resizing, and reconfiguring) are started with the left
button press; the choice between these three movements is decided by the starting point and by the node which is pressed.
There are special areas to start reconfiguring (it depends on the object) and resizing (usually it is the border); at any inner
point of an object the forward movement of the whole object is usually started. The start of rotation is distinguished from
other three movements by using the different button (the right one). The rotation of an object usually starts at any point.
There are no limits on the size of the nodes, but their possible shapes are limited to three variants and are described by this
enumeration.
enum NodeShape { Circle, Polygon, Strip };
Circular node is defined by its central point and radius.
Polygonal node is described by an array of points representing its vertices. Polygon must be convex!
Strip node is defined by two points and radius. The strip node can be also looked at as a rectangle with two additional
semicircles on the opposite sides. Those two points are the middles of two opposite sides to which the semicircles are
added; the diameter of those semicircles is equal to the width of the strip.
In addition to the location, size, and shape, each node has a parameter which determines the shape of the mouse cursor when
the mouse is moved across this node. Usually the change of the cursor, while the mouse is moved from one object to
another and across the different parts of the objects, is the only prompt for users that they can grab an object under the
mouse and move it or change it in one way or another, so it is better to make the shape of the cursor informative and not
confusing. Throughout all further examples you will find out that, for example, if the rectangular object can be resized in
the horizontal or vertical direction, then the Cursors.SizeWE and Cursors.SizeNS are used over its borders.
The resizing possibilities for objects of an arbitrary shape are usually signaled by the Cursors.Hand; if the whole
object can be moved in any direction, then it is often signalled by the Cursors.SizeAll.
Each node has one more very important parameter which describes its overall behaviour. The possible values of this
parameter are described by the Behavior enumeration; I will return to this parameter a bit later.
An array of nodes covering an object is called a cover (class Cover). Any cover must include at least one node. There are
no other restrictions on the number of nodes, their types, or sizes. The relative positions of the nodes are not regulated at
all. If you want an object to be movable by any point, then the whole area of object must be covered by the nodes; any gaps
in such situation will result in the appearance of the places by which an object cannot be moved. The overlapping of the
nodes is not a problem at all. In many cases such overlapping is used for better design of covers and there are situations of
the curved borders when it is impossible to design a good cover without overlapped nodes. If the overlapped nodes are used
for different movements, then their order in the cover is very important as the decision about the possible movement is made
according to this order. Usually the number of nodes is small even for very complicated objects, but there are cases when a
significant number of nodes are needed. This often happens for the objects with the curved borders; covers of such type are
called the N-node covers.
World of Movable Objects 20 (978) Chapter 1 Requirements, ideas, and algorithm
Technically I saw two ways to add movable / resizable features to the objects: either to use an interface or an abstract class;
after trying both ways, I decided upon an abstract class. To turn any graphical object into being movable and resizable, it
must be derived from the abstract class GraphicalObject and three crucial methods of this class must be overridden.
public abstract class GraphicalObject
{
public abstract void DefineCover ();
public abstract void Move (int dx, int dy);
public abstract bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher);
For graphical objects, the cover is organized in the DefineCover() method. Controls are automatically wrapped by a
graphical object of the SolitaryControl class; for controls none of these three methods is needed as everything is
automated.
Each node in the cover has its personal identification number (from interval between 0 and the number of nodes in the
cover); this number is often used for the node identification when the decision about the movement is made in the
MoveNode() method. It will be shown in many examples of this book that the number of the node is not the only way to
make such a decision; the nodes of different shapes are often used to organize different kind of movements, so the shape of
a node can be as helpful as its number. The node number is the only way of an absolutely correct identification of the
touched node in the case when all or some of the nodes have similar shape.
Note. Though the whole technique of moving and resizing of objects is based on the idea of cover, the DefineCover()
method is usually the only place where you have to think about the cover! The rule is simple: organize the cover and forget
about it. There is one exception of this rule; it is mentioned several lines below in the explanation of the MoveNode()
method.
Move (dx, dy) is the method for forward moving of the whole object for a number of pixels passed as the parameters.
The drawing of any graphical object with any level of complexity is usually based on one or few very simple elements
(Point and Rectangle) and some additional parameters (sizes). While moving the whole object, the sizes are not
changed, so only the positions of these basic points have to be changed in this method.
MoveNode (iNode, dx, dy, ptMouse, catcher) is the method for individual moving of the nodes. This method
returns a Boolean value which indicates whether the required movement is allowed. In the case of forward movement, the
true value must be returned if any of the proposed movements along X or Y axes is allowed. If the movement of one
node results in relocation of other nodes, it is natural to call the DefineCover() method from inside the
MoveNode() method, and then it does not matter what value is returned by the MoveNode() method. The call for the
DefineCover() from inside the MoveNode() method may happen even for the forward movement when movement
of one node affects the relocation of other nodes and it usually happens with rotation when all the nodes must be relocated.
This is the exception of the previously mentioned rule that cover is not even thought about anywhere outside the method of
its definition.
Though the call of the DefineCover() method from inside the MoveNode() method looks like a very good idea, it
has a limitation of its own. In some cases, the movement of a node starts the resizing of an object, which in its turn
demands the change of the number of nodes in the cover (this depends on the cover design); in such situation the call to the
DefineCover() method from inside the MoveNode() method may cause a problem because in the new cover the
node with the same number (the one which was originally pressed and caught) may belong to the node with different rules
for moving. To avoid this problem, in such cases the cover is not changed throughout the resizing but only when an object
is released; a detailed explanation of such case is given in the chapter Curved borders. N-node covers.
MoveNode() method has five parameters:
int iNode Identification number of the node – the same number that was used in the
DefineCover() method on design of this node.
int dx Movement (in pixels) along the horizontal scale; positive number means the movement
from left to right. Use this parameter if you write the code for forward movement.
int dy Movement (in pixels) along the vertical scale; positive number means the movement from
top to bottom. Use this parameter if you write the code for forward movement.
Point ptMouse The mouse cursor position. For calculations of forward movement, I simply ignore this
parameter and use the previous pair (dx, dy); for calculations of rotation, I ignore the
World of Movable Objects 21 (978) Chapter 1 Requirements, ideas, and algorithm
previous pair and use only this mouse position; I found it much more reliable for
organizing any rotations.
MouseButtons catcher Informs about the mouse button which was used to grab an object. If by the logic of an
application the object can be grabbed by any mouse button, then ignore this parameter; if
the move is allowed by one button only, then use this parameter; in case the node can be
involved in both types of movement (forward movement and rotation), this is a very
useful parameter to distinguish between them.
The MoveNode() method can be the longest of all three methods because it must include the code for moving each
node, but the code of this method is usually simple as more often than not the code for different nodes is partly the same.
There are interesting situations when the MoveNode() method is very short though the number of nodes is big. For
example, the N-node covers often consist of a significant number of nodes, but for such covers the MoveNode() method
is usually very short because the behaviour of all those nodes is identical.
In currently used applications, you work with unmovable graphical objects and you deal with all of them as if they are
placed on the same single surface, but even in this case there is an order of drawing those objects. This order determines the
way you see these objects because those that are drawn later appear atop of those that were drawn before. When all the
objects are movable, they can be easily moved across the screen and they are often moved either above or underneath other
objects. The system of the screen objects becomes multilevel with each object occupying its personal level. This is like a
move from the old model of the world with all the starts fixed at predefined positions at the same sphere to the
understanding of the Universe’s infinity with each object having its personal distance from the Earth and moving in its own
way.
An object derived from the GraphicalObject class gets an ability to be moved and resized, but it can be really moved
and resized only if it is registered with the mover object of the Mover class. Mover supervises the whole moving /
resizing process and in addition can provide a lot of information associated with it. Regardless of the number of different
objects involved in moving / resizing, usually there is a single mover per form (dialog). However, there are situations when
it is better (easier and more reliable) to have several movers in the form; for example, when you have movable objects on
different panels or on pages of tab control. Situation with several movers is very rare; I’ll demonstrate it in some examples,
but usually there is only one mover in the form and this mover provides everything.
Only three mouse events – MouseDown, MouseMove, and MouseUp – are used for the whole moving / resizing
process. For each event its own method is written and these are the methods in which mover works and must be mentioned.
To organize the moving / resizing of the objects in the form, several steps must be made.
1. Declare and initialize a Mover object.
Mover mover;
mover = new Mover (this);
2. Register with the mover all the objects that you want to move and / or resize.
mover .Add (…);
mover .Insert (…);
3. Write the code for three mouse events.
private void OnMouseDown (object sender, MouseEventArgs e)
{
mover .Catch (e .Location, e .Button);
}
private void OnMouseUp (object sender, MouseEventArgs e)
{
mover .Release ();
}
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
}
This is not a simplification; this is the code which you can see in nearly every form of the Demo application regardless of
the number or complexity of the objects involved in moving / resizing. The calls to three mover methods (one call per each
World of Movable Objects 22 (978) Chapter 1 Requirements, ideas, and algorithm
mouse event) are the only needed lines of code! Further on you will see some additional lines of code inside three methods
for mouse events, but they are used only for some auxiliary things like dealing with helpful information, changing the order
of objects on the screen, or calling different context menus on different objects.
These three mouse events – MouseDown, MouseMove, and MouseUp – are the standard and often the only places
where a mover is used. There are two other events – MouseDoubleClick and Paint – where mover can be
mentioned and used, but this happens only on special occasions.
I often use the MouseDoubleClick event for opening the tuning forms of the complex objects, for example, scales and
different plotting areas. Without mover at hand (before implementing the moving / resizing of objects), I had to decide
about the clicked object by comparison of the mouse location and the boundaries of the objects. Mover can do this job
much better; mover informs not only about the occurrence of any catch but also about the class of the caught object, its
order in the queue, and other useful things. Any object derived from the GraphicalObject gets a unique identification
number; this id helps to identify an object when there are several or many objects of the same class.
For example, in case of many identical rounded strips on the screen, double click with the left button on any of them brings
the pressed strip on top of all others. An element to be brought on top is identified through the id number.
private void OnMouseDoubleClick (object sender, MouseEventArgs e)
{
if (mover .Release () && e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is Strip_SimpleCoverAdh)
{
PopupFigure (grobj .ID);
}
}
}
private void PopupFigure (long id)
{
for (int i = strips .Count - 1; i > 0; i--)
{
if (id == strips [i] .ID)
{
Strip_SimpleCoverAdh elem = strips [i];
… …
Inside the Paint event, mover can be mentioned on those rare occasions when covers have to be visualized, but it is really
a rare thing as good examples of design are those which do not require such visualization.
private void OnPaint (object sender, PaintEventArgs e)
{
mover .DrawCovers (e .Graphics);
… …
There can be a lot of different situations for moving / resizing of the stand alone screen objects and there are even more
variants when you have a set of objects on the screen; these objects can be independent of each other or be in some kind of
relations. Those objects can be placed in an arbitrary way to each other; they can stay apart from each other or overlap.
Objects can be involved exclusively in individual movements, or they can move synchronously, or influence each others
movements in different ways. Objects can be solid, or they might have some holes through which the underlying objects
can be seen. If the underlying object is seen through the hole in the upper object, then you would expect that that
underlying object can be grabbed for moving (“caught”) through the hole in the upper object. By allowing such things, the
objects on different levels can be moved around and resized without changing their levels. Any type of these numerous
movement possibilities originates from the movement of some node; for this purpose any node has one more very important
parameter which describes the main feature of its behaviour. The possible values of this parameter are defined by the
members of the Behaviour enumeration*.
*
This parameter was used beginning from the very first version of my movable graphics, but throughout the years its main
idea and the use in different situations have changed. At the beginning, it was used only for describing the possibility of
movement, so the enumeration was called MovementFreedom. From the very first version, there were four members in
that enumeration but two of them turned out to be redundant as the difference in the node behaviour was defined not by this
World of Movable Objects 23 (978) Chapter 1 Requirements, ideas, and algorithm
enum Behaviour { Nonmoveable, Moveable, Transparent = 4, Frozen };
To be involved in moving / resizing, all objects of the form are registered in the mover queue. While making the decision
about the possibility of catching any object, the mover checks the covers according to their order in the queue; the cover of
each object is analysed node after node according to their order in the cover. When the first node containing the mouse
point is found, then there are several possible reactions depending on the Behaviour parameter of this node:
Behaviour.Nonmoveable Object is unmovable by this point. At the same time such node does not allow mover to
look anywhere further; all other nodes and objects at this point are blocked from mover.
The analysis is over, try another point.
Behaviour.Frozen Object under the mouse cannot be moved by this point but it is recognized by the mover
as any other object, so, for example, the context menu can be easily called for it.
Behaviour.Moveable The possibility of movement is decided by the MoveNode() method of this object
according to the number or shape of the node and the movements restrictions, if there
are any.
Behaviour.Transparent Mover skips this and all the following nodes of the same cover and continues the
analysis of the situation from the next object in its queue.
parameter but by the code in the MoveNode() method of an object. At the same time, the new and very interesting
possibilities of the nodes behaviour were added to the algorithm. This new possibilities were described by the new
members which were added to the same enumeration. After the exclusion of the redundant members of the enumeration
and adding new their total number is still four. But with this change of the enumeration members, I felt more and more that
the enumeration name became very confusing. Beginning from version 6.08, I decided to rename this enumeration, so that
its new name – Behaviour –describes its purpose much better. The change of the enumeration name required to change
the names of some methods in the Cover and CoverNode classes; otherwise the names of those methods would have
no sense at all. These changes are mentioned in the MoveGraphLibrary_Classes.doc in the section Important changes.
World of Movable Objects 24 (978) Chapter 1 Requirements, ideas, and algorithm
public enum Clipping { Visual, Safe, Unsafe };
• Visual – elements can be moved only inside the client area.
• Safe – elements can be moved from view only across the right and bottom borders of the form.
• Unsafe – elements can be moved from view across any border.
Different Mover constructors set different levels of restrictions on moving objects. Constructor without any parameters
sets the Unsafe level of clipping but it is used extremely rarely: there are only few examples in the Demo application
which use this constructor. All other constructors set the Visual level of clipping. There are no constructors which set
the Safe level, but this level is used in many applications; the needed clipping level can be changed at any moment by
the Mover.Clipping property.
When the Clipping.Visual level is used, it does not mean that any object is always fully in view. The clipping is
organized not for objects but for the mouse when it has grabbed an object. Under such clipping the mouse cannot go
outside the visible part of the form, so this guarantees that an object cannot be entirely moved beyond the borders. Any part
of the caught object can cross the border; it can be a small part or nearly a whole object which goes out of view, but the
mouse which is stuck to some point inside an object cannot cross the border, so, regardless of where the movement ends,
some part of an object is still left in view. By pressing this part, though it can be very small, an object can be returned back
into full view.
I want to underline once more the difference between the well known mouse clipping which is used for decades and this
Mover.Clipping.
• Standard mouse clipping limits mouse movements even for a free mouse not attached to any object.
• Mover.Clipping limits movements of the mouse only in situation when any object is caught and mouse tries
to steal it out from visible area. When the mouse is released, there is no more clipping of this type and mouse can
be moved anywhere.
When the Clipping.Visual level is used, it does not mean that an object cannot find itself behind right or bottom
border. Even if an object cannot be moved across borders, those borders can be moved across objects! (You may not like
the rules on the other side of your country border and you may have no wish to be on the other side, but neighbouring ruler
may have different views on your property and different plans about its future…There are crazy rulers in the world…)
The unlimited shrinking of an object can be another cause of its disappearance. To avoid this, the minimum sizes must be
declared for any class of resizable objects; these restrictions are used in the MoveNode() method to check the possibility
of the proposed movement. The restriction on minimum size of objects is used in the majority of classes which can be seen
in the Demo application. However, there are two other possibilities which are required from time to time; both of them are
also demonstrated in Demo application.
The first approach is to allow the shrinking of an object to a tiny size or even to total disappearance. The shrinking of an
object to the size less than the predetermined minimum is considered as the user’s command to delete this object from view;
so the object is removed.
In some very rare situations an object can be squeezed to a tiny size or even disappear from view but continue to exist; there
is still a possibility to grab this invisible object and increase it, thus making it visible again. This approach is used only on
those rare occasions when there are several objects side by side and still visible neighbours inform users that the empty
space between objects is not absolutely empty, but there exists an invisible object which can be found and caught by the
mouse.
Technical note. To avoid the screen flicker throughout the moving / resizing of the screen objects, do not forget to switch
ON the double-buffering in any form where you use the moving / resizing algorithm. It has nothing to do with the described
technique, but it is a nice feature from Visual Studio. Moving and resizing can be applied not only to the objects inside any
form but also to those which are placed on panels or tab controls. Unfortunately, Visual Studio does not allow to avoid
flicker on panels and tab controls in the same easy way as in forms. To avoid flicker in those situations, the modified
(derived) classes must be used. It is easy to do; to simplify it even more, I included such classes into the
MoveGraphLibrary.dll; several examples demonstrate the use of these classes.
In every form you will see different objects involved in different types of movement. In addition to movements, there is
often the reordering of objects to change their appearance on the screen; there is also calling of different context menus on
different classes of objects or their parts. All these things are started by the mouse, and it is obvious that it would be not
enough to take into consideration only the mouse button which starts one or another action. For example, the forward
movement of objects (plus their resizing and reconfiguring) and putting an object on top of others are all started with the left
button. Rotation of objects and calling of context menus are started with the right button, so something else must be
considered in addition to mouse button to distinguish all these possibilities.
To distinguish one command from another, I often pay attention to the distance between the points of MouseDown and
MouseUp events. In the ideal case, the decision must be based on having zero or not zero distance between two points,
but the request of pressing and releasing mouse at exactly the same point and not moving it even for a single pixel between
two events can be too strong for a lot of users, so I consider the move for “not more than three pixels” as “not moved”.
(Three pixels for “not moved” decision is my own suggestion; this number can be easily changed in either way.)
Implementation of this rule in my programs results in such system of decisions:
Left button If the mouse is not moved between two events, then it is a command to popup an object; otherwise it is
simply a move of an object.
Right button If the mouse is not moved between two events, then it is a menu call; otherwise it is a rotation.
When an object is caught, moved, and released, then a sequence of events in which it is involved is this one: MouseDown –
MouseMove – MouseUp. Throughout all three events the mover plays the main role as the organizer and conductor of the
whole process, but I will start some explanations of the role which mover plays from the last stage.
A lot of actions are decided inside the OnMouseUp() method after the Mover.Release() call. First, this method
informs by its returned value whether any object was released or not. If any object was released, then it would be very
helpful to have more information on this object. The Release() method has variants which inform about the order of
released object in the queue (iObject), the number of released node in the cover (iNode), and the shape of this node
(shape).
bool Release ()
bool Release (out int iObject)
bool Release (out int iObject, out int iNode)
bool Release (out int iObject, out int iNode, out NodeShape shape)
Even if the first of these variants is used, the order of released object in the queue can be obtained with the
Mover.ReleasedObject property
int iInMover = mover .ReleasedObject;
When the order of object in the queue is known, it is easy to get the object itself.
GraphicalObject grobj = mover [mover .ReleasedObject] .Source;
There is an alternative way to find the same released object; it uses not the order of object in the queue but another property.
GraphicalObject grobj = mover .ReleasedSource;
Any object derived from the GraphicalObject automatically gets unique identification number at the moment of
initialization. The ID property of the base class allows to get this number for any object at any moment and to use this
number for identification of objects. This is widely used when you need to find what object was pressed, to what class it
belongs, and so on.
Mover is the conductor of the whole moving / resizing process for all the objects in its queue. As all objects are movable
and usually there is only one mover in the form, then all objects are registered in the same queue and mover supervises all
objects in the form. To some extent mover can be associated with the mouse, as it catches the objects by the mouse, moves
them with the mouse, and then releases them at the place to which the mouse was moved. When the mouse is moved
around the screen, then a lot of information can be obtained inside the OnMouseMove() method from the
Mover.Move() call and several properties of the Mover class. The return value of the Mover.Move() method
tells if any object is moved at the moment or not. The link between the mover and the mouse is especially strong during the
moving process because the only parameter of the Mover.Move() method is the mouse position.
bool Move (Point ptMouse)
The same set of data about the caught object can be needed throughout moving: the order of object in the queue, the number
of node by which this object is moved, and the shape of this node. This information is obtained from three properties.
World of Movable Objects 26 (978) Chapter 1 Requirements, ideas, and algorithm
int iObject = mover .CaughtObject; // Gets the index of the caught element.
int iNode = mover .CaughtNode; // Gets the number of the caught node.
NodeShape shape = mover .CaughtNodeShape; // Gets the shape of the caught node.
The caught object itself can be received with either of these calls
GraphicalObject grobj = mover .CaughtSource; or
GraphicalObject grobj = mover [mover .CaughtObject] .Source;
There is also a property to inform if any object is caught by the mover at this moment or not
bool bCaught = mover .Caught;
Even if no object is caught for moving, the mover can produce all the needed data about the objects that are under the
mouse cursor. Mover “senses” the movable objects under the mouse cursor and gives the standard set of information about
them.
bool bSensed = mover .Sensed; // Gets the indication of any movable object under the cursor.
int iObject = mover .SensedObject; // Gets the index of the element underneath.
int iNode = mover .SensedNode; // Gets the number of the node underneath.
Mover is like a shark which is not hungry at the moment and lazy to attack but watches carefully and keeps track of
everything that is going around. In this way mover can produce information about any point of the form and not especially
under the cursor; the information is returned in the form of the MoverPointInfo object. In addition to the number of
object in the queue, the number of node in the cover, and the shape of node, there are also the behaviour of the node (of the
Behaviour enumeration) and the shape of cursor above this node. It is possible to get either the information about the
upper node at the particular point
MoverPointInfo PointInfoUpper (Point pt)
or about all the nodes which overlap at this point
List<MoverPointInfo> PointInfoAll (Point pt)
Every move of an object starts by pressing the object with a mouse. The method associated with it – Mover.Catch() –
returns the value which indicates if any object was really caught or not. This method has three variants which can be used
in different scenarios depending on what is needed. All three variations have one mandatory parameter – the point where
the mouse was pressed, but differ in using other parameters.
mover .Catch (Point ptMouse, MouseButtons catcher)
This is the most frequently used variant of the Catch() method. The second parameter
specifies the mouse button by which an object was caught. In nearly all the situations it is a very
important parameter as it allows to distinguish the forward movement from rotation; it also
indicates the request for calling a context menu.
mover .Catch (Point ptMouse)
The variant without any additional parameter is used rarely enough; you can use it, if you do not
care by what button an object is caught. In such case the system simply substitutes the left button
as the default value and goes on.
mover .Catch (Point ptMouse, MouseButtons catcher, bool bShowAngle)
This version of the method is often used for the forms with the textual comments. The majority of
these comments are derived from the Text_Rotatable class; the moving and rotation of all
these objects are used so often that they are automated and do not need to be mentioned anywhere
in the code. However, throughout the rotation of any comment, its angle can be shown nearby,
and this is regulated by the third parameter of this method. Usually, this parameter is controlled
by the user and can be changed at any moment, for example, via some context menu.
With all this information from mover, it is possible to organize any kind of moving and resizing. In the further examples
you will see the use of these properties and methods, but some of them can be needed only for the most complicated forms.
I think that it is better to have some information about them in one place than to look for it throughout the book. Now we
can start with the real examples and see how it all works beginning from the simplest programs and going step by step to
more complicated applications.
Just a reminder: there is not a single unmovable object in this application. You can move everything.
World of Movable Objects 27 (978) Chapter 2 First acquaintance with nodes and covers
on either the number of the pressed node (the most common case) or the shape. In such primitive case of a single node,
there is no chance to make one decision or another: the only node in the cover is used to move the whole object, so the
MoveNode() method has only to call the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
bRet = true;
}
return (bRet);
}
Polygon is described by an array of its vertices. Polygonal node is also described by its vertices, but such node must be
convex. We can work with arbitrary polygons; they are not obliged to be convex and in further examples you’ll see
polygons of very strange shape, but here we need a very simple example, so the array of points used by the
Polygon_Primitive class on initialization must describe a convex polygon.
public class Polygon_Primitive : GraphicalObject
{
Point [] pts;
SolidBrush brush;
// -------------------------------------------------
public Polygon_Primitive (Point [] points, Color color)
{
pts = points;
brush = new SolidBrush (color);
}
Convex polygon can be covered by a single polygonal node of identical shape. There are several constructors for polygonal
nodes; in case of Polygon_Primitive class the simplest of constructors is used.*
public override void DefineCover ()
{
CoverNode node = new CoverNode (0, pts);
node .Clearance = true;
cover = new Cover (new CoverNode [] { node });
}
There is one extra line inside the Polygon_Primitive.DefineCover() method; I’ll explain this line slightly
ahead in remark on some default and non-default parameters.
Moving of some polygon around the screen means a synchronous change of positions for all its vertices.
public override void Move (int dx, int dy)
{
Size size = new Size (dx, dy);
for (int i = 0; i < pts .Length; i++)
{
pts [i] += size;
}
}
Our polygon can be only moved around but not changed, so we have the same situation as with a circle: MoveNode()
method calls the Move() method. Code of the Polygon_Primitive.MoveNode() method is identical to the
code of the Circle_Primitive.MoveNode() method.
Rounded strip is described by two points and radius. You can look at any rounded strip as some rectangle with additional
semicircles on two opposite sides; two specified points are the central points of semicircles (figure 2.2).
*
Constructors for polygonal nodes are described in MoveGraphLibrary_Classes.doc.
World of Movable Objects 30 (978) Chapter 2 First acquaintance with nodes and covers
public class Strip_Primitive : GraphicalObject
{
Point pt0, pt1;
int radius;
SolidBrush brush;
// -------------------------------------------------
public Strip_Primitive (Point ptA, Point ptB, int rad, Color color)
{
pt0 = ptA;
pt1 = ptB;
radius = rad;
brush = new SolidBrush (color);
}
There are eight different constructors for strip nodes; two points are the
mandatory parameters for all of them. In case of the
Strip_Primitive class, I also specify radius of the node and the
cursor shape over this node. Fig.2.2 Two basic points of any rounded strip
public override void DefineCover () are the central points of semicircles
{
cover = new Cover (new CoverNode [] { new CoverNode (0, pt0, pt1, radius,
Cursors .SizeAll) });
}
Moving of a strip around the screen means the synchronous change of positions for both basic points.
public override void Move (int dx, int dy)
{
Size size = new Size (dx, dy);
pt0 += size;
pt1 += size;
}
Cover of this strip consists of a single node, so the MoveNode() method has only to call the Move() method. You can
expect that in such case the code of the Strip_Primitive.MoveNode() method would be identical to the code of
the MoveNode() methods for primitive circles and polygons, but it is not so. This code is even simpler.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
Move (dx, dy);
return (true);
}
The use of the identical code would be correct and this simplification was done on purpose. As a rule, forward movement
of all objects is done by the left button (the right button is used for rotation), but this is not a mandatory rule. I want to
demonstrate that movement can be organized in different ways and there is an explanation just on the next page.
The example with these primitive objects which can be only moved demonstrates a lot of things that are important and
which are used for all the types of moving / resizing even for much more complicated objects. Let us look at some
important details.
Default and non-default parameters
The cover for any object is organized as a set of nodes. Nodes can be of three different types, so there are three big groups
of the CoverNode constructors. The parameters which must be always declared on construction of a node include the
node number in the cover, position, and sizes. Other parameters often get the default values and can be declared either
during the construction or later. Of the three types of nodes, the polygons are often used to move objects around the screen,
so their default cursor is Cursors.SizeAll. Nodes of two other types – circles and strips – are often used in a small
size and mostly for resizing or reconfiguring of objects, so the default cursor for nodes of these two types is
Cursors.Hand. Because in the Form_Nodes.cs all three types of nodes are used in a big size and for moving objects
around the screen, I decided to keep their cursors consistent and used for circles and strips those constructors which allow to
World of Movable Objects 31 (978) Chapter 2 First acquaintance with nodes and covers
set the cursor shape. Here is the Strip_Primitive.DefineCover() method; the strip nodes are defined by
middle points on two opposite sides and the radius of semicircles.
public override void DefineCover ()
{
cover = new Cover (new CoverNode [] { new CoverNode (0, pt0, pt1, radius,
Cursors .SizeAll) });
}
Visualization of covers is discussed much later, but I would like to make one remark here at the very beginning. Cover
visualization means visualization of nodes; this includes for each node possible painting of the interior area and drawing of
its border with a thin line. Because the nodes of different shapes are usually used in different sizes, they have different
default values even for visualization: the interior areas of circles and strips are wiped out, while the interior areas of
polygons are not. Again, for consistency in this example, I added one line into the
Polygon_Primitive.DefineCover() method so that this cover is also wiped out on request for visualization.
public override void DefineCover ()
{
CoverNode node = new CoverNode (0, pts);
node .Clearance = true;
cover = new Cover (new CoverNode [] { node });
}
You can comment the extra line in this method and see the results; this will show you the standard view of the polygonal
nodes when they are visualized.
Decision on the buttons to move an object
The MoveNode() method allows to specify by which button each node can be moved. For the Circle_Primitive
and Polygon_Primitive classes, the possibility of moving only by the left button is declared.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
bRet = true;
}
return (bRet);
}
For the Strip_Primitive class, I did not include into the mentioned method the checking of the pressed button, thus
allowing the moving of a strip by any button.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
Move (dx, dy);
return (true);
}
Certainly, in any real application it would be a big mistake to put simultaneously on the screen the objects which can be
moved by any button an by only one button. This will be the most confusing thing for users of any program; in further
examples of this Demo application you will not see such a mix. But to demonstrate different possibilities, I organized such
thing in this Form_Nodes.cs. If you decide to make the moving of all three primitive figures in this form identical, you can
either add the same checking of the pressed button into the Strip_Primitive.MoveNode() method or delete those
checkings from two other classes. In the first case the strip will become movable by the left button only; in the second case
all three objects will become movable by any button.
The order of objects
In order to become movable, all objects must be registered with a mover. Mover has a queue of objects with which it has to
work; registering an object means including it into the mover queue.
World of Movable Objects 32 (978) Chapter 2 First acquaintance with nodes and covers
When objects are moved, they can be placed anywhere; it is a common situation when objects overlap. If two or more
objects share the same part of the screen and somebody wants to pickup an element at the point of overlapping, then the
common expectation will be that the upper object has to be caught. Mover does not know anything about the drawing of
objects and their appearance on the screen; mover makes the decision about the object to catch only according to the order
of objects in the queue, so it is the developer’s responsibility first to place the objects in the appropriate order in the queue
and then to draw them in the correct order.
While deciding about the order of objects in the queue, first take into consideration some restrictions that are imposed by an
operating system. The elements viewed on top of others must precede them in the queue, but there is the strict rule enforced
by the system: all controls are always shown atop all graphical objects. Thus we receive the rule 1.
Rule 1. All controls must precede all graphical objects in the queue.
There are four graphical objects and one control in the Form_Nodes.cs (figure 2.1); this control must be placed at the head
of the queue. Here is the FillMoversQueue() method to register all the objects; it is obvious that the button takes the
leading position in the queue.
private void FillMoversQueue ()
{
mover .Add (info);
mover .Add (circle);
mover .Add (poly);
mover .Add (strip);
mover .Insert (0, btnCovers);
}
The second rule coordinates the order of graphical objects in the queue with the order of their drawing and is based on the
fact that graphical objects are painted from the bottom layer to the top.
Rule 2. The order of drawing objects must be opposite to their order in the queue.
In other words, the drawing of graphical objects must go from the tail of the queue to the head. In such a way the graphical
object shown on top of others always precede them in the queue; then the top one is always caught by the mover. Exactly as
expected.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
strip .Draw (grfx);
poly .Draw (grfx);
circle .Draw (grfx);
info .Draw (grfx);
… …
The whole process of moving all objects in the Form_Nodes.cs is organized in such a way.
1. Declare mover.
Mover mover;
2. Initialize mover.
public Form_Nodes ()
{
InitializeComponent ();
mover = new Mover (this);
… …
3. Construct all movable objects and register them with the mover.
public Form_Nodes ()
{
… …
circle = new Circle_Primitive (new Point (150, 120), 100, Color .Blue);
poly = new Polygon_Primitive (new Point [] { new Point (340, 60),
new Point (600, 120), new Point (410, 240) }, Color .Lime);
strip = new Strip_Primitive (new Point (100, 340),
World of Movable Objects 33 (978) Chapter 2 First acquaintance with nodes and covers
new Point (320, 240), 30, Color .Red);
info = new Text_Horizontal (this, new Point (300, 300), infotext);
info .BackColor = Color .Yellow;
FillMoversQueue ();
}
4. Write the code for three mouse events through which the whole process is organized. All three methods are used
in the Form_Nodes.cs in the simplest form as there is nothing else except moving of objects.
private void OnMouseDown (object sender, MouseEventArgs e)
{
mover .Catch (e .Location, e .Button);
}
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
}
private void OnMouseUp (object sender, MouseEventArgs e)
{
mover .Release ();
}
All graphical objects in the Form_Nodes.cs example are of the simplest possible type: each object is movable and non-
resizable, so its cover consists of a single node. Objects in the next example are only a bit more complicated: there are lines
which can be moved and resized, so their covers must contain more than one node.
Solitary lines
File: Form_Lines_Solitary.cs
Menu position: Graphical objects – Basic elements – Lines – Solitary lines
There are several straight lines in the Form_Lines_Solitary.cs
example (figure 2.3) and all of them belong to the LineSegment
class, so the diversity of classes in this example is less than in the
previous one. But the objects of the LineSegment class can be
moved and resized, so the covers of such objects are more interesting
and have to consist of more than one node.
Line is infinitive. Segment is some part of the line limited by a pair of
points, so two points are enough for segment definition. Segment
length is changed by moving the end points. Suppose that the width of
currently changed line is one pixel and you press one end point and
move it into the location of another end point. The line will vanish
from view. To avoid such disappearance, the LineSegment class
has the minimum allowed size; the length of any line is not allowed to
be less than this value (minLen = 10).
public class LineSegment : GraphicalObject
{
PointF ptA, ptB;
Pen m_pen; Fig.2.3 Segments can be moved, resized, and rotated
static int minLen = 10;
double compensation;
// -------------------------------------------------
public LineSegment (PointF pt0, PointF pt1, Pen pn)
{
ptA = pt0;
if (Auxi_Geometry .Distance (pt0, pt1) >= minLen)
{
ptB = pt1;
World of Movable Objects 34 (978) Chapter 2 First acquaintance with nodes and covers
}
else
{
ptB = Auxi_Geometry .PointToPoint (ptA,
Auxi_Geometry .Line_Angle (pt0, pt1), minLen);
}
pen = pn;
}
Each line can be moved by any inner point and resized by both end points (ptA and ptB), so the cover for elements of
this class consists of three nodes.
public override void DefineCover ()
{
cover = new Cover (new CoverNode [] {new CoverNode (0, ptA),
new CoverNode (1, ptB),
new CoverNode (2, ptA, ptB, Cursors .SizeAll)});
}
Two circular nodes are placed at the segment end points; the strip node covers the segment itself. Both types of nodes have
multiple constructors which allow either to declare all the parameters or to omit some of them. In the case of
LineSegment class, I use the simplest type of constructor for circular nodes and declare only its central point. This
means that circular nodes at the ends get the default radius (three pixels) and the default view of the mouse cursor
(Cursors.Hand). For the strip node, I use not the default cursor but set it to Cursors.SizeAll. This is done on
purpose to inform users that pressing the line at the ends or anywhere inside starts different actions. The default radius of
semicircles at the ends of a strip node is three pixels, so the strip node width is six pixels.
While writing about the previous Form_Nodes.cs example, I
mentioned the default node parameters and wrote that on
visualization the areas of two types of nodes – circles and strips –
are wiped out; this is demonstrated at figure 2.4.
Even such a simple cover which consists of only several nodes has
some interesting aspects which must be taken into consideration
while designing the cover. Circles at the ends and semicircles of
the strip have the same radius; centers of the circles and of those
semicircles are also the same. If the strip would be placed in the
cover ahead of circles, then these circles would be entirely covered
by the strip; then the circles would be totally blocked from mover
(from the mouse) and there would be no chances for resizing of a Fig.2.4 Objects of the LineSegment class with
line. To avoid this, the strip must be the last node in the cover. visualized covers
Such designed cover looks very good for usually used thin enough line segments. However, segments are used in many
different situations and their width can vary. Further on you will see a tuning form in which the line width can be changed
in [1, 10] interval; in some cases even wider lines are needed. The width of a real line can be from one pixel and up, but
this parameter is not mentioned anywhere in the design of cover for the LineSegment class. Regardless of the segment
width, its cover has the same width; you have to understand some possible negative aspects of such decision.
When line is thin enough (less than the default width of the strip nodes which is six pixels), the difference between the
width of a line and the width of the cover is not a problem; slightly bigger width of a node only makes the grabbing of this
line easier. However, in those rare situations when the line is wider than six pixels, such a cover design with the fixed width
of node is not a good idea, as there will be points inside the line closer to its sides which will be not covered by any node
and it would be impossible to grab the line by those points. In such special cases of wide lines, it would be much better to
make the width of the cover equal to the real width of a line by using the CoverNode constructor with the parameter
declaring its size.
Forward movement of a line has to change synchronously the positions of both end points.
public override void Move (int dx, int dy)
{
Size size = new Size (dx, dy);
ptA += size;
ptB += size;
}
World of Movable Objects 35 (978) Chapter 2 First acquaintance with nodes and covers
When line is pressed by mouse and, as a result, is caught by mover, the decision about the possibility of movement is made
inside the MoveNode() method. When line is caught by the left button, the decision on either to change the line (its
length, or angle, or both) or simply to move it forward without any changes depends on the number of the caught node. If it
is a strip node covering the whole line (i = 2), then the Move() method is called from inside the MoveNode()
method and the line is simply moved forward without any changes in its view. For two other nodes (circular nodes at the
ends) the proposed new length is checked against the minimum allowed length; if the new length is going to be not less than
the allowed minimum, then the end point is moved to the new position.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF ptNew;
switch (iNode)
{
case 0:
ptNew = new PointF (ptA .X + dx, ptA .Y + dy);
if (Auxi_Geometry .Distance (ptNew, ptB) >= minLen)
{
A_Point = ptNew;
bRet = true;
}
break;
case 1:
ptNew = new PointF (ptB .X + dx, ptB .Y + dy);
if (Auxi_Geometry .Distance (ptA, ptNew) >= minLen)
{
B_Point = ptNew;
bRet = true;
}
break;
case 2:
Move (dx, dy);
break;
}
}
… …
Segments of the LineSegment class can be rotated. Rotation is started by pressing some node with the right button, so
the code for rotation is also included into the MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right)
{
PointF ptC = Auxi_Geometry .Middle (ptA, ptB);
double angleMouse = Auxi_Geometry .Line_Angle (ptC, ptM);
Angle = angleMouse - compensation;
DefineCover ();
bRet = true;
}
return (bRet);
}
World of Movable Objects 36 (978) Chapter 2 First acquaintance with nodes and covers
Rotation goes exactly as it is supposed to go, but… Visually rotation looks fine, but the code for rotation in this example
Form_Lines_Solitary.cs is not absolutely correct. I do not want to discuss the rotation here; it is too early. Further on,
there is a special chapter Rotation in which I describe this process in details. After it you can return back to this example
and find yourself, what is wrong with this code.* At the moment it looks OK, so let it work this way.
This Form_Lines_Solitary.cs demonstrates an interesting solution for the problem of the correct order of drawing. The
order of movable objects in the mover queue is determined in the RenewMover() method. Button is at the head of the
queue, then goes the information, and then all the lines.
private void RenewMover ()
{
mover .Clear ();
mover .Add (btnCovers);
mover .Add (info);
for (int i = 0; i < lines .Count; i++)
{
mover .Add (lines [i]);
}
}
The order of drawing must be opposite: all the lines beginning from the last one, then information. This is the right order of
drawing; figure 2.3 shows the information over the lines, so everything works correctly. Suppose that you decide to change
the order and to show the information below the lines; in this case you will have to make changes both in the
RenewMover() method and in the OnPaint() method. It is easy to do when you have such a simple form with only
few objects to draw, but what about the examples with a lot of objects? It would be much better if you would have to make
the changes of order only in the RenewMover() method and let the painting go in the correct order automatically. Is it
possible to organize such a thing? Yes, if you rely on the mover! Mover can help to check the class of any object from its
queue; this solves the problem.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
GraphicalObject grobj;
for (int i = mover .Count - 1; i >= 0; i--)
{
grobj = mover [i] .Source;
if (grobj is LineSegment_Simple)
{
(grobj as LineSegment_Simple) .Draw (grfx);
}
else if (grobj is Text_Horizontal)
{
(grobj as Text_Horizontal) .Draw (grfx);
}
if (bShowCovers)
{
mover [i] .DrawCover (grfx);
}
}
}
*
The rotation of these lines looks OK because each line is painted with one color; the rotation is always organized around
the middle point and you cannot visually distinguish one end of line from another. There are two simple ways to
demonstrate that something is incorrect with the rotation of these lines. For example, you can either mark one end of line
by different color (I even put an extra line of code into the Draw() method and commented it; you can turn this comment
into the working code) or organize the rotation around another point, for example, around one of the ends; in both cases you
will see something strange. Not always, but occasionally. Maybe this will give you some tip on what is wrong with this
code. After reading the chapter Rotation, you will make the needed changes of the code in a minute. I want to underline
that the problem is not in the code of the LineSegment class, but there is some fault in the code of the
Form_Lines_Solitary.cs example.
World of Movable Objects 37 (978) Chapter 2 First acquaintance with nodes and covers
Objects have to be drawn in order from the end of queue to its head. In the OnPaint() method, there is a loop through
the objects of the queue in this needed order; mover checks the class of each object and calls the appropriate method for
drawing. The appropriate means the method of drawing for the identified class.
Let us return back to the problem of the length restriction. The length of a line is not allowed to become less than the
predetermined minimum. This is a standard situation with many but not all of the objects that I demonstrate. Suppose that
you would allow the squeezing of a line to a zero size. What are you going to show at the screen in such situation? The
first problem will be simply with the drawing, but you can go around this problem by adding the small checking of the size
in the Draw() method. Much more serious problem is the total disappearance of an object from view while it still exists
somewhere in the queue. If users do not see an object, they do not know about it. It becomes a phantom; a ghost of that
rare type which never shows itself to anyone. Do you need such objects in the program? What will be the purpose of
keeping alive such invisible elements?
Usually I do not allow such situations by imposing the minimum limits on any resizable objects. However, there can be
different solutions which are demonstrated in several examples beginning from the next chapter.
• The first solution is to get rid of an object if it is released with an extremely small or zero size. Usually there is
another and much better way to get rid of the unneeded objects, for example, through the context menu. But if it is
declared that users can delete objects by squeezing them to tiny size, then it is a normal solution. In such case you
have to take out the restriction from the MoveNode() method but add similar checking into the OnMouseUp()
method. If you find out that the released object is a segment of extremely short or zero length, then this segment
must be deleted from the List of lines and one more thing must be done immediately. All movable objects are
registered in the mover queue. If an object is deleted, then the queue must be updated. That is why there is a
RenewMover() method in every example and this method is called whenever there is any change in the number
of the movable objects.
• Another variant is to restore an object to some minimum size if it is released with a size less than the allowed
minimum. This variant is nearly the same as not allowing to go below minimum.
• The third and rarely used variant is to allow the existence of such invisible object. It is done on those rare
occasions when it is not a stand alone element but it is used with other objects that clearly indicate that there is an
object, though it can be invisible at the moment. The presence of other objects not only informs about the invisible
one but shows where it stays now, so there is no problem in grabbing the invisible one and enlarging it to some
normal size. One example of such type is the bar chart in which users can change the size of each bar. Any bar
can be reduced to zero, but an empty space in a row of other bars clearly indicates that there is a bar with zero
value which can be grabbed and enlarged. Such example is demonstrated in one program in the chapter Data
visualization.
World of Movable Objects 38 (978) Chapter 3 Rectangles
Rectangles
This chapter shows the moving / resizing of one type of primitive objects: rectangles. But though they are
primitive, there are different possibilities of their resizing.
Rectangle is the most common of all the shapes that can be seen at the screen. It is also considered as a simple enough
shape for moving and resizing, so from the very beginning of the Windows system the only allowed form of a window was
rectangle. In the hierarchy of standard classes window or a dialog (now it is called form) is related to the Control class.
The problems of moving / resizing of different controls and their combinations are discussed in this book much later; this
chapter is about the moving / resizing of only graphical rectangles. Historically any window could be resized by any
border; I will start with such case.
names speak for themselves. As you see, the first variant is for non-resizable rectangles, while three others are for different
types of resizing.
enum Resizing { None, NS, WE, Any };
The Rectangle_Standard class has minimum allowed size for its objects to prevent their accidental disappearance.
When you construct such rectangle, the initially declared sizes are first checked against this minimum size; after it the
rectangle sizes are compared with allowed intervals which are declared through another parameter. If intervals are not
declared, then rectangle is set to be non-resizable; otherwise the range parameter calculates the resizing type for this
particular rectangle.
public Rectangle_Standard (RectangleF rect, RectRange range, int rad, int half,
Color color)
{
rc = new RectangleF (rect .X, rect .Y, Math .Max (minsize, rect .Width),
Math .Max (minsize, rect .Height));
if (range == null)
{
wMin = wMax = rc .Width;
hMin = hMax = rc .Height;
}
else
{
wMin = Math .Max (minsize, Math .Min (rc .Width, range .MinWidth));
wMax = Math .Max (rc .Width, range .MaxWidth);
hMin = Math .Max (minsize, Math .Min (rc .Height, range .MinHeight));
hMax = Math .Max (rc .Height, range .MaxHeight);
}
RectRange realrange = new RectRange (wMin, wMax, hMin, hMax);
resize = realrange .Resizing;
… …
Four rectangles in the Form_Rectangles_Standard.cs get four different types of resizing.
public Form_Rectangles_Standard ()
{
… …
rects .Add (new Rectangle_Standard (new RectangleF (50, 50, 100, 70),
Color .Blue)); // Resizing .None
RectRange rr = new RectRange (70, 70, 40, 300);
rects .Add (new Rectangle_Standard (new RectangleF (100, 160, 70, 120), rr,
radius, halfstrip, Color .Yellow)); // Resizing .NS
rr = new RectRange (30, 400, 80, 80);
rects .Add (new Rectangle_Standard (new RectangleF (220, 100, 230, 80), rr,
radius, halfstrip, Color .LightGreen)); // Resizing .WE
rr = new RectRange (30, 300, 30, 300);
rects .Add (new Rectangle_Standard (new RectangleF (320, 210, 190, 120),rr,
radius, halfstrip, Color .Cyan)); // Resizing .Any
… …
As I have already mentioned, these rectangles can use one of the standard constructors of the Cover class; the resize
parameter for the cover is defined from the analysis of the parameters of initialization.
public override void DefineCover ()
{
cover = new Cover (rc, resize, radius, halfstrip);
}
This resize parameter totally defines the number of nodes in the cover, their types, and order.
• For a non-resizable rectangle (the Blue one at figure 3.1), there is a single node covering the whole rectangle.
• If a rectangle is resizable only in one direction (Yellow and Green rectangles from the picture), then its cover
consists of three nodes: two appropriate opposite sides are covered with two narrow rectangular nodes and then
comes the same big node for the whole area.
World of Movable Objects 40 (978) Chapter 3 Rectangles
• For a fully resizable rectangle (the Cyan one), there are nine nodes in a cover: four small circular nodes in the
corners, then the rectangular nodes on borders, and then the big rectangular node covering the whole area.*
The nodes overlap with each other. At the points of their overlapping, the resizing to be started is decided by the first of
those nodes in the cover. To start any kind of movement, the appropriate node must be found with a mouse cursor. It is
more difficult to find the small node than the big one (remember that normally covers are not shown), so the smallest nodes
are usually the first in the cover. That is why for the fully resizable rectangle I decided about such order of nodes: circular
nodes from the corners, then the nodes on borders, and then the big node for the whole area.
Location of any rectangle is described by a single point – its top left corner, so for the forward movement of any rectangle
only this point must be changed; the Move() method is simple and short.
public override void Move (int dx, int dy)
{
rc .X += dx;
rc .Y += dy;
}
On the contrary, the Rectangle_Standard.MoveNode() method is lengthy enough but, as can be seen from the
code of this method, it is simple. The method is lengthy, because:
• There are four different cases of resizing.
• Each case has its own combination of nodes.
• On moving nearly any node, the proposed new size must be checked against the allowed minimum and maximum
sizes.
Though the MoveNode() method becomes longer, it is still very simple because all those checks for different nodes are
either similar or even identical. Here is the part of the MoveNode() method for the circular node in the top left corner
( i = 0 ) of the fully resizable rectangle.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float wNew, hNew;
switch (resize)
{
case Resizing .Any:
… …
else if (iNode == 0) //LT corner
{
hNew = rc .Height - dy;
if (hMin <= hNew && hNew <= hMax)
{
MoveBorder_Top (dy);
bRet = true;
}
wNew = rc .Width - dx;
if (wMin <= wNew && wNew <= wMax)
{
MoveBorder_Left (dx);
bRet = true;
}
}
… …
*
When I started to work on movability of the screen elements, it was my standard practice to change the number of nodes in
the cover depending on the required resizing. Later I understood that it was much better and more reliable to have for
designed class the fixed number of nodes in its cover but to squeeze the unneeded nodes to zero size. Examples which are
discussed in this book were prepared throughout the years and older classes can use the changing number of nodes.
World of Movable Objects 41 (978) Chapter 3 Rectangles
There are two controls in the Form_Rectangles_Standard.cs (figure 3.1); these controls allow to change the sizes of the
nodes and to see how the easiness of resizing depends on those values. In this example the radius of the corner circles and
the width of the sensitive strips along the movable borders are totally independent, but in many similar examples I put some
restrictions and impose some correlation between them. The circles in the corners are the best and the most obvious places
for resizing; for this reason I often do them bigger than the nodes along borders. I also do not see any sense in making these
circles smaller than the border nodes. But these are only my suggestions based on the years of implementing similar covers
in a lot of different rectangular objects; the standard covers do not have these restrictions.
Though this part of the book is about the covers for the standard and popular graphical objects, for the purpose of better
demonstration this and other examples include some controls and more complex objects. Those controls or combination of
controls allow to change some needed parameters. The controls and more complicated objects are the subjects of the same
moving / resizing mechanism; the involvement of controls and groups of objects into the moving will be described in details
further on, but all the movable / resizable objects are mentioned in the same methods of the forms, so I would like to say
several words of explanation here for better understanding of the whole process.
Control paired with comment can be easily moved around the screen by grabbing it anywhere near control border or at any
point of comment. These two controls with their comments constitute a single object of the
RigidlyBoundRectangles class – one of the classes used for design of user-driven applications. Overall there are
seven movable objects in the Form_Rectangles_Standard.cs; initially they are registered with the mover in such order:
1. The button to switch the visualization of covers ON / OFF.
2. The RigidlyBoundRectangles object to change the parameters of the nodes. Both controls with their
comments can move only synchronously and never change their relative positions, so it is a single object for the
mover.
3. Information (a Text_Horizontal object).
4. Four rectangles (objects of the Rectangle_Standard class) in such an order: blue, yellow, green, and cyan.
I have already mentioned the rules of correct order for objects in the mover queue and their drawing. Regardless of the
form content, the objects must be included into the mover queue in such an order.
1. Controls and complicated objects consisting exclusively of controls.
2. Objects based on combination of controls and graphical parts.
3. Graphical objects.
If the order of objects in the form is set at its initialization and is not going to change throughout the lifetime of this form,
then mover queue can be organized once and is fixed from this moment. The drawing of objects must be done in the
opposite order to their placement in the mover queue. If the order of objects can be changed throughout the form lifetime,
then, whenever it happens, the mover queue is reorganized and the form needs a repainting. Changing of the mover queue
can be caused not only by reordering of the existing objects but also by their removing from the screen or by adding the new
objects. There are two ways to keep mover queue in order. The first one is simply to look through the mover queue, to find
the needed object, and to move it to the new position in this queue. The second way is much more reliable: to have in the
form a RenewMover() method which does exactly the same thing of rearranging the mover queue but guarantees that
all the objects are included into mover queue in correct order. Beginning from this example and further on you will find the
RenewMover() method in nearly any form (example) which I am going to demonstrate. Here is the RenewMover()
method for the Form_Rectangles_Standard.cs.
private void RenewMover ()
{
mover .Clear ();
mover .Insert (0, lrsView);
mover .Insert (0, btnCovers);
mover .Add (info);
for (int i = 0; i < rects .Count; i++)
{
mover .Add (rects [i]);
}
}
The order of rectangles in the Form_Rectangles_Standard.cs can be changed by a single mouse click. If a rectangle is
clicked by the left button and the distance between the occurring of MouseDown and MouseUp events is not bigger than
three pixels, then this rectangle must be moved atop all others.
World of Movable Objects 42 (978) Chapter 3 Rectangles
private void OnMouseUp (object sender, MouseEventArgs e)
{
if (mover .Release ())
{
if (e .Button == MouseButtons .Left &&
Auxi_Geometry .Distance (ptMouse_Down, e .Location) <= 3)
{
GraphicalObject grobj = mover [mover .ReleasedObject] .Source;
if (grobj is Rectangle_Standard)
{
PopupRectangle (grobj .ID);
}
}
}
}
Any movable object has a unique identification number; this id of the clicked rectangle is easily obtained. By checking the
id of each rectangle in the List of rectangles, the pressed element is easily found. Then this rectangle is moved to the
head of the List, the mover queue is reorganized according to the new order of elements, and the form is repainted,
demonstrating the new rectangle on top.
private void PopupRectangle (long id)
{
for (int i = rects .Count - 1; i > 0; i--)
{
if (id == rects [i] .ID)
{
Rectangle_Standard elem = rects [i];
rects .RemoveAt (i);
rects .Insert (0, elem);
RenewMover ();
Invalidate ();
break;
}
}
}
There is no need in rewriting the OnPaint() method according to the new order of rectangles. As was explained in the
previous example, the OnPaint() method goes through the mover queue starting from its end and calls the appropriate
Draw() method for each of the objects. This automatically draws all the rectangles correctly regardless of their order at
any particular moment.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
GraphicalObject grobj;
for (int i = mover .Count - 1; i >= 0; i--)
{
grobj = mover [i] .Source;
if (grobj is Rectangle_Standard)
{
(grobj as Rectangle_Standard) .Draw (grfx);
}
else if (grobj is Text_Horizontal)
{
(grobj as Text_Horizontal) .Draw (grfx);
}
else if (grobj is RigidlyBoundRectangles)
… …
World of Movable Objects 43 (978) Chapter 3 Rectangles
This Form_Rectangles_Standard.cs is the first example where you can see both the request for renewing a cover for some
existing object and a request for renewing mover queue. These two things, though they both work with covers, are used for
absolutely different purposes and in different situations. There must be a clear understanding of their differences.
When parameters regulating the size of a node or the number of nodes in the cover are changed, then the cover of an object
has to be renewed by calling the DefineCover() method. For example, in this form there is an easy way to change the
radius of circular nodes or the width of nodes along the borders. Each rectangle has to be notified about the new radius of
the circular nodes.
private void ValueChanged_numericRadius (object sender, EventArgs e)
{
radius = Convert .ToInt32 (numericUD_Radius .Value);
foreach (Rectangle_Standard rect in rects)
{
rect .Radius = radius;
}
Invalidate ();
}
Setting the new radius of the circular node via the Rectangle_Standard.Radius property requires the new
definition of the cover for the involved rectangle. In this form it means for all the rectangles, as the changes are spread on
all of them simultaneously.
public int Radius
{
get { return (radius); }
set
{
radius = Math .Abs (value);
DefineCover ();
}
}
This sequence of calls does not affect in any way the objects in the mover queue. Objects are the same, they retain the same
order, so there is absolutely no need in calling the RenewMover() method. On the other hand, if there is any change in
the number of objects or in their order, then the call for the RenewMover() method is mandatory.
There will be some difference when we come to the examples with the complex objects (the objects consisting of
individually movable parts), but until then these are the rules for renewal of covers and queues.
A remark about minor problem which some people might consider not so minor.
Suppose that in the current example you are moving not the whole rectangle but one of its sides in such a way that you are
squeezing this object. What happens when you reach the minimum allowed size of rectangle and the MoveNode()
method does not allow to squeeze the caught rectangle any more? The rectangle is not squeezed any more, but the mouse
cursor continues its move. Up till this moment the cursor and the caught border (caught node) were moving synchronously;
from now on there is a shift between the border and the mouse and this shift only grows if you continue to move the mouse
in the same direction.
The stopped border signals you that this rectangle cannot be squeezed any more. Usually you will release the mouse button
at the moment when the caught border is stopped, so I don’t consider this situation with the stopped border and still moving
cursor as a serious problem. If, without releasing the mouse button, you start moving it in the opposite direction, then the
rectangle will start growing according to the movement of the mouse, but there will be the same shift between the cursor
and the moving border which was at the moment when you start the reverse movement of the mouse. I do not consider this
situation as any fault in the algorithm, but some people might have different opinion and they would prefer the mouse cursor
to be stopped if the caught object is not allowed to move further on. There is an easy way to solve the problem in exactly
such way: if further movement of the caught element is not allowed, then the cursor is also stopped. Only I am not going to
describe this solution now but I’ll explain this solution in the chapter Movement restrictions. It is far ahead (somewhere 250
pages further on) and the solution is easy, but everything has to be explained at the right moment. Until then all the
examples will work in the same way with the mouse cursor still moving even if the change of an element is stopped.
If the cursor is allowed to move only with the spot of an object at which it is originally pressed, then it is called the adhered
mouse technique. After reading about this technique, you can slightly change the code of all the examples in the next seven
chapters.
World of Movable Objects 44 (978) Chapter 3 Rectangles
For those who are familiar with the standard cursor clipping, I can tell beforehand that that proposed technique of the
adhered mouse has nothing to do with such clipping. The cursor clipping works only for rectangular areas; adhered mouse
works with objects of arbitrary shape in arbitrary environment. It will be even demonstrated with labyrinths and curved
paths. At the right moment you will find that the technique is really easy and that the same technique can be applied even to
such simple examples as moving the sides of rectangles.
*
I do not share an opinion widely spread among the developers that users are so stupid that the fool-proof procedures must
be installed in all the corners of the programs and for all the situations. If users know that they can delete an object by
diminishing it to zero or tiny size and if they do it, then they really want to get rid of this object. In any way, you can add an
additional question to confirm the request for deleting an object but leave this obvious way to delete the screen objects for
those users who want to do it.
World of Movable Objects 45 (978) Chapter 3 Rectangles
new public void Draw (Graphics grfx)
{
if (rc .Width > 0 && rc .Height > 0)
{
grfx .FillRectangle (brush, rc);
}
}
The decision about possible erasing of an object of the Rectangle_StandardDisappear class and the real erasing,
if it is needed, are made after releasing the mouse button and checking the sizes at this moment, so the new and interesting
changes of the code can be found in the OnMouseUp() method of the form.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iReleased, iNode;
if (mover .Release (out iReleased, out iNode))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover [iReleased] .Source;
if (grobj is Rectangle_StandardDisappear)
{
if (iNode != grobj .NodesCount - 1)
{
Rectangle_StandardDisappear rect =
grobj as Rectangle_StandardDisappear;
if (Math .Min (rect .RectAround .Width,
rect .RectAround .Height) <=
Rectangle_StandardDisappear .SizeDisappearance)
{
long id = grobj .ID;
for (int i = rects .Count - 1; i >= 0; i--)
{
if (id == rects [i] .ID)
{
rects .RemoveAt (i);
RenewMover ();
break;
}
}
}
}
else
… …
Some of the methods and properties which are useful in this case were briefly mentioned in the subsection From algorithm
to working programs; now it is time to look at their use in a real program.
1. If anything has to be done in the OnMouseUp() method, then only in case of releasing an object. The return
value of the Mover.Release() method is the best indication of whether it happened or not. There are several
variants of this method. For the post-release analysis I need to identify both the released object and the node, so I
use the variant which provides this information.
if (mover .Release (out iReleased, out iNode))
2. The number of the released object in the mover queue allows to check the class of the released object. If it is a
rectangle of the needed class (Rectangle_StandardDisappear), then I get the released rectangle for further
analysis.
GraphicalObject grobj = mover [iReleased] .Source;
if (grobj is Rectangle_StandardDisappear)
{
… …
World of Movable Objects 46 (978) Chapter 3 Rectangles
Rectangle_StandardDisappear rect = grobj as Rectangle_StandardDisappear;
3. Sorry, there is one more checking before the rectangle for further analysis is picked out. The released rectangle
may have different types of resizing, but in any case the last node in the cover is the big polygonal (rectangular)
node which is responsible not for resizing of this object but for moving. When an object is simply moved around,
its size never changes, so there is no chance that at the end of such movement an object becomes small enough to
be deleted. Thus, the situation with the last node being released is not interesting for possible elimination of
rectangle and must not be considered. The rectangle is useful for further analysis only after two positive checks.
GraphicalObject grobj = mover [iReleased] .Source;
if (grobj is Rectangle_StandardDisappear)
{
if (iNode != grobj .NodesCount - 1)
{
Rectangle_StandardDisappear rect =
grobj as Rectangle_StandardDisappear;
4. If the released rectangle is so small that it has to be erased from the screen, then this rectangle must be found in the
list of all the rectangles and deleted from there. Identification is done with the help of the
GraphicalObject.ID property.
if (Math .Min (rect .RectAround .Width, rect .RectAround .Height) <=
Rectangle_StandardDisappear .SizeDisappearance)
{
long id = grobj .ID;
for (int i = rects .Count - 1; i >= 0; i--)
{
if (id == rects [i] .ID)
{
rects .RemoveAt (i);
5. Any change in the number of movable objects requires the renewal of the mover queue, this renewal is done by the
RenewMover() method.
rects .RemoveAt (i);
RenewMover ();
The change in the number of rectangles also requires the redrawing of the form, but there is no direct call for such
redrawing. If there is no direct call, then it must be hidden somewhere. The RenewMover() method is called only when
the number or order of objects on the screen is changed. In all such cases the redrawing of the form is a mandatory thing, so
I decided to place such call as the last statement inside the RenewMover() method.
private void RenewMover ()
{
mover .Clear ();
mover .Insert (0, rigidrectsView);
… …
if (bAfterInit)
{
Invalidate ();
}
}
When the form is loaded and the mover queue is filled for the first time, this redrawing is not needed, so I added a Boolean
field which originally has the false value but changes its value immediately after the first filling of mover queue.
bool bAfterInit = false;
public Form_Rectangles_StandardToDisappear ()
{
… …
RenewMover ();
bAfterInit = true;
}
World of Movable Objects 47 (978) Chapter 3 Rectangles
Further on, if there are multiple places with RenewMover() call, then the call for redrawing is included into this method.
covers the whole area of an object and is used for its moving. The second node is the same for all four variants of resizing.
The position of the first node and the cursor above it depend on the side which is chosen for resizing.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [2];
switch (sideMove)
{
case Side .W:
nodes [0] = new CoverNode (0, new Rectangle (rc .Left - halfsense,
rc.Top, 2 * halfsense, rc.Height), Cursors.SizeWE);
break;
case Side .N:
nodes [0] = new CoverNode (0, new RectangleF (rc .Left,
rc.Top - halfsense, rc.Width, 2 * halfsense), Cursors.SizeNS);
break;
… …
}
nodes [1] = new CoverNode (1, rc, Cursors .SizeAll);
cover = new Cover (nodes);
}
The MoveNode() method in this case is much simpler than in the previous because the covers for all four cases are
similar, consist of two nodes only, and in any case there is a check for one dimension only. To avoid disappearance of
rectangle, there is a limit on its minimum size. The limit on the maximum size is passed as a parameter during the
construction, so the MoveNode() method has to check the proposed new size against both limits. All the checks are
needed only for one node (i = 0); for another node there is an automatic call of the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
int newW, newH;
switch (sideMove)
{
case Side .W:
newW = rc .Width - dx;
if (minLen <= newW && newW <= maxLen)
{
rc .X += dx;
rc .Width -= dx;
bRet = true;
}
break;
… …
}
else
{
Move (dx, dy);
}
}
return (bRet);
} Fig.3.4 Menu on rectangles
By clicking any rectangle with the right button you call the context menu which allows to set the sliding side and to switch
the visual prompt on the border ON or OFF (figure 3.4). It is the first example in this book which uses the context menu for
setting the parameters; further on I will use menus more and more.
World of Movable Objects 49 (978) Chapter 3 Rectangles
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double dist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
if (mover .Release () && e .Button == MouseButtons .Right && dist <= 3)
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is Rectangle_ResizeByOneSide)
{
rcPressed = grobj as Rectangle_ResizeByOneSide;
ContextMenuStrip = menuOnStrip;
}
}
}
In all the examples of this Demo application, context menus are called on the release of the right button and only if the
distance between the points of the MouseDown and MouseUp events is small. I feel comfortable with the limit of three
pixels between the mouse positions on these events. If you want, you can set another value which is better for you.
rectangle symmetrically changeable and at the same time making only one of two opposite sides movable; it would be
natural to have two movable opposite sides, so the number of nodes on the borders is zero (non-resizable rectangle), two
(resizable along one dimension only), or four (resizable in both ways). As all the rectangles are movable, there must one
big rectangular node to cover (and move) the whole object; thus, the total number of nodes is one, three, or five. This is the
part of the DefineCover() method for a horizontally resizable rectangle (the Green one from figure 3.6).
public override void DefineCover ()
{
int cxL = ptCenter .X - wHalf;
int cxR = ptCenter .X + wHalf;
int cyT = ptCenter .Y - hHalf;
int cyB = ptCenter .Y + hHalf;
int half = 3;
CoverNode [] nodes;
switch (resize)
{
… …
case Resizing .WE:
nodes = new CoverNode [] {
new CoverNode (0, new Point [] {new Point (cxL - half, cyT),
new Point (cxL + half, cyT),
new Point (cxL + half, cyB),
new Point (cxL - half, cyB)},
Cursors .SizeWE),
new CoverNode (1, new Point [] {new Point (cxR - half, cyT),
new Point (cxR + half, cyT),
new Point (cxR + half, cyB),
new Point (cxR - half, cyB)},
Cursors .SizeWE),
new CoverNode (2, new Point [] {new Point (cxL, cyT),
new Point (cxR, cyT),
new Point (cxR, cyB),
new Point (cxL, cyB)}) };
break;
… …
cover = new Cover (nodes);
}
Objects of the Rectangle_SymmetricalChange class has the minimum allowed size which prevents any rectangle
from disappearance. Because everywhere in the code of this class not the full sizes of rectangle are used but half sizes, it
makes sense to turn the minimum size of rectangle into the limitation on the half size; in such way all calculations become
easier. Here is the part of the MoveNode() method for the same case of the horizontally resizable rectangles.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (resize)
{
… …
case Resizing .WE:
if (iNode == 0) // on left side
{
if (wHalf - dx >= minHalfSize)
{
wHalf -= dx;
bRet = true;
}
}
World of Movable Objects 53 (978) Chapter 3 Rectangles
else if (iNode == 1) // on right side
{
if (wHalf + dx >= minHalfSize)
{
wHalf += dx;
bRet = true;
}
}
else
{
Move (dx, dy);
}
break;
… …
Rotation
A lot of objects can be involved both in forward movement and rotation. I decided to interrupt the
demonstration of standard elements for a discussion of rotation. After it the gallery of movable elements will
continue but already with their involvement in rotation.
The list of standard elements discussed in the previous chapters was opened by the solitary lines (figure 2.3). The lines in
the Form_Lines_Solitary.cs can be rotated, but though the rotation of those lines looks OK, the code for that rotation is not
absolutely correct. After discussion of the first example from this chapter you will easily understand why the code in the
Form_Lines_Solitary.cs has a flaw.
The common rule for all movable objects: the design of their covers does not depend on whether these objects are going to
be rotated or not. Covers are designed to provide movements; the start of the forward movement and rotation are
distinguished not by the touched place but by the pressed mouse button. Certainly, it can be organized in a different way.
You can organize different places to start forward movement and rotation, but from my point of view it would be a wrong
design because then users would have to know and remember the difference between these areas. It is much better when
any object can be moved and rotated by any inner point. This will not demand from users any extra knowledge about the
screen objects with which they deal in one program or another.
The cover of an object does not depend on its presumable involvement in rotation, but there must be some code that makes
the rotation possible.
The first mandatory thing is an additional parameter which describes the angle of an object. Without such an angle, it is
impossible to define the position of an object involved in rotation. For some objects such angle already exists among the
parameters and is used all the time. For others it is needed only for the period of rotation; especially for such objects this
angle must be defined at the moment when the rotation starts. Each class of rotated objects has its StartRotation()
method which includes the calculation of this angle. Because the rotation usually starts when an object is caught by the
right mouse button, then you will find in all further examples that this StartRotation() method is called from inside
the OnMouseDown() method of the form.
Usually one angle is enough even for the most complex objects because during the rotation the relative angles between the
parts are not changed, so the positions of all basic points are calculated according to this angle and the sizes. However, it
depends on the object and in some cases it is easier to calculate the starting angles for several basic points and to change
them synchronously throughout the rotation.
Rotation of an object can be started by catching it at any inner point and the center of rotation can be also changeable. What
is really calculated at the starting moment is the difference between the angle from the mouse position to the center of
rotation and that angle of an object. I call this difference between two angles a compensation. This compensation angle is
never changed throughout the rotation; with the help of this compensation, the angle of an object can be calculated from the
position of the cursor, and thus you get all the basic points and the view of an object at any moment.
MoveNode() method of an object is the place where the real movement is checked and described, so an additional code
in this method is the second mandatory thing to organize rotation. The MoveNode() method has several parameters
which include the linear mouse move along both axes and the mouse position. It may look like one of these is redundant,
but it is not so: my experience based on design and use of many different movable objects shows that the linear shifts along
the axes are better for forward movements, while for rotation the exact mouse position is the best and gives the most
accurate and reliable results.
Circles
File: Form_Circles_Nonresizable.cs
Menu position: Graphical objects – Basic elements – Circles – Non-resizable circles
While trying to organize the examples of each chapter in an order from simple to complicated, I was surprised myself when
I realized that the simplest object to explain rotation would be a circle. Objects with curved borders were the most
problematic for resizing until I thought out the N-node covers (I will write about them further on), but if you take as an
example a non-resizable circle, then it is the best object to talk about rotation.
When I began to design the class of circles for this example – Circle_Nonresizable – I immediately understood
one more amazing thing: it was enough to have a single node cover for this class. I did not expect it; I was sure that after
that first example with the primitive objects (Form_Nodes.cs, figure 2.1) I was not going to use such primitive covers
anywhere else, but here we are, starting the exploration of rotation and again using the most primitive objects. Though there
World of Movable Objects 58 (978) Chapter 4 Rotation
Polyline
File: Form_Polyline.cs
Menu position: Graphical objects – Basic elements – Lines – Polyline
The previous example of rotation used an object
(circle) which had one angle in description, so it was
enough to calculate only one compensation angle.
This new example demonstrates an object which
needs an array of compensation angles.
There are movable objects of four different classes
in the Form_Polyline.cs (figure 4.2).
• The familiar small button to switch the
covers ON and OFF is turned into the
SolitaryControl object.
• The green text is an object of the
Text_Rotatable class. This class is
similar to the Text_Horizontal class
which was used in several previous
examples, but objects of the
Text_Rotatable class can be rotated.
I’ll write about this class a bit later in the
Fig.4.2 Polyline can be moved, reconfigured, and rotated
chapter Texts.
• The small red circle which marks the rotation center belongs to the Spot_Unrestricted class. It is a non-
resizable circle which can be moved to any place around the screen.
• The blue line consisting of several segments is a Polyline object. This object is based on an array of
independently movable points; each pair of consecutive points is connected by a line.
public class Polyline : GraphicalObject
{
PointF [] pts;
Pen pen;
PointF ptAnchor;
// two arrays are used only for rotation between MouseDown and MouseUp
double [] radius;
double [] compensation;
World of Movable Objects 62 (978) Chapter 4 Rotation
Polyline consists of segments. End points of these segments can be moved independently, so they must be defined at the
moment of construction.
public Polyline (PointF ptC, PointF [] pt, Pen penLine)
{
ptAnchor = ptC;
pts = new PointF [pt .Length];
for (int i = 0; i < pt .Length; i++)
{
pts [i] = pt [i];
}
pen = penLine;
radius = new double [pts .Length];
compensation = new double [pts .Length];
}
Two arrays – radius[] and compensation[] – are not needed for forward movement of polyline but they are needed
for rotation and are used between the MouseDown and MouseUp events.
The cover of a polyline consists of the small circular nodes on all the ends of segments and the strip nodes over the
segments. The default width of the strip nodes is six pixels. To simplify the finding and moving of the end points, I
decided slightly to enlarge the circular nodes over these points; their radius is five pixels. Circular nodes in the cover
precede all the strip nodes.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [pts .Length + (pts .Length - 1)];
for (int i = 0; i < pts .Length; i++)
{
nodes [i] = new CoverNode (i, pts [i], 5);
}
for (int i = 0; i < pts .Length - 1; i++)
{
nodes [pts .Length + i] =
new CoverNode (pts .Length + i, pts [i], pts [i + 1]);
}
cover = new Cover (nodes);
}
Moving of the whole line means the synchronous movement of all points in array.
public override void Move (int dx, int dy)
{
Size size = new Size (dx, dy);
for (int i = 0; i < pts .Length; i++)
{
pts [i] += size;
}
}
If you try to move polyline by the left button, then the reaction depends on the type of the caught node: each point (a
circular node) can be moved individually, while any segment (a strip node) moves the whole line. The first pts.Length
nodes of the cover are circular nodes, so if the number of the caught node is less than pts.Length, then it is the
movement of the point with the same number as the node. If the number of the caught node is bigger, then it really does not
matter as it must be one of segments (strip nodes) and any segment moves all of them synchronously, so the Move()
method must be called.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode < pts .Length)
World of Movable Objects 63 (978) Chapter 4 Rotation
{
pts [iNode] += new Size (dx, dy);
}
else
{
Move (dx, dy);
}
bRet = true;
}
… …
Whenever I write the part of the MoveNode() method for forward movement, I use the pair of parameters (dx, dy)
for calculations. However, there is no law that you have to do the calculations with the help of only these parameters.
There are situations when the mouse position (ptM) looks better even for this part of the MoveNode() method.
Circular node over the point is small enough; you can ignore the possibility of small difference between the point and the
cursor at the starting moment of such movement and decide that the new position for the caught point is exactly at the
mouse position. In such case you can change one line of the code above to the
pts [i] = ptM;
If you want to be absolutely correct, you can get and remember the shifts (xShift, yShift) between the mouse
position and the exact point at the start of the movement; these shifts can be used for exact calculations of position for the
point throughout the movement.
pts [i] = new Point (ptM .X + xShift, ptM .Y + yShift);
Polyline rotation starts when the line is pressed (caught) by the right mouse. At this moment the polyline must be informed
about the center of rotation through its Anchor property and the StartRotation() method must be called. As
usual, this method gets one parameter – the mouse position.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (e .Button == MouseButtons .Right && grobj is Polyline)
{
(grobj as Polyline) .Anchor = spot .Center;
(grobj as Polyline) .StartRotation (e .Location);
bInRotation = true;
Invalidate ();
}
}
}
There is one significant change in the Polyline.StartRotation() method in comparison with the previous
example. Because of the independency of all polyline basic points, radius and compensation for each point must be
calculated at the start of rotation and used throughout the rotation. Polyline is a classical example of an object that uses not
a single compensation angle but an array of such angles to organize rotation.
When you press a polyline to start the rotation, you have the center of rotation (ptAnchor), the mouse position
(ptMouse), and the polyline which is described by the array of points (pts[]). Draw a line (imaginary line) from the
mouse cursor to the center of rotation; then draw another line (also an imaginary one) from one of the points to the center of
rotation. You have an angle between two lines and you have the distance of the point from the rotation center; these two
values must be fixed for the whole duration of rotation. Two values are calculated for each point, so the
StartRotation() method produces two arrays.
public void StartRotation (Point ptMouse)
{
double angleMouse = -Math .Atan2 (ptMouse .Y – ptAnchor .Y,
ptMouse .X – ptAnchor .X);
for (int i = 0; i < pts .Length; i++)
{
radius [i] = Auxi_Geometry .Distance (ptAnchor, pts [i]);
World of Movable Objects 64 (978) Chapter 4 Rotation
compensation [i] = Auxi_Common .LimitedRadian (angleMouse +
Math .Atan2 (pts [i] .Y - ptAnchor .Y, pts [i] .X - ptAnchor .X));
}
}
Position of each point throughout the rotation is calculated in that part of the MoveNode() method which is associated
with the movement by the right button. Calculations are identical for all the points, but use two personal values for each of
them. The current angle of the mouse is calculated prior to this. Knowing the mouse angle and the compensation angle for
particular point, you receive the real angle from the center of rotation to this point. Knowing the angle of the point and the
distance from rotation center, you calculate the point location. All points change their positions, so the cover must be
renewed by the DefineCover() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (catcher == MouseButtons .Right)
{
double angleMouse = -Math.Atan2 (ptM .Y – ptAnchor .Y,
ptM .X – ptAnchor .X);
for (int j = 0; j < pts .Length; j++)
{
pts [j] = Auxi_Geometry .PointToPoint (ptAnchor,
angleMouse - compensation [j], radius [j]);
}
DefineCover ();
bRet = false;
}
return (bRet);
}
Rotation of the polyline in my example can be started by pressing the line anywhere. However, there can be different
opinions on this matter or different requests for its organization. For example, the rotation can be supposed to start only by
the end points of segments or, on the contrary, only by the inner points of segments but not by their ends. Such adjustments
can be easily made by the small changes in the OnMouseDown() method and in the MoveNode() method.
Suppose that you want to allow rotation by the end points but not by the segments. This means that rotation can be started
only on the circular nodes; this adds one more check into the OnMouseDown() method.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (e .Button == MouseButtons .Right &&
grobj is Polyline &&
mover .CaughtNodeShape == NodeShape .Circle)
{
(grobj as Polyline) .Anchor = spot .Center;
(grobj as Polyline) .StartRotation (e .Location);
… …
The synchronous change of the MoveNode() method also includes an additional check, but this is the checking of the
node number as only the first pts.Length nodes of the cover are circular.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (catcher == MouseButtons .Right)
{
if (iNode < pts .Length)
{
World of Movable Objects 65 (978) Chapter 4 Rotation
double angleMouse =
-Math .Atan2 (ptM .Y –ptAnchor .Y, ptM .X –ptAnchor .X);
… …
These two possible changes are commented in the code of the Form_Polyline.cs; if you turn these comments into the
working code, then polyline will be rotated only by the end points of all segments.
While the Polyline object is under rotation, the circles for the ends of all segments are painted. To determine, if these
circles must be shown, there is an additional variable bInRotation which changes its value to true inside the
OnMouseDown() method when the rotation starts and returns back to false in the OnMouseUp() method. The
same thing can be organized without any additional variable but with some help from mover. Comment all the places where
bInRotation is mentioned (four lines in the code) and change the checking in the OnPaint() method for such line
(this line is left as a comment there).
if (mover .Caught && mover .CaughtSource is Polyline
&& mover .Catcher == MouseButtons .Right)
There is one more object in the Form_Polyline.cs (fig. 4.2) which can be rotated, though you will not find any traces of it in
the code. The text which you see on the screen is an object of the Text_Rotatable class. Objects of this class can be
used by themselves, as you see in this form; the class is also used as a base class for different types of comments.
Text_Rotatable objects have very simple cover consisting of one polygonal node; rotation of such text goes around its
middle point. Rotation can be started by pressing with the right button at any inner point of the text.
The Text_Rotatable objects and its derivatives are used so often that I automated the whole process of their forward
movement and rotation. Because of this automation, no mentioning of these things are needed anywhere, but everything
works smoothly if an object is registered with a mover.
As I already mentioned, objects of the Text_Rotatable class have a very primitive cover consisting of one polygonal
(always rectangular) node; in the Form_Polyline.cs you can see it together with other covers. All classes derived from the
Text_Rotatable have the same type of cover, but you will never see the covers of those classes. Cover for the
Text_Rotatable class is left visible only for the purpose of better explanation. Covers for this and all the derived
classes do not need any visualization because any text can be simply moved and rotated by any inner point. The
explanation of a simple way to get rid of cover visualization is given further on in the section Visualization of covers in the
chapter Some interesting possibilities.
Rectangles
File: Form_Rectangles_AllMovements.cs
Menu position: Graphical objects – Basic elements – Rectangles – Move, resize, and rotate
Several classes of rectangles with different types of resizing were demonstrated in the previous chapter but none of them
was involved in rotation. Here is one more class of rectangles – Rectangle_AllMovements.
When we deal with some rectangle with horizontal and vertical borders, then it is natural to describe its geometry either by
Rectangle parameter or declare the coordinates of the top left corner and two sizes. The horizontal size of such rectangle
is often called width, vertical size – height. When we can rotate rectangle on arbitrary angle, then we need something else
to describe its geometry, because after 90 degrees rotation width turns into height. An alternative way to describe a
rotatable rectangle is to store the coordinates of all four corners; this is used in the Rectangle_AllMovements class.
public class Rectangle_AllMovements : GraphicalObject
{
protected PointF [] pts = new PointF [4];
protected double angle;
PointF center;
SolidBrush brush;
double compensation; // only between MouseDown and MouseUp
int radiusCorner = 6;
protected static int minSide = 20;
Any Rectangle_AllMovements object is initialized by a central point of rectangle (ptC), two sizes, and an angle
(angleDegree); this set of parameters allows to calculate all four corner points. If you use angleDegree to draw an
imaginary line via initial point ptC, then the size of rectangle along this line is called the width of rectangle (w), while the
size in orthogonal direction is called its height (h). After the four corner points are calculated, only these points are used
World of Movable Objects 66 (978) Chapter 4 Rotation
throughout the code. The central point will appear again especially for rotation between the MouseDown and MouseUp
events.
public Rectangle_AllMovements (PointF ptC, double w, double h,
double angleDegree, Color color)
{
w = Math .Max (minSide, Math .Abs (w));
h = Math .Max (minSide, Math .Abs (h));
angle = Auxi_Convert .DegreeToRadian (angleDegree);
double radius = Math .Sqrt (w * w + h * h) / 2;
double angle_plus = Math .Atan2 (h, w);
pts = new PointF [4] {
Auxi_Geometry.PointToPoint (ptC, angle + angle_plus, radius),
Auxi_Geometry.PointToPoint (ptC, angle - angle_plus + Math.PI, radius),
Auxi_Geometry.PointToPoint (ptC, angle + angle_plus + Math.PI, radius),
Auxi_Geometry.PointToPoint (ptC, angle - angle_plus, radius) };
brush = new SolidBrush (color);
}
Figure 4.3 shows the geometry of the
Rectangle_AllComments object together with
the variables which are used in the constructor of
this class; radius is marked at this figure by a
single letter r.
The cover of this class is nearly identical to the
Rectangle_Standard class with only some
changes in the mouse cursor shape over several
nodes.
When objects of the Rectangle_Standard
class which you see in the
Form_Rectangles_Standard.cs (figure 3.1) have
nodes on their borders, then these nodes are used for
resizing only along horizontal or vertical axis. For
Fig.4.3 Comments on this figure are the same as used in the
these directions, the cursor in the form of
constructor of the Rectangle_AllMovements class
Cursors.SizeWE or Cursors.SizeNS
gives the best information about the possible movement. The Rectangle_AllMovements class has exactly the same
set of nodes, but the whole rectangle together with all its nodes can be turned on an arbitrary angle, so the use of those
standard cursors is inappropriate. Instead, the
Cursors.Hand cursor is used for circular and
strip nodes. There is no need to mention any cursor
in construction of the nodes for this cover as the
Cursors.Hand cursor is the default one both for
circular and strip nodes.
Any object involved in rotation must have an
angle field. Imagine a rectangle which is
positioned with its sides along the horizontal and
vertical axes like a blue rectangle at figure 4.4.
Draw an imaginary horizontal line through the
central point of rectangle; this line has an
angle = 0. Now start rotating the rectangle; the
line will rotate synchronously. There will be an
angle between the horizontal line and the imaginary
one; this is the current angle of this rectangle at any
moment. If you start rotating the rectangle
counterclockwise, then the angle is positive and Fig.4.4 Rectangle_AllMovements objects
growing. This is the way the angles are used in all
World of Movable Objects 67 (978) Chapter 4 Rotation
the examples of this book and in all my applications. Angles are positive if measured counterclockwise from the base line;
angles are negative if measured clockwise from the base line!*
Rectangles of the Rectangle_AllMovements class can be resized
by corners and sides, so their cover consists of nine nodes: four circular
nodes in the corners, four strip nodes on the sides, and one big polygonal
node for the whole area. Figure 4.5 shows a cover together with the node
numbers. By comparison of figures 4.3 and 4.5 you can see that the
nodes in the corners have the same numbers as the points in the array
pts[]. The width of all strip nodes is set by default (six pixels); the
diameter of the circular nodes is enlarged to 12 pixels
(radiusCorner = 6). Numbers of nodes on the figure makes the
understanding of the MoveNode() method easier.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [9];
Fig.4.5 Cover and the node numbers
for (int i = 0; i < 4; i++)
{
nodes [i] = new CoverNode (i, pts [i], radiusCorner);
}
for (int i = 0; i < 4; i++)
{
nodes [i + 4] = new CoverNode (i + 4, pts [i], pts [(i + 1) % 4]);
}
nodes [8] = new CoverNode (8, pts);
cover = new Cover (nodes);
}
The forward movement of any rectangle means the synchronous movement of its four corners.
public override void Move (int dx, int dy)
{
SizeF size = new SizeF (dx, dy);
for (int i = 0; i < 4; i++)
{
pts [i] += size;
}
}
The MoveNode() method in the part which describes the resizing reminds similar method for the
Rectangle_Standard class but is more complicated because a rectangle may have an arbitrary angle. Here is the part
of the Rectangle_AllMovements.MoveNode() method for moving one of the corner nodes.
*
I am sorry that I have to mention and even underline the statement which is well known all round the world to everyone
who is older than 10 (in some countries even earlier), but when you have to deal with the Microsoft products like Visual
Studio, you can expect to see a lot of interesting things. Many years ago, because it started from their very first version,
Microsoft decided to calculate the angles in the opposite way!!! I understand why it happened. The top left corner of the
screen has the (0, 0) coordinate with the positive coordinates growing to the right and to the bottom of the screen. Microsoft
developers coded the standard formula for angle calculations but forgot for a second (which turned into 30 years now) that
for the whole world (outside the screen) the positive values on the Y axis are going not down but up! Maybe the guys were
sure that they would easily turn everyone into their faith, but certainly it did not work. The world continues to count the
angles in the same way as it was doing for many many centuries; Microsoft is also very stubborn and never changed that old
decision. The result? Throughout all these years millions of programmers around the world have to turn their brains inside
out whenever they have to use some of the standard functions from the Microsoft’s Math library.
Now you will be not surprised to see the strange sign in front of the calls to the Math.Atan2() method in my code. I
have to remember all the time that this is one of their methods that return the result with the wrong sign and I have to
change it. The funniest side of this process is that only some methods return the wrong results, so you have to be even more
alert.
World of Movable Objects 68 (978) Chapter 4 Rotation
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
double angleMouse;
if (catcher == MouseButtons .Left)
{
double newD, newW, newH;
PointF ptOpposite, ptA_oppositeside, ptB_oppositeside;
switch (iNode)
{
case 0:
if (Auxi_Geometry .SameSideOfLine (pts [1], pts [2],
ptM, pts [3]) &&
Auxi_Geometry .SameSideOfLine (pts [2], pts [3],
ptM, pts [1]))
{
ptOpposite = pts [2];
angleMouse = Auxi_Geometry .Line_Angle (ptOpposite, ptM);
newD = Auxi_Geometry .Distance (ptOpposite, ptM);
newW = Math .Abs (newD * Math .Cos (angleMouse - angle));
newH = Math .Abs (newD * Math .Sin (angleMouse - angle));
if (newW >= minSide && newH >= minSide)
{
pts = new PointF [4] { ptM,
Auxi_Geometry .PointToPoint (ptM,
angle + Math .PI, newW),
ptOpposite,
Auxi_Geometry .PointToPoint (ptM,
angle - Math .PI / 2, newH) };
DefineCover ();
bRet = true;
}
}
break;
… …
It looks like this piece of code needs several words of explanation.
1. A small possible difference between the center of the circular node and the exact point at which the node is pressed
and caught is ignored; the cursor point ptM is supposed to become the new point for the caught corner. This code
is for the corner with i = 0. The opposite corner of rectangle has i = 2, so ptOpposite gets the actual
point of that opposite corner.
2. The corner which is moved must stay inside the right angle based on three other corners; this is checked by using
twice the Auxi_Geometry.SameSideOfLine() method.
3. angleMouse is the angle from the central point of rectangle to the current mouse position. The central point is
on the line between two opposite corners, so I can use these two points to calculate the needed angle. The distance
between two opposite corners gives the diagonal of rectangle. Both calculations use methods from the
MoveGraphLibrary.dll; all methods are described in the MoveGraphLibrary_Classes.doc (see section
Programs and Documents at the end of the book).
4. (angleMouse – angle) is the angle between the line from central point to the caught corner (mouse
position) and the angle of rectangle. Knowing this difference between two angles and the diagonal of rectangle, it
is easy to calculate the proposed width (newW) and height (newH) of rectangle.
5. Check the proposed new sizes against the allowed minimum sides for rectangle. If none of the proposed sides is
too small, then calculate the new points for the corners. One of these corners, the one which is opposite to the
mouse, is not moving at all and retains its position (ptOpposite); another one is going to be at the mouse
position (ptM), but two remaining corners must be calculated; this is easy to do.
6. If all the checks allow to move the corner, then the points of all four corners are saved and the cover is defined.
World of Movable Objects 69 (978) Chapter 4 Rotation
Calculations for moving the sides are slightly different but only in details.
The previous explanation was about the resizing of an arbitrary turned rectangle; now it is time to look into the rotation
process which starts by the right button press at any point of rectangle. When an object of the
Rectangle_AllMovements class is caught with the right mouse button, then the StartRotation() method is
called.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Right &&
mover .CaughtSource is Rectangle_AllMovements)
{
(mover .CaughtSource as Rectangle_AllMovements)
.StartRotation (e .Location);
}
}
}
The StartRotation() method gets the mouse position as a parameter and calculates three things.
1. The center of rotation; this is the central point of the pressed rectangle.
2. The angle from the rotation center to the mouse position.
3. The compensation angle which is the difference between this angle to the mouse and the angle of an object.
public void StartRotation (Point ptMouse)
{
center = Center;
double angleMouse = Auxi_Geometry .Line_Angle (center, ptMouse);
compensation = Auxi_Common .LimitedRadian (angleMouse - angle);
}
This compensation angle is not going to change throughout the rotation and is used to calculate the corner points in that part
of the MoveNode() method which deals with rotation. In the Rectangle_AllMovements.MoveNode() method
you can see not only the check of the pressed button but also an additional check with the bRotate value. I have already
mentioned that the same rectangles can be switched between being rotatable and being non-rotatable via the commands of
the context menu. This possibility is not used in the Form_Rectangles_WithRotation.cs; in this example all rectangles are
always rotatable.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right)
{
angleMouse = Auxi_Geometry .Line_Angle (center, ptM);
angle = angleMouse - compensation;
double radius = Radius;
double w = Auxi_Geometry .Distance (pts [0], pts [1]);
double h = Auxi_Geometry .Distance (pts [0], pts [3]);
double angle_plus = Math .Atan2 (h, w);
pts = new PointF [4] {
Auxi_Geometry.PointToPoint (center, angle + angle_plus, radius),
Auxi_Geometry.PointToPoint (center,
angle - angle_plus + Math.PI, radius),
World of Movable Objects 70 (978) Chapter 4 Rotation
Auxi_Geometry.PointToPoint (center,
angle + angle_plus + Math.PI, radius),
Auxi_Geometry.PointToPoint (center, angle - angle_plus, radius) };
DefineCover ();
bRet = true;
}
return (bRet);
}
Some comments on this code.
1. angleMouse is the angle from the central point of rectangle (it is also the rotation center) to the current mouse
position.
2. Knowing the compensation which was calculated at the starting of rotation, it is easy to achieve the angle of
rectangle.
3. Rectangle is rotated around its central point. radius is the distance from this central point to vertices, so it is
half of the rectangle diagonal.
4. The pts[] array contains corner points. Distance between pts[0] and pts[1] is the width of rectangle;
distance between pts[0] and pts[3] is its height.
5. anglePlus is the angle between the bottom of rectangle and its diagonal.
6. Knowing the central point, half of the diagonal (radius), and two sizes, it is easy to calculate the points for all
four corners.
7. All the basic points (four corners) change their location, so the cover must be redefined.
Certainly, the dimensions of rectangle are not going to change throughout rotation, so they can be also calculated inside the
StartRotation() method and saved in some additional fields.
Reminder. I hope that now you can easily understand what was wrong with the rotation of solitary lines in the
Form_Lines_Solitary.cs (figure 2.3).
World of Movable Objects 71 (978) Chapter 5 Texts
Texts
Texts play a very important role in the visualization of information. Depending on the request, texts can be
involved in individual movements or they can influence each others movements. More often than not a text is
used as part of some complex object and then such text is involved in individual, synchronous, and related
movements. Further on you will see many examples of texts used as parts of complex objects, but we are
moving from simplest examples to more complex, so this chapter is only about the individual movements of
texts and some aspects of their related movements.
application might not have methods to organize saving / restoring via binary file. If you need such methods, you can easily
add them.
I have already demonstrated a dozen of examples with some explanation on the screen organized as Text_Horizontal
object. I have already shown an easy way of tuning such information via the commands of context menu, so there is no
more problem with font or color of such text. There is an easy way to save and restore all the parameters of such
explanations, so no tuning is going to be lost. Does it mean that I can use such explanations in the form of
Text_Horizontal elements in all further examples? Well, I can, but there is one thing which I do not like and which I
would prefer to change.
Consider the situation with the examples from the previous chapters. When you open any of those examples for the first
time, a small explanation on the screen is very helpful as it gives you a brief description of all possible actions with the
elements of the current example. Later you know all possible actions with these screen elements and you do not need this
information any more, but it is still on the screen. Certainly, you can move it aside, but it would be much better if you, as a
user, would easily control the appearance / disappearance of this information area and allow it on the screen only when you
really want. I need some similar object which user can easily open or close at any moment. Such class is demonstrated in
the next example.
Information on request
File: Form_ClosableInfo.cs
Menu position: Graphical objects – Texts – Information on request
The new example (figure 5.4) is purposely made very similar to the previous one. All solitary words belong to the
Text_Horizontal class. Bigger object with an explanation belongs to the ClosableInfo class. The new element in
this example is the small button with a question sign. Small movable buttons were already used in the previous example, so
you already know that they can be moved by border. The movability of all controls is discussed further on in the chapter
Individual controls and there you wil find out how controls can be moved and resized. Both things are done by control
border, but this is correct for enabled controls. This is the standard mode for all controls, so nearly always they are moved
by borders. However, the disabled controls can be moved by any inner point and this is correct for situation shown at
figure 5.4. Explanation of such easy movement of disabled controls is far ahead in the chapter Some interesting
possibilities in the Form_TrickWithCOntrols.cs (figure 20.3).
While writing about the previous
example and the use of the
Text_Horizontal class to
organize the needed explanation
on the screen, I already mentioned
that such screen explanation would
be much better if user has a chance
to open or close it at his own wish.
For several decades we all work
with the screen windows which are
closed by clicking a small cross in
the top right corner. Everyone is
familiar with such feature and the
purpose of similar cross in the
same corner of the text will be
obvious to everyone. Thus, the
idea of the view for the
ClosableInfo class was ready
and the class needed only code.
Fig.5.4 All solitary words belong to the Text_Horizontal class. Information
Objects of new class have to play
exactly the same role as the belongs to the ClosableInfo class which is derived from the
Text_Horizontal objects in Text_Horizontal class.
the previous example, so the easiest way of implementation is to derive the new class from the Text_Horizontal class.
public class ClosableInfo : Text_Horizontal
{
float fCloseSide = 24;
RectangleF rcClose;
World of Movable Objects 76 (978) Chapter 5 Texts
SolidBrush brushClose = new SolidBrush (Color .OrangeRed);
Pen penCross = new Pen (Color .White, 3);
Form_ClosableInfoParams formParams = null;
It is enough to use the constructor of the base class; only the width of the area must be enlarged to provide the space for a
small square with the cross.
public ClosableInfo (Form form, PointF ptAnchor, string txt, Font fnt,
Color clr, bool show_frame, Color back)
: base (form, ptAnchor, txt, fnt, clr, show_frame, back)
{
CalcSizes ();
}
void CalcSizes ()
{
base .CalcSizes ();
fW += fCloseSide;
rcClose = new RectangleF (RectAround .Right - (fCloseSide + wAdd / 3),
RectAround .Top + hAdd / 2, fCloseSide, fCloseSide);
}
A ClosableInfo object is going to be non-resizable but movable by any inner point except the small square in the
corner. The best and the easiest solution is to cover the whole area with a single polygonal (rectangular) node and to place
ahead of it another smaller node which prevents the small square from being used for moving. The small node can be either
declared with the Behaviour.Frozen parameteror not mentioned at all in the MoveNode() method; I use the second
variant. The cursor over this small node is changed from standard to Cursors.Hand in order to inform users that
something special can be done by pressing this area.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, rcClose, Cursors .Hand),
new CoverNode (1, RectAround) };
cover = new Cover (nodes);
}
When a ClosableInfo object is moved, then its basic point – the top left corner – must be changed and the small square
with the cross must be moved synchronously.
public override void Move (int dx, int dy)
{
ptLT += new Size (dx, dy);
rcClose .X += dx;
rcClose .Y += dy;
}
Only the second node – the big one – is used for moving an object, so only the reaction on pressing this node must be
specified in the ClosableInfo.MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 1)
{
Move (dx, dy);
bRet = true;
}
}
return (bRet);
}
World of Movable Objects 77 (978) Chapter 5 Texts
*
Similar auxiliary form with tuning of these two colors can be seen in an application which accompanies my article [18].
World of Movable Objects 80 (978) Chapter 5 Texts
mover .Insert (0, info);
}
… …
For the InfoOnRequest class, the drawing is done in the same way with checking of visibility inside the Draw()
method, but I also want to be consistent and to hide the needed checking somewhere inside the method of registering.
All graphical objects are derived from the GraphicalObject class. This base class has IntoMover() method which
for simple objects work like Mover.Insert() method. This GraphicalObject.IntoMover() method is
included into the base class because it is extremely useful for complex objects and I’ll write about it in the chapter Complex
objects. For simple objects, I never use this method because it gives no advantages. For the InfoOnRequest class, I
decided to use this method and to include the visibility checking inside.
new public void IntoMover (Mover mover, int iPos)
{
if (Visible)
{
if (0 <= iPos && iPos <= mover .Count)
{
mover .Insert (iPos, this);
}
}
}
By using InfoOnRequest.IntoMover() method, I can avoid using the direct checking of visibility when objects of
this class are mentioned inside some RenewMover() method.
There is one more BIG advantage of switching from ClosableInfo objects to InfoOnRequest objects. When
ClosableInfo object is used, the situation with clicking a small cross in its corner must be checked and then there are
three standard lines to deal with consequences of such click (see a piece of code two pages back). For InfoOnRequest
objects, all these things are hidden inside the MoveGraphLibrary.dll and are done automatically, so closing of information
and renewing of mover queue are not mentioned anywhere else but they work in nearly all examples further on.
Rotatable texts
File: Form_Text_Rotatable_Class.cs
Menu position: Graphical objects – Texts – Rotatable texts
There was no text rotation in the previous examples of this chapter. Now it is time to add such very useful feature to the
texts, but in order to do this, I have to use another class of objects.
I have already demonstrated the use of the movable / rotatable text in one of the previous examples (Form_Polyline.cs,
figure 4.2). The Text_Rotatable class is included into the MoveGraphLibrary.dll and nearly all the work with this
class is automated. This was done on purpose because the movable and rotatable texts are used very often; such automation
allows to get rid of the same lines of code in many places. On the other hand, it would be interesting to look into some
World of Movable Objects 81 (978) Chapter 5 Texts
details of designing such texts, so here is the Form_Text_Rotatable_Class.cs (figure 5.7) designed around the
Text_Rotatable_Demo class which is the copy of the Text_Rotatable class.
public class Text_Rotatable_Demo : GraphicalObject
{
Form formParent;
PointF ptMiddle; // Text_Rotatable_Demo is the centered text
string m_text;
Font m_font;
double m_angle; // in radians
Color m_color;
SolidBrush brushBack;
bool bRotatable;
float fW, fH;
double compensation; // is used only between MouseDown and MouseUp
Label labelAngle = null;
The difference between Text_Horizontal and Text_Rotatable classes is well seen from comparison of their
fields.
• Text is rotatable, so the value of its current angle must be stored (m_angle).
• Rotation can be regulated, so there is a special flag (bRotatable).
• The process of rotation was
already discussed in the
previous chapter, so there is a
standard field for compensation
angle which is used throughout
the rotation
(compensation).
• There is one more field
(labelAngle) about which
I’ll write further on.
Horizontal texts are often used for some
general information on the form and for
better visualization they are often used
with frame and some background which
differs from the surrounding form.
Rotatable text is mostly used as very
short explanation for some screen
element, so it has no frame and no
background. There is a brush to paint
the background of rotatable text, but by Fig.5.7 Objects of the Text_Rotatable_Demo class
default its colour is declared transparent (though user can change this transparency at any moment).
public Text_Rotatable_Demo (Form form, // form in which this text is used
PointF ptM, // central point
string txt, // text itself
Font fnt, // font
double ang_Deg, // text angle (in degrees)
Color clr) // color
{
formParent = form;
ptMiddle = ptM;
m_text = CheckedText (txt);
m_font = fnt;
m_color = clr;
m_angle =
Auxi_Common .LimitedRadian (Auxi_Convert .DegreeToRadian (ang_Deg));
CalcSizes ();
World of Movable Objects 82 (978) Chapter 5 Texts
brushBack =
new SolidBrush (Auxi_Colours .TransparentColor (form .BackColor, 1));
bRotatable = true;
}
The design of cover is determined by the set of requirements to the Text_Rotatable class.
• Text may have multiple lines.
• Text can be moved and rotated by any point. The sensitive area is not combined of the rectangles for each line but
is a single rectangle with the width determined by the longest line.
• Size of this solitary rectangle is determined only by the text itself and the used font. There is no frame around
rotatable text and no extra spaces on the sides.
• An object is not resizable by the mouse. The size of an object can change only as the result of using another font
or changing the text itself.
Text sizes are calculated on the basis of the used font.
void CalcSizes ()
{
SizeF sizef = Auxi_Geometry .MeasureString (formParent, m_text, m_font);
fW = sizef .Width;
fH = sizef .Height;
}
By using these sizes and the text angle, corners of rectangular area are calculated by the
Auxi_Geometry.TextCorners() method.
public PointF [] Corners
{
get { return (Auxi_Geometry .TextCorners (fW, fH, m_angle, ptMiddle,
TextBasis .M)); }
}
This method uses the TextBasis enumeration which is explained several pages further on. It is not a good idea to use an
unexplained part of the code, but this explanation is much easier to understand with a small illustration which can be found
at figure 5.8.
Cover consists of a single node, but the behaviour of this node depends on whether the particular text is declared movable or
not. It looks a bit strange first to develop a whole theory of turning any screen object into movable / resizable and then to
think about turning such movable objects into unmovable and to design a special addition for this purpose, but… I will
write much more about this situation later while discussing the design of user-driven applications. The reason for turning
movable objects into unmovable is the necessity of such actions from time to time. The main principle of the user-driven
applications is that user has total control over applications and decides at any moment about the main features of any object.
It is possible that at some moment user will prefer to turn otherwise movable objects into unmovable; for this situation an
object must be ready to switch its behaviour.*
The turn of any object into unmovable does not mean that it is simply excluded from the mover queue and becomes
invisible for mover. No, it is still sensitive, and with the mover help it can be found, for example, to change its color, font,
or do something else, but it becomes unmovable. Regardless of whether the text is movable or unmovable, its cover
consists of a single node; the difference is only in behaviour of this node.
• For an unmovable text, I use the Behaviour.Frozen value for behaviour and Cursors.Default as the
shape of cursor.
*
Further on I’ll write about the rules of user-driven applications and I’ll insist that the appearance of a single unmovable
object among all other movable objects is the best way to destroy even good application. Here I write about the occasional
necessity of switch from movable object to unmovable and do not say a word about the stupidity of such change. Yet, there
is no contradiction. Text is often used as some part of bigger objects. While text is turned into unmovable, then its
individual move is forbidden and the accidental change of its relative position. At the same time the big object is movable
and when it is moved, all its related elements – movable and unmovable individually – move synchronously with the main
object. You will see it further on in complicated objects on which the scientific and engineering applications are based. For
all those objects, the switch of texts into individually unmovable is required.
World of Movable Objects 83 (978) Chapter 5 Texts
• For a movable text, I do not specify these parameters, so they get the default values of polygonal nodes:
Behaviour.Moveable and Cursors.SizeAll.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, Corners));
if (!Movable)
{
cover .SetNodeBehaviourCursor (0, Behaviour .Frozen, Cursors .Default);
}
if (TransparentForMover)
{
cover .SetBehaviour (Behaviour .Transparent);
}
}
Transparency of elements for mover is discussed far ahead (more than 300 pages further on). Up till then all demonstrated
objects are not transparent for mover, so the last part of the Text_Rotatable_Demo.DefineCover() method is not
used.
Position of rotatable text is determined by its central point, so for the forward moving it is enough to move this point for a
specified number of pixels.
public override void Move (int dx, int dy)
{
ptMiddle += new SizeF (dx, dy);
}
The MoveNode() method is also very simple as there is only one node in the cover and no restrictions on any movement
at all. For the forward movement, the Move() method is called from inside the MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
bRet = true;
}
… …
In the Form_Text_Rotatable_Class.cs (figure 5.7), you can see three different objects of the Text_Rotatable_Demo
class. Interesting things with all of them start in the OnMouseDown() method of the form.
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 .Right && grobj is Text_Rotatable_Demo)
{
Text_Rotatable_Demo txtrot= grobj as Text_Rotatable_Demo;
txtrot .StartRotation (e .Location);
txtrot .AngleLabel (checkboxLabel .Checked);
}
}
}
When any Text_Rotatable_Demo object is caught by the right button, then it means that rotation is started. At this
moment two methods of this class are called. The first one is the
Text_Rotatable_Demo.StartRotation()method; it calculates the compensation angle between the angle to the
point where the mouse have caught the text and the angle of the text. Use of this compensation angle was already explained
World of Movable Objects 84 (978) Chapter 5 Texts
earlier in the chapter Rotation. If the parameter of the second method – Text_Rotatable_Demo.AngleLabel() –
is set to true, then this method organizes a special label to show the changing angle of the text throughout the rotation.
Any textual information can be shown either as graphical object or as a special control to show texts (Label control).
When I know that there are no controls in the form, then I prefer to show the needed information as a graphical object. But
all controls are always shown on top of all the graphical objects, so, if there are any controls in the form, they can block this
graphical information from view. The Text_Rotatable_Demo class is designed to be used in any situation with any
combination of objects in view, so it is better to show the needed auxiliary information related to rotation as a Label.
Texts can be positioned at any place on the screen. While some text is in rotation, the information about its angle must be
shown somewhere very close to this text; in such way it will be easier to look simultaneously at the text and the auxiliary
information. The Text_Rotatable_Demo.AngleLabel() method calculates the appropriate position for a label
(cxLabel, cyLabel), creates a Label with the needed text, and puts it at the top of all controls of the form; thus the
new label appears above everything else. Position of the text in rotation is described by its central point; the label with the
information about the angle appears at the horizontal line of this central point. Whether this label appears to the left or to
the right of the text depends on which side of the form (right or left) the text itself is shown.
public void AngleLabel (bool bShowLabel)
{
if (bShowLabel)
{
… …
labelAngle =
Auxi_Common .CreateInfoLabel (new PointF (cxLabel, cyLabel),…);
formParent .Controls .Add (labelAngle);
labelAngle .BringToFront ();
}
}
Rotation of any Text_Rotatable_Demo object is described in the second half of the
Text_Rotatable_Demo.MoveNode() method; this technique was discussed in the chapter Rotation. .The
compensation angle was already calculated in the StartRotation() method at the beginning of rotation; now the
current mouse position gives the angle to the mouse and together with the compensation angle it gives the current angle of
the text.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
… …
else if (catcher == MouseButtons .Right && bRotatable)
{
double angleMouse = -Math.Atan2 (ptMouse .Y – ptMiddle .Y,
ptMouse .X – ptMiddle .X);
Angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
The Text_Rotatable_Demo.Angle property sets the new angle of the text. The new angle means that the
rectangular area of the text has turned, so the cover has to be updated by calling the DefineCover() method.
public double Angle
{
get { return (angle); }
set
{
angle = value;
DefineCover ();
UpdateLabelAngle ();
}
}
World of Movable Objects 85 (978) Chapter 5 Texts
The angle gets new value; the cover is defined according to this new angle. Then the method to update the label is called;
this method will work only if the label was created before.
private void UpdateLabelAngle ()
{
if (labelAngle != null)
{
labelAngle .Text = Convert .ToInt32 (
Auxi_Common .LimitedDegree (Angle_Degree)) .ToString ();
labelAngle .Update ();
}
}
When any Text_Rotatable_Demo object is released by mover, then the Release() method of this object must be
called.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is Text_Rotatable_Demo)
{
(grobj as Text_Rotatable_Demo) .Release ();
}
… …
This Text_Rotatable_Demo.Release() method is doing anything at all only if the label with the information was
created earlier; in such case the label is deleted from the set of controls. Thus, the label is shown, if required, only between
the MouseDown and MouseUp events and disappears after it without any trace.
public void Release ()
{
if (labelAngle != null)
{
formParent .Controls .Remove (labelAngle);
labelAngle = null;
}
}
The Text_Rotatable_Demo class demonstrates the design of movable and rotatable texts; it also includes the
possibility of showing the currently changing angle throughout the rotation of a text. The class is nearly an exact copy of
the Text_Rotatable class which is included into the MoveGraphLibrary.dll and is widely used in all my programs.
Several examples (forms) in the big Demo application accompanying this book use this Text_Rotatable class, but
you will never see any calls to its StartRotation() method or any mentioning of the labels. Yet, all those numerous
texts can be moved and rotated, and the labels are shown, if required. The reason to hide the technical side (the code) of
dealing with the Text_Rotatable objects is simple: I do not want to repeat again and again all the calls to these
methods in every form where I use Text_Rotatable, so the work with the Text_Rotatable class is automated
inside the Mover class. For this particular class of texts, mover knows how to create a label, how to change the
information in this label, and when to delete the label. Everything is organized exactly in the same way as it is shown for
the Text_Rotatable_Demo class, but everything is done behind the curtains.
Several lines back I mentioned that a label with information about angle “is shown, if required”. Where and how it is
organized for the Text_Rotatable class? Label with the angle value has to be shown only starting from the
MouseDown event when some Text_Rotatable object is pressed with the right button. Mover is responsible for
organizing a label and dealing with it, so it would be natural to inform mover about the need for label at the moment when
the text is caught. This is done by using the Mover.Catch() method with an additional parameter.
mover .Catch (Point ptMouse, MouseButtons catcher, bool bShowAngle)
This version of the Mover.Catch() method is often used for the forms with the textual comments. Occasionally these
comments belong to the Text_Rotatable class but more often to the classes derived from Text_Rotatable.
World of Movable Objects 86 (978) Chapter 5 Texts
Usually, the third parameter in the Mover.Catch() method is controlled by the user and can be changed at any
moment, for example, via some context menu, so user decides about showing or not showing of the additional label with the
angle information.
How can mover do all the needed things with such label? Mover can easily check the type of the caught object. If it is a
text and the parameter of the Catch() method is set to true, then mover organizes a label, shows there the changing
angle throughout the rotation, and deletes the label when the text is released. It is all mover responsibility; everything
works correctly; you do not need to think about it. Certainly, if for any reason you are not satisfied with the mover
demonstration of the angle, then do not use this version of the Catch() method and organize the showing of angle
yourself. I have demonstrated the technique on the example of the Text_Rotatable_Demo class; you can do it in a
similar way or any other.
The Text_Rotatable class is also used as a base class for many different types of comments associated with simple
and complex objects. For example, the MoveGraphLibrary.dll includes comments to be used with rectangles
(CommentToRect), circles (CommentToCircle), rings (CommentToRing), and several others. Each of them can be
moved individually and synchronously with associated element. Those comments detain their relative position when an
associated object is moved or resized, so all these comments have some positioning coefficients. The number and use of
these coefficients depend on the shape of the associated object but otherwise the behaviour of all these comments is
identical to what was demonstrated with the Text_Rotatable_Demo elements. Text_Rotatable objects are used
in several examples of Demo application but the derived classes are used in nearly every example further on.
Any object of the Text_Spotted class is just a text, so it has the same set of parameters which you saw in the
Text_Rotatable_Demo class. Certainly, there are some differences; otherwise I would not design a special class. The
minor difference is in absence of the background color. The Text_Rotatable class contains background color, but
nearly always the background is transparent and is not seen at all. I do not need to show background for rotatable elements
in this example, so I excluded the background color from the new class. There are also additional colors which are used for
auxiliary drawing and several fields which need some explanation.
public class Text_Spotted : GraphicalObject
{
Form formParent;
PointF [] pts;
TextBasis basis;
string m_text;
SizeF sizef;
Font m_font;
double m_angle; // in radians
Color clrText;
Color clrNodes;
Color clrBasicNode;
Pen penFrame;
float fSpotRadius = 4;
bool bShowSpots;
bool bRotatable;
bool bCursorUnusual;
int iCursor;
double compensation;
One special field in this class is related to the geometry of these texts and special spots of their area. Before starting to play
with the texts of this example, I’ll write several words about these spots.
Any text occupies some rectangular area on the screen; the sizes of this rectangle are determined by the text itself and the
used font. Even for a text consisting of several lines I use a single rectangular area with the width determined by the longest
line. Consider some horizontally written text which occupies a rectangular area. This rectangle has nine special points: four
corners, four points in the middle of the sides, and the central point of rectangle. The TextBasis enumeration includes
exactly nine members to work with these special points. I hope that eight of their names are obvious; the M member is
associated with the middle.
public enum TextBasis { NW, N, NE, W, M, E, SW, S, SE };
If you have horizontal text and draw three horizontal lines on the top of the text, in the middle, and at the bottom, then each
line goes through three special points. The order of these special points is from top to bottom and on each line from left to
right.
The MoveGraphLibrary.dll includes several methods to draw the texts and to get text coordinates; all these methods use
the TextBasis enumeration. For example, one of the methods allows to draw the text anchoring it on the specified
screen point by the text points specified by member of this enumeration. To draw the text “January” which you can see in
the top left corner of figure 5.8, you can use a line of code shown below; the specified point will be in the bottom left corner
of letter J. Some of the parameters specify the font, angle, and color. The last two parameters specify the point in the form
and the special point of the text which must be associated with this real point.
Auxi_Drawing .Text (grfx, "January", Font, -0.5, ForeColor,
new PointF (80, 30), TextBasis .SW);
Another very useful method can be seen in the constructor of the Text_Spotted class; it returns the set of those special
points for a text with the known sizes, angle, position, and the special point associated with this position.
public Text_Spotted (Form form, PointF ptAnch, TextBasis basicpoint,
string txt, Font fnt, double ang_Deg, Color clr)
{
formParent = form;
m_text = CheckedText (txt);
sizef = Auxi_Geometry .MeasureString (form, m_text, fnt);
m_angle =
Auxi_Common .LimitedRadian (Auxi_Convert .DegreeToRadian (ang_Deg));
World of Movable Objects 88 (978) Chapter 5 Texts
basis = basicpoint;
pts = Auxi_Geometry .TextGeometry (sizef, m_angle, ptAnch, basis);
m_font = fnt;
… …
The array of points returned by the Auxi_Geometry.TextGeometry() method is filled by the points according to
the TextBasis enumeration, so the first three points are for the top line from left to right, then three points for the
central line, and the last three are from the bottom line of the text (see the word Sample with the dots at figure 5.8). These
nine points are used for the cover design in the Text_Spotted class.
Cover of any Text_Spotted object consists of 10 nodes: nine of them are the small circular nodes exactly on those
special points of the text area; the last node is a polygon covering the whole area of the text. Those nine points are
calculated by one of the methods from the MoveGraphLibrary.dll and include four corners, four points in the middle of
each side, and the central point of the rectangular area. The previous explanation about the TextBasis enumeration and
the array of points together with the code of the DefineCover() method makes it obvious that the first nine nodes of
the cover go in parallel with the members of the TextBasis enumeration which makes the work with the
Text_Spotted objects easier as the number of a node can be cast into the value of enumeration and vice versa.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [10];
for (int i = 0; i < 9; i++)
{
nodes [i] = new CoverNode (i, pts [i], fSpotRadius);
}
nodes [9] = new CoverNode (9, Frame);
if (bCursorUnusual)
{
nodes [9] .Cursor = Auxi_Common .CursorByOrder (iCursor);
}
cover = new Cover (nodes);
}
There is one unusual thing in the cover of the Text_Spotted class; it is so unusual that this word is included into the
name of the Boolean variable which regulates its use – bCursorUnusual. These texts can be moved around the screen
without any restrictions and this is signaled by using the Cursors.SizeAll (this is the default cursor shape for any
polygonal node). Far ahead in the book I’ll demonstrate the Form_Colors_Circle.cs example in which the change of
standard cursor for very small texts of the Text_Spotted class will be very helpful; for such cases there is a small
addition inside the Text_Spotted.DefineCover() method.
I have to admit that 10 nodes are used in this cover not because they provide different types of movement but because they
simplify my explanation and visualization of some features. The same movement can be organized with a cover of a single
rectangular node, but then I will have to organize similar set of nine points and keep track of them; mover takes this burden
from me. As all 10 nodes are used for the same kind of movement, then for forward movement there is no need in checking
the number of the caught node; the MoveNode() method is very simple.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
}
else if (catcher == MouseButtons .Right && bRotatable &&
iNode != (int) basis)
{
double angleMouse = -Math .Atan2 (ptM .Y - AnchorPoint .Y,
ptM .X - AnchorPoint .X);
Angle = angleMouse - compensation;
bRet = true;
}
World of Movable Objects 89 (978) Chapter 5 Texts
return (bRet);
}
Rotation of these texts is organized in nearly standard way but with one small
addition or better to call it exception. Each text has nine special points; each one
of them can be used as the center of rotation. All special points except one are
painted in violet; the point which is currently selected as rotation center is painted
in red. Rotation can be started when the right button is pressed at any point except
the area of the node that covers the rotation center; for this purpose there is an
additional comparison of the caught node with the number of the current center
point in the second half of the MoveNode() method.
else if (catcher == MouseButtons .Right &&
bRotatable &&
i != (int) basis)
All 13 rotatable texts in the Form_Texts_MoveAsSample.cs – 12 months plus a
sample – belong to the Text_Spotted class and are supposed to work in the
same way, but if you try to move them forward, you will find something
interesting: the names of the months can be moved by any inner point, but the
Sample can be moved only by pressing one of the colored spots. To organize such
a strange difference in their behaviour, I added several lines of code into the
OnMouseDown() method. First, on catching any Text_Spotted object, I
check which of the texts is caught. Checking is easily organized by comparison of
identification numbers.
private void OnMouseDown (object sender,
MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button)) Fig.5.9 All months are lined if any
{ special spot of the Sample is
GraphicalObject grobj = pressed
mover .CaughtSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Text_Spotted)
{
int iNode = mover .CaughtNode;
if (grobj .ID == sample .ID)
{
… …
}
else
{
if (iNode < Enum .GetNames (typeof (TextBasis)) .Length)
{
(grobj as Text_Spotted) .TextBasis = (TextBasis) iNode;
Invalidate ();
}
}
… …
If it is not the Sample, then it is one of the months and will be moved according to the mentioned
Text_Spotted.MoveNode() method. The only thing which is done in such case in the OnMouseDown() method
is the setting of the new rotation center if any small circular node is pressed. The caught node is one of the small nodes if its
number is less than the number of members in the TextBasis enumeration.
if (iNode < Enum .GetNames (typeof (TextBasis)) .Length)
{
(grobj as Text_Spotted) .TextBasis = (TextBasis) iNode;
Invalidate ();
}
World of Movable Objects 90 (978) Chapter 5 Texts
If the Sample is caught, then the reaction depends on the node by which it was caught:
• If it is one of the circular nodes (colored spots), then all months are lined at their initial places and their lining is
defined by the pressed spot of the Sample (figure 5.9).
• If the pressed point is not on any of the colored spots, then not only the months ignore any command and continue
to stay freely wherever they are, but the Sample itself is released from mover and is not going to move anywhere.
Prior to invention of adhered mouse technique (I’ll write about this further on) there were only two examples in the
whole Demo application with the use of Mover.Release() method anywhere outside the OnMouseUp()
method, but I have never said that it can be used only inside the MouseUp event. You can decide yourself, which
events to use for catching and releasing objects. I prefer to use the MouseDown and MouseUp events, but
there is no strict rule about it.
void OnMouseDown (object sender, MouseEventArgs e)
{
… …
if (grobj .ID == sample .ID)
{
if (iNode < Enum .GetNames (typeof (TextBasis)) .Length)
{
sample .TextBasis = (TextBasis) iNode;
for (int i = 0; i < month .Length; i++)
{
month [i] .Relocate (ptAnchor [i], sample .TextBasis,
sample .Angle_Degree);
}
Invalidate ();
}
else
{
mover .Release ();
}
}
… …
Figure 5.8 makes it obvious that any month can be rotated individually and such rotation has no effect on other objects.
The rotation of texts was already discussed in the previous chapter; rotation of the Text_Spotted objects starts in exactly
the same way by calling the Text_Spotted.StartRotation() 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)
{
… …
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is Text_Spotted)
{
(grobj as Text_Spotted) .StartRotation (e .Location);
}
}
}
}
It does not matter whether it is one of the months or a Sample is pressed by the right button; the StartRotation()
method for the pressed object is called. Each object can be rotated around one of nine special points; the compensation
angle depends on the currently selected rotation center for the pressed object.
World of Movable Objects 91 (978) Chapter 5 Texts
public void StartRotation (Point ptMouse)
{
Point ptAnchor = pts [(int) basis];
double angleMouse = -Math .Atan2 (ptMouse .Y - ptAnchor .Y,
ptMouse .X - ptAnchor .X);
compensation = Auxi_Common .LimitedRadian (angleMouse - angle);
}
StartRotation() method is called for any object of the Text_Spotted class pressed with the right button, but
when you continue with rotation, then you see immediately the difference between the rotation of any month or Sample.
Any month is turning around in an ordinary way. When the Sample is rotated, then all months immediately get the same
angle and rotate synchronously.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Text_Spotted &&
grobj .ID == sample .ID &&
e .Button == MouseButtons .Right)
{
for (int i = 0; i < month .Length; i++)
{
month [i] .Angle = sample .Angle;
}
}
Invalidate ();
}
}
In the Form_Texts_MoveAsSample.cs you can see only the mechanism of synchronous rotation in which a set of related
objects copy the angle from the Sample. In the plotting and tuning forms which you will see further on the same idea is
used not only for rotation but for forward movement also: sample has its own anchor point from which the shifts are
calculated; when such sample is moved around, all the related texts get the identical shifts from their anchor points.
World of Movable Objects 92 (978) Chapter 5 Texts
Comments to points
File: Form_StripCommented.cs
Menu position: Graphical objects – Texts – Comments to points
In the previous examples I have shown objects of the Text_Rotatable_Demo and Text_Spotted classes. The first
class demonstrates the features of movable and rotatable texts; the second class demonstrates the related movements of such
texts. In the real applications texts can be involved in related movements with each other, but much more often are the
situations when texts are associated with graphical objects of different shapes or with some special points of such objects, so
texts are more often involved in synchronous or related movements with objects of different shapes. Usually these are the
objects of some size, like rectangles, circles, or rings; in such cases the text position is calculated from the basic points and
borders of those objects. Association of comments with rectangles, circles, and rings is so popular that I have included into
the MoveGraphLibrary.dll three special classes: CommentToRect, CommentToCircle, and CommentToRing. All
three classes have common features which they inherit from their base class Text_Rotatable.
In further examples, especially in examples with complex objects, you will see a lot of comments to those objects. All these
comments are either of the Text_Rotatable class or derived from this class. When comment is associated with any
sizable object, positioning of comment is based on using one, two, or three coefficients which transform the relative position
to the “parent” object into absolute position of comment. When any comment is associated with some point, then there are
no sizes of the “parent” object and there are two possibilities to determine the position of such comment. The first way is to
use angle and distance from the “parent” point; this is exactly as using positioning coefficients for other classes of
comments. Another way is to declare some method which describes the relative positioning. Both ways can be used in case
of CommentToPoint class which is also derived from the Text_Rotatable class and is also included into the
MoveGraphLibrary.dll. New example demonstrates the use of the CommentToPoint class and I decided to use in this
example the positioning of comment by special method.
CommentToPoint class is derived from the Text_Rotatable class, so all the fields for visibility parameters are inside
the base class. The only new fields which are needed in the new class are the fields to define the relative position of
comment in one way or another.
public class CommentToPoint : Text_Rotatable
{
PointF ptParent;
double angleToCmnt; // from ptParent to the center of comment
double distToCmnt; // from ptParent to the center of comment
bool bUseCalcMethod = true;
Delegate_Location CalcLocation = null;
CommentToPoint object gets all the needed parameters at the moment of initialization.
public CommentToPoint (Form form, PointF ptPar, double angleToCmnt_Degree,
double fDist, string txt, Font fnt, double angleDegree, Color clr)
If positioning has to be organized by special method, then this method is declared by the
CommentToPoint.SetCalcMethod() method and in this case the distance and angle declared at the moment of
initialization do not matter at all.
Among the simplest movable objects demonstrated in the very first example of this book (Form_Nodes.cs, figure 2.1) you
can see a rounded strip. Two rounded ends of such strip are visually indistinguishable from each other and I had to add
comments to the centers of semicircles. Slightly ahead, in the chapter Curved borders. N-node covers, I’ll use rounded strip
as one of the objects which need such special type of cover. In order to explain the cover design of a strip, I had to add
some comments to the figure of the strip and there were two ways to do it. I could grab the screen view of a strip and add
comments to it in some special application, for example, in Paint. If later I decide to change this figure in the book, I would
have to repeat this process again. Another way is to prepare a small example with commented strip. When such strip is
moved or resized, each comment has to change its screen position without changing its relative position to some special
point of the strip. I had no doubts that throughout the book preparation I’ll change the strip view, size, and position not
once, so the second way of preparing figure was preferable. Thus, the Form_StripCommented.cs was born and
figures 2.2 and 7.3 were prepared with this example.
The cover design for such strips will be explained further on. Just now it is enough to mention six special points of any
rounded strip: these are two central points of semicircles and four corners of the straight part. Each special point gets its
own comment of the CommentToPoint class. The main purpose of this example is the demonstration of work with these
comments.
World of Movable Objects 93 (978) Chapter 5 Texts
This CalcCmntLocation_ctpC_0() method determines that C0 comment has to be placed on the main axis of the
strip at the distCentersCmnt distance from the associated point. Because the angle of the strip is calculated as an
angle from ptC0 to pointC1, then the angle used in calculation (strip .Angle + Math .PI) means that this
comment is always positioned outside the rectangular part of the strip. Calculation of comment position takes the
immediate effect when the CommentToPoint.UpdateLocation() method is called.
Four comments are associated with the corners of rectangular part of the strip; all four are positioned in similar way, so it is
enough to look at the initialization of one of them.
private void PrepareComments ()
{
… …
pts = strip .CornerPoints ();
ctpPts_0 = new CommentToPoint (this, pts [0],
Auxi_Convert .RadianToDegree (strip .Angle + Math .PI / 2),
distPointsCmnt, "pts[0]", fntComments, clrCmnt_Corners);
ctpPts_0 .SetCalcMethod (CalcCmntLocation_ctpPts_0);
ctpPts_0 .UpdateLocation ();
… …
}
Corners of the rectangular part are obtained with the Strip_Nnodes.CornerPoints() method and then the comment
is placed along the diameter line of semicircle outside the strip.
private PointF CalcCmntLocation_ctpPts_0 ()
{
return (Auxi_Geometry .PointToPoint (strip .CornerPoint_0,
strip .Angle + Math .PI / 2, distPointsCmnt));
}
How the needed movements of all comments are organized when the strip is moved or resized? Central points of
semicircles are obtained with the Strip_Nnodes.MiddlePoints() method and corners of the straight part with the
Strip_Nnodes.CornerPoints() method; then each comment is informed about the position of its “parent” point.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Strip_Nnodes)
{
PointF [] pts = strip .MiddlePoints ();
ctpC_0 .ParentPoint = pts [0];
ctpC_1 .ParentPoint = pts [1];
pts = strip .CornerPoints ();
ctpPts_0 .ParentPoint = pts [0];
ctpPts_1 .ParentPoint = pts [1];
ctpPts_2 .ParentPoint = pts [2];
ctpPts_3 .ParentPoint = pts [3];
}
Invalidate ();
}
}
The adjustment of the comment position depends on the way its position has to be calculated. If this positioning depends on
the angle and distance from the “parent” point, then the Strip_Nnodes.ParentPoint property uses that pair of
values to calculate the new position of comment. If the position has to be determined by special method declared at the
moment of comment initialization (this is our case!), then this method is called.
Are there other situations when positions of the strip comments in the Form_StripCommented.cs must be changed? If
positioning of all comments would be absolutely individual then nothing else would be needed. But I decided that
comments of each group must be positioned at the same distance from their associated points. One group consists of two
comments to centers of semicircles; another group includes four comments to the corners of the straight part. Thus, when
World of Movable Objects 95 (978) Chapter 5 Texts
C0 or C1 comment is released at some new position, not only the position of this comment must be adjusted by its own
method of position calculation (the released comment must be placed along the main axis), but another comment must be
placed at the same distance from its associated point as the released comment. This is done inside the OnMouseUp()
method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is CommentToPoint)
{
PointF ptCmnt = (grobj as CommentToPoint) .Location;
PointF pt;
if (id == ctpC_0 .ID || id == ctpC_1 .ID)
{
if (id == ctpC_0 .ID)
{
pt = strip .MiddlePoint_0;
}
else
{
pt = strip .MiddlePoint_1;
}
distCentersCmnt = Auxi_Geometry .Distance (pt, ptCmnt);
ctpC_0 .UpdateLocation ();
ctpC_1 .UpdateLocation ();
}
… …
In similar way, when any comment to the rectangular part of the strip is released, positions of all other comments of this
group must be updated.
We are moving from the simplest examples to more complex and the number of possibilities for individual and related
movements will increase with the growth of the number of involved elements. I decided to unify the distance from the
“parent” point to associated comment for comments of each group but I did not put any restrictions on rotation, so each
comment can be rotated individually and have its own angle. However, there are situations when the same angle for
comments of a group is very useful and I’ll demonstrate such examples further on. This will be demonstrated, for example,
with the PieChart, and in case of this class users decide about the individual or synchronous rotation. There is also a
chance to link or not to link the rotation of the main object with rotation of its associated comments. This is also
demonstrated in the example with the PieChart objects.
You have the same question of individual or synchronous changes when you start to play with visibility parameters. In the
Form_StripCommented.cs, a short menu can be called on any comment. There are only two commands in this menu – to
change font and color – but the result of using one or another standard dialog is wider than the change of the pressed
comment. I decided that in this simple example all six comments must have the same font and the color for comments of
each group must be also the same. In addition, three auxiliary lines inside the strip have the same color as two comments
for the centers of semicircles. These simplifications of tuning are against some rules of user-driven applications, but we
steadily move in the right direction.
One more change which brings us nearer to real user-driven applications: on closing the Form_StripCommented.cs, all
objects are saved in Registry and will be restored in exactly the same view the next time you call this example.
World of Movable Objects 96 (978) Chapter 6 Polygons
Polygons
The discussion of simple elements was interrupted only to describe the rotation procedure. Such interruption
was needed because a lot of elements can be involved both in forward movement and rotation. Now we can
return to some simple enough and widely used objects and include rotation into their movements. This
chapter starts with regular polygons and then other polygons are considered.
Regular polygons
File: Form_RegularPolygon_CoverVariants.cs
Menu position: Graphical objects – Basic elements – Polygons (solid) – Regular
In case you need to draw a regular unicolor polygon, you have to declare several parameters: center of a polygon; radius for
its vertices, number of vertices, angle of the first vertex, and the color to fill the figure. These parameters are used for
construction of the RegPoly_CoverVariants objects.
public RegPoly_CoverVariants (PolygonType polytype,
PointF center,
float rad,
int vertices,
double angleDegree,
Color clr)
For the purpose of initialization, angle is declared in degrees; I think it is more natural for users to describe the position
around the circle in degrees. All further angle calculations are done in radians. Calculation of points for all vertices is easy,
but I have to do it so often in many of my examples that in the MoveGraphLibrary.dll there exists the
Auxi_Geometry.RegularPolygon() method to get the vertices for a regular polygon.
protected PointF [] Vertices
{
get { return (Auxi_Geometry.RegularPolygon (ptC, radius, nVertices, angle));}
}
If polygons in the
Form_RegularPolygon_CoverVariants.cs
are shown without covers, then all these
polygons look similar and differ only by
color. When the visualization of covers is
switched ON, it is obvious that there are
three different types of covers (figure 6.1).
At the same time all these regular polygons
belong to the same class
RegPoly_CoverVariants, so the
representatives of the same class may have
different covers and, as a result, can be
moved and resized differently. Constructor
of the RegPoly_CoverVariants class
has one parameter which belongs to the
PolygonType enumeration and
determines the initial type of resizing.
Later the type of resizing can be changed;
to do this, click a polygon with the right
button and choose in the opened menu one Fig.6.1 Regular polygons with different covers which provide different
of three possibilities of resizing. Members types of resizing
of the PolygonType enumeration
clearly inform about the resizing type associated with each of them.
public enum PolygonType { NonResizable, ZoomByVertices, ZoomByBorder };
The first type of regular polygons is only movable by inner points but not resizable at all. Yellow polygon from figure 6.1,
the green one near the left border, and violet polygon belong to this type. The cover of non-resizable polygon consists of a
World of Movable Objects 97 (978) Chapter 6 Polygons
single polygonal node equal to the area of an object. It is a very rare situation that a cover consists of a single node, but it
can happen, so there is a special cover constructor for such case.
public Cover (CoverNode node)
For non-resizable regular polygons this cover constructor is used.
public override void DefineCover ()
{
PointF [] pts = Vertices;
CoverNode [] nodes;
switch (type)
{
case PolygonType .NonResizable:
cover = new Cover (new CoverNode (0, pts));
break;
… …
Polygons of the next type are resizable only by vertices; red polygon and the green one on the right side of figure 6.1 are of
this type. Each vertex of these polygons is covered by a small circular node. The movability of the whole polygon is
provided exactly in the same way as in the previous case by a big polygonal node.
public override void DefineCover ()
{
PointF [] pts = Vertices;
CoverNode [] nodes;
switch (type)
{
… …
case PolygonType .ZoomByVertices:
nodes = new CoverNode [nVertices + 1];
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i], 5);
}
nodes [nVertices] = new CoverNode (nVertices, pts);
cover = new Cover (nodes);
break;
… …
Resizing of polygons by vertices works correctly and it is not a problem for users to start the resizing by any vertex, but
there is one more variant which fits even better with the standard design of movable / resizable objects. The most common
(and expected by users!) solution is the resizing of an object by any border point. This is the third variant of regular
polygons in the Form_RegularPolygon_CoverVariants.cs; blue, cyan, and brown polygons from figure 6.1 have such
covers. In these polygons, there are no circular nodes on vertices, but each segment of perimeter between two neighbouring
vertices is covered by a strip node. While these strip nodes cover the segments between vertices, they also cover vertices
themselves, so any point of perimeter can be used for resizing in identical way. Here is the design of cover for such
polygons.
public override void DefineCover ()
{
PointF [] pts = Vertices;
CoverNode [] nodes;
switch (type)
{
… …
case PolygonType .ZoomByBorder:
nodes = new CoverNode [nVertices + 1];
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i],
pts [(i + 1) % nVertices], 5);
}
World of Movable Objects 98 (978) Chapter 6 Polygons
nodes [nVertices] = new CoverNode (nVertices, pts);
cover = new Cover (nodes);
break;
}
}
We have polygons with different types of resizing, but the code of the Form_RegularPolygon_CoverVariants.cs itself
does not have a single place where the exact type of resizing for the grabbed polygon is checked or analysed. So, where is
that piece of code which determines the resizing of the pressed polygon? Any movement of any object starts in its
MoveNode() method, so the first place to look at is the RegPoly_CoverVariants.MoveNode() method. There
are three cases in this method corresponding to different types of covers.
For non-resizable polygons, there is an automatic call of the Move() method and the whole polygon is moved.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (type)
{
case PolygonType .NonResizable:
Move (dx, dy);
bRet = true;
break;
… …
For polygons resizable only by vertices, the decision is based on the number of the pressed node. If it is the last node (the
one which covers the whole object), then the Move() method is called; all other nodes are responsible for resizing, and in
this case it does not matter which one of them is caught. The nodes on vertices are small, so if any “vertex” is caught, then
at this moment the mouse is either exactly at the vertex or very close to it. Because of this closeness, the new radius for the
vertices is simply calculated as the distance between the mouse and the center of an object.* If this distance exceeds the
minimum allowed radius, then the radius of vertices is changed thus resizing the polygon.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
double distance;
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (type)
{
… …
case PolygonType .ZoomByVertices:
if (iNode < nVertices)
{
distance = Auxi_Geometry .Distance (ptC, ptM);
if (distance >= minRadius)
{
radius = Convert .ToSingle (distance);
}
}
else
{
Move (dx, dy);
}
*
It is very easy to take into consideration the difference between the current radius and the distance from mouse to the
center, but this difference never exceeds a few pixels, so I ignored this difference. If you want higher accuracy, you can
achieve it by adding a couple of lines into the code.
World of Movable Objects 99 (978) Chapter 6 Polygons
break;
… …
There can be different variants for doing the same things. For example, in the shown code the decision about movement is
based on the number of the node but it can be also based on its shape. In the above shown code substitute the line
if (i < nVertices)
with the line
if (cover .GetNodeShape (i) == NodeShape.Circle)
The result will be the same.
For polygons resizable by any border point, the procedure is a bit more complicated than in the previous case. The strip
node connecting two consecutive vertices can be grabbed for moving at any point; the distance between this point and the
center can vary and can differ a lot from the radius of vertices, so this distance cannot be substituted as the new radius. But
it can be used for calculations of the new radius if multiplied by the special scaling coefficient. This coefficient is not going
to change throughout the resizing, so it has to be calculated once at the moment when an object is caught for resizing. The
calculation of this coefficient is done by the special method of the RegPoly_CoverVariants class.
public void StartScaling (Point ptMouse)
{
scaling = radius / Auxi_Geometry .Distance (ptC, ptMouse);
}
As I already mentioned, this coefficient is calculated once when an object is caught for resizing, so this method must be
called from the OnMouseDown() method of the form.
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 RegPoly_CoverVariants &&
mover .CaughtNodeShape == NodeShape .Strip)
{
(grobj as RegPoly_CoverVariants) .StartScaling (e .Location);
}
}
ContextMenuStrip = null;
}
Now, when you know everything about the origin and calculation of the scaling coefficient, it is easier to understand its use
in the RegPoly_CoverVariants.MoveNode() method.
scaling is the ratio between the radius of vertices and the distance from the mouse to the center of polygon calculated at
the initial moment of resizing. Throughout the resizing, the ratio between these two parameters does not change, so, while
moving the caught strip node, this coefficient is used to calculate the changing radius of vertices.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
double distance;
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (type)
{
… …
case PolygonType .ZoomByBorder:
if (iNode < nVertices)
{
distance = Auxi_Geometry .Distance (ptC, ptM);
World of Movable Objects 100 (978) Chapter 6 Polygons
float radNew = Convert .ToSingle (distance * scaling);
if (radNew >= minRadius)
{
radius = radNew;
}
}
else
{
Move (dx, dy);
}
bRet = true;
break;
}
}
return (bRet);
}
For polygons resizable by border, the decision inside the MoveNode() method about the needed action – to move the
whole polygon or resize – is also based on the number of the caught node. The last node in the cover is responsible for
moving the whole object, so the attempt to move this node results in calling of the Move() method. All other nodes are
responsible for resizing, and again it does not matter which one of them is caught, but the scaling coefficient must be
included into calculations.
For polygons of the RegPoly_CoverVariants class, you can change on the fly not only the type of resizing but also
the allowed movement. By default, any polygon can be moved freely around the screen, but the same context menu on each
polygon allows to limit such movement either to horizontal or to vertical direction. The cover of an object on such action is
not affected at all, but the RegPoly_CoverVariants.Move() method which is responsible for moving the whole
polygon has three different options. Any polygon is grabbed and moved by the mouse in the same way, or better to say that
the mouse is moved in the same way, but, depending on the type of the allowed movement, either both of the parameters are
used by the RegPoly_CoverVariants.Move() method or only one of them.
public override void Move (int dx, int dy)
{
switch (movetype)
{
case MovementAllowed .Any: ptC += new Size (dx, dy); break;
case MovementAllowed .Horizontal: ptC .X += dx; break;
case MovementAllowed .Vertical: ptC .Y += dy; break;
}
}
There is no visual indication of the type of movement which is currently allowed for each polygon. If you want, you can
easily add such indication, for example, somewhere in the center of a polygon. For example, there can be an arrow Left –
Right, or Up – Down, or both of them. But I do not think that such indication is needed. I will return to the discussion of
changing the movability of objects further on in the book.
I am not going to delete or rewrite the previous several lines, but at the last moment I added a special type of visualization
which informs about the possible movement for each polygon. The node which is responsible for moving the whole object
is always the same: it is the polygonal node which covers the whole object. It is always the last node regardless of the cover
type and I can do the same analysis of allowed movement which you see further on by checking the number of the node.
Yet, I decided to do it differently just to demonstrate one more possibility. The node for moving polygons is the only
polygonal node in all three variants of covers, so this part of code is based on the shape of nodes.
By default, the polygonal nodes get the standard cursor Cursors.SizeAll. For polygons with unrestricted movement
the default cursor is used. When movement is restricted to one direction only, then the changed cursor signals about the
allowed movement.
public override void DefineCover ()
{
… …
if (movetype == MovementAllowed .Horizontal)
{
cover .SetCursor (NodeShape .Polygon, Cursors .SizeWE);
World of Movable Objects 101 (978) Chapter 6 Polygons
}
else if (movetype == MovementAllowed .Vertical)
{
cover .SetCursor (NodeShape .Polygon, Cursors .SizeNS);
}
}
Polygons in the Form_RegularPolygon_CoverVariants.cs are constructed with different types of covers. Later the cover
of any polygon can be changed to a different type; such change means another type of resizing for this polygon. Also the
order of polygons on the screen can be changed as any of them can be put on top of all others by a simple left button click.
Let us look at the details of all these things.
The set of polygons is organized when the form is constructed. There are more polygons in the form than are shown at
figure 6.1. When the form is loaded, nine polygons are constructed with the number of vertices changing from three to 12.
List<RegPoly_CoverVariants> polygons = new List<RegPoly_CoverVariants> ();
// -------------------------------------------------
private void OnLoad (object sender, EventArgs e)
{
… …
InitPolygons ();
RenewMover ();
bAfterInit = true;
}
private void InitPolygons ()
{
polygons .Clear ();
for (int i = 3; i < 12; i++)
{
Point pt = new Point (
notCloser + rand.Next (nSeed) % (ClientSize.Width - 2 * notCloser),
notCloser + rand.Next (nSeed) % (ClientSize.Height - 2 * notCloser));
int rad = minRadius + rand .Next (nSeed) % 120;
int angleDeg = rand .Next (nSeed) % 360;
Color clr = Auxi_Colours .ColorPredefined (i);
polygons .Add (new RegPoly_CoverVariants ((PolygonType) (i % 3),
pt, rad, i, angleDeg, clr));
}
}
The first objects in mover queue must be controls and only after them all graphical objects, so we have such order of objects
in the case of the Form_RegularPolygon_CoverVariants.cs
1. Button to show information.
2. Button to switch covers ON and OFF.
3. Information, if it is visible.
4. All polygons in the same order as they are included into their List.
private void RenewMover ()
{
mover .Clear ();
mover .Add (scHelp);
mover .Add (scCovers);
info .IntoMover (mover, mover .Count);
foreach (RegPoly_CoverVariants poly in polygons)
{
mover .Add (poly);
}
if (bAfterInit)
{
World of Movable Objects 102 (978) Chapter 6 Polygons
Invalidate ();
}
}
Buttons and information never change their places in the mover queue, but polygons can be easily reordered. It is enough to
click any polygon with the left button to put it on top of all others. If the distance between the points of pressing and
releasing the left button is greater than three pixels, then it is considered as an ordinary forward moving of the pressed
polygon; the lesser distance is considered as a request to move the pressed polygon into the head of the List.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is RegPoly_CoverVariants && fDist <= 3)
{
PopupPolygon (grobj .ID);
}
}
… …
To decide about relocation of the pressed polygon into the head of the List, I have first to find the current position of this
pressed polygon in the List. This is done in the PopupPolygon() method by comparison of the id of the pressed
polygon with id of all polygons in the List. Change of order among movable objects requires the renewal of the mover
queue; this is done by the RenewMover() method.
private void PopupPolygon (long id)
{
for (int i = polygons .Count - 1; i > 0; i--)
{
if (id == polygons [i] .ID)
{
RegPoly_CoverVariants poly = polygons [i];
polygons .RemoveAt (i);
polygons .Insert (0, poly);
RenewMover ();
break;
}
}
}
Two things are organized through the commands of context menu which can be
called on any polygon (figure 6.2):
Fig.6.2 Menu on polygons to change
• Change of cover which means the change of the resizing type.
the resizing and allowed movement
• Change of the allowed movement.
There are two groups of commands in menu; each group consists of three commands. As usual, the decision about opening
menu is done inside the OnMouseUp() method, but before opening this menu, the identification of the pressed polygon
is done with the Identification() method.
The Identification() method goes through the List of polygons and finds the touched one by comparison of id
numbers.
private void Identification (long idClicked)
{
iPolyTouched = -1;
for (iPolyTouched = polygons .Count - 1; iPolyTouched >= 0; iPolyTouched--)
{
World of Movable Objects 103 (978) Chapter 6 Polygons
if (idClicked == polygons [iPolyTouched] .ID)
{
return;
}
}
}
First three commands of menu allow to change the resizing type of polygon which means the change of its cover. There is a
possibility that at this moment the visualization of covers in the form is switched ON; in this case the repainting of the form
is needed. I included into the code the unconditional repainting. Here is the code of the method associated with the third
command of menu; after this command the pressed polygon becomes resizable by any border point.
private void Click_miResizableByBorder (object sender, EventArgs e)
{
polygons [iPolyTouched] .PolygonType = PolygonType .ZoomByBorder;
Invalidate ();
}
Commands to change the possible movement of the pressed polygon do not require any repainting. Here is the method
associated with the last command of menu; after this command the pressed polygon can be moved only vertically.
private void Click_miVerMove (object sender, EventArgs e)
{
polygons [iPolyTouched] .MovementType = MovementAllowed .Vertical;
}
All commands of this menu change some property of the pressed polygon but they change neither the order of movable
objects nor their number, so there is no need to call the RenewMover() method.
Convex polygons
File: Form_Polygons_Convex.cs
Menu position: Graphical objects – Basic elements – Polygons (solid) – Convex
For moving / resizing of all objects only three types of nodes are used. One of these types is polygon, and the only
condition for any polygonal node is that it must be convex. In all the previous examples, the polygonal nodes were either
rectangles or regular polygons. But there is no requirement that polygonal node must be of a “simple” or “standard” shape;
it can be an arbitrary polygon as far as it is convex. Let us look at the example of such arbitrary polygonal nodes.
Objects of the ConvexPolygon class are very simple. Each one is determined by a set of vertices and some color is
needed for painting (figure 6.4; I am sorry, but the only suitable place for this figure is two pages ahead). There is also a
limit on minimal distance between the neighbouring vertices, so vertices cannot block each other and all of them are always
in view.
public class ConvexPolygon : GraphicalObject
{
PointF [] pts;
SolidBrush brush;
PointF ptInitial; // at the starting moment of reconfiguring
static double minSegment = 15.0;
World of Movable Objects 106 (978) Chapter 6 Polygons
The easiest way to initialize a convex polygon is to declare it as a regular polygon with the needed number of vertices; this
guarantees the initial convexity. There is also a special method Auxi_Geometry.ConvexPolygon() which returns
an array of vertices for convex polygon.
public ConvexPolygon (PointF ptC, float rad, int nVertices, double angleDegree,
Color color)
{
pts = Auxi_Geometry .ConvexPolygon (ptC, rad, nVertices,
Auxi_Convert .DegreeToRadian (angleDegree));
brush = new SolidBrush (color);
}
There is another constructor in which you can simply declare the set of vertices, but the checking of convexity for the
proposed set of vertices must be done before using this constructor. (In the MoveGraphLibrary.dll, there is an
Auxi_Geometry.PolygonConvexity() method to do such checking.)
public ConvexPolygon (PointF [] points, Color color)
{
pts = points;
brush = new SolidBrush (color);
}
ConvexPolygon objects can be involved in two different movements:
• Polygon can be reconfigured by changing the position of any vertex.
• Polygon can be moved around the screen.
Reconfiguring is provided by placing a small circular node over each vertex; to make reconfiguring easier, I slightly
enlarged radius of these nodes from their default value. Forward movement of the whole polygon is provided by a single
polygonal node which has exactly the same shape as an object.
public override void DefineCover ()
{
int nVertices = pts .Length;
CoverNode [] nodes = new CoverNode [nVertices + 1];
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i], 4);
}
nodes [nVertices] = new CoverNode (nVertices, pts);
cover = new Cover (nodes);
}
Basic points of any convex polygon are its vertices, so the ConvexPolygon.Move() method provides the synchronous
change of all these points.
public override void Move (int dx, int dy)
{
SizeF shift = new SizeF (dx, dy);
for (int i = 0; i < pts .Length; i++)
{
pts [i] += shift;
}
}
MoveNode() method describes the reaction on moving any particular node. All nodes except the last one are responsible
for reconfiguring. These nodes differ from the last one by shape, so this branch of code can be started either by checking
the node number (it is shown in the code) or its shape (this variant is commented).
When you try to move some vertex, you start from the situation of definitely convex polygon and have to check whether the
proposed movement of the pressed vertex is going to keep the polygon convex or not. All other vertices stay at their places.
There is an Auxi_Geometry.ConvexPolygonVertexAllowed() method which checks the convexity for moving
of one vertex, so this method is used inside the ConvexPolygon.MoveNode() method.
World of Movable Objects 107 (978) Chapter 6 Polygons
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode < pts .Length) // or cover [i].Shape == NodeShape.Circle
{
PointF ptNew = new PointF (pts[iNode] .X + dx, pts[iNode] .Y + dy);
if (Auxi_Geometry .ConvexPolygonVertexAllowed (pts, iNode, ptNew,
minSegment))
{
pts [iNode] = ptNew;
bRet = true;
}
}
else
{
Move (dx, dy);
}
}
return (bRet);
}
In some classes of objects from the
previous examples I purposely set
a restriction on minimal allowed
size; these restrictions prevent the
disappearance of objects after
squeezing. In the convex
polygons, the disappearance by
squeezing is impossible as I put a
restriction on minimal segment
length, but there is similar problem
of possible disappearance as a
result of reconfiguring. The
easiest way to see such situation is
to place two vertices of triangle on
one horizontal line and then move
the third vertext to the same line.
Depending on the positions of
vertices, you can see either part of
the line or nothing at all. I am
writing about the view of a Fig.6.4 Elements of the ConvexPolygon class
polygon with its cover switched
OFF. There can be configurations in which it is difficult to see the exact vertex location; in such case switch the covers ON
and there will be no problems for reconfiguring.
Still, it is not a good situation, especially because in real applications the covers are never switched ON. I am writing about
the possibility of bad situation after reconfiguring and there are different possible solutions to prevent such situation. For
example, you can limit inner angles by something lesser than 180 degrees and then you will always see convex polygon.
There is one missing feature in the shown convex polygons – they are not resizable. In all the examples I try to demonstrate
that all objects are movable by any inner point and resizable by any border point. Why these convex polygons are non-
resizable? The explanation is simple: there is no obvious point from which the scaling must be calculated. It is not a
problem at all to organize scaling of these polygons, but there must be a point to which such convex polygon can squeeze
and from which it can expand. In the next chapter I am going to demonstrate an example of more complex convex polygons
with holes. Those holes have a shape of regular polygon, so there is a central point of such hole and the same central point
can be used for zooming of the outer border. So it is not a problem to zoom convex polygon, but some point must be
declared as a basic point for such zooming.
World of Movable Objects 108 (978) Chapter 6 Polygons
Triangles
File: Form_Triangles.cs
Menu position: Graphical objects – Basic elements – Triangles
Throughout the previous examples I was moving from the simplest to more and more complicated objects. Let us change
the trend, make a step back, and play with triangles. Objects of the Triangle class can be reconfigured by any vertex, so
it is the same reconfiguring which was demonstrated with convex polygons in the previous example, but there are no
restrictions in the new case, so any triangle can be reconfigured into any other triangle, into a line, or even into a single dot.
With such transformations, there can be some strange or even amusing results: a triangle can disappear. Certainly, when I
write about transformation of triangle into a line or even its total disappearance, I am talking about the case without cover
visualization. When the covers are visualized, then positions of all vertices are obvious and it is easy to return a normal
view of any triangle. In the real applications the covers are rarely or never visualized, so the discussion of some strange
possible situations can be very helpful. The possibility of cover visualization in this example will help to analyse several
cases.
I want to demonstrate some border situations of
unconstrained resizing. It is even possible to
take normal movable and resizable triangle and
reconfigure it in such a way that it will still look
like a triangle but it will turn into unmovable
until some additional reconfiguration. Let us
analyse such situations. Understanding of such
situations will help to prevent them in case of
more complex objects in real applications.
When three vertices of triangle are placed on a
straight line then, depending on the position of
the middle vertex, you can see a thin line, or a
collection of separate dots, or even nothing. A
triangle also disappears from view when all
three vertices are moved into the same point;
only the visualization of cover can help in
finding such object.
The cover design for these triangles is obvious
from figure 6.5: three small circular nodes on
vertices and the polygonal (in this case
triangular) node covering the whole area of an
object. At the same time, there is obvious
difference in view near the vertices of yellow Fig.6.5 Covers of the Triangle objects may have different order
and blue triangles; this difference is caused by of nodes; this is perfectly seen in the area of vertices.
the change of nodes order in their covers.
When covers are visualized and the size of circular nodes is increased, the order of nodes for each triangle is easily detected
from the figure. Covers are painted so as to demonstrate the order of nodes for mover analysis. Mover starts from node
with number zero and moves on according to the increasing numbers, so the drawing of nodes starts from the last one and
goes to the first node in the cover. Also do not forget that the inner area of circular nodes is cleaned on visualization.
• If you see the clean circles on the corners of triangles, then it means that they are painted after triangular node, so
cover starts with these nodes.
• If you see corners of triangular node inside circles, then triangular node was painted after circular nodes, so
triangular node is the first one in the cover.
The order of nodes is determined by two values from the NodesOrder enumeration; the names of these values speak for
themselves.
public enum NodesOrder { CirclesTriangle, TriangleCircles };
Geometry of any triangle is described by three points – coordinates of its vertices. For painting we need some brush; for
better visualization of covers we can change radius of circular nodes; the order of nodes is determined by some value from
the NodesOrder enumeration.
World of Movable Objects 109 (978) Chapter 6 Polygons
public class Triangle : GraphicalObject
{
NodesOrder m_orderNodes;
PointF [] pts;
SolidBrush brush;
int radiusNode;
There is a standard rule of organizing covers: small nodes must precede bigger nodes. Usually circular nodes are small
while polygonal nodes are much bigger. According to this rule, the polygonal node to cover the whole triangle area must be
the last one in the cover of Triangle objects and this is correct for the case of NodesOrder.CirclesTriangle.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [4];
switch (m_orderNodes)
{
case NodesOrder .CirclesTriangle:
for (int i = 0; i < 3; i++)
{
nodes [i] = new CoverNode (i, pts [i], radiusNode);
}
nodes [3] = new CoverNode (3, pts);
break;
… …
}
cover = new Cover (nodes);
}
For the NodesOrder.TriangleCircles case, the same nodes are used but in different order: the biggest polygonal
node takes the leading position.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [4];
switch (m_orderNodes)
{
… …
case NodesOrder .TriangleCircles:
nodes [0] = new CoverNode (0, pts);
for (int i = 0; i < 3; i++)
{
nodes [i + 1] = new CoverNode (i + 1, pts [i], radiusNode);
}
break;
}
cover = new Cover (nodes);
}
The order of nodes is declared at the moment of initialization for Triangle objects. To underline the fact that the order
of nodes “circles first, then polygon” is looked at as a standard case, it is possible to skip the declaration of nodes order in
constructor and in such case the NodesOrder.CirclesTriangle is used by default. This is demonstrated by the
green triangle.
private void InitTriangles ()
{
triangles .Clear ();
triangles .Add (new Triangle (null, Color .Lime));
triangles .Add (new Triangle (NodesOrder.CirclesTriangle, …, Color.Blue));
triangles .Add (new Triangle (NodesOrder.TriangleCircles, …, Color.Yellow));
triangles .Add (new Triangle (NodesOrder.TriangleCircles, …, Color.Cyan));
}
World of Movable Objects 110 (978) Chapter 6 Polygons
In triangle, all three vertices are its basic points, so all of them must be synchronously moved by the Triangle.Move()
method.
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;
}
}
Triangle.MoveNode() method is also simple. There are no restrictions on individual movements of circular nodes, so
any caught circular node is simply moved for required number of pixels along both axes. If the polygonal node is caught,
then the whole object is moved. These rules work for both cases of the nodes order; two parts of the code differs only by
the use of the node numbers.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (m_orderNodes)
{
case NodesOrder .CirclesTriangle:
if (iNode == 3)
{
Move (dx, dy);
}
else
{
SizeF size = new SizeF (dx, dy);
pts [iNode] += size;
bRet = true;
}
break;
case NodesOrder .TriangleCircles:
if (iNode == 0)
{
Move (dx, dy);
}
else
{
SizeF size = new SizeF (dx, dy);
pts [iNode - 1] += size;
bRet = true;
}
break;
}
}
return (bRet);
}
All three crucial methods – DefineCover(), Move(), and MoveNode() – for moving and reconfiguring of
Triangle objects are simple. There are no restrictions on moving of any node, so where the problems with these
triangles are coming from? Let us start with reconfiguring of triangle into a line. In this demonstration I purposely use
triangles with different order of nodes in their covers because this curious situation does not depend on this order and can
happen with any triangle.
1. Switch ON the visualization of covers and position two triangles as shown at figure 6.6.a. Two vertices of each
triangle are placed at exactly the same horizontal line. Visualization of covers makes such positioning much easier
World of Movable Objects 111 (978) Chapter 6 Polygons
because the borders of polygonal nodes are shown with thin red lines and our eyes can see even a one pixel fracture
of such line.
2. Now move the third vertex anywhere between other two and place it at the same horizontal line; this is easily
visually controlled as our triangle has to turn into a straight red line without any fractures (figure 6.6.b).
Fig.6.6.a One side of each triangle is strictly horizontal Fig.6.6.b Move the third vertex inline with other two
Now move the mouse cursor across this red line. You will see that mouse cursor does not change its shape over the line; it
means that mover does not detect this triangle which is turned into a line. If you have three vertices on a line, you see three
circular nodes at their positions and a line connecting them. As a result, there is no real area by which an object can be
moved to another place. It is the problem of a polygonal node defined by three points and becoming non-convex. Any
three points not placed on the same line define a convex polygon. But when these three points are on the same line, then
there is no polygon at all; the area of such polygon is null; mover cannot detect it and mover cannot call the Move()
method in such situation because it does not detect any node.. When all three vertices are on the same line, their personal
nodes can be still moved individually as each circular node is still detected, but the whole object cannot be moved. Only
when one of the vertices is moved from the same line with others, the object will gain some area by which it can be moved
again.
You can make one more step in situation from figure 6.6.b and switch the covers OFF. As a result, everything will
disappear; there will be neither straight lines nor circles. The lines are still there, but where are they? Do you like such
situation? I doubt. There is still a chance that while the cursor is moved around the screen it will cross one of the invisible
circular nodes and the change of cursor will signal you that you have a chance to press the mouse and try to move
something invisible aside. For this example, I purposely enlarged all circular nodes. In normal situation they are much
smaller and the chance of detecting such small invisible circular node is slim.
What can be done as a protection from such situation? Solutions can be different but they are of the same type as was done
with rectangles. For example, there can be a restriction on angle between two sides or minimal allowed distance from any
vertex to the opposite side of triangle. Any developer has to make his own decision, but any possibility of the ghost object
must be excluded.
Let us turn to another special situation in which the order of nodes in the cover can play its role. I am speaking about the
situation when by moving vertices you squeeze normally looking triangle into really small one. Try such steps one by one.
Fig.6.7.a Approximately equilateral Fig.6.7.b Triangles are squeezed by Fig.6.7.c Sizes are not changed, but
triangles moving their vertices one covers are switched ON.
after another
1. Switch the visualizations of covers OFF and make two triangles approximately equilateral (figure 6.7.a).
World of Movable Objects 112 (978) Chapter 6 Polygons
2. Squeeze triangles by moving one vertex after another but try to keep triangles nearly equilateral (figure 6.7.b).
3. Now try to move triangles without changing them. With the yellow one, you will have no problems: anywhere
inside this triangle the cursor changes its shape from the Cursors.Hand to Cursors.SizeAll and this
signals you that at such moment by pressing the left button you can grab this triangle and move it anywhere around
the screen without changing the configuration of triangle. Try to move the small blue triangle and you’ll find that
it is impossible because at any point inside this blue triangle the cursor still has the shape of a hand. It informs you
that by pressing the left button you can start some kind of reconfiguration but not the movement of the whole
object. To understand the difference in movements of two small triangles, switch ON the covers (figure 6.7.c).
Instead of blue triangle, you now see three overlapping circular nodes. These three circles cover the whole area of triangle,
so they totally block from mover the polygonal (in this case – triangular) node which stays in the cover of the blue triangle
behind circular nodes. If mover does not feel the polygonal node then mover can’t use it and for such combination of
triangle geometry and node sizes this blue triangle becomes unmovable. It is unmovable until the moment when you move
one of the circular nodes farther away from two other circular nodes and thus open a gap among them through which the
mover will see the underlying polygonal node.
Yellow triangle has approximately the same size as the blue one and three circular nodes in its cover overlap in exactly the
same way, but the polygonal node takes the leading position in the cover of this yellow triangle and it is always seen by
mover. Nodes are analysed according to their order in the cover, so this yellow triangle is always movable regardless of its
size. At the same time, yellow triangle is always reconfigurable as parts of circular nodes are never blocked by the leading
polygonal node.
Does it mean that the standard practice of positioning the nodes in the cover – first circles, then strips, and polygons at the
end – is incorrect and must be changed? I shall not say so. The standard practice of positioning nodes in the mentioned
order is based on very many examples. There can be special cases when you would prefer to break this rule, but in case of
triangles I’ll not recommend to do it.
To illustrate this special case of movability problem, I purposely increased the radius of circular nodes to such value
(20 pixels!) which is never used in real applications. Usually the circular nodes are much smaller (radius of three – five
pixels) and to demonstrate the same situation with normally used radius, you’ll have to squeeze triangles to such small size
that they will become hardly visible. This means that such squeezing of triangles must be avoided in some other way. Such
ways were demonstrated with rectangles: you either set restriction on minimal size of an object or consider such squeezing
to a tiny size as a command to delete the object.
I hope that while squeezing the triangles by moving their vertices one by one you had a feeling that that was definitely the
wrong way of resizing those objects. Certainly, there has to be much easier way of resizing, but it can be used only after
some change of the covers. This is one of interesting things that you will see in the next example.
World of Movable Objects 113 (978) Chapter 6 Polygons
Chatoyant polygons
Titles of the previous sections in this chapter tell about the type of demonstrated objects; for some of those objects the
corresponding title also announces the most interesting feature of resizing. In the name of the current section, there is no
information about possible change of the involved objects and I doubt that I can formulate it in a short way. I cannot
explain in a word or two, what can be done with these polygons as they can be transformed into figures which I could not
even imagine before I thought out such reconfiguring. Objects of the ChatoyantPolygon class demonstrate some
features which were already shown in the examples of regular polygons and triangles, but this is only a small part of what
can be done with these new polygons.
Cover of the ChatoyantPolygon class uses the nodes of all three possible types to organize moving, resizing, and
reconfiguring of objects. When a ChatoyantPolygon 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 this polygon and the union of the
polygonal nodes covers the whole area of an object. Later, when any circular node can be moved around the screen to an
arbitrary place, the point associated with this node (it can be one of the vertices or central point) also moves and polygon
quickly changes its shape. As a result, the strips can partly cover the perimeter and partly be inside the area of this object,
but an object can be still moved, reconfigured, and zoomed in the same way.
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 object.
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 for 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
ChatoyantPolygon class:
1. N circular nodes on the vertices.
2. One circular node on the central point.
3. N strip nodes between the neighbouring vertices.
4. N triangular nodes for the inner area.
World of Movable Objects 116 (978) Chapter 6 Polygons
To simplify the reconfiguring of these polygons, radius of all circular nodes is slightly enlarged.
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);
}
Such polygon with N vertices has (N + 1) independently movable basic points, so the Move() method has to change
synchronously these (N + 1) points.
public override void Move (int dx, int dy)
{
SizeF shift = new SizeF (dx, dy);
for (int i = 0; i < VerticesNum; i++)
{
ptVertices [i] += shift;
}
m_center += shift;
}
Any movement of a polygon starts when this polygon is pressed with a mouse. Depending on the pressed button and the
caught node, it can be a command for reconfiguring, resizing, forward movement of the whole figure, or rotation. All four
possibilities are demonstrated by these polygons. As usual, some preliminary calculation must be made at the starting of
resizing and rotation, so resizing starts with calling the ChatoyantPolygon.StartScaling() method and rotation
starts with calling the ChatoyantPolygon.StartRotation() 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 ChatoyantPolygon)
{
ChatoyantPolygon poly = grobj as ChatoyantPolygon;
if (e .Button == MouseButtons .Left)
{
poly .StartScaling (e .Location, mover .CaughtNodeShape);
}
else if (e .Button == MouseButtons .Right)
{
poly .StartRotation (e .Location);
}
World of Movable Objects 117 (978) Chapter 6 Polygons
}
}
ContextMenuStrip = null;
}
The real movements of our polygons are described by the ChatoyantPolygon.MoveNode() method, but two types
of movement require some preliminary actions.
All vertices can be moved individually without affecting each other, so there is no way to describe the positions of all of
them by one or two general parameters. Instead, the position of each vertex is defined by the distance and the angle from
the central point, so two sets of values must be calculated before rotation or zooming is started. This is done by the
ChatoyantPolygon.FillVerticesArrays() method.
private void FillVerticesArrays ()
{
for (int i = 0; i < VerticesNum; i++)
{
radii [i] = Auxi_Geometry .Distance (m_center, ptVertices [i]);
angles [i] = Auxi_Geometry .Line_Angle (m_center, ptVertices [i]);
}
}
We have already explored several examples of rotation and know the main rule of such movement. Before starting any
rotation, the compensation angle must be calculated. Because vertices in these polygons are independently movable, then
individual compensation angle for each vertex must be calculated.
public void StartRotation (Point ptMouse)
{
FillVerticesArrays ();
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
for (int i = 0; i < VerticesNum; i++)
{
compensation [i] = Auxi_Common.LimitedRadian (angleMouse - angles [i]);
}
}
For the same reason – existence of independently movable vertices – not one scaling coefficient but a whole array of
coefficients for all vertices must be calculated at the moment when zooming starts. Scaling starts only if any strip node is
pressed; this condition is not checked inside the OnMouseDown() method of the form, so it must be checked at the
beginning of the ChatoyantPolygon.StartScaling() method.
public void StartScaling (Point ptMouse, NodeShape nodeshape)
{
if (nodeshape == NodeShape .Strip)
{
FillVerticesArrays ();
double distanceMouse = Auxi_Geometry .Distance (m_center, ptMouse);
for (int i = 0; i < VerticesNum; i++)
{
scaling [i] = radii [i] / distanceMouse;
}
}
}
With all the preparations finished, we can look now into the ChatoyantPolygon.MoveNode() method which
defines all possible movements of our polygons. In the ChatoyantPolygon class, the type of movement initiated by
pressing any node is determined by the number of node; the order of nodes was mentioned above and can be seen from the
DefineCover() method.. If one of the vertices is moved, then it is only an individual relocation of this vertex and
nothing else.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
World of Movable Objects 118 (978) Chapter 6 Polygons
{
if (iNode < VerticesNum)
{
ptVertices [iNode] += new SizeF (dx, dy);
}
If it is the node over central point, then this central point is moved.
else if (iNode == VerticesNum)
{
m_center += new SizeF (dx, dy);
}
Triangular nodes compose the last group in the cover. If one of them is pressed, then the whole object is moved by calling
the Move() method.
else if (iNode >= 2 * VerticesNum + 1)
{
Move (dx, dy);
}
Otherwise it can be only one of the strip nodes; in this case the set of scaling coefficients is used for zooming.
else
{
double distanceMouse = Auxi_Geometry .Distance (m_center, ptM);
if (distanceMouse > 25)
{
for (int j = 0; j < VerticesNum; j++)
{
ptVertices [j] = Auxi_Geometry .PointToPoint (m_center,
angles [j], distanceMouse * scaling [j]);
}
}
}
If the move was started by the right button, then, regardless of the caught node, it is rotation. Previously calculated sets of
radii and compensation angles for all vertices are used to calculate their new positions.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right && bRotate)
{
double angleMouse = -Math .Atan2 (ptM .Y - m_center .Y,
ptM .X - m_center .X);
for (int j = 0; j < VerticesNum; j++)
{
ptVertices [j] = Auxi_Geometry .PointToPoint (m_center,
Auxi_Common .LimitedRadian (angleMouse - compensation [j]),
radii [j]);
}
DefineCover ();
return (true);
}
return (false);
}
In this piece of code you can see that not only the pressed button is checked, but there is also another check for the value of
the bRotate field. This field is assigned a true value in the constructor of the ChatoyantPolygon class; later
World of Movable Objects 119 (978) Chapter 6 Polygons
this value can be changed by the Rotatable property. In the current example the value of the bRotate field can be
set via the command of menu which can be called on any polygon. I can’t think out a good visible mark to inform about the
possibility of rotation for such polygons, so in both cases – rotatable or non-rotatable – polygon looks identically. If you
can’t rotate any polygon, first call this menu and look at the line with this command. If there is no check mark before the
command line, then it means that you have switched this property OFF beforehand and forgot about it.
Here is one more remark about zooming. In the current version, the ChatoyantPolygon.StartScaling()
method gets two parameters; the second one is the shape of the pressed node.
public void StartScaling (Point ptMouse, NodeShape nodeshape)
Zooming is supposed to start only with the strip nodes, so this method starts with the check of the node shape. You can
easily move this checking one level up into the OnMouseDown() method of the form and call the StartScaling()
method only when some strip node is pressed. In this way the StartScaling() method will have only one parameter
and will look similar to the StartRotation() method. I even put the needed lines of code into the
OnMouseDown() method but commented them. There are no advantages or disadvantages in either way. At least, I
don’t see them and in further examples randomly select one way or another.
The example with the chatoyant polygons is the first to demonstrate that with the help of those primitive nodes not only the
simple objects can be moved but much more complicated.
A program is poorly designed if the same mouse clicks in different forms produce different results. In any real program I
avoid such situation, but this is a Demo program and in different examples of this program I purposely demonstrate similar
but slightly different reactions on the same mouse clicks. I want to demonstrate how mover can be involved in all of them
and what changes must be made in the code. This Form_Polygons_Chatoyant.cs is not the first example in which the
order of objects on the screen is changed by a simple click of the left button; there are such variants in previous examples.
• The most often used is the situation when the clicked object is moved on top of all others. First this change is
made in the List of objects and then the RenewMover() method uses this List to update mover queue.
You can see this, for example, in the Form_Rectangles_Standard.cs and
Form_RegularPolygon_CoverVariants.cs.
• When the number of demonstrated objects is not changed throughout the work of some example, then it is possible
to change the order of objects directly in the mover queue. This is done in the Form_Circles_Nonresizable.cs
which has no RenewMover() method. The clicked circle is moved on top of all others by putting it ahead of all
other circles in the mover queue.
• In the Form_Rectangles_FixedRatio.cs the order of rectangles is also changed inside the mover queue, but in that
example the clicked rectangle is moved only one level up, so positions of two rectangles in the mover queue are
reversed and this is all.
This is a developer’s choice to move the clicked object only one level up or on top of all other objects; I demonstrate both
variants. Whether to use a special List of objects or change their order directly in the mover queue depends on the set of
actions organized in each particular example. If there is a fixed set of objects and only their order in this set can be
changed, then you can do everything inside the mover queue. If there are possibilities for adding and deleting objects, then
the variant with separate List of objects and RenewMover() method is preferable. Beginning from the first version
of the book which appeared in the fall of 2010, the Form_Polygons_Chatoyant.cs used a fixed set of four polygons, so any
clicked polygon was moved one level up by direct use of the mover queue. While rewriting this book in 2014, I slightly
changed this example by allowing to add new polygons and this caused some related changes in the code. In the new
version, and I hope not to change it any more, the clicked polygon reverses positions with the previous one in the List of
polygons and then the RenewMover() method is called to update positions of all objects in the mover queue.
private void PolygonOneLevelUp (long idPoly)
{
for (int i = polygons .Count - 1; i > 0; i--)
{
if (idPoly == polygons [i] .ID)
{
polygons .Reverse (i - 1, 2);
RenewMover ();
break;
}
}
}
World of Movable Objects 120 (978) Chapter 6 Polygons
We are moving to more and more complex objects and step by step I include into the examples the features which are
standard for user-driven applications.
Objects in all previous examples were extremely simple and I decided that saving / restoring of primitive rectangles, circles,
or regular polygons was not needed. Chatoyant polygons can be turned into very interesting objects and there is a
possibility that some users will like to save them, at least, until the next session. As a result, the ChatoyantPolygon
class got the needed methods and now everything in the Form_PolygonsChatoyant.cs is saved and restored.
In this example you work not only with initially designed polygons, but you can add new polygons at any moment. Though
it looks like a very small addition, it has several consequences.
• If you have an instrument to add new elements, you need similar simple instrument to delete elements.
• Throughout possible transformations of some polygon, you may receive some shape with different interesting
variants for further transformation. If you want to try all of them, then you need an easy way to get a copy of any
polygon at any moment.
• While playing with transformations of those polygons, you might often feel that the best way to continue would be
to delete a vertex or to add one or several. Thus, it would be nice to have such commands at hand.
• By adding new objects, you can come to an overcrowded form and may decide that the next best step will be to
return directly to the default view and start again. Thus, you need a command to return directly to the initial view.
There are several more possibilities which are not shown with this example only because I don’t want to make it too
complicated, but with nearly each one of the above remarks I put a comment that “it would be nice to have a command …”.
All these commands can be divided into two big groups: some commands deal with polygons individually while others are
applied to all of them. In general, and you will see this in many examples further on, I will organize two context menus:
one with commands to deal with the pressed object and another menu with general commands for the form. In the
Form_PolygonsChatoyant.cs, I decided to increase the number of context menus, so three different menus (!) can be
called on any polygon (figures 6.10).
As usual, the decision about calling one or another menu and some other decisions like erasing the information, changing
the level of the pressed polygon, or opening a tuning form for information, all these decisions are made when a mouse
button is released.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObject;
NodeShape shapeNode;
if (mover .Release (out iObject, out iReleasedNode, out shapeNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is ChatoyantPolygon && fDist <= 3)
{
PolygonOneLevelUp (grobj .ID);
}
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
if (grobj is ClosableInfo)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, null,
PointToScreen (ptMouse_Up));
}
else if (grobj is ChatoyantPolygon)
{
polyPressed = grobj as ChatoyantPolygon;
switch (shapeNode)
{
case NodeShape .Polygon:
World of Movable Objects 121 (978) Chapter 6 Polygons
ContextMenuStrip = menuOnPolygonalNode;
break;
case NodeShape .Circle:
ContextMenuStrip = menuOnCircularNode;
break;
default:
ContextMenuStrip = menuOnStripNode;
break;
}
}
}
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
}
It happened so that I need to start different commands when different parts of polygon are pressed. These parts are covered
by nodes of three different shapes, so it is absolutely natural in this case of chatoyant polygons to base the menu selection
on the shape of the pressed node.
Fig.6.10a Menu on polygonal Fig.6.10b Menu on Fig.6.10c Menu on strip Fig.6.10d Menu at empty
nodes circular nodes nodes places
• When the mouse is pressed anywhere inside the area of polygon (any one of polygonal nodes), then the opened menu –
menuOnPolygonalNodes – contains the commands for the whole object. The pressed polygon can be duplicated
or deleted; it is also possible to change the possibility of rotation for this polygon (figure 6.10a).
• When the released node is circular, then with a high probability it is a node over some vertex, though it can be also a
node over the central point of this polygon. The first command in the menuOnCircularNodes is always available
and allows to change the color of the pressed point (figure 6.10b). Another command of this menu makes sense only
for some vertex and only when there are more than three vertices in the pressed polygon. Both conditions are checked
when menu is going to be opened.
private void Opening_menuOnCircularNode (object sender, CancelEventArgs e)
{
ToolStripItemCollection items = menuOnCircularNode .Items;
items ["miDeleteVertex"] .Enabled = polyPressed .VerticesNum > 3 &&
iReleasedNode < polyPressed .VerticesNum;
}
• When the strip node was pressed, then the opened menu contains a single command and allows to insert a new vertex at
the pressed point (figure 6.10c). It is a pretty simple action but with two remarks.
The new vertex has to get some color. At first I thought about setting it between the colors of two neighbours, but then
there will be no visual information about the appearance of the new vertex. In the current version, the new vertex is
always White; after it you can change its color through the command of another context menu. If you add the new
vertex in situation with visualized covers, then the new vertex appears as another circular node.
Declaring a new vertex somewhere on a strip node is natural because it automatically determines two neighbours of this
new vertex. At the same time this is the wrong place for the new vertex. There is no sense to leave this vertex at the
straight line between two others; the new vertex is needed only to change configuration of the whole polygon, so this
vertex will be moved somewhere else. Usually, when you decide to organize new vertex, you already keep in mind the
place where you are going to move it. You will move it there, but in a separate movement after the new vertex appears
World of Movable Objects 122 (978) Chapter 6 Polygons
on the screen. This process can be organized in more natural way: press with the left button on the strip node and move
this point to the needed position for new vertex without releasing the button. There is no problem in uniting two
processes into one press-and-move; this technique is demonstrated further on in the example
Form_GraphManualDefinition.cs in the chapter Applications for science and engineering. Unfortunately, in the
current example the polygon resizing is started with the left press anywhere on the strip nodes. Of two different actions
which can be organized as press-and-move starting with the left button on the strip nodes I selected the resizing. But
keep in mind that adding a vertex with further reconfiguring can be started in the same way.
In the Form_AddChatoyantPolygon.cs, the sizes of this square are equal to diameter of all small circles; position of this
square is determined by its central point.
public class Square_Sample : GraphicalObject
{
Point ptCenter;
int half;
Color clr;
This small square is movable but non-resizable, so its cover consists of a single node. More often than not polygonal nodes
are used in big size and for the purpose of moving some object; for this reason their default cursor shape is
Cursors.SizeAll. This small square can be moved around, but I want to inform users that this area can be used to
change the associated color in exactly the same way as the small circles. I have already changed the shape of the mouse
cursor over these circles to Cursors.Hand, so I need to change the cursor over this small square to the same shape. For
circles, I have to change the cursor in the OnMouseMove() method; for the square I can do the same while defining its
cover.
public override void DefineCover ()
{
cover = new Cover (new Rectangle (ptCenter .X - half, ptCenter .Y - half,
2 * half, 2 * half), Resizing .None);
cover .SetCursor (Cursors .Hand);
}
This small square can be moved around the screen and the standard color dialog can be called on it. Both actions are started
by the left button, so I distinguish these actions by analysis of the distance between two points where this square is grabbed
and released. As usual, if this distance is small (not more than three pixels), then the standard dialog is opened.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObject, iReleasedNode;
NodeShape shapeNode;
if (mover .Release (out iObject, out iReleasedNode, out shapeNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Square_Sample && fDist <= 3)
{
SelectCenterColor ();
}
… …
The Form_AddChatoyantPolygon.cs was developed for the very
first version of the book in year 2009 and was never changed since
then. It works according to its purpose and there is nothing against
the rules of user-driven applications. Yet, from my point of view
on the design of user-driven applications, there can be a better
version. I do not like this coexistence of the movable / resizable
objects which mover detects (big polygon and small square) with
other objects – those small circular samples – which are not
detected by mover. It is better when mover can detect all elements
even if some of them do not need to be moved individually. I am
not going to substitute the old version with the new one; I prefer to
demonstrate the Form_AddChatoyantPolygon_New.cs which has
exactly the same functionality but uses polygon of another class.
The new example is not used to add new chatoyant polygons to any
Fig.6.12 The new form for organizing chatoyant
other forms but it works like an independent example.
polygons have the same view as the old
one.
World of Movable Objects 126 (978) Chapter 6 Polygons
File: Form_AddChatoyantPolygon_New.cs
Menu position: Miscellaneous – Define chatoyant polygon
The view of the new form for organizing chatoyant polygons (figure 6.12) looks exactly as the old one, but these two forms
use different classes for their main objects. In the new version, polygon with all small circles positioned near its vertices is
a single object of the RegPoly_Chatoyant_PlusCircles class which is derived from the RegPoly_Chatoyant
class used in the previous example. The only additional fields in the new class are the distance from vertices to circles and
radius of these circles. When these two values are added to the radius of polygon (radius of its vertices) they give the
distance from the polygon central point to the centers of circles (fRadiusToCenters).
public class RegPoly_Chatoyant_PlusCircles : RegPoly_Chatoyant
{
float fSpaceToCircles, fRadiusSmall, fRadiusToCenters;
On initialization of such object, I use the same parameters as were used for the RegPoly_Chatoyant object but add to
them a couple of new parameters.
public RegPoly_Chatoyant_PlusCircles (PointF center, double rad, int vertices,
double angRad, Color clrC, Color [] clrs,
float fSpace, float radsmall)
: base (center, rad, vertices, angRad, clrC, clrs)
{
fSpaceToCircles = fSpace;
fRadiusSmall = radsmall;
fRadiusToCenters =
Convert .ToSingle (Radius + fSpaceToCircles + fRadiusSmall);
}
New polygon is movable and resizable, so its cover has to include big node covering the whole area of polygon and a set of
strip nodes over its perimeter line. In addition, there is a set of small circular nodes to cover all colored circles. Circular
nodes of this cover never overlap with other nodes, so the order of nodes is not so important but… Circular nodes are
associated with the vertices and the code is easier to write and understand when the number of the pressed circular node is
equal to the number of the color to be changed. Thus, the order of nodes is classical: small circular nodes, after them strip
nodes, and the last one is the polygonal node.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [2 * VerticesNumber + 1];
PointF [] ptCircles = Auxi_Geometry .RegularPolygon (Center,
fRadiusToCenters, VerticesNumber, Angle);
for (int i = 0; i < VerticesNumber; i++)
{
nodes [i] = new CoverNode (i, ptCircles [i], fRadiusSmall);
}
PointF [] pts = Vertices;
for (int i = 0; i < VerticesNumber; i++)
{
nodes [i + VerticesNumber] = new CoverNode (i + VerticesNumber,
pts [i], pts [(i + 1) % VerticesNumber]);
}
nodes [2 * VerticesNumber] = new CoverNode (2 * VerticesNumber, pts);
cover = new Cover (nodes);
}
There are no new basic points in the derived class, so there is no need for new Move() method.
MoveNode() method of the derived class is nearly identical to the method of the base class; only the numbers of the
nodes for resizing and forward movement are different.
public override bool MoveNode (int iNode, int cx, int cy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
World of Movable Objects 127 (978) Chapter 6 Polygons
{
if (iNode == 2 * VerticesNumber)
{
Move (cx, cy);
}
else if (VerticesNumber <= iNode && iNode < 2 * VerticesNumber)
{
double distanceMouse = Auxi_Geometry .Distance (Center, ptM);
if (distanceMouse * ScalingCoef >= MinimalRadius)
{
Radius = distanceMouse * ScalingCoef;
}
bRet = true;
}
}
else
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
Angle = angleMouse - Compensation;
bRet = true;
}
return (bRet);
}
Because small circles are now parts of the RegPoly_Chatoyant_PlusCircles class, then their drawing must be
included into the Draw() method of this class.
new public void Draw (Graphics grfx)
{
base .Draw (grfx);
PointF [] pts = Auxi_Geometry .RegularPolygon (Center, fRadiusToCenters,
VerticesNumber, Angle);
for (int i = 0; i < pts .Length; i++)
{
Auxi_Drawing .FillCircle (grfx, pts [i], fRadiusSmall, Colors [i]);
Auxi_Drawing .Circle (grfx, pts [i], fRadiusSmall, Color.DarkGray);
}
}
What differences in the code of the Form_AddChatoyantPolygon_New.cs are caused by using polygon of the new class?
• There is no drawing of small circles in the OnPaint() method as it is now done by the Draw() method of the
polygon itself.
• Both special situations in the OnMouseMove() method – calculation of circles throughout the resizing or
moving of polygon and changing the mouse cursor over the circles – are not needed any more and the
OnMouseMove() method becomes really primitive.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
if (mover .CaughtSource is SolitaryControl ||
mover .CaughtSource is CommentedControl)
{
Update ();
}
Invalidate ();
}
}
• Call to set the new color for one of circles (and for the vertex associated with this circle) is still initiated from
inside the OnMouseUp() method, but it is done not as a result of checking the mouse position against the areas
World of Movable Objects 128 (978) Chapter 6 Polygons
of all circles. Now all circles are sensed by mover, so it is a reaction on the release of some object by mover and at
the same time mover provides the number of the released node which is also the vertex number.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObject, iReleasedNode;
if (mover .Release (out iObject, out iReleasedNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is RegPoly_Chatoyant_PlusCircles &&
iReleasedNode < polygon .VerticesNumber && fDist <= 3)
{
SelectVertexColor (iReleasedNode);
Invalidate ();
}
… …
I have demonstrated two variants of the same task based on two different classes of regular polygons.
• The Form_AddChatoyantPolygon.cs uses the polygon of the RegPoly_Chatoyant class and a set of circles
which do not belong to the realm of movable objects.
• The Form_AddChatoyantPolygon_New.cs uses the polygon of the RegPoly_Chatoyant_WithCircles
class which includes similar circles as additional parts of the main object (polygon).
Both variants work correctly, but I’ll recommend the second one. I am not going to insist that the first variant is wrong and
only the second one is correct. From my experience, it is much better to use in design only elements which mover can
recognize. In the world of movable objects, it is much better to have all elements belonging to this kingdom and to rely on
the mover as much as possible.
World of Movable Objects 129 (978) Chapter 6 Polygons
Dorothy’s house
File: Form_DorothyHouse.cs
Menu position: Miscellaneous – Dorothy’s house
All previous examples in this book demonstrate abstract geometrical figures. This example is especially for those who want
to see something real. From the programming point of view, a primitive house is similar to rectangles and polygons, but
maybe this makes the set of examples more interesting and definitely more diverse. By the way, there are going to be more
houses in further examples.
The house turning around some point looks like a flying house. I think that the most famous of all the flying buildings was
the Dorothy’s house, so I called this class House_Dorothy and put the name of the owner on the building. (That famous
Dorothy was not the owner of the farmhouse but she became the most famous of its tenants.)
Figure 6.13 shows Dorothy’s house together with its cover.
There is a small circular node at the edge of the roof; this
node allows to change only the height of the roof. When you
start the big Demo program accompanying this book, there is
another house in the very first form that you see – in the
Form_Main.cs. That house can be reconfigured in different
ways and the edge of its roof can be moved both vertically
and horizontally. I cannot remember seeing in Kansas a
single house with the ridge moved anywhere from the center,
so in this Dorothy’s house the roof top can be moved only up
or down but not to the sides. The House_Dorothy class
has one specific rule of resizing: when either left or right side
of the house is moved, the house changes symmetrically on
both sides. Because of this rule, it is easier to keep among the
fields of the class the positions of all five basic points which
belong to the perimeter of the house.*
There is also one more point around which the house can be Fig.6.13 Form_DorothyHouse.cs
rotated, this point is marked with a small red spot. The
decision on the center of rotation affects the equations used in calculations, but these equations are always at the same level
of simplicity. Anyway, in this example the house is rotated around the middle point of the ceiling.
public class House_Dorothy : GraphicalObject
{
PointF ptAnchor; // rotation center; middle of the ceiling
PointF ptLT, ptRT, ptRB, ptLB; // four corners of the building
PointF ptRidge; // roof top
double angle; // in radians
double width; // house width
double height; // house height (rectangular part)
double roof; // roof height
The angle of the house is calculated as an angle of the ceiling starting from the corner next to the name.
Cover consists of 10 nodes: five circular nodes on the vertices, four strips along the sides of the rectangular part, and the
polygonal node (pentagon) to cover the whole area. Radius of circular nodes is slightly bigger than the default value.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [10];
int nr = 5;
nodes [0] = new CoverNode (0, ptLT, nr);
nodes [1] = new CoverNode (1, ptRT, nr);
nodes [2] = new CoverNode (2, ptRB, nr);
*
In this and many other examples I use the same type of abbreviations for the corners of rectangle. While writing some
text, it is a common rule to declare first vertical and then horizontal positioning (for example, top left corner). In math, the
coordinates on a plane are shown in the opposite order: first goes X coordinate and then Y. I use abbreviations according to
the math order of coordinates, so they are LT (left top), RT (right top), LB (left bottom), and RB (right bottom).
World of Movable Objects 130 (978) Chapter 6 Polygons
nodes [3] = new CoverNode (3, ptLB, nr);
nodes [4] = new CoverNode (4, ptRidge, nr);
nodes [5] = new CoverNode (5, ptLT, ptLB);
nodes [6] = new CoverNode (6, new PointF [] { ptLT, ptRT }, Cursors .Hand);
nodes [7] = new CoverNode (7, ptRT, ptRB);
nodes [8] = new CoverNode (8, ptLB, ptRB);
nodes [9] = new CoverNode (9, new PointF [] {ptLT, ptRidge, ptRT, ptRB,
ptLB});
cover = new Cover (nodes);
}
You can see from the DefineCover() method that for one strip node – the one along the ceiling –different constructor is
used than for three others. Figure 6.13 shows that this strip node is also visualized differently; this is the result of using
another constructor. The small change in the definition of one strip node was done purposely; this change allows to see
simultaneously the cover and the red spot marking the center of rotation.
When a node is defined by a single point, it is organized as a circular node. When circular node is visualized, its area is
painted in white (I am writing about the default colors). When a node is defined by two points, it is organized as a strip and
is also painted in white on visualization. Both types of nodes have the default cursor Cursors.Hand.
The definition of a node by an array of points is the standard way to declare a polygonal node; by default such node gets the
Cursors.SizeAll cursor shape. However, if there are less than three elements in the array of points, then polygonal
node cannot be constructed; in such case a node of different shape is constructed and the result depends on the number of
points in the provided array. If there is only one point in array, then a circular node is constructed; if there are two points,
then a strip node is constructed. Instead of polygonal node we receive either circle or strip, but parameters of such node get
the default values of the polygonal node! Thus constructed node has the cursor shape Cursors.SizeAll and the area
of this node is not painted with any brush on visualization. The second thing is just what I need from the node on the
ceiling, because I do not want this strip to close the view of the red circle. But I have to declare another cursor for this node
– the Cursors.Hand – which is used for the nodes on all other sides of the house. That was the reason why one node
of the house – the strip node along the ceiling – got special design.
Forward movement of the house means the synchronous move of all its basic points.
public override void Move (int dx, int dy)
{
SizeF shift = new SizeF (dx, dy);
ptLT += shift;
ptRT += shift;
ptRB += shift;
ptLB += shift;
ptRidge += shift;
ptAnchor += shift;
}
Individual movement of each node is described by the MoveNode() method. The movement of the polygonal node
( i = 9 ) results in movement of the whole house, so in this case the Move() method must be called. An attempt to
move any other node is described in such way. First, the current mouse position must be checked. There are no restrictions
on the mouse movements, but there are restrictions on the house sizes. For example, the mouse can grab the side of the
house and then there can be an attempt to move the mouse across the opposite side of the house. Without any restrictions,
this will turn the house inside out. This is not allowed, so the change of the house is stopped somewhere on the way, though
the cursor can continue its movement. The possibility for resizing is based on checking whether the mouse and another
basic point are on the same side of the line or on different sides. The selection of these point and line depends on the
number of the caught node. Here is the part of the MoveNode() method for the case of moving the floor ( i = 8 ).
The ceiling defined by its two end points (ptLT, ptRT) is used as a line while the two points to be kept on opposite sides
of this line are the current mouse position (ptM) and the upper point of the roof (ptRidge).
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
PointF pt;
PointF ptC_Floor =
Auxi_Geometry .PointToPoint (ptAnchor, angle - Math .PI / 2, height);
World of Movable Objects 131 (978) Chapter 6 Polygons
double distToFloor, distToCeiling, distToMiddle;
if (catcher == MouseButtons .Left)
{
switch (iNode)
{
case 0: // ptLT
… …
case 8: // floor
if (!Auxi_Geometry .SameSideOfLine (ptLT, ptRT, ptM, ptRidge))
{
distToCeiling = Auxi_Geometry .Distance_PointLine (ptM,
ptLT, ptRT, out pt);
height = Math .Min (Math .Max (minHeight, distToCeiling),
maxHeight);
CalculateBasicPoints ();
}
break;
… …
When the floor is moved, the mouse point (ptM) and the upper point of the roof (ptRidge) have to be on the opposite
sides of the ceiling (ptLT, ptRT).
if (!Auxi_Geometry .SameSideOfLine (ptLT, ptRT, ptM, ptRidge))
If this condition is satisfied, then the distance between the cursor and the ceiling is calculated.
distToCeiling = Auxi_Geometry .Distance_PointLine (ptM, ptLT, ptRT, out pt);
House has an interval for changing its height (minHeight, maxHeight). The calculated distance to the ceiling can be
used as the new house height, but the new height must be inside the allowed interval.
height = Math.Min (Math .Max (minHeight, distToCeiling), maxHeight);
On any resizing of the house, nearly all the basic points get new positions, so it is easier to calculate all of them. All points
are calculated by the CalculateBasicPoints() method.
private void CalculateBasicPoints ()
{
ptLT = Auxi_Geometry .PointToPoint (ptAnchor, angle + Math .PI, width / 2);
ptRT = Auxi_Geometry .PointToPoint (ptLT, angle, width);
ptLB = Auxi_Geometry .PointToPoint (ptLT, angle - Math .PI / 2, height);
ptRB = Auxi_Geometry .PointToPoint (ptRT, angle - Math .PI / 2, height);
ptRidge = Auxi_Geometry.PointToPoint (ptAnchor, angle + Math.PI / 2, roof);
}
This method uses the Auxi_Geometry.PointToPoint() method which calculates the second point on the base of
three parameters: initial point, angle, and distance to the second point.
Rotation of the House_Dorothy object is similar to rotation of rectangle described in the chapter Rotation. Rotation can
be started by pressing the house with the right button at any inner point.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Right)
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is House_Dorothy)
{
(grobj as House_Dorothy) .StartRotation (e .Location);
}
}
}
}
World of Movable Objects 132 (978) Chapter 6 Polygons
The House_Dorothy.StartRotation() method calculates the compensation angle between the angle to the mouse
(from the center of rotation) and the angle of the house. The angle to the mouse is calculated from the rotation center for
which the middle of the ceiling was chosen.
public void StartRotation (Point ptMouse)
{
double angleMouse = -Math .Atan2 (ptMouse .Y – ptAnchor .Y,
ptMouse .X – ptAnchor .X);
compensation = Auxi_Common .LimitedRadian (angleMouse - angle);
}
This compensation is fixed for the whole time of rotation (between the MouseDown and MouseUp events) and allows to
calculate the angle of the house depending on the current angle of the mouse throughout the rotation. The angle of the
house, in its turn, is used to calculate all the basic points of the house.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (catcher == MouseButtons .Right)
{
double angleMouse = -Math.Atan2 (ptM.Y - ptAnchor.Y,
ptM.X - ptAnchor.X);
angle = angleMouse - compensation;
CalculateBasicPoints ();
}
DefineCover ();
return (bRet);
}
An interesting feature of the House_Dorothy class is that its DefineCover() method is called from inside the
MoveNode() method not only for rotation but for the forward movement also. This is the easiest way to work with this
class. Because of that small addition in design – the mirror movement of both sides on resizing – the movement of any
node causes the relocation of nearly all other nodes; in such case it is much easier to redefine the cover.
The House_Dorothy class demonstrates again one important thing: there is nothing special in the design of covers for the
objects involved in rotation. The same objects can be involved in rotation or not; this is provided by adding several lines of
code into the OnMouseDown() method of the class but not by anything special in its cover design. Certainly, if the objects
of the class are going to be rotated, then some sort of the StartRotation() method must be written.
Some readers might think that fixing the rotation center is too strong restriction for this example. You can make this
example a bit more interesting and, like an exercise, organize rotation around an arbitrary center. One of the previous
examples – the Form_Polyline.cs – can help you in organizing such rotation. There are three obvious steps to organize
rotation around an arbitrary point
• Declare a small RotationCenter object and include it into the list of movable objects (register it with the
mover).
• At the starting moment of rotation get the coordinates of the rotation center and calculate distances and angles from
this center to all basic points of the house (there are six basic points).
• Throughout the rotation, use these arrays of distances and angles to calculate positions of all basic points of the
house.
World of Movable Objects 133 (978) Chapter 7 Curved borders. N-node covers
difference between those two points allowed me often to ignore it and to identify the movable mouse point as the new
border position.
To resize the objects with the curved borders, I use exactly the same idea of the thin sensitive area over border. I cannot
cover curved border with a single node, so I use a big amount of small nodes; the united area of all these nodes works like a
sensitive strip. The covers which use the proposed idea of covering the borders with the big number of small identical
nodes are called N-node covers. The shape of those small nodes and their relative positioning – these things depend on
each developer and his preferences. Users do not know these details, they only find whether an object can be resized easily
and without mistakes or there are some problems on resizing.
The most common technique which I prefer to use is to cover the curved border with a set of overlapping circular nodes; in
the Form_NnodeCovers.cs I use small circular nodes on the borders of circles and rounded parts of strips. The
neighbouring circles overlap in such a way that there are no gaps along the border and at any place the sensitive area is wide
enough to make the resizing easy and comfortable. After working with straight borders I came to the conclusion that the
optimal width of the sensitive area on borders is six pixels and tried to organize the curved sensitive area according to the
same rule.
For overlapping circular nodes I often use radius of
five pixels while the distance between the centers of
neighbouring circles is not greater than eight pixels.
For such two values the minimum width of sensitive
strip is six pixels and it is not a problem to grab any
border covered by such strip. The advantage of using
a set of overlapping circles is in the simplicity of their
positioning as calculation of their central points is
easy. A set of overlapping circular nodes is not the
only possible solution; the borders of the rings in the
Form_NnodeCovers.cs are covered by a set of
polygonal nodes. Each small polygonal node has a
form of a trapezoid with two sides placed along the
radiuses. The trapezoids do not overlap but stay side
by side; all the trapezoids have the same height, so the Fig.7.2 Objects with their covers
width of such strip is constant along the entire border.
Figure 7.2 demonstrates the objects together with their covers.
Only in one of the previous examples – in the Form_Polygons_Chatoyant.cs – the original number of designed objects can
be increased at any moment, but in that example (figure 6.8) all the objects belong to the same ChatoyantPolygon
class. In the current example, we have objects of three absolutely different shapes; these objects have different rules of
resizing, they need different covers, and it is natural to organize three different classes. Yet, these objects are used in one
form and for some operations like drawing or changing their order on the screen it is easier to work with them as with
representatives of the same class. In order to simplify my work, I declared an abstract class Element_Nnodes and then
organized three classes of elements derived from this abstract class.
public abstract class Element_Nnodes : GraphicalObject
{
protected FigureNnodes figure;
protected double angle;
… …
// ------------------------------------------------- Figure
public FigureNnodes Figure
{
get { return (figure); }
}
}
The only property to be found in the abstract class is the Element_Nnodes.Figure property which returns the shape
of the particular object. The shape variants are described by the FigureNnodes enumeration.
public enum FigureNnodes { Circle, Ring, Strip };
All objects in the Form_NnodeCovers.cs example can be rotated, so each object must have an angle field. This field is
included into the abstract Element_Nnodes class and there is nothing else in this class; everything else for our circles,
World of Movable Objects 135 (978) Chapter 7 Curved borders. N-node covers
strips, and rings can be found in the derived classes. Let us begin with the simplest of these objects – circles, but before
going into the details of the movable circles used in this example, I want to mention one auxiliary class.
Any circle is described by its central point and radius. When we deal with some multicolored circle, a set of colors for its
sectors is needed. Each sector has its own sector angle, so there must be a set of angles associated with sectors. On
initialization, I often declare some set of values and then use the ratio between these values to calculate angles for all
sectors. Angle of a circle is the angle from which the first sector starts; from here sectors can go in one or another direction.
Auxiliary lines can be used to draw the outer border and the borders between neighbouring sectors. Usually a resizable
circle has limit on minimal radius; this prevents the disappearance of circles after squeezing. In different examples you will
find different classes of circles but all of them have a set of mentioned parameters, so an auxiliary class for circle data – the
CircleData class – is included into MoveGraphLibrary.dll and is used in different classes of circles which you will see
in this example and further on.
public class CircleData
{
PointF m_center;
float m_radius;
double m_angle;
double [] vals;
double []sector_angle;
List<Color> clrs = new List<Color> (); // one color per sector
Rotation dirDrawing;
bool bShowBorder, bShowPartitions;
Pen penBorder, penPartitions;
static float minRadius = 15; // min size is set to avoid disappearance
Now we can start with the circles in our example.
Circles
Circles in the Form_NnodeCovers.cs belong to the Circle_Nnodes class. All geometrical and visibility parameters
of the circle are “hidden” inside the CircleData field and in view there are the fields which are used for cover design:
number of the small circular nodes on border (nNodesOnBorder), radius of these small nodes (nrSmall), and the
distance between the centers of neighbouring nodes (distanceNeighbours).
public class Circle_Nnodes : ElementNnodes
{
CircleData dataCircle;
double compensation;
int nNodesOnBorder;
int nrSmall = 5;
int distanceNeighbours = 8;
Circle_Nnodes class has four different constructors. To initialize the circle seen at figure 7.1, I use an array of
numbers to set the ratio between sectors. Colors for the first and last sectors are declared; all other sectors get the colors
from the smooth palette between these two values.
void InitElements ()
{
elements .Insert (0, new Circle_Nnodes (new PointF (240, 150), 80,
new double [] { 2, 6, 4, 1, 5, 7, 3 },
Color .Yellow, Color .Violet));
… …
Figure 7.2 shows that the border of a circle is covered by the overlapping small circular nodes. First, the needed number of
such nodes is calculated by the NodesOnBorder() method.
private void NodesOnBorder ()
{
nNodesOnBorder = Convert.ToInt32 ((2 * Math .PI * dataCircle .Radius) /
distanceNeighbours);
}
World of Movable Objects 136 (978) Chapter 7 Curved borders. N-node covers
After it the cover can be organized. Cover consists of those small nodes plus one big circular node covering the whole
object exactly to its border. Small circular nodes are used for resizing; big circular node is used for moving the whole
object. As usual, the biggest node must be the last one in the cover, but there is one more reason to put this big circular
node at the end. If you change the order of nodes and put the big circular node at the head, then it will close that half of all
small nodes which is inside the border; the border will be still sensitive but only from outside; this will considerably worsen
the resizing.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [nNodesOnBorder + 1];
for (int i = 0; i < nNodesOnBorder; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (Center,
2 * Math .PI * i / nNodesOnBorder, Radius), nrSmall);
}
nodes [nNodesOnBorder] = new CoverNode (nNodesOnBorder, Center, Radius,
Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
All nodes in this cover are circular, so the decision on particular movement to be started by catching one or another node
cannot be based on its shape but only on its number. For big circles, there can be a huge number of small nodes on the
border, but there is no need to write code for each of them. There is a special case of the big circular node which is
responsible for forward movement, while the reaction on moving any small border node is absolutely identical and does not
depend on particular number of the caught node. The only thing which is done on moving any of the small nodes is the
calculation of new radius. Forward movement of a circle and its rotation were already discussed in the example with the
Circle_Nonresizable class; there is nothing new in these parts.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == nNodesOnBorder)
{
Move (dx, dy);
}
else
{
float fRadNew =
Convert .ToSingle (Auxi_Geometry .Distance (Center, ptM));
if (fRadNew >= MinimumRadius)
{
dataCircle .Radius = fRadNew;
bRet = true;
}
}
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
dataCircle .Angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
World of Movable Objects 137 (978) Chapter 7 Curved borders. N-node covers
Rings
Let us move from circles to rings. Any ring is described by its central point and two radii. There are two restrictions on
sizes – one for minimal inner radius and another for minimal width of ring – but everything else is very similar to circles, so
there is similar auxiliary class for geometrical and visualization parameters of rings. This RingData class is included into
the MovegraphLibrary.dll and is used in different classes of rings which you will see in the current and further examples.
public class RingData
{
PointF m_center;
float rInner;
float rOuter;
double m_angle;
double [] vals;
double [] sector_angle;
List<Color> clrs = new List<Color> (); // one color per sector
Rotation dirDrawing;
bool bShowBorders, bShowPartitions;
Pen penBorders, penPartitions;
static float minInnerRadius = 10;
static float minWidth = 15;
Rings in the Form_NnodeCovers.cs belong to the Ring_Nnodes class. All needed geometrical and visibility
parameters are inside the RingData field; other fields are used for cover design. There are no small overlapping circular
nodes, but there are three sets of polygonal (in this case trapezoidal) nodes, so there are three fields for numbers of these
nodes. Two values are needed to describe the geometry of small nodes on the borders: width of every small node along the
border line (wNode) and the size of sensitive strip on each side of the border line (hHalf).
public class Ring_Nnodes : Element_Nnodes
{
RingData dataRing;
double compensation;
int nNodesOnOuter;
int nNodesOnInner;
int nNodesPoly;
float hHalf = 4; // the same hHalf is going inside and outside
int wNode = 10;
For the ring from figure 7.1, a set of values is used to calculate the angles for all sectors and then the colors for all sectors
are declared.
void InitElements ()
{
… …
Ring_Nnodes ring = new Ring_Nnodes (new PointF (530, 190), 50, 100, 0,
new double [] { 7, 4, 2, 6, 5 });
ring .SetColors (new Color [] { Color .Yellow, Color .Lime, Color .Cyan,
Color .Blue, Color .Violet });
… …
Ring cover is more complicated than circle cover. First, there are two resizable borders instead of one and each border must
be covered by its own set of nodes. Second, the area between two borders must be used for moving the whole object and it
is impossible to cover it with a single node, so one more set of nodes must be used. It is easy to cover the inner and outer
borders of the ring by two sets of small circular nodes in exactly the same way as was done for circles, but in order to show
that the overlapping of small circles is not the only possible solution, I replaced those small nodes with two sets of polygons
in the shape of trapezoids. Each trapezoid covers 10 pixels of the border on which it is placed and spreads for four pixels
outside and inside from the border, so the height of each trapezoid is eight pixels. Based on the standard width of each
trapezoid (wNode = 10), the needed numbers of them to cover both the inner border (nNodesOnInner) and the
outer border (nNodesOnOuter) are calculated.
The area of the ring itself is also covered with a set of trapezoids. They can be calculated in different ways, but they have to
cover the area of a ring without any gaps. To decrease the number of calculations, their sides are placed at the same radii as
the sides of the small trapezoids on the outer border, so the number of nodes to cover the area of a ring (nNodesPoly) is
World of Movable Objects 138 (978) Chapter 7 Curved borders. N-node covers
equal to the number of nodes on the outer border. Thus, the number of nodes in all three mentioned sets depends on two
radii (outer and inner) and is calculated by the NodeNumbers() method.
private void NodeNumbers ()
{
nNodesOnOuter =
Convert .ToInt32 ((2 * Math .PI * dataRing .OuterRadius) / wNode);
nNodesOnInner =
Convert .ToInt32 ((2 * Math .PI * dataRing .InnerRadius) / wNode);
nNodesPoly = nNodesOnOuter;
}
The Ring_Nnodes.DefineCover() method is too long to include here its full text, but it is not needed as the code is
simple and the trapezoids for all three sets are calculated in the same way using some very simple geometry. The most
important thing is the order of nodes in the cover.
1. Nodes on the outer border.
2. Nodes on the inner border.
3. Nodes to cover the ring area.
I’ll demonstrate and comment calculation of the first set of nodes covering the outer border.
public override void DefineCover ()
{
PointF [] pts = new PointF [4];
CoverNode [] nodes =
new CoverNode [nNodesOnOuter + nNodesOnInner + nNodesPoly];
float rSmaller, rBigger;
double anglePerNode, angleNext;
rSmaller = dataRing .OuterRadius - hHalf;
rBigger = dataRing .OuterRadius + hHalf;
anglePerNode = 2 * Math .PI / nNodesOnOuter;
pts [0] = Auxi_Geometry .PointToPoint (Center, 0, rSmaller);
pts [1] = Auxi_Geometry .PointToPoint (Center, 0, rBigger);
for (int i = 0; i < nNodesOnOuter; i++) // nodes on outer border
{
angleNext = anglePerNode * (i + 1);
pts [2] = Auxi_Geometry .PointToPoint (Center, angleNext, rBigger);
pts [3] = Auxi_Geometry .PointToPoint (Center, angleNext, rSmaller);
nodes [i] = new CoverNode (i, pts, Cursors .Hand);
pts [0] = pts [3];
pts [1] = pts [2];
}
… …
• Each polygonal node (trapezoid) is described by four corner points which are positioned in pairs on two radii.
• A set of trapezoids covers the closed arc of 360 degrees. Any node of this set is used to move the outer border. All
these nodes work identically and for the moving of the outer border it does not matter which of these nodes is
pressed, so I can start my calculations of nodes from any angle and place them side by side. For easiness, I start
calculations from zero angle.
• Two corners of trapezoid (pts[0] and pts [3]) are positioned at different radii but at the same distance from
the central point (rSmaller). This distance is slightly smaller than the radius of the border.
rSmaller = dataRing .OuterRadius - hHalf;
• Two other corners of trapezoid (pts[1] and pts [2]) are also positioned at different radii but at equal
distance from the central point (rBigger). This distance is slightly bigger than the radius of the border.
rBigger = dataRing .OuterRadius + hHalf;
• The number of nodes in the set is known, so the angle per each node is easily calculated.
anglePerNode = 2 * Math .PI / nNodesOnOuter;
World of Movable Objects 139 (978) Chapter 7 Curved borders. N-node covers
• I start calculations by defining two corners of the first trapezoid (pts[0] and pts[1]); these two corners are
positioned on radius with zero angle.
pts [0] = Auxi_Geometry .PointToPoint (Center, 0, rSmaller);
pts [1] = Auxi_Geometry .PointToPoint (Center, 0, rBigger);
• Further calculation is done in cycles. The second pair of corners for the node is positioned on another radius which
is turned for anglePerNode from the radius for the first pair.
for (int i = 0; i < nNodesOnOuter; i++) // nodes on outer border
{
angleNext = anglePerNode * (i + 1);
pts [2] = Auxi_Geometry .PointToPoint (Center, angleNext, rBigger);
pts [3] = Auxi_Geometry .PointToPoint (Center, angleNext, rSmaller);
• Polygonal node in the form of trapezoid is defined by its four corners. As a rule, polygonal nodes are used for
forward moving of objects and their default cursor Cursors.SizeAll informs users about this possibility. In
case of these rings, small trapezoidal nodes on borders are used for resizing, so I want to change the cursor shape
over all nodes of this set and use the appropriate constructor.
nodes [i] = new CoverNode (i, pts, Cursors .Hand);
• Next node of the set is positioned side by side with the current node, so two corners of these nodes take the same
positions and the first pair of corners for the next node is the same as the second pair from the previous node.
pts [0] = pts [3];
pts [1] = pts [2];
For the set of nodes on the inner border, calculations are identical only rSmaller and rBigger are calculated on two
sides of inner radius of the ring.
For the set of trapezoids covering the ring area, calculations are the same with corners of trapezoids placed exactly on inner
and outer borders of the ring.
All nodes in this cover are polygonal, so the decision on particular movement started by one or another node cannot be
based on its shape but only on the number of the pressed node. The Ring_Nnodes.MoveNode() method has to
include more checking, as the moving of the outer border has to be checked against one restriction (minimum allowed width
of the ring) and the proposed movement of the inner border has to be checked against the same minimum allowed width but
also against the minimum allowed radius of this border. Though there can be a lot of nodes in the cover, the MoveNode()
method is simple because the reaction on moving a node is not specific for each of them but depends only on the set to
which this node belongs. There are three sets of nodes – on outer border, on inner border, and between two borders – so
there are three variants in the MoveNode() method when some node is pressed by the left button.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode >= nNodesOnOuter + nNodesOnInner)
{
Move (dx, dy);
}
else if (iNode >= nNodesOnOuter)
{
float newInner =
Convert .ToSingle (Auxi_Geometry .Distance (Center, ptM));
if (MinimumInnerRadius <= newInner &&
newInner <= dataRing .OuterRadius - MinimumWidth)
{
dataRing .InnerRadius = newInner;
bRet = true;
}
}
World of Movable Objects 140 (978) Chapter 7 Curved borders. N-node covers
else
{
float newOuter =
Convert .ToSingle (Auxi_Geometry .Distance (Center, ptM));
if (dataRing .InnerRadius + MinimumWidth <= newOuter)
{
dataRing .OuterRadius = newOuter;
bRet = true;
}
}
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
Angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
Forward movement and rotation of ring and circle are identical. Each object has an angle (this is a starting angle of the first
sector); each class has its own StartRotation() method to calculate the compensation angle at the initial moment of
rotation; this compensation angle is not changed throughout the rotation and is used at any moment to calculate the real
angle of an object.
Strips
Rounded strips in the Form_NnodeCovers.cs belong to the Strip_Nnodes class. Though strip is a very simple figure,
there are some problems in explanation of its initialization. These explanations are much easier when you see a sketch of
strip with comments (figure 7.3). I have already used such strip with comments in one of the previous examples
(Form_StripCommented.cs, figure 5.10) to explain the idea of comment associated with some point. Now it is time to
look at the cover design for such strip and the details of its resizing.
You can consider a rounded strip as rectangle with two semicircles
added on opposite sides; middle points of these sides – C0 and C1 –
are the centers of semicircles. In some situations I can mention
length and width of the rounded strip. Its length is measured along
the main axis – the line connecting those two points C0 and C1.
The full length consists of the length of the straight part plus two
radii of semicircles. Width of the strip is equal to the diameter of
semicircles. For many operations with the strip, I use the corners of
the straight part. These four points are returned as an array by the
Strip_Nnodes.CornerPoints() method; order of points in
this array is seen from figure 7.3. Several methods and properties
of the Strip_Nnodes class mention these points as corners
though in reality there are no corners in such strip. Angle of a strip
is calculated as an angle from C0 to C1.
public class Strip_Nnodes : Element_Nnodes Fig.7.3 Points C0 and C1 are the middle points of
{ two opposite sides of rectangle (and the
PointF ptC0, ptC1; central points of semicircles). Several
float m_radius; methods and properties of the
SolidBrush brush; Strip_Nnodes class mention the
int nNodesOnSemicircle; points of the pts[] array as corners
static float minRadius = 12;
though there are no real corners in rounded
static float minStraight = 20;
strips.
int nrSmall = 5;
int distanceNeighbours = 8;
Based on the geometry of a strip, the most natural way to initialize it is to declare those two central points of semicircles and
their radius.
World of Movable Objects 141 (978) Chapter 7 Curved borders. N-node covers
public Strip_Nnodes (PointF ptA, PointF ptB, float rad, Color color)
{
figure = FigureNnodes .Strip;
ptC0 = ptA;
ptC1 = ptB;
m_radius = Math .Max (minRadius, Math .Abs (rad));
angle = Auxi_Geometry .Line_Angle (ptC0, ptC1);
if (Auxi_Geometry .Distance (ptC0, ptC1) < minLength)
{
ptC1 = Auxi_Geometry .PointToPoint (ptC0, angle, minLength);
}
brush = new SolidBrush (color);
NodesOnSemicircle ();
}
However, this is not the best way of initialization if you need to position the strip at some particular place on the screen.
For such case, it is possible to declare the central point of the strip, half length of its straight part, radius, and angle. As
usual, the initial angle is declared in degrees while all calculations are done in radians.
public Strip_Nnodes (PointF ptC, float halflength, float rad,
double angleDegree, Color color)
As any other object, rounded strip has to be moved forward by any inner point and resized by any border point. There is no
problem with forward movement of the strip as we have nodes in the shape of rounded strip, so any strip can be covered by
a single node of such shape. Border of rounded strip includes straight and curved parts. The straight parts of border are
covered by a pair of narrow strip nodes. Borders of semicircles are covered by two sets of small overlapping circular nodes
in exactly the same way as was demonstrated in the Circle_Nnodes class. The number of nodes in each set is calculated
by the Strip_Nnodes.NodesOnSemicircle() method.
private void NodesOnSemicircle ()
{
nNodesOnSemicircle = (int) (Math .PI * m_radius / distanceNeighbours) + 1;
}
There is one significant difference between resizing in the
Circle_Nnodes and Strip_Nnodes classes. In circles, you
can press any small node on the border and the reaction will be
identical. Strips are symmetrical, so without any comments near by,
you cannot visually distinguish one straight side from another and
one semicircle from another. But by pressing visually
undistinguishable from each other semicircular borders, you start
different resizing. Mover knows what it has to do in each case
because it identifies the number of the pressed node. I prefer to show
cover with comments (figure 7.4); this will help a lot in
understanding the Strip_Nnodes.DefineCover() and
Strip_Nnodes.MoveNode() methods.*
Fig.7.4 An object of the Strip_Nnodes class
Cover for the Strip_Nnodes class includes two sets of small
with cover and comments for basic
circular nodes and three strip nodes which are definitely bigger.
points.
Usually nodes are included into a cover beginning from the small
ones, but here I slightly changed the order and put the strip nodes on the straight parts of border ahead of the circular nodes
which cover the curves. This cover includes not a solitary circular node which can be a problem to find but two sets of such
nodes. Reaction on pressing a circular node depends not on its number but on the set to which it belongs. These sets of
circular nodes cover a good piece of border, so it really does not matter whether these nodes are at the head of a cover or a
bit later. Thus we receive such order of nodes in the cover.
1. Two strip nodes along the straight parts of the border. The first one is based on points pts[0] and pts[1]; the
second one is based on pts[2] and pts[3]
*
The normal way to prepare such figure would be to grab the picture of some rounded strip with visualized cover, put it, for
example, into Paint program, and add the needed comments. If later I decide to change the proportions or angle of the strip
(and this will definitely happen), I’ll have to repeat the whole process and maybe not once. Instead, I can easily prepare the
needed illustrations with the help of the Form_StripCommented.cs example from the chapter Texts.
World of Movable Objects 142 (978) Chapter 7 Curved borders. N-node covers
2. Two sets of circular nodes along the curved parts of the border. The first set covers the border around point C0;
the second set – around point C1.
3. One big strip node to cover the whole object.
The small nodes on the curves of the border are placed in the same way as they were placed on the border of a circle: each
small node has radius of five pixels (nrSmall = 5) and the distance between the centers of neighbouring circles is not
more than eight pixels (distanceNeighbours = 8).
Circular nodes are often used in a small size; strip nodes are often very narrow. By default, during visualization their area is
painted in white and only the node border is shown by the thin red line. In case of the Strip_Nnodes class, there are
three strip nodes. Two of them are narrow and I do not want to change their default parameters on visualization. The last
one covers the whole object. I do not want this node to wash out the whole view of an object when its cover is visualized,
so I dismiss the clearance of this node. It is easy to do as this is the last node in the cover.
public override void DefineCover ()
{
PointF [] pts = CornerPoints ();
CoverNode [] nodes;
int nNodes, k0;
double small_angle = Math .PI / (nNodesOnSemicircle - 1);
nNodes = 2 + 2 * nNodesOnSemicircle + 1;
nodes = new CoverNode [nNodes];
nodes [0] = new CoverNode (0, pts [0], pts [1], nrSmall);
nodes [1] = new CoverNode (1, pts [2], pts [3], nrSmall);
k0 = 2;
for (int i = 0; i < nNodesOnSemicircle; i++)
{
nodes [k0 + i] = new CoverNode (k0 + i,
Auxi_Geometry .PointToPoint (ptC0, angle + Math .PI / 2 +
i * small_angle, m_radius),
nrSmall);
}
k0 += nNodesOnSemicircle; // 2 + nNodesOnHalfCircle
for (int i = 0; i < nNodesOnSemicircle; i++)
{
nodes [k0 + i] = new CoverNode (k0 + i,
Auxi_Geometry .PointToPoint (ptC1, angle - Math .PI / 2 +
i * small_angle, m_radius),
nrSmall);
}
k0 = nodes .Length - 1;
nodes [k0] = new CoverNode (k0, ptC0, ptC1, m_radius, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetNodeClearance (nodes .Length - 1, false);
}
There are two restrictions on the minimum allowed sizes; these restrictions help to avoid an accidental disappearance of a
strip throughout its squeezing.
• Minimum radius of semicircles is limited by 12 pixels (minRadius = 12); this also means that the minimum
width of a strip is limited by 24 pixels.
• Minimum length of the straight part is limited by 20 pixels (minStraight = 20).
As usual, these restrictions are taken into consideration inside the MoveNode() method when one or another node is
caught for moving.
When a strip node over the straight part of the border is moved, then the width of an object is changed. Basic points C0 and
C1 are not affected by such move, so the move of one straight side is mirrored by the move of another straight side. The
length of the straight part is not changed, but width of the strip changes and with it changes the radius of semicircles. Thus,
the total length of the strip also changes. The strip node under move must be kept on the same side of the main axis (line
C0 – C1) and the minimum allowed distance from the main axis is equal to minimum allowed radius. Here is the part of the
MoveNode() method for the first node of cover (i = 0); this is the strip node based on points pts[0] and pts[1].
World of Movable Objects 143 (978) Chapter 7 Curved borders. N-node covers
Code for opposite strip node (i = 1) is nearly identical with only one different parameter for the
Auxi_Geometry.SameSideOfLine() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
double fDist;
PointF [] pts = CornerPoints ();
if (iNode == 0)
{
if (Auxi_Geometry .SameSideOfLine (ptC0, ptC1, ptM, pts [0]))
{
fDist = Auxi_Geometry .Distance_PointLine (ptM, ptC0, ptC1);
if (fDist >= minRadius)
{
m_radius = Convert .ToSingle (fDist);
DefineCover ();
bRet = true;
}
}
}
… …
Rounded strip can be looked at as a rectangle with semicircular additions on two opposite sides. When any small node on
the curve is moved, then only the length of the straight part of this strip is changed. This change goes in such a way that the
central point of the opposite semicircle does not move at all but the central point of the pressed semicircle moves along the
main axis. This resizing needs the calculation of one special parameter at the starting moment.
Suppose that you pressed with a mouse somewhere on the curved border around C0. At this moment the distance from the
mouse cursor to the line [pts[1], pts[2]] is calculated. This distance – fStartingDistanceToRect – is the
distance from mouse to the nearest side of rectangular part; it is also the distance from the mouse to the diameter of
semicircle which is pressed. The border of semicircle and its diameter are moving synchronously, so this distance is fixed
for the whole process of resizing. At the same time the distance between diameters of two semicircles is going to change
according to mouse movement. Distance from mouse to the opposite side of rectangular part is the sum of the fixed
distance to the nearest side and the changing length of the straight part. There is a limit on minimal length of the straight
part, so this helps to decide whether the movement of the mouse can be transformed into change of the straight part or not.
Here is the part of the MoveNode() method for circular nodes around C0; part of this method for the opposite curve is
very much alike.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
… …
else if (iNode < 2 + nNodesOnSemicircle) // circular nodes around ptC0
{
fDist = Auxi_Geometry .Distance_PointLine (ptM, pts [0], pts [3]);
// distance to the opposite side of rectangle
if (fDist - fStartingDistanceToRect >= minStraight &&
Auxi_Geometry. SameSideOfLine (pts [0], pts [3], ptM, ptC0))
{
ptC0 = Auxi_Geometry .PointToPoint (ptC1, angle + Math .PI,
fDist - fStartingDistanceToRect);
DefineCover ();
bRet = true;
}
}
… …
World of Movable Objects 144 (978) Chapter 7 Curved borders. N-node covers
The last node of the cover is responsible for forward movement of the whole strip. As usual, in such case the
MoveNode() method simply calls the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else
{
Move (dx, dy);
}
… …
Now we are familiar with cover design for all objects in the Form_NnodeCovers.cs and it is time to look at some
interesting moments of using objects with N-node covers. Part of what is going in the OnMouseDown() and
OnMouseUp() methods of the form is similar to all the previous examples, but there are also absolutely new features
which originate from this special type of covers. First, let us look at more familiar parts. Any movement starts with the
press of the mouse.
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 Strip_Nnodes &&
mover.CaughtNodeShape == NodeShape .Circle)
{
(grobj as Strip_Nnodes) .StartLengthChange (e .Location,
mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is Element_Nnodes)
{
(grobj as Element_Nnodes) .StartRotation (e .Location);
}
}
}
}
I have already mentioned that for any circular node of a Strip_Nnodes object the distance from the mouse to the nearest
side of rectangular part must be calculated. This is done by calling the Strip_Nnodes.StartLengthChange()
method. The call to this method is similar to the use of the ChatoyantPolygon.StartScaling() method which
was mentioned in the section about the chatoyant polygons. The StartLengthChange() method has to receive the
number of the node as the second parameter; there are two movable curved parts of the border and this number allows to
identify which of the strip ends is going to be moved.
public void StartLengthChange (Point ptMouse, int iNode)
{
PointF [] pts = CornerPoints ();
if (iNode < 2 + nNodesOnSemicircle)
{
fStartingDistanceToRect =
Auxi_Geometry .Distance_PointLine (ptMouse, pts [1], pts [2]);
}
World of Movable Objects 145 (978) Chapter 7 Curved borders. N-node covers
else
{
fStartingDistanceToRect =
Auxi_Geometry .Distance_PointLine (ptMouse, pts [0], pts [3]);
}
}
Now let us look at what is really unique for the N-node covers. The biggest difference
between the N-node covers and the covers from all the previous examples is not in the
number of nodes but in the fact that the number of nodes in the cover can vary. For all
the previous examples only the shape of an object or its required resizing determined
the number of nodes in the cover. With the objects in the Form_NnodeCovers.cs it is
different: the number of nodes is determined by the size of object (figures 7.5). This
can become a big problem, even a disaster, if you try to use such covers in the same
way as others.
For all previously demonstrated objects with the possibility of resizing, there were no
restrictions on calling the DefineCover() method on any change of the sizes. It is
done, for example, in different classes of rectangles and polygons. When the number of
nodes in the cover is fixed for the whole class (or for an object with the fixed number of
vertices) and does not depend on the size of the particular object, then the
DefineCover() method can be called at any moment.
When an object is caught by mover, it means that it is caught by one or another node of
the cover. Movement of this node is translated into the movement of an object by the
MoveNode() method in which the exact type of movement (forward movement of the
whole object, resizing, or reconfiguring) is often determined according to the number of
the caught node. If the number of nodes in the cover can change during the period of
time when an object is grabbed by mover, then you can expect a lot of strange
situations. The number of the caught node is determined at the beginning of any
movement and is not going to change in any way until the end of the movement. But if
the total number of nodes in the cover depends on the size of an object, can change
throughout the resizing, and the whole set of nodes consists of nodes with different
types of movements (for example, the Ring_Nnodes class has three such groups),
then the specified number can easily move from one group to another or even go
outside the whole interval of numbers for nodes. In the first case the type of movement Fig.7.5 A small strip is made
will simply change somewhere on the way; this is definitely an unexpected thing for wider and released. At
anyone. In the second case the program can crash. this moment its cover
To understand the first situation, let us look once more at the Ring_Nnodes class. must be redefined.
I’ll remind the order of nodes in its cover: first the nodes on the outer border, then the nodes on the inner border, and then
the nodes for the whole area. Suppose that you pressed the inner border and began to squeeze the hole. With the
diminishing hole, less and less nodes are needed to cover the inner border. If you will be constantly changing the cover
according to the changing size, then at some moment the specified number of the caught node will move into the set of
nodes that cover the whole area of the ring and instead of squeezing the hole you will see the movement of the ring. It is
definitely not the expected thing. The rule of N-node covers prevents you from running into such situation.
The rule of N-node covers. The DefineCover() method cannot be called from inside the MoveNode() method and
though the cover has to be changed because the sizes of an object have changed, the call to the DefineCover() method
must be postponed until the release of an object. Before the DefineCover() method is called to update the cover, the
new number of nodes must be calculated.
The redesign of cover for a previously caught object is done when this object is released; this happens after the call of the
Mover.Release() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
World of Movable Objects 146 (978) Chapter 7 Curved borders. N-node covers
if (e .Button == MouseButtons .Left)
{
if (grobj is Element_Nnodes)
{
RedefineCover (mover .ReleasedObject);
if (dist <= 3)
{
PopupFigure (grobj .ID);
}
}
… …
If the released object belongs to the Element_Nnodes class, then RedefineCover() method of the form determines
the exact class of this object (there are three different classes which are derived from the Element_Nnodes class) and
calls the RedefineCover() method of particular class.
private void RedefineCover (int iObj)
{
GraphicalObject grobj = mover [iObj] .Source;
if (grobj is Ring_Nnodes)
{
(grobj as Ring_Nnodes) .RedefineCover ();
Invalidate ();
}
else if (grobj is Circle_Nnodes)
{
(grobj as Circle_Nnodes) .RedefineCover ();
Invalidate ();
}
else if (grobj is Strip_Nnodes)
{
(grobj as Strip_Nnodes) .RedefineCover ();
Invalidate ();
}
}
This is one possible way of calling the RedefineCover() method for the caught object. In this variant the order of
released object in the mover queue is sent as a parameter and then the type of an object is determined. There can be another
variant with the direct passing of an element.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (grobj is Element_Nnodes)
{
RedefineCover (grobj);
… …
Certainly, in such case the small change of the RedefineCover() method will be also needed.
private void RedefineCover (GraphicalObject grobj)
{
if (grobj is Ring_Nnodes)
{
(grobj as Ring_Nnodes) .RedefineCover ();
Invalidate ();
}
… …
Any class which uses the N-node cover must have the RedefineCover() method. The method is always very simple:
calculate the new number of nodes according to the new sizes and call the DefineCover() method of the class. Here is
the RedefineCover() method for the Ring_Nnodes class.
World of Movable Objects 147 (978) Chapter 7 Curved borders. N-node covers
public void RedefineCover ()
{
NodeNumbers ();
DefineCover ();
}
Is the mentioned rule of the N-node covers a mandatory one? Are there any exceptions? Yes, there are exceptions. You
have to understand those two cases when the rule has to be used:
1. When the number of the caught node can move from the group of nodes with one behaviour to the group of nodes
with different behaviour.
2. When the number of the caught node can move outside the range of nodes.
In all other cases the DefineCover() method can be called from inside the MoveNode() method without any
problems. For example, you can see such thing in the Strip_Nnodes class. The resizing of a strip with the requirement
for another number of nodes can occur only on moving the two strip nodes on the border, but these nodes never change their
numbers (they are always 0 and 1); the caught strip node will have the same number in any new cover. That was the
reason to put these strip nodes ahead of the circular nodes in the cover. The resizing caused by moving the circular nodes
which cover the curves does not change the number of nodes, so again the number of the caught node will be the same in
the new cover as at the starting moment of such movement. Thus, there are no dangerous situations with calling the
DefineCover() method from any part of the Strip_Nnodes.MoveNode() method.
For the Circle_Nnodes and Ring_Nnodes classes the situation is different: you cannot call the DefineCover()
method from inside the MoveNode() method but only after the release of the caught object.
There is one more interesting object in the Form_NnodeCovers.cs: it is a group with three buttons inside (figure 7.1). This
is the first appearance of an ElasticGroup object, though this class is widely used in my applications and you will see
many examples with this class further on. The ElasticGroup class is discussed in the chapter Groups and here are only
few words about this object. Group can be moved around by any inner point. Group is not resizable directly, but the frame
automatically adjusts its size and position to all changes of inner elements. In this example, you can move and resize inner
buttons and the frame will be always around them. Title of the group can be moved along the upper line of the frame
between the left and right sides. ElasticGroup class is included into the MoveGraphLibrary.dll together with its
special tuning form which is usually opened after the right button click anywhere inside the group. However, the
appearance of such tuning form would require an explanation of the possible tuning and I prefer to postpone such
explanation until another example further on. In this case of the Form_NnodeCovers.cs, the right click inside the group
calls a short menu; the only command of this menu allows to change the font for the buttons inside the group and for the
title of the group.
File: Form_Circles_Multicolored.cs
Menu position: Graphical objects – Circles – Multicolored circles
Circles of the Circle_Nnodes class are used in
one more example -
Form_Circles_Multicolored.cs (figure 7.6).
Whatever has to be explained about
Circle_Nnodes class is already explained in the
previous example. There is nothing new in dealing
with these circles in the new example but it was
organized for one special purpose. Further on,
somewhere 200 pages ahead I’ll return again to the
problem of circle resizing and will demonstrate that
the same resizing can be organized with much
simpler covers. I want to have two different
examples with circles of similar view and
behaviour but with absolutely different covers. So
this is the first of these examples and all its circles
have a classical N-node cover.
Arcs
Thin arcs
File: Form_Arcs_Thin.cs
Menu position: Graphical objects – Basic elements – Arcs – Thin arcs (N-node cover)
In the previous example, there are three different
classes that use the N-node covers. In those three
classes this special type of cover is used to
organize the resizing of objects. However, the
same idea of many identical nodes can be used to
organize the moving of objects and the
Ring_Nnodes class has already demonstrated it.
In the current example you can see objects which
are not resizable at all; there is nothing to resize in
a thin line, so the arcs from the
Form_Arcs_Thin.cs (figure 7.7) can be only
moved and rotated.* Each arc is painted with a
relatively thin pen and this characteristic of the
objects is even mentioned in the name of their class
– Arc_Thin.
Nearly all the things that you can see in the code of Fig.7.7 Thin arcs with their covers. An auxiliary line is shown only
the Arc_Thin class and of the throughout the rotation.
Form_Arcs_Thin.cs were already used in the
previous examples, so you are going to see familiar parts of code here.
Arc is some part of a circle, so the central point and radius of this circle are needed. The initial position of any arc is also
characterized by an angle of one of its ends. Another parameter is the arc angle which is the angle from one end to another.
Also some arc color must be declared.
public class Arc_Thin : GraphicalObject
{
PointF m_center;
float m_radius;
double angleStart;
double angleArc;
Pen m_pen;
int nNodes;
double compensation;
float minRadius = 20;
// -------------------------------------------------
public Arc_Thin (PointF ptC, float rad,
double angleStart_Degree, double angleArc_Degree, Pen pn)
{
m_center = ptC;
m_radius = Math .Max (minRadius, Math .Abs (rad));
angleStart = Auxi_Convert .DegreeToRadian (angleStart_Degree);
angleArc = Math .Min (Math .Max (-Math .PI * 2,
Auxi_Convert .DegreeToRadian (angleArc_Degree)), Math .PI * 2);
m_pen = pn;
nNodes = Math .Max (Convert .ToInt32 (
Math .Abs (angleSector) * m_radius / distanceNeighbours) + 1, 2);
}
*
There is one parameter that can be changed in a thin arc: it is angle (or length), but I would like to demonstrate such thing
much later when a special technique of glueing the mouse cursor to the moved object will be discussed. So we will return
to the thin arcs in the chapter Movement restrictions.
World of Movable Objects 149 (978) Chapter 7 Curved borders. N-node covers
To organize the needed movement, the full length of an arc is covered by the small overlapping circular nodes. Usually I
use the small nodes with a radius of five pixels (nrSmall = 5) and the centers of the neighbouring nodes are placed not
farther away from each other than eight pixels (distanceNeighbours = 8). This makes the sensitive strip at least six
pixels wide; from my point of view it is enough for easy grabbing of an arc. I want to demonstrate together an arc and its
cover, so I dismissed the clearance of all nodes on visualization.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [nNodes];
double delta = angleSector / (nNodes - 1);
for (int i = 0; i < nNodes; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (m_center,
angleStart + i * delta, m_radius), nrSmall);
}
cover = new Cover (nodes);
cover .SetClearance (false);
}
There is no resizing of those arcs, so the number of nodes in the cover – nNodes – is calculated only once during
initialization. This number is not going to change throughout any movements and the redefinition of cover is never needed.
If an arc angle is set at 360 degrees, then you receive a closed ring instead of an arc; in such case it is impossible to see the
rotation though it works. For smaller arc angles, the gap makes the rotation obvious. Rotation is organized in a standard
way by using the Arc_Thin.StartRotation() method to calculate the initial compensation angle.
public void StartRotation (Point ptMouse)
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
compensation = Auxi_Common .LimitedRadian (angleMouse - angleStart);
}
This Arc_Thin.StartRotation() method is called when an arc is pressed with the right button to start a rotation.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (e .Button == MouseButtons .Right)
{
if (grobj is Arc_Thin)
{
Arc_Thin arc = grobj as Arc_Thin;
arc .StartRotation (e .Location);
float rad = arc .Radius;
rcAuxi = new RectangleF (arc .Center .X - rad,
arc .Center .Y - rad, 2 * rad, 2 * rad);
bDrawAuxiCircle = true;
Invalidate ();
}
}
}
}
There are several additional lines of code in the OnMouseDown() method; they are used to organize the better
visualization of rotation. Arc has no visible central point and when the arc is short, as in the case of the blue arc at
figure 7.7, the path of the arc in rotation is not obvious. At the starting moment of rotation, the rectangle around its circular
path is calculated and the flag to draw this path gets the true value (bDrawAuxiCircle = true). Throughout the
rotation of an arc, its circular path is shown with an auxiliary thin line and the arc moves along this circle as a train in the
toy railroad.
World of Movable Objects 150 (978) Chapter 7 Curved borders. N-node covers
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
if (bDrawAuxiCircle)
{
if (mover .CaughtSource is Arc_Thin)
{
grfx .DrawEllipse (penAuxi, rcAuxi);
}
}
arcBlue .Draw (grfx);
arcGreen .Draw (grfx);
… …
Wide arcs
File: Form_Arcs_Wide.cs
Menu position: Graphical objects – Basic elements – Arcs – Wide arcs (N-node cover)
The cover for thin arcs is simple enough and there are no problems in moving them. But what are you going to do with
wider arcs? For really wide arc which looks more like some part of a ring, the polygonal nodes in the shape of trapezoids
can be used in the same way as was demonstrated in the Ring_Nnodes class.
public class Arc_Wide : GraphicalObject
{
PointF m_center;
float rInner;
float rOuter;
double angleStart;
double angleArc;
SolidBrush m_brush;
int nNodes;
double compensation;
float minInnerRadius = 20;
float minArcWidth = 10;
float maxNodeWidth = 12;
double anglePerNode;
Area of wide arc is covered by a set of polygonal
nodes placed side by side (figure 7.8). Two
corners of each trapezoid are placed on the inner
border of the arc, two others – on outer border;
these four points are placed (in pairs) along two
radii. Trapezoids are made narrow enough
(maxNodeWidth = 12) so theoretically there
must be no gaps between their combined area and
the outer border of an arc. Because these arcs are
not resizable, then the number of needed nodes
(nNodes) and the angle per each node are
calculated at the moment of initialization. This Fig.7.8 Wide arcs with N-node cover
angle – anglePerNode – is the angle between
those two sides of trapezoid which are going along radii.
public Arc_Wide (PointF ptC, float rIn, float rOut,
double angleStart_Degree, double angleArc_Degree, Color clr)
{
m_center = ptC;
rInner = Math .Max (rIn, minInnerRadius);
rOuter = Math .Max (rOut, rInner + minArcWidth);
angleStart = Auxi_Convert .DegreeToRadian (angleStart_Degree);
angleArc = Math .Min (Math .Max (-Math .PI * 2,
Auxi_Convert .DegreeToRadian (angleArc_Degree)), Math.PI * 2);
World of Movable Objects 151 (978) Chapter 7 Curved borders. N-node covers
double lengthOuter = rOuter * Math .Abs (angleArc);
nNodes = Math .Max (Convert .ToInt32 (Math .Ceiling (lengthOuter /
maxNodeWidth)), 1);
anglePerNode = angleArc / nNodes;
m_brush = new SolidBrush (clr);
}
When the number of nodes (nNodes) and the angle per each node (anglePerNode) are known, it is easy to calculate all
nodes starting from one side of the arc and putting those nodes side by side. Two corners of neighbouring nodes always
take the same positions.
public override void DefineCover ()
{
PointF [] pts = new PointF [4];
CoverNode [] nodes = new CoverNode [nNodes];
double angle = angleStart;
pts [0] = Auxi_Geometry .PointToPoint (m_center, angle, rInner);
pts [1] = Auxi_Geometry .PointToPoint (m_center, angle, rOuter);
for (int i = 0; i < nNodes; i++)
{
angle += anglePerNode;
pts [2] = Auxi_Geometry .PointToPoint (m_center, angle, rOuter);
pts [3] = Auxi_Geometry .PointToPoint (m_center, angle, rInner);
nodes [i] = new CoverNode (i, pts);
pts [0] = pts [3];
pts [1] = pts [2];
}
cover = new Cover (nodes);
}
As you see, there is not big difference between thin and wide arcs. There is a switch from circular nodes to polygonal nodes
and instead of one auxiliary line throughout the rotation there will be two lines. Moving of the thin and wide arcs in the last
two examples is absolutely identical; there is no difference in their Move() and MoveNode() methods.
The calculations of trapezoids for the cover of the Arc_Wide class are not complicated but there exists another way to
organize covers for such arcs. Those covers will be not of the N-node type. Instead, there will be very few simple nodes,
but some of them must have special features. The use of covers with such special nodes is explained in the next chapter, so
you will see more arcs soon.
World of Movable Objects 152 (978) Chapter 8 Transparent nodes
Transparent nodes
If you have an object of some very unusual shape or an object with holes, then design of cover in the standard
way by filling the object area with circular, strip, and polygonal nodes can become a problem. For some
objects the standard way of covering their area by a set of ordinary nodes can demand a lot of painstaking
work with equations. Surprisingly, the covers for the same objects can be easily organized with the help of
transparent nodes.
Ring was one of the examples which I used to explain the idea of the N-node covers. As seen from figure 7.2, cover for any
object of the Ring_Nnodes class consists of three different sets of polygonal (trapezoidal) nodes: one set of small nodes
on the outer border, another set of small nodes on the inner border, and the third set of nodes for area of the ring itself. The
first two sets of nodes in such cover provide the resizing; the third set of nodes is used for moving the whole ring. Suppose
that you want to develop movable but non-resizable rings. At first it looks like you can take the cover of the
Ring_Nnodes class, get rid of all the border nodes, leave only an array of polygons to cover the area of a ring, and
everything is going to be OK. It is like organizing a cover for some wide arc with arc angle of 360 degrees, so the needed
number of trapezoids can be calculated from this arc angle and then the cover design from the Arc_Wide class can be
used (see previous page).
In reality it is not so simple. The united area of trapezoids produces a regular polygon with the hole of the same shape. All
vertices of the outer border of such polygon are positioned on the outer circle of the ring and the vertices of the inner border
are positioned on the inner circle. The problem is that the area of this regular polygon with identical hole is not equal to the
area of the ring and there are differences between them on both ring borders. We have exactly the same situation in the
Ring_Nnodes class, but in that class the small discrepancies are closed by other nodes along the borders. Without those
nodes, we receive some problems on both borders:
• There are small non-sensitive areas along the outer border.
• Small parts of the hole become sensitive, though they do not belong to the ring.
The difference between the exact area of the ring and the area of such cover designed of trapezoids is not big; this difference
also decreases if the number of trapezoids is increased. You can try to ignore this problem because the discrepancy between
circle and its approximation by a regular polygon with big number of vertices can be really small, but this is not a good-
looking solution. Much better and really good solution can be achieved with the use of transparent node. There exists the
ideal cover for non-resizable ring and this cover consists of only TWO nodes!
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [2];
nodes [0] = new CoverNode (0, center, rInner, Behaviour .Transparent);
nodes [1] = new CoverNode (1, center, rOuter, Cursors .SizeAll);
cover = new Cover (nodes);
}
There is no class of rings in the Demo application which has exactly such DefineCover() method so you can look at
this code as a theoretical example. But further on I’ll demonstrate the Form_SetOfObjects.cs in which you can find rings
of the Ring_EOS class. Those rings are resizable and the partitions between their sectors are movable, but both things are
regulated via the commands of context menu. If for any Ring_EOS object you switch OFF the possibilities of resizing and
partitions moving, then such ring will have exactly such type of cover.
This is really an elegant cover consisting of two circular nodes. One of them – the bigger one – covers the whole area of the
ring together with the circular hole inside. If this node would be left without any additions (better to say – extractions), then
the ring would become movable not only by any point of its area, which is correct and expected, but also by any point of its
hole, which is definitely a mistake. To solve this problem, the second circular node is used. This node covers the hole and
“burns out” this part of the bigger sensitive area, but there are two conditions (or rules) for such burning:
• Special node to “burn out” part of sensitive area must have the parameter Behaviour.Transparent.
• This special node must be included into cover ahead of the node from which it burns out some part.
The order of nodes is extremely important in this case. I have mentioned not once that mover analyses each cover node
after node according to their order in the cover. When mover finds the node with the Transparent behaviour, then not
only this node is ignored but also all further nodes of the same cover. Beginning from this node, an object becomes
transparent for mover and mover looks for something farther on in its queue of objects to grab and move. Two facts
World of Movable Objects 153 (978) Chapter 8 Transparent nodes
contribute to an extraordinary importance and, at the first glance, a strange use of transparent nodes: the invisibility of nodes
and nonequivalence of the area of object and area of its cover.
For the majority of objects these two areas can be equivalent or nearly the same. To make an object movable by any inner
point, its whole area must be covered by some set of nodes. If an object is movable but not resizable, then the sensitive area
is limited by the area of object, so the area of cover can be equivalent to the area of object. When an object is resizable,
then the resizing is usually done by the border. To make the grabbing of the border easier, the border is covered by some
sensitive strip, so the area of cover becomes slightly wider than the object itself. All this is used in the simple and most
obvious cases; for the complicated cases of nontrivial areas only the transparent nodes can help.
In some situations the use of transparent nodes allows to change the covers with a lot of nodes (and a lot of their
calculations) into really simple; the above shown change of cover for rings is such a case. Further on I will show an
example with a crescent and this example is remarkable from two points. First, it shows a power of using the transparent
nodes. Second, it is an example in which the area of object itself (I mean visible or sensitive area) differs so much from the
area of cover that it would be difficult even to imagine.
Pay attention to two aspect of using the transparent nodes.
• The shape of cursor above transparent node is never declared and is set to Cursors.Default. Certainly, if
mover looks through the transparent node and finds some non-transparent node from another object underneath,
then the cursor takes the shape that is defined by that node at the lower level.
• Transparent nodes are extremely useful in dealing with covers to which they belong but they do not affect other
covers. This means that if needed, the area of transparent nodes can far exceed the object area. If some cover
nodes are transparent, then there is no requirement for the whole cover area to be equal to object area. Transparent
nodes can be big and absolutely different in shape from object area if the use of such nodes allows to organize
object movement by any inner point. Some of further examples will perfectly illustrate this feature of transparent
nodes.
Now let us look how transparent nodes work in objects of different shapes and classes.
Rings
File: Form_Rings.cs
Menu position: Graphical objects – Basic elements – Rings – Multicolor rings
The first example of an object using a transparent node in its cover is going to be a ring of the Ring_Multicolor class.
Borders of these rings are covered by two sets of small circular overlapping nodes, so it is still an example of N-node cover
and similar to the case of Ring_Nnodes class, but instead of the third set of trapezoids I use here two big circular nodes.
public class Ring_Multicolor : GraphicalObject
{
RingData dataRing;
double compensation;
int nNodesOnOuter;
int nNodesOnInner;
int nrSmall = 5;
int distanceNeighbours = 8;
// -------------------------------------------------
public Ring_Multicolor (PointF ptC, float rIn, float rOut,
double angleDegree, double [] fVals)
{
dataRing = new RingData (ptC, rIn, rOut, angleDegree, fVals);
NodesOnBorders ();
}
There are no colors among the parameters of this constructor and the new ring gets a set of default colors. There is another
constructor in which colors for the first and last sectors can be declared; colors of all other sectors will be organized as a
smooth palette between these two values.
public Ring_Multicolor (PointF ptC, float rIn, float rOut, double [] fVals,
Color clr_0, Color clr_1)
{
dataRing = new RingData (ptC, rIn, rOut, fVals, clr_0, clr_1);
World of Movable Objects 154 (978) Chapter 8 Transparent nodes
NodesOnBorders ();
}
In the Form_Rings.cs example both variants of
constructors are used.
The resizing of such rings is provided by two sets
of small nodes covering both borders (figure 8.1).
It is similar to the covers of the Ring_Nnodes
class which can be seen at figure 7.2, but in the
new class the border nodes have circular shape.
The whole cover in the Ring_Multicolor
class consists of such nodes and in such order.
1. Set of circular nodes on the outer border.
2. Set of circular nodes on the inner border.
3. Circular node over the hole.
4. Big circular node up to the outer border.
This is an interesting cover consisting entirely of
circular nodes!
All small border nodes have radius of five pixels
(nrSmall = 5); central points of
neighbouring nodes are positioned with a step of
eight pixels (distanceNeighbours = 8).
Number of nodes on each border is determined by Fig.8.1 Rings with the covers using a transparent node
the border radius and the distance between the
centers of neighbouring circular nodes.
private void NodesOnBorders ()
{
nNodesOnOuter = Convert .ToInt32 (
(2 * Math .PI * dataRing .OuterRadius) / distanceNeighbours);
nNodesOnInner = Convert .ToInt32 (
(2 * Math .PI * dataRing .InnerRadius) / distanceNeighbours);
}
When the number of nodes is calculated and the order of nodes is known, there are no problems with the cover design. This
cover consists exclusively of circular nodes of different size and by default the area of circular nodes is cleaned on
visualization. I want to demonstrate rings together with their covers, so when the cover design is over I change the
clearance of this cover to false.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [nNodesOnOuter + nNodesOnInner + 2];
for (int i = 0; i < nNodesOnOuter; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (Center,
2 * Math .PI * i / nNodesOnOuter, dataRing .OuterRadius),
nrSmall);
}
int k = nNodesOnOuter;
for (int i = 0; i < nNodesOnInner; i++)
{
nodes [k + i] = new CoverNode (k + i,
Auxi_Geometry .PointToPoint (Center, 2 * Math .PI * i /
nNodesOnInner, dataRing .InnerRadius),
nrSmall);
}
k += nNodesOnInner;
nodes [k] = new CoverNode (k, Center, dataRing .InnerRadius,
Behaviour .Transparent);
World of Movable Objects 155 (978) Chapter 8 Transparent nodes
nodes [k + 1] = new CoverNode (k + 1, Center, dataRing .OuterRadius,
Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
Two classes of multicolor rings – Ring_Nnodes and Ring_Multicolor – are very much alike. The use of
transparent node in the Ring_Multicolor class allowed to exclude from its cover the whole set of polygonal nodes –
the set of trapezoids to cover the area between two borders. But there are still two sets of nodes on the borders, so in all
other aspects it is a classical example of the N-node cover and thus we still need to update this cover at the moment when
the ring is released. This cover renewal is required only after resizing but not after forward movement of the ring, so the
call to the Ring_Multicolor.RedefineCover() method is preceded by the check of the number of released node.
The check is easy as only two last nodes must be excluded from starting the cover renewal.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iWasObject, iWasNode;
if (mover .Release (out iWasObject, out iWasNode))
{
GraphicalObject grobj = mover [iWasObject] .Source;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is Ring_Multicolor && iNode < grobj.NodesCount - 2)
{
(grobj as Ring_Multicolor) .RedefineCover ();
Invalidate ();
}
… …
In the previous sentence I wrote that “only two last nodes must be excluded from starting the cover renewal” and in the code
I check that the released node is not one of the last two nodes. I want to remark that it is enough to check that the released
node is not the last one (in the next line I marked the change with the Bold style)
else if (grobj is Ring_Multicolor && iNode < grobj.NodesCount - 1)
and the checking will be still correct. Why?
The last (and the biggest) circular node of the cover is responsible for forward movement of the ring and such movement
does not require redefinition of cover on release of an object. The second from the end node is transparent. It is not used
for any moving; when mover detects such node, it entirely forgets about this object and this piece of code has nothing to do
with further actions. So I can put at the end of checking either 1 or 2; both variants are correct.
World of Movable Objects 156 (978) Chapter 8 Transparent nodes
polygons (Form_RegularPolygon_CoverVariants.cs, figure 6.1). Strip nodes are used only for outer border, so this part
of the MoveNode() method works only for strip nodes.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
… …
else if (cover .GetNodeShape (i) == NodeShape .Strip)
{
double radiusNew = distanceMouse * scaling;
if (radiusNew >= (radiusCircle +
minCircleToSide) / Math .Cos (Math .PI / nVertices))
{
radiusVertices = Convert .ToSingle (radiusNew);
bRet = true;
}
}
else
{
Move (dx, dy);
}
… …
When mover senses the Transparent node, it skips the whole cover of this object, so in this case the MoveNode()
method is not called. If the MoveNode() method is called and the caught node is neither a circle nor a strip, then the only
left option is the polygonal node which is used to move the whole object. In this case the Move() method is called.
Objects can be rotated. To organize rotation, additional fields are needed as it was demonstrated for regular and convex
polygons in the previous examples. For regular polygon, it is enough to have a single compensation angle. For convex
polygon, radius and compensation angle must be stored for each vertex, so two arrays are needed (radius_Outer[] and
compensation_Outer[]). Similar array of scaling coefficients is needed for zoom of the outer border
(scaling_Outer[]).
public class ConvexPoly_RegPolyHole : GraphicalObject
{
PointF m_center;
PointF [] ptsOut;
float radiusVertices_Inner;
int nVertices_Inner;
double angle_Inner;
SolidBrush brush;
float minRadius_Inner = 10;
float minWidth = 10;
float minSegment_Outer = 12;
// used between MouseDown and MouseUp
double [] angle_Outer; // for zoom of outer border
double [] scaling_Outer; // for zoom of outer border
double [] compensation_Outer; // only for rotation
double [] radius_Outer; // only for rotation
double scaling_Inner; // for zoom of inner border
double compensation_Inner; // for rotation
There are two ways to construct ConvexPoly_RegPolyHole objects. The easiest way is to declare all four needed
parameters for regular polygon of inner border, while for outer border declare only the number of vertices and angle (it is an
angle of the first vertex). In this case the outer border is also organized as a regular polygon and the distance between two
borders is at least twice as big as minimum allowed width. I call it the easiest way of construction because no preliminary
checking is needed and it is guaranteed that no restrictions are broken by this constructor.
public ConvexPoly_RegPolyHole (PointF ptC,
float radiusIn, int nVerticesIn, double angIn_Degree,
int nVerticesOut, double angOut_Degree, Color color)
{
nVertices_Inner = Math .Max (Math .Abs (nVerticesIn), 3);
nVerticesOut = Math .Max (Math .Abs (nVerticesOut), 3);
m_center = ptC;
radiusVertices_Inner = Math .Max (Math .Abs (radiusIn), minRadius_Inner);
angle_Inner = Auxi_Convert .DegreeToRadian (
Auxi_Common .LimitedDegree (angIn_Degree));
double rOuter = (radiusVertices_Inner + 2 * minWidth) /
Math .Cos (Math .PI / nVerticesOut);
double angOut = Auxi_Convert .DegreeToRadian (
Auxi_Common .LimitedDegree (angOut_Degree));
ptsOut = Auxi_Geometry .RegularPolygon (m_center,
rOuter, nVerticesOut, angOut);
PrepareArrays (ptsOut .Length);
brush = new SolidBrush (color);
}
The outer border can be reconfigured in any possible way until it continues to be a convex polygon. Any polygon can be
saved and later restored; for this restoration another constructor is needed. In this constructor, vertices of the outer border
are declared as an array of points.
public ConvexPoly_RegPolyHole (PointF ptC, float radiusIn, int nVerticesIn,
double angIn_Degree, PointF [] verticesOut, Color color)
Before using this constructor, checking of parameters is needed.
• Points for vertices of outer border must represent a convex polygon.
• The shortest segment of this polyline (outer border) must be checked against the minimum allowed length of
segment.
World of Movable Objects 160 (978) Chapter 8 Transparent nodes
• Distance between two polylines (inner and outer borders) must be bigger than minimum allowed width.
This constructor together with the mandatory preliminary checking can be seen in the code of the
ConvexPoly_RegPolyHole.FromRegistry() method.
public static ConvexPoly_RegPolyHole FromRegistry (RegistryKey regkey,
string strAdd)
{
… …
if (Auxi_Geometry .PolygonConvexity (pts) &&
Auxi_Geometry.Polyline_ShortestSegment(pts,true) >= minSegment_Outer &&
Auxi_Geometry .Distance_Polylines (ptsIn, pts) >= minWidth)
{
ConvexPoly_RegPolyHole poly = new ConvexPoly_RegPolyHole (ptC,
radiusIn, nVerticesIn, Auxi_Convert .RadianToDegree (angleIn),
pts, Auxi_Convert .ToColor (strs, 6));
… …
Now it is time to look at the cover of the ConvexPoly_RegPolyHole objects (figure 8.4). Both borders can be used
for resizing and the outer vertices can be used for reconfiguring of the
outer border, so overall the cover of each object consists of five different
groups of nodes. Nodes are included into the cover in such order.
1. A set of strip nodes covers the segments of the inner border.
The number of strips is equal to the number of vertices in this
polygon.
2. Circular nodes are positioned on the vertices of the outer border.
3. A set of strip nodes covers the segments of the outer border.
The number of strips is equal to the number of vertices in the
outer polygon.
4. Polygonal transparent node covering the hole. Fig.8.4 A polygon with its cover
5. Big polygonal node in the form of a convex polygon up to the
outer border.
public override void DefineCover ()
{
PointF [] ptsInner = Vertices_Inner;
int nVertices_Outer = ptsOut .Length;
CoverNode [] nodes =
new CoverNode [nVertices_Inner + 2 * nVertices_Outer + 1 + 1];
for (int i = 0; i < nVertices_Inner; i++)
{
nodes [i] = new CoverNode (i, ptsInner [i],
ptsInner [(i + 1) % nVertices_Inner]);
}
int j0 = nVertices_Inner;
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [j0 + i] = new CoverNode (j0 + i, ptsOut [i], 5);
}
j0 += nVertices_Outer;
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [j0 + i] = new CoverNode (j0 + i, ptsOut [i],
ptsOut [(i + 1) % nVertices_Outer]);
}
j0 += nVertices_Outer;
nodes [j0] = new CoverNode (j0, ptsInner, Behaviour .Transparent);
nodes [j0 + 1] = new CoverNode (j0 + 1, ptsOut);
cover = new Cover (nodes);
}
World of Movable Objects 161 (978) Chapter 8 Transparent nodes
Both borders (inner and outer) are covered by the strip nodes (figure 8.4), so the decision about the particular movement
inside the MoveNode() method of this class cannot be based on the shape of the grabbed node but only on its number.
• The last node in the cover is the only one which is used for forward moving of the whole object, so for this node
there is a standard call of the Move() method from inside the MoveNode() method. This is the only polygonal
node which has to be mentioned in the MoveNode() method, so instead of its number the shape of this node can
be used; I show this possibility in comment.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == cover .NodesCount - 1)
// or if (cover [iNode] .Shape == NodeShape .Polygon)
{
Move (dx, dy);
}
else
Three different groups of nodes – strip nodes on segments of the inner border, strip nodes on segments of the outer border,
and circular nodes on the vertices of the outer border – must be checked for the possibility of the proposed movement. In
each case, some restrictions play their role.
• In case of the inner border, there is minimal allowed radius for inner vertices and minimal allowed distance
between two borders. The scaling coefficient scaling_Inner is calculated at the starting moment of resizing.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
PointF [] pts_In;
if (iNode < nVertices_Inner) // inner resizing
{
double radiusNew =
scaling_Inner * Auxi_Geometry .Distance (m_center, ptM);
pts_In = Auxi_Geometry .RegularPolygon (m_center, radiusNew,
nVertices_Inner, angle_Inner);
if (minRadius_Inner <= radiusNew &&
Auxi_Geometry .PointsInsideConvexPolygon (pts_In, ptsOut) &&
Auxi_Geometry .Distance_Polylines (pts_In, ptsOut) >= minWidth)
{
radiusVertices_Inner = Convert .ToSingle (radiusNew);
DefineCover ();
bRet = true;
}
}
… …
• In case of the outer vertices, the proposed move of the caught vertex must still keep the outer border as a convex
polygon. If it does, then the new distance between two borders is checked against the minimum allowed distance.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (iNode < nVertices_Inner + nVertices_Outer) // circles on outer vertices
{
int k = iNode - nVertices_Inner;
PointF ptNew = new PointF (ptsOut [k] .X + dx, ptsOut [k] .Y + dy);
if (Auxi_Geometry .ConvexPolygonVertexAllowed (ptsOut, k, ptNew,
minSegment_Outer))
World of Movable Objects 162 (978) Chapter 8 Transparent nodes
{
PointF ptCur = ptsOut [k];
ptsOut [k] = ptNew;
if (Auxi_Geometry .Distance_Polylines (Vertices_Inner, ptsOut) >=
minWidth)
{
DefineCover ();
bRet = true;
}
else
{
ptsOut [k] = ptCur;
}
}
}
… …
• Segments of the outer border are used for scaling of this border. In this case, there is a restriction on minimum
length of segments and the minimum allowed distance between two borders.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (iNode < nVertices_Inner + 2 * nVertices_Outer) // outer resizing
{
if (m_center != ptM)
{
double distMouse = Auxi_Geometry .Distance (m_center, ptM);
PointF [] ptsOut_New = new PointF [ptsOut .Length];
for (int j = 0; j < ptsOut_New .Length; j++)
{
ptsOut_New [j] = Auxi_Geometry .PointToPoint (m_center,
angle_Outer [j], distMouse * scaling_Outer [j]);
}
pts_In = Vertices_Inner;
int iShortest;
if (Auxi_Geometry .Polygon_ShortestSegment (ptsOut_New,
out iShortest) >= minSegment_Outer &&
Auxi_Geometry .PointsInsideConvexPolygon (pts_In, ptsOut) &&
Auxi_Geometry .Distance_Polylines (pts_In, ptsOut) >= minWidth)
{
ptsOut = ptsOut_New;
DefineCover ();
bRet = true;
}
}
}
… …
Inner border of such object is always a regular polygon, while the outer border is a convex polygon. The resizing started on
the borders use the same idea of scaling but with different variations which are defined by the number of the caught node.
void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is ConvexPoly_RegPolyHole)
{
ConvexPoly_RegPolyHole poly = grobj as ConvexPoly_RegPolyHole;
World of Movable Objects 163 (978) Chapter 8 Transparent nodes
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNodeShape == NodeShape .Strip)
{
poly .StartScaling (e .Location, mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
poly .StartRotation (e .Location);
}
}
}
}
All vertices of the hole in the shape of regular polygon stay at the same distance from the center of an object, so it is enough
to have a single scaling coefficient for all of them.
public void StartScaling (Point ptMouse, int iNode)
{
double distanceMouse = Auxi_Geometry .Distance (center, ptMouse);
if (iNode < nVertices_Inner) // on inner border
{
scaling_Inner = radiusVertices_Inner / distanceMouse;
}
else // on outer border
{
for (int i = 0; i < ptsOut .Length; i++)
{
angle_Outer [i] = Auxi_Geometry .Line_Angle (center, ptsOut [i]);
scaling_Outer [i] = Auxi_Geometry .Distance (center,
ptsOut [i]) / distanceMouse;
}
}
}
The outer border is a convex polygon with the semi-independent positioning of vertices. They are semi-independent
because they have to produce a convex polygon at any moment, but from the point of zooming they are absolutely
independent, so for them the whole set of scaling coefficients must be calculated (one coefficient per vertex). In addition to
the scaling coefficients, the set of angles must be also calculated (one per vertex); throughout the scaling, the angles of the
outer vertices do not change and all those vertices are moving along their personal radii.
Similar thing happens when the rotation of any ConvexPoly_RegPolyHole object is started. For the inner regular
polygon, it is enough to calculate a single compensation angle. For the outer convex polygon, two arrays must be
calculated: one includes radii for all vertices, another – personal compensation angles for all vertices.
public void StartRotation (Point ptMouse)
{
double angleMouse = Auxi_Geometry .Line_Angle (center, ptMouse);
compensation_Inner = Auxi_Common .LimitedRadian (angleMouse - angle_Inner);
for (int i = 0; i < ptsOut .Length; i++)
{
radius_Outer [i] = Auxi_Geometry .Distance (center, ptsOut [i]);
compensation_Outer [i] = Auxi_Common .LimitedRadian (angleMouse –
Auxi_Geometry .Line_Angle (center, ptsOut [i]));
}
}
World of Movable Objects 164 (978) Chapter 8 Transparent nodes
Crescent
File: Form_Crescent.cs
Menu position: Graphical objects – Basic elements – Crescent
Previous examples of this chapter show the objects of
different shapes but they are similar in the way they use the
transparent nodes. Each object has a hole of some shape; this
hole is covered by transparent node which “burns out” some
part of the main area. In each case the transparent node is
smaller than the whole object and is placed entirely inside the
area of an object. However, there is no such rule that
transparent node must be surrounded on all sides by the area
of an object in which it is used. As other nodes, transparent
nodes can be placed separately, side by side, or overlap with
other nodes. There is no sense at all to place a transparent
node without any overlapping with other non-transparent
nodes of the cover because such transparent node is simply
useless, but there are no restrictions on placing these nodes
and each case depends on the purpose of using such node.
The new example with crescent demonstrates two things:
• Transparent nodes can be used not only to burn the
hole but to cut out some part of the main object.
• Transparent node can be bigger or much bigger than Fig.8.5 Crescent object with its cover
the object in the cover of which it is used. Thus, the
area of the cover can be much bigger than the area of visible object and their shapes can be absolutely different. I
want to remind that transparent nodes have no effect on other screen objects, so the overlapping of transparent
nodes from some object with other objects does not matter at all.
When years ago I invented an algorithm for moving screen objects, it was initially aimed at moving the standard plotting
areas in scientific applications and such areas are always rectangular. Then I began to use the same algorithm with objects
of different shapes and in a while I was especially looking for such objects which would create significant problems in
applying to them the existing algorithm. Now my goal was to create an algorithm useful with objects of any shape and
really problematic cases were of much higher interest to me than easy to implement cases. Crescent became an object of
special interest because for some time I could not
find an elegant solution for turning any crescent
into movable object. When I understood that an
attempt to cover crescent with some set of standard
nodes was a dead-end, then I switched from
thinking about addition of nodes one to another to
the opposite idea of subtraction and quickly came
to really elegant idea of two circular nodes one of
which is transparent. The solution was in front of
my eyes (and slightly above the head) on nearly
every second cloudless night.
We often see the Moon as a crescent. The Moon
itself is circular, but when the bigger part of it is
shadowed by bigger circle, then we see a shining
crescent above the head. This is exactly the idea of
movable crescent: take some circle with the radius
rOuterBorder and cut out the bigger part of it
by another circle with bigger radius
rInnerBorder.
Fig.8.6 Two angles and three sizes marked on this sketch are used
This is a decision for making any crescent as fields in the Crescent class
movable. For resizing I use only four points: two
horns (points B and C at figure 8.5) and two points (A and D) on borders in the widest part of crescent. For these points, the
reaction on their movement is expected and natural.
World of Movable Objects 165 (978) Chapter 8 Transparent nodes
• If you move points B or C, you change the distance between two horns and thus change the size of crescent. The
crescent width is not affected by such movement.
• If you move points A or D, you change the width of the crescent but not the distance between its horns.
Certainly, there are restrictions on both types of movement; these restrictions are used in the MoveNode() method.
It is not a problem at all to cover both borders by two sets of overlapping circular nodes and thus organize the resizing by
any border point, but I cannot decide even for myself what kind of resizing I would expect as a reaction on moving other
border points so I decided not to use them.
Figure 8.6 gives a sketch of crescent with auxiliary lines and some comments which are identical to the fields used in the
Crescent class. (Menu on crescent allows to switch ON / OFF the visualization of those auxiliary lines but without any
comments.)
public class Crescent : GraphicalObject
{
PointF ptA, ptB, ptC, ptD, ptCenterOuter, ptCenterInner;
// points are marked in the book (fig.8.5)
double rInnerBorder, rOuterBorder; // rOuterBorder < rInnerBorder !
double width; // width of crescent in the middle
double h; // half distance between the horns
double overhang; // distance from the middle point of the line
// between the horns to the nearest point of crescent
double angle; // angle of an axis from ptCenterInner to ptCenterOuter
double alpha; // angle of the outer arc (half of the arc)
double betta; // angle of the inner arc (half of the arc) betta < alpha !
SolidBrush brush;
double compensation;
int nMinWidth = 10;
Figure 8.5 shows the crescent cover. There are six nodes; all of them are circles. As usual, the smallest nodes are at the
head; these four nodes cover the points of resizing. The last node in the cover provides movement of the whole object by
any inner point and this node must be preceded by transparent node which cuts out the bigger part of the last node.
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);
}
We have a cover consisting of circular nodes, while the area of an object is not even close to any circle. Yet, this object can
be moved forward and rotated by any inner point and can be resized by the points which look most natural for such
operation.
All nodes of the cover are circular, so the analysis of the movement possibility inside the MoveNode() method is based on
the number of the caught node. First we have the smallest nodes at four special points in the order A – D – B – C, then the
transparent node, and then the node to provide forward movement and rotation. Thus, if there are any restrictions on
resizing, then they must be considered for nodes 0, 1, 2, and 3. What are the limitations on resizing such crescent?
Point A can be moved along the AD line one way or another. If it is moving away from point D, then crescent is becoming
wider. It stops being a crescent when point A crosses the BC line; I put the limit of moving point A in this direction at 3
pixels before such crossing, so that crescent will not lose its shape. When point A is moving towards point D, then crescent
becomes thinner. I want always to have some space between the nodes on these two points so that by its middle part even
World of Movable Objects 166 (978) Chapter 8 Transparent nodes
the thinnest crescent could be moved around the screen. Thus, I put the limit on the minimum width of a crescent; this
minimum width is slightly bigger than the diameter of those nodes.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
PointF ptH_base;
if (catcher == MouseButtons .Left)
{
double a, b, x_cross, y_cross;
ptH_base = Auxi_Geometry .Middle (ptB, ptC);
double maxWidth, widthNew, minH, hNew;
PointF ptNew;
switch (iNode)
{
case 0: // ptA
maxWidth = width + overhang - 3;
Auxi_Geometry .Perpendicular ((PointF) ptM, ptCenterInner, ptD,
out a, out b, out x_cross, out y_cross);
ptNew = new PointF (Convert .ToSingle (x_cross),
Convert .ToSingle (y_cross));
widthNew = Auxi_Geometry .Distance (ptNew, ptD);
double overhangNew = Auxi_Geometry .Distance (ptNew, ptH_base);
if (nMinWidth <= widthNew &&
widthNew < maxWidth &&
overhangNew >= 3 &&
Auxi_Geometry .Distance (ptH_base, ptNew) <=
Auxi_Geometry .Distance (ptH_base, ptD) - 3)
{
ptA = ptNew;
width = widthNew;
overhang = overhangNew;
rInnerBorder = h * h / (2 * overhang) + overhang / 2;
ptCenterInner = Auxi_Geometry .PointToPoint (ptA,
angle + Math .PI, rInnerBorder);
betta = Math .Asin (h / rInnerBorder);
bRet = true;
}
break;
… …
Results of moving point D are already described in the previous sentences. The code for the node over this point is similar
to the previous case with only minor changes, so I decided not to include it here.
It is not so easy to explain some of the limits which you can see in the MoveNode() method for moving points B and C. I
made some estimates and put it so that crescent will always have the sharp horns. Anyway, this crescent is only an example
to show the use of transparent nodes in design, moving, and resizing of not very often used but interesting objects.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
case 2: // ptB
case 3: // ptC
minH = overhang + width + 3;
hNew = Auxi_Geometry .Distance_PointLine (ptM, ptCenterInner, ptD);
if (hNew >= minH)
{
h = hNew;
rInnerBorder = h * h / (2 * overhang) + overhang / 2;
World of Movable Objects 167 (978) Chapter 8 Transparent nodes
rOuterBorder = h * h / (2 * (overhang + width)) +
(overhang + width) / 2;
alpha = Math .Asin (h / rOuterBorder);
betta = Math .Asin (h / rInnerBorder);
ptB = Auxi_Geometry .PointToPoint (ptH_base,
angle + Math .PI / 2, h);
ptC = Auxi_Geometry .PointToPoint (ptH_base,
angle - Math .PI / 2, h);
ptCenterInner = Auxi_Geometry .PointToPoint (ptA,
angle + Math .PI, rInnerBorder);
ptCenterOuter = Auxi_Geometry .PointToPoint (ptCenterInner,
angle, rInnerBorder + width - rOuterBorder);
bRet = true;
}
break;
… …
If you need to organize the crescent rotation, what point would you choose as a central point of rotation? Crescent is a part
of the circle with the central point ptCenterOuter, so it would be natural to select this point, but… In such case a
crescent with auxiliary lines in view (or with cover switched ON) would need a lot of space to be in view throughout the
rotation. I decided to make this needed area smaller and organized rotation around point A – the middle point on the inner
border. The first indication of this fact can be seen in the Crescent.StartRotation() method where an angle to the
mouse must be calculated as an angle of the line from the central point of rotation to the mouse point.
public void StartRotation (Point ptMouse)
{
double angleMouse = Auxi_Geometry .Line_Angle (ptA, ptMouse);
compensation = Auxi_Common .LimitedRadian (angleMouse - angle);
}
Throughout the rotation, all basic points of crescent are calculated except point A; this means that this point is unchanged
throughout rotation.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
PointF ptH_base;
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (ptA, ptM);
angle = angleMouse - compensation;
ptD = Auxi_Geometry .PointToPoint (ptA, angle, width);
ptH_base = Auxi_Geometry.PointToPoint (ptA, angle + Math.PI, overhang);
ptB = Auxi_Geometry .PointToPoint (ptH_base, angle + Math .PI / 2, h);
ptC = Auxi_Geometry .PointToPoint (ptH_base, angle - Math .PI / 2, h);
ptCenterInner =
Auxi_Geometry .PointToPoint (ptA, angle + Math .PI, rInnerBorder);
ptCenterOuter =
Auxi_Geometry .PointToPoint (ptD, angle + Math .PI, rOuterBorder);
bRet = true;
}
return (bRet);
}
World of Movable Objects 168 (978) Chapter 8 Transparent nodes
Sector of a circle
Sector of a circle is another type of object that can demonstrate an interesting example of using transparent nodes. There is
one limitation on all the examples of this section: the angle of a sector must not exceed 180 degrees. If by the requirements
of the particular task you need to work with bigger sectors, then similar but slightly different (and a bit simpler!) covers
must be used. Such cover will be shown in the next section of this chapter, but in the current section we are going to deal
only with sectors which do not exceed 180 degrees.
Circle sector can be unmovable or movable, non-resizable or resizable, and resizing can be organized in different ways.
Regardless of all these things, the parameters to describe sector geometry are the same, so I organized the
CircleSectorData class which is used in different classes of movable / resizable sectors.
public class CircleSectorData
{
PointF m_center;
float m_radius;
double angleStart;
double angleSector; // -180 <= angleSector <= 180 degree
SolidBrush brush;
static float fMinRadius = 20;
Sector geometry is defined by central point (m_center), radius (m_radius), and two angles. The first of them –
angleStart – sets the angle for one of the sides; the second – angleSector – defines the sector angle which is the
angle from that first side to another. As usual, positive angles are measured counterclockwise from the given line.
Objects of the CircleSectorData class contain only data; they are not involved in any movement, so they have no
cover. This data is used to describe resizable sectors and I do not want those sectors to disappear as a result of squeezing, so
there is a limit on minimal allowed radius.
There are several examples in this section; all of them demonstrate sectors of circles. All these sectors use the same
mechanism to organize forward movement and rotation but differ in resizing. There are two sectors in each example, there
is no mechanism to add new objects, and you cannot change the order of objects by a simple click. It is easy to add all these
things, but I purposely excluded them in order to keep the code simpler. All these things were demonstrated in many
previous examples, so it is not a problem for you to add them if you want some kind of a small exercise.
The first example works with non-resizable sectors. This is a very rare type of objects in my programs, so look at the first
example as some kind of preliminary step to following examples.
Non-resizable sectors
File: Form_CircleSector_Nonresizable.cs
Menu position: Graphical objects – Basic elements – Sector of a circle – Non-resizable sector
All parameters which are needed for any sector of the CircleSector_Nonresizable class are “hidden” inside the
CircleSectorData field. The only addition to this standard data is the compensation field which is used in the
standard way throughout the rotation.
public class CircleSector_Nonresizable : GraphicalObject
{
CircleSectorData dataSector;
double compensation;
One sector side is associated with angleStart; another one is moved from this first one on angleSector. There is no
restriction on counting the sides clockwise or counterclockwise and in the Form_CircleSector_Nonresizable.cs I
purposely demonstrate both cases.
private void OnLoad (object sender, EventArgs e)
{
… …
sectors .Add (new CircleSector_Nonresizable (new PointF (200, 200), 110,
10, 30, Color .Blue));
sectors .Add (new CircleSector_Nonresizable (new PointF (400, 400), 170,
160, -135, Color .Lime));
… …
World of Movable Objects 169 (978) Chapter 8 Transparent nodes
Sectors of the CircleSector_Nonresizable class are only movable, so there are no nodes on borders. Only forward
movement and rotation by any inner point is required. Both movements must be started at any inner point, so the sensitive
area of the cover must be equal to sector area.
Sector is some part of a circle, so it is natural to try using a circular node for moving of such object, but the part of the
circular node which is outside the sector must be not used for moving an object. This part of the circle must disappear and
the needed “cut out” is done by two polygonal
transparent nodes (in this case rectangles) which
are positioned next to sector sides. The sizes of
these rectangular nodes are calculated in such a
way that each one closes and eliminates half of the
full circle. Part of the circle is eliminated by both
polygonal nodes, but it does not matter. Whenever
mover detects a transparent node at any point, there
is no analysis on the further part of this cover; it is
ignored regardless of what is there behind that
transparent node. The transparent nodes are used
to exclude some parts of another node, so the
transparent nodes must be placed in the cover
ahead of the node from which they have to cut out
some part.
At figure 8.7 points A and B mark two ends of the
sector arc; these are the points where two sides of
sector meet with the circle. For my calculations to Fig.8.7 Covers for non-resizable sectors
be correct, I need point B to be clockwise from
point A as shown at figure 8.7. First, the angles to points A and B are defined (angleA and angleB); these calculations
depend on the sign of the angleSector. Then the corners of two rectangles are calculated. The rectangles are based on
the sides A and B; the corners of rectangles are going to be the points of two polygonal nodes. In the code below, array
ptsA[] contains the corners of rectangle next to point A, while the array ptsB[] contains the corners of rectangle next to
point B.
public override void DefineCover ()
{
PointF ptA, ptB;
double angleA, angleB;
if (SectorAngle >= 0)
{
angleB = StartAngle;
angleA = StartAngle + SectorAngle;
}
else
{
angleA = StartAngle;
angleB = StartAngle + SectorAngle;
}
ptA = Auxi_Geometry .PointToPoint (Center, angleA, Radius);
ptB = Auxi_Geometry .PointToPoint (Center, angleB, Radius);
PointF [] ptsA = new PointF [] {
ptA,
Auxi_Geometry .PointToPoint (ptA, angleA + Math .PI / 2, Radius),
Auxi_Geometry .PointToPoint (Center, angleA + 3 * Math .PI / 4,
Radius * Math .Sqrt (2)),
Auxi_Geometry .PointToPoint (Center, angleA - Math .PI, Radius) };
PointF [] ptsB = new PointF [] {
ptB,
Auxi_Geometry .PointToPoint (Center, angleB - Math .PI, Radius),
Auxi_Geometry .PointToPoint (Center, angleB - 3 * Math .PI / 4,
Radius * Math .Sqrt (2)),
Auxi_Geometry .PointToPoint (ptB, angleB - Math .PI / 2, Radius) };
World of Movable Objects 170 (978) Chapter 8 Transparent nodes
CoverNode [] nodes = new CoverNode [3] {
new CoverNode (0, ptsA, Behaviour .Transparent),
new CoverNode (1, ptsB, Behaviour .Transparent),
new CoverNode (2, Center, Radius, Cursors .SizeAll) };
cover = new Cover (nodes);
cover .SetClearance (false);
}
In several examples further on you will see the use of rectangular transparent node to cut out a semicircle and in all those
examples the standard set of code lines is substituted by the use of the
Auxi_Geometry.PolygonAroundSemicircle() method, so you will see something like
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (Center, angleA,
Radius, Rotation .Counterclock);
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (Center, angleB,
Radius, Rotation .Clockwise);
Circle sector has one basic point – the central point of the circle – so the Move() method is extremely short.
public override void Move (int dx, int dy)
{
dataSector .Center += new SizeF (dx, dy);
}
MoveNode() method has to describe reaction on moving each node. We have a cover consisting of three nodes of which
two are transparent. I have already explained that the MoveNode() method never deals with transparent nodes, thus, the
MoveNode() method of this class does not need any checking at all. If there is a call for the MoveNode() method of
this class, then it can be only from the remaining non-transparent node which is responsible for sector forward movement
and in this case the Move() method must be called. This is for the case of the left button pressed while rotation is
organized in a standard way with calculation of compensation at the starting moment and using this value throughout
the rotation to calculate the object angle. The compensation is calculated to that side of the sector which is associated
with the angleStart.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
StartAngle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
Usually two places in the code are crucial for decisions on further actions when some screen object is pressed. First is the
MoveNode() method of the particular class of the involved object. Second is the OnMouseUp() method of the form
where it happens. Sectors of the CircleSector_Nonresizable class are so simple that they are not even mentioned
in the Form_CircleSector_Nonresizable.OnMouseUp() method, but I still want to include here the code of this method
because I want to attract attention to one interesting fact.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
if (e .Button == MouseButtons .Right && fDist <= 3)
World of Movable Objects 171 (978) Chapter 8 Transparent nodes
{
if (mover .ReleasedSource is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, null,
PointToScreen (ptMouse_Up));
}
}
}
}
I want to underline the fact that all lines of code except one in this OnMouseUp() method are needed only because the
form contains information of the InfoOnRequest class for which a special tuning form can be opened at any moment. If
not for this information, the OnMouseUp() method would be much simpler and shorter. In the previous version of the
same example, there was constantly shown information of the Text_Horizontal class without any tuning at all and
then the same method was much simpler and looked like this.
private void OnMouseUp (object sender, MouseEventArgs e)
{
mover .Release ();
}
That was all, though all elements moved in exactly the same way. When you look at the code of some methods, try to
figure out what is really essential to organize some movement and what is added only for better explanation.
Nonresizable circle sector is another example of an object the shape of which is far away from the shape of the used nodes.
There is one circular node in the cover, but the elegance of solution is ensured by two rectangles. Years ago Henry Kuttner
wrote several marvellous stories about one interesting family. Members of the Hogben family could, among other things,
construct an ultrasonic device on the basis of an old battery and several wires.* I always admired their work and here I tried
to do something in their style. To design a movable circle sector, I used two rectangles!
OK, let us move to more interesting circle sectors. Border of any sector consists of three parts and I am going to make these
parts movable one by one. The first change makes arc movable.
*
“Exit the Professor” by Henry Kuttner and C.L.Moore
World of Movable Objects 172 (978) Chapter 8 Transparent nodes
public class CircleSector_MovableArc : GraphicalObject
{
CircleSectorData dataSector;
Pen penArc = new Pen (Color .DarkGray, 3);
int nNodesOnArc;
double compensation;
int nrSmall = 5;
int distanceNeighbours = 8;
Forward movement and rotation by any inner point are provided by one circular node and two rectangular nodes. As was
shown in the previous example, two transparent rectangles must stay ahead of the big circle. The number of small circular
nodes on the border is calculated in the standard way. This number depends on the radius of nodes, distance between the
neighbours, and the sector angle. Taking into consideration the possibility of arcs with a small angle, I put additional
condition that the number of those small nodes cannot be less than two. To make the resizing easier, all these small nodes
are the first in the cover.
The first part of the DefineCover() method in which the corner points of two big rectangular nodes are calculated is
identical to the previous example; the second part of method is slightly different because all the small nodes must be
included into cover ahead of three big nodes.
public override void DefineCover ()
{
double angleA, angleB;
if (SectorAngle >= 0)
{
angleB = StartAngle;
angleA = StartAngle + SectorAngle;
}
else
{
angleA = StartAngle;
angleB = StartAngle + SectorAngle;
}
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (Center, angleA,
Radius, Rotation .Counterclock);
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (Center, angleB,
Radius, Rotation .Clockwise);
CoverNode [] nodes = new CoverNode [nNodesOnArc + 2 + 1];
double delta = SectorAngle / (nNodesOnArc - 1);
for (int i = 0; i < nNodesOnArc; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (Center,
StartAngle + i * delta, Radius), nrSmall);
}
nodes [nNodesOnArc] =
new CoverNode (nNodesOnArc, ptsA, Behaviour .Transparent);
nodes [nNodesOnArc + 1] =
new CoverNode (nNodesOnArc + 1, ptsB, Behaviour .Transparent);
nodes [nNodesOnArc + 2] =
new CoverNode (nNodesOnArc + 2, Center, Radius, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
The CircleSector_MovableArc.MoveNode() method has to check the number of the caught node because nearly
all of non-transparent nodes are responsible for resizing and only the last one provides moving of the whole sector.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
World of Movable Objects 173 (978) Chapter 8 Transparent nodes
{
if (iNode < nNodesOnArc)
{
float fRadNew =
Convert .ToSingle (Auxi_Geometry .Distance (Center, ptM));
if (fRadNew >= CircleSectorData .MinimumRadius)
{
Radius = fRadNew;
bRet = true;
}
}
else
{
Move (dx, dy);
}
}
… …
We have looked at the changes of the sector class when arc became movable. What changes are needed in the form itself?
Only one. We are exploring the use of transparent nodes, but in this example we have sectors with N-node cover type.
When an object with such type of cover is released after resizing, it usually needs a new cover because the number of nodes
depends on the size.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iWasObject, iWasNode;
if (mover .Release (out iWasObject, out iWasNode))
{
GraphicalObject grobj = mover [iWasObject] .Source;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is CircleSector_MovableArc)
// && iWasNode != grobj .NodesCount - 1
{
(grobj as CircleSector_MovableArc) .RedefineCover ();
Invalidate ();
}
… …
Inside the OnMouseUp() method you see a call to the CircleSector_MovableArc.RedefineCover() method.
In the shown code the cover is renewed in all cases whenever a sector is released by the left button. If there was not a
resizing but a simple movement of the sector around the screen, then this renewal of cover is not needed, but in the current
version it happens. If you want to exclude the unneeded renewal of cover after forward movement, then insert into the code
that short piece with additional checking which is now commented and for the last node in the cover – the big circular node
– the renewal will be not called.
they are both either ON or OFF. In order to receive such figure, I added a small piece of code into the OnPaint() method
of the form; this piece is still there but commented.
The CircleSector_OneMovableSide class has some additional fields in comparison with the previous case of sector
with unmovable sides; these fields are used to regulate the movement of the sliding side inside the available interval of
angles.
public class CircleSector_OneMovableSide : GraphicalObject
{
CircleSectorData dataSector;
Pen penMovableBorder = new Pen (Color .DarkGray, 3);
int nNodesOnArc;
double compensation, compensationResectoring;
double minAngle, maxAngle;
double angleFull; // caculated once at initialization
int nrSmall = 5;
int distanceNeighbours = 8;
The unmovable side of sector is determined by the
angleStart. Angle for the movable side is
angleStart + angleSector. The cover for
the CircleSector_OneMovableSide sector
differs from the previous case by one strip node over
the movable side; this node takes its place in the cover
after all circular nodes on the arc. Thus, we have an
additional line in the DefineCover() method for
this new strip node.
The first part of the DefineCover() method is the
same as for the previous sectors, the second part is
slightly changed. Nodes are included into cover in
such order:
1. Small circular nodes on the arc.
2. Strip node on the movable side.
3. Two transparent rectangular nodes. Fig.8.9 Sectors with movable arc and one side
4. One big circular node.
public override void DefineCover ()
{
double angleA, angleB;
if (SectorAngle >= 0)
{
angleB = StartAngle;
angleA = StartAngle + SectorAngle;
}
else
{
angleA = StartAngle;
angleB = StartAngle + SectorAngle;
}
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (Center, angleA,
Radius, Rotation .Counterclock);
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (Center, angleB,
Radius, Rotation .Clockwise);
CoverNode [] nodes = new CoverNode [nNodesOnArc + 1 + 2 + 1];
double delta = SectorAngle / (nNodesOnArc - 1);
for (int i = 0; i < nNodesOnArc; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (Center,
StartAngle + i * delta, Radius), nrSmall);
World of Movable Objects 175 (978) Chapter 8 Transparent nodes
}
nodes [nNodesOnArc] = new CoverNode (nNodesOnArc, Center,
Auxi_Geometry .PointToPoint (Center, StartAngle + SectorAngle, Radius));
int iTransp = nNodesOnArc + 1;
nodes [iTransp] = new CoverNode (nNodesOnArc, ptsA, Behaviour.Transparent);
nodes [iTransp + 1] =
new CoverNode (nNodesOnArc + 1, ptsB, Behaviour .Transparent);
nodes [iTransp + 2] =
new CoverNode (nNodesOnArc + 2, Center, Radius, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
Sector can be easily rotated around, so at the starting moment of changing the sector its unmovable side may have any angle
(angleStart). While the sliding side of the sector is moved, the difference between two sides cannot exceed
180 degrees. The movable side is always in the same direction from unmovable as at the moment of initialization. The
maximum angle on which the movable side can go from unmovable is called angleFull.
public CircleSector_OneMovableSide (PointF ptC, float rad,
double angleStart_Degree, double angleSector_Degree, Color color)
{
dataSector = new CircleSectorData (ptC, rad, angleStart_Degree,
angleSector_Degree, color);
NodesOnArc ();
angleFull = (SectorAngle >= 0) ? Math .PI : -Math .PI;
}
If the strip node on the movable side of a sector is caught, then the StartResectoring() method must be called to
calculate the range of angles in which this side can be turned around.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is CircleSector_OneMovableSide)
{
CircleSector_OneMovableSide sector =
grobj as CircleSector_OneMovableSide;
if (e .Button == MouseButtons .Left &&
mover .CaughtNodeShape == NodeShape .Strip)
{
sector .StartResectoring (e .Location);
}
else if (e .Button == MouseButtons .Right)
{
sector .StartRotation (e .Location);
}
}
}
}
The StartResectoring() method calculates absolute angles between which the movable side can go; these angles are
called minAngle and maxAngle. These two angles are determined by the angle of the unmovable side at this moment
(angleStart) and angleFull which was calculated at the moment of initialization.
The special compensation for changing the angle of sector (compensationResectoring) which you see in the
StartResectoring() method is used for more accurate turn of the movable side. This is an angle between the mouse
position at the starting moment and the angle of the movable side. Though this is a small angle, I cannot ignore it;
otherwise it can result in the program crash. The cause of this angle appearance is the finite width of the strip node over the
movable side. If I would allow to catch the movable side only by the line of this side (one pixel width!), then there would
be no need in such compensation, but no user will like such a decision. Catching the side by any point in its vicinity (a strip
World of Movable Objects 176 (978) Chapter 8 Transparent nodes
of 6 pixels width) is much easier, but then consider such a situation. You have a sector opened on the maximum allowed
angle of 180 degrees and you catch the movable side one or two pixels outside the sector border. If this small difference is
ignored and the mouse position is looked at as the new position of the side, then it would be a violation of the rule that
sector cannot exceed 180 degree. For this reason, I take into consideration this small compensationResectoring
angle.
public void StartResectoring (Point ptMouse)
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
double angleSlider = angleStart + angleSector;
compensationResectoring =
Auxi_Common .LimitedRadian (angleMouse - angleSlider);
if (angleFull > 0)
{
minAngle = angleStart;
maxAngle = angleStart + angleFull;
}
else
{
maxAngle = angleStart;
minAngle = angleStart + angleFull;
}
}
Whenever the side is moved by mouse, this compensation is used to calculate the exact angle of the side.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == nNodesOnArc) // sliding side
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
double angleSlider = angleMouse - compensationResectoring;
if (angleSlider > maxAngle)
{
angleSlider -= 2 * Math .PI;
}
else if (angleSlider < minAngle)
{
angleSlider += 2 * Math .PI;
}
if (minAngle <= angleSlider && angleSlider <= maxAngle)
{
SectorAngle = angleSlider - StartAngle;
bRet = true;
}
}
… …
startAngle is shown with wide pen but of different color (blue). Figure 8.10 is prepared in such way that only one
cover is visualized, so you can see here sectors both with and without covers.
There are several new fields in the CircleSector_FullyResizable class because in this class different technique is
used to determine whether the movement of the sector side is allowed or not.
public class CircleSector_FullyResizable : GraphicalObject
{
CircleSectorData dataSector;
Pen penMovableBorder = new Pen (Color .DarkGray, 3);
Pen penStartSide = new Pen (Color .Blue, 3);
int nNodesOnArc;
double compensation;
double angleInit, angleToIn, angleEnd;
PointF ptSeg_0, ptSeg_1; // two points to define the full opening
PointF ptIn;
int nrSmall = 5;
int distanceNeighbours = 8;
Cover of the fully resizable sector differs
from the previous case by one additional
strip node, but I also changed the order of
nodes. The number of small circular
nodes along the arc depends on the size of
the sector while two strip nodes along the
sides are always the same, so I put those
strip nodes ahead of the small circular
nodes. These strip nodes have numbers 0
and 1, so it is always known what side is
associated with each of them. Thus, we
have such order of nodes.
1. Strip node on the side associated
with angleStart +
angleSector.
Fig.8.10 Fully resizable sectors
2. Strip node on the side associated
with angleStart; this side is marked with blue line.
3. Circular nodes along the arc.
4. Two transparent polygonal nodes.
5. Big circular node for the whole circle.
In the first part of the DefineCode() method the corners of two rectangular nodes are calculated. This part is identical to
the previous cases. Nodes and cover are constructed in the second part of the DefineCode() method.
public override void DefineCover ()
{
double angleA, angleB;
if (SectorAngle >= 0)
{
angleB = StartAngle;
angleA = StartAngle + SectorAngle;
}
else
{
angleA = StartAngle;
angleB = StartAngle + SectorAngle;
}
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (Center, angleA,
Radius, Rotation .Counterclock);
World of Movable Objects 178 (978) Chapter 8 Transparent nodes
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (Center, angleB,
Radius, Rotation .Clockwise);
CoverNode [] nodes = new CoverNode [2 + nNodesOnArc + 2 + 1];
nodes [0] = new CoverNode (0, Center, Auxi_Geometry .PointToPoint (Center,
StartAngle + SectorAngle, Radius));
nodes [1] = new CoverNode (1, Center, Auxi_Geometry .PointToPoint (Center,
StartAngle, Radius));
double delta = SectorAngle / (nNodesOnArc - 1);
for (int i = 0; i < nNodesOnArc; i++)
{
nodes [2 + i] = new CoverNode (2 + i,
Auxi_Geometry .PointToPoint (Center, StartAngle + i * delta, Radius),
nrSmall);
}
int iTransp = 2 + nNodesOnArc;
nodes [iTransp] =
new CoverNode (nNodesOnArc, ptsA, Behaviour .Transparent);
nodes [iTransp + 1] =
new CoverNode (nNodesOnArc + 1, ptsB, Behaviour .Transparent);
nodes [iTransp + 2] =
new CoverNode (nNodesOnArc + 2, Center, Radius, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
In the previous case of sector with only one movable side, the range of possible movement of that side [minAngle,
maxAngle] was calculated at the starting moment when the side was caught for moving. For sector with two movable
sides this technique works not so well, so I use another algorithm.
The main problem here is to guarantee that the movable side is always going to be not farther than 180 degrees from the
unmovable side and never moves into another half of the circle making a sector of more than 180 degrees. For this purpose,
at the starting moment of movement one additional point (ptIn) is determined in that half of a circle through which the
caught side is allowed to move. The location of this point is determined by the initial sign of the angleSector and also
by the side which is caught for moving.
public void StartResectoring (int iNode)
{
StartAngle = Auxi_Common .LimitedRadian (StartAngle);
if (iNode == 0) // end line
{
ptSeg_0 = Auxi_Geometry .PointToPoint (Center, StartAngle, Radius);
ptSeg_1 = Auxi_Geometry .PointToPoint (Center, StartAngle + Math .PI,
Radius);
if (angleInit >= 0)
{
angleToIn = StartAngle + Math .PI / 2;
}
else
{
angleToIn = StartAngle - Math .PI / 2;
}
}
else // start line
{
angleEnd = Auxi_Common .LimitedRadian (StartAngle + SectorAngle);
ptSeg_0 = Auxi_Geometry .PointToPoint (Center, angleEnd, Radius);
ptSeg_1 = Auxi_Geometry .PointToPoint (Center, angleEnd + Math .PI,
Radius);
if (angleInit >= 0)
{
angleToIn = angleEnd - Math .PI / 2;
World of Movable Objects 179 (978) Chapter 8 Transparent nodes
}
else
{
angleToIn = angleEnd + Math .PI / 2;
}
}
ptIn = Auxi_Geometry .PointToPoint (Center, angleToIn, Radius);
}
Points ptSeg_0 and ptSeg_1 are two ends of the diameter which goes along the side that is currently not moved. The
caught side can move only through one half organized by this diameter; ptIn belongs to that side which is allowed for
movement. So the check for the side in the MoveNode() method is based on the fact that this auxiliary point and the
current mouse position cannot be on different sides of the mentioned diameter.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
if (iNode == 0)
{
if (!Auxi_Geometry.OppositeSideOfLine(ptSeg_0, ptSeg_1, ptM, ptIn))
{
SectorAngle =
Auxi_Common .LimitedRadian (angleMouse - StartAngle);
bRet = true;
}
}
else if (iNode == 1)
{
if (!Auxi_Geometry.OppositeSideOfLine(ptSeg_0, ptSeg_1, ptM, ptIn))
{
SectorAngle = Auxi_Common.LimitedRadian (angleEnd -angleMouse);
StartAngle = Auxi_Common.LimitedRadian (angleEnd -SectorAngle);
bRet = true;
}
}
… …
Two examples demonstrate two different techniques to check the movement possibility. I am not sure which of them is
better, so I demonstrate both.
Do you think that we have analysed all possible situations with circle sectors? I think that there are interesting cases which
are not covered by this series of examples so I am going to return to circle sectors again further on.
One general remark on the resizable circle sectors. It is a standard situation in programming that the most problematic and
the most interesting cases to deal with are the boundary cases. For the analysed objects, these are the cases with sector
angle near zero or 180 degrees. In the first case it is a problem of visualizing the disappearing sector. In the second case
this is the problem of going beyond semicircle. In some programs sectors have to stay inside semicircle and for such
objects the demonstrated covers and technique can be used. If there is no upper limit of 180 degrees and bigger sectors are
allowed, then something has to be done with its cover because two rectangular nodes cannot be used for bigger angles. The
possible solution is demonstrated in the next example.
World of Movable Objects 180 (978) Chapter 8 Transparent nodes
Figure 8.11 shows both variants of the cover; some parts of the covers are identical; others are different and depend on the
gap angle. The first thing to do in this cover design is to determine the needed number of nodes and to declare the first node
which is a transparent node to cut out the inner hole.
public override void DefineCover ()
{
int nNodes;
if (Math .Abs (angleArc) <= Math .PI)
{
nNodes = 4;
}
else
{
nNodes = 3;
}
CoverNode [] nodes = new CoverNode [nNodes];
nodes [0] = new CoverNode (0, m_center, rInner, Behaviour .Transparent);
… …
The next step is to calculate transparent node(s) which turn ring into arc.
When the gap angle is 180 degrees or more (yellow arc), it is done by two rectangular nodes exactly as it was done in the
previous examples with circle sector.
public override void DefineCover ()
{
… …
if (Math .Abs (angleArc) <= Math .PI)
{
//nNodes = 4;
double angleA, angleB;
if (angleArc >= 0)
{
angleB = angleStart;
angleA = angleStart + angleArc;
}
else
{
angleA = angleStart;
angleB = angleStart + angleArc;
}
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (m_center,
angleA, rOuter, Rotation .Counterclock);
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (m_center,
angleB, rOuter, Rotation .Clockwise);
nodes [1] = new CoverNode (1, ptsA, Behaviour .Transparent);
nodes [2] = new CoverNode (2, ptsB, Behaviour .Transparent);
}
… …
When the gap angle is less than 180 degrees (cyan arc), only one rectangular node is needed. At figure 8.11, this node is
definitely bigger than it is needed for this particular gap, and there is a very simple explanation. One corner of this
polygonal node is positioned on the central point, while three other corners are placed at the same distance from the central
point. The minimal sufficient polygon to cut out the gap must have its two outer sides as tangents to outer circle, but in
such case the distance from central point to those three vertices depends on the gap angle. Bigger than minimal required
polygon makes no problems because it is a transparent node, but the calculations of bigger rectangle are much easier. I
simply calculate the needed distance to the vertices for the worst case of 180 degree gap and use this distance regardless of
the gap angle.
public override void DefineCover ()
{
… …
else
World of Movable Objects 182 (978) Chapter 8 Transparent nodes
{
//nNodes = 3;
double angleGap;
if (angleArc > 0)
{
angleGap = angleArc - 2 * Math .PI;
}
else
{
angleGap = angleArc + 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 + angleGap/2, dist),
Auxi_Geometry.PointToPoint (m_center, angleStart + angleGap, dist) };
nodes [1] = new CoverNode (1, pts, Behaviour .Transparent);
}
… …
The last node in the cover is always the same: it is a big circular node up to the outer border and this is the only sensitive
node in the cover.
public override void DefineCover ()
{
… …
nodes [nNodes - 1] =
new CoverNode (nNodes - 1, m_center, rOuter, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
I demonstrate these covers for the case of the wide arcs because the picture of such arcs makes everything obvious. The
same type of covers can be used for thin arcs and instead of multiple small circular nodes in a cover you will have only
three or four nodes.
If for some reason you do not want to have variable number of nodes, you can make it four nodes for both cases. Here are
the small changes of the DefineCover() method which you will have to do.
• Declare from the beginning that there are four nodes in the cover.
CoverNode [] nodes = new CoverNode [4];
• Organize one more identical node in case of the small gap angle.
nodes [1] = new CoverNode (1, pts, Behaviour .Transparent);
nodes [2] = new CoverNode (2, pts, Behaviour .Transparent);
• The last node will be the same but always with the number three.
nodes [3] = new CoverNode (3, m_center, rOuter, Cursors .SizeAll);
World of Movable Objects 183 (978) Chapter 8 Transparent nodes
For scaling of regular polygon, there must be special coefficient calculated at the starting moment of resizing. For rotation
of any border its compensation angle is calculated at the starting moment of rotation.
Taking into consideration all these things, we receive such set of fields for the
RegPoly_RegHole_RotatableBorders class.
public class RegPoly_RegHole_RotatableBorders : GraphicalObject
{
PointF m_center;
float rOuter, rInner;
int nVerticesOut, nVerticesIn;
double angleOut, angleIn;
SolidBrush brush;
Pen penBorder_AllChanges, penBorder_ResizableOnly, penBorder_RotatableOnly;
bool bResize_Outer;
bool bResize_Inner;
bool bRotate_Figure;
bool bRotate_OuterBorder;
bool bRotate_InnerBorder;
double scaling_Outer, scaling_Inner;
double compensation_Outer, compensation_Inner;
static float minWidth = 12;
static float minRadius_Inner = 20;
If polygon is resizable, then it must be resizable by any point of its border. The easiest way to organize such resizing is to
cover the border segments by narrow strip nodes; we already used such covers in the previous examples of regular
polygons, so this is a standard decision. As we have two borders in the new class of polygons, then their cover has to
include two sets of strip nodes. The order of these sets does not matter and in all classes with polygons and holes I begin
with the nodes on inner border. Forward movement and rotation of polygon with a hole is provided with a standard pair of
nodes: big polygonal node up to the outer border of polygon must be preceded by transparent node with the shape of a hole,
so in the new class this transparent node is also a regular polygon. Everything is obvious and the design of such cover is not
a problem.
public override void DefineCover ()
{
PointF [] ptsOut = VerticesOut;
PointF [] ptsIn = VerticesIn;
int nNodes = nVerticesIn + nVerticesOut + 1 + 1;
CoverNode [] nodes = new CoverNode [nNodes];
for (int i = 0; i < nVerticesIn; i++)
{
nodes[i] = new CoverNode (i, ptsIn [i], ptsIn [(i + 1) % nVerticesIn]);
}
int j0 = nVerticesIn;
for (int i = 0; i < nVerticesOut; i++)
{
nodes [j0 + i] = new CoverNode (j0 + i, ptsOut [i],
ptsOut [(i + 1) % nVerticesOut]);
}
j0 = nVerticesIn + nVerticesOut;
nodes [j0] = new CoverNode (j0, ptsIn, Behaviour .Transparent);
nodes [j0 + 1] = new CoverNode (j0 + 1, ptsOut);
… …
Looks like everything is ready and only one line with the call to the Cover constructor is missing in the above code. In
reality the DefineCover() method of this class has a whole second half. There are no more nodes to be organized, but
there is a chance that parameters of some of the created nodes must be changed.
I want to remind about one example from the very beginning of this book. The Form_Rectangles_Standard.cs
(figure 3.1) demonstrates four rectangles with different types of resizing. Depending on its type of resizing, each rectangle
has different number of nodes in its cover. As a result, the nodes with the same numbers in different rectangles are
responsible for different movements, so there are four different cases in the Rectangle_Standard.MoveNode()
method. This is one way to organize movements of objects with some regulating parameters which can change the cover.
World of Movable Objects 185 (978) Chapter 8 Transparent nodes
In the RegPoly_RegHole_RotatableBorders class I use another solution: the number of nodes is always the same
as demonstrated in the first half of the DefineCover() method, but if some nodes are not needed, then they are switched
OFF. What is the easiest way to exclude any node from consideration in the MoveNode() method of the class? To
declare this node transparent. In this particular case of polygons it is a good solution because the area excluded from
consideration is so small in comparison with the full area of an object that it has no effect on other movements.
Look at figure 8.12. The outer border of the yellow polygon is neither resizable nor rotatable and the lack of any wide line
along this border informs about it. If you switch ON the cover visualization in such case, you will still see narrow strip
nodes along that border, but there will be no reaction if you try to press those nodes with a mouse. By default, strip nodes
are six pixels wide but this is their full width, so if this node is declared transparent then only the clicks not farther away
than three pixels from the border are ignored. If you press anywhere else inside polygon, then it can be moved and rotated
in a standard way.
The strip nodes over each border of polygon must be declared transparent only if this border is neither resizable nor
rotatable. The same rule is applied to strip nodes on both borders independently, so here is the second part of the
MoveNode() method.
public override void DefineCover ()
{
… …
if (!bResize_Inner && !bRotate_InnerBorder)
{
for (int i = 0; i < nVerticesIn; i++)
{
nodes [i] .SetBehaviourCursor (Behaviour .Transparent,
Cursors .Default);
}
}
if (!bResize_Outer && !bRotate_OuterBorder)
{
j0 = nVerticesIn;
for (int i = 0; i < nVerticesOut; i++)
{
nodes [j0 + i] .SetBehaviourCursor (Behaviour .Transparent,
Cursors .Default);
}
}
cover = new Cover (nodes);
}
I would call it the third type of using transparent nodes. The first was to “burn out” some holes. The second was to “cut
out” some part. In both cases the transparent nodes cover a significant part of object and are useful especially because of
their big enough size. In the third case the transparent nodes are useful because of their small size which is insignificant in
comparison with the object area. There is one more difference with the first two cases. In those two cases, the use of
transparent nodes allows to simplify the cover design. In the third case, the use of transparent nodes allows to simplify the
MoveNode() method because the node number is fixed.
Polygons of the RegPoly_RegHole_RotatableBorders class has a restriction on minimal distance between two
borders (minWidth = 12), but there can be different views on using this restriction. Consider the case of the cyan
polygon from figure 8.12; both borders of this polygon have the same number of vertices. Rotate one border in such way
that vertices of both borders will stay on the same radii and then start moving one border closer to another. If the minimum
allowed width is looked at as the minimum allowed width for current angles of vertices, then two borders can be moved
really close to each other, but no rotation of borders will be allowed after such movement. In the current version of these
polygons another view on minimum allowed width is realized: borders resizing is allowed only until the situation in which
rotation of borders is still possible. This means that minimum allowed width is in reality the minimum allowed difference
between the radius for middle points of outer segments and the radius of inner vertices.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
World of Movable Objects 186 (978) Chapter 8 Transparent nodes
{
double radiusNew;
double distanceMouse = Auxi_Geometry .Distance (m_center, ptM);
if (iNode < nVerticesIn) // inner resizing
{
if (bResize_Inner)
{
radiusNew = distanceMouse * scaling_Inner;
if (minRadius_Inner <= radiusNew &&
radiusNew <=
rOuter * Math .Cos (Math .PI / nVerticesOut) - minWidth)
{
rInner = Convert .ToSingle (radiusNew);
DefineCover ();
}
}
}
else if (iNode < nVerticesIn + nVerticesOut) // outer resizing
{
if (bResize_Outer)
{
radiusNew = distanceMouse * scaling_Outer;
if (radiusNew >=
(rInner + minWidth) / Math .Cos (Math .PI / nVerticesOut))
{
rOuter = Convert .ToSingle (radiusNew);
DefineCover ();
}
}
}
… …
Each rotation starts with calculation of the compensation angle. For rotation of regular polygon, it is enough to calculate
one compensation angle, but our polygons have three different types of rotation – only inner border, only outer border, or
the whole polygon – so there are three cases to calculate the needed compensation. Selection of the needed case depends on
the node which is pressed to start rotation.
public void StartRotation (Point ptMouse, int iNode)
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
if (iNode < nVerticesIn)
{
compensation_Inner = Auxi_Common .LimitedRadian (angleMouse - angleIn);
}
else if (iNode < nVerticesIn + nVerticesOut)
{
compensation_Outer = Auxi_Common.LimitedRadian (angleMouse - angleOut);
}
else
{
compensation_Inner = Auxi_Common.LimitedRadian (angleMouse - angleIn);
compensation_Outer = Auxi_Common.LimitedRadian (angleMouse - angleOut);
}
}
Certainly, all three cases of rotation are described in the MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
World of Movable Objects 187 (978) Chapter 8 Transparent nodes
{
… …
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptM);
if (iNode < nVerticesIn) // inner border rotation
{
if (bRotate_InnerBorder)
{
AngleIn = angleMouse - compensation_Inner;
bRet = true;
}
}
else if (iNode < nVerticesIn + nVerticesOut) // outer border rotation
{
if (bRotate_OuterBorder)
{
AngleOut = angleMouse - compensation_Outer;
bRet = true;
}
}
else if (iNode == nVerticesIn + nVerticesOut + 1)
{
if (bRotate_Figure)
{
SetAngles (angleMouse - compensation_Outer,
angleMouse - compensation_Inner);
bRet = true;
}
}
}
return (bRet);
}
All possibilities of resizing and rotation for each polygon individually are
regulated via the commands of context menu. Figure 8.13 shows the view of
context menu for green polygon from figure 8.12.
The last command in menu allows to regulate the appearance of small rectangle Fig.8.13 Menu on polygon
with information about the current angle of rotated border. In the example
Form_Text_Rotatable_Class.cs similar informative rectangle was already demonstrated but in that example it was
organized as a Label object. In the current example it is organized as a graphical object of the Text_Horizontal
class.
Text_Horizontal txtAngle = null;
This special object with information is used only throughout the border rotation, so it is initialized when any strip node is
pressed with the right button; information appears slightly to the right of the cursor.
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 RegPoly_RegHole_RotatableBorders)
{
RegPoly_RegHole_RotatableBorders poly =
grobj as RegPoly_RegHole_RotatableBorders;
if (e .Button == MouseButtons .Left)
{
… …
World of Movable Objects 188 (978) Chapter 8 Transparent nodes
}
else if (e .Button == MouseButtons .Right)
{
Identification (grobj .ID);
if (mover .CaughtNodeShape == NodeShape .Strip && bShowAngle)
{
tmpH = Auxi_Geometry .RoundMeasureString (this, "Anything",
info .Font) .Height / 2;
txtAngle = new Text_Horizontal (this,
new Point (e .X + 12, e .Y - tmpH / 2),
poly .AngleToShow (mover .CaughtNode), info .Font);
}
… …
When you move any object around the screen, you usually look at this object, so throughout the rotation this information
with the current angle of rotated border is shown slightly to the right from the cursor.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
if (mover .CaughtSource is RegPoly_RegHole_RotatableBorders &&
bShowAngle &&
txtAngle != null)
{
txtAngle .Text =
polygons [iPolyTouched] .AngleToShow (mover .CaughtNode);
txtAngle .Location = new Point (e .X + 12, e .Y - tmpH / 2);
}
Invalidate ();
}
}
In many other examples, objects of the Text_Horizontal class are used as normal movable objects and are included
into mover queue. This special object is never moved by itself; it only appears for the period of another object rotation, so
there is no sense to include it into mover queue. But this information must be seen regardless of the level at which you
rotate the polygon border, so this small rectangle with information must be shown on top of all other graphical objects.
Thus, it is the last graphical object to be painted.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (bShowAngle && txtAngle != null)
{
txtAngle .Draw (grfx);
}
}
File: Form_ConvexPoly_RegHole_RotatableBorders.cs
Menu position:
Graphical objects – Basic elements – Polygons (perforated) – Convex with regular hole; different rotations
30 pages back I already demonstrated convex polygons with holes in the shape of regular polygons. Polygons in this new
example are designed in the same way, so in the new class there is the same set of fields for geometry.
public class ConvexPoly_RegHole_RotatableBorders : GraphicalObject
{
PointF m_center;
PointF [] ptsOut;
float radiusVertices_Inner;
int nVertices_Inner;
double angle_Inner;
World of Movable Objects 189 (978) Chapter 8 Transparent nodes
SolidBrush brush;
Pen penBorder_AllChanges, penBorder_ResizableOnly, penBorder_RotatableOnly;
bool bResize_Outer;
bool bResize_Inner;
bool bRotate_Figure;
bool bRotate_OuterBorder;
bool bRotate_InnerBorder;
bool bReconfigure;
New polygons use the same wide pens as in the previous example to inform about the possibility of resizing and rotation.
They also use the same set of Boolean variables to regulate all movements; there is one extra Boolean variable to regulate
the possibility of reconfiguring.
The new class uses the same size restrictions as in the previous example but there is the principal difference in interpretation
of the limit on minimal width. The difference is perfectly demonstrated at figure 8.14 with the cyan polygon. It is obvious
that that the inner border cannot be turned around because it will try to cross the outer border. In the previous example such
situation is not allowed. In the ConvexPoly_RegHole_RotatableBorders class, the possibility of resizing is
checked only for the
currently existing angles
for all vertices and the
possibility of later rotation
is not taken into
consideration. Inner
border can expand and will
be stopped only on the
minimum allowed distance
from outer border
(minWidth = 10).
When later you try to rotate
inner border, it will rotate
only until the same
restriction allows. For the
case of cyan polygon from
figure 8.14, its inner
border can be slightly
rotated counterclockwise
but not farther on.
Checking the possibility of
any movement is done in
exactly the same way as in Fig.8.14 From the previous example these polygons differ by the type of outer border:
the older example of instead of regular polygon it is convex.
convex polygons with
regular polygonal holes; only in the new class in each case there is an additional checking of the variable which regulates
this particular movement.
Menu on polygons in this example (figure 8.15) is similar to the menu from the
previous example with only two changes: there is an extra command to regulate
reconfiguration, but there is no command for showing rotation angle because the
additional rectangle with such information is not used in this example.
There is an interesting (or curious?) situation with regulating the outer border
movement. Suppose that you switched OFF the resizability of outer border. You
cannot scale this border any more but you can achieve the same result by moving its
vertices one after another if the reconfiguration is allowed. I think that this
confusing situation is caused by not absolutely correct formulation of commands.
Maybe scalable instead of resizable will be more correct, but I decided not to Fig.8.15 Menu on polygons
change anything and simply to inform about the possibility of the back way when
the outer border is declared non-resizable.
World of Movable Objects 190 (978) Chapter 8 Transparent nodes
because from time to time I think about adding more plug variants into this example. I am not sure why I did not do it up
till now, but there is a chance that I’ll do it one day.
All plugs are resizable by borders. Forward moving, resizing, and rotation of regular polygons and circles was already
discussed in the previous chapters. For resizing, polygons have narrow strip nodes over border segments, while circle
border is covered by a set of overlapping small circular nodes. In each case the last node in the cover is the big node which
covers the whole area of an object up till the border. Depending on the shape of an object, it is either circular node or
polygonal node.
public override void DefineCover ()
{
CoverNode [] nodes;
if (shape == Shape .Circle)
{
nodes = new CoverNode [nNodesOnCircle + 1];
for (int i = 0; i < nNodesOnCircle; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (center,
2 * Math .PI * i / nNodesOnCircle, radius), nrSmall);
}
nodes [nNodesOnCircle] = new CoverNode (nNodesOnCircle, center, radius,
Cursors .SizeAll);
}
else
{
// first the strips along border segments; the last node is polygon
nodes = new CoverNode [nVertices + 1];
PointF [] pts = Vertices;
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i], pts [(i+1) % nVertices], 5);
}
nodes [nVertices] = new CoverNode (nVertices, pts);
}
cover = new Cover (nodes);
}
In the middle of figure 8.16 you can see three big colored rectangles with a lot of holes in each of them; rectangles belong
to the AreaWithHoles class, while each hole is an element of the Hole class. Elements of the Hole class are definitely
not movable by themselves; the Hole class is only a convenient way to describe each hole in the big rectangular areas.
Holes can take the same shape as Plug objects, so not surprisingly at all that fields in two classes are nearly identical.
public class Hole
{
Shape shape;
PointF center;
float radius;
int nVertices;
double angle;
Rectangular areas of the AreaWithHoles class vary in sizes and they have different number of holes which are lined in
rows and columns.
public class AreaWithHoles : GraphicalObject
{
RectangleF rc;
int nRow, nCol;
List<Hole> holes = new List<Hole> ();
SolidBrush brush;
float spaceAroundHole = 10;
The number of rows and columns is determined at the moment of initialization; the shape of each hole, the number of
vertices for each polygonal hole, and the angle for each polygonal hole are set at random.
World of Movable Objects 192 (978) Chapter 8 Transparent nodes
public AreaWithHoles (RectangleF rect, int rows, int columns, Color clr)
{
nRow = Math .Max (rows, 1);
nCol = Math .Max (columns, 1);
rc = new RectangleF (rect .Left, rect .Top,
Math .Max (rect .Width,
nCol * 2 * (spaceAroundHole + Hole .MinimumRadius)),
Math .Max (rect .Height,
nRow * 2 * (spaceAroundHole + Hole .MinimumRadius)));
DefineHoles ();
brush = new SolidBrush (clr);
}
private void DefineHoles ()
{
int maxVerticesInHole = 9;
int nSeed, nVertices, iRow, jCol;
Random rand = Auxi_Common .RandomByCurTime (out nSeed);
float dx = rc .Width / nCol;
float dy = rc .Height / nRow;
float radius = Math .Min (dx, dy) / 2 - spaceAroundHole;
for (int i = 0; i < nRow * nCol; i++)
{
iRow = i / nCol;
jCol = i % nCol;
PointF pt = new PointF (rc .Left + dx / 2 + jCol * dx,
rc .Top + dy / 2 + iRow * dy);
nVertices = rand .Next () % (maxVerticesInHole - 1);
if (nVertices == 0)
{
holes .Add (new Hole (pt, radius));
}
else
{
holes.Add (new Hole (pt, radius, nVertices + 2, rand.Next (120)));
}
}
}
Each AreaWithHoles object has a simple but remarkable cover: all nodes except the last one are transparent. These
transparent nodes cover the holes. There is one transparent node per each hole; area of each transparent node is equal to the
area of the hole it covers. There is a single non-transparent node in the cover; it is a big rectangular node for the whole area.
This node provides the moving of area by any inner point not belonging to any hole.
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);
}
World of Movable Objects 193 (978) Chapter 8 Transparent nodes
When an area with holes is moved around the screen, the only basic point of this area (its top left corner) and the basic
points of all its holes must be changed synchronously.
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;
}
}
Rectangles with a lot of holes look interesting, but their MoveNode() method is extremely simple. There is a single non-
transparent node in the cover; this node is used for moving, so there must be a call for Move() method from inside the
MoveNode() method and nothing else.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
bRet = true;
}
return (bRet);
}
Thus, we have simple objects of the Plug and AreaWithHoles classes. Their team-work produces interesting results,
but before looking into the details of their work I would like to write several words about one more class involved in this
work. Closer to the left side of figure 8.16 you can see the Plugs group with eight buttons inside. Each button can be
moved and resized individually by its border. On any movement of the inner elements the group adjusts the frame, so it is
always shown around all the buttons. The group can be moved by any inner point (not too close to any button) and by all
the points of its frame. The title of the group can be moved left and right between the borders. Group belongs to the
ElasticGroup class; this class is discussed in details in the chapter Groups. Group of this class already appeared in one
of the previous examples (Form_NnodeCovers.cs, figure 7.1) and in that older example only the font for inner elements
and title could be changed. In the current Form_FillTheHoles.cs example, the right click inside the group allows to open
the standard tuning form for any ElasticGroup object. In this tuning form you can change all the visibility parameters
of the group. This tuning form is provided together with the ElasticGroup class and they are both discussed in details
further on.
Now it is time to look at the work of the Form_FillTheHoles.cs. The idea of this example-game came from the real game
for the kids of two, three, or four years old. There are sets of plastic forms with holes and plugs; kids try to close each hole
by the plug of the appropriate shape. I strongly oppose the use of computers by such small kids, so do not look at this
simple game as the substitution for the real game. In the real game which I remember all details were made of plastic so all
their sizes were fixed. For the screen analogue I made the plugs scalable, so there is one more parameter by which you have
to fit the plugs to the holes.
When you first start the Form_FillTheHoles.cs, there are three different areas with holes, just as shown at figure 8.16, but
there are no plugs in view.
private void DefaultView ()
{
… …
plugs .Clear ();
areas .Clear ();
areas .Add (new AreaWithHoles (new RectangleF (200, 100, 420, 420), 3, 3,
Color .Green));
areas .Add (new AreaWithHoles (new RectangleF (250, 150, 560, 420), 3, 4,
Color .Blue));
World of Movable Objects 194 (978) Chapter 8 Transparent nodes
areas .Add (new AreaWithHoles (new RectangleF (300, 50, 420, 560), 4, 3,
Color .Red));
… …
}
New plug appears on the screen when any button inside the group is clicked. Shape of the new plug is determined by the
pressed button while the color is set at random. New plugs appear on the line not far from the upper side of the form.
private void MouseUp_btnFigure (object sender, MouseEventArgs e)
{
Button btn = sender as Button;
btn .Invalidate ();
float radius = 50;
PointF pt = new PointF (100 + rand .Next (600), radius + 10);
int nVertices = Convert .ToInt32 ((string) btn .Tag);
Plug plug;
if (nVertices == 0)
{
plug = new Plug (pt, radius,
Auxi_Colours .ColorPredefined (rand .Next (7)));
}
else
{
plug = new Plug (pt, radius, nVertices, 0,
Auxi_Colours .ColorPredefined (rand .Next (7)));
}
plugs .Insert (0, plug);
RenewMover ();
}
Number of areas is regulated by the commands of two context menus. One menu can
be called on any area (figure 8.17). Two last commands of this menu allow either to
delete the pressed area or all areas except the pressed one. At least one area must be
always on the screen, so both commands are available only when there are two or more
areas.
The same menu includes commands to change the order of areas on the screen. The
pressed area can be moved one level up or down; it can be also moved on top of all
areas or below all of them. These are four standard commands to change the order of Fig.8.17 Menu for the colored
objects on the screen and you will see the same commands in many examples. areas
To add a new area, you need to call context menu at any empty place (figure 8.18). For the new
area, the number of rows and columns is determined at random from the [2, 4] interval and then
the hole shapes are determined at random as was explained earlier.
private void Click_miAddArea (object sender, EventArgs e) Fig.8.18 Menu at
{ empty places
int nRows = RandomInteger (3) + 2; // {0, 1, 2} + 2
int nCols = RandomInteger (3) + 2; // {0, 1, 2} + 2
int cx = 150 + 50 * (nCols - 2);
int cy = 30 + 50 * (nRows - 2);
int cell = 140;
areas .Insert (0, new AreaWithHoles (
new RectangleF (cx, cy, cell * nCols, cell * nRows), nRows,
nCols, Auxi_Colours .ColorPredefined (RandomInteger (8))));
RenewMover ();
}
Plugs and colored areas are organized into two special Lists.
List<AreaWithHoles> areas = new List<AreaWithHoles> ();
List<Plug> plugs = new List<Plug> ();
World of Movable Objects 195 (978) Chapter 8 Transparent nodes
All movable objects must be registered in the mover queue; if there is any change in the number or order of such objects,
then mover queue must be renewed. Controls have to take the leading positions in this queue; if there are some objects
which consist both of controls and graphical objects (ElasticGroup is of such a type), then such objects must take
position between controls and pure graphical objects. These are standard rules which are based on a special status of
controls which are always shown atop all graphical objects. The order of graphical objects depends on the idea of each
particular task. I want plugs to move atop all the areas as this will be much better for viewing their fit with the holes. Also I
want information to be shown above all other graphical objects (areas and plugs). Thus, we receive such order of objects in
the mover queue: single button associated with the information area, group, information, plugs, and areas.
public void RenewMover ()
{
mover .Clear ();
groupButtons .IntoMover (mover, 0);
mover .Insert (0, scHelp);
info .IntoMover (mover, mover .Count);
for (int i = 0; i < plugs .Count; i++)
{
mover .Add (plugs [i]);
}
for (int i = 0; i < areas .Count; i++)
{
mover .Add (areas [i]);
}
if (bAfterInit)
{
Invalidate ();
}
}
The main task in this example is to grab the needed plug and then move, rotate, and resize this plug in such a way as to
close some hole of the same shape. Looks like everything interesting must take place in the OnMouseDown() and
OnMouseUp() methods.
Though there are objects of different classes, only the plugs are mentioned in the OnMouseDown() method. Two methods
of the Plug class must be called if any plug is caught.
• If any strip node is caught, it can be only the border of a polygonal plug and then the Plug.StartScaling()
method must be called.
• If a plug is caught by the Right button, then the Plug.StartRotation() method must be called.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (mover .CaughtSource is Plug)
{
mover .Clipping = Clipping .Unsafe;
Plug plug = mover .CaughtSource as Plug;
if (plug .Shape == Shape .Polygon)
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNodeShape == NodeShape .Strip)
{
plug .StartScaling (e .Location);
}
}
else if (e .Button == MouseButtons .Right)
{
plug .StartRotation (e .Location);
World of Movable Objects 196 (978) Chapter 8 Transparent nodes
}
}
}
}
ContextMenuStrip = null;
}
Scaling of regular polygons was already explained in the Form_RegularPolygon_CoverVariants.cs (figure 6.1);
calculation of a single compensation angle was also explained in several previous examples, so the use of
StartScaling() and StartRotation() methods do not need any explanation. But in the code of the
OnMouseDown() method you can see a very rare change of the mover clipping level and this needs some explanation.
The Mover.Clipping property was never mentioned in the previous examples and there are only few places in the
whole Demo application where you can see the use of this property.
When any mover is initialized, its clipping level is set by default to Visual and then all objects which are supervised by
this mover (all objects which are registered in its queue) can be moved only in view. I mentioned the mover clipping levels
in the very first chapter of the book in the section Safe and unsafe moving and resizing. It was even before any movement
of objects was demonstrated; now I return to this explanation with an example of object which requires the change of the
mover clipping level.
When the Clipping.Visual level is used, and this is a default level for any mover, it does not mean that any object is
always fully in view. The clipping is organized not for objects but for the mouse when it has grabbed an object. Under
such clipping, the mouse with the caught object cannot go outside the visible part of the form, so this guarantees that an
object cannot be entirely moved beyond the borders. Any part of the caught object can cross the border; it can be a small
part or nearly a whole object which goes out of view, but the mouse which is stuck to some point inside an object, cannot
cross the border, so, regardless of where the movement ends, some part of an object is still in view. By pressing this part,
though it can be very small, an object can be returned back into full view.
The Clipping.Unsafe level allows mover to take the grabbed object across any border of the form. If an object is
moved across upper or left border and dropped there, there are no chances to return it back into view. Theoretically it is
possible, because you can move the mouse into the same place across the border, press the button without seeing any object,
and move it back into view if you caught the object, but it is harder than finding a black cat in the dark room. If an object is
dropped somewhere across the right or bottom border, then chances to return it back are higher because you can enlarge the
form and maybe see an object. All depends on how far you moved it before. In any case, if you move an object across the
border, it is usually a sign that you want to get rid of it and this movement is an easier way to delete an object than even
using the command Delete from menu. Such throwing of objects overboard works better with the small objects, so I
decided to demonstrate it with the plugs in this example.
• When the Plug object is caught, then the mover clipping level is changed to Clipping.Unsafe; this is shown
in the code above.
• When the Plug object is released, then the first thing to do is to check the possibility of its disappearance.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
int iWasObject, iWasNode;
if (mover .Release (out iWasObject, out iWasNode))
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is Plug)
{
if (!PlugDisappeared (id))
{
… …
World of Movable Objects 197 (978) Chapter 8 Transparent nodes
• If the plug is really dropped somewhere outside the visible area of the form, then it is a garbage; this garbage is
deleted from the List of plugs, and the mover queue must be renewed. All these things are done by the
PlugDisappeared() method.
private bool PlugDisappeared (long id)
{
Rectangle rcClient = ClientRectangle;
bool bRet = false;
for (int i = 0; i < plugs .Count; i++)
{
if (plugs [i] .ID == id)
{
Rectangle rcPlug = Rectangle .Round (plugs [i] .RectAround);
if (Rectangle .Empty == Rectangle.Intersect (rcPlug, rcClient))
{
plugs .RemoveAt (i);
RenewMover ();
bRet = true;
}
break;
}
}
return (bRet);
}
• The initial Clipping.Visual level must be restored. I do not bother about whether it was the special case with
some plug or maybe some other object is released; in any case the clipping level is set whenever the mouse is
released. Restoring the normal clipping level for the mover is the last line inside the OnMouseUp() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
mover .Clipping = Clipping .Visual;
}
It was interesting but this is not the main thing in the Form_FillTheHoles.cs. The main task is fitting the plugs with the
holes. Let us look how it is done. Both areas and plugs are movable, so it is possible to move an area in such position that
some plug will fit with the hole underneath, but the probability of such fit is so low that I do not consider this case. More
natural situation is when you move some plug, resize it, and rotate it in order to fit it with some hole. For circles rotation
does not matter but for all regular polygons it is needed with very high probability. As I have to check the possibility of
match between hole and plug at the moment when this plug is released, then everything interesting for this case must be
inside the OnMouseUp() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is Plug)
{
if (!PlugDisappeared (id))
{
if (fDist <= 3)
{
PopupPlug (id);
}
World of Movable Objects 198 (978) Chapter 8 Transparent nodes
CheckMatch ((grobj as Plug));
}
}
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is Plug)
{
CheckMatch ((grobj as Plug));
}
… …
Whether this plug fits the hole is checked by the CheckMatch() method. There are several levels of checking inside this
method. Though the Hole and Plug objects have identical set of parameters, I do not believe in absolutely ideal
positioning of the plugs, so for each checking by central point, radius, or angle there is some accuracy (or difference) which
is allowed.
private void CheckMatch (Plug plug)
{
long idPlug = plug .ID;
for (int iArea = 0; iArea < areas .Count; iArea++)
{
AreaWithHoles area = areas [iArea];
if (area .Bounds .Contains (plug .RectAround))
{
for (int iHole = 0; iHole < area .Holes .Count; iHole++)
{
Hole hole = area .Holes [iHole];
if (!HoleBlocked (iArea, iHole))
{
if (Auxi_Geometry .Distance (plug .Center, hole .Center) <=
fAccuracy_Center)
{
if (Math .Abs (plug .Radius - hole .Radius) <=
fAccuracy_Radius &&
plug .VerticesNumber == hole .VerticesNumber)
{
… …
There is one special aspect of angles comparison for regular polygons. Visually there is no difference between vertices, so
the difference of angles multiple to (360 / nVertices) degrees must be ignored.
If there is a match between the plug and the hole, then:
• Plug is removed from general List of plugs.
• Hole is removed from the List of holes in the area. If it was the last hole in the area, then the new set of holes is
organized for this area.
private void CheckMatch (Plug plug)
{
… …
if (bClose)
{
for (int j = 0; j < plugs .Count; j++)
{
if (plugs [j] .ID == idPlug)
{
plugs .RemoveAt (j);
break;
}
}
area .Holes .RemoveAt (iHole);
World of Movable Objects 199 (978) Chapter 8 Transparent nodes
if (area .Holes .Count == 0)
{
area .RenewHoles ();
}
area .DefineCover ();
RenewMover ();
}
… …
With the examples in the book becoming more and more complex, we are moving in the direction of real user-driven
applications.
• If any object needs some change of parameters, then it is usually organized via the commands of menu on this
particular object (figure 8.17).
• General commands for the whole example are included into menu which can be called at any empty place
(figure 8.18). Among other commands, there is often a possibility to restore the default view of the form.
• No changes are lost, so on closing an example its view is saved; I prefer to save it in Registry. When the same
example starts again, the same view is restored from Registry.
Sliding partitions
Several examples in the previous chapters demonstrate the multicolor objects. For example, each sector of circles and rings
in the Form_NnodeCovers.cs (figure 7.1) has its own color. Those objects of the Circle_Nnodes and
Ring_Nnodes classes are resizable by borders, but the ratio between their sectors is fixed at the moment of design and
does not change throughout the time of their life. In this section I show objects of simple shape – rectangles, circles, and
rings – which, in addition to the expected resizing by their borders, have movable partitions between their inner parts.
The very first example of rectangles in this book (Form_Rectangles_Standard.cs, figure 3.1) demonstrates the easiest way
for users to move and resize any rectangle: there are circular nodes on the corners, thin rectangular nodes on the borders,
and the big rectangular node covering the whole object. In the Rectangle_SlidingPartitions class the same idea
is used, but in addition to strip nodes on the borders similar strip nodes are used on inner partitions. The partitions can be
horizontal or vertical (figure 9.1); the reaction on moving any border depends on whether this border is parallel to partitions
or orthogonal (I’ll write about this difference slightly further), but the order of nodes is always the same.
• Four circular nodes on corners in such order: top left corner, top right corner, bottom right corner, and bottom left
corner. Radius of these nodes is slightly enlarged from the default value; this makes the resizing by corners even
easier.
• Four strip nodes on the sides; the order is left,
right, top, and bottom.
• Strip nodes on partitions; vertical partitions are
numbered from left to right, while horizontal are
numbered from top to bottom.
• Big rectangular node for the whole area.
Rectangle_SlidingPartitions object together
with its cover is shown at figure 9.2. You can see from the
code of the DefineCover() method that first eight nodes
and the last one are organized in similar way for all
rectangles, while other nodes depend on the direction of
partitions. Also two nodes at figure 9.2 are marked for the
case when there are N nodes in the cover.
public override void DefineCover ()
{
float radiusCorner = 5;
float cxL = rc .Left; Fig.9.2 Rectangle and its cover
float cxR = rc .Right;
float cyT = rc .Top;
float cyB = rc .Bottom;
CoverNode [] nodes = new CoverNode [4 + 4 + (sizes .Length - 1) + 1];
nodes [0] = new CoverNode (0, new PointF (cxL, cyT), radiusCorner,
Cursors .SizeNWSE); // LT corner
nodes [1] = new CoverNode (1, new PointF (cxR, cyT), radiusCorner,
Cursors .SizeNESW); // RT corner
nodes [2] = new CoverNode (2, new PointF (cxR, cyB), radiusCorner,
Cursors .SizeNWSE); // RB corner
nodes [3] = new CoverNode (3, new PointF (cxL, cyB), radiusCorner,
Cursors .SizeNESW); // LB corner
nodes [4] = new CoverNode (4, new PointF (cxL, cyT), new PointF (cxL, cyB),
Cursors .SizeWE); // left side
nodes [5] = new CoverNode (5, new PointF (cxR, cyT), new PointF (cxR, cyB),
Cursors .SizeWE); // right side
nodes [6] = new CoverNode (6, new PointF (cxL, cyT), new PointF (cxR, cyT),
Cursors .SizeNS); // top side
nodes [7] = new CoverNode (7, new PointF (cxL, cyB), new PointF (cxR, cyB),
Cursors .SizeNS); // bottom side
int k = 8;
if (dirAcrossPartitions == LineDir .Hor)
{
float cx = rc .Left;
for (int i = 0; i < sizes .Length - 1; i++)
{
cx += sizes [i];
nodes [k + i] = new CoverNode (k + i, new PointF (cx, cyT),
new PointF (cx, cyB), Cursors .SizeWE);
}
World of Movable Objects 202 (978) Chapter 9 Intermediate summary on graphical primitives
}
else
{
float cy = rc .Top;
for (int i = 0; i < sizes .Length - 1; i++)
{
cy += sizes [i];
nodes [k + i] = new CoverNode (k + i, new PointF (cxL, cy),
new PointF (cxR, cy), Cursors .SizeNS);
}
}
k = nodes .Length - 1;
nodes [k] = new CoverNode (k, rc);
cover = new Cover (nodes);
}
Nodes at figure 9.2 are numbered and this makes the explanation of the MoveNode() method easier. I am going to
explain this method for the case of vertical partitions.
The easiest is the situation with the last node in the cover which is responsible for forward movement of the whole object; in
this case simply the Move() method is called.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == cover .NodesCount - 1)
{
Move (dx, dy);
}
… …
In all other cases, there is some resizing; of those cases the easiest are the movements of upper and lower borders. Only one
restriction is considered; it prevents the disappearance of rectangle after squeezing.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else
{
int jPartition;
float sizePrevNew, sizeNextNew;
if (dirAcrossPartitions == LineDir .Hor)
{
float widthNew;
switch (i)
{
… …
case 6: // top
if (rc .Height - dy >= minAlongPartitions)
{
rc .Y += dy;
rc .Height -= dy;
bRet = true;
}
break;
World of Movable Objects 203 (978) Chapter 9 Intermediate summary on graphical primitives
case 7: // bottom
if (rc .Height + dy >= minAlongPartitions)
{
rc .Height += dy;
bRet = true;
}
break;
… …
When two other sides are moved, then not only the size of the big area is changed, but also the sizes of all sectors are
changed as the ratio between the sectors must be unchanged. There is a limit on minimal size of any sector, so this
proposed change of the big area is checked by the possible size change of the smallest sector.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
case 4: // left side
widthNew = rc .Width - dx;
if (widthNew * fPart [iMinSegment] >= minBetweenPartitions)
{
rc .X += dx;
rc .Width -= dx;
SizesFromRatio (rc .Width);
bRet = true;
}
break;
… …
The ratio between sectors is known; if the change of the area is allowed, then sizes of all sectors are changed by the
SizesFromRatio() method according to the coefficients of the fPart[] array.
public void SizesFromRatio (float sum)
{
for (int i = 0; i < sizes .Length; i++)
{
sizes [i] = Convert .ToSingle (sum * fPart [i]);
}
}
When any corner node is moved, it is simply a combination of results from moving two neighbouring sides. Here is the
code for the top left corner; you can see the same code in the cases of left and top sides.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
case 0: // LT corner
widthNew = rc .Width - dx;
if (widthNew * fPart [iMinSegment] >= minBetweenPartitions)
{
rc .X += dx;
rc .Width -= dx;
SizesFromRatio (rc .Width);
bRet = true;
}
if (rc .Height - dy >= minAlongPartitions)
{
rc .Y += dy;
rc .Height -= dy;
bRet = true;
}
break;
… …
World of Movable Objects 204 (978) Chapter 9 Intermediate summary on graphical primitives
When any partition is moved, then only the sizes of two segments on the sides of this partition are changed; both sizes are
checked against the minimal allowed size of any segment. There are always eight nodes in the cover ahead of the nodes
over partitions, so the number of the pressed partition is easily obtained from the number of the pressed node.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
default:
jPartition = iNode - 8;
sizePrevNew = sizes [jPartition] + dx;
sizeNextNew = sizes [jPartition + 1] - dx;
if (sizePrevNew >= minBetweenPartitions &&
sizeNextNew >= minBetweenPartitions)
{
sizes [jPartition] += dx;
sizes [jPartition + 1] -= dx;
bRet = true;
}
break;
… …
MoveNode() method describes the process of moving when some node is already caught. It is a standard procedure that
at the starting moment of any movement when some node is just pressed with a mouse, some values must be calculated;
later these values are used to regulate the possibility of resizing. This initial calculation often depends on the number or the
shape of the pressed node. This preliminary calculation can be required not for all nodes but only for some of them. It is
possible to check the need of calculation in the OnMouseDown() method of the form and start the calculations if they are
needed. Another possibility is to call the appropriate method if an object of some class is pressed and then make the
decision inside that special method for calculations. In the Form_RectanglesSlidingPartitions.cs everything works by the
second variant.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left &&
mover .CaughtSource is Rectangle_SlidingPartitions)
{
(mover .CaughtSource as Rectangle_SlidingPartitions)
.StartResizing (ptMouse_Down, mover .CaughtNode);
}
}
}
For the Rectangle_SlidingPartitions class, different types of preliminary calculations are needed for nodes on
borders and on partitions; nothing at all is needed for the last big rectangular node, so it is excluded.
public void StartResizing (Point ptMouse, int iNode)
{
if (iNode < 8) // calculate distribution
{
RatioFromSizes ();
Auxi_Common .MinValue (fPart, false, out iMinSegment);
// iMinSegment - segment with minimal size between partitions
}
else if (iNode < cover .NodesCount - 1)
{
// calculate coordinates of two neighbours
int jPartition = iNode - 8;
float shiftToPrevious = 0;
for (int i = 0; i < jPartition; i++)
World of Movable Objects 205 (978) Chapter 9 Intermediate summary on graphical primitives
{
shiftToPrevious += sizes [i];
}
coorPrevious = (dirAcrossPartitions == LineDir .Hor) ?
(rc .Left + shiftToPrevious) : (rc .Top + shiftToPrevious);
coorNext = coorPrevious + sizes [jPartition] + sizes [jPartition + 1];
}
}
• There are eight nodes on borders – four on corners and four on sides – and for nearly all of them the ratio between
the sizes of sectors must be calculated before the resizing starts. This is not needed only if one of the sides
orthogonal to partitions is pressed. Depending on the direction of partitions, these nodes have different numbers.
It is not a problem at all to check the need for such calculation in each particular case, but I skipped this checking.
There are two nodes of eight for which calculation of ratio is not needed, code is much simpler without additional
checking, while the calculations are easy.
public void RatioFromSizes ()
{
double sizesSum = (dirAcrossPartitions == LineDir .Hor) ? rc .Width
: rc .Height;
for (int i = 0; i < sizes .Length; i++)
{
fPart [i] = sizes [i] / sizesSum;
}
}
• When any partition is moved, sizes of sectors on two sides of this line can be changed, so the changing sizes must
be checked all the time against the minimum allowed size of any sector. To do this, the positions for opposite sides
of those two sectors are calculated.
These are nearly all interesting things about the rectangles with sliding partitions. Looking at figures of the same rectangle
with and without its cover (figure 9.2), I would like to add one more thing. There is one very narrow sector approximately
in the middle of rectangle. It is barely seen at the upper figure because it is so narrow and the neighbouring colors do not
differ too much, but at the lower figure it is easily detected by two overlapping strip nodes. By default, the strip nodes are
six pixels wide; with the minimal allowed size of sectors set at four pixels, the lower node is not entirely blocked by the
previous node, so the lower node is seen as two pixels wide line and can be even caught and moved aside by this line. You
can change the minimum allowed size of sectors in either way; all depends on the real tasks which can use this or similar
objects. Further on I am going to demonstrate some sliders in rectangles and on plot areas. Those sliders are similar in
design to the sliding partitions in this example and there are variants in which the sliders are allowed to be moved on top of
each other.
When you move any partition, you change the angles of two sectors on its sides. Movement of any partition changes angles
of two neighbouring sectors and there exists minimum allowed sector angle. If any movement has some limits, then those
restrictions are usually calculated at the starting moment of this movement. In the current example, the clear indication of
such moment is the catch of some strip node. In such case the
Circle_SlidingPartitions.StartResectoring() method must be called; the only parameter of this method
is the number of the caught node.
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 Circle_SlidingPartitions)
{
Circle_SlidingPartitions circle = grobj as Circle_SlidingPartitions;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNodeShape == NodeShape .Strip)
{
circle .StartResectoring (mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
circle .StartRotation (e .Location);
}
}
}
}
The StartResectoring() method looks simple, but I think that some explanations on its code are needed.
public void StartResectoring (int iNode)
{
iBorderToMove = iNode - nNodesOnCircle;
double angleCaughtBorder = Angle;
double [] sector_angle = SectorAngles ();
for (int i = 0; i < iBorderToMove; i++)
{
angleCaughtBorder += sector_angle [i];
}
if (DrawingDirection == Rotation .Clockwise)
{
iSector_Clockwise = iBorderToMove;
min_angle_Resectoring =
angleCaughtBorder + sector_angle [iSector_Clockwise];
iSector_Counterclock = (iSector_Clockwise == 0) ?
(dataCircle .SectorsNumber - 1) : (iSector_Clockwise - 1);
max_angle_Resectoring =
angleCaughtBorder - sector_angle [iSector_Counterclock];
}
else
{
iSector_Counterclock = iBorderToMove;
max_angle_Resectoring =
angleCaughtBorder + sector_angle [iSector_Counterclock];
iSector_Clockwise = (iSector_Counterclock == 0) ?
(dataCircle .SectorsNumber - 1) : (iSector_Counterclock - 1);
min_angle_Resectoring =
angleCaughtBorder - sector_angle [iSector_Clockwise];
}
World of Movable Objects 208 (978) Chapter 9 Intermediate summary on graphical primitives
two_sectors_sum_values =
Values [iSector_Clockwise] + Values [iSector_Counterclock];
}
Any Circle_SlidingPartitions object is described by some parameters which are stored in its CircleData
field. In our case of moving any partition; the most interesting data inside this field are the starting angle for the first sector
(m_angle) and the set of sector angles (sector_angles[]); they allow to calculate the starting angle of any sector.
The first thing to do is to calculate the angle for the caught partition (angleCaughtBorder). Then the range for
moving this partition is calculated, but for this calculation two things must be taken into consideration.
1. The direction of drawing the circle: it can be clockwise or counterclockwise.
2. Number of the caught partition gives numbers of sectors on its sides; then the set of angles gives initial angles of
these two sectors and the angles of neighbouring partitions. Angle increases counterclockwise, so
min_angle_Resectoring means the angle of the next partition clockwise, while
max_angle_Resectoring is the angle of the next partition counterclockwise. Our caught partition can move
only between these two neighbours, but also the sum of angles for two neighbouring sectors is not going to change.
Moving of any node is described in the MoveNode() method of the class. For moving the partition, the real allowed
range is going to be slightly less than calculated in the StartResectoring() method as this class of circles does not
allow the complete disappearance of any sector and there is minimum allowed sector angle (minSectorAngle).
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else // border between two sectors
{
… …
if (min_angle_Resectoring + minSectorAngle <= angleMouse &&
angleMouse <= max_angle_Resectoring - minSectorAngle)
{
double part_Counterclock = (max_angle_Resectoring - angleMouse)
/ (max_angle_Resectoring - min_angle_Resectoring);
if (iBorderToMove == 0)
{
Angle = angleMouse;
}
double [] vals = dataCircle .Values;
vals [iSector_Counterclock] =
two_sectors_sum_values * part_Counterclock;
vals [iSector_Clockwise] =
two_sectors_sum_values - vals [iSector_Counterclock];
dataCircle .Values = vals;
}
}
… …
As usual, the decisions about the aftermath of some mouse click or movement are made inside the OnMouseUp() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iReleased, iNode;
NodeShape shape;
if (mover .Release (out iReleased, out iNode, out shape))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
World of Movable Objects 209 (978) Chapter 9 Intermediate summary on graphical primitives
{
if (grobj is Circle_SlidingPartitions)
{
if (iNode != grobj .NodesCount - 1)
// && shape != NodeShape .Strip
{
(grobj as Circle_SlidingPartitions) .RedefineCover ();
Invalidate ();
}
else
{
if (fDist <= 3) // && shape != NodeShape .Strip
{
PopupCircle (grobj .ID);
}
}
The code of the method looks simple, but there can be variants. Two additional checkings are commented in the code but
can be turned into working pieces if you decide to use them.
1. Call of the RedefineCover() method is not needed when the circle is simply moved around, so the last node
of the cover is excluded. But the cover redefinition is also not needed when some partition between segments is
moved, so their cases can be also excluded.
2. I consider a very small distance between the points of mouse press and release as the request to bring the pressed
circle on top of others. However, the accurate positioning of partitions often requires their very small movement
and this can cause an additional but not expected change of circle order. This can be avoided by additional
checking of the node shape.
File: Form_Rings_SlidingPartitions.cs
Menu position: Graphical objects – Basic elements – Rings – Rings with sliding partitions
There are only minor differences
between classes of circles and rings
with sliding partitions and hardly any
difference at all between the forms in
which they are demonstrated
(figure 9.4). There is the same limit on
minimal sector angle, the same
calculation of the range for partition
movement. Certainly, there are two
borders, so one point of each strip node
is based on inner border and another
point – on outer border of the ring.
Set of objects
File: Form_SetOfObjects.cs
Menu position: Graphical objects – Basic elements – Set of objects in a form
Examples in several previous
chapters demonstrate objects of
different shapes and explain the
design of their covers. In the
Form_SetOfObjects.cs (figure 9.5)
you can see the same familiar shapes.
Here the objects of many different
classes are put together in order to
analyse their coexistence and to find
out, if there are going to be any
problems or something special in
dealing with an unlimited number of
elements of such variety.
There are graphical elements of 11
different classes; all of them are
derived from the abstract class
ElementOfSet. All objects can be
moved and rotated by any inner point;
all of them are resizable by any
border point; circles and rings have
the sliding partitions between their
sectors. Some objects of different Fig.9.5 Form_SetOfObjects.cs works with an unlimited number of graphical
classes might look like they belong to objects of 11 different classes.
the same class but only at the moment
of initialization; later on they can be transformed in different ways.
public abstract class ElementOfSet : GraphicalObject
{
protected Figure figure;
protected bool bResize;
protected bool bRotate;
protected double angle;
protected Pen penBorder;
protected Pen penPartition;
new public abstract RectangleF RectAround { get; }
public abstract PointF Center { get; }
public abstract Color Color { get; set; }
public abstract double Angle { get; set; }
public abstract ElementOfSet Copy (PointF pt);
public abstract void Draw (Graphics grfx);
public abstract void StartRotation (Point ptMouse);
public abstract void RedefineCover ();
By default, any new ElementOfSet object is resizable and rotatable.
public ElementOfSet ()
{
bResize = true;
bRotate = true;
angle = 0;
penBorder = new Pen (Color .DarkGray, 3);
}
In the Form_SetOfObjects.cs, a wide pen with different styles is used to draw the borders of elements and in this way to
inform about the possibility of resizing and rotation. Objects in the current example have no independent rotation of
borders, so the change of border style informs about possible actions with an element itself.
World of Movable Objects 211 (978) Chapter 9 Intermediate summary on graphical primitives
• If an element is both resizable and rotatable, then a solid wide pen is used for its border.
• If an element is resizable but not rotatable, then wide pen has the DashStyle.Dash style.
• If an element is non-resizable but rotatable, then wide pen has the DashStyle.Dot style.
• If an element is neither resizable nor rotatable, then there is no wide border.
These rules are applicable for outer borders of all elements and for hole borders of elements with holes. There are two types
of elements with sliding partitions – circles and rings. Movability of these partitions is also regulated; if the partitions are
movable then these partitions are drawn with a wide solid pen. A couple of elements at figure 9.5 demonstrate the use of
different wide pens. Ring in the top right corner is resizable but not rotatable. Next to it, there is a triangle with circular
hole; this element is not resizable but rotatable.
Switch of resizing or rotation changes the style of the used pen, so in each case the repainting is needed, but such changes of
the possible movements have different effect on the cover design. Resizing always depends on the border(s) movability
which is provided by some set of nodes over the border(s). To change the possibility of resizing, the cover of an element
must be changed, so there is a mandatory call for DefineCover() method.
public bool Resizable
{
get { return (bResize); }
set
{
bResize = value;
RedefineCover ();
BorderStyle ();
}
Switch of rotation possibility does not require any change of cover. The value of bRotatable field is checked inside the
MoveNode() method to find out the possibility of rotation, but the only reaction at the moment of rotatability change is
the change of the pen style and repainting of the form.
public bool Rotatable
{
get { return (bRotate); }
set
{
bRotate = value;
BorderStyle ();
}
}
Here are 11 classes which are derived from the ElementOfSet class and are
used in this example. Name of each class ends with the abbreviation _EOS
(Element of Set), so it is a clear indication that this class is used in the
Form_SetOfObjects.cs. I tried to include into the name of each class the
information about the shape of elements. For some classes the name is long,
though I did my best in an attempt to make it shorter and at the same time
informative. In the list below, I also mention the type of possible resizing.
Rectangle_EOS Rectangle is resizable by sides and corners.
Circle_EOS Circle is resizable by border and has movable
partitions between sectors.
Ring_EOS Ring is resizable by borders and has movable
partitions between sectors.
Strip_EOS Strip can be resized by any border point. The
curved parts of the border are used to change
the length; the straight parts of the border
allow to change the strip width.
RegularPolygon_EOS Regular polygon is resizable by any border
point.
World of Movable Objects 212 (978) Chapter 9 Intermediate summary on graphical primitives
RegPoly_IdenticalHole_EOS
This is a regular polygon with identical hole,
so the number of vertices in both borders is
the same and the angle is also the same.
Polygon is resizable by both borders.
ChatoyantPolygon_EOS N points are organized into an infinitive loop
by connecting each pair of consecutive points
by strip nodes. One more point is called
central though it can be positioned anywhere
in relation to those N points. All these N + 1
points can be moved individually around the
screen thus causing a reconfiguration.
Zooming is done by any point of those strip
nodes with a central point used as a center for
zooming; the same point is used as rotation
center.
ConvexPolygon_EOS Any vertex can be moved individually until
the convexity of the whole polygon is not
broken. There is an auxiliary point which is
called central, but it is used only for rotation
and zooming. This point is neither visualized
nor used in cover design. When the whole
polygon is moved, then this point moves
synchronously, but individual move of
vertices does not affect this special point and
it can find itself even outside the polygon
area. Pairs of consecutive vertices are
connected by strip nodes which are used for
zooming; the central point is used as the
basic point for scaling. When central point is
near the border or outside the polygon, then
zooming of such polygon may look a bit
strange. (Central point can be visualized;
there is a commented line inside the Fig.9.6 Each class is represented by a
Draw() method). pair of objects of which one is
fully resizable and another one
RegPoly_RegPolyHole_EOS
is only movable without any
Regular polygon with a regular polygonal resizing at all.
hole; the number of vertices and angle are
individual for both borders. Polygon is resizable by both borders.
RegPoly_CircularHole_EOS
Regular polygon with a circular hole is resizable by both borders.
ConvexPoly_RegPolyHole_EOS
Convex polygon with a regular polygonal hole is resizable by both borders. Vertices of the
outer border can be moved individually until the convexity of that border is not broken.
Similar objects were already demonstrated in the previous examples and their special features were discussed, but there is
one important feature which was added to all these classes in the Form_SetOfObjects.cs: their ability to be resized or
rotated can be changed at any moment individually for any object or simultaneously for all of them. There is no switch of
movability for objects in this example though it can be organized in the same way.
In the chapter Transparent nodes I have demonstrated some objects which could be switched between resizable and non-
resizable. The number of nodes in the covers of those objects depends only on geometry while the behaviour of some nodes
is switched between normal and transparent depending on the required resizability. In the summary to that chapter I
mentioned some negative aspects of such cover design and also mentioned that I prefer to use covers with different number
of nodes when there is any need of resizability changing. So in all 11 classes of elements used in the current example,
covers for resizable and non-resizable objects have different number of nodes. Here is an example of cover design for the
ConvexPolygon_EOS class. There are 2 * N + 1 nodes in the cover of resizable polygon (N is the number of vertices)
and only a single node in case of non-resizable polygon.
World of Movable Objects 213 (978) Chapter 9 Intermediate summary on graphical primitives
public override void DefineCover ()
{
CoverNode [] nodes;
if (bResize)
{
int nVertices = pts .Length;
nodes = new CoverNode [2 * nVertices + 1];
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i], 4);
}
for (int i = 0; i < nVertices; i++)
{
nodes [nVertices + i] = new CoverNode (nVertices + i, pts [i],
pts [(i + 1) % nVertices]);
}
nodes [2 * nVertices] = new CoverNode (2 * nVertices, pts);
}
else
{
nodes = new CoverNode [] { new CoverNode (0, pts) };
}
cover = new Cover (nodes);
}
It is possible to organize more variants even for such primitive elements as convex polygons. For example, inclusion into
cover of circular nodes over vertices and strip nodes over border segments can be regulated independently, so it would be
possible to have resizable polygons without possibility of reconfiguring and polygons which can be reconfigured but
without possibility of scaling. You will see such variants closer to the end of the book when in the
Form_ElementsAndGroups.cs (figure 21.68) I’ll demonstrate an example with all the features I could think out. But this
is going to happen far ahead and now let us turn to the work of this Form_SetOfObjects.cs.
When any object is caught by mover, it can be a starting point for some resizing or rotation; such possibility is decided
inside the OnMouseDown() method on the basis of the pressed button.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtSource is ElementOfSet)
{
StartResizing (e .Location);
}
}
else if (e .Button == MouseButtons .Right)
{
StartRotation (e .Location);
}
}
ContextMenuStrip = null;
}
In this piece of code, the StartResizing() is only the generic name for a whole set of methods that are slightly
different for each class of objects. For many of them it is a zooming operation which is applied to all parts of an object; for
others it can be a change in one direction only. To transform the general StartResizing() call into the specific
method of some class, first this class must be determined. Also the methods of different classes depend on some additional
parameters like the number of the pressed node or its shape; the use of those additional parameters is determined by the
design of cover for each class.
World of Movable Objects 214 (978) Chapter 9 Intermediate summary on graphical primitives
private void StartResizing (Point pt)
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is RegularPolygon_EOS)
{
(grobj as RegularPolygon_EOS) .StartScaling (pt);
}
else if (grobj is ConvexPolygon_EOS)
{
(grobj as ConvexPolygon_EOS) .StartScaling (pt, mover .CaughtNode);
}
else if (grobj is ChatPoly_EOS)
{
(grobj as ChatPoly_EOS) .StartScaling (pt, mover .CaughtNodeShape);
}
else if (grobj is Circle_EOS)
{
if (mover .CaughtNodeShape == NodeShape .Strip)
{
(grobj as Circle_EOS) .StartResectoring (mover .CaughtNode);
}
}
… …
Usually the resizing of objects of any class is started not by any node of its cover but only by some of them. The decision is
often based on the shape of the caught node; this node shape is one of the parameters that are received from mover. The
checking of the needed conditions can be done in the form or in the particular class; in the last case this parameter must be
passed into the method of the class. I do not see any advantages or disadvantages in either case; both of them work fine;
you can see the use of both ways in the code above.
The start of rotation looks more straightforward: the abstract method from the base ElementOfSet class is overridden in
each particular class and has the same parameter – point of the mouse press – for all of them.
private void StartRotation (Point pt)
{
if (mover .CaughtSource is ElementOfSet)
{
(mover .CaughtSource as ElementOfSet) .StartRotation (pt);
}
}
The OnMouseMove() method is usually very simple; there must be a single call to one of the mover methods and this
would be enough for all movements of all the classes because other details are in the MoveNode() methods of those
classes. However, you can see that for two classes there is a call to the Update() method.
private void OnMouseMove (object sender, MouseEventArgs e)
{
ptMouse_Move = e .Location;
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is ElasticGroup || grobj is SolitaryControl)
{
Update ();
}
Invalidate ();
}
}
When you deal only with graphical objects, then the use of the standard double buffering solves the problem of the screen
flickering. When you start to move controls or groups of controls, then the system makes the decision about the proper
moment for their redrawing and it is always done with some delay. Adding a couple of lines into the OnMouseMove()
method solves the problem by enforcing the immediate update of the screen.
World of Movable Objects 215 (978) Chapter 9 Intermediate summary on graphical primitives
When any object is finally released by mover, it is time to decide about some actions and it is not always very easy to do. I
have already written at the beginning of the book that after releasing an object by the left button, there is a choice to
interpret the command as a finished movement or an order to bring an object on top of others. On releasing an object by the
right button, there is similar choice between the end of ordinary rotation and a call for context menu or tuning form. The
Form_SetOfObjects.cs demonstrates all these variants; as usual, the choice is made on the basis of a distance between two
points where the mouse was pressed and released.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
int iReleased, iNode;
NodeShape shape;
if (mover .Release (out iReleased, out iNode, out shape))
{
GraphicalObject grobj = mover [iReleased] .Source;
if (e .Button == MouseButtons .Left)
{
if (grobj is ElementOfSet)
{
(grobj as ElementOfSet) .RedefineCover ();
if (fDist <= 3)
{
CheckPopup (iReleased, iNode, shape);
}
Invalidate ();
}
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
if (grobj is ElementOfSet)
{
elemPressed = grobj as ElementOfSet;
ContextMenuStrip = menuOnFigures;
}
else if (grobj is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, null,
PointToScreen (ptMouse_Up));
}
else if (grobj is ElasticGroup)
{
groupAdd .ParametersDialog (this, GroupParametersChanged,
PointToScreen (ptMouse_Up), false);
}
}
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
}
You can be surprised to see the call for the RedefineCover() method on release of any ElementOfSet object by the
left button. (In reality it will be any object derived from this class.)
else if (grobj is ElementOfSet)
{
(grobj as ElementOfSet) .RedefineCover ();
World of Movable Objects 216 (978) Chapter 9 Intermediate summary on graphical primitives
I have already explained earlier that the cover must be redefined only when the change of sizes requires the new number of
nodes in the cover. This happens only in the case of N-node covers; even then this requirement is not always mandatory but
may depend on the cover design and the released node. Of those 11 classes of graphical objects shown at figure 9.5, only
four have such type of covers – Circle_EOS, Ring_EOS, Strip_EOS, and RegPoly_CircularHole_EOS – so
the use of the RedefineCover() call in the OnMouseUp() method is a simplification of code. I prefer to add empty
RedefineCover() methods into other seven classes instead of adding more if statements into the method above.
There can be any number of different objects in this form; all of them can be moved around, so there can be many
overlapped objects. If the distance between the points of pressing and releasing the mouse is really small (not greater than
three pixels) then the pressed object is moved on top of all others.
if (dist <= 3)
{
CheckPopup (iReleased, iNode, shape);
}
When the number of object in the mover queue is known, it is easy to make several steps.
1. Get the id of this object.
2. Find this object in the List of all objects.
3. Move the pressed object to the first position in this List.
4. Renew the mover queue according to the new order of objects.
5. Redraw the screen.
I used the same technique in many of the previous examples but in all of them it was enough to pass a single parameter to
such method – the number of the pressed object. Then why are there three parameters in the CheckPopup() method in
this form? From my point of view, variant of the method with three parameters is correct and in all the previous examples
the simplified version was used in order not to distract attention from more important features.
The covers for nearly all objects in the Form_SetOfObjects.cs have one common feature: a single node to cover the whole
area and several or many nodes to cover the border(s). From my observations, a tiny move of a border to adjust the view by
one or two pixels is used much more often than the tiny move of the whole object. So, if there is a tiny move of any node
on a border, then it is more likely to be the border adjustment than an attempt to bring this object atop. With the small move
of the main area of an object the probability is reverse and it is more likely to be an attempt to put this object in full view on
top of others. For these reasons I added the analysis of the additional parameters into the CheckPopup() method. For
eight classes of 11 there is a checking of the shape of the node because there is only one polygonal node and this is the node
that covers the whole area of an object. For three remaining classes I am checking if it is the last node in the cover.
(Usually the area of an object is covered by the biggest node; all smaller nodes on borders are included into the cover ahead
of that big one.)
private void CheckPopup (int iInMover, int iNode, NodeShape shape)
{
bool bPopup = false;
Figure figure = (mover [iInMover] .Source as ElementOfSet) .Figure;
switch (figure)
{
case Figure .Rectangle:
case Figure .RegularPolygon:
case Figure .ConvexPolygon:
case Figure .ChatoyantPolygon:
case Figure .PerforatedPolygon:
case Figure .RegPoly_CircleHole:
case Figure .RegPoly_RegPolyHole:
case Figure .ConvexPoly_RegPolyHole:
if (shape == NodeShape .Polygon)
{
bPopup = true;
}
break;
case Figure .Circle:
case Figure .Ring:
case Figure .Strip:
World of Movable Objects 217 (978) Chapter 9 Intermediate summary on graphical primitives
if (iNode ==
(mover [iInMover] .Source as ElementOfSet) .NodesCount - 1)
{
bPopup = true;
}
break;
}
if (bPopup)
{
PopupFigure (mover [iInMover] .Source .ID);
}
}
What other features can be found in the Form_SetOfObjects.cs? Context
menus can be called on objects and at empty places. In the really complex
applications which are discussed later there can be many different context
menus in a single form. The standard practice is to have a personal menu for
each class of objects, so in some of examples further on you’ll see up to 10
different menus or more. In the current form, there are many different classes,
but all objects are derived from the ElementOfSet class, so one menu is
used for all of them; another menu can be opened at any empty place.
Figure 9.7 shows the menu opened on one of the figures. Several commands
of the upper group allow to change the order of objects. These four commands
to move an object one position up or down and to move it to one or another end
of the List, these are the standard commands that are used throughout all of
Fig.9.7 Context menu on one of the
my applications. I found this set of commands enough even for the most
objects
complicated cases with a lot of objects on screen.
The second group of commands deals with the parameters of movability. Two commands allow to switch between rotatable
– non-rotatable and resizable – non-resizable. The third command allows to fix – unfix the internal partitions, so this
command can be applied only to circles and rings; for all other objects this line in menu is disabled.
Figure 9.8 demonstrates the menu which can be called at any empty place of the form.
The commands of this menu regulate the rotatability and resizability (outer and inner) of
the objects in view, only these commands are applied to all objects simultaneously.
The regulation of the object involvement in rotation is a small addition to its ability to be
rotated. The rotation process was described in details in the chapter Rotation. When an
object is caught by the right button, this is the starting moment of rotation and the
StartRotation() method must be called. All classes derived from the
ElementOfSet class have such methods but they differ in one thing. The only
parameter for such a call is the current mouse position which is used to calculate either a
single compensation angle or a whole array of compensations. For some of the objects
from the Form_SetOfObjects.cs, this single compensation angle is a difference between Fig.9.8 Menu at empty places
the angle to the mouse and the object angle; objects of the Rectangle_EOS class are
of such a type. Other objects may have several independent basic points; for each of them the angle between the mouse
position and this particular basic point must be calculated; objects of the ChatPoly_EOS class belong to this group. To
rotate an object, this single compensation or the whole array of compensations is used to calculate the position of all the
basic points; this is done in that part of the MoveNode() method which is used when an object is moved by the right
button.
The regulation of the object involvement in rotation is organized in identical way for all the objects. The base class
ElementOfSet has a field bRotate. At the moment of initialization any graphical object in this form is rotatable
because in the constructor of the base class this field is assigned the true value. The command from menu on any object
(figure 9.7) allows to switch this value for the pressed object.
private void Click_miRotatable (object sender, EventArgs e)
{
elemPressed .Rotatable = !elemPressed .Rotatable;
Invalidate ();
}
World of Movable Objects 218 (978) Chapter 9 Intermediate summary on graphical primitives
The call to redraw the form is included into the Click_miRotatable() method because the view of the object border
depends on whether an object is rotatable or not. The change of the bRotate value is very important as the check of this
value is included into that part of the MoveNode() method which is associated with the move by the right button and
which provides the object rotation. You can find this line in the methods for all 11 classes of elements.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right && bRotate)
{
… …
}
return (bRet);
}
The switch between the resizable – non-resizable states of the same object is organized in absolutely different way. In the
DefineCover() methods of all these elements you can see two branches of the code. The second branch is always
shorter and very simple as it does not include the nodes on the borders.
public override void DefineCover ()
{
CoverNode [] nodes;
if (bResize)
{
… …
}
else
{
… …
}
cover = new Cover (nodes);
}
When an object is resizable, the smaller nodes on the border nearly always precede in the cover the bigger nodes responsible
for moving an object. Though these bigger nodes are the same in both cases, they have different numbers for resizable and
non-resizable objects of the same class, so the part of code in the MoveNode() method which deals with forward
movement of objects has to be divided into two branches also. And the only thing that is needed in the MoveNode()
method for non-resizable objects is to call the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (bResize)
{
… …
}
else
{
Move (dx, dy);
}
… …
The fixing – unfixing of the inner partitions (this is organized only for circles and rings) can be regulated by the special
parameter bFixSectors (this is how it is organized in the Circle_EOS and Ring_EOS classes) or can be
World of Movable Objects 219 (978) Chapter 9 Intermediate summary on graphical primitives
regulated by the same bResize parameter. It depends on how much flexibility you are ready to give to the users of your
applications. As I recommend to give users the full control, then it is better not to regulate two different movements by a
single parameter.
In the Form_SetOfObjects.cs the fixing / unfixing of the partitions is organized in such a way. When the corresponding
command of menu is called, the SwitchSectorsFixing() method of the pressed element is called. This menu
command is enabled only for elements of two classes, so only two derived classes have this method. The redrawing of the
form is needed in this case because there is a possibility that covers are shown.
private void Click_miSlidingPartitions (object sender, EventArgs e)
{
if (elemPressed .Figure == Figure .Circle)
{
(elemPressed as Circle_EOS) .SwitchSectorsFixing ();
}
else // elemPressed .Figure == Figure .Ring)
{
(elemPressed as Ring_EOS) .SwitchSectorsFixing ();
}
Invalidate ();
}
The whole process for circles and rings is identical, so let us look at the Circle_EOS class. The called method switches
the value of the bFixSectors and calls the cover redefinition.
public void SwitchSectorsFixing ()
{
bFixSectors = !bFixSectors;
dataCircle .ShowPartitions = !bFixSectors;
RedefineCover ();
}
The RedefineCover() method simply calls the DefineCover() method but with the preliminary calculation of the
numbers of needed nodes.
public override void RedefineCover ()
{
NodesOnBorders ();
DefineCover ();
}
The NodesOnBorders() method has to define two different numbers which can vary. If the circle is resizable, then the
number of the small circular nodes (nNodesOnCircle) which cover the border of the circle has to be calculated; for non-
resizable circle this number is zero. The number of nodes on partitions between sectors (nNodesOnSides) is either equal
to the number of sectors or zero.
private void NodesOnBorders ()
{
if (bResize)
{
nNodesOnCircle =
Convert .ToInt32 ((2 * Math .PI * radius) / distanceNeighbours);
}
else
{
nNodesOnCircle = 0;
}
nNodesOnSides = bFixSectors ? 0 : vals .Length;
}
Some of the elements have only two variants – resizable or non-resizable – and there are two branches of code in their
DefineCover() method; I already demonstrated such code for the ConvexPolygon_EOS class. In the Circle_EOS
class the situation is more interesting. The Circle_EOS.NodesOnBorders() method prepares two values; each value
is either zero or positive, so there are four different situations for any Circle_EOS object. The
World of Movable Objects 220 (978) Chapter 9 Intermediate summary on graphical primitives
Circle_EOS.DefineCover() method does not have four different branches of code. If the calculated number of
nodes is positive, then the appropriate set of nodes is organized; if the value is zero, then this loop is skipped.
public override void DefineCover ()
{
CoverNode [] nodes;
nodes = new CoverNode [nNodesOnCircle + nNodesOnSides + 1];
for (int i = 0; i < nNodesOnCircle; i++)
{
nodes [i] = new CoverNode (i, Auxi_Geometry .PointToPoint (Center,
2 * Math .PI * i / nNodesOnCircle, Radius), nrSmall);
}
double angleForLine = Angle;
double [] sector_angle = SectorAngles ();
for (int i = 0; i < nNodesOnSides; i++) // on borders between sectors
{
nodes [nNodesOnCircle + i] = new CoverNode (nNodesOnCircle + i, Center,
Auxi_Geometry .PointToPoint (Center, angleForLine, Radius));
angleForLine += sector_angle [i];
}
nodes [nodes .Length - 1] = new CoverNode (nodes .Length - 1, Center,
Radius, Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
If the circle is non-resizable, then the number of circular nodes on the border is zero and the first loop is skipped. If the
borders between the sectors are declared fixed, then the number of nodes on these borders is zero and the second loop is
skipped. The only node which always exists in the cover of this class is the big circular node to move an object. I have
written about changing the object movability in the chapter Texts; there will be more about it in the examples with more
complex objects further on.
When any parameter regulating the ability of objects to be resized or rotated is changed, the cover has to be renewed by
calling the DefineCover() method, but the call for RenewMover() method is not needed as the number of objects
registered with mover is not changed. This is true for all simple objects. In the next chapter you will see that the situation
with complex objects is different.
World of Movable Objects 221 (978) Chapter 9 Intermediate summary on graphical primitives
Fig.9.9 In this example you can see objects of nearly all classes which were demonstrated in the previous chapters
Figure 9.9 shows the Form_ReferenceBook_Primitives.cs. In the top right corner of this figure you can see a table (this is
a ListView control) with some information about objects which are demonstrated in this example. It is possible that
later I can add more elements but just now the number of different classes is 31. There are no new classes; all of them were
already demonstrated before. Each of these classes was used in one or another example. Access to those examples is
through the commands of the main menu and its submenus and there is no direct jump from one example to another. In the
Form_ReferenceBook_Primitives.cs you can switch between the elements by a single click on the line in the ListView,
so it can be much better for comparison of objects from different classes. All demonstrated elements can be divided into
several groups according to their shape.
• Arcs (three classes)
• Circles (three classes)
• Crescents
• Lines (two classes)
World of Movable Objects 222 (978) Chapter 9 Intermediate summary on graphical primitives
• Polygons (seven classes represent solid polygons and polygons with holes)
• Rectangles (seven classes)
• Rings (two classes)
• Circle sectors (four classes)
• Rounded strips
• Triangles
Certainly, rectangles and triangles also belong to polygons, but I separated them in the previous chapters and this List of
shapes and classes is only the summary of the previous examples. Only few of the previously demonstrated classes are not
included into this example and this is done on purpose. For example, there are no objects which can disappear after
squeezing. If I would include such objects, I would have to add some instrument of adding new objects into this example
and I do not want to overload this example with such features.
For the same reason, there are no context menus in this example, so there are no ways to change the visibility parameters of
the demonstrated elements. For some of the elements I have demonstrated previously the regulation of the movability and
resizability; in the current example all movements are available and there is no way to change them. Anyway, this is only a
reference book while each of these classes has its own example with the full demonstration of its features.
Group in the Form_ReferenceBook_Primitives.cs belongs to the ElasticGroup class. I try to organize the sequence of
examples in the book in such way that if some object of the new class appears, then it is time to introduce this class and to
give its detailed description. With the ElasticGroup class the situation is different: the discussion of the groups is
further on while the class is so useful in interface design that I use it all the time. ElasticGroup objects were already
introduced but on those occasions their inner elements were only controls. In the current example there are two different
elements inside the group: one of them is big control (the ListView control) while another one belongs to the
CommentedControl class.
When you select any line in the List, short information about the selected class is shown in the TextBox control while
the name of the class appears as a comment to this control. The TextBox control together with associated comment
constitute a CommentedControl object. Because it is the inner element of the ElasticGroup object, then the group
frame adjusts its size and position to all changes of this element.
One column of the table shows the special mark for those objects which can be rotated. As you can find from the table,
nearly all objects can be rotated but the majority of rectangles cannot. The explanation of this strange observation is simple:
different types of rectangles were introduced before the explanation of rotation was given and I decided not to change those
classes. There is one class of rectangles with rotation – class Rectangle_AllMovements – and all other classes of
rectangles can be involved in rotation by adding similar code lines.
The Form_ReferenceBook_Primitives.cs is the summary of objects but at the same time this example demonstrates that
the same rules are applied to movement of objects of absolutely different shapes, so let us look once more on these main
rules.
Any movement (resizing, reconfiguring) starts when an object is pressed by mouse, so if there is anything interesting or
special, then it must be mentioned inside the OnMouseDown() method. Forward movement, resizing, and reconfiguring
are started by the left button press.
Forward movement of any object requires only the synchronous change of some basic points of the pressed object. These
basic points are specific for each class, so this movement is described in the Move() method of particular class.
Resizing and reconfiguring start at special areas of an object; the resizing usually starts by any border point while
reconfiguring starts at some special points. There are rare situations of unrestricted resizing or reconfiguring, but usually
there are some limits on possible movements of the caught part. These limits depend on the point where the movement
starts and on the relative positions of some other parts of the same object, so at the starting moment of resizing or
reconfiguring there is a call to StartScaling() or similar method of the class. Nearly always such method requires the
current mouse position; in addition it can require the number of the pressed node or its shape.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
NodeShape shapeNode = mover .CaughtNodeShape;
World of Movable Objects 223 (978) Chapter 9 Intermediate summary on graphical primitives
if (e .Button == MouseButtons .Left)
{
switch (figureInView)
{
case FigureType .CircleWithPartitions: // 2
if (shapeNode == NodeShape .Strip)
{
(grobj as Circle_SlidingPartitions)
.StartResectoring (mover .CaughtNode);
}
break;
case FigureType .RegularPolygon: // 9
if (grobj is RegularPolygon_EOS)
{
(grobj as RegularPolygon_EOS)
.StartScaling (e .Location);
}
break;
… …
case FigureType .ChatoyantPolygon: // 11
if (grobj is ChatoyantPolygon)
{
(grobj as ChatoyantPolygon)
.StartScaling (e .Location, mover .CaughtNodeShape);
}
break;
case FigureType .ConvexPolyRegPolyHole: // 12
if (grobj is ConvexPoly_RegPolyHole &&
shapeNode == NodeShape .Strip)
{
(grobj as ConvexPoly_RegPolyHole)
.StartScaling (e .Location, mover .CaughtNode);
}
break;
… …
Rotation can be started at any point of an object but it is triggered by the right button press, so it is another part of the
OnMouseDown() method. For any object one or several compensation angles must be calculated at the starting moment
of rotation. The number of needed compensation angles depends not on the complexity of the involved object but only on
the number of its basic points. The calculations are specific for the class, but the only parameter needed for all those
calculations is 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;
NodeShape shapeNode = mover .CaughtNodeShape;
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right)
{
switch (figureInView)
{
case FigureType .ArcThin: // 0
if (grobj is Arc_Thin)
{
(grobj as Arc_Thin) .StartRotation (e .Location);
World of Movable Objects 224 (978) Chapter 9 Intermediate summary on graphical primitives
}
break;
… …
case FigureType .RegularPolygon: // 9
if (grobj is RegularPolygon_EOS)
{
(grobj as RegularPolygon_EOS)
.StartRotation (e .Location);
}
break;
… …
There are really rare situations when an object and its parts can be involved in different rotations. I demonstrate objects of
two classes which, in addition to rotation of the whole object, allow the individual rotation of borders. For such special
cases the initial mouse position is not enough and the StartRotation() method of the specific class also requires the
number of the pressed node, but I want to underline once more that this is a very special situation.
private void OnMouseDown (object sender, MouseEventArgs e)
{
… …
else if (e .Button == MouseButtons .Right)
{
switch (figureInView)
{
… …
case FigureType .RegPolyRegHoleRotatableBorders: // 14
if (grobj is RegPoly_RegHole_RotatableBorders)
{
(grobj as RegPoly_RegHole_RotatableBorders)
.StartRotation (e .Location, mover .CaughtNode);
}
break;
case FigureType .ConvexPolyRegHoleRotatableBorders: // 15
if (grobj is ConvexPoly_RegHole_RotatableBorders)
{
(grobj as ConvexPoly_RegHole_RotatableBorders)
.StartRotation (e .Location, mover .CaughtNode);
}
break;
… …
Classes with the N-node type of covers have a privilege to be mentioned in the OnMouseUp() method. More often than
not this type of covers is used for objects with the curved borders, so you can see that there are circles, rings, and circle
sectors. The number of nodes in the cover of such object depends on the sizes of an object. When the resizing of such
object is over, the new number of nodes must be calculated and the new cover must be organized, so at this moment the
RedefineCover() method is called from within the OnMouseUp() method. The cover renewal is not a mandatory
thing for any resizing of those objects but only for the cases when the new number of nodes is needed. However, the cover
renewal is not a time consuming process and on some occasions I skip additional checking in order to make the code
simpler, so you can find some situations with a call to redefine the cover even if it is not needed.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iWasObject, iWasNode;
NodeShape shape;
if (mover .Release (out iWasObject, out iWasNode, out shape))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (fDist <= 3)
World of Movable Objects 225 (978) Chapter 9 Intermediate summary on graphical primitives
{
PopupElement (figureInView, iWasObject);
}
switch (figureInView)
{
case FigureType .CircleNnodes: // 4
if (grobj is Circle_Nnodes)
{
(grobj as Circle_Nnodes) .RedefineCover ();
}
break;
… …
case FigureType .RegPolyCircularHole: // 12
if (grobj is RegPoly_CircularHole &&
shape == NodeShape .Circle)
{
(grobj as RegPoly_CircularHole) .RedefineCover ();
}
break;
case FigureType .RingMulticolored: // 23
if (grobj is Ring_Multicolor &&
iWasNode < grobj .NodesCount - 1)
{
(grobj as Ring_Multicolor) .RedefineCover ();
}
break;
… …
Now it is time to move from primitive graphical elements to complex objects. Such objects consist of parts which can move
both individually and synchronously. These parts have familiar shapes and now we know how all these parts can move
individually, so the most interesting and definitely new things in the next chapter are the synchronous and related
movements of those parts.
World of Movable Objects 226 (978) Chapter 10 Complex objects
Complex objects
In one of the previous chapters (Texts) I have demonstrated an example with some related movements. The
objects in that example were independent, but movement of one of them could affect other objects. More
common is the situation when the related movements exist between the parts of some complex objects; those
parts can be involved both in individual and related movements. This chapter looks into the details of such
situations.
The CommentToRect_Demo class is derived from the Text_Rotatable class, so the individual forward movement
and rotation of these comments are going automatically without any mentioning of an object in the code. We need to
organize synchronous and related movements of comments and for this purpose there are three new fields in this class of
comments: the first one is the “parent” rectangle; two others are the coefficients to describe comment position in relation to
this rectangle. A CommentToRect_Demo object can be initialized either by assigning these coefficients or the comment
location. In the last case the coefficients are calculated by one method from the MoveGraphLibrary.dll.
public CommentToRect_Demo (Form form, RectangleF rc, PointF pt,
string txt, Font fnt, double ang_Deg, Color clr)
: base (form, Point .Round (pt), txt, fnt, ang_Deg, clr)
{
rcParent = rc;
Auxi_Geometry .CoefficientsByLocation (rcParent, pt, out xCoef, out yCoef);
}
Coefficients along both scales are calculated in similar way, so it is enough to look at the rules for calculation of horizontal
coefficient.
• If the point is to the left of rectangle, then the coefficient is equal to the distance from the point to the left border of
rectangle and has negative sign.
• If the point is to the right of rectangle, then the coefficient is equal to the distance from the point to the right border
of rectangle and has positive sign.
• If the point is between the left and right borders of rectangle, then the coefficient belongs to the [0, 1] range with 0
on the left border and 1 – on the right. (For vertical coefficient, upper border of rectangle is associated with 0 and
lower border – with 1.)
These rules provide unique coefficient for every point except the point exactly on the right border of rectangle and the next
point to the right of it. Their coefficients are determined by different rules and both coefficients are equal to 1; this situation
is analysed in the book of exercises “Easy Tasks for Movable Objects”.
At any moment comment position is determined in two different ways: there is a direct definition by some point and another
definition through rectangle and a pair of relational coefficients to this rectangle. On any movement of either rectangle or
comment, only one of parameters is changed directly and causes the recalculation of another parameter.
• If rectangle is moved or resized, then fixed coefficients are used to calculate the new position of comment
according to the changed area. Any comment is informed about the change of its parent rectangle via the
CommentToRect_Demo.ParentRect property.
public RectangleF ParentRect
{
get { return (rcParent); }
set
{
rcParent = value;
Location = Point .Round (Auxi_Geometry .LocationByCoefficients
(rcParent, xCoef, yCoef));
}
}
• If comment is moved, then its new position is determined by the Move() method of the base class. This new
position can be received from the Location property and is used to calculate the new coefficients in relation to
the unchanged rectangle.
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
Auxi_Geometry .CoefficientsByLocation (rcParent, Location,
out xCoef, out yCoef);
}
This is a pretty simple mechanism which provides the correct involvement of comments in individual and related
movements. Now let us look at the design and movements of rectangles with such associated comments.
World of Movable Objects 228 (978) Chapter 10 Complex objects
The Rectangle_WithComments class is designed as simple as possible: one colored rectangle and a List of
associated comments.
public class Rectangle_WithComments : GraphicalObject
{
RectangleF rc;
SolidBrush brush;
List< CommentToRect_Demo > m_comments = new List< CommentToRect_Demo > ();
Rectangle may have an arbitrary number of related comments and all these comments are involved in some synchronous or
related movements with their “parent”, but absence or presence of comments do not affect the cover of rectangle in any
way. Rectangle has its own cover; comments have their covers, and the design of these covers have nothing to do with any
kind of related movements. A rectangle has to be moved by any inner point and to be resized by any border point, so the
needed type of cover is standard; it was already demonstrated with the very first example of rectangles in this book
(Form_Rectangles_Standard.cs, figure 3.1).
public override void DefineCover ()
{
cover = new Cover (rc, Resizing .Any);
}
In all the examples with graphical primitives, there was a special small button to switch ON / OFF cover visualization. This
button will occasionally appear in further examples when I’ll be explaining some new things in cover design, but otherwise
you’ll not see this button any more. There is no cover visualization in the Form_Rectangles_WithComments.cs, so I’ll
remind about the type of cover organized by this standard method of the Cover class. Four small circular nodes on the
corners and four thin nodes along the sides allow to resize rectangle by any border point; one big rectangular node covers
the whole rectangle and allows its forward movement by any inner point. Whenever rectangle is moved or resized, all its
associated comments must be informed about the new rectangle area and adjust their positions to this new area.
There is only one basic point in rectangle – its top left corner, so only this point is changed in the Move() method. One
extra line is included into this method; this is the call of the InformRelatedElements() method which has to inform
all comments.
public override void Move (int dx, int dy)
{
rc .X += dx;
rc .Y += dy;
InformRelatedElements ();
}
The MoveNode() method is much longer than the Move() method because there are nine nodes in the cover and for eight
of them the proposed movement of the node has to be checked for the possibility of such movement. If movement of any
node is allowed, then some size of rectangle, or its position, or both are changed and the comments have to be informed
about this change. Calls of the InformRelatedElements() method are not scattered throughout the code of the
MoveNode() method because there is better and easier solution. Whenever some movement of any node is allowed, this
method has to return true value, so it is enough to check the value to be returned at the end of the MoveNode()
method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float wNew, hNew;
… …
else if (iNode == 0) //LT corner
{
hNew = rc .Height - dy;
if (minSide <= hNew)
{
MoveBorder_Top (dy);
bRet = true;
World of Movable Objects 229 (978) Chapter 10 Complex objects
}
wNew = rc .Width - dx;
if (minSide <= wNew)
{
MoveBorder_Left (dx);
bRet = true;
}
}
… …
}
if (bRet == true)
{
InformRelatedElements ();
}
return (bRet);
}
Passing of information about the area of rectangle to all associated comments is crucial for organizing the related
movements but in reality it is primitive.
private void InformRelatedElements ()
{
foreach (CommentToRect_Demo comment in m_comments)
{
comment .ParentRect = rc;
}
}
I have already shown what this CommentToRect_Demo.ParentRect property is doing: the location of comment is
adjusted to the new rectangle by using the existing coefficients.
Now, when the details of two involved classes – Rectangle_WithComments and CommentToRect_Demo – are
explained, it is time to look at how it all works in the form where the objects of these two classes are involved in individual
and related movements..
The first and the most important thing in organizing such movements is the correct registering of the complex
Rectangle_WithComments objects in the mover queue. Here are several statements that must be considered.
• A rectangle might have an arbitrary number of comments.
• Comments and rectangles can move individually; each of them has its own cover, so each one must be registered in
the mover queue individually.
• Comments can be placed anywhere but they are always shown atop their “parent” rectangle. To be shown above
the rectangle, the comments must be painted after this rectangle. When any comment is placed above its “parent”
rectangle and the mouse is pressed on comment, then the expected object to be caught is this comment and not a
rectangle. Thus, all comments must be registered before their parent in the mover queue.
• Comments can be added and deleted at an arbitrary moment; the request for such actions can come from different
places in the code, for example, as a reaction on selection of different menu commands.
Combine these statements together and it will be obvious that it is unreliable to change the queue of the movable objects
manually on any change of the situation with comments.
Many different graphical objects are used in the previous examples and in all of them those objects use for registering only
one of two methods: it is either
Mover .Add (GraphicalObject grobj)
or
Mover .Insert (int iPos, GraphicalObject grobj)
All graphical objects are derived from the GraphicalObject class. This base class has IntoMover() method which
can be used for registering simple (not complex !) graphical objects because GraphicalObject.IntoMover()
method simply calls the Mover.Insert() method.
World of Movable Objects 230 (978) Chapter 10 Complex objects
public void IntoMover (Mover mover, int iPos)
{
mover .Insert (iPos, this);
}
You can use this method to register any simple graphical object and it will work correctly. If you try to use this method
with complex objects, it will register the main part but not the associated components because there is no information about
possible existence and the number of components. In order to organize the correct registering of any complex object, you
need to write such method for your particular class and always use only this method for registering.
Here is such IntoMover() method for the Rectangle_WithComments class.
public void IntoMover (Mover mover, int iPos)
{
mover .Insert (iPos, this);
for (int i = m_comments .Count - 1; i >= 0; i--)
{
mover .Insert (iPos, m_comments [i]);
}
}
While registering the Rectangle_WithComments object, first the cover of rectangle is inserted into the mover queue at
the required position; then all related comments are inserted at the same position ahead of their “parent” rectangle. Thus, a
rectangle will be correctly registered together with all its comments regardless of their number.
The change in the number of movable objects in the form might happen in different cases: adding a new rectangle, deleting
a rectangle with all its comments, and adding or deleting any comment. In each case the RenewMover() method of the
form is called.
void RenewMover ()
{
mover .Clear ();
mover .Add (scHelp);
mover .Add (scCovers);
info .IntoMover (mover, mover .Count);
for (int i = 0; i < rects .Count; i++)
{
mover .Add (rects [i]);
}
if (bAfterInit)
{
Invalidate ();
}
}
These two methods of the class and of the form work in pair.
• The Rectangle_WithComments.IntoMover() method guarantees that any rectangle is registered correctly
regardless of the number of its comments.
• The RenewMover() method guarantees that all objects of the form are registered fully and correctly. Pay
attention that comments are not even mentioned in this method because their correct registration is hidden inside
the Rectangle_WithComments.IntoMover() method.
Special IntoMover() method is designed for each complex class with the individual and related movements of the parts.
The RenewMover() method is developed for each form with the changing number of movable / resizable objects.
Is there any difference in mover dealing with simple objects and complex objects? Not a bit! Mover does not know
anything about the real objects but deals only with their covers. If any node is caught for moving, this is translated into the
MoveNode() method of the corresponding object. Mover does not know anything about whether it is going to be an
individual movement or a synchronous one; only the correct method of the caught object is invoked, so if the related
movements must be started, then the request for it must be somewhere inside the Move() or MoveNode() methods of the
complex object. In the case of the Rectangle_WithComments class, the synchronous or related movement of all the
comments is started by the InformRelatedElements() method.
World of Movable Objects 231 (978) Chapter 10 Complex objects
This is the procedure to organize the individual and related movements for rectangles with comments, but exactly the same
technique is used for other types of complex objects. The calculation of specific coefficients can be different and depends
on the shape of the “parent” object, but the idea is exactly the same. It also does not matter, how many levels of related
objects are included into the chain of linked objects. This example has only two levels (rectangle – comments); one of the
further examples will have more levels of related objects.
Before moving to the next example, I want to mention two features of the Form_Rectangles_WithComments.cs. The first
one is the system of commands and their distribution among different context menus.
Fig.10.2a Menu on comments Fig.10.2b Menu on rectangles Fig.10.2c Menu at empty places
The “least important” elements in this example are comments. They are ruled by rectangles (or they belong to rectangles)
and there are no elements which are subordinate to comments. Thus, it is possible to change the visibility parameters of any
comment (color and font) or even delete a comment and these actions affect only the involved comment and no other
element. Three mentioned possibilities are associated with three commands of the menu on comments (figure 10.2a).
From the point of importance or hierarchy, rectangles stay one level above the comments. Rectangles are subordinate to the
form itself and each rectangle rules over the associated comments. In menu on rectangles (figure 10.2b), there are
commands to deal with rectangle and commands which are applied to all comments of the pressed rectangle. There are also
two different commands to add new comment. The first variant adds new comment with all parameters set by default, while
the second variant allows to organize new comment with all the needed parameters just from the beginning. I’ll return to
this command several lines further on.
Menu which can be called at any empty place of the form (figure 10.2c) includes the commands that either change the full
view of the form (restore default view or globally change the font), or are applied to all elements (set universal font for all
elements except information; switch visualization of comments rotation ON / OFF), or add elements of the next level (add
rectangle).
The Form_Rectangles_WithComments.cs (figure 10.1) example deals with few elements of simple enough classes but it
perfectly illustrates the main rules of organizing any user-driven application. In such application, any visibility parameter of
any screen element is totally controlled by users, so there must be some simple and universal way to do it. Nobody is going
to learn some special rules of dealing with one or another form (application); it is much better if there are the same rules for
all of them.
• If you need to change some parameters of particular object, you have to call menu on this object and this menu has
to include commands to change all those parameters. Each command of this menu allows to change the parameter
of only this pressed object and nothing else.
• If you want to change some parameter for a group of elements subordinate to another object, you have to call menu
on that “parent” and there must be commands to change parameters for all its “children”.
• If you want to change some parameters of the form (application) or parameters of all objects inside this form, you
have to call context menu at any empty place of the form.
These rules work in all the examples further on regardless of the number of hierarchy levels in each particular form.
The second thing I want to mention are the first two commands in menu on rectangles (figure 10.2b).
I use this Form_Rectangles_WithComments.cs example to explain the organization of individual, related, and
synchronous movements, so the text or visibility parameters of comments for rectangles maybe not so important. The first
command of menu allows to add new comment with default parameters.
private void Click_miAddCommentQuick (object sender, EventArgs e)
{
rectPressed .AddComment (this, ptMouse_Up, "New comment");
RenewMover ();
}
World of Movable Objects 232 (978) Chapter 10 Complex objects
As you see, font and color for new comment are not mentioned here and even the text is always the same. In the code of the
Rectangle_WithComments.AddComment() method the visibility parameters of new comment are not mentioned.
public void AddComment (Form form, PointF pt, string txt)
{
CommentToRect_Demo cmnt = new CommentToRect_Demo (form, rc, pt, txt);
cmnt .ParentID = ID;
m_comments .Add (cmnt);
}
Only the constructor of the CommentToRect_Demo class shows that font and color are received from the form.
public CommentToRect_Demo (Form form, RectangleF rc, PointF pt, string txt)
: this (form, rc, pt, txt, form .Font, 0, form .ForeColor)
{
}
Comment visibility parameters – font and color – can be changed later via the commands of comment menu (figure 10.2a),
but there is no way to change the text. Well, it is only in this example because for the purpose of this example it does not
matter at all.
But addition of new comments is needed in many different cases and is used in several examples further on, so I decided to
introduce better variant even in such simple example. The second command in menu on rectangles (figure 10.2b) opens an
auxiliary form which allows to set font, color, and text; then new comment can be added to the pressed rectangle.
private void Click_miAddCommentCustom (object sender, EventArgs e)
{
Form_NewComment form =
new Form_NewComment (this, PointToScreen (ptMouse_Up));
if (DialogResult .OK == form .ShowDialog ())
{
string text = form .CommentText;
if (!string .IsNullOrEmpty (text))
{
rectPressed .AddComment (new CommentToRect_Demo (this,
rectPressed .MainArea, ptMouse_Up, text,
form .CommentFont, 0, form .CommentClr));
RenewMover ();
}
}
}
Figure 10.3 shows the Form_NewComment.cs. There are only few
controls to set all the needed parameters of the new comment (color, font,
and text), but there is one interesting idea implemented in the work of this
form. According to rules of user driven applications, all controls inside
the form are movable; two bigger controls are also resizable, so you can
change the view of this form to whatever you want. All changes are
saved on closing this form and the next time it will be opened with the
same view. You can also change the font of the form by calling context
menu at any empty place. These things are standard for all user-driven
Fig.10.3 Default view of the form to
applications and I have to mention them here only because the discussion
organize new comment
of these rules is far ahead.
Interesting in this form is the implementation of related movements. The TextBox control is the biggest element in the
form and also plays the main role in organizing new comments, so I declared this control a dominant element in the form.
Three other controls play the role of subordinates. When the dominant element is moved or resized, all its subordinates
adjust their positions in order to retain the same relative position. It works similarly to the process of moving and resizing
of rectangle with comments. There is only one difference between two processes: subordinate controls cannot be placed
inside the area of the dominant control. If you try to move and release any other control inside the area of TextBox
control, this subordinate is automatically moved outside this area. We’ll look into details of all involved classes and their
work together in the chapter Groups in special section Dominant and subordinate controls.
World of Movable Objects 233 (978) Chapter 10 Complex objects
people are afraid of such flexibility; the discussion of different views on such flexibility organized under full users’ control
is also in the chapters further on.
Fig.10.4 While any rectangle or comment is involed in some movement, all parameters of rectangle and comment are
shown in the controls of the big group
Before going into some details of code, I want to finish with all the auxiliary parts, so I have to mention the system of
menus in the new example. Especially because some commands in menus give a clear indication that comments of this
example have some new features. To be absolutely correct, CommentToRect objects have nothing special because two
classes – CommentToRect and CommentToRect_Demo – are identical. Both classes are derived from the
Text_Rotatable class; if one of them demonstrates some new feature then this feature belongs to the base class and
was simply not used before.
There are four different context menus in the Form_Rectangles_WithComments_Advanced.cs. Three of them are called
in similar way to the previous example: on comments, on rectangles, and at empty places. Menu at empty places are
identical for both examples, so there is no sense to repeat it again (see figure 10.2c). Menu which can be called inside any
group consists of only two commands and allows either to call the tuning form to modify the pressed group or to set the
default view of this group (figure 10.5c).
For the easiness of comparison, I put menu on comments (figure 10.5a) and menu on rectangles (figure 10.5b) in the same
order as in the previous example. By comparison of two pairs of menus from figures 10.2 and 10.5, you can easily find that
there is one new thing: in the current example comments to rectangles can be hidden and unveiled!
Our comments which can be hidden are derived from the Text_Rotatable class.
public class CommentToRect : Text_Rotatable
In the chapter Texts I already used the exact copy of this class – the Text_Rotatable_Demo class – to explain the text
rotation (Form_TextRotatable_Class.cs, figure 5.7). If you look through the code of the Text_Rotatable_Demo
class, you are not going to find anything about the possibility of hiding such elements, so we need to look somewhere even
deeper.
public class Text_Rotatable : GraphicalObject
GraphicalObject is the base class for all graphical objects in my system; there is no way to look deeper than this level
and we do not need to go further on as this GraphicalObject class contains the key to our puzzle of comments which
can be hidden and unveiled. This truly base class has the field which describes the visibility of any graphical object; not
only comments but any object!
public abstract class GraphicalObject
{
private bool m_visible;
Objects are organized to be seen by users, so by default any new object is visible.
public GraphicalObject ()
{
m_visible = true;
… …
At any moment the value of this field can be received or changed with the GraphicalObject.Visible property.
public bool Visible
{
get { return (m_visible); }
set { m_visible = value; }
}
In many scientific / engineering applications, the possibility of hiding and unveiling some information on the screen is very
useful and is widely used. Program with rectangles and comments is one of the preliminary steps in design of complicated
scientific applications, so I decided to demonstrate such possibility in the current example.
Now let us look how those groups for demonstration of parameters can help in understanding the collaboration of rectangles
and comments. All the information appears at the moment when some rectangle or comment is pressed by mouse. When
an object is released, all controls inside the groups are immediately cleaned, so we need to examine only the process
between MouseDown and MouseUp events.
When anything is pressed with a mouse, it is exactly ONE object. If it is a rectangle, then it belongs to the
RectangleCommented class; if it is a comment, then it belongs to the CommentToRect class.
RectangleCommented rectShowParams;
CommentToRect cmntShowParams;
In any case it is a single object of one or another class, but nearly always both groups are filled with information, so there
must be some logic in selecting the object which parameters are shown in another group. This selection is done when either
rectangle or comment is pressed, so we need to look into the OnMouseDown() method of the form
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, bShowAngle))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is RectangleCommented)
{
RectangleCommented rect = grobj as RectangleCommented;
World of Movable Objects 240 (978) Chapter 10 Complex objects
if (e .Button == MouseButtons .Left)
{
rectShowParams = rect;
cmntShowParams = CommentToShowParams (rectShowParams);
rect .StartResizing (e .Location, mover .CaughtNode);
ShowParams ();
}
}
else if (grobj is CommentToRect)
{
cmntShowParams = grobj as CommentToRect;
rectShowParams = RectangleToShowParams (cmntShowParams);
ShowParams ();
}
}
ContextMenuStrip = null;
}
If any rectangle is pressed, then the comment for the group Comment is determined by the CommentToShowParams()
method.
private CommentToRect CommentToShowParams (RectangleCommented rect)
{
if (rect .Comments .Count > 0)
{
// get the first visible comment; otherwise simply the first
List<CommentToRect> cmnts = rectShowParams .Comments;
for (int i = 0; i < cmnts .Count; i++)
{
if (cmnts [i] .Visible)
{
return (cmnts [i]);
}
}
return (cmnts [0]);
}
return (null);
}
• If the pressed rectangle has visible comments, then the first of them is selected.
• If all comments of rectangle are hidden, then the first of them is selected.
• If rectangle has no comments, then it is one of those cases when the group Comment is going to be empty.
If comment to rectangle is pressed, then the rectangle for the group Rectangle is determined by the
RectangleToShowParams() method. This rectangle is the “parent” of the pressed comment and this rectangle is
identified in exactly the same way which was described in the previous section.
private RectangleCommented RectangleToShowParams (CommentToRect cmnt)
{
RectangleCommented rect = null;
long idParent = cmnt .ParentID;
for (int i = 0; i < rects .Count; i++)
{
if (idParent == rects [i] .ID)
{
rect = rects [i];
break;
}
}
return (rect);
}
World of Movable Objects 241 (978) Chapter 10 Complex objects
Is it possible to see the group Comment filled with information while the Rectangle group is empty? Yes, it is very easy to
achieve by pressing any comment inside the groups; you will see such situation whenever you try to move or rotate any
comment inside the groups. It happens because those comments of the CommentedControl class belong to the same
CommentToRect class as the comments associated with rectangles of the RectangleCommented class. Discussion of
controls with comments is further on in the chapter Control + graphical text, but I can tell you beforehand that relative
positioning of text in such pair is organized in exactly the same way as positioning of comments associated with rectangles.
When you read that chapter slightly more than 100 pages ahead, you can return to this example and look at the details of
positioning a comment to the control by moving any comment inside these groups.
Plot analogue
File: Form_PlotAnalogue.cs
Menu position: Graphical objects – Complex objects – Plot analogue
The new example of complex
objects also can be looked at as the
next preliminary step in design of
plots for scientific and engineering
applications. Real plots in such
programs have a lot of different
parameters which are very
important and crucial for their use.
Those plots are going to be the
theme of discussion a bit later, while
in this example I want to
demonstrate their analogue. In such
way I can eliminate many details
and look at these analogues only
from the point of complex objects
design.
Real plot consists of a single
rectangular plotting area, an
arbitrary number of horizontal and
vertical scales, and an arbitrary
number of textual information
associated with the plotting area and Fig.10.6 These objects work as plot analogues with plotting areas, scales, and
scales. Not only the number of all comments
the additional parts is arbitrary but
their positions are also arbitrary. Scales can be placed at any side of the plotting area or atop the area itself. Comments can
be placed anywhere in relation to the associated plotting area or scale.
The real scales consist of the main line with ticks and numbers (or words). These parts together occupy some rectangular
area. In my scale analogues (HorScaleAnalogue and VerScaleAnalogue classes), I use only main line and
ticks, while the numbers are excluded (figure 10.6). This is the biggest simplification of the demonstrated model; all other
things work as they do in the real plots. The Form_PlotAnalogue.cs example allows to investigate several combinations of
two-level relations between elements (plotting area – scale, plotting area – comment, and scale – comment); the movement /
resizing of plotting area includes elements from three levels (area – scale – scale comments).
First, let us look at the classes of involved elements.
Comments which are used in this example are of the CommentToRect class. The class is derived from the
Text_Rotatable class, so all these comments can be moved and rotated at any moment. Any comment is positioned by
its central point; rotation goes around the same point. Any comment is associated either with rectangular plotting area or
with area of some scale. Area of the scale is determined by its main line and the combined area of its ticks, so this is also a
rectangle. Thus, any comment is associated with rectangular area. Positioning of any comment in relation to its “parent”
rectangle is defined by two coefficients: one of them is for horizontal positioning, another – for vertical. It is organized
exactly as in the previous example of rectangles with comments. I’ll remind the rules for calculation of coefficient for
horizontal positioning. Calculation depends on the relative position of comment.
• Central point of comment is to the left of rectangle. Coefficient is negative and its absolute value is equal to the
distance from the left border of rectangle to the central point of comment.
World of Movable Objects 242 (978) Chapter 10 Complex objects
• Central point of comment is to the right of rectangle. Coefficient is positive and its absolute value is equal to the
distance from the right border of rectangle to the central point of comment.
• Central point of comment is between the left and right borders of rectangle. Coefficient belongs to the [0, 1] range
with 0 on the left border and 1 – on the right.
The coefficient along the vertical scale is calculated in similar way with 0 on the upper border and 1 – on the lower border
of rectangle.
CommentToRect class uses the cover prepared by its base Text_Rotatable class. This cover consists of a single
rectangular node; sizes of rectangle are determined by the text of comment and the used font.
Comment can be related either to a plotting area or a scale. Comment does not know about the class of its parent, and there
is no need for such knowledge. In any way the “parent” has a rectangular area; the knowledge of this area in combination
with two positioning coefficients is enough for correct involvement of any comment in individual movements and related
movements with its “parent”. The use of rcParent and two coefficients was explained in the example of rectangles with
comments (Form_Rectangles_WithComments.cs, fig.10.1); the CommentToRect_Demo class from that example uses
exactly the same technique.
Scales can be either horizontal (HorScaleAnalogue class) or vertical (VerScaleAnalogue class). Two of these
classes differ only by direction of the main line but otherwise they are identical, so I will write further on only about the
horizontal scales.
public class HorScaleAnalogue : GraphicalObject
{
Rectangle rcParent; // main plotting area
Point pt_LT, pt_RB; // end points of the central line
double posCoef; // positioning coefficient of the main line to the plotting area
Side sideTicks = Side .N; // N or S
int nTicksLen = 12;
Pen penLine, penTicks, penDots;
Font fntComments;
List<CommentToRect> m_comments = new List<CommentToRect> ();
The main line of the scale is described by two end points (pt_LT and pt_RB) and is shown with a wide pen. There is
always six ticks on one or another side of the main line; ticks are shown with thin pen of the same color. Two end ticks and
main line constitute three sides of rectangle; the fourth side of this rectangle is shown with a dotted pen; this rectangle is the
main area of the scale.
The length of horizontal scale is equal to the width of associated plotting area. Horizontal scale can be moved only up or
down. Main line of the scale can be placed on the screen higher than plotting area, below this area, or inside the area. As
this main line can change its relative position to rectangular area of the plot only in one direction, then it is enough to have
one positioning coefficient. This coefficient describes the position of the main line in relation to vertical borders of the plot;
to calculate this coefficient, the same rules are used as for calculating positioning coefficients of comments.
Scale is not individually resizable, so it is enough to have a cover consisting of a single node. In normal situation (and by
default) scale is movable but at any moment it is possible to fix its relative position to the plotting area thus making such
scale unmovable. For these two modes, the only node of the cover has different parameters. When relative position of the
scale is fixed, the node behaviour is changed to Behaviour.Frozen. When it happens, the scale becomes unmovable
by itself, but it is still moved around whenever the “parent” plot is moved. The Behaviour.Frozen means that the
mover cannot grab an object for relocation but still recognizes the node, so, for example, a context menu can be called on
such object.
public override void DefineCover ()
{
int cyTop = (sideTicks == Side .N) ? (pt_LT .Y - nTicksLen) : pt_LT .Y;
Rectangle rect =
Rectangle .FromLTRB (pt_LT .X, cyTop, pt_RB .X, cyTop + nTicksLen);
CoverNode [] nodes;
if (Movable)
{
nodes = new CoverNode [] { new CoverNode (0, rect, Cursors .SizeNS) };
}
else
World of Movable Objects 243 (978) Chapter 10 Complex objects
{
nodes = new CoverNode [] { new CoverNode (0, rect, Behaviour .Frozen,
Cursors .Hand) };
}
cover = new Cover (nodes);
cover .SetClearance (false);
}
Scale has two fields to adjust or retain its position along the plotting area whenever this area is changed or the scale is
moved. The first one is the plotting area rcParent; the second one is the already mentioned coefficient posCoef. On
any movement of either plot or scale only one of these parameters is changed.
• If the plotting area is moved or resized, then the coefficient is not changed and is used to calculate the new scale
position according to the changed area. Value of the new plotting area is sent to the scale via the
HorScaleAnalogue.ParentRect property.
public Rectangle ParentRect
{
get { return (rcParent); }
set
{
rcParent = value;
int cy = Convert .ToInt32 (Auxi_Geometry .CoorByCoefficient
(rcParent .Top, rcParent .Bottom, posCoef));
pt_LT = new Point (rcParent .Left, cy);
pt_RB = new Point (rcParent .Right, cy);
CommentsNotification ();
DefineCover ();
}
}
• When scale is moved individually, then its basic points (these are two end points of the main line) are changed
synchronously and positioning coefficient of the scale is recalculated. The scale area has changed, so all comments
associated with this scale must be informed about the change. Sending information to comments and adjusting
their positions according to the new scale area are done in exactly the same way as was shown in the previous
example of rectangles with comments.
public override void Move (int dx, int dy)
{
pt_LT .Y += dy;
pt_RB .Y += dy;
posCoef = Auxi_Geometry .CoefficientByCoor (rcParent .Top,
rcParent .Bottom, pt_LT .Y);
CommentsNotification ();
}
private void CommentsNotification ()
{
Rectangle rect = Rectangle;
foreach (CommentToRect comment in m_comments)
{
comment .ParentRect = rect;
}
}
Plot analogue is a simple rectangle which can be associated with auxiliary parts, so the PlotAnalogue class has few
fields.
public class PlotAnalogue : GraphicalObject
{
Form form;
Rectangle rc;
SolidBrush m_brush;
Font fntComments;
World of Movable Objects 244 (978) Chapter 10 Complex objects
List<HorScaleAnalogue> m_horScales = new List<HorScaleAnalogue> ();
List<VerScaleAnalogue> m_verScales = new List<VerScaleAnalogue> ();
List<CommentToRect> m_comments = new List<CommentToRect> ();
static int minSide = 60;
Rectangular plotting area can be resized by corners and sides, so it is possible to use a standard cover consisting of nine
nodes.
public override void DefineCover ()
{
cover = new Cover (rc, Resizing .Any);
}
Rectangle has only one basic point – its top left corner. For a solitary rectangle, it would be enough to change this point
inside the Move() method, but there are three lists of possibly associated parts and all those parts must be informed about
any change of the main area.
public override void Move (int dx, int dy)
{
rc .Location += new Size (dx, dy);
InformRelatedElements ();
}
One of the first examples in the book – the Form_Rectangles_Standard.cs (figure 3.1) – already demonstrated rectangles
with such cover, so the MoveNode() methods of the old class Rectangle_Standard and of the new class
PlotAnalogue must be similar. Only in addition the new class must inform all the associated elements about any change
of its sizes throughout the InformRelatedElements() method. The ParentRect property of scales and comments
are called to adjust their positions; these properties were already discussed earlier.
private void InformRelatedElements ()
{
foreach (CommentToRect comment in m_comments)
{
comment .ParentRect = rc;
}
foreach (HorScaleAnalogue scale in m_horScales)
{
scale .ParentRect = rc;
}
foreach (VerScaleAnalogue scale in m_verScales)
{
scale .ParentRect = rc;
}
}
When the ParentRect property of the scale gets the new value of the plotting area, then the new position of the scale is
calculated and the CommentsNotification() method of the scale is called. Through this method, all the comments
of the scale get the new area of the scale and adjust their positions to it. Thus, the adjusting of positions is done as a chain
reaction from the top (PlotAnalogue class) to the bottom (CommentToRect class). Whether there are two elements
in the chain (plot – scale) or three (plot – scale – comment), it does not matter; all the elements in the chain adjust their
positions.
I have already mentioned that complex objects cannot be registered with the mover by the mover.Add() or
mover.Insert() methods. Instead, the IntoMover() method must be designed for each class of complex objects.
There are too many places where the set of elements of the complex classes can be changed; on any such change the mover
queue must be renewed and it cannot be done manually. The class itself has to guarantee that any member of this class is
going to be registered correctly regardless of the particular set of its parts. Here is the PlotAnalogue.IntoMover()
method.
new public void IntoMover (Mover mover, int iPos)
{
mover .Insert (iPos, this);
for (int i = m_comments .Count - 1; i >= 0; i--)
{
World of Movable Objects 245 (978) Chapter 10 Complex objects
mover .Insert (iPos, m_comments [i]);
}
for (int i = m_horScales .Count - 1; i >= 0; i--)
{
m_horScales [i] .IntoMover (mover, iPos);
}
for (int i = m_verScales .Count - 1; i >= 0; i--)
{
m_verScales [i] .IntoMover (mover, iPos);
}
}
Any comment is a simple object, so it can be registered with a standard Mover.Insert() method.
mover .Insert (iPos, m_comments [i]);
Scale is a complex object, so scales have to use IntoMover() method of their class; code of the method for horizontal
and vertical scales is identical.
new public void IntoMover (Mover mover, int iPos)
{
mover .Insert (iPos, this);
for (int i = m_comments .Count - 1; i >= 0; i--)
{
mover .Insert (iPos, m_comments [i]);
}
}
I want to organize an unrestricted movement of all comments at any moment, so comments are always shown atop the
associated “parents” (it can be a scale or plotting area) and placed in the mover queue ahead of them. This can be seen from
the code of the HorScaleAnalogue.IntoMover() method: comments precede the scale itself.
Scales can be placed anywhere in relation to the main area. They can be moved slightly to the side of the plotting area, but
it is not a rare situation when scales partly or entirely overlap with this area. To be movable at any moment, scales must
precede the plot itself in the mover queue. As a result, after using the PlotAnalogue.IntoMover() method we have
such order of elements in the mover queue.
1. All vertical scales.
2. All horizontal scales.
3. All comments of the main plotting area.
4. The PlotAnalogue object itself.
Covers of all these parts are simple and there is no need to visualize them in the current example. Yet, you can see a small
button which allows to visualize the covers. Further on, in discussion of the real plots we’ll come to the problem of
organizing some needed movements and then I’ll remind you about this possibility of switching the covers ON for better
understanding of some problematic situations.
PlotAnalogue class represents the analogue of really complex plots which are used in many scientific and engineering
applications. In parallel with moving from simple to really complex objects, I try to change the design of examples in the
way which is much closer to the standards of user-driven applications. I think it is time to formulate the main rules of such
applications.
Rule 1 All elements are movable.
Rule 2 All parameters of visibility must be easily controlled by users.
Rule 3 Users’ commands on moving / resizing of objects or on changing the parameters of visualization must be
implemented exactly as they are; no additions or expanded interpretation by developer are allowed.
Rule 4 All parameters must be saved and restored.
Rule 5 The above mentioned rules must be implemented at all the levels beginning from the main form and up to the
farthest corners.
The time for detailed discussion of these rules will come later (the second half of the book is devoted to this); here I want
only to mention, how these rules are transformed into the code of this particular Form_PlotAnalogue.cs.
World of Movable Objects 246 (978) Chapter 10 Complex objects
Rule 1. There are no unmovable elements in this form. Some elements (scales) can be turned into unmovable but only by
direct command from user. The need for such change of movability was announced by users of very complicated
applications; here I only demonstrate the possibility and its realization. Changing of the scale movability is done via the
context menu which can be called on any scale. Usually the change of movability does not affect the view of the objects,
but in this example I even made this feature obvious by changing the style of the lines with which the scales are drawn:
main line of any movable scale is shown by solid line, for fixed scale – by the dashed line.
Rule 2 is demonstrated in this form in several ways, but, as it is usually done, the control of visual parameters is organized
via the context menus. There are three types of objects in this form (areas, scales, and comments); each one has its own
menu. Figures 10.7 show menus for these objects and one more menu which can be called at any empty place.
Fig.10.7a Menu on areas Fig.10.7b Menu on scales Fig.10.7d Menu at empty places
Track bars
File: Form_Trackbars.cs
Menu position: Graphical objects – Complex objects – Track bars
Track bars are well known elements of the interface design. The idea of an object in which a small bar slides along another
bar and allows to select any value from some
range can be very useful in many situations.
Unfortunately, the implementation of this
control in VisualStudio is so clumsy that
after each attempt of using the standard
TrackBar control I had to reject it and turn
to something else. But the main idea of the
track bar is perfect and a good element of
such type is so desirable from time to time
that it has to be used. So, I designed the
graphical Trackbar class which fits
perfectly with all other elements of the user-
driven applications and with the rules of such
applications.
An object of the Trackbar class can be
used in two different ways: it can be a stand
alone element or it can be associated with
some rectangular area. The difference in
features for these two cases required the
implementation of two different covers.
Both variants of using the Trackbar
objects are demonstrated in the
Form_Trackbars.cs. In this example, four
track bars are associated with movable /
resizable rectangle, while two other track
bars are independent elements (figure 10.8). Fig.10.8 Track bars can be used as independent objects or together with
any rectangle.
A Trackbar object occupies some area;
somewhere inside this area there is a small slider which can be moved between two ends of the track bar. Let us start with
this small element.
The small sliding bar belongs to the TrackbarSlider class.
public class TrackbarSlider : GraphicalObject
{
Side sideHead;
Point pt_LT, pt_RB; // the spike can move between these two points
double coef; // from [0, 1]; 0 is on the Left for HOR trackbar and on top for VER trackbar
Point ptSpike; // usually the real spike; for rectangular slider - middle of the head side
SliderShape m_shape;
int m_head, m_body, m_tail; // sizes of three parts; not all of them are used for each shape
int wHalf;
bool bShowEdge;
Pen m_pen;
SolidBrush m_brush;
Four different variations of the slider shape are described by the SliderShape enumeration; the names give no chances
for mistakes.
enum SliderShape {Triangle, Rectangle, Pentagon, Sexangle };
Figure 10.9 demonstrates all four shapes of sliders. Colors of these sliders are purposely set the same as at figure 10.8;
comments mark the parts which are mentioned in the TrackbarSlider class description.
World of Movable Objects 248 (978) Chapter 10 Complex objects
Four fields of the class are used to describe the geometry of a slider but
not all of them are used for each shape. For example, the triangular and
rectangular sliders use only one field to describe the length. Sliders of
different shapes have different number of vertices but they are all convex
polygons, so the cover for any slider consists of a single polygonal node.
public override void DefineCover () Fig.10.9 Four possible shapes of the sliders
{
cover = new Cover (new CoverNode (0, Corners,
(pt_LT .X == pt_RB .X) ? Cursors .SizeNS : Cursors .SizeWE));
}
I use only horizontal or vertical track bars, so my sliders move only horizontally or vertically. Depending on the allowed
movement, the mouse cursor over this cover gets one or another standard shape which shows the direction of possible slider
movement.
The sideHead field of the class determines the spike side; pay attention that for any slider the spike side is orthogonal to
the allowed movement of this slider.
Slider cannot exist as a stand alone object but only as part of some Trackbar object and can be moved only between two
ends of this track bar. At figure 10.8, two sliders – red and violet – are shown as stand alone colored elements without any
auxiliary parts, but this is only an illusion; further on I’ll explain how this illusion is organized. Two fields of the
TrackbarSlider object – pt_LT and pt_RB – determine two ends of the straight segment. While a slider is
moved, the end point of its spike (ptSpike) moves along this segment between two end points and cannot go anywhere
else. (For rectangular slider this ptSpike is associated with the middle point of the flat end.) The [pt_LT, pt_RB]
segment is associated with the [0, 1] range and the current position of the spike is determined as a slider coefficient which
takes the value from the [0, 1] range.
The MoveNode() method for sliders is easy as the movement is allowed only in one direction; here is the part of the
TrackbarSlider.MoveNode() method for slider movement along the horizontal line.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
int axisNew;
if (sideHead == Side .N || sideHead == Side .S)
{
axisNew = ptSpike .X + dx;
if (pt_LT .X <= axisNew && axisNew <= pt_RB .X)
{
Move (dx, 0);
bRet = true;
}
}
… …
There are two different constructors for sliders. In one of them, not only the line segment for the spike movement is
determined but also the whole slider geometry which includes shape, sizes of three parts, and the maximum width. Sliders
of all four shapes are symmetrical and I prefer to work with half of the width (wHalf).
public TrackbarSlider (Side side_head, Point ptLT, Point ptRB, double poscoef,
SliderShape shape, int h_headslope, int h_straight,
int h_backslope, int w_half)
In another constructor, only the segment is defined and the side into which the spike is looking. The last parameter is
important because spikes of two opposite directions can move along the same segment.
public TrackbarSlider (Side side_head, Point ptLT, Point ptRB, double poscoef)
: this (side_head, ptLT, ptRB, poscoef, SliderShape .Pentagon, 8, 12, 0, 4)
{
}
World of Movable Objects 249 (978) Chapter 10 Complex objects
In this constructor the slider geometry is omitted and is set by default, so slider gets the shape of pentagon and default sizes.
Such constructor is used, for example, for brown slider at the top of figure 10.8.
I want to mention one important definition before switching from sliders to track bars. Any slider has a central axis which
goes from its spike to the middle of the opposite side. Any slider is moved only orthogonally to its axis. The spike can
move only inside the segment [pt_LT, pt_RB] and throughout such movement the slider axis moves inside a rectangle of
which this segment is one side; another size of this rectangle is equal to full length of the slider. In the following text this
rectangle is called the slider rectangle though it has to be called the rectangle in which slider axis can move. I think the
correct naming is too long to be used all the time and instead use shortened version. Pay attention that this slider rectangle
has nothing to do with the slider shape; it is also independent of the slider width.
Any slider is an individually movable part of some track bar; now it is time to look at the Trackbar class.
public class Trackbar : GraphicalObject
{
Form formParent;
Form_TrackbarParams formParams;
Rectangle rcSlider; // area in which the slider axis can move
Rectangle rcTicks; // exists if the ticks are shown
Rectangle rcArea; // includes rcSlider and rcTicks (if shown)
bool bShowTicks;
int spaceSliderToTicks; // space between the head of the slider and ticks
int lenTicks;
int nTicks;
Pen penTicks;
bool bShowAreaBorder; // area border can be shown
Pen penBorder; // usually it is a dotted line with rarely positioned dots
bool bDependent;
Rectangle rcParent = new Rectangle (100, 100, 100, 100);
double coef = 0.5;
TrackbarSlider m_slider;
List<CommentToRect> m_comments = new List<CommentToRect> ();
// comments are related to rcArea
Delegate_Draw DrawTrackbar = null; // it can include drawing of the bar and the border
rcSlider field of this class is the slider rectangle which I have just mentioned, so it is a rectangle in which the slider
axis is moving. There is another rectangle which, if it exists (!), is always in front of the slider spike. (Like a mechanical
hare is always in front of greyhounds at the dog race.) This rcTicks rectangle exists only if ticks are shown. There can
be an additional space between two rectangles. For cover design the most important is rectangle rcArea which unites
those two rectangles. I decided to illustrate the Trackbar.CoverDefine() method by placing small figures of track
bars with the shown covers next to the pieces of code which produce these covers.
public override void DefineCover ()
{
if (Movable)
{
if (bDependent)
{
cover = new Cover (Area, Resizing .None); // 1 node
if (m_slider .HeadSide == Side .N || m_slider .HeadSide == Side .S)
{
cover .SetCursor (Cursors .SizeNS);
}
else
{
cover .SetCursor (Cursors .SizeWE);
}
}
else
{
if (m_slider .HeadSide == Side .N || m_slider .HeadSide == Side .S)
{
World of Movable Objects 250 (978) Chapter 10 Complex objects
cover = new Cover (Area, Resizing .WE); // 3 nodes
}
else
{
cover = new Cover (Area, Resizing .NS); // 3 nodes
}
}
}
else
{
cover = new Cover (Area, Resizing .None); // 1 node
cover [0] .SetBehaviourCursor (Behaviour .Frozen, Cursors .Default);
}
}
We are dealing with the complex objects of the Trackbar class; these
objects are involved in different individual, synchronous, and related movements; some of these objects have dependent
comments, and at the same time the covers of these objects are absolutely primitive. These are the same covers which we
used in one of the first examples of this book, in the very first case when we started to work with rectangles
(Form_Rectangles_Standard.cs, figure 3.1).
As I mentioned before, track bar can be used as an independent object but also can be used as related to some rectangle.
When it is an independent object, then its length can be changed by moving two opposite sides; these sides have to be
covered by two additional thin nodes and in this case the cover consists of three nodes. This is the case of the brown slider.
When the track bar is associated with some rectangle, then there is no individual resizing and it is enough to have one node
in the cover. I want to underline that there is no individual resizing but the length of such track bar changes synchronously
with the change of the ruling rectangle. There are two different cases of associated track bars; because in both cases the
cover consists of a single node, then these cases are undistinguishable by the cover picture. The difference is signaled by
the change of the mouse cursor over these track bars. Track bar with the blue slider is movable and the cursor over this
track bar is changed to Cursors.SizeNS, while in another case (violet slider) it is Cursors.Default which is
usually an arrow pointing into the top left corner.
While preparing this example, I organized several track bars in order to demonstrate different constructors, slider shapes,
variants of fixed and movable track bars, and so on. These track bars have the names which tell something about their
initial position and status but after it you can change these track bars in any different way. The only thing which is
determined from the beginning and cannot be changed later is their dependency status;everything else is under your full
control. One of the main rules declare that “All parameters of visibility must be easily controlled by users” and to do it there
is a special tuning form which can be called through the menu on each track bar. Movability of track bars can be changed
via commands of menus on track bars and rectangle.
Why is there any need to regulate the movability of track bars? Suppose that you organized some plotting area with a track
bar at the side of this area. You moved the track bar in such a way that it is placed exactly as you like it to be. The plotting
area is movable and resizable, so it can be positioned anywhere around the screen; the track bar will retain its relative
position throughout such movements of the plot. Plotting area can be moved by any inner point and resized by any border
point. A track bar is often positioned next to the rectangle with which it is associated, so the nodes to move a track bar and
to move / resize the plotting area often stay next to each other or even overlap. In such situation, it can easily happen that
instead of moving or resizing the plotting area, the track bar can be moved. To avoid such accidental movement of the track
bar, it can be declared temporarily unmovable, for example, via a context menu. An unmovable track bar continues to react
correctly to all movements and changes of the plotting area; it is also recognized by mover. Such track bar is simply
“frozen”, so the context menu on the unmovable track bar can be called in exactly the same way as on a movable one.
The switch to the unmovable track bar does not mean that its slider cannot move! The slider is movable regardless of the
movability of the “parent” track bar. For example, two dependent track bars in the Form_Trackbars.cs are declared
unmovable on initialization (red and violet), but all four sliders are movable.
Several pairs of objects in the Form_Trackbars.cs are involved in related movements.
• Rectangle – track bar
• Track bar – slider
• Track bar – comment
Not all of these pairs can be considered as examples of complex objects. In the complex object some element – a “child” –
exists as a part of another “parent” object and is included into its class. For the pair rectangle – track bar, there is no such
World of Movable Objects 251 (978) Chapter 10 Complex objects
inclusion. Their interaction is organized not inside the Trackbar class but via the OnMouseMove() method of the
form. When the main rectangle is changed, four interested track bars get the new value of this rectangle and adjust their
positions.*
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is Plot || grobj is RectCorners)
{
Rectangle rc = plot .PlottingArea;
trackbar_Btm .ParentRect = rc;
trackbar_Left .ParentRect = rc;
trackbar_Right .ParentRect = rc;
trackbar_Top .ParentRect = rc;
}
… …
The pair track bar – slider is a classical case of interaction between the parts of complex object. The complex object
Trackbar includes as its part an element of the TrackbarSlider class.
public class Trackbar : GraphicalObject
{
… …
TrackbarSlider m_slider;
Code includes variants for horizontal and vertical track bars; these variants differ in details but the main idea is the same, so
I’ll demonstrate only variant of horizontal independent track bar.
When a track bar is resized by moving its left side (node number 0), it informs its slider about such change. As usual,
changes are done inside the Trackbar.MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
switch (iNode)
{
case 0:
if (m_slider .HeadSide == Side .N ||
m_slider .HeadSide == Side .S)
{
if (rcSlider .Width - dx >= minTrackbarLength)
{
rcSlider .X += dx;
rcSlider .Width -= dx;
if (bShowTicks)
{
rcTicks .X = rcSlider .X;
rcTicks .Width = rcSlider .Width;
}
rcArea .X = rcSlider .X;
*
The main object of this example – plot – belongs to the Plot class and is declared in the form but there is no
declaration of any RectCorners object. Yet, this class is mentioned in the code of the OnMouseMove() method. The
Plot class is discussed further on in the chapter Applications for science and engineering and there I will explain the
exceptional need of the RectCorners class for resizing of the plotting areas.
World of Movable Objects 252 (978) Chapter 10 Complex objects
rcArea .Width = rcSlider .Width;
InformRelatedElements ();
}
}
… …
Position of any “child” is determined by some area of existence and coefficient(s) which describe the position inside this
area. Area of existence is set by “parent” while the coefficient(s) can change only as the result of individual movement of a
“child”. In the case of a slider, the area of existence is some segment of a line; the end points of this segment are
determined by the track bar.
private void InformRelatedElements ()
{
… …
Point ptLT, ptRB;
switch (m_slider .HeadSide)
{
case Side .N:
default:
ptLT = rcSlider .Location;
ptRB = new Point (rcSlider .Right, rcSlider .Top);
break;
case Side .S:
ptLT = new Point (rcSlider .Left, rcSlider .Bottom);
ptRB = new Point (rcSlider .Right, rcSlider .Bottom);
break;
… …
}
m_slider .SliderEndPoints = new Point [] { ptLT, ptRB };
}
Slider gets the new area of its existence and uses its positioning coefficient to adjust the spike position.
public Point [] SliderEndPoints
{
get { return (new Point [] { pt_LT, pt_RB }); }
set
{
int head_level;
if (sideHead == Side .N || sideHead == Side .S)
{
head_level = value [0] .Y;
pt_LT = new Point (Math .Min (value [0] .X, value [1] .X),
head_level);
pt_RB = new Point (Math .Max (value [0] .X, value [1] .X),
head_level);
ptSpike = new Point (Auxi_Geometry .CoorByCoefficient (pt_LT .X,
pt_RB .X, coef), head_level);
}
… …
DefineCover ();
}
}
When slider is individually moved, then it can be moved only along the allowed segment and throughout such movement
the positioning coefficient is calculated. This is done inside the TrackbarSlider.MoveNode() method. Well, to be
absolutely correct, I showed the code of this MoveNode() method four pages back and there is no coefficient calculation
inside that code. I have to remind that slider cover consists of a single node and in such case if the movement of the node is
allowed, then the MoveNode() method simply calls the Move() method of the same class. The coefficient calculation is
inside the TrackbarSlider.Move() method.
World of Movable Objects 253 (978) Chapter 10 Complex objects
public override void Move (int dx, int dy)
{
if (sideHead == Side .N || sideHead == Side .S)
{
ptSpike .X += dx;
coef = Auxi_Geometry.CoefficientByCoor (pt_LT .X, pt_RB .X, ptSpike.X);
}
else
{
ptSpike .Y += dy;
coef = Auxi_Geometry.CoefficientByCoor (pt_LT .Y, pt_RB .Y, ptSpike.Y);
}
}
Any Trackbar object has a single “child” of the TrackbarSlider class but it might have an arbitrary number of
comments. These comments are of the CommentToRect class with their positions related to the full rectangular area of
the track bar (rcArea). When the area of the track bar is changed, all associated comments are informed about this
change. The same InformRelatedElements() method is used to inform not only the slider but also all comments.
private void InformRelatedElements ()
{
foreach (CommentToRect cmnt in m_comments)
{
cmnt .ParentRect = rcArea;
}
… …
The track bar with the green slider (figure 10.8) has three associated comments.
void DefaultView ()
{
… …
trackbar_Right = new Trackbar (this, rc, 10, Side .W, 0.1,
SliderShape .Sexangle, 8, 10, 8, 4, true, 2, 5, 11, Draw_TrackbarR);
trackbar_Right .Slider .InnerColor = Color .Green;
trackbar_Right .AddComment (6, 0, "0", Font, 0, ForeColor,);
trackbar_Right .AddComment (6, 1, "1", Font, 0, ForeColor);
trackbar_Right .AddComment (8, 0.5, "Movable", Font, 90, ForeColor);
… …
Work of the CommentToRect objects used as elements of some complex object was already demonstrated in the example
of plot analogue so there is nothing new in moving or positioning of these comments. The track bar with the violet slider
also has a comment, but you do not see it at figure 10.8. There is one interesting feature in using this comment; I want to
write about this special case.
Any object has a visibility parameter; user can switch it ON / OFF and in this way change the view of the program at any
moment. The switch of visibility is often used in the complex applications with a lot of information on the screen; users can
hide some information (or even bigger objects) and visualize them again later. In the case of the track bar with the violet
slider, I demonstrate the semi-automatic switch of visibility.
Initially this comment is organized and hidden. Pay attention that this comment is associated with the track bar though its
appearance and disappearance is linked with the slider of this track bar. The slider is used only as an “anchor” because I
want this comment to appear near the slider.
void DefaultView ()
{
… …
trackbar_Top = new Trackbar (this, rc, -5, Side .S, 0.5,
SliderShape .Pentagon, 8, 8, 0, 6, false, 0, 0, 0, null);
trackbar_Top .Slider .InnerColor = Color .Magenta;
trackbar_Top .ShowAreaBorder = false;
trackbar_Top .Movable = false;
World of Movable Objects 254 (978) Chapter 10 Complex objects
trackbar_Top .AddComment (trackbar_Top .Slider .PositionCoefficient, -8,
trackbar_Top .Slider .PositionCoefficient .ToString ("F2"),
Font, 0, Color .Blue);
… …
First two parameters of the Trackbar.AddComment() method are the coefficients which describe comment position in
relation to the rcArea of the track bar. Because the first coefficient (x_coef) is equal to the slider positioning
coefficient and the second coefficient (y_coef) has small negative value, then this comment is positioned slightly above
the slider.
There is no comment next to violet slider at figure 10.8. I would not write special code to organize always invisible
comment, so we need to find when this comment is turned into visible. Visibility of comment is changed by using its
Visibility property; simple search shows that this comment is turned into visible inside the
UpdateTrackbarComment() method.
private void UpdateTrackbarComment ()
{
trackbar_Top .Comments [0] .NewLocationByCoefficients (
trackbar_Top .Slider .PositionCoefficient,
trackbar_Top .Comments [0] .YCoefficient);
trackbar_Top .Comments [0] .Text =
trackbar_Top .Slider .PositionCoefficient .ToString ("F2");
trackbar_Top .Comments [0] .Visible = true;
}
The text of this method shows that comment position is synchronized with the slider position, the text of the comment is
changed according to the current value of slider coefficient, and then this comment is turned into visible. Now we need to
find from where this UpdateTrackbarComment() method is called. Another search shows that it is called only from
inside the OnMouseMove() method where it is used even twice.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is TrackbarSlider && grobj .ID == trackbar_Top .Slider .ID)
{
UpdateTrackbarComment ();
}
… …
Invalidate ();
}
else
{
MoverPointInfo info = mover .PointInfoUpper (e .Location);
int iObject = info .ObjectNum;
if (iObject >= 0 &&
mover [iObject] .Source is TrackbarSlider &&
mover [iObject] .Source .ID == trackbar_Top .Slider .ID)
{
if (trackbar_Top .Comments [0] .Visible == false)
{
UpdateTrackbarComment ();
Invalidate ();
}
}
else
{
if (trackbar_Top .Comments [0] .Visible == true)
{
trackbar_Top .Comments [0] .Visible = false;
Invalidate ();
World of Movable Objects 255 (978) Chapter 10 Complex objects
}
}
}
}
The first call of the UpdateTrackbarComment() method is obvious.
When any object is moved
if (mover .Move (e .Location))
and this object is slider
GraphicalObject grobj = mover .CaughtSource;
if (grobj is TrackbarSlider
and not any slider but the particular slider identified by its id number
&& grobj .ID == trackbar_Top .Slider .ID)
then the comment is visualized and shows the positioning coefficient throughout all this movement.
The second call of the UpdateTrackbarComment() method happens when the mouse is moved around without
moving any object. This case can be even more interesting then the first one.
In the very first chapter of this book in the subsection From algorithm to working programs I mentioned that mover can
give information about objects at any point. I mentioned this possibility and this is the first example in which I use such
information. This information is given in the form of a MoverPointInfo object. It can be information about the upper
node at the specified point or about all nodes which overlap at the specified point. Information about node includes the
number of object to which this node belongs, so starting from this data you can easily get information about the whole
object. Just now I am especially interested in situation when the mouse cursor is above the particular slider, so I am
interested in information about the upper object under the cursor.
MoverPointInfo info = mover .PointInfoUpper (e .Location);
The MoverPointInfo gives me information about the existence of object under cursor. If any object exists at this
point, then the full information about this object is given. If there is any object under the mouse, then its number in the
mover queue is either zero or bigger.
int iObject = info .ObjectNum;
if (iObject >= 0 &&
Knowing this number, I can check the class of object under cursor.
mover [iObject] .Source is TrackbarSlider &&
If it is a TrackbarSlider object, then I check the identification number of this slider.
mover [iObject] .Source .ID == trackbar_Top .Slider .ID)
If it is really the needed slider and the comment is not visible yet, then the UpdateTrackbarComment() method
updates the text of comment and makes it visible. If the comment is already visible, then no actions are needed because the
comment text does not change while the slider does not move.
if (trackbar_Top .Comments [0] .Visible == false)
{
UpdateTrackbarComment ();
Invalidate ();
}
When the violet slider is released, the mouse cursor is still above this slider, so the information is still shown. But the
moment you move the cursor outside this slider, another part of the OnMouseMove() method is called. The violet slider
is not under cursor any more and the visibility of comment is switched to false.
else if (trackbar_Top .Comments [0] .Visible == true)
{
trackbar_Top .Comments [0] .Visible = false;
Invalidate ();
}
World of Movable Objects 256 (978) Chapter 10 Complex objects
The visibility of comment is switched OFF and the comment becomes invisible until the next moment when the mouse
arrives again over this slider.
Trackbar object does not look like a very complicated one. At the same time there is a whole set of parameters which
can change its view. With so many parameters to modify, the best way to do it is to have a tuning form for objects of this
class. MoveGraphLibrary.dll contains a special tuning form for track bars (figure 10.10). In the
Form_TrackbarParams.cs this tuning form can be called via the command of menu at any Trackbar object.
This Form_TrackbarParams.cs consists of two main parts: one is used to modify a slider, while another allows to change
ticks and border. Any change of parameters in this tuning form is immediately reflected in view of the modified track bar.
Slider is a small element, but the list of its changeable parameters is not too short:
• Slider direction
• Shape
• Head length
• Body length
• Tail length
• Width
• Color
• Additional color to show the edge.
Ticks have slightly shorter list of changeable parameters.
• Number of ticks
• Length
• Width
Fig.10.10 Tuning form for Trackbar objects
• Color
• Space between ticks and slider
If the border of the track bar is shown, then it is shown by a dotted line with sparsely positioned dots. There is no way to
change the style of this line, but its color can be changed.
Slider is the only mandatory part of the track bar to be shown. Ticks and border can be easily eliminated; track bars at
figure 10.8 demonstrate all possible combinations:
• Track bars with brown and green sliders have all parts in view.
• Track bar with grey slider has ticks, but there is no border.
• Track bar with blue slider has border, but there are no ticks.
• Track bars with red and violet sliders have neither border nor ticks, so these sliders look like lonely elements.
10 pages back I formulated the rules of user-driven applications and used the previous example to give some comments on
those rules except the last one. If we try to squeeze the first four rules of user-driven applications into most primitive and
compact form, then they declare that everything must be movable, resizable, and tunable. On closing an application (a
form), everything must be saved in order to be restored the next time in exactly the same way. One more rule could not be
commented in the previous example because that example was too simple for this rule.
Rule 5 All rules of user-driven applications must be implemented at all the levels beginning from the main form and up
to the farthest corners.
In the Form_Trackbars.cs (figure 10.8) everything is movable and tunable. When the form is closed, everything is saved;
the next time you call this form, you see exactly the same view as it was before closing. When you decide to modify some
track bar, you call the auxiliary Form_TrackbarParams.cs (figure 10.10). Let us check how the rules of user-driven
applications work at this level.
This form contains several groups; all these groups belong to the ElasticGroup class. All inner elements and all groups
are movable. Only the sizes of the small buttons are fixed while all other controls are resizable. Each control with
World of Movable Objects 257 (978) Chapter 10 Complex objects
comment is an object of the CommentedControl class. Such pair can be moved around by moving its control while a
comment can be moved individually and rotated. All these things allow to reconfigure the form in an arbitrary way. Each
group can be modified individually by calling its tuning form. (I don’t want to introduce the
Form_ElasticGroupParams.cs just now; this tuning form is discussed later together with the ElasticGroup class. Just
now you can click with the right button the group you want to modify and you will have no problems in using the opened
form.) In both ways – by moving the elements inside the Form_TrackbarParams.cs and by calling tuning form on any of
its groups – you can change the view of this form in any way you prefer. The next time you decide to use this form, it will
have the same view as you gave it before. So all rules of user-driven applications are implemented at this auxiliary level
and rule five of user-driven applications also works in the current example.
While writing about the Form_TrackbarParams.cs, I want to mention one thing which is
not specific to this form but is some kind of a general problem.
Fig.10.11 Menu outside
Font is only one of many parameters which you can change but the font change always the groups
causes specific problem. Rule three categorically forbids developers to add anything to
users’ actions or to give some additional interpretation to such actions. If user orders the font change, then it is a pure font
change and nothing more. In the Form_TrackbarParams.cs, you can call a small menu anywhere outside all the groups;
the second command of this menu (figure 10.11) allows to change the font for all elements in the form.
Usually I work with a small font and all the forms in my applications are prepared with the currently used small font. Many
people prefer to use bigger fonts and my default variants of the forms might be not good for such users. Anyone can use the
second command of this menu and set the font he prefers. When bigger font is ordered for controls, they usually increase
their sizes*; bigger controls inside the Form_TrackbarParams.cs will simply overlap and destroy the whole view. Then
you can use the first command of the same menu; this command will return the default view but using the font you ordered
before.
Throughout the time I can tackle the same problem in different ways. In some examples further on you’ll find two different
commands in similar cases: one command allows to restore the default view with original default font while another
command restores the default view by using previously ordered font.
*
Decades ago Microsoft placed this mine and it is still active. I’ll write about this problem further on.
World of Movable Objects 258 (978) Chapter 10 Complex objects
Filename viewers
File: Form_FilenameViewers.cs
Menu position: Graphical objects – Complex objects –Filename viewers
Next example demonstrates several solutions to a small task which often has to be solved during a design of any program
dealing with files. The task is known for decades, so it was already solved in different ways in many programs.
While working with many applications, you often have to read data from one or another file. You select a command in
menu or click a small button with familiar icon and then the standard dialog is opened. After selecting the needed file you
continue your work. If there are a lot of different files with similar data or if the time of work with this piece of data is
lengthy enough, then users often like to see some kind of reminder about the origin of the particular piece of data. The
name of the source file must be shown somewhere on the screen. As the task of showing a filename is known for years and
the name of an input file is demonstrated in many programs, then everyone is familiar with popular solutions to this
problem. Again we have a case of the well known task into which the use of movable / resizable screen elements can bring
something new.
The time when we used only a root directory with several subdirectories and the time of the filenames not longer than eight
symbols have long passed. Now people use much longer names for better reminding of what is inside each file. It is not a
rare situation when there are several hundred thousand files on the hard drive of computer. To deal with such amount of
files, people organize a lot of subdirectories, so that the path to some files can include up to 10 or 15 levels. As a result, a
filename with the full path can become very long; too long to show it on the screen in one line. Even if the screen is big
enough to show the whole filename in one line, this line will take too much valuable space. If designer decides to limit the
space for showing a filename, then he has to make a decision about the part of the full name to be shown in the allowed
space. In some cases users need to see the beginning of the path; in other cases they need the name of the file with the
names of several last subdirectories. To provide such flexibility, some mechanism of scrolling the name must be added.
Even with such scrolling, developer cannot provide the needed view of a filename for each user in all the situations.
Another possibility is to show a long name as a collection of subdirectories in a form of a staircase. This view makes the
information more obvious and shortens the length of each stair but requires to show several lines of text; each line is short
enough, but as I mentioned before, there can be 10 or more lines, so it requires a good piece of screen area. Depending on
the situation, users might be interested in several upper stairs or only in the bottom stairs of such presentation. The best
solution is to give users an easy way to regulate the size of the area through which they look at the filename and the
opportunity to select the view they prefer.
Regardless of whether you want to show the filename in one line or as a set of several lines, the area with information is
rectangular and its size must be regulated by users. Often enough an application must show the names of several files in
separate areas; in such case some explanation is needed for each area. A Rectangle_WithComments object looks
good enough for the new task but this class was especially designed for the Form_Rectangles_WithComments.cs example
and includes some features which are not needed for demonstration of filenames. The mentioned class also lacks a couple
of things which are needed for filename demonstration, so I am going to use another class. This
Form_FilenameViewers.cs example is based on using the ResizableRectangle_Commented class from the
MoveGraphLibrary.dll.
public class ResizableRectangle_Commented : GraphicalObject
{
RectangleF rc;
Resizing resize;
float wMin, wMax, hMin, hMax;
Delegate_Draw m_draw = null;
Delegate_NoParams m_change = null;
List<CommentToRect> m_comments = new List<CommentToRect> ();
The main part of any ResizableRectangle_Commented object is a rectangle. Initial sizes can be declared together
with the resizing ranges and in this way a rectangle can be non-resizable, resizable only in one direction, or in both. The
number of associated comments starts from zero and is not limited. Constructors of this class do not include any
mentioning of comments, so at the starting moment there are no comments at all.
public ResizableRectangle_Commented (RectangleF rect,
SizeF sizeMin, SizeF sizeMax,
Delegate_Draw drawmethod, Delegate_NoParams changemethod)
World of Movable Objects 259 (978) Chapter 10 Complex objects
Comments used in this new class are of the CommentToRect class; positioning and involvement of comments in
individual movements and also in synchronous and related movement with associated rectangle were already discussed in
the Form_Rectangles_WithComments.cs example. In that example I used the unicolored rectangles of the
Rectangle_WithComments class which needed only the simplest painting. Rectangles of the
ResizableRectangle_Commented
class can be used in different situations to
demonstrate different types of information;
on initialization each rectangle gets a
special method which is used for drawing
(drawmethod). In some situations the
resizing or movement of rectangle might
need not only redrawing but some
additional actions; such rectangle gets one
more method on initialization
(changemethod).
Default view of the
Form_FilenameViewers.cs (figure 10.12)
shows seven objects of the
ResizableRectangle_Commented
class. At first each object is initialized and
gets one comment which gives an idea of
how a file name is going to be shown in this
rectangle. For example, here is the code for
Fig.10.12 Default view of the Form_FilenameViewers.cs
initialization of an object marked as case A.
private void DefaultView ()
{
… …
int cxL = 2 * spaces .FormSideSpace;
int nW = 320;
SizeF [] sizeTitle = Auxi_Geometry .MeasureStrings (this, strTitle,
fntSelected);
double yCoef = -(sizeTitle [0] .Height / 2 + 2);
rrcs [0] = new ResizableRectangle_Commented (
new RectangleF (cxL, 100, nW, 30), new SizeF (50, 20),
new SizeF (1200, 1000), Draw_rectFullNameHead);
rrcs [0] .AddComment (new CommentToRect (this, rrcs [0] .MainArea,
sizeTitle [0] .Width / 2 / nW, yCoef,
strTitle [0], fntSelected, 0, Color .Green));
… …
Of the seven rectangles used in this example only the one marked as case D collaborates with a pair of additional controls
(two small buttons at the sides of this rectangle). Collaboration means the need of some method to pass the information
from rectangle to those buttons, so this rectangle is the only one which needs the use of the second method which I
mentioned before.
private void DefaultView ()
{
… …
cy = rrcs [2] .MainArea .Top + 80;
btnLeft .Location = new Point (cxL, Convert .ToInt32 (cy));
btnRight .Location = new Point (btnLeft .Right + nW + 2 * spaces .HorMin,
Convert .ToInt32 (cy));
rrcs [3] = new ResizableRectangle_Commented (
new RectangleF (btnLeft .Right + spaces .HorMin, cy, nW, 30),
new SizeF (50, 20), new SizeF (1200, 1000),
Draw_rectPartOfName, Change_rectPartOfName);
… …
It is much easier to talk about particular cases if each of them is marked in some short and clear way, so I added one letter
comment to each rectangle.
World of Movable Objects 260 (978) Chapter 10 Complex objects
private void DefaultView ()
{
… …
rrcs [0] .InsertComment (0, new CommentToRect (this, rrcs [0] .MainArea,
0.96, yCoef, "A", fntMarks, clrMarks));
rrcs [1] .InsertComment (0, new CommentToRect (this, rrcs [1] .MainArea,
0.96, yCoef, "B", fntMarks, clrMarks));
… …
Third small button in the top left corner of the form (figure 10.12) allows to select the name of any file on computer. Do
not be afraid to select any file; there is no attempt to open the file or to do anything with it. Only the name of the file is
taken and shown in different ways inside the rectangles of this form (figure 10.13).
rectangle (figure 10.14f). This small difference is only in the code of the drawing method.
Case G This case does not produce any new view but allows to choose one of the previously described cases. Because
there are no buttons next to this rectangle, then case D is excluded from the set of possible choices. There are
five command lines in the context menu which can be called on this rectangle (figure 10.15); these commands
are used to order the cases A, B, C, E, and F.
Rule three of user-driven
applications demands that user’s
commands have to be executed
just as they are without any
addition from designer of a
program, even if he (designer!)
knows what would be nice to
change in addition to the
command.
Three commands of this menu
show the information in one line;
Fig.10.15 Menu on rectangle G allows to select one of five views
two others need many lines. When
you order the change of view, then
the view of the filename is changed but not the size of rectangle. Thus, on such a change you can see
occasionally one line on top of rectangle with a lot of empty space below, or you can see only one line of the big
staircase while all other lines are out of view. In such cases you will use mouse to change the sizes of rectangle.
It is easy to add such change into the code, but do not blame a designer (me!) for doing a poor job by not thinking
about such possibilities. I know about these possibilities but I am not going to add any code for an automatic
adjustment of rectangle to the new view. It is very easy to do, but this is against the rules of user-driven
applications. When you will come to the discussion of these rules and explanations, maybe then you will change
your view on this subject.
The cases from the Form_FilenameViewers.cs are not all the variants which can be used to show the filename. For
example, the buttons for scrolling the name can be substituted by graphical objects; there are pros and cons of such
substitution, but I decided not to do it in this form. Anyway, I only wanted to show that the use of movable / resizable
objects can bring new ideas even into such a well known task as showing a filename. More variants of filename viewers
will be shown in the chapter Useful objects.
Movement restrictions
Up till now I was explaining how to organize one type of movement or another. A majority of these
movements were absolutely unrestricted; if there were some limitations on movements, then they were not the
main theme of discussion. However, there are situations when the movement restrictions become very
important. There can be different movement restrictions; some of them are discussed in this chapter.
The term movement restrictions can be applied to different situations, so it is better to begin with some definitions because a
solution for each case depends on the clear understanding of what is really meant by the term. Here is the list of situations
which are discussed in this chapter.
• General restrictions set by clipping level.
• Personal restrictions on object sizes not caused by other objects.
• Restrictions caused by the coexistence of objects on the screen. These restrictions work in two opposite situations:
prevention of overlapping and enforced keeping of one object in the area of another.
The clipping level is set at the moment of mover initiation; it can be also changed at any moment later by the
Mover.Clipping property, but, depending on the situation, there are some peculiarities.
• If no object is caught by mover at the moment of calling this property, then the mover.Clipping property can
be used to set any needed level. For example, in the Form_SetOfObjects.cs (figure 9.5) the clipping level is
changed at the beginning; this allows to move any object across the right and bottom borders of the form.
public Form_SetOfObjects ()
{
InitializeComponent ();
mover = new Mover (this);
mover .Clipping = Clipping .Safe;
… …
In this way you can move any object out of view across two borders but you can always find them later by
enlarging the form. Such type of temporarily hiding objects is a standard and widely used feature of user-driven
applications.
• If an object is already caught by mover, then the same mover.Clipping property can be used but only if the
new level widens the area of clipping. Thus, the Visual level can be changed to any other; the Safe level
can be switched to Unsafe but not to Visual.
The best way to understand and compare different clipping levels is to have all of them simultaneously. It is impossible to
do with a form but can be demonstrated with several panels and this is done in the Form_ClippingLevels.cs (figure 11.1).
File: Form_ClippingLevels.cs
Menu position: Miscellaneous – Clipping levels
There are four panels in this form; each panel is turned into a CommentedControlLTP object. Three colored panels
demonstrate three different
clipping levels. For each of
these panels, the clipping
level is fixed (each one was
determined at the moment of
initialization) and is shown
in the comment. For the
fourth panel, the clipping
level can be changed via the
commands of its context
menu.
Clipping levels can be
demonstrated only by
restrictions on moving
objects from view. The
complexity of objects does
not matter at all, so I use the
most primitive object in this
example. All of them are
only movable but not
resizable, so they have the
simplest possible covers to
provide those movements. Fig.11.1 Clipping levels for three colored panels are fixed; they are mentioned in the titles.
Clipping level for the fourth panel can be changed via menu.
Cover for a circle of the
Circle_Simple class consists of a single circular node. Usually the possibility of pressing an object for further
movement around the screen is signaled by the Cursors.SizeAll cursor shape, but for better demonstration in this
example I decided to organize three classes of elements with three different cursors, so circles use the cursor which is used
as a default one for all circular nodes.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, m_center, m_radius));
}
World of Movable Objects 266 (978) Chapter 11 Movement restrictions
Cover for a square of the Square_Simple class also consists of a single node.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0,
Auxi_Geometry .CornersOfRectangle (rc), Cursors .Cross));
cover .SetClearance (true);
}
Cover for a ring of the Ring_Simple class has to include two nodes of which the first one is transparent.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, m_center, radiusIn, Behaviour .Transparent),
new CoverNode (1, m_center, radiusOut, Cursors .SizeAll)};
cover = new Cover (nodes);
cover .SetClearance (false);
}
Simple elements of three classes are only auxiliary objects which are used to demonstrate different clipping levels. The
most interesting objects in this example are panels and the movers which work on those panels and in the form itself.
Overall there are five movers in the Form_ClippingLevels.cs example. There is no upper limit on using any number of
movers. Each mover deals only with the objects which are registered in its queue. If you want, you can easily use several
movers to deal with different objects in the same form; this has no advantages but causes problems.* One mover is enough
to regulate moving / resizing of any number of arbitrary objects at a single plane. If there are several planes and objects
cannot go from one to another, then each plane has its own mover. For this reason each page of tab control has its own
mover; there are several examples with tab controls in the big Demo application. For the same reason each panel has its
own mover for the objects residing on this panel. So each panel in the Form_ClippingLevels.cs has its own mover and
there is one more mover for the form itself. Let us start with this mover.
Mover for the form is organized in a standard way.
mover = new Mover (this);
Mover for the form does not pay any attention to whatever is going on the panels. For this mover, each panel is an ordinary
element with the standard behaviour. Four panels are turned into CommentedControlLTP objects; there is also a small
button which is transformed into SolitaryControl object. Objects of these two classes were already used in the
previous examples, though the detailed description of these classes will be given later. For now it is enough to know the
rules of their moving and resizing. The small button is not resizable but can be moved by any point near its border. Each
panel can be resized by corners and by some small areas near the middle of the sides. Panel can be moved by all other
points near borders and also by its title.
Panel with circles provides the Clipping.Unsafe type of movement for its elements. It means that circles can be
moved across any border of their panel. Panel is resizable, so theoretically there is a chance to enlarge it and see again the
circles which were previously moved over the border and dropped somewhere to the right or below. For two other sides
this is impossible. To organize an unsafe type of movement, mover has to be initialized without any parameter.
private void OnLoad (object sender, EventArgs e)
{
moverInnerCircles = new Mover (); // Clipping.Unsafe
… …
Panel with rings allows movement of its elements only inside visible area. Any part of any element can cross the border and
be out of view, but an element is moved by cursor and cursor is not allowed to cross any border. Thus, when any ring is
released, some part of it is still in view, so it can be pressed again and then the whole ring can be returned into view. The
allowed area of cursor movement is smaller than panel area, so the part of a ring which is always in view is big enough for
*
In the previous version of this book, there was one example in which several movers inside the same form organized
moving of different objects. While describing that Form_ManyMovers.cs, I tried to underline some problems which were
caused by using several movers in the same space. I even demonstrated the ways to go around some of the problems, but
later I found that I have mentioned not all bad situations. I do not see any sense in introducing the bad practice which has
no advantages at all, so I threw out that example.
World of Movable Objects 267 (978) Chapter 11 Movement restrictions
easy grabbing. I want to underline again that those restrictions on cursor movement work only throughout the time when
anything is caught by mover. When no object is caught, there are no restrictions on cursor movement. Mover gets the
Clipping.Visual level of restrictions when it is initialized with a parameter.
moverInnerRings = new Mover (panelRings); // Clipping.Visual
Mover on the panel with squares has an intermediate level of clipping which cannot be set directly by using one or another
constructor but only with the help of the Mover.Clipping property.
moverInnerSquares = new Mover (panelSquares);
moverInnerSquares .Clipping = Clipping .Safe;
Mover on the panel with the mixture of elements is initialized with the Clipping.Visual level.
moverInnerMix = new Mover (panelMixture); // Clipping.Visual
Later this clipping level can be changed to any other throughout the commands of context menu. This change from any
level to any other level is possible because at the moment of such change no object is caught by mover.
Do not be afraid to make any experiments with moving elements from view across the borders and leaving them there: there
is another context menu which can be called at any empty place and two commands of this menu allows either to refresh the
sets of elements on the panels or to reinstall the default view of the form. The last command reinstalls the original view but
remains the visibility parameters for titles and information.
The Form_ClippingLevels.cs demonstrates and allows to compare three levels of mover clipping which are applied to
different areas (in this case – panels). One of the previous examples uses different clipping levels for moving different
objects in the same area. Let us look once more at the
Form_FillTheHoles.cs (figure 11.2). When the form
is constructed, its mover is initialized in the standard
way with the form as a parameter, so the clipping level
is automatically set to Visual and any caught object
is not supposed to move across any border.
public Form_FillTheHoles ()
{
InitializeComponent ();
mover = new Mover (this);
… …
}
The real situation in this form is more interesting.
Small button with the question sign, rectangular object
with information, group with buttons inside, and big
colored boards with the holes cannot be moved across Fig.11.2 Only the “plugs” can be moved across the right and
the borders and are always visible. At least partly! bottom borders; other objects are not to cross any
Boards can be deleted by the command of their context border
menu, so there is a way to get rid of some boards. But
the “plugs” (circles and regular polygons which are used to close the holes) have no context menu, so there is no way to get
rid of the unneeded “plugs”… except throwing them across any border. Why and how it becomes possible?
In the OnMouseDown() method of the form, the mover clipping is changed to Unsafe if any “plug” is caught.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (mover .CaughtSource is Plug)
{
mover .Clipping = Clipping .Unsafe;
… …
This change of clipping level is possible because it widens the clipping area from Visual to Unsafe. The Unsafe
clipping allows to move the caught object across any border, but that level of clipping is achieved only when a “plug” is
caught, so the analysis of the situation with any object beyond the borders is limited to the situation of a “plug being out of
view”.
World of Movable Objects 268 (978) Chapter 11 Movement restrictions
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
… …
else if (grobj is Plug)
{
if (!PlugDisappeared (id))
{
… …
}
mover .Clipping = Clipping .Visual;
}
If any object is released by the mover and this object happened to be a “plug”, then the PlugDisappeared() method
is used. It identifies the “plug” via the id of the caught object and gets the rectangular area around this “plug”. If this area
is out of view, then the “plug” is deleted and the mover queue is renewed.
private bool PlugDisappeared (long id)
{
Rectangle rcClient = ClientRectangle;
bool bRet = false;
for (int i = 0; i < plugs .Count; i++)
{
if (plugs [i] .ID == id)
{
Rectangle rcPlug = Rectangle .Round (plugs [i] .RectAround);
if (Rectangle .Empty == Rectangle .Intersect (rcPlug, rcClient))
{
plugs .RemoveAt (i);
RenewMover ();
bRet = true;
}
break;
}
}
return (bRet);
}
If an object was moved across the right or lower border, the total disappearance of this object can be checked by enlarging
the form. To avoid further possibility of moving any object across the borders, the original Visual level of clipping has
to be restored. This is done at the end of the OnMouseUp() method. At this moment, it is possible to switch back from
the wider to narrower area of clipping (from Unsafe to Visual) because an object was already released by the
mover.Release() method. Without any object being caught at the moment, the mover clipping can be set at any level.
World of Movable Objects 269 (978) Chapter 11 Movement restrictions
Fig.11.7 This analogue of upper menu is used for design of a block diagram
An object that is shown inside the Form_UpperMenu.cs differs from the view of standard menus, but all the differences
are caused by the implementation of ideas of user-driven applications. An upper menu in any standard application consists
of a set of menu items that are placed in one line on a fixed distance from each other starting from the left border of a form.
Usually each menu item consists of one word; rarely more, but because of this possibility, the space between the
neighbouring menu items is wider than a space between the words inside an item, so it is always easy to distinguish one
item from another. On clicking a menu item, either a submenu is opened or a form associated with this item is called.
An object of the UpperMenu_BD class plays the same role in the block diagram but this object has features which are
standard for objects of user-driven applications.
• This “upper menu” can be moved around the screen.
• Width of “menu line” can be easily changed by moving its left and right borders.
• Items of this “upper menu” can be moved inside the “menu line”.
Regardless of the position of menu line or positions of the parts inside the menu line, this UpperMenu_BD object works as
a normal analogue of menu and its menu items either show links to submenus or allow to call associated forms. Both things
World of Movable Objects 272 (978) Chapter 11 Movement restrictions
are demonstrated in the Form_BlockDiagram.cs. In the current example, I want only to demonstrate the design of the
UpperMenu_BD class and to discuss the use of personal restrictions imposed by the parts of such an object on their
neighbours.
public class UpperMenu_BD : GraphicalObject
{
RectangleF rcMenu;
RectangleF [] rcs;
string [] texts;
Font font;
An UpperMenu_BD object occupies some rectangular area (rcMenu) inside which several rectangular areas (rcs[]) are
occupied by menu items; the sizes of these inner rectangles depend on the texts and the used font. Movability and
resizability of the UpperMenu_BD object and its inner parts are based on its cover. You can design a cover consisting of
the appropriate nodes, but if you put these nodes not in the best order, you might have problems on using such object. I
have to admit that exactly this thing happened in the original version of the UpperMenu_BD class. Some of mistakes
have to be remembered in order not to repeat them again, so I want to mention that mistake from the old version.
At the beginning the cover of the UpperMenu_BD class contained nodes in such order: first were the nodes for menu
items, then two nodes on the left and right borders of menu line, and the last big node was used to move the whole menu.
Everything worked fine, but there were occasional problems on trying to change the menu width. The first and the last
menu items are usually placed close to the ends of menu line; in such situation the nodes of these two items blocked halves
of the border nodes. Only outer halves of the border nodes were not blocked, so the possibility to catch the border node for
changing menu line was not good. The original order of nodes in the UpperMenu_BD class was caused by the easiness of
coding when the number of the pressed menu item was the same as the number of its node. But this was against the
standard practice of placing smaller nodes ahead of the bigger nodes. Those two strips on the left and right borders are the
smallest nodes in the cover; it was a wrong decision to place them after the bigger nodes of menu items. The problems on
menu line resizing demonstrated again that it is always better to obey the rules which are based on the design of many
classes.
In the new version, I solved this problem by changing the order of nodes. Now the nodes on the sides of menu line have
numbers zero (left border) and one (right border), then go the nodes on menu items (from left to right), and then the big
node covering the whole menu line.
public override void DefineCover ()
{
int nTexts = rcs .Length;
CoverNode [] nodes = new CoverNode [nTexts + 3];
nodes [0] = new CoverNode (0, new RectangleF (rcMenu.Left - 3, rcMenu .Top,
6, rcMenu .Height), Cursors .SizeWE);
nodes [1] = new CoverNode (1, new RectangleF (rcMenu.Right - 3, rcMenu.Top,
6, rcMenu .Height), Cursors .SizeWE);
for (int i = 0; i < rcs .Length; i++)
{
nodes [i + 2] = new CoverNode (i + 2, rcs [i], Cursors .Hand);
}
nodes [nTexts + 2] = new CoverNode (nTexts + 2, rcMenu);
cover = new Cover (nodes);
}
There are no restrictions on making an UpperMenu_BD object wider, but there are restrictions on moving any part of an
object and these restrictions are imposed by positions of other parts.
• The left border of menu rectangle can be moved freely outside but it cannot be moved on top of the first menu
item, so this item is always in full view.
• The same restriction with the right border as the last menu item is always shown in full.
For each menu item, there are restrictions for both left and right movements.
• If there is another menu item in the direction of movement, then there is a minimum allowed distance between the
neighbours – minspace.
• The end item (first or last) cannot be moved across the border of menu line.
World of Movable Objects 273 (978) Chapter 11 Movement restrictions
rectangular parts inside the UpperMenu_BD object allows to use the standard Cursor.Clip property when an object
is caught for moving; this is the only addition to the OnMouseDown() method of the form.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtSource is UpperMenu_BD)
{
int iNode = mover .CaughtNode;
if (1 < iNode && iNode < um .Texts .Length + 2)
{
Rectangle rcClip = um .ClipRect (iNode - 2, e .Location);
Cursor .Clip = RectangleToScreen (rcClip);
}
}
}
}
ContextMenuStrip = null;
}
Clipping is needed only if some menu item is caught for moving; this is checked by the number of the caught node. If the
clipping is needed, then the clipping area is calculated by the UpperMenu_BD.ClipRect() method; it is enough to
pass to this method the number of the pressed menu item and the cursor position at the starting moment of movement.
public Rectangle ClipRect (int iText, Point ptMouse)
{
int cxL_limit = (iText == 0) ? rcMenu .Left
: (rcs [iText - 1] .Right + minspace);
int cxR_limit = (iText == rcs .Length - 1) ? rcMenu .Right
: (rcs [iText + 1] .Left - minspace);
return (Rectangle .FromLTRB (ptMouse .X - (rcs [iText] .Left - cxL_limit),
rcMenu .Top, ptMouse .X + (cxR_limit - rcs [iText] .Right),
rcMenu .Bottom));
}
Though an UpperMenu_BD object consists of several parts which can be moved individually, it is not a complex but a
simple object. Parts of this object have no covers of their own. There is a single cover for all the parts and the object is
registered in the mover queue with a simple Mover.Add() method.
Similar looking example of an upper menu could be designed in a different way with each menu item represented by an
individual screen element and a set of such elements united into a complex object. In such case the same movement
restrictions will be caused by other objects. Such situations are analysed in the next section.
World of Movable Objects 275 (978) Chapter 11 Movement restrictions
File: Form_SlidersInRectangle.cs
Menu position: Movement restrictions – Sliders in rectangle
Rectangle of the ResizableRectangle class and two sliders of the SliderInRectangle class are separate
objects, but at the moment of initialization sliders are informed about the area of their existence.
private void OnLoad (object sender, EventArgs e)
{
… …
rr = new ResizableRectangle (new RectangleF (100, 100, 400, 300),
new Size (100, 100), new Size (500, 500), DrawRect, ChangeRect);
sliderHor = new SliderInRectangle (rr .Rectangle, LineDir .Hor, 0.4,
new Pen (Color .Blue));
sliderVer = new SliderInRectangle (rr .Rectangle, LineDir .Ver, 0.8,
new Pen (Color .Green, 3));
… …
The resizable rectangle gets the initial size; two other parameters define minimum and maximum alowed sizes for this
rectangle. There are also two methods passed as parameters. The method for drawing is very primitive; as you see from
figure 11.8, the rectangle is shown with a thin grey line. The second method is used whenever the rectangle is moved or its
sizes are changed; this method is an important part of discussion around this example and we’ll return to this method a bit
later.
On initialization, slider gets a rectangle on which to
reside, line direction, and the positioning coefficient
which determines the initial position inside the
rectangle. This coefficient is from the [0, 1]
range with 0 value associated with the left or top
border, while the right and bottom borders are
associated with coefficient 1.
Sliders can be moved only inside the rectangle. To
be movable, they must precede the rectangle in the
mover queue.
private void RenewMover ()
{
… …
mover .Add (sliderHor);
mover .Add (sliderVer);
mover .Add (rr);
… … Fig.11.8 Sliders in rectangle
}
At the end of the previous section I wrote that restrictions imposed by one object on the movements of other objects must be
taken into consideration inside the OnMouseMove() method of the form, but there is nothing of this kind in the
Form_SlidersInRectangle.OnMouseMove() method.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Update ();
Invalidate ();
}
}
In the current example such restrictions are imposed behind the curtains via the ChangeRect() method which was passed
as one of parameters during the rectangle initialization. While anything is moved, the Mover.Move() method calls the
MoveNode() method of the class to which the caught object belongs. When rectangle is moved or resized, the
ResizableRectangle.MoveNode() method is called. If the current rectangle was initialized with some method to
be called on its change, then this method is called. In our case it is the ChangeRect() method which informs both sliders
about the new area of rectangle.
World of Movable Objects 278 (978) Chapter 11 Movement restrictions
private void ChangeRect ()
{
sliderHor .Area = rr .Rectangle;
sliderVer .Area = rr .Rectangle;
}
In the SliderInRectangle.Area property, there are two cases for horizontal and vertical sliders. These variants are
similar, so let us go step by step through the case of horizontal sliders. The correct order of statements inside the
SliderInRectangle.Area property is very important.
• First, the positioning coefficient of the slider in the current rectangle is calculated.
public RectangleF Area
{
set
{
float coor;
double coef;
if (dir == LineDir .Hor)
{
coef = Auxi_Geometry.CoefficientByCoor (rc.Top, rc.Bottom, ptLT.Y);
• Then the new value of rectangle is applied.
rc = value;
• Now the calculated coefficient is used to determine the position in the changed rectangle and the two end points of
the slider are calculated.
coor = Auxi_Geometry.CoorByCoefficient (rc.Top, rc.Bottom, coef);
ptLT = new PointF (rc .Left, coor);
ptRB = new PointF (rc .Right, coor);
The above shown expresions are for the case of horizontal sliders; the vertical sliders are moved in similar way.
else
{
coef = Auxi_Geometry.CoefficientByCoor (rc.Left, rc.Right, ptLT.X);
rc = value;
coor = Auxi_Geometry.CoorByCoefficient (rc .Left, rc .Right, coef);
ptLT = new PointF (coor, rc .Top);
ptRB = new PointF (coor, rc .Bottom);
}
DefineCover ();
}
File: Form_SlidersChangeableOrder.cs
Menu position: Movement restrictions –
Sliders with changeable order
Next example includes more sliders of each direction
(figure 11.9), but all of them are still movable inside the whole
area of rectangle. This means that these sliders do not pay
attention to the positions of other sliders; the only important
thing for them is the parental rectangle. So everything is going
exactly as in the previous example; only the whole set of
sliders must be informed about any change of rectangle. This
means that the main change is in the ChangeRect()
method which is passed as a parameter on initialization of the
resizable rectangle.
private void ChangeRect ()
Fig.11.9 Moving can change the order of sliders
{
foreach (SliderInRectangle slir in horSliders)
{
slir .Area = rr .Rectangle;
World of Movable Objects 279 (978) Chapter 11 Movement restrictions
}
foreach (SliderInRectangle slir in verSliders)
{
slir .Area = rr .Rectangle;
}
}
There are also some changes in the RenewMover() and OnPaint() methods of the form, but these are the results of
the increased number of objects.
File: Form_SlidersUnchangeableOrder.cs
Menu position: Movement restrictions – Sliders with unchangeable order
The situation when there are multiple sliders in the plotting area and these sliders put limits on the movements of their
neighbours is common enough. This will be the case of the third example in a row. Sliders of each type (direction) make
up their own List.
public partial class Form_SlidersUnchangeableOrder : Form
{
Mover mover;
ResizableRectangle rr;
List<SliderInRectangle> horSliders = new List<SliderInRectangle> ();
List<SliderInRectangle> verSliders = new List<SliderInRectangle> ();
The view of this example (figure 11.10) is similar
to the previous one and in order to understand the
difference you need to start moving any slider.
The main difference is that no slider can move
over its neighbour, so each slider has its own
limits for individual movement; these limits also
depend on whatever movement the neighbours
have made before. All interesting things are
going to happen inside the OnMouseDown()
method when any slider is caught by mover. At
last we came to the case where the restrictions
imposed by one object on another are analysed
inside the OnMouseDown() method of the
form, so this method is definitely bigger than in
the previous case. It is bigger but fairly simple.
Because the analysis of situation for horizontal
and vertical sliders is very much alike, it is Fig.11.10 Movements of these sliders are restricted by neighbours,
enough to look at the code for one of them; I’ll so the order of sliders is never changed.
give comments for horizontal sliders.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button) &&
e .Button == MouseButtons .Left &&
mover .CaughtSource is SliderInRectangle)
{
SliderInRectangle sliderPressed =
mover .CaughtSource as SliderInRectangle;
long id = sliderPressed .ID;
int iPressed;
if (sliderPressed .Direction == LineDir .Hor)
{
First, the caught slider has to be identified. It is identified not only by its id (this is easy), but, based on this id, the position
of the caught slider (iPressed) in the List of all sliders of the same direction must be determined.
for (iPressed = horSliders .Count - 1; iPressed >= 0; iPressed--)
{
World of Movable Objects 280 (978) Chapter 11 Movement restrictions
if (id == horSliders [iPressed] .ID)
{
break;
}
}
The order of the caught slider in the List is important because for the first slider the upper boundary of its movement is
determined by the upper border of rectangle while for all others it is determined by coordinate of the previous slider.
if (iPressed > 0)
{
sliderPressed.Limit_Top = horSliders [iPressed-1] .Ends [0] .Y;
}
else
{
sliderPressed .Limit_Top = rr .Top;
}
The same happens with the lower border of movement: for the last slider in the List it is determined by the bottom side
of rectangle while for all other sliders it is determined by coordinate of the next slider.
if (iPressed < horSliders .Count - 1)
{
sliderPressed .Limit_Bottom =
horSliders [iPressed + 1] .Ends [0] .Y;
}
else
{
sliderPressed .Limit_Bottom = rr .Bottom;
}
As shown in the above code of the OnMouseDown() method, at the moment when any horizontal slider is caught for
moving, the new limits of this movement are set through the SliderInRectangle.Limimt_Top and
SliderInRectangle.Limit_Bottom properties.
public float Limit_Top
{
get { return (fTop); }
set { fTop = value; }
}
public float Limit_Bottom
{
get { return (fBottom); }
set { fBottom = value; }
}
The limits are set and the movement of the slider already started, so these limits must work in the
SliderInRectangle.MoveNode() method. The code of this method was already shown several pages back, but for
easiness of understanding I’ll repeat here the needed part of this method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (dir == LineDir .Hor)
{
float cyNew = ptLT .Y + dy;
if (Math .Max (fTop, rc .Top) <= cyNew &&
cyNew <= Math .Min (fBottom, rc .Bottom))
{
Move (dx, dy);
World of Movable Objects 281 (978) Chapter 11 Movement restrictions
bRet = true;
}
}
… …
The proposed new slider position is checked against the limits and the movement is allowed only if the proposed position is
inside the allowed range. In this piece of code you see that the proposed new popsition is checked not only against the
allowed range but also against the rectangle borders. This is done because the same MoveNode() method is used in all
situations with and without neighbouring sliders.
Figure 11.11 shows a view of the Form_CommentToRectLimited.cs. The form demonstrates three cases of putting the
restriction on comment movements. All comments in this form are related to one or another rectangle. Let us start with
initialization of all rectangles and comments.
In the top left corner of the form you can see a cyan rectangle with which a comment “With rcFixed” is associated; grey
frame shows an area in which the central point of this comment can move. This is a very simple example with the most
unusual feature: the cyan rectangle and the area for moving this comment are fixed. They are NOT MOVABLE!
On the very first page of this book you can read a statement that “there is not a single unmovable object in [all the
examples]”. This statement is absolutely correct for all the examples and it was so from the very first moment when I began
to work on this book. But in this form I decided to step aside from this rule and to include a couple of unmovable objects
among all other movable. The main rule of user-driven applications demands all objects to be movable. Further on I’ll write
more about the importance of this rule and about the consequences of making any exceptions, but words are only words and
a simple demonstration can be better than a lot of them. Here I decided to demonstrate why movable and unmovable
elements cannot be mixed. They simply do not want to coexist. Everything else in this form is movable and this
unmovable element makes a mess. With all other elements you can do everything; a single even primitive unmovable
object makes user’s life much harder. At any moment you have to remember about this unmovable element which happens
to be on the way each time you try to move any other element in this form. When later you come to the discussion of rules
for user-driven applications, just remember this example.
The unmovable cyan rectangle rcFixed is the only object which is never changed, so it does not need to be saved and
restored. Area of existence for comment to this rectangle – rcFixed_CmntLimit – is also fixed; two rectangles are
different and partly overlap.
public Form_CommentToRectLimited ()
{
… …
rcFixed = new Rectangle (40, 110, 200, 140);
rcFixed_CmntLimit =
Rectangle .FromLTRB (rcFixed .Left + rcFixed .Width / 2,
rcFixed .Top - 80, rcFixed .Right + 140,
rcFixed .Bottom + 50);
}
Comment to the fixed rectangle is not fixed and is organized together with all other elements.
private void OnLoad (object sender, EventArgs e)
{
… …
cmntToFixed = new CommentToRectLimited (this, rcFixed, 50, 0.8,
"With rcFixed", rcFixed_CmntLimit);
… …
Mover has nothing to do with ordinary rectangles; of these three elements, the comment is the only one which needs to be
registered with the mover.
private void RenewMover ()
{
… …
mover .Add (cmntToFixed);
… …
The second case is more interesting as there is a comment associated with a movable / resizable rectangle (the green one in
the bottom left corner of figure 11.11). The allowed area of movement for this comment is set inside this green rectangle
and this area is slightly smaller than the “parent” rectangle. For better understanding, this area is shown with a thin grey
frame.
int dxShift = 12;
int dyShift = 8;
private void OnLoad (object sender, EventArgs e)
{
… …
rrLime = new ResizableRectangle (new Rectangle (100, 350, 200, 180),
new Size (100, 100), new Size (1200, 1200),
World of Movable Objects 283 (978) Chapter 11 Movement restrictions
Draw_rrLime, Change_rrLime);
RectangleF rc_rr = rrLime .Rectangle;
RectangleF rr_Limit = RectangleF .FromLTRB (rc_rr .Left + dxShift,
rc_rr .Top + dyShift,
rc_rr .Right - dxShift, rc_rr .Bottom - dyShift);
cmnt_rrLime = new CommentToRectLimited (this, rc_rr, 0.3, 0.7,
"Inside resizable", Color .Blue, rr_Limit);
… …
Any object of the ResizableRectangle class is associated with two methods of which one is mandatory and another is
optional. The mandatory method is used for drawing a rectangle; the optional method is used on any change in location or
size of rectangle to inform the associated elements. In case when a resizable rectangle is not associated with any other
object, the second method is not needed. If a resizable rectangle is associated with other objects, then this method is used to
pass the new boundaries of rectangle to those objects. In the case of green rectangle, the boundaries of the area allowed for
moving the comment also depend on the boundaries of the “parent” rectangle”, so the coordinates of both rectangles must
be forwarded to the comment.
private void Change_rrLime ()
{
RectangleF rc = rrLime .Rectangle;
RectangleF rc_Limit = RectangleF .FromLTRB (rc .Left + dxShift,
rc .Top + dyShift, rc .Right - dxShift, rc .Bottom - dyShift);
cmnt_rrLime .ParentRect = rc;
cmnt_rrLime .MovementArea = rc_Limit;
}
In some situations you can observe a strange movement of this comment. Make the green rectangle really big; then put the
comment at the right border of the area allowed for its movement and close to its top right corner. Now start squeezing the
rectangle by moving the right border. You decrease only the width of the green rectangle; the height of this rectangle is not
changed, but the comment moves not only to the left (this is expected as it must stay inside the squeezing rectangle) but also
slightly down. Is it some kind of an error in the code?
Certainly, not; it is the demonstration of the algorithm under which the trying to escape comment is returned into the
rectangular area allowed for its existence. When the resizable rectangle is squeezed, then the allowed area for comment is
squeezed also; if the central point of comment happens to be outside this allowed area, then the comment is moved into the
allowed area. The enforced relocation of comment into the allowed area is always done along the line between the central
point of comment and the center of rectangle. This is an inclined line and the inclination is bigger for the higher rectangle;
that is why I advised to make a rectangle big enough at the beginning. (The inclination depends on the ratio between the
height and the width, but you need some width at the beginning to start the horizontal squeezing.)
The third case differs from the previous cases in two aspects:
• There are several comments associated with one resizable rectangle.
• Each of these comments has its own area of allowed movement.
These four areas are not shown at figure 11.11, but the drawing of their
borders can be easily switched ON and OFF via the menu on the yellow
rectangle. Suppose that you have some game board for four players or you
want to show a map of some area and you want to comment the sides of this
map. As any other element on the screen, those comments are movable, but
you do not want them to be movable absolutely freely; you want these
comments to be easily associated with their sides of the board. Figure 11.12
demonstrates the rectangle, all four comments, and the boundaries of the
areas in which they are allowed to move. These areas start from the sides of
rectangle and go beyond any real place to which a comment can be moved.
Also in this case the “parent” rectangle and the rectangle of allowed
movement do not even overlap. In all other aspects this example is similar
to the previous one. Fig.11.12 Comments can be moved on the
screen, but each one is always
First, a resizable rectangle is initialized; then four comments associated with shown next to the proper side
this rectangle get their initial positions.
World of Movable Objects 284 (978) Chapter 11 Movement restrictions
private void OnLoad (object sender, EventArgs e)
{
… …
rrYellow = new ResizableRectangle (new Rectangle (600, 250, 200, 200),
new Size (80, 80), new Size (2000, 2000),
Draw_rrYellow, Change_rrYellow);
RectangleF rc = rrYellow .Rectangle;
cmnt_North = new CommentToRectLimited (this, rc, 0.5, -40, "North",
Color .Blue, RectangleF .FromLTRB (rc .Left, rc .Top - 2000,
rc .Right, rc .Top));
cmnt_South = new CommentToRectLimited (this, rc, 0.5, 40, "South",
Color .Red, RectangleF .FromLTRB (rc .Left, rc .Bottom,
rc .Right, rc .Bottom + 2000));
cmnt_West = new CommentToRectLimited (this, rc, -40, 0.5, "West",
Color .Green, RectangleF .FromLTRB (rc .Left - 2000,
rc .Top, rc .Left, rc .Bottom));
cmnt_East = new CommentToRectLimited (this, rc, 40, 0.5, "East",
Color .Orange, RectangleF .FromLTRB (rc .Right, rc .Top,
rc .Right + 2000, rc .Bottom));
… …
On any change in location or size of the “parent” rectangle, all four comments must be informed about this change and their
allowed areas for movement must be changed also.
private void Change_rrYellow ()
{
RectangleF rc = rrYellow .Rectangle;
cmnt_North .ParentRect = cmnt_South .ParentRect =
cmnt_West .ParentRect = cmnt_East .ParentRect = rc;
cmnt_North .MovementArea = RectangleF .FromLTRB (rc .Left, rc .Top - 2000,
rc .Right, rc .Top);
cmnt_South .MovementArea = RectangleF .FromLTRB (rc .Left, rc .Bottom,
rc .Right, rc .Bottom + 2000);
cmnt_West .MovementArea = RectangleF .FromLTRB (rc .Left - 2000, rc .Top,
rc .Left, rc .Bottom);
cmnt_East .MovementArea = RectangleF .FromLTRB (rc .Right, rc .Top,
rc .Right + 2000, rc .Bottom);
}
Now all elements are ready and we can look at the movements of all these comments inside their allowed rectangular areas.
Usually, when there are some restrictions on the movement of an object and this object comes to the border of the allowed
area, then this object is stopped while the mouse cursor continues to move on. However, in the case of a comment we move
only its central point while the comment is simply painted around this point. When we have a movable point and a
rectangular area of its allowed movement, it is easy to calculate the allowed area for cursor movement, so that cursor is not
moved farther on when the comment is stopped. For correct calculations you need only to take into consideration the
difference between the central point of comment and the mouse point at the moment when the comment is caught for
movement; then add these initial shifts along two axes to the allowed area for comment movement.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, true))
{
GraphicalObject grobj = mover .CaughtSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is CommentToRectLimited)
{
CommentToRectLimited cmnt = grobj as CommentToRectLimited;
int dx = e .X - cmnt .Location .X;
int dy = e .Y - cmnt .Location .Y;
RectangleF rcLimit = cmnt .MovementArea;
World of Movable Objects 285 (978) Chapter 11 Movement restrictions
RectangleF rcClip = RectangleF.FromLTRB (rcLimit .Left + dx,
rcLimit .Top + dy,
rcLimit .Right + dx,
rcLimit .Bottom + dy);
Cursor .Clip = RectangleToScreen (Rectangle .Round (rcClip));
}
}
}
ContextMenuStrip = null;
}
This easy technique of stopping the mouse cursor at the moment when the caught object cannot be moved farther on can be
used in this case because the position of the movable element is described by a point and its allowed area for movement is a
rectangle. In general, when an object has some more interesting shape than a point and the limits of its movement can be
also of an arbitrary shape, another technique can be used; this technique is described at the end of this chapter in the section
Overlapping prevention.
Though the Form_CommentToRectLimited.cs is a simple one, it is designed according to all the rules of user-driven
applications. It means that all visibility parameters for comments and rectangles can be easily changed by users via two
context menus while the parameters of area with information can be changed via its tuning form. It also means that all the
parameters are saved on closing the form and are restored when the form is opened again. There is one exception in
changing the parameters of visibility in this form; not surprisingly at all that this exception is related to exception of one
element from movement.
The call of the proper context menu is based on the information received from mover. The cyan rectangle in the top left
corner of the form (figure 11.11) is left unmovable; it is not registered with the mover, so mover does not know about the
existence of this element and cannot inform about it when the mouse is pressed inside the area of that rectangle. Thus, this
rectangle is always in the same place and its color cannot be changed.
I do not use the CommentToRectLimited class too often, but in the chapter User-driven applications there is an
example which demonstrates a very interesting case of using this class.
Balls in rectangles
File: Form_BallsInRectangles.cs
Menu position: Movement restrictions – Balls in rectangles
When the Form_BallsInRectangles.cs is opened for the first time, there is only one board with the balls in view, so
figure 11.13 was prepared after some manipulations. Adding of new rectangular area with balls is done with the help of a
special group. Certainly, this is a group of the ElasticGroup class, so you can change its visibility parameters via its
tuning form and change the view by
moving inner elements. Two controls
inside the group are resizable.
There can be between two and 15 balls
on each board. Usually balls on a
board have different radii but it is
possible to give the same radius to all
of them.
• Balls can be moved only
inside their “parent” board.
• Boards can be moved around
and resized. Board squeezing
can push the balls closer to
each other.
These two statements about balls
behaviour show that the restrictions on
their movements are based not on their
resizing limitations but on position and
size of an object from another class.
Fig.11.13 Balls in rectangles
World of Movable Objects 286 (978) Chapter 11 Movement restrictions
Let us start with the simplest objects of this example – the balls.
An object of the Ball class is very simple. Its position is determined by the central point (m_center), radius
(m_radius) is fixed at the moment of initialization.
public class Ball : GraphicalObject
{
PointF m_center;
float m_radius;
Color clrEdge, clrLight;
double xCoef, yCoef;
float cxLeft, cxRight, cyTop, cyBtm; // center can move only inside these limits
Four coordinates determine a rectangle in which the central point of a ball can move. Two colors are used to make the view
of these balls more interesting. They look like some light is shining on them somewhere from top and left. The position of
the bright spot is described by two coefficients; both of them are from the [0, 1] range with the [0, 0] value associated with
the top left corner of the square around the ball. By default, all balls are initialized with a pair of coefficients (0.33, 0.33).
Color of the ball changes from the clrLight at this point to clrEdge at the ball border.
As any non-resizable circle, a ball has a very simple standard cover consisting of a single circular node.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, m_center, m_radius));
}
The Ball.Move() method is extremely simple as only the relocation of the central point is required.
public override void Move (int dx, int dy)
{
m_center += new SizeF (dx, dy);
}
The Ball.MoveNode() method is the place where the proposed move of the ball center is checked against the allowed
area.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float cxNew = m_center .X + dx;
float cyNew = m_center .Y + dy;
if (cxLeft <= cxNew && cxNew <= cxRight)
{
Move (dx, 0);
bRet = true;
}
if (cyTop <= cyNew && cyNew <= cyBtm)
{
Move (0, dy);
bRet = true;
}
}
return (bRet);
}
Balls live inside rectangular areas, so it is time to look at the details of area design.
The important parameters of the rectangular board (class Rectangle_WithBalls) are the area itself and the List of
balls that reside on this board. A board also has minimum and maximum sizes with both width and height allowed to
change inside the [100, 500] range.
World of Movable Objects 287 (978) Chapter 11 Movement restrictions
public class Rectangle_WithBalls : GraphicalObject
{
RectangleF rc;
List<Ball> balls = new List<Ball> ();
SolidBrush brush;
bool bSameSize;
RectRange range = new RectRange (100, 500, 100, 500);
Rectangular board can be resized in the simplest and expected way by its corners and sides; this is provided by a standard
cover.
public override void DefineCover ()
{
cover = new Cover (rc, Resizing .Any);
}
This cover also provides the board moving by any inner point. While the board is moved, all its balls must move
synchronously.
public override void Move (int dx, int dy)
{
rc .Location += new SizeF (dx, dy);
foreach (Ball ball in balls)
{
ball .Move (dx, dy);
}
SetAreas ();
}
A ball can move by itself or as a result of synchronous movement with its “parent”. In both cases the Ball.Move()
method is called, but for the enforced movement it is not enough. Any ball can move only inside the board. When the area
has changed, a ball must be informed about this change; the SetAreas() method informs all the associated balls about
their new areas. Position of a ball is determined by the location of its center, so for each ball the allowed area of its
movement is determined by the board rectangle but deflated by the radius of a ball on each side.
private void SetAreas ()
{
foreach (Ball ball in balls)
{
ball.SetArea (rc.Left + (ball.Radius + 1), rc.Right - (ball.Radius + 2),
rc.Top + (ball.Radius + 1), rc.Bottom - (ball.Radius + 2));
}
}
Resizing of a board is done by its MoveNode() method. First, personal restrictions on the moving sizes must be taken
into consideration: the sizes of board must always stay inside the allowed range. If the proposed size of a board is inside the
range, then the side can be moved. Here is the part of the MoveNode() method for moving the top left corner.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float hNew, wNew;
… …
else if (iNode == 0) //LT corner
{
hNew = rc .Height - dy;
if (range .MinHeight <= hNew && hNew <= range .MaxHeight)
{
MoveBorder_Top (dy); // SetAreas() inside
bRet = true;
}
World of Movable Objects 288 (978) Chapter 11 Movement restrictions
wNew = rc .Width - dx;
if (range .MinWidth <= wNew && wNew <= range .MaxWidth)
{
MoveBorder_Left (dx);
bRet = true;
}
}
… …
On changing any size of the board, the SetAreas() method must be called to inform all the balls about the change. For
example, here is a simple method for moving the upper side of the board.
private void MoveBorder_Top (int dy)
{
rc .Y += dy;
rc .Height -= dy;
SetAreas ();
}
As I showed before, the SetAreas() method sends the value of new area to each ball inside; this area is individual
because it slightly depends on the radius of particular ball.
Each ball gets its own new area of existence through the Ball.SetArea() method. Four parameters of this method
define the sides of the new allowed area for the ball. Fields inside the ball are renewed with these new values, but this is not
enough. The current position of the ball is checked against the new area; if it happens that the ball is outside the allowed
rectangle, then the ball is pushed into the new area.
public void SetArea (float cxL, float cxR, float cyT, float cyB)
{
cxLeft = cxL;
cxRight = cxR;
cyTop = cyT;
cyBtm = cyB;
m_center = new PointF (Math .Min (Math .Max (cxLeft, m_center.X), cxRight),
Math .Min (Math .Max (cyTop, m_center .Y), cyBtm));
DefineCover ();
}
I want to attract your attention to differences in two similar examples.
The first one is the green rectangle with comment from the previous example Form_CommentToRectLimited.cs
(figure 11.11). Comment position is described by its central point, so it is an example of interaction between rectangle and
point. Any change in position or size of rectangle affects the point (comment) and causes its movement because the relative
position of the point has to be retained. For this purpose, the point has two positioning coefficients.
In the current Form_BallsInRectangles.cs example we also have the interaction between rectangle and point (the ball
central point), but the main idea of this interaction is different. Instead of the retaining relative position we have an idea of
minimal movement for the point. Any change in position or size of rectangle changes the area allowed for point, but the
consequences vary. If the point is still in the allowed area, then it does not move at all; if it is found to be outside the area,
then it makes the minimal movement to return into the allowed area. This rule of maximum allowed laziness results in
slightly different movement when the ball has to be moved. The enforced relocation is organized not along the line to the
center of rectangular area but to the nearest point inside rectangle, so when the board is squeezed by some side and any ball
is enforced to move, then it moves either horizontally or vertically.
Two remarks about the Form_BallsInRectangles.cs.
1. This example is designed according to the rules of user-driven applications but one rule is implemented not
entirely. It is about the restoring of exactly the same view which the form had at the moment of the previous
closing. The overall view of the form, the group, and information are restored without any changes. The number
of the balls inside each rectangle is exactly the same as it was, but I decided to ignore the positions, colors, and
sizes of the balls, so the restored rectangle has the new set of balls inside, though their number is the same. It is not
a problem to write the code and organize the exact restoring of the view, but it has nothing to do with the
discussion of the movement restrictions, so I use a lighter version of restoration. (When you write an example with
lazy elements, it is easy to become lazy yourself.)
World of Movable Objects 289 (978) Chapter 11 Movement restrictions
2. I have some doubts about the right place for this example in the book. From the point of movement restrictions,
this example is similar to the Form_UpperMenu.cs which is included into the previous section. But the current
example is very close in idea and design to the next one which has to be in the current section, so I decided to place
these two examples one after another.
Movements of any object are analysed in the MoveNode() method of its class. Our balls have restrictions imposed by the
board and there are also other balls which can block the ball movements. All these cases must be analysed inside the
Ball_SCNO.MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF ptNew = Center + new SizeF (dx, dy);
if (MovementArea .Contains (ptNew))
{
foreach (Ball_SCNO ball in Neighbours)
{
if (ball .ID != ID &&
ball .EdgeColor == EdgeColor &&
Auxi_Geometry .Distance (ball .Center, ptNew) <=
ball .Radius + Radius)
{
return (false);
}
}
Move (dx, dy);
bRet = true;
}
}
return (bRet);
}
The proposed new ball position is easily calculated
PointF ptNew = Center + new SizeF (dx, dy);
The move can happen only if this new position is inside the allowed area.
if (MovementArea .Contains (ptNew))
Even if this point is inside the allowed area, this is not the end of checks; the proposed new position must be checked
against all other balls on the board. An overlapping of two balls is easy to detect as in this case the distance between their
centers must be less than the sum of their radii. Overlapping of the balls with different colors is allowed, so the check is
needed only against the balls of the same color.
foreach (Ball_SCNO ball in Neighbours)
{
if (ball .ID != ID &&
ball .EdgeColor == EdgeColor &&
Auxi_Geometry .Distance (ball .Center, ptNew) <=
ball .Radius + Radius)
{
The checking works; the balls of the same color do not overlap, but I am not satisfied with the overall behaviour. Each time
when the ball cannot move farther on it stops but the mouse continues its movement. Because there are a lot of restrictions
then this happens too often. Let us see if anything can be done to improve the program behaviour.
World of Movable Objects 291 (978) Chapter 11 Movement restrictions
Overlapping prevention
In the previous subsection we have an example of overlapping prevention. The first several examples in this section also
deal with the overlapping prevention, but absolutely different technique is going to be used. In the previous examples an
object could be stopped, but the mouse continued to move. In all the examples of this section, if an object is stopped, then
the mouse also does not move farther on and stays exactly on that spot of an object where it first caught it. If you need to
organize such behaviour for an object moving inside the rectangular area, then it is possible to calculate the rectangular
clipping area for a mouse and use the standard clipping procedure. If an object is supposed to move inside the non-
rectangular area, then the standard clipping is of no help. For such cases the Mover class has something else in its store.
Other examples at the end of this section will show that this new technique can be used for the purpose which is opposite to
overlapping prevention: it will help to keep an object on track.
Adhered mouse
File: Form_AdheredMouse.cs
Menu position: Movement restrictions – Adhered mouse
The Form_AdheredMouse.cs looks very simple
with only few objects in view. There is a board in
the form of a regular polygon, a small circle which
can be moved only inside this board, and one
control to change the number of board vertices
(figure 11.15). There is also a standard pair of
elements consisting of a small button and
associated information.
This board belongs to the
RegularPolygonWithCircle class; the
circle belongs to the
CircleInsideConvexPoly class. Though
the name of the board informs that its form is a
regular polygon and the name of the circle hints
about a convex polygon, this is not a mistake. The
circle is designed to move inside a convex polygon
without crossing its borders; the use of this object
inside a regular polygon is a private case.
This is not the first class of regular polygons in my
examples, so the set of needed fields is well Fig.11.15 The circle can move only on the board
known. To describe a regular polygon, you need
its central point (ptC), radius of vertices (m_radius), number of vertices (nVertices), angle of the first vertex
(m_angle), and some brush (brush). There is also a pen to draw the border (m_pen).
public class RegularPolygonWithCircle : GraphicalObject
{
PointF ptC;
float m_radius;
int nVertices;
double m_angle;
SolidBrush brush;
Pen m_pen;
CircleInsideConvexPoly m_circle;
float radCircle = 30;
The board with a circle is a classical complex object: whenever the board is moved, its circle moves synchronously, but the
circle can also move individually. If during its own movement the circle runs into the border, it stops and the cursor stops
also. From the moment the circle is pressed by the left button, the cursor is adhered to the same point on the circle until the
mouse button is released. This is an unusual cursor behaviour because in all the previous examples whenever an object was
stopped by some restriction, the cursor continued its movement. The new cursor behaviour is more natural, but to obtain
such natural behaviour something has to be done.
World of Movable Objects 292 (978) Chapter 11 Movement restrictions
Among the parameters of the RegularPolygonWithCircle constructor you can find several expected values to
describe a regular polygon but you can also find two strange parameters which constructors for other classes of regular
polygons or circles did not use. One of these parameters is the form in which the whole work is done; another is the mover
which supervises the moving process.
public RegularPolygonWithCircle (Form frm, Mover mvr, PointF center, float rad,
int vertices, double angleDegree, Color clrPoly,
PointF ptCircle, Color clrCircle)
{
ptC = center;
m_radius = Math .Max (Math .Abs (rad), 200);
nVertices = Math .Min (Math .Max (3, Math .Abs (vertices)), 12);
m_angle =
Auxi_Convert .DegreeToRadian (Auxi_Common .LimitedDegree (angleDegree));
brush = new SolidBrush (clrPoly);
m_pen = new Pen (Color .DarkGray);
if (!Auxi_Geometry .PointInsideConvexPolygon (ptCircle, CircleAreal))
{
ptCircle = ptC;
}
m_circle = new CircleInsideConvexPoly (frm, mvr, ptCircle, radCircle,
clrCircle, CircleAreal);
}
You can find the form as a parameter in constructors of some objects which use texts; this is done because the size of a text
must be calculated. If there is no text in an object, then its constructor does not need a form among the parameters. Mover
is not needed among the parameters at all because an object does not need to know whether it is movable or not. Mover
supervises the moving / resizing of all objects from its queue, but objects cannot regulate mover or demand anything from
their supervisor. At least, it was so up till this example and it means that there is definitely something special about new
example. As you can see from the above code, both of these special parameters are not even saved in the
RegularPolygonWithCircle object but they are both forwarded into the constructor of the subordinate circle, so the
CircleInsideConvexPoly class is the real recipient of both unusual parameters.
public CircleInsideConvexPoly (Form frm, Mover mvr, PointF pt, float rad,
Color clr, PointF [] areal)
{
form = frm;
supervisor = mvr;
m_center = pt;
m_radius = Math .Max (minRadius, Math .Abs (rad));
brush = new SolidBrush (clr);
ptsAllowed = areal;
}
In addition to these two parameters, circle gets the area in which its center can move. For a board with a shape of regular
polygon, this area also has the shape of regular polygon, but it is smaller than the original board. This area is easily
calculated when the radii of the circle and of the board vertices are known.
protected PointF [] CircleAreal
{
get
{
return (Auxi_Geometry .RegularPolygon (ptC, m_radius –
Convert .ToSingle (radCircle / Math .Cos (Math .PI / nVertices)),
nVertices, m_angle));
}
}
Moving of all objects in the Form_AdheredMouse.cs is organized with the standard three mouse events. When any object
is pressed by the mouse and caught by mover, there are two special situations. When rotation of the board has to start in
response to a right mouse press, the reaction is standard and was discussed earlier: the compensation angle must be
calculated. When the circle is pressed with the left button, nothing new is expected because it is a standard movement of a
primitive object. But this is the place where something interesting starts.
World of Movable Objects 293 (978) Chapter 11 Movement restrictions
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 CircleInsideConvexPoly)
{
(grobj as CircleInsideConvexPoly) .InitialMouseShift (
e .Location);
}
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is RegularPolygonWithCircle)
{
(grobj as RegularPolygonWithCircle).StartRotation (e.Location);
}
}
}
ContextMenuStrip = null;
}
Our circle is an object of solid sizes. It can be pressed far away from its center, so the shift between the pressed point and
the circle center must be saved to organize accurate moving.
public void InitialMouseShift (Point pt)
{
dxMouseFromCenter = pt .X – m_center .X;
dyMouseFromCenter = pt .Y – m_center .Y;
}
Movement of any object is described by its MoveNode() method. Many previous examples demonstrate that if there exist
some movement restrictions, then they are used inside this method to determine the possibility of moving. Circle is a
primitive object with the cover consisting of a single node, so there is no checking for the number of the pressed node. But
circle has a limitation of its movement in the shape of a convex polygon inside which the circle center is allowed to move;
the proposed movement of the circle center is checked against this area.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF centerNew = new PointF (ptM .X - dxMouseFromCenter,
ptM .Y - dyMouseFromCenter);
if (Auxi_Geometry .PointInsideConvexPolygon (centerNew, ptsAllowed))
{
Center = centerNew;
bRet = true;
}
else
{
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (
new PointF (m_center .X + dxMouseFromCenter,
m_center .Y + dyMouseFromCenter)));
supervisor .MouseTraced = true;
bRet = false;
}
World of Movable Objects 294 (978) Chapter 11 Movement restrictions
}
return (bRet);
}
When the circle is nowhere near border and its proposed new position is inside the allowed area, then this movement is
allowed and the circle gets the new position.
if (Auxi_Geometry .PointInsideConvexPolygon (centerNew, ptsAllowed))
{
Center = centerNew;
bRet = true;
}
The real problem is in the case when the proposed movement is not allowed! For better understanding of the problem, I’ll
remind about the order of steps and methods involvement in the moving process. We are somewhere in the middle of
movement and in the form we have a very simple OnMouseMove() method.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
}
The mouse cursor was already moved by the Mover.Move() method and after it all the needed parameters including the
new mouse position and proposed movements of the caught object along both axes are sent into the MoveNode()
method of the caught object. This object is a circle and only there it is found that the proposed movement is not allowed.
The circle is not moved, but it also means that the cursor must be returned back. Easy? Such movement of the cursor there
and back again in an instant is absolutely invisible; nobody will see it, but there is a problem. The circle is caught by the
mover, so each move of the cursor is transformed into the movement of the circle. If processed in the normal way, this back
movement of the cursor is going to be transformed into the synchronous movement of the circle, so the cursor moves back,
but the circle synchronously moves from it. The only way to avoid such thing is to cut temporarily the link between the
cursor and the caught circle for this back movement of a cursor. This is done by using twice the Mover.MouseTraced
property.
First the link between the mouse (mover) and the object is cut
supervisor .MouseTraced = false;
Then mouse cursor is moved back to the same point on a circle,
Cursor .Position = form .PointToScreen (Point .Round (
new PointF (m_center .X + dxMouseFromCenter,
m_center .Y + dyMouseFromCenter)));
After it the link between the mouse (mover) and the caught object is reinstalled.
supervisor .MouseTraced = true;
Because the proposed move of an object failed, the MoveNode() method has to return false value.
bRet = false;
The important thing is that all mover parameters are not affected by this temporarily cut of the link with the mover, so the
caught object, the caught node, and everything else are unchanged. Because the move is not allowed, the MoveNode()
method immediately returns false value; the caught object – circle – waits for the next movement at the old position and
the mouse cursor retains its relative position on the caught object. Thus, it does not move either. For any observer, the
cursor stopped together with an object.
Now you can see why an object which is glued with the mouse throughout the period of movement has to get those two
additional parameters on initialization. The form is needed for the transformation of coordinates from one system to
another; the mover (supervisor) is needed because only mover can cut and reinstall the link with an object.
Without these two parameters, the adhered mouse cannot be organized, but there is one more way to pass these parameters.
Instead of passing them at the moment of initialization, they can be passed at the movement start via the
World of Movable Objects 295 (978) Chapter 11 Movement restrictions
InitialMouseShift() method. I prefer the way I demonstrate in this example; you can do it differently and decide
for yourself, which way you prefer.
One more important detail. There are two ways to determine the new position of an object by the parameters of the
MoveNode() method: either to rely on the pair of shifts along two axis (dx, dy) or on the new mouse position (ptM).
I mentioned several times that I always use the second choice throughout the rotations, but for normal forward movement,
resizing, and reconfiguring I prefer the first option. This is correct anywhere except the cases when I have to use this
Mover.MouseTraced property to go back and forth. In all such cases, the cursor has to be glued at the same point of an
object throughout the whole movement. The shift from some basic point to the cursor is calculated at the first moment and
has to be unchanged throughout the process. It works much better if in such a case the proposed position of this basic point
is calculated from the changing cursor position with the help of that shift.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF centerNew = new PointF (ptM .X - dxMouseFromCenter,
ptM .Y - dyMouseFromCenter);
… …
Circle in labyrinth
File: Form_CircleInLabyrinth.cs
Menu position: Movement restrictions – Circle in labyrinth
The next example demonstrates the use of the same technique
of adhered mouse in some new environment. There is a small
circle which hopes to find its way through labyrinth
(figure 11.16). This labyrinth (class Labyrinth) consists
of a set of walls (class Wall). Labyrinth construction has
nothing to do with the discussed problems of movability, so
I’ll explain the labyrinth design at the end of this example.
The small circle (class Circle_Wandering) can be
moved along the corridors through the labyrinth. If this circle
runs into a wall, it does not move farther on and the cursor
does not move either. The cursor is adhered to that point of
the circle which it initially pressed and stays at this circle
point throughout the whole movement until the moment of
mouse release.
An object of the Circle_Wandering class is a standard
non-resizable circle. This simple object has a primitive cover
consisting of a single circular node. To define any simple Fig.11.16 Circle in labyrinth
circle, only a central point and radius are needed.
public class Circle_Wandering : GraphicalObject
{
Form form;
Mover supervisor;
Labyrinth lab;
PointF m_center;
float m_radius;
SolidBrush brush;
This circle is going to move in the area restricted by some structure. When the movement is blocked, the circle is supposed
to stop and the mouse must be adhered to the same point on the circle. Thus, three additional parameters must be passed to
the circle on initialization: form, mover, and labyrinth.
public Circle_Wandering (Form frm, Mover mvr, Labyrinth lab_init,
PointF pt, float r, SolidBrush brsh)
World of Movable Objects 296 (978) Chapter 11 Movement restrictions
Human eye is a perfect instrument which can detect the discrepancy of one or two pixels. In this case which requires an
absolute accuracy, when the ball is caught by mover (by the mouse!), it is impossible to simplify the task by assumption that
the circle is caught at its central point. The initial shift between the circle center and the mouse position must be stored and
then used throughout the whole movement. For this purpose, the InitialMouseShift() method is called from inside
the OnMouseDown() method.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (mover .CaughtSource is Circle_Wandering)
{
(mover .CaughtSource as Circle_Wandering)
.InitialMouseShift (e .Location);
}
}
}
The possibility of proposed circle movement is checked in the Circle_Wandering.MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF ptFrom, ptTo, ptCross;
PointF ptCenterNew = new PointF (ptM .X - dxMouseFromCenter,
ptM .Y - dyMouseFromCenter);
for (int j = 0; j < lab .Walls .Count; j++)
{
lab .Segment (j, out ptFrom, out ptTo);
if (Auxi_Geometry .Distance_PointSegment (ptCenterNew,
ptFrom, ptTo) <= m_radius ||
Auxi_Geometry .Segment_Crossing (ptFrom, ptTo, m_center,
ptCenterNew, out ptCross))
{
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (
new PointF (m_center .X + dxMouseFromCenter,
m_center .Y + dyMouseFromCenter)));
supervisor .MouseTraced = true;
return (false);
}
}
Center = ptCenterNew;
bRet = true;
}
return (bRet);
}
This is again the case in which the link between mover and the caught circle must be cut temporarily, so the whole
procedure is organized in the way similar to the previous example.
1. The calculation of the proposed circle position is based not on the pair of mouse movements along two axes
(dx, dy) but on the exact mouse position ptM and the shift from the mouse to the circle center. Shifts along
two axes were already calculated at the starting point of this movement, so the proposed position for the central
point is easily obtained.
PointF ptCenterNew = new PointF (ptM .X - dxMouseFromCenter,
ptM .Y - dyMouseFromCenter);
World of Movable Objects 297 (978) Chapter 11 Movement restrictions
2. There are two checks for the possibility of movement. Both checks are done against each wall of the labyrinth.
Segment is determined by its end points ptFrom and ptTo.
lab .Segment (j, out ptFrom, out ptTo);
3. The first checking estimates the distance between the segment of the wall and the center of the circle; it cannot
become less than circle radius.
if (Auxi_Geometry .Distance_PointSegment (ptCenterNew,
ptFrom, ptTo) <= m_radius ||
For the normal move of a circle it would be enough to have this checking only, but at some moment I found that if
the circle is moved against the wall at the speed of light or close to it, then it can go through (I hope you heard
something about neutrino…). To return this circle back from becoming a particle into a normal screen object, I
had to add another checking. One method from the MoveGraphLibrary.dll solved the problem easily; the circle
is not allowed to cross any segment on the way.
Auxi_Geometry .Segment_Crossing (ptFrom, ptTo, m_center,
ptCenterNew, out ptCross))
4. If any checking failed, the mouse cursor has to be returned back. I want to remind that when the MoveNode()
method is called, the mouse cursor has already moved! To return the cursor back, the link between the mover and
the caught object (circle) must be temporarily cut, the cursor returned back, and then the same link reinstated. And
do not forget to return false from the MoveNode() method in this case because the move is not allowed.
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (
new PointF (m_center .X + dxMouseFromCenter,
m_center .Y + dyMouseFromCenter)));
supervisor .MouseTraced = true;
return (false);
Comments on labyrinth construction. Labyrinth is combined of the straight walls.
public class Labyrinth
{
PointF ptLT;
float cell;
Pen pen;
List<Wall> walls;
Each wall segment is described by its two end points. The ends of each segment are described
not in pixels and real screen coordinates but in “cell coordinates”. Imagine the same labyrinth
drawn on the squared paper with the columns numbered from left to right and rows numbered
from top to bottom starting from the top left corner (figure 11.17). All wall segments are
included into the special array coors[]. Each pair of integers in this array describes an end
point of some wall segment as a pair (row, column). I start to fill this array from the bottom left
corner of labyrinth and go segment after segment.
public Form_CircleInLabyrinth ()
{
… …
int [] coors = new int [] { 7, 0,
0, 0,
Fig.11.17 Columns
0, 1,
are numbered from
1, 1,
left to right, rows
1, 3,
from top to bottom.
… …
In most cases two consecutive pairs of integers describe two ends of some segment. This labyrinth is constructed only of
horizontal and vertical segments. If two consecutive pairs have the same row or column number, then it is vertical or
horizontal segment which is added to the List of walls; otherwise it is a switch to another part of the walls. It happened so
that there are only two such switches in the whole coors array; for my own better understanding, I included extra empty
lines into the coors array at those two places.
World of Movable Objects 298 (978) Chapter 11 Movement restrictions
List<Wall> walls = new List<Wall> ();
for (int i = 0; i < coors .Length - 3; i += 2)
{
WallEnd A = new WallEnd (coors [i], coors [i + 1]);
WallEnd B = new WallEnd (coors [i + 2], coors [i + 3]);
if (A .Column == B .Column || A .Row == B .Row)
{
walls .Add (new Wall (A, B));
}
}
When the whole List of walls is organized, it is enough to declare the screen coordinates for the top left corner of
labyrinth and the cell size in pixels; this will turn the labyrinth from cell coordinates into screen coordinates.
labyrinth = new Labyrinth (walls, new PointF (80, 60), 30,
new Pen (Color .Blue, 3));
Exactly the same labyrinth is used in the next example and similar idea of labyrinth design is used in one more example of
this book.
Strip in labyrinth
File: Form_StripInLabyrinth.cs
Menu position: Movement restrictions – Strip in labyrinth
This is another example with an object to be moved
around a lot of obstacles. The same labyrinth is used in
the Form_StripInLabyrinth.cs (figure 11.18) as in the
previous example, but the movable object is more
interesting. It is a resizable rounded strip. The sizes of
the strip are defined in such a way that the strip cannot
be simply pressed and moved through this labyrinth; the
strip has to be stopped and turned a bit to pass each turn
of a corridor.
The strip (class Strip_Wandering) has a cover
which was demonstrated at figure 7.2. Curved parts of
the border are covered by the small circular nodes; by
moving this part of the strip border, the length of an
object can be changed. Straight parts of the border are
covered by the thin nodes; by moving these parts of the
border, the width of an object can be changed. Strip can
be moved and rotated by any inner point. The
geometry of a strip, the possibility of its rotation, and
the changing length require more parameters to
describe the object position than in case of a circle. The Fig.11.18 Strip in labyrinth
technique of returning the cursor back in case the proposed movement of a strip is not allowed is similar to what was used in
the previous example, so the form, mover, and labyrinth must be also mentioned among the fields of this class.
public class Strip_Wandering : GraphicalObject
{
Form form;
Mover supervisor;
Labyrinth lab;
PointF ptC0, ptC1;
float radius;
double angle;
SolidBrush brush;
When any object is grabbed for moving, it usually requires the saving of some parameters which are not going to change
during the initiated movement but are needed to calculate the object position throughout this movement. For rotation, it is
usually a compensation angle; for zooming it is often some scaling coefficient. In case of a strip we have both, but because
World of Movable Objects 299 (978) Chapter 11 Movement restrictions
of the expectation that on some restricted movements there can be a request to return the cursor back, there is a storing of an
additional parameter.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Strip_Wandering)
{
(grobj as Strip_Wandering)
.StartLengthChange (e.Location, mover.CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is Strip_Wandering)
{
(grobj as Strip_Wandering) .StartRotation (e .Location);
}
}
}
}
I have already explained the StartLengthChange() method for a strip while writing about similar strip in the chapter
Curved borders. N-node covers. The code for this method is nearly the same, but here you can see an extra call to the
InitialCatch() method.
public void StartLengthChange (Point ptMouse, int iNode)
{
InitialCatch (ptMouse);
PointF [] pts = CornerPoints ();
if (iNode < 2)
{
}
else if (iNode < 2 + nNodesOnHalfCircle)
{
fStartingDistanceToRect =
Auxi_Geometry .Distance_PointLine (ptMouse, pts [1], pts [2]);
}
else
{
fStartingDistanceToRect =
Auxi_Geometry .Distance_PointLine (ptMouse, pts [0], pts [3]);
}
}
When an object is pressed by the mouse with the intention to start some movement, it is just the right moment to calculate
some parameters which are fixed for the whole duration of this movement and are used to calculate the new position (or
sizes) according to the mouse movement. These parameters are different for different movements and, as a rule, they are
calculated in different methods. Up till now we saw three different types of stored parameters:
• When an object is pressed by the left button for resizing, then scaling coefficient is calculated.
• When an object is pressed by the right button for rotation, then compensation angle is calculated; in some cases the
distance from the center of rotation is also needed.
• When there is a chance of the future restricted movement which requires the back move of a cursor, then the shift
between the pressed point and some object basic point is calculated.
For the Strip_Wandering class, I decided to combine all these calculations into a single InitialCatch() method
and call this method on all the occasions. The basic point from which the shifts are calculated is also the center of rotation.
World of Movable Objects 300 (978) Chapter 11 Movement restrictions
private void InitialCatch (PointF pt)
{
center = Center;
length = Auxi_Geometry .Distance (ptC0, ptC1);
rMouseFromCenter = Auxi_Geometry .Distance (center, pt);
double angleMouse = Auxi_Geometry .Line_Angle (center, pt);
compensation = Auxi_Common .LimitedRadian (angleMouse - angle);
}
If you make a quick search throughout the code of the Form_StripInLabyrinth.cs file, you will find out that this method is
called not only at the moment of the first mouse press but also in each part of the Strip_Wandering.MoveNode()
method. This happens because different movements of a strip can be stopped by the walls; in all these situations the cursor
must be returned back.
Any possible move of a strip in labyrinth must be checked against the restrictions. Strip can be moved forward or rotated,
its length or width can be changed – any of these actions can be stopped by the walls. The exact movement depends on the
caught node or the pressed button, but all branches of the Strip_Wandering.MoveNode() method are organized in
the similar way:
• The cursor has its new location ptM.
• The new size of the strip or its new location is calculated on the basis of this mouse position.
• The proposed location of the strip is checked against the walls of the labyrinth by the
Labyrinth.StripAllowedPosition() method.
• If there are no problems with the proposed position of the strip, then this move or resizing is finalized. If the move
is not allowed, then the ReturnCursor() method must return the cursor back without disturbing the strip
itself.
Here is part of the Strip_Wandering.MoveNode() method for node number zero. This is the node which covers
one of the straight parts of the strip border. By moving this node, the strip width is changed, but because the width is equal
to the diameter of semicircles, then the total length is also changed. Labyrinth.StripAllowedPosition()
method gets three parameters which totally describe the geometry of the strip, so this method can check the proposed
position of the strip against all walls.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
double fDist;
PointF [] pts = CornerPoints ();
float radiusNew;
PointF ptNew;
if (iNode == 0)
{
if (Auxi_Geometry .SameSideOfLine (ptC0, ptC1, ptM, pts [0]))
{
fDist = Auxi_Geometry .Distance_PointLine (ptM, ptC0, ptC1);
if (fDist >= minR)
{
radiusNew = Convert .ToSingle (fDist);
if (!lab .StripAllowedPosition (ptC0, ptC1, radiusNew))
{
ReturnCursor ();
return (false);
}
radius = Convert .ToSingle (fDist);
InitialCatch (ptM);
DefineCover ();
bRet = true;
World of Movable Objects 301 (978) Chapter 11 Movement restrictions
}
}
}
… …
The Labyrinth.StripAllowedPosition() method is used with nearly any movement (there is one exception!)
and checks if the proposed position of the strip is not going to be too close to the walls of labyrinth. This checkimg is based
on the calculation of distance between two segments. One is the segment of the wall; another is a segment between the
centers of the strip semicircles. The distance from this second segment to the strip border is equal to the radius of
semicircles; if the distance between two mentioned segments is less than this radius, then such position of a strip is not
allowed.
There is one movement in which the use of the above mentioned Labyrinth.StripAllowedPosition() method
with three parameters is not enough. It is the case of the most simple forward movement and there is the same kind of
problem with a very quick movement of an object which I have mentioned in the previous example. For the case of forward
movement, there is similar method with four parameters; additional parameter allows to avoid quick movement through the
walls.
If the proposed movement of the strip is not allowed, then the ReturnCursor() method temporarily cuts the link
between the movement of the cursor and the movement of an object, returns cursor back, and then reinstates the link. To
return the cursor back, two parameters are used; these are the parameters which were saved by the InitialCatch()
method: the distance between the mouse and the center of a strip and the compensation angle.
public void ReturnCursor ()
{
center = Center;
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (
Auxi_Geometry .PointToPoint (Center, angle + compensation,
rMouseFromCenter)));
supervisor .MouseTraced = true;
}
new labyrinths is going to be an object of the LineSegment class. This class was already introduced at the beginning of
the book immediately after I explained the idea of nodes and covers (Form_Lines_Solitary.cs, figure 2.3). Because this
happened long ago (more than 250 pages back), I’ll quickly refresh your memory about the LineSegment class.
Segment of line is described by its two end points and is shown by using some pen.
public class LineSegment : GraphicalObject
{
PointF ptA, ptB;
Pen m_pen;
static int minLen = 10;
Segment length can be changed by moving the end points. Because there are no restrictions on moving these points, you
can simultaneously change the line angle. By catching the line at any inner point with the left button you can start its
forward movement; if you catch the line with the right button, then rotation is started. Rotation of a segment goes around its
middle point. To avoid the accidental disappearance of an element, there is a limit on minimal segment length.
The cover of such object consists of three nodes: two circular nodes at the end points are used to change the length while the
strip node along the whole segment provides forward movement and rotation.
public override void DefineCover ()
{
float radius = Math .Max (3, m_pen .Width / 2);
cover = new Cover (new CoverNode [] {
new CoverNode (0, ptA),
new CoverNode (1, ptB),
new CoverNode (2, ptA, ptB, Cursors .SizeAll)});
}
At the default view of the Form_SpotInArbitraryLabyrinth.cs (figure 11.19) you see a simple labyrinth and a small
colored spot. This spot belongs to the Spot_Restricted class and even the name of the class signals that this element
might have some problems with its movement around the screen. It certainly has some problems as it is not allowed to go
through the walls. Otherwise it is a very simple circular spot which is described by its central point, radius, and color.
public class Spot_Restricted : GraphicalObject
{
Form form;
Mover supervisor;
List<LineSegment> walls = new List<LineSegment> ();
PointF m_center;
float m_radius = 5;
Color m_color = Color .Red;
static float m_minRadius = 3;
static float m_maxRadius = 12;
These three spot parameters – central point, radius, and color – are determined at the moment of initialization. Because the
technique of adhered mouse is used with such spot, then form and mover are also among the parameters of constructor.
public Spot_Restricted (Form frm, Mover mvr,
PointF ptC, float radius, Color color)
When similar small circle was moved through the labyrinth in the example Form_CircleInLabyrinth.cs, then the
constructor of the Circle_Wandering class included the labyrinth through which that circle had to move. The
Spot_Restricted object does not have similar parameter in its constructor, so this spot has to be informed about all
possible walls on the way in some other way. The explanation is just on the next page.
Radius of such spot can vary in the [3, 12] range; in the current example radius can be changed via the command of context
menu which can be called on the spot. There are two interesting things about the size of this spot.
The simplest cover for any movable and non-resizable circle (radius of this spot can be changed only through menu
commands) consists of a single circular node with an area equal to this circle. However, for the spots of minimal size
(radius of three pixels) it can be a problem for some people to catch such tiny spot with a mouse, so I put a restriction that
the node radius cannot be less than five pixels. Thus, for the smallest spot the node is bigger than the spot itself and such
spot can be grabbed not only by any point inside its border but also at the points which are outside the spot but close to its
border.
World of Movable Objects 303 (978) Chapter 11 Movement restrictions
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, m_center, Math .Max (m_radius, 5)));
}
Second interesting thing starts in the OnMouseDown() method of the form when the spot is pressed with the left button.
This is a standard procedure that if the caught object needs some preliminary calculations at the starting moment of
movement, then some method of the caught class like StartResectoring() or StartLengthChange() is called at
this moment. This happens only for resizable objects and never for non-resizable. Our spot is a primitive non-resizable
object, so what is the idea of the Spot_Restricted.StartMoving() method which is called when the spot is
pressed?
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 Spot_Restricted)
{
spot .StartMoving ();
… …
This is a very important addition to the technique of adhered mouse which I am going to use whenever possible in the
following examples.
The first example with adhered mouse (Form_AdheredMouse.cs, figure 11.15) uses a big enough circle which can be
caught by any inner point, so the shift between initial mouse position and the circle center is calculated at the starting
moment and is used throughout the whole process of movement. The next example (Form_CircleInLabyrinth.cs,
figure 11.16) uses a small circle and similar calculation with initial mouse shift. When you deal with some small object, it
is much easier to make an initial mouse movement and after it throughout the whole process of object moving to consider
the mouse position as the new object position. Our eyes can hardly detect such initial mouse shift for one, two, or three
pixels; even if you detect it, there is no problem. (Further on you will see that for the movement along some path this initial
mouse shift is even a very good solution because after it the mouse cursor moves exactly along the path.) The
Spot_Restricted.StartMoving() method makes the needed initial shift of the mouse cursor and puts it exactly
into the center of the spot.
public void StartMoving ()
{
AdjustCursorPosition (m_center);
}
Starting from this moment, the cursor is adhered to the central point and for any allowed movement the mouse position
determines the central point of the spot. The StartMoving() method is called when the spot is already caught by
mover, so for this initial mouse shift the link between mover and the caught object must be temporarily cut and immediately
reinstalled after the cursor shift.
private void AdjustCursorPosition (PointF pt)
{
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (pt));
supervisor .MouseTraced = true;
}
Different examples were designed throughout the years, so in different classes the action to be made at the starting moment
can be called StartMoving, StartResizing, StartReconfiguring, or Press, but in all cases when the
cursor has to be shifted to some point, the same AdjustCursorPosition() method is used.
Adjustment of the cursor position is not the only thing which has to be done when the spot is ready to move. There can be a
lot of walls on the way. As you will see further on, there are a lot of commands to change the labyrinth view. Such changes
can be done at any moment, so the spot has to be informed about the current labyrinth status at the same moment when the
spot starts its movement. Our spot gets the whole List of walls via the Spot_Restricted.Walls property.
World of Movable Objects 304 (978) Chapter 11 Movement restrictions
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 Spot_Restricted)
{
spot .StartMoving ();
… …
spot .Walls = segmentsAll;
}
… …
All possible restrictions for the spot movement must be considered in the Spot_Restricted.MoveNode() method.
Suppose that you have a single wall with two end points A and B. The current spot position is described by its m_center
field. The proposed spot position is described by the mouse position ptM. Spot can move to this new point if it is not
closer to the wall (A, B) than the spot radius. This will be correct for ideal thin walls; because the walls can be drawn with
wide enough pen, this pen width has to be considered. To avoid the possibility of very quick spot movement through the
wall, the distance between two segments: (m_center, ptM) and (A, B) is checked.
If the movement is not allowed, then the mouse cursor is returned into the central point of the spot.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
LineSegment wall;
for (int j = 0; j < walls .Count; j++)
{
wall = walls [j];
if (Auxi_Geometry .Distance_Segments (m_center, ptM, wall .A_Point,
wall .B_Point) <= m_radius + wall .Pen .Width / 2)
{
AdjustCursorPosition (m_center);
return (false);
}
}
Center = ptM;
bRet = true;
}
return (bRet);
}
Many things in the Form_SpotInArbitraryLabyrinth.cs are initiated via the commands of several context menus, so it is
better to see pictures of these menus (figures 11.20) prior to discussion.
Figures 11.20 show four different menus (two of them with submenus). As usual, the menu to be called is determined
inside the OnMouseUp() method of the form depending on the class of the pressed object.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
… …
if (e .Button == MouseButtons .Right && fDist <= 3)
World of Movable Objects 305 (978) Chapter 11 Movement restrictions
{
if (grobj is LineSegment)
{
segmentPressed = grobj as LineSegment;
ContextMenuStrip = menuOnSegment;
}
else if (grobj is Spot_Restricted)
{
ContextMenuStrip = menuOnSpot;
}
else if (grobj is GroupOfLines)
{
ContextMenuStrip = menuOnGroup;
}
else if (grobj is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, null,
PointToScreen (ptMouse_Up));
}
}
}
else
{
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
}
Then you move the mouse without releasing the pressed button.
private void OnMouseMove (object sender, MouseEventArgs e)
{
ptMouse_Move = e .Location;
if (mover .Move (e .Location))
{
… …
}
else
{
if (bTemporaryFrame)
{
Invalidate ();
}
}
}
Throughout this movement of the pressed mouse you see a rectangular frame based on two points; the point of initial press
and the current mouse point are used as the opposite corners of this rectangle.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (bTemporaryFrame)
{
grfx .DrawRectangle (penFrame,
Auxi_Geometry .RectangleAroundPoints (ptMouse_Down, ptMouse_Move));
}
… …
}
When at last the button is released, then the temporary frame is gone, but if at this moment there are some segments (at least
one) inside the temporary frame, then the group is organized and it contains all those segments that were rounded.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
if (bTemporaryFrame)
{
Rectangle rc = Auxi_Geometry .RectangleAroundPoints (
ptMouse_Down, e .Location);
List<LineSegment> segmentsInFrame = new List<LineSegment> ();
for (int i = segments .Count - 1; i >= 0; i--)
{
if (rc .Contains (segments [i] .RectAround))
{
segmentsInFrame .Insert (0, segments [i]);
segments .RemoveAt (i);
}
}
if (segmentsInFrame .Count > 0)
{
World of Movable Objects 309 (978) Chapter 11 Movement restrictions
group = new GroupOfLines (segmentsInFrame, penFrame);
RenewMover ();
}
bTemporaryFrame = false;
Invalidate ();
}
… …
Suppose that there is no group in our example; figure 11.19 demonstrates such situation. In this case all segments are
included into the List of segments.
List<LineSegment> segments = new List<LineSegment> ();
Four pages back I mentioned the command to organize a new wall; you can see from the code of the
Click_miAddNewWall() method that the new wall segment is added to this List. When the mouse button with the
temporary frame in view is released, then all rounded segments are excluded from the List<>segments and are included
into another List<>segmentsInFrame; the organized group is based on this List.
group = new GroupOfLines (segmentsInFrame, penFrame);
As a result, all walls in the form are now divided between two Lists. There are segments which belong to the group; if
there are still segments not included into the group, then they belong to the List<>segments. From now on, if you
apply some command to the wall, then the reaction depends on whether this wall is inside the group or not. For example,
there is a command to use the pressed wall as a sample for others. (Using as sample means using the same pen to draw
other walls.) If the pressed wall belongs to the group, then all walls of the group will get the same pen; if the pressed wall is
outside the group, then all walls which are outside the group get the same pen.
There are also commands which can be applied to all walls of the group through its special menu (figure 11.20c).
Discussion of different groups is four chapters ahead and here we have a chance to look beforehand into some details of
group design. I think it is interesting to do because the GroupOfLines class is very useful though its design is extremely
simple. Group has no title; it is shown as a slightly rounded frame around all inner elements. The group is so primitive that
for a long time there was no command to modify its frame; now it is easily done (figure 11.20c, second command from the
end).
Any object of the GroupOfLines class is complex but from the point of cover design and from the point of its
movements it is simple.
Group is organized around some set of wall segments. The combined rectangular area of all these segments is calculated,
then this rectangle is inflated by 10 pixels on each side, and the resulting rectangle rcFrame is used to draw the frame.
Group is movable by any inner point but resizable only indirectly as a result of moving its inner walls; the cover for such
rectangular area consists of a single polygonal (rectangular) node. Group can be grabbed by any inner point and by any
point of its frame; to make catching by the frame easier, the sensitive area is slightly bigger than the frame.
public override void DefineCover ()
{
Rectangle rc = rcFrame;
rc .Inflate (3, 3);
cover = new Cover (rc, Resizing .None);
}
When the group is moved, all its inner elements (walls) are moved synchronously.
public override void Move (int dx, int dy)
{
rcFrame .X += dx;
rcFrame .Y += dy;
foreach (LineSegment elem in lines)
{
elem .Move (dx, dy);
}
}
The part of the GroupOfLines.MoveNode() method which is responsible for the forward movement is extremely
simple as the MoveNode() method only calls the Move() method.
World of Movable Objects 310 (978) Chapter 11 Movement restrictions
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
bRet = true;
}
… …
Situation with rotation is more interesting. You might not even understand it from the very beginning, but rotation of such
group is ambiguous. Group is rotated around its central point and I do not expect strong objections to this decision. But
there can be two different rules for changing the angle of each wall in the group.
• Wall can change its angle according to the group rotation.
• Wall can rotate around without changing its angle at all.
For better understanding of both cases imagine a horizontal wall at the same level (with the same Y coordinate) as rotation
center. Now rotate the group for 90 degrees. If the first rule is implemented, then horizontal wall will become vertical,
while in the second case it will be always horizontal. Seats in big dipper are going around according to the second rule; you
will feel yourself not comfortable at all if big dipper would implement the first rule.
However, we are not in big dipper, so I decided to use the first rule for wall rotation in the group. It means that I have to
implement the classical type of rotation with some needed calculations 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 (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right)
{
… …
else if (grobj is GroupOfLines)
{
(grobj as GroupOfLines) .StartRotation (e .Location);
}
… …
At the starting moment of rotation, distance and compensation angle for each end point of each wall in the group are saved.
Each wall has two end points, so the size of each array is twice as big as the number of walls in the group.
public void StartRotation (Point ptMouse)
{
compensation = new double [2 * lines .Count];
radius = new double [2 * lines .Count];
ptMiddle = Auxi_Geometry .Middle (rcFrame);
double angleMouse = Auxi_Geometry .Line_Angle (ptMiddle, ptMouse);
for (int i = 0; i < lines .Count; i++)
{
compensation [i * 2] = Auxi_Common .LimitedRadian (angleMouse –
Auxi_Geometry .Line_Angle (ptMiddle, lines [i] .A_Point));
compensation [i * 2 + 1] = Auxi_Common .LimitedRadian (angleMouse –
Auxi_Geometry .Line_Angle (ptMiddle, lines [i] .B_Point));
radius [i * 2] = Auxi_Geometry.Distance (ptMiddle, lines [i] .A_Point);
radius [i * 2 + 1] =
Auxi_Geometry .Distance (ptMiddle, lines [i] .B_Point);
World of Movable Objects 311 (978) Chapter 11 Movement restrictions
}
}
These compensation[] and radius[] arrays are used in the standard way in that part of the
GroupOfLines.MoveNode() method which is responsible for group rotation.
public override bool MoveNode (int iNode, 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 (ptMiddle, ptM);
for (int j = 0; j < lines .Count; j++)
{
lines [j] .EndPoints (
Auxi_Geometry .PointToPoint (ptMiddle,
angleMouse - compensation [j * 2], radius [j * 2]),
Auxi_Geometry .PointToPoint (ptMiddle,
angleMouse - compensation [j * 2 + 1], radius [j * 2 + 1]));
}
}
return (bRet);
}
Though our GroupOfLines object is rectangular and can be involved in rotation, there is one principal difference
between the rotation of our group and of any ordinary rectangle. Any rectangle involved in rotation uses an angle field
to describe its current angle; for example, look at the
Form_Rectangles_AllMovements.cs (figure 4.4)
which deals with rectangles of the
Rectangle_AllMovements class. Throughout
the rotation of such rectangle, its sizes never change,
while currently changing angle allows to calculate the
new positions of corner points.
Our group has no angle field because its angle is
always zero and the frame has only horizontal and
vertical lines, but the group sizes are constantly
changing throughout the rotation; figure 11.22
illustrates these changes.
Our group has neither angle nor sizes of its own, but Fig.11.22 Group change throughout the rotation
every move of any inner wall requires the update of the
group. Forward movement of the group does not require its update but group rotation does.
private void OnMouseMove (object sender, MouseEventArgs e)
{
ptMouse_Move = e .Location;
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is LineSegment)
{
if (group != null && grobj .ParentID == group .ID)
{
group .Update ();
}
}
World of Movable Objects 312 (978) Chapter 11 Movement restrictions
else if (grobj is GroupOfLines && e .Button == MouseButtons .Right)
{
group .Update ();
}
Invalidate ();
… …
Update of the group consists of recalculating of its frame and cover.
public void Update ()
{
CalcFrame ();
DefineCover ();
}
This whole section of the book is devoted to the use of adhered mouse technique; the GroupOfLines class appeared in
this example as an instrument to make the labyrinth design simpler and faster. Now I go more and more into the details of
this auxiliary class but do not think that this is a waist of time. Though the GroupOfLines class deals with very simple
inner elements, the idea of its work and many nuances of its design copy a very powerful ElasticGroup class. The
main idea of such groups is in the absolute change of the dominance: not the group rules the behaviour of its inner elements,
but any movement of inner elements changes the group. The group frame simply adjusts to any change inside and this is
perfectly illustrated by the commands from menu which can be called on the group (figure 11.20c).
Suppose that you want to repeat some part of labyrinth. You can round this part with a frame and then call menu on the
group. There are even two different commands which allow to place the copy either to the right (figures 11.23) or below.
Fig.11.23 Group of walls before and after using the command Duplicate walls (to the right)
private void Click_miDuplicateWalls_Right (object sender, EventArgs e)
{
Size sizeShift = new Size (group .FrameArea .Width, 0);
int nElems = group .ElementsNumber;
for (int i = 0; i < nElems; i++)
{
LineSegment segment = new LineSegment (
group .Elements [i] .A_Point + sizeShift,
group .Elements [i] .B_Point + sizeShift,
group .Elements [i] .Pen);
group .AddSegment (segment);
}
RenewMover ();
}
In any case, the new part is automatically included into the group. This automatic update of the group works because the
GroupOfLines.AddSegment() method calls the update of the group.
public void AddSegment (LineSegment elem)
{
elem .ParentID = this .ID;
World of Movable Objects 313 (978) Chapter 11 Movement restrictions
lines .Add (elem);
Update ();
}
Another interesting thing which can be applied to the group is zooming. Closer to the end of the book in the section All
things bright and beautiful I demonstrate an example with a lot of different elements (Form_ElementsAndGroups.cs,
figure 21.68). In that example, any set of elements can be united into a group and THREE different types of zooming can
be applied to those groups (figure 21.78). You can look into that chapter for explanation of those variants. In the current
example I implement only one variant of zooming with only few available zooming coefficients (see submenu at
figure 11.20c). Central point of the group is used as the basic point for zooming and the selected coefficient changes the
distance to end points of all walls inside the group. Coefficients allow to enlarge or squeeze the group. (There is a theory
of Bing Bang, but there is also an oscillating universe theory…)
private void Click_miZoomCoef (object sender, EventArgs e)
{
double coef = Convert .ToDouble ((sender as ToolStripMenuItem) .Text);
PointF ptM = Auxi_Geometry .Middle (group .FrameArea);
for (int i = 0; i < group .ElementsNumber; i++)
{
group .Elements [i] .Zoom (ptM, coef);
}
group .Update ();
foreach (LineSegment segment in group .Elements)
{
AvoidSpotSegmentOverlap (segment);
}
RenewMover ();
}
Labyrinth can be changed in many different ways and the small spot can be moved around all the hurdles by the adhered
mouse. There is no predetermined way for the spot. In the next section the same technique of adhered mouse is used to
move similar small spots along the predetermined paths.
World of Movable Objects 314 (978) Chapter 11 Movement restrictions
As you can see from this code, the MoveNode() method is very simple, but the most interesting thing in this movement is
hidden one layer dipper.
Point ptM is the current position of the mouse cursor. Even if you try to move the cursor strictly along the line, I have no
doubts that cursor will not stay exactly on the line and will move to one side or another. At the same time I want the cursor
to move only along the line, so when you see the statement
Center = ptM;
it does not mean that the current mouse position is declared as the new central point of the spot. The
SpotOnLine.Center property gets the value (ptM), calculates the point of segment which is the nearest to this point,
and places the spot at this calculated point.
public PointF Center
{
get { return (m_center); }
set
{
m_center = Auxi_Geometr.NearestPointOnSegment (value, ptEndA, ptEndB);
DefineCover ();
}
}
The used method Auxi_Geometry.NearestPointOnSegment() calculates the nearest point not on the infinitive
line but on the segment which is specified by two end points, so it will not allow to move the spot beyond those segment
ends. Thus, we have the spot on the segment while the cursor can be somewhere outside the line, so the cursor has to be
switched back to the spot central point. Certainly, throughout such cursor mandatory movement the link between mover
and spot must be temporarily cut and this is done inside the MoveNode() method.
Movement of a spot along the arc is organized in similar way but there are some nuances. The main idea is the same, but
there are more calculations for this curved trail and there is also one catch. Thanks to such traps, the programming is
interesting and exciting.
Some fields in the class SpotOnArc are the same as in the previous SpotOnLine class; others are different because
the trail has different shape.
public class SpotOnArc : GraphicalObject
{
Form form;
Mover supervisor;
PointF m_center;
float m_radius = 5;
Color clr = Color .Red;
PointF ptCenter_Arc;
float radius_Arc;
RectangleF rcAroundCircle;
double minPositiveDegree_Arc, maxPositiveDegree_Arc;
In the MoveGraphLibrary.dll, there are all needed methods to calculate the point on the circle when the angle is known or
to calculate an angle for the known point; there are also methods to convert radians and degrees back and forth. To imagine
and describe the position of any point, I prefer to use degrees, while for calculations radians are used. The trail can be a full
circle or only some part of it (an arc); both cases are represented at figure 11.24.* While initializing a SpotOnArc
object, you declare the parameters of the spot and of its trail.
public SpotOnArc (Form frm, Mover mvr, PointF ptArcCenter, float fArcRadius,
double angleArcStart_Degree, double angleArc_Degree,
PointF pt, float r, Color clrSpot)
Two angles are needed for an arc: an angle from the center of a circle to one end point of the arc and the arc angle which is
an angle from this end to another one. Do not forget that the sign of any angle is calculated in the normal way – positive
angles are going counterclockwise; I have already mentioned this in the chapter Rotation.
*
In the initial version, the big arc was really closed, but then I made a small gap in it to demonstrate the trap that I am going
to explain.
World of Movable Objects 317 (978) Chapter 11 Movement restrictions
If the arc is shorter than the closed circle, then any movement of the spot must be checked for the possibility of going
beyond the end points. It is easier to do on the straight segment; on the arc we have to deal with the angles and the source
of the small problem is the possibility of jumping from the positive angles into negative or vice versa while moving along
the arc. For easier angles checking, those angles are changed in such way that they are definitely positive and after it I work
with the [minPositiveDegree_Arc, maxPositiveDegree_Arc] range; only the movement inside this range
is allowed.
In the most cases it would be enough but not in the case of nearly closed arc. The length of the gap depends on the arc
angle and the radius of the trail. If you declare an arc with a very small gap angle (for nearly closed arc from figure 11.24
the arc angle is 354 degrees), then the gap is obvious. If you move the spot slowly, it will stop at the end point before the
gap; if you try to move the spot faster, chances are high that the spot will jump over the gap. With the really fast movement
of the mouse the spot can jump over the gap of 15 degrees (maybe more). This is not good at all; the trail is the trail; the
spot is not supposed to move over the gaps; something must be done.
There can be different ways to prevent such jumping over the ends of an arc; you can implement your own algorithm. My
idea in solving the problem of the gaps is based on setting the ranges of angles for the first and the last quarters of the arc
and not allowing to change the position directly between these quarters. The additional checking of the jumps over the gap
is needed only for non-closed arcs; during the initialization of such arc, the special flag bCheckGap is set and two
needed ranges are calculated.
public SpotOnArc (Form frm, Mover mvr, PointF ptArcCenter, float fArcRadius,
double angleArcStart_Degree, double angleArc_Degree,
PointF pt, float r, Color clrSpot)
{
… …
if (-360 < angleArc_Degree && angleArc_Degree < 360)
{
bCheckGap = true;
double quarter = (maxPositiveDegree_Arc - minPositiveDegree_Arc) / 4;
firstQuaterPositiveAngle = new double [] {
minPositiveDegree_Arc, minPositiveDegree_Arc + quarter };
lastQuaterPositiveAngle = new double [] {
maxPositiveDegree_Arc - quarter, maxPositiveDegree_Arc };
}
… …
If the bCheckGap is set to true, then in the MoveNode() method an additional checking of the proposed
movement is organized.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
if (bCheckGap)
{
… …
If it is found that the angle to the current spot position belongs to the first quarter of the arc and the angle to the proposed
spot position belongs to the last quarter or vice versa, then such movement is not allowed, the spot is not moved anywhere,
and the cursor is returned on its previous position inside the spot.
if ((Auxi_Common .ValueInRange (angleViaCenter_Degree,
firstQuaterPositiveAngle, ValueInRangeMask .INCLUDE_BOTH) &&
Auxi_Common .ValueInRange (angleViaNewCenter_Degree,
lastQuaterPositiveAngle, ValueInRangeMask .INCLUDE_BOTH)) ||
(Auxi_Common .ValueInRange (angleViaCenter_Degree,
lastQuaterPositiveAngle, ValueInRangeMask .INCLUDE_BOTH) &&
Auxi_Common .ValueInRange (angleViaNewCenter_Degree,
firstQuaterPositiveAngle, ValueInRangeMask .INCLUDE_BOTH)))
{
AdjustCursorPosition (m_center);
return (false);
}
World of Movable Objects 318 (978) Chapter 11 Movement restrictions
}
When mover deals with a respectable spot and there are no more attempts of frivolous jump over a gap, then the nearest
(from cursor) point on the arc is declared as the new spot position, the spot is moved into this point, and the cursor is moved
into the central point of the spot.
Center = Auxi_Geometry .EllipsePoint (ptCenter_Arc, radius_Arc, radius_Arc,
Auxi_Convert .RadianToDegree (angleViaNewCenter), 1.0);
AdjustCursorPosition (m_center);
bRet = true;
In each case of this example we have two visually distinguishable objects: there is a trail and there is a small spot which is
moved along this trail. The adhered mouse technique is used to move a small object while the trail only makes this
movement more obvious and allows to control visually whether there are any mistakes in movement or not. The same
technique can be used in situations when there is only one object and the movement of some part is used to change the size
of this object.
File: Form_SegmentOnLine.cs
Menu position: Graphical objects – Basic elements – Lines – Changeable segment on the line
Two next examples have some similarities with the previous one, but for better explanation I decided to organize two
examples instead of one: the first one deals with the straight lines, the second – with the arcs. As usual, the case of the
straight lines is easier, so we shall start with them. In the previouos example Form_SpotsOnLineAndArcs.cs
(figure 11.24) we have a segment of the straight line and a colored spot that is allowed to move only along this segment.
To simplify the code, the segment is even left unmovable and only the spot is allowed to move along this segment. Let us
move step by step from the previous example to the new one.
• Imagine a straight segment from the previous example with not one but two spots positioned on its ends.
• Start moving one of the spots; it will move along the segment. At the same time let the associated end point move
with this spot so that they always stay together.
• Now make this spot invisible. Mover still detects it, so there is no problem in moving the spot after you initially
pressed it and this is easy to do because the spot is always at the segment end point.
Because the invisible spot is always at the end of the segment, then we can declare this spot to be the part of segment. And
what we have as a result of all these changes? We are looking at the line segment, we can press its end point and move.
Does it remind you something that you have already seen in one of the previous chapters? Yes, you are right, there was
exactly such an example at the very beginning of the book: the Form_Lines_Solitary.cs (figure 2.3) in the chapter First
acquaintance with nodes and covers. I purposely organized the new example – Form_SegmentOnLine.cs, figure 11.25 –
as a copy of that old example and the difference is only in using the new technique of adhered mouse cursor. Well, there
are some differences. First, the segments of the current example can be also rotated. Second, there is some difference in
movement of the end points.
In the old example – Form_Lines_Solitary.cs –
the length of any segment can be changed easily by
moving any segment end. Usually even in that old
example the cursor will stay with the caught node
(somewhere close to the end point of a segment),
but there is one exception: if you try to make a
segment very short by moving one end point to
another, then at some moment when the length of a
segment will be at the allowed length minimum,
the segment will stop changing while the cursor
will continue its movement.
In the new example – Form_SegmentOnLine.cs –
when the end point cannot move any more, then
mouse cursor does not move either. This happens
when the length of a segment reaches one of its
limits (the SegmentOnLine class has limits
both for minimum and maximum length) and when
you try to move cursor anywhere to the side from
the allowed line. You will not even see such an
Fig.11.25 Form_SegmentOnLine.cs
World of Movable Objects 319 (978) Chapter 11 Movement restrictions
attempt of cursor to jump anywhere from the segment; the cursor will move several pixels aside but will be returned back
immediately and our eye cannot see such quick movements back and forth.
The last step of our imaginary transformation from the previous to the current example declares the change from visible spot
to invisible. This means that we simply get rid of the unneeded part (colored spot) as we already have such invisible spot –
it is the circular node at the end of segment. Segment covers were already shown at figure 2.4; the new class of segments –
the SegmentOnLine class – is going to use exactly the same cover; the only difference is in using the adhered mouse
technique. To organize the use of the adhered mouse, the SegmentOnLine class, in addition to three fields for
visualization, must have fields for form and mover.
public class SegmentOnLine : GraphicalObject
{
Form form;
Mover supervisor;
PointF ptA, ptB;
Pen m_pen;
You can declare an object of the SegmentOnLine class either by two end points or by one end point plus an angle to
another and the length. In both cases during the initialization the proposed length is checked and is set inside the allowed
range.
public SegmentOnLine (Form frm, Mover mvr, PointF pt0, PointF pt1, Pen pn)
{
form = frm;
supervisor = mvr;
ptA = pt0;
double dist = Auxi_Geometry .Distance (pt0, pt1);
if (minLen <= dist && dist <= maxLen)
{
ptB = pt1;
}
else
{
ptB = Auxi_Geometry .PointToPoint (ptA,
Auxi_Geometry .Line_Angle (pt0, pt1), (minLen + maxLen) / 2);
}
m_pen = pn;
}
Segment cover consists of three nodes: two circular nodes at the ends and a strip node along the whole length.
public override void DefineCover ()
{
float radius = Math .Max (3, m_pen .Width / 2);
cover = new Cover (new CoverNode [] {new CoverNode (0, ptA, radius),
new CoverNode (1, ptB, radius),
new CoverNode (2, ptA, ptB, radius)});
}
When segment is pressed, it can be the start of forward movement, resizing, or rotation.
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 SegmentOnLine)
{
SegmentOnLine segment = grobj as SegmentOnLine;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNodeShape == NodeShape .Circle)
{
World of Movable Objects 320 (978) Chapter 11 Movement restrictions
segment .StartLengthChange (mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
segment .StartRotation (e .Location);
}
}
}
}
When any end node is pressed (their numbers are 0 and 1), it is the first step in using the adhered mouse. Cursor must be
moved exactly to the associated end point of segment; at the same time the range for moving this end point is calculated;
everything is done by the SegmentOnLine.StartLengthChange() method.
public void StartLengthChange (int iNode)
{
supervisor .MouseTraced = false;
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);
}
supervisor .MouseTraced = true;
}
When you move an end point, then there is such order of steps.
• Mouse cursor has already moved to the ptM location.
• The point nearest to this point inside the allowed segment [pt0_possible, pt1_possible] is calculated.
• The associated end of segment is placed at the calculated point.
• Mouse cursor is moved into the same point. For this mandatory movement of mouse cursor, the temporary cut of
the link between mover and segment is needed.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (iNode)
{
case 0:
supervisor .MouseTraced = false;
Point_A = Auxi_Geometry .NearestPointOnSegment (ptM,
pt0_possible, pt1_possible);
Cursor .Position = form .PointToScreen (Point .Round (ptA));
bRet = true;
supervisor .MouseTraced = true;
break;
case 1:
supervisor .MouseTraced = false;
World of Movable Objects 321 (978) Chapter 11 Movement restrictions
Point_B = Auxi_Geometry .NearestPointOnSegment (ptM,
pt0_possible, pt1_possible);
Cursor .Position = form .PointToScreen (Point .Round (ptB));
bRet = true;
supervisor .MouseTraced = true;
break;
… …
we need two additional nodes to move the end points, so the number of nodes has to be five or six depending on the gap
angle (figure 11.26). In the current example, I am going to demonstrate really thin arcs. To make their resizing easier, I put
on the end points circular nodes not of the default size but slightly bigger (nrSmall = 5) and put these two nodes at the
head of the cover so that transparent nodes are not going to cut out any part of them. Cover has two variants for arc angles
below and above 180 degrees, but in both variants the first three nodes are identical: two small circular nodes on the end
points and a big transparent circular node to cut out the inner part.
public override void DefineCover ()
{
float rOuter = m_radius + 3;
float rInner = m_radius - 3;
int nNodes;
if (Math .Abs (angleArc) <= Math .PI)
{
nNodes = 6;
}
else
{
nNodes = 5;
}
CoverNode [] nodes = new CoverNode [nNodes];
nodes [0] = new CoverNode (0, Auxi_Geometry .PointToPoint (m_center,
angleStart, m_radius), nrSmall);
nodes [1] = new CoverNode (1, Auxi_Geometry .PointToPoint (m_center,
angleStart + angleArc, m_radius), nrSmall);
nodes [2] = new CoverNode (2, m_center, rInner, Behaviour .Transparent);
… …
If the arc angle is not bigger than 180 degrees (the blue arc from figure 11.26), then two additional transparent nodes are
needed to cut out the remaining part of the ring. Calculation of these two rectangular transparent nodes was already
explained with the CircleSector_Nonresizable class (see figure 8.7 and the explanation near by).
public override void DefineCover ()
{
… …
if (Math .Abs (angleArc) <= Math .PI)
{
//nNodes = 6;
double angleA, angleB;
if (angleArc >= 0)
{
angleB = angleStart;
angleA = angleStart + angleArc;
}
else
{
angleA = angleStart;
angleB = angleStart + angleArc;
}
PointF [] ptsA = Auxi_Geometry .PolygonAroundSemicircle (m_center,
angleA, rOuter, Rotation .Counterclock);
PointF [] ptsB = Auxi_Geometry .PolygonAroundSemicircle (m_center,
angleB, rOuter, Rotation .Clockwise);
nodes [3] = new CoverNode (3, ptsA, Behaviour .Transparent);
nodes [4] = new CoverNode (4, ptsB, Behaviour .Transparent);
}
… …
If the gap angle of the arc is less than 180 degrees (the green arc from figure 11.26), then one additional transparent node is
enough.
World of Movable Objects 323 (978) Chapter 11 Movement restrictions
public override void DefineCover ()
{
… …
else
{
//nNodes = 5;
double angleGap;
if (angleArc > 0)
{
angleGap = angleArc - 2 * Math .PI;
}
else
{
angleGap = angleArc + 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 + angleGap / 2,
dist),
Auxi_Geometry.PointToPoint (m_center, angleStart +angleGap, dist) };
nodes [3] = new CoverNode (3, pts, Behaviour .Transparent);
}
… …
In both cases the last node of the cover is the one that allows to move the arc around the screen. It is a big circular node, but
only a small part of it – exactly the arc – is going to be used because everything else is cut out by the preceding transparent
nodes.
public override void DefineCover ()
{
… …
nodes [nNodes - 1] = new CoverNode (nNodes - 1, m_center, rOuter,
Cursors .SizeAll);
cover = new Cover (nodes);
cover .SetClearance (false);
}
The number of nodes in the cover of such arc and the view of the cover depends on the arc angle. Any arc is changeable, so
its cover has to change every time the arc angle crosses the value of 180 degrees. While writing about the N-node covers, I
explained the possibility of error behaviour when the cover with a changeable number of nodes is changed on a fly and
recommended to change such cover only at the moment when the object is released. At first I did the same with the
Arc_Thin_ChangeableAngle class and it had a very simple Release() method which was called from inside the
OnMouseUp() method of the form.
private void OnMouseUp (object sender, MouseEventArgs e)
{
if (mover .Release ())
{
//GraphicalObject grobj = mover .ReleasedSource;
//if (grobj is Arc_Thin_ChangeableAngle)
//{
// (grobj as Arc_Thin_ChangeableAngle) .Release ();
//}
}
bDrawAuxiCircle = false;
Invalidate ();
}
//public void Release ()
//{
// DefineCover ();
//}
World of Movable Objects 324 (978) Chapter 11 Movement restrictions
However, both the method and its call are commented now in the text because there is no danger in calling the
DefineCover() method from inside the MoveNode() method in this particular case of the
Arc_Thin_ChangeableAngle class. There is no danger because in both variants of covers the small circular nodes
on the end points always have the numbers 0 and 1. If you want, you can take out the lines with the call of
DefineCover() method from inside the MoveNode() method, but then you need to return into working condition
the commented lines. (From time to time I forget that some commented lines must be left as they are because they are used
for explanation in the book and mistakenly delete such lines. Anyway, if I do the same mistake again, you can reinstall
these lines of code from here.)
When the changeable arc is caught by mover, then it can be the start of the length change or rotation; in both cases special
methods of the Arc_Thin_ChangeableAngle class must be called.
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 Arc_Thin_ChangeableAngle)
{
Arc_Thin_ChangeableAngle arc = grobj as Arc_Thin_ChangeableAngle;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode < 2)
{
arc .StartResizing (mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
arc .StartRotation (e .Location);
float rad = arc .Radius;
rcAuxi = new RectangleF (arc .Center .X - rad,
arc .Center .Y - rad, 2 * rad, 2 * rad);
bDrawAuxiCircle = true;
Invalidate ();
}
}
}
}
The StartResizing() method is called when any of two small circular nodes is pressed and this method has to do two
things. The first one is the tiny move of the mouse cursor exactly to the arc end point.
public void StartResizing (int iCaughtNode)
{
if (iCaughtNode == 1)
{
ptEnd = Auxi_Geometry .PointToPoint (m_center, angleStart + angleArc,
m_radius);
}
else
{
ptEnd = Auxi_Geometry .PointToPoint (m_center, angleStart, m_radius);
}
AdjustCursorPosition (ptEnd);
… …
The second thing is the calculation of the arc angle range in which the caught end point can be moved. I put into the code of
the Arc_Thin_ChangeableAngle class two restrictions on the arc angle, so this angle can be changed between 10
and 350 degrees.
protected double minArc_Degree = 10;
World of Movable Objects 325 (978) Chapter 11 Movement restrictions
protected double maxArc_Degree = 350;
But these are the limits for arc angle, while the caught end point might have any angle and I need to calculate the limits for
moving this caught end. Several pages back I wrote about the spot on the arc trail and about the problem of the spot trying
to jump over a small gap from one end point to another. Now that spot is transformed into a small circular node at the arc
end and we have exactly the same problem of the end point trying to move beyond the limit in an attempt to turn an arc into
the closed circle. I do not want to allow it and the needed calculations are in the second part of the StartResizing()
method; these calculations are a bit lengthy to be included into the book.
Now everything is ready for the move of the caught end point and the MoveNode() method is responsible for this
movement. Any new cursor position has to be transformed into the new end point position. Nobody expects that the mouse
is going to move exactly along the circle. The nearest point on the circle is considered as the proposed new end point. If it
is inside the allowed range, then the end point is moved into this point and the cursor is also placed on the circle. If this
proposed point is outside the allowed range, then the end point is calculated according to this range and cursor is moved into
this point. For each cursor relocation, there is a temporary cut of the link between mover and arc.
The arcs of the Arc_Thin_ChangeableAngle class can be moved, rotated, and their angle (and thus their length) can
be changed. One thing is still missing: user cannot change the arc radius. Let us add this feature.
File: Form_Arcs_FullyChangeable.cs
Menu position: Graphical objects – Basic elements – Arcs – Fully changeable arcs
Before we start to design the class of fully changeable arcs, we have to decide about the way we are going to change the arc
radius. The new class – Arc_Thin_FullyChangeable – differs from the previous class
Arc_Thin_ChangeableAngle by one feature only, so it will be derived from the previous class and I would like it to
inherit also the way users have to work with it.
public class Arc_Thin_FullyChangeable : Arc_Thin_ChangeableAngle
{
Pen penWider;
int nrForMiddle = 8;
PointF ptInnerLimit, ptOuterLimit;
New arc must be rotated and moved forward by any inner point, while its length can be changed by moving the end points.
What options we have to organize the radius change?
The standard solution for any object covering some area is to resize it by border and move it by inner points; unfortunately,
this rule is not suitable for a line which has no inner area and consists entirely of the border. For such object we have to
make some decision and divide the border on the parts to move an object and the parts to resize it. From my point of view,
for the arc it would be enough to have a single place to change the radius, while the entire remaining area (arc, border) is
still used to move an arc around the screen. I would suggest to use the middle point of the arc for changing its radius; this is
not absolutely new idea as similar solution is used for crescent (see Form_Crescent.cs at figure 8.5 in the chapter
Transparent nodes). Because this solution about the place of radius change is not obvious for users, this special part of the
arc must differ visually from the whole
arc. I see two ways to distinguish this
area: either to use different color or
change the width; in the
Arc_Thin_FullyChangeable
class the width of this small area is
changed.
Now, when the decision about the place
for radius change is made, we have to
design the cover for the new class. The
special area must be covered by an
additional node; this is the only
difference in cover from the basic class.
The special place is made obvious by
increasing its width, so I am going to use
a circular node over it with radius
slightly bigger than for the nodes on the
end points. Figure 11.27 shows the
Form_Arcs_FullyChangeable.cs with
Fig.11.27 Arc_Thin_FullyChangeable objects and their covers
World of Movable Objects 326 (978) Chapter 11 Movement restrictions
}
public WaySegmentType SegmentType
{
get { return (segment_type); }
}
public PointF PointStart
{
get { return (ptStart); }
}
public PointF PointFinish
{
get { return (ptFinish); }
}
}
I think that any complex way can be described by a system of straight segments and arcs of different radius and length, so to
describe the type of each segment I have the enumeration consisting of two elements
public enum WaySegmentType { Line, Arc };
Now I have to organize two derived classes of segments. For straight segments I have the Way_Line class. Any object
of this class can be initialized either by two end points or by one end point plus the length and the angle to another. I am not
sure for what purpose I set the limit on minimum allowed length of such segments but I did it. The angle of any straight
segment is calculated from its ptStart point to its ptFinish point.
World of Movable Objects 329 (978) Chapter 11 Movement restrictions
public class Way_Line : Way_Segment
{
double m_angle;
static double minLength = 10;
// -------------------------------------------------
public Way_Line (PointF pt0, PointF pt1)
{
segment_type = WaySegmentType .Line;
ptStart = pt0;
m_angle = Auxi_Geometry .Line_Angle (pt0, pt1);
if (Auxi_Geometry .Distance (pt0, pt1) >= minLength)
{
ptFinish = pt1;
}
else
{
ptFinish = Auxi_Geometry.PointToPoint (ptStart, m_angle, minLength);
}
}
// -------------------------------------------------
public Way_Line (PointF pt0, double angleDegree, double len)
{
segment_type = WaySegmentType .Line;
ptStart = pt0;
m_angle = Auxi_Convert .DegreeToRadian (angleDegree);
len = Math .Max (Math .Abs (len), minLength);
ptFinish = Auxi_Geometry .PointToPoint (ptStart, m_angle, len);
}
The class for arc segments – Way_Arc – is at the same level of simplicity though it requires slightly more lines of code
for initialization.
public class Way_Arc : Way_Segment
{
PointF m_center;
float m_radius;
double angleStart, angleArc;
RectangleF rcAroundCircle;
double angleStart_Deg, angleArc_Deg; // only for drawing
static double minLength = 15;
static float minArcRadius = 20;
// -------------------------------------------------
public Way_Arc (PointF ptArcCenter, int nArcRadius,
double angleStart_Degree, double angleArc_Degree)
{
segment_type = WaySegmentType .Arc;
m_center = ptArcCenter;
m_radius = Math .Max (nArcRadius, minArcRadius);
angleStart_Deg = angleStart_Degree % 360;
angleArc_Deg = Math .Min (Math .Max (-360, angleArc_Degree), 360);
double angleFinish_Degree = angleStart_Deg + angleArc_Deg;
ptStart = Auxi_Geometry .PointToPoint (m_center,
Auxi_Convert .DegreeToRadian (angleStart_Deg), m_radius);
ptFinish = Auxi_Geometry .PointToPoint (m_center,
Auxi_Convert .DegreeToRadian (angleFinish_Degree), m_radius);
angleStart = Auxi_Convert .DegreeToRadian (angleStart_Deg);
angleArc = Auxi_Convert .DegreeToRadian (angleArc_Deg);
rcAroundCircle = new RectangleF (m_center .X - m_radius,
m_center .Y - m_radius, 2 * m_radius, 2 * m_radius);
}
World of Movable Objects 330 (978) Chapter 11 Movement restrictions
Another constructor of the Way_Arc class allows to initialize an arc by one end point and an arc angle (angle to another
end point).
By using these classes Way_Line and Way_Arc, it is possible to define any system of connected roads. Now we can
look at the spot which is going to move along these roads. It is similar to spots which were used in the previous examples,
so a lot of things will be familiar. For spot visualization only the central point, radius, and color are needed. The spot is
kept on the line by the same technique of adhered mouse, so two more fields – form and mover – are also needed. The road
system is described as a List of segments.
public class SpotOnSegments : GraphicalObject
{
Form form;
Mover supervisor;
PointF m_center;
float m_radius = 5;
Color clr = Color .Red;
List<Way_Segment> segments = new List<Way_Segment> ();
// -------------------------------------------------
public SpotOnSegments (Form frm, Mover mvr, List<Way_Segment> segs,
PointF pt, float r, Color clrSpot)
{
form = frm;
supervisor = mvr;
segments = segs;
m_center = NearestPoint (pt);
m_radius = r;
clr = clrSpot;
}
Among the parameters of initialization is the spot position, but it does not mean that the spot is placed exactly at this point.
You can calculate the point at which you want initially to put the spot. If your calculations are correct, then the spot is
placed at this point, but in any case the calculation of the nearest point among all the segments is done with the
NearestPoint() method and the spot is placed at that calculated point.
private PointF NearestPoint (PointF pt)
{
PointF ptNearest, ptNearest_2;
double dist_2;
double dist = segments [0] .DistanceToSegment (pt, out ptNearest);
for (int i = 1; i < segments .Count; i++)
{
dist_2 = segments [i] .DistanceToSegment (pt, out ptNearest_2);
if (dist > dist_2)
{
dist = dist_2;
ptNearest = ptNearest_2;
}
}
return (ptNearest);
}
Whenever the spot is moved, the same NearestPoint() method is used to find the nearest point on segments; the spot
is moved to this point and cursor goes with it. The nearest point is always on one of segments, so the spot cannot go
anywhere from the road system.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
m_center = NearestPoint (ptM);
DefineCover ();
World of Movable Objects 331 (978) Chapter 11 Movement restrictions
AdjustCursorPosition (m_center);
bRet = true;
}
return (bRet);
}
File: Form_SpotOnConnectedSegments.cs
Menu position: Movement restrictions – Spot on connected segments
Moving of the colored spot along the system of connected segments is demonstrated in the
Form_SpotOnConnectedSegments.cs. This simple set of segments (figure 11.29) demonstrates both the easiness of
movement along the connected
segments and some possible
problems. For the better explanation,
I added the numbers of segments on
the figure; in the real application they
are not shown.
There are four segments in the set and
there are different possibilities for
going from one segment to another.
• When a segment is
continued by another one,
like connection between
segments 1 and 2, then there
are no variants and the spot
goes smoothly from one
segment to another.
• In the same way you can Fig.11.29 Colored spot on a set of connected segments
move the spot from segment
3 (or segment 1) to segment 0; it is enough to move the cursor farther left and the spot is going in the expected
direction.
• When you move the spot along segment 0 to the right, then further way depends on your wish: try to move the
cursor (spot) to the right and up – it will go on segment 3; try to move it right and down – it will continue along
segment 0. Everything goes as you want and expect.
• Now move the spot slowly to the left along the segment 3 and try to move the cursor down on approaching the
joint point of three segments; chances are high that on slow movement the spot will switch from segment 3 to
segment 1. If we look at the moving spot as a train on the rails, then such turn is too sharp and impossible for any
train. However, in our algorithm the NearestPoint() method looks through all the segments of the set and
allows such turn because, when you push the cursor down, the nearest point will be on segment 1 and not on
segment 0.
If we want to prohibit the direct turn from segment 3 to segment 1 and vice versa, then some changes must be done in the
algorithm of selecting the best segment and maybe some changes in the class. I want to keep the code simple, so the new
version is shown in the separate example.
File: Form_SpotOnCommentedWay.cs
Menu position: Movement restrictions – Spot on the way
In the name of the file with the new example you can see the word Commented. When I tried to explain the right and wrong
movements of the spot in the previous example Form_SpotOnConnectedSegments.cs, I had to add the numbers of
segments at figure 11.29 artificially; these numbers are not shown in the working application. The new example uses much
more complex set of trails and I change them from time to time. If each segment has some comment, for example, a
number, then the construction and the explanation are much easier. In the previous example such set of classes is used:
Way_Segment, Way_Line, and Way_Arc. In the new example similar set of classes is used to describe the elements of
the road system: Way_SegmentCommented, Way_LineCommented, and Way_ArcCommented. The difference
between two sets of classes can be seen by looking at the Way_SegmentCommented class and comparing it with the
Way_Segment class.
World of Movable Objects 332 (978) Chapter 11 Movement restrictions
public abstract class Way_SegmentCommented
{
protected WaySegmentType segment_type;
protected PointF ptStart, ptFinish;
protected CommentToCircle m_comment;
public abstract double Length { get; }
public abstract PointF Center { get; }
public abstract void Draw (Graphics grfx, Pen pen, bool bMarkEnds,
Color clrEnds);
public abstract double DistanceToSegment (PointF pt, out PointF ptNearest);
public abstract CommentToCircle Comment { get; }
While drawing new segments, their end points can be marked with different color; this makes easier the understanding of
the whole set of segments.
New segments have a Center property. For the arc segment, it is the center around which the arc is constructed; for the
straignt segment, it is the middle point between its ends.
Any new segment may have a comment of the CommentToCircle class. This class is derived from the
Text_Rotatable class and is included into the MoveGraphLibrary.dll but it is used not too often. In the big Demo
application accompanying this book the CommentToCircle class is used in the Form_PlotsVariety.cs, but this
example will appear only in the chapter Data visualization somewhere far ahead, so I need to give some explanations on the
rules of positioning CommentToCircle objects. It is obvious from the name that such comments are designed to be
used with the circles, while in the current example they are used both with arcs and straight segments.
When a CommentToCircle object is used with some arc, then it is positioned in relation to the circle of which this arc
is only a part. The central point of comment is described by an angle from the circle center and additional coefficient.
When the coefficient is inside the [0, 1] range, then this comment is inside the circle: 0 means the center of the circle, while
coefficient 1 puts the comment on the border. When the coefficient is greater than 1, then this comment is placed outside
the circle and the coefficient means the distance in pixels from the border of the circle to the central point of comment.
World of Movable Objects 333 (978) Chapter 11 Movement restrictions
When a CommentToCircle object is used with straight segment, then you need to imagine the circle with the center in
the middle of segment and the radius equal to half of segment length. Thus, both end points of straight segment are on the
border of this imaginable circle and you position the comment in relation to this imaginable circle.
The Form_SpotOnCommentedWay.cs (figure 11.30) allows to work with different ways by changing the number of
segments; there is a combo box in the form to do it. This example starts with four segments connected one after another
(figure 11.31a). If you increase gradually the number of segments, then somewhere throughout this process you have a
system of 11 segments (figure 11.31b), while figure 11.31c shows the maximum system of 18 segments. The same
maximum way is better seen at figure 11.30.
segmentsInView contains all segments shown at each particular moment. Numeric control allows to change the
number of such segments and the List is populated by the SetCurrentWay() method.
private void SetCurrentWay (int nSegments)
{
segmentsInView .Clear ();
for (int i = 0; i < nSegments; i++)
{
segmentsInView .Add (maximumWay [i]);
}
}
segmentsAvailable includes only few segments at each moment. This list contains the segment currently used by
the spot and the connected segments to which the spot can move directly from this one.
Depending on the current configuration of the shown segments (segmentsInView) and the
position of the spot, this list contains between two and four elements (for this particular
maximumWay), but the population of this List is an interesting process.
Let us check how it works when the Form_SpotOnCommentedWay.cs is started for the first time.
private void OnLoad (object sender, EventArgs e)
{
RestoreFromRegistry ();
if (bRestore == false)
{
… …
SetMaximumWay ();
SetCurrentWay (4);
spot = new SpotOnCommentedWay (this, mover, segmentsInView,
new PointF (400, 100), 7, Color .Magenta);
int iUsedSeg = spot .CurrentlyUsedSegment;
PrepareAvailableWay (iUsedSeg);
spot .SetWay (segmentsAvailable);
iCurrentlyUsedFromAvailable = spot .CurrentlyUsedSegment;
}
btnHelp .Enabled = !info .Visible;
RenewMover ();
bAfterInit = true;
}
First, maximumWay is populated by the SetMaximumWay() method.
Next, the initial road system is limited to four segments by calling the SetCurrentWay() method with appropriate
parameter, so after this call the segmentsInView includes four segments.
Then the spot is initialized.
spot = new SpotOnCommentedWay (this, mover, segmentsInView,
new PointF (400, 100), 7, Color .Magenta);
Among the parameters, the SpotOnCommentedWay constructor gets the currently organized system of segments
(segmentsInView) on which the spot has to appear. Though another parameter of the same constructor declares the
point where the spot must appear, this point is used only as the first approximation. The spot is positioned there but after it
the nearest point on the available segments is found and the spot is placed at this point. Thus, the spot cannot be placed
anywhere except the available segments.
After the spot is placed on some segment, it can return the number of this segment.
int iUsedSeg = spot .CurrentlyUsedSegment;
With this known number and currently used system of segments, the short list of really available segments at this moment –
segmentsAvailable – can be populated by the PrepareAvailableWay() method. This short list always
includes the segment on which the spot is currently positioned and the segments to which the spot can move directly from
this segment. Those available segments depend on the current configuration.
World of Movable Objects 335 (978) Chapter 11 Movement restrictions
private void PrepareAvailableWay (int iCurrentlyUsedSegment)
{
segmentsAvailable .Clear ();
numbersAvailable .Clear ();
int nSegments = Convert .ToInt32 (numericUD_Trails .Value);
MakeSegmentAvailable (iCurrentlyUsedSegment);
switch (iCurrentlyUsedSegment)
{
case 0:
if (nSegments >= 2) MakeSegmentAvailable (1);
if (nSegments >= 6) MakeSegmentAvailable (5);
break;
case 1:
MakeSegmentAvailable (0);
if (nSegments >= 3) MakeSegmentAvailable (2);
break;
… …
By using several calls of the MakeSegmentAvailable() method, two lists are prepared: one –
segmentsAvailable – contains the available segments, another – numbersAvailable – only numbers of these
segments.
private void MakeSegmentAvailable (int i)
{
segmentsAvailable .Add (segmentsInView [i]);
numbersAvailable .Add (i);
}
The short list segmentsAvailable contains all segments available for the spot at the current moment; all other
segments simply do not exist for the spot. Then this short list is sent to the spot as the full system of segments and the spot
returns the number of the occuped segment from this short list (iCurrentlyUsedFromAvailable).
spot .SetWay (segmentsAvailable);
iCurrentlyUsedFromAvailable = spot .CurrentlyUsedSegment;
Similar exchange of information between the spot and the form happens when the spot is moved.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is SpotOnCommentedWay)
{
int iNewSegment = spot .CurrentlyUsedSegment;
if (iNewSegment != iCurrentlyUsedFromAvailable)
{
PrepareAvailableWay (numbersAvailable [iNewSegment]);
spot .SetWay (segmentsAvailable);
iCurrentlyUsedFromAvailable = spot .CurrentlyUsedSegment;
}
}
Invalidate ();
}
}
When the spot is moved, its MoveNode() method is called. The nearest point on the available segments is found; the
spot is moved into this point, and the number of this segment is returned into the OnMouseMove() method.
int iNewSegment = spot .CurrentlyUsedSegment;
If this number differs from the previously occupied segment, then it means that the spot moved from one segment to
another. In such case the new available system of segments must be prepared; this new system of available segments is sent
to the spot, and the spot returns the number of the newly occupied segment among the new set of available segments.
World of Movable Objects 336 (978) Chapter 11 Movement restrictions
if (iNewSegment != iCurrentlyUsedFromAvailable)
{
PrepareAvailableWay (numbersAvailable [iNewSegment]);
spot .SetWay (segmentsAvailable);
iCurrentlyUsedFromAvailable = spot .CurrentlyUsedSegment;
}
There can be an arbitrary combination of segments; they can cross each other; arcs can be so lengthy that with a tiny
addition the way can cross itself (a real pigtail road). At the same time, there are no problems with the spot going over such
crossing and the spot never jumps to the wrong way. There are no problems at such crossings because at any moment the
spot has a list of available segments; if another road on the crossing is not included into this list, then the possibility of the
spot switching to this road is not even considered.
In the previous example with the spots on arcs we had a problem of the spot trying to jump over a small gap; solution of this
problem required some additional code which is not always easy to understand. There is no such code in the current
example because the solution is absolutely different: do not use the arcs which are close to the full circle. If you need to
design such a way, divide such arc into three or four. You can see such solution in the Form_SpotOnCommentedWay.cs
with segments 2, 3, and 4 (figure 11.30). These three arcs belong to the same circle, so I could easily unite them into one
arc, for example, with number 2. But then with really quick movement of the spot it can jump from one end of long
segment 2 to another end of the same segment because the current segment is always among the available. To prohibit such
movement, this nearly closed circular way is divided into three arcs. It would be enough to divide such big arc into two, for
example, by uniting segments 2 and 3. Because I needed a point of connection for segment 17, I decided to divide my
nearly full circle into three parts.
*
This example uses two images of this dog which are saved as two files in the …\WorldOfMoveableObjects\bin\Release
subdirectory.
World of Movable Objects 337 (978) Chapter 11 Movement restrictions
The problem started on resizing by sides, but we’ll come to this resizing step by step.
The Rectangles_FixedRatioAdh class has a minimum allowed size which excludes the disappearance of rectangles
after squeezing. I also added the possibility of declaring minimum and maximum allowed sizes for rectangles.
public Rectangle_FixedRatioAdh (Form frm, Mover mvr,
RectangleF rect, Color color,
float minW, float maxW, float minH, float maxH)
Declaring minimum allowed sizes for rectangle does not mean that on squeezing a rectangle it will diminish to exactly these
sizes. The most important is the sides ratio which is calculated from the initial rectangle size. But if minimum sizes are
among the parameters, then such rectangle can be squeezed only until the moment when one of these limits is reached. This
is going to be the real minimum allowed rectangle with current ratio; this rectangle has minimum sizes (minWidth,
minHeight) and minimum diagonal minDiagonal. The same happens with maximum allowed sizes (maxWidth,
maxHeight) and maximum diagonal maxDiagonal. In the current example, two rectangles – yellow and cyan – are
declared with limitations on their sizes.
private void OnLoad (object sender, EventArgs e)
{
… …
rects .Add (new Rectangle_FixedRatioAdh (this, mover,
new Rectangle (100, 160, 70, 120), Color .Yellow,
60, 600, 50, 600));
rects .Add (new Rectangle_FixedRatioAdh (this, mover,
new Rectangle (320, 210, 190, 120), Color .Cyan,
40, 400, 50, 350));
… …
There is another and simpler constructor without four last parameters. In such case minimum size is set at 20 pixels
(minsize = 20) while the enlarging will be stopped only by the form sides.
Four circular nodes on corners, four thin nodes along borders, and one big node for the whole area – this is a classical cover
for rectangles, so the standard Cover constructor for rectangles can be used.
public override void DefineCover ()
{
cover = new Cover (rc, Resizing .Any);
}
I am going to use the adhered mouse for resizing. This means that if rectangle is pressed anywhere near border, then its
position is adjusted directly to the border and beginning from this moment the mouse position will determine the new border
position. The initial adjustment of cursor position requires a temporary disconnection between mover and the caught object
(rectangle); also the limits for cursor movement must be calculated. Both things are done by the
Rectangles_FixedRatioAdh.StartResizing() method which is called at the starting moment of resizing.
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 Rectangle_FixedRatioAdh)
{
if (e .Button == MouseButtons .Left)
{
rectPressed = grobj as Rectangle_FixedRatioAdh;
rectPressed .StartResizing (e .Location, mover .CaughtNode);
… …
I’ll be explaining some situations started on resizing by upper corners and upper side, so I want to remind about some node
numbers in this standard cover. Circular node in the top left corner has number 0; circular node in the top right corner has
number 1; upper side is covered by node number 6. To make explanations more obvious, I added the drawing of some
auxiliary line, so throughout the resizing of any rectangle you can see its minimal allowed size, maximum allowed size, and
the line along which the cursor has to move (figures 11.35). If you press a rectangle for which the maximum size is not set,
World of Movable Objects 340 (978) Chapter 11 Movement restrictions
then you do not see the bigger rectangle and the beam is going up to the form border. For my explanation I need figures
with bigger rectangle in view, so yellow rectangle is the best.
Suppose that you press on the top left corner of yellow rectangle (figures 11.35a). The bottom right corner will be the base
corner which is not going to move while the caught corner can be moved along the inclined line between maximum and
minimum allowed sizes for this rectangle.
Fig.11.35a Pressed on the Fig.11.35b Pressed on the Fig.11.35c Pressed on the Fig.11.35d Pressed on the
top left corner left half of the upper right half of the upper top right corner
(iNode = 0) side (iNode = 6) side (iNode = 6) (iNode = 1)
If you press the top right corner of rectangle (figures 11.35d), then you have similar situation with the bottom left corner
used as the base point. Thus, for the cases of the corner nodes the StartResizing() method is simple.
public void StartResizing (Point ptMouse, int iNode)
{
if (iNode == 8)
{
return;
}
PointF [] corner = Auxi_Geometry .CornersOfRectangle (rc);
int cx, cy;
PointF ptCursor;
if (iNode < 4) // corners
{
ptCursor = corner [iNode];
iBase = (iNode + 2) % 4;
ptBase = corner [iBase];
if (iNode == 0) // LT corner
{
angleBeam = Math .PI - angle;
}
else if (iNode == 1) // RT corner
{
angleBeam = angle;
}
else if (iNode == 2) // RB corner
{
angleBeam = -angle;
}
else // iNode == 3 // LB corner
{
angleBeam = angle - Math .PI;
World of Movable Objects 341 (978) Chapter 11 Movement restrictions
}
ptEndIn = Auxi_Geometry .PointToPoint (ptBase, angleBeam, minDiagonal);
if (bMaxSizeLimited)
{
ptEndOut =
Auxi_Geometry .PointToPoint (ptBase, angleBeam, maxDiagonal);
}
else
{
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam, 4000);
}
}
… …
AdjustCursorPosition (ptCursor);
}
In the old Rectangles_FixedRatio class of rectangles with fixed sides ratio, there is no adhered mouse. If the upper
border is pressed, then vertical movement of cursor determines the new height, while the new width is calculated according
to the fixed ratio. When the upper border is pressed, you can also move the cursor horizontally and such movement does
not affect anything.
In the new class Rectangles_FixedRatioAdh, the adhered mouse is used when any corner is pressed and it would be
natural to expect the same cursor behaviour when any side is pressed. For adhered mouse, there is no such thing as
movement without reaction. At first I thought about the simplest cursor trajectory: if you press the upper border, then
cursor is allowed to move only vertically as it is the best and natural trajectory to change the height. Then I understood that
there was a problem..
Suppose that you press at the upper border of rectangle somewhere closer to the top left corner (figure 11.35b) and start
moving the cursor down. The height is decreasing and the width is decreasing according to the sides ratio. At some
moment the left border of rectangle will move slightly to the right from cursor coordinate and when the cursor will move
down and will try to catch the rectangle (to reinstall the link between mover and rectangle), there will be nothing. Vertical
trajectory for cursor cannot be used.
When you press on the corner or somewhere on border near this corner you expect similar reactions, so I decided that if the
upper border is pressed closer to the top left corner, then the same base corner is used (the bottom right corner). If cursor
moves along the inclined trajectory leading to the same corner, then this cursor will never miss the rectangle and the
resizing can go without problems. For resizing started on some side, another part of the StartResizing() method is
used.
public void StartResizing (Point ptMouse, int iNode)
{
… …
else // sides
{
if (iNode == 4) // left side
{
cy = ptMouse .Y;
ptCursor = new PointF (rc .Left, cy);
iBase = (cy - rc .Top >= rc .Height / 2) ? 1 : 2;
}
else if (iNode == 5) // right side
{
cy = ptMouse .Y;
ptCursor = new PointF (rc .Right, cy);
iBase = (cy - rc .Top >= rc .Height / 2) ? 0 : 3;
}
else if (iNode == 6) // top
{
cx = ptMouse .X;
ptCursor = new PointF (cx, rc .Top);
iBase = (cx - rc .Left >= rc .Width / 2) ? 3 : 2;
}
World of Movable Objects 342 (978) Chapter 11 Movement restrictions
else // iNode == 7 // bottom
{
cx = ptMouse .X;
ptCursor = new PointF (cx, rc .Bottom);
iBase = (cx - rc .Left >= rc .Width / 2) ? 0 : 1;
}
ptBase = corner [iBase];
angleBeam = Auxi_Geometry .Line_Angle (ptBase, ptCursor);
if (iNode == 4 || iNode == 5)
{
ptEndIn = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
Math .Abs (minWidth / Math .Cos (angleBeam)));
}
else
{
ptEndIn = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
Math .Abs (minHeight / Math .Sin (angleBeam)));
}
if (bMaxSizeLimited)
{
if (iNode == 4 || iNode == 5)
{
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
Math .Abs (maxWidth / Math .Cos (angleBeam)));
}
else
{
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
Math .Abs (maxHeight / Math .Sin (angleBeam)));
}
}
else
{
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam, 4000);
}
}
… …
When any corner or side of rectangle is pressed, then the StartResizing() method calculates two end points of
segment for cursor movement – ptEndIn and ptEndOut. These points are used in the OnMoveNode() method to
adjust the cursor position (ptM) to the nearest point inside the segment (ptNearest).
When some corner is moved, the OnMoveNode() method is simpler because this point (ptNearest) also determines the
new position of the moving corner. The only unchangeable corner throughout such resizing is always the opposite corner; it
has the ptBase coordinate and the new sizes of rectangle are calculated between these two points.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
if (iNode == 8)
{
{
Move (dx, dy);
}
else
{
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
if (iNode < 4)
World of Movable Objects 343 (978) Chapter 11 Movement restrictions
{
rc .Width = Math .Abs (ptBase .X - ptNearest .X);
rc .Height = Math .Abs (ptBase .Y - ptNearest .Y);
}
switch (iNode)
{
case 0: // LT corner
rc .Location = ptNearest;
break;
case 1: // RT corner
rc .Location = new PointF (ptBase .X, ptNearest .Y);
break;
case 2: // RB corner
break;
case 3: // LB corner
rc .Location = new PointF (ptNearest .X, ptBase .Y);
break;
… …
}
AdjustCursorPosition (ptNearest);
… …
When some side is moved, the code of the OnMoveNode() method is more complicated but not too much. First, only
one size of rectangle can be calculated from the difference between ptBase and ptNearest while another size is
calculated by using the sides ratio. Also, one of two corners on the opposite side can be used as the base point and this
gives variants in calculations. The code below is for the case of moving the upper side; it is easier to understand this piece
of code because there are figures 11.35b and 11.35c.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
switch (iNode)
{
… …
case 6: // top side
rc .Height = ptBase .Y - ptNearest .Y;
rc .Width = (float) (rc .Height / Math .Tan (angle));
if (ptNearest .X > ptBase .X)
{
rc .Location = new PointF (ptBase .X, ptNearest .Y);
}
else
{
rc .Location = new PointF (ptBase .X - rc .Width,
ptNearest .Y);
}
break;
… …
}
AdjustCursorPosition (ptNearest);
… …
Remark on the used idea and possiible improvement. Figures 11.35 explain the idea of base point selection. When you
press two nearby points on the border to start resizing, you expect that these resizings will go in similar way. That was my
reason to select one or another corner on the opposite side as the base (unmovable) point throughout the resizing. However,
if you press two neighbouring points near the middle of the border, it is possible that for these points the base corners will
be different. Maybe it is not a big problem because when you press near the middle, then it doesn’t matter which of the
corners is selected.
There exists an improvement of the current algorithm which eliminates the abrupt change of the base point. This improved
algorithm has slightly longer code and I do not demonstrate it but I’ll explain the idea. The base point is not going to be
World of Movable Objects 344 (978) Chapter 11 Movement restrictions
only one or another corner on the opposite side but simmetrically positioned point on the opposite side. When you press
some corner to move, then this will be the opposite corner; for moving the corners there is no difference at all. When you
press anywhere else on the border, then symmetrically positioned point on the opposite side is declared as the base point and
the only point which is not going to move. This base point divides the opposite side into two segments which will change
their length proportionally to this division. Thus, both corners of the bottom side will move when you change the rectangle
height, but the length of rectangle will be still set according to the sides ratio. In such improved algorithm, if you press the
border at the middle of the upper side, the base point will be at the middle of the lower side and both halves of the lower
side will change identically.
Such improvement of the currently used algorithm does not require too much work. Maybe I’ll show it in the next variant,
but you can easily do it yourself.
File: Form_Rectangles_AllMovementsAdh.cs
Menu position: Graphical objects – Basic elements – Rectangles – All movements plus adhered mouse
After using the adhered mouse technique with
rectangles which are resized in a very special
way (fixed ratio of sides) I decided to apply
the same technique to the most popular
rectangles which can be moved, resized, and
rotated without any additional regulations.
Objects of the
Rectangle_AllMovements class were
introduced in the
Form_Rectangles_AllMovements.cs
(figures 4.4) when I first explained the
rotation of rectangles. I am going to use that
old class as the base class for new rectangles.
That base class has minimal allowed size of
rectangles and I decided not to add any more
limitations. If you want some rectangle with
additional possible limitations on minimal and
maximum sizes, you can copy some code from Fig.11.36 Cover is the same as in the base class
the Rectangles_FixedRatioAdh class which is used in the previous example.
public class Rectangle_AllMovementsAdh : Rectangle_AllMovements
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut, ptOnOppositeSide;
PointF [] ptsAllowed = new PointF [4];
The adhered mouse is used throughout the resizing. As usual, at the starting moment of resizing some calculations are
needed, so the StartResizing() method is called. Throughout the resizing, this
Form_Rectangles_AllMovementsAdh.cs (figures 11.36) shows some auxiliary lines which makes the resizing
possibilities clearer. Coordinates which are needed for these auxiliary lines are obtained after the call of the
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 Rectangle_AllMovementsAdh)
{
rectPressed = grobj as Rectangle_AllMovementsAdh;
if (e .Button == MouseButtons .Left)
{
rectPressed .StartResizing (e .Location, mover .CaughtNode);
if (mover .CaughtNode < 4)
{
World of Movable Objects 345 (978) Chapter 11 Movement restrictions
ptsAreaAllowed = rectPressed .AllowedArea;
}
else if (mover .CaughtNode < 8)
{
ptEndIn = rectPressed .InnerEnd;
ptEndOut = rectPressed .OuterEnd;
}
… …
Though this and previous example uses rectangles with similar covers – four
nodes on corners, four nodes on sides, and the big rectangular node for the
whole area – the order of nodes on corners and sides is different.
Figure 11.37 shows the cover for the Rectangle_AllMovementsAdh
class together with the node numbers.
The StartResizing() method has two absolutely different parts for the
cases of the caught corners and sides. The parts are different because
depending on whether a corner or a side is pressed, further allowed cursor
movements are different. Both cases are illustrated by seprate figures on
which the area (or allowed path) for the pressed mouse are shown with
auxiliary lines. Cyan rectangle from figure 11.38 has the same numbering of Fig.11.37 Cover and the node numbers
nodes as similar rectangle from figure 11.37.
Suppose that the upper corner of rectangle (iNode = 0) from figure 11.37 is pressed. There is minimal allowed sizes for
rectangles, so the caught corner cannot go below or to the left of two shown grey lines. The area allowed for movement of
the caught node is seen at figure 11.38 as the right angle. On the screen you will see these two lines going to the form
borders. In reality this area is described as a convex polygon going far beyond the form borders.
Throughout the movement of the caught corner, three corners of rectangle change
their positions and only the corner opposite to the caught one does not move. This
corner is covered by the iBase node and its position is ptBase. Corner nodes
are numbered in the counterclock order and the coordinates of all corners are
known, so it is easy to calculate the angle from the iBase node to the next one; in
our case this is the angle of the lower side. Knowing this angle and minimal
allowed sizes for rectangle, it is easy to calculate the apex of our right corner
(tsAllowed[0]). From this point other three points for big convex polygon are
calculated.
public void StartResizing (Point ptMouse, int iNode) Figure 11.38 The upper corner of
{ rectangle is pressed; its
if (iNode == 8) allowed area for movement
{ is limited by two grey lines.
return;
}
PointF ptNearest;
if (iNode < 4) // corners
{
ptNearest = pts [iNode];
int iBase = (iNode + 2) % 4;
PointF ptBase = pts [iBase];
double angleToNext = Auxi_Geometry .Line_Angle (pts [iBase],
pts [(iBase + 1) % 4]);
ptsAllowed [0] = Auxi_Geometry .PointToPoint (ptBase,
angleToNext + Math .PI / 4, minSide * Math .Sqrt (2));
ptsAllowed [1] = Auxi_Geometry .PointToPoint (ptsAllowed [0],
angleToNext, 5000);
ptsAllowed [2] = Auxi_Geometry .PointToPoint (ptsAllowed [0],
angleToNext + Math .PI / 4, 7000);
ptsAllowed [3] = Auxi_Geometry .PointToPoint (ptsAllowed [0],
angleToNext + Math .PI / 2, 5000);
}
… …
World of Movable Objects 346 (978) Chapter 11 Movement restrictions
Now suppose that the right side of rectangle (iNode = 7) is pressed. Two points are calculated: ptNearest on the
caught side and ptOnOppositeSide on the opposite side of rectangle. Cursor is allowed to move only along the beam
which is orthogonal to the caught side (figure 11.39); the angle of this line is angleBeam. There is minimal allowed
size for rectangles, so one end of the beam is minSide away from the ptOnOppositeSide; this is the ptEndIn
point. Another end of the allowed way is set far away at ptEndOut.
public void StartResizing (Point ptMouse, int iNode)
{
… …
else // sides
{
ptNearest = Auxi_Geometry .NearestPointOnSegment (ptMouse,
pts [iNode - 4], pts [(iNode - 3) % 4]);
ptOnOppositeSide = Auxi_Geometry .NearestPointOnSegment (ptMouse,
pts [(iNode + 2) % 4], pts [(iNode + 3) % 4]);
double angleBeam =
Auxi_Geometry .Line_Angle (ptOnOppositeSide, ptMouse);
ptEndIn =
Auxi_Geometry .PointToPoint (ptOnOppositeSide, angleBeam, minSide);
ptEndOut =
Auxi_Geometry .PointToPoint (ptOnOppositeSide, angleBeam, 5000);
}
AdjustCursorPosition (ptNearest);
}
In both cases (for corners and sides) at the starting moment of movement the cursor is shifted to the calculated point
ptNearest and the resizing starts. All movements are described in the
OnMoveNode() method which has two different parts for corners and sides. Well,
there are also small pieces of code for moving the whole rectangle and for rotation, but
these parts are standard and do not need any discussion.
For corners, the allowed movement works in such way.
public override bool MoveNode (int iNode, int dx,
int dy,Point ptM, MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{ Fig.11.39 When the side is
if (iNode < 4) // corners pressed, mouse can be
{ moved only orthogonally
PointF ptNext, ptPrev; to this side.
if (Auxi_Geometry .PointInsideConvexPolygon (ptM, ptsAllowed))
{
pts [iNode] = ptM;
Auxi_Geometry .Distance_PointLine (ptM, pts [(iNode + 1) % 4],
pts [(iNode + 2) % 4], out ptNext);
Auxi_Geometry .Distance_PointLine (ptM, pts [(iNode + 2) % 4],
pts [(iNode + 3) % 4], out ptPrev);
pts [(iNode + 1) % 4] = ptNext;
pts [(iNode + 3) % 4] = ptPrev;
DefineCover ();
bRet = true;
}
else
{
AdjustCursorPosition (pts [iNode]);
bRet = false;
}
}
… …
World of Movable Objects 347 (978) Chapter 11 Movement restrictions
• Four corners of rectangle are stored in the pts[] array; points in this array are numbered in the same way as
nodes over them, so pts[iNode] is associated with the node with number iNode. We use the adhered mouse
which was initially (at the starting moment of corner movement) shifted axactly on the corner point and throughout
the corner movement this caught corner and the mouse cursor have to take the same position. Thus, the new
position of the caught corner must be the same ptM which is obtained as a parameter.
pts [i] = ptM;
• Two neighbouring corners may change their positions but they are going to be at the same side lines where they
were before. Auxi_Geometry.Distance_PointLine() method returns the nearest point on the line; this
nearest point on the known side line is going to be the new corner point. Corners are numbered in
counterclockwise order and according to this order I call the positions of two neighbouring corners ptNext and
ptPrev.
Auxi_Geometry .Distance_PointLine (ptM, pts [(i + 1) % 4],
pts [(i + 2) % 4], out ptNext);
Auxi_Geometry .Distance_PointLine (ptM, pts [(i + 2) % 4],
pts [(i + 3) % 4], out ptPrev);
pts [(i + 1) % 4] = ptNext;
pts [(i + 3) % 4] = ptPrev;
• Positions of four corners can be used to calculate the new cover.
• If mouse moved outside the allowed area ptsAllowed[], then it is returned to the current corner position.
For sides, the allowed movement and the code inside the OnMoveNode() method are absolutely different.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
else if (iNode < 8) // sides
{
double newW, newH;
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
if (iNode == 4)
{
newH = Auxi_Geometry .Distance (ptNearest, ptOnOppositeSide);
pts [0] = Auxi_Geometry .PointToPoint (pts [3],
angle + Math .PI / 2, newH);
pts [1] = Auxi_Geometry .PointToPoint (pts [2],
angle + Math .PI / 2, newH);
}
else if (iNode == 5)
{
newW = Auxi_Geometry .Distance (ptNearest, ptOnOppositeSide);
pts [1] = Auxi_Geometry .PointToPoint (pts [0],
angle + Math .PI, newW);
pts [2] = Auxi_Geometry .PointToPoint (pts [3],
angle + Math .PI, newW);
}
else if (iNode == 6)
{
newH = Auxi_Geometry .Distance (ptNearest, ptOnOppositeSide);
pts [2] = Auxi_Geometry .PointToPoint (pts [1],
angle - Math .PI / 2, newH);
pts [3] = Auxi_Geometry .PointToPoint (pts [0],
angle - Math .PI / 2, newH);
}
else // iNode == 7
{
newW = Auxi_Geometry .Distance (ptNearest, ptOnOppositeSide);
World of Movable Objects 348 (978) Chapter 11 Movement restrictions
pts [0] = Auxi_Geometry .PointToPoint (pts [1], angle, newW);
pts [3] = Auxi_Geometry .PointToPoint (pts [2], angle, newW);
}
DefineCover ();
bRet = true;
AdjustCursorPosition (ptNearest);
}
… …
In this case mouse can move only along the beam which is orthogonal to the caught side. The nearest allowed point on this
beam is calculated and this point (ptNearest) is going to be the next mouse position. Further calculations depend on the
caught side; I’ll use the code for the side with the iNode = 7 (see figure 11.38).
• Distance between ptNearest and ptOnOppositeSide gives new size of rectangle; depending on the caught
side, it is either new width or new height.
newW = Auxi_Geometry .Distance (ptNearest, ptOnOppositeSide);
• Angle for such rectangles is the angle from pts[2] to pts[3] (and the same angle from pts[1] to pts[0]).
Positions for two end points of the moving side have to be calculated; two other corners do not change positions.
With already known angle and new size, the calculations are simple.
pts [0] = Auxi_Geometry .PointToPoint (pts [1], angle, newW);
pts [3] = Auxi_Geometry .PointToPoint (pts [2], angle, newW);
All calculations are simple enough and I like the variant with adhered mouse much more than the old one.
File: Form_Rectangles_WithCells.cs
Menu position: Graphical objects – Basic elements – Rectangles – With cells
Usually when an object is resizable, then it is resizable by any border point. There can be some limitations to this rule based
on specific use of one or another object. For example, if there is a requirement to resize a rectangle only horizontally, then
the resizing is allowed only by two sides, but the size can be changed in similar way on the left or on the right. However,
there can be special cases when such symmetry in resizing is not needed and even purposely limited to one side only. The
current example deals with rectangles which have asymmetrical resizing and other interesting requirements.
Suppose that you have a set of elements which you need to show at the screen for users’ selection. If these elements are
digits (0 – 9) or letters (A – Z), then they can be represented as strings and shown as lines in a ListBox or ComboBox
control. In similar way I have organized the years selection in the Form_YearsSelection.cs (figure 17.2) which you will
see further on in the chapter User-driven applications. The use of mentioned controls is possible in all cases when elements
for selection can be easily shown as strings. Even in such case I, as a user, would prefer to see all possibilities
simultaneously if there are not too many of them. I would also prefer, both as developer and user, to deal not with controls
but with graphical objects. This will be not the preferable but the only possible solution if elements cannot be shown as
strings but have to be painted. (One of such examples is the selection of the line dash style.)
My idea of the best solution will be to have some kind of a table with cells occupied by the elements of a set; by clicking
some cell, user marks the element to be used. Each cell has a rectangular shape; all cells have equal size; all cells are united
into an object of rectangular shape. There is only a question of organizing such union or, to formulate it in another way,
there is a question of relative positioning of the cells inside this object. There are two extreme solutions with cells
organized into one row or one column, but there are also different intermediate solutions. As we work now in the world of
user-driven applications, then developer has no right to decide about the number of rows and columns for such table but
user will organize it in the way he prefers. To do this, the table must be resizable, but in such a way that all occupied cells
(cells with information inside) must be always in view. Step by step we formulated the idea and the main requirements for
the Rectangle_CellsInView_Demo class.*
public class Rectangle_CellsInView_Demo : GraphicalObject
{
Form form;
Mover supervisor;
RectangleF rcArea;
PointF ptEndIn, ptEndOut;
*
Addition of the “_Demo” part at the end of the class name signals that this is the copy of original class from the
MoveGraphLibrary.dll.
World of Movable Objects 349 (978) Chapter 11 Movement restrictions
float fW_cell, fH_cell;
int cellsAlwaysInView;
Delegate_Draw m_draw = null;
int nRows, nCols;
int minCellWidth = 16;
int minCellHeight = 16;
Cursor cursorArea = Cursors .SizeAll;
int iCellPressed;
The current example Form_Rectangles_WithCells.cs demonstrates three different objects of this class (figure 11.40). the
One of those rectangles is visualized with its cover and its is obvious that resizing is possible by two sides and one corner.
This unusual type of resizing is determined by the rules for showing the cells with iinformation.
• Elements of a set are shown row after row starting from the top left corner.
• All occupied cells (their number is cellsAlwaysInView) must be always in view.
• If all cells are shown in one row, then there are at least cellsAlwaysInView columns in view; There is no
limit on the width of rectangle, so there can be an empty space to the right of those cells.
• If there is not enough space for all cells in the first row, then the next row is occupied from left to right. If needed,
more rows are added.
• The number of columns is determined by the width of rectangle. There is no limit on rectangle height, so there can
be an empty space below the last row.
When rectangle with the cells is initialized, then the number of occupied cells (nCellsToShow) and the cell size
(fWcell, fHcell) are determined. At the same time the proposed rectangular area (rc) is only a suggestion and can be
changed if it does not allow to show all cells according to the above mentioned rules. The presence of first two parameters
signals the use of adhered mouse for resizing of such rectangles.
public Rectangle_CellsInView_Demo (Form frm, Mover mvr, RectangleF rc,
int nCellsToShow, float fWcell, float fHcell,
Delegate_Draw drawmethod)
Throughout the resizing, the number of rows and columns can be changed, so I decided to organize resizing only by the
bottom right corner and its two adjacent sides. It is very easy to add resizing by other sides and corners, but this decision
allows me to demonstrate a non-standard case. Cover shown at figure 11.40 is a simplified (shortened) version of covers
which were demonstrated in the chapter Rectangles. For the Rectangle_Standard class (figure 3.1) the standard
cover for rectangles
from the Cover
class is used.
Because here the
shortened version is
needed, then the
standard variant
cannot be used and
the
DefineCover()
method must define
all the nodes. There
is a standard order of
nodes from the
smallest to the
biggest. Strips along
two borders have the
standard six pixels Fig.11.40 Throught the resizing of these rectangles, their occupied cells are always in view.
width while the
circular node over the corner is slightly enlarged.
public override void DefineCover ()
{
float cxL = rcArea .Left;
float cxR = rcArea .Right;
World of Movable Objects 350 (978) Chapter 11 Movement restrictions
float cyT = rcArea .Top;
float cyB = rcArea .Bottom;
int nHalf = 3;
CoverNode [] nodes = new CoverNode [4];
nodes [0] = new CoverNode (0, new PointF (cxR, cyB), 5, Cursors .SizeNWSE);
nodes [1] = new CoverNode (1, new RectangleF (cxR - nHalf, cyT, 2 * nHalf,
rcArea .Height), Cursors .SizeWE);
nodes [2] = new CoverNode (2, new RectangleF (cxL, cyB - nHalf,
rcArea .Width, 2 * nHalf), Cursors .SizeNS);
nodes [3] = new CoverNode (3, rcArea, cursorArea);
cover = new Cover (nodes);
}
Adhered mouse is used for all three nodes on the border, but the case of the corner node is slightly different from the case of
side nodes. Whenever the rectangle is pressed, its StartMoving() method is called.
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 Rectangle_CellsInView_Demo)
{
Rectangle_CellsInView_Demo elem =
grobj as Rectangle_CellsInView_Demo;
elem .StartMoving (e .Location, mover .CaughtNode);
textBox1 .Text = elem .CellPressed .ToString ();
}
}
}
In addition to calculations of values needed for further resizing, this StartMoving() method determines the number of
the pressed cell. If the last node of the cover is pressed (iNode == 3), then it is the start of forward movement for the
whole object and no other calculations are needed.
public void StartMoving (Point ptMouse, int iNode)
{
iCellPressed = OccupiedCell (ptMouse);
if (iNode == 3)
{
return;
}
… …
Of other three variants (there are three nodes for resizing), the easiest is the case of the bottom side of rectangle.
Resizing by the bottom side. Throughout this resizing, positions of the occupied cells are not going to change. The number
of columns is going to be the same as at the starting moment and the minimum number of rows is the same as occupied cells
have at this moment. At the starting moment cursor is shifted exactly on the border of rectangle; after it cursor can be
moved only up or down. The upper point of cursor movement (ptEndIn) is determined by the minimum number of rows.
The other end of this movement (ptEndOut) is set far away, but in reality it will be limited by the form border.
public void StartMoving (Point ptMouse, int iNode)
{
… …
else // if (iNode == 2) // Bottom side
{
ptNearest = new PointF (ptMouse .X, rcArea .Bottom);
int minRows = cellsAlwaysInView / nCols;
if (cellsAlwaysInView > minRows * nCols)
{
minRows++;
World of Movable Objects 351 (978) Chapter 11 Movement restrictions
}
ptEndIn = new PointF(ptMouse.X, rcArea.Top + minRows * fH_cell + nAdd);
ptEndOut = new PointF (ptMouse .X, rcArea .Top + 4000);
}
AdjustCursorPosition (ptNearest);
}
With cursor allowed to move only along the straight segment, we have a standard technique of adhered mouse which was
already demonstrated in several of previous examples. For any cursor movement, the nearest point on segment is calculated
and this point gives the coordinate for rectangle side. Cursor is switched to the same point.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == 2) // bottom side
{
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
rcArea .Height = ptM .Y - rcArea .Top;
nRows = Convert.ToInt32 (Math.Truncate (rcArea .Height / fH_cell));
bRet = true;
AdjustCursorPosition (ptNearest);
… …
Resizing by the right side. Throughout this resizing, calculations are similar, though the view of the table can change.
Cursor will move along the horizontal line for which two ends must be calculated. Rectangle height will not change, though
rows at the bottom can become empty with the increase of the rectangle width. The crucial point is the left end of the
allowed cursor movement; this point (ptEndIn) is determined by the squeeze of occupied cells into minimum number of
columns, but all occupied cells must be visible. Opposite end of cursor movement (ptEndOut) is declared to be far away,
but the real movement will be limited by the form border.
public void StartMoving (Point ptMouse, int iNode)
{
… …
else if (iNode == 1) // Right side
{
ptNearest = new PointF (rcArea .Right, ptMouse .Y);
int minCols = cellsAlwaysInView / nRows;
if (cellsAlwaysInView > minCols * nRows)
{
minCols++;
}
ptEndIn =
new PointF (rcArea .Left + minCols * fW_cell + nAdd, ptMouse .Y);
ptEndOut = new PointF (rcArea .Left + 4000, ptMouse .Y);
}
… …
Resizing by the corner. This is the most interesting case. There is no straight trajectory for cursor movement, so the only
thing to do at the starting moment is to move cursor exactly on the corner.
public void StartMoving (Point ptMouse, int iNode)
{
… …
PointF ptNearest;
if (iNode == 0) // RB corner
{
ptNearest = new PointF (rcArea .Right, rcArea .Bottom);
}
World of Movable Objects 352 (978) Chapter 11 Movement restrictions
… …
AdjustCursorPosition (ptNearest);
}
There is no trajectory for cursor movement; cursor is allowed to move around the big area and the corner of rectangle has to
move synchronously with cursor. If such position allows to see all occupied cells, then this movement is allowed and the
new sizes of rectangle are set.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0) // RB corner
{
int nRows_new = Convert .ToInt32 (
Math .Truncate ((ptM .Y - rcArea .Top) / fH_cell));
int nCols_new = Convert .ToInt32 (
Math .Truncate ((ptM .X - rcArea .Left) / fW_cell));
if (nRows_new * nCols_new >= cellsAlwaysInView)
{
rcArea .Width = ptM .X - rcArea .Left;
rcArea .Height = ptM .Y - rcArea .Top;
nRows = nRows_new;
nCols = nCols_new;
bRet = true;
}
… …
If the proposed movement of the corner point is not allowed because there is not enough space to show all occupied cells,
then the table is not changed and cursor is returned to the corner point in a standard way used with adhered mouse.
… …
else
{
AdjustCursorPosition (new PointF (rcArea .Right,
rcArea .Bottom));
bRet = false;
}
… …
There are three Rectangle_CellsInView_Demo objects in the Form_Rectangles_WithCells.cs (figure 11.40).
Rectangles with digits and letters are demonstrated only in this example; rectangle with special marks will appear again in
an auxiliary form for tuning lines with markers. It was this Form_MarkedLineParams.cs (figure 18.51) which required
the design of the Rectangle_CellsInView class.
When one of those marks is selected, then it has to be highlighted in one way or another; I prefer to use different
background color for selected cell. In any way, the number of the selected cell must be determined. There is no such
change in the current example, but when any rectangle is pressed, then its StartMoving() method starts with
calculation of the pressed cell.
public void StartMoving (Point ptMouse, int iNode)
{
iCellPressed = OccupiedCell (ptMouse);
… …
If it happens inside one of the occupied cells, then the Occupiedcell() method returns some value from the
[0, cellsAlwaysInView - 1] range; otherwise it returns -1. This method can determine the cell number for any
point, so it can be used without mouse press, for example, throughout the mouse movement across the rectangle.
World of Movable Objects 353 (978) Chapter 12 Simpler covers
Simpler covers
Familiar objects
Graphical primitives of familiar shapes are going to appear in this chapter. Such objects were demonstrated in many
examples of the first several chapters and I would say that there were enough variants of regular polygons, circles, and
rings. Yet, I want to demonstrate that for some of those objects simpler covers can be organized and these new covers
provide exactly the same functionality. The most amazing thing about the examples of this chapter is the type of objects for
which the simpler covers can be designed. Mostly, but not only, these are the same objects with which I had the main
problem years ago before I thought out the N-node covers. Prior to that, it was a real challenge to design simple enough
cover for an object with curved borders; then the N-node covers were invented and that was a huge step in design. Not only
the simplest circles and rings but much more complicated objects like sets of rings (you will see them further on) got the N-
node covers and I was sure that that was the best possible cover for them. A lot of time has passed since then and at one
moment I understood that the covers for circles, rings, and rounded strips can be even much simpler than was explained in
the chapter Curved borders. N-node covers.
Only do not think after reading this chapter that the N-node covers are not needed any more. The N-node covers are very
useful in some situations and those examples were the best to demonstrate this technique, but in this section I am going to
demonstrate that for circles, rings, and rounded strips it will be enough to have only two, four, or five nodes in their covers.
In addition to this simplification of covers, I want to use the adhered mouse. From my point of view, combination of these
two improvements produces the best result, but I am going to make the steps one at a time.
Usually the explanation about straight lines and objects based on straight lines is simpler than about the objects with curved
borders. However, in this case it is easier to start with the curved borders, so we start with a circle.
Circles
File: Form_Circles_SimpleCover.cs
Menu position: Graphical objects – Basic elements – Circles – Simple cover (two nodes)
The multicolored circles of the Circle_Nnodes class were first used in the Form_NnodeCovers.cs (figure 7.1) to
explain the design of N-node covers; several circles of this class are also used in the Form_Circles_Multicolored.cs
(figure 7.6). The resizing of such circles is organized by covering the border with a set of small overlapping nodes
(figure 7.2). In my examples, the radius of each small circular node is usually five pixels; the distance between the centers
of neighbouring nodes is eight pixels. In such way the whole border is covered by a sensitive strip of varying width but at
any place it is not narrower than six pixels.
The number of nodes covering the border of any Circle_Nnodes object depends on the length of this border. When the
circle radius is increased, the old set of nodes
cannot cover the whole border of bigger length.
Thus, on releasing a circle after resizing, the
new number of nodes is calculated and the cover
is redefined. It is perfectly seen in the
Form_Circles_Multicolored.cs with the
visualization of covers switched ON. Can we
design another cover for the same circle in
which the whole border will be closed by one
sensitive node? Optimal shape of such narrow
strip along the circle border is obvious – it must
be a ring. We do not have a node of such shape,
but maybe we can use something else?
Forward movement and rotation of any circle
was described before and it is easily organized
by a single circular node. One node is enough
for forward movement and rotation because they
are started by different mouse buttons.
Forward movement and resizing are started by
the same left button, so for two different Fig.12.1 Cover of each Circle_SimpleCover object consists of
movements started with the same button we two circular nodes
World of Movable Objects 354 (978) Chapter 12 Simpler covers
need at least one more node. In the Circle_Nnodes class, resizing is provided by a set of small nodes covering the
whole border; in the new Circle_SimpleCover class it is provided by a single circular node which is slightly bigger
than the first one. The resizing has to be started at any pixel in the vicinity of visible circle border. The needed ring is the
difference between two coaxial circular nodes of which one is slightly smaller than the circle itself, while the second node is
slightly bigger (figure 12.1).
This Circle_SimpleCover class is very simple. As you can see from the code below, all its parameters for
visualization are in the CircleData field and the constructor includes only the initialization of this part.
public class Circle_SimpleCover : GraphicalObject
{
CircleData circle;
double distMouseFromBorder;
protected double compensation;
int delta = 4;
// -------------------------------------------------
public Circle_SimpleCover (PointF ptC, float rad, double angleDegree,
double [] fVals)
{
circle = new CircleData (ptC, rad, angleDegree, fVals);
}
Cover of the Circle_SimpleCover class consists of two nodes and the important thing is not only a small difference in
size between them but also the order of these nodes: the first node is slightly less than the whole circle (delta is the
difference); the second node is slightly bigger. In my old examples with the Circle_Nnodes class, the sensitive strip
along the border has a varying width between six and ten pixels; in the Circle_SimpleCover class we have a steady
width of the strip along the border. The width of this strip is 2 * delta.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, Center, Radius - delta, Cursors .SizeAll),
new CoverNode (1, Center, Radius + delta)};
cover = new Cover (nodes);
cover .SetClearance (false);
}
Mover analyses the nodes according to their order in the cover. The first node covers nearly the whole object and the mouse
press nearly anywhere inside the circle is caught by this node. Only the narrow strip along the border is not covered by the
first node but covered by the second node; thus, any mouse press in the border vicinity catches the second node and allows
the resizing while the mouse press anywhere else inside circle starts forward movement.
The strip along the border is narrow enough but it has some width and the resizing can be started when the mouse is pressed
not exactly on the border but slightly aside. When the circle is pressed on the node covering the border, then the
Circle_SimpleCover.StartResizing() method is called.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Circle_SimpleCover)
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode != 0)
{
(grobj as Circle_SimpleCover) .StartResizing (e .Location);
}
}
… …
This StartResizing() method calculates the distance between the point of the mouse cursor and the circle border.
World of Movable Objects 355 (978) Chapter 12 Simpler covers
public void StartResizing (Point ptMouse)
{
distMouseFromBorder = Auxi_Geometry .Distance (Center, ptMouse) - Radius;
}
Throughout the resizing, this initial shift is used by the Circle_SimpleCover.MoveNode() method for more
accurate calculation of the new radius.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
Move (dx, dy);
}
else
{
float radNew = Convert .ToSingle (Auxi_Geometry .Distance (Center,
ptM) - distMouseFromBorder);
if (MinimumRadius <= radNew)
{
Radius = radNew;
bRet = true;
}
}
}
else if (catcher == MouseButtons .Right)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
Angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
How crucial is the use of the Circle_SimpleCover.StartResizing() method at the starting moment of resizing?
From my point of view, it is absolutely not crucial when circles are used as stand alone objects and the delta value is
only three or four pixels. With a small experiment, you can decide for yourself, if it is crucial or not. In the current version
of this program, you press the mouse near the border and thus catch the circle for resizing. Then move the border and stop
without releasing the mouse. With a good eyesight, you will see the difference between cursor position and border line at
the starting moment and the same difference between them at the end. Now comment the line with the
StartResizing() call inside the OnMouseDown() method, rebuild solution, and then try the same experiment.
You will see some distance at the beginning of movement, but then the border line will be always exactly under mouse
cursor. This is an obvious result because the default value for distMouseFromBorder is zero and the new radius
inside MoveNode() method will be calculated to the current mouse position without any adjustments.
A simple cover of two nodes provides forward movement, rotation, and resizing of circles. Similar technique is used in the
new rings.
Attention! In this Form_Circles_SimpleCover.cs example any circle is brought on top of others not by an ordinary left
button click but by a double click. In the examples which are used only to explain some features and which are populated
with simple objects, an ordinary left click is used to bring any object on top. It is possible that you want only to move an
object for one or two pixels, but in addition an object also pops up. It is an unexpected reaction but for such simple
programs it is not a problem. In much more sophisticated scientific and engineering applications I prefer to avoid such
situations and even a tiny move of any object does not affect the order of objects on the screen. So, in those applications the
left double click is used to bring an object on top. As we are moving closer to these complex programs, I decided to change
some rules beforehand.
World of Movable Objects 356 (978) Chapter 12 Simpler covers
Rings
File: Form_Rings_SimpleCover.cs
Menu position: Graphical objects – Basic elements – Rings – Simple cover (four nodes)
When I first introduced the idea of N-node covers, I used both circles and rings to explain this technique (figure 7.2). If in
the previous example we constructed a very simple cover for circles, maybe there is similar way to simplify the ring cover?
Let us organize another class of rings and try to do something.
public class Ring_SimpleCover : GraphicalObject
{
RingData dataRing;
double distMouseFromBorder;
protected double compensation;
int delta = 3;
// -------------------------------------------------
public Ring_SimpleCover (PointF ptC, float rOut, float rIn,
double angleDegree, double [] fVals)
{
dataRing = new RingData (ptC, rOut, rIn, angleDegree, fVals);
}
There is not a big difference between
organizing the new simple covers for
circles and rings. Ring has two
borders which must be covered by
sensitive nodes to make ring fully
resizable. The idea of making each
border resizable is the same as for a
circle: to have two circular nodes of
which the first one is slightly less and
the second one is slightly bigger than
the border. The order of nodes is
from inside the ring and is going
outside. The same transparent node
is used to cut the hole, so we have
only four circular nodes (!) to make
our ring movable, resizable, and
rotatable (figure 12.2).
• node [0] – transparent; makes
hole
• node [1] – provides resizing of
the inner border
Fig.12.2 Rings of the Ring SimpleCover class with their covers
• node [2] – provides forward
movement and rotation
• node [3] – provides resizing of the outer border
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, Center, InnerRadius - delta, Behaviour .Transparent),
new CoverNode (1, Center, InnerRadius + delta),
new CoverNode (2, Center, OuterRadius - delta, Cursors .SizeAll),
new CoverNode (3, Center, OuterRadius + delta)};
cover = new Cover (nodes);
cover .SetClearance (false);
}
Any ring can be resized by two borders; thus, the number of the caught node is important for resizing and the
Ring_SimpleCover.StartResizing() method has to be called only for odd nodes.
World of Movable Objects 357 (978) Chapter 12 Simpler covers
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 Ring_SimpleCover)
{
Ring_SimpleCover ring = grobj as Ring_SimpleCover;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode % 2 != 0)
{
ring .StartResizing (e .Location, mover .CaughtNode);
}
}
else if (e .Button == MouseButtons .Right)
{
ring .StartRotation (e .Location);
}
}
}
}
The number of the caught node defines the border from which the initial mouse shift is calculated in the
StartResizing() method..
public void StartResizing (Point ptMouse, int iNode)
{
if (iNode == 1)
{
distMouseFromBorder =
Auxi_Geometry .Distance (Center, ptMouse) - InnerRadius;
}
else // iNode == 3
{
distMouseFromBorder =
Auxi_Geometry .Distance (Center, ptMouse) - OuterRadius;
}
}
The initial shift of the cursor from the caught border - distMouseFromBorder - is used in the
Ring_SimpleCover.MoveNode() method for accurate calculation of the new border radius throughout the whole
process of resizing.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
float radNew = Convert .ToSingle (
Auxi_Geometry .Distance (Center, ptM) - distMouseFromBorder);
if (iNode == 1) // inner border
{
if (MinimumInnerRadius <= radNew &&
radNew + MinimumWidth <= OuterRadius)
{
InnerRadius = radNew;
bRet = true;
}
World of Movable Objects 358 (978) Chapter 12 Simpler covers
}
else // iNode == 3 // outer border
{
if (InnerRadius + MinimumWidth <= radNew)
{
OuterRadius = radNew;
bRet = true;
}
}
… …
Let us return for some time to the objects with straight lines – regular polygons.
Regular polygons
File: Form_RegularPolygon_SimpleCover.cs
Menu position: Graphical objects – Basic elements – Polygons (solid) – Regular (simple cover)
Regular polygons were already discussed in the chapter Polygons (see Form_RegularPolygon_CoverVariants.cs at
figure 6.1). Those polygons of the RegPoly_CoverVariants class demostrate three variants of resizing and have
three types of covers depending on the type of resizing. To organize the resizing of regular polygon by any border point,
each segment of the border in that class is covered by a narrow strip node; such polygon of N vertices has N+1 nodes in its
cover. New RegPoly_SimpleCover class has a cover consisting of only two nodes regardless of the number of
vertices and still provides the same resizing by any border point.
Any regular polygon is described by its central point, radius of vertices, number of vertices, and the angle to the first of
them. These parameters do not depend on the type of used cover.
public class RegPoly_SimpleCover : GraphicalObject
{
PointF m_center;
protected double m_angle;
protected float radiusVertices;
int nVertices;
SolidBrush m_brush;
float m_delta = 3; // strip width is 2*delta
protected double scaling, compensation;
double deltaR;
static float minR = 20;
The idea of the new cover is similar to the one used for circles. There are two nodes of the same shape copying the shape of
the border. The first node is slightly smaller than the polygon itself and provides forward movement. The second node is
slightly bigger than the polygon; small part of this node which is not blocked by the first node provides the resizing
(figure 12.3). A narrow part of the second node along the polygon border is not blocked by the first node, so resizing can
be started anywhere along the border.
Both nodes of the cover have the shape of a regular polygon; for cover design, we need the arrays of their vertices.
Calculation of those vertices is elementary. Each node copies the shape of original polygon and vertices of these polygons
are placed on the same radii. If minimal distance between the node border and polygon border is m_delta, then the
distance between v ertices on the same radius is calculated by a simple expression.
deltaR = m_delta / Math .Cos (Math .PI / nVertices);
deltaR is the difference between radii of node vertices and polygon vertices; vertices for both nodes are calculated by a
standard method which returns the vertices of regular polygon with the known center and radius. Vertices of the smaller
node are returned by the VerticesMinus property; vertices of the bigger node – by the VerticesPlus property.
public PointF [] VerticesPlus
{
get { return (Auxi_Geometry .RegularPolygon (m_center,
radiusVertices + deltaR, nVertices, m_angle)); }
}
World of Movable Objects 359 (978) Chapter 12 Simpler covers
public PointF [] VerticesMinus
{
get { return (Auxi_Geometry .RegularPolygon (m_center,
radiusVertices - deltaR, nVertices, m_angle)); }
}
As I mentioned before, cover consists of two polygonal nodes with the smaller one staying ahead of the bigger one. One
more parameter has to be specified for bigger polygonal node. Polygonal nodes are often used for moving objects around
the screen and their default cursor of the Cursors.SizeAll shape signals about it. The part of the second node not
blocked by the first node is used for resizing, so the cursor for the bigger node has to change its shape. As a rule, the
Cursors.Hand is used to inform about the possibility of resizing.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, VerticesMinus),
new CoverNode (1, VerticesPlus, Cursors .Hand) };
cover = new Cover (nodes);
cover .SetClearance (false);
}
cursor path to the beam along the radius. This is the main improvement in the new class, but there is also another one
smaller improvement.
Sensitive area around the border is narrow enough, so the initial shift between the mouse and the border cannot exceed
several pixels. In the Form_Circles_SimpleCover.cs, the distance between circle border and mouse cursor is calculated at
the starting moment of resizing and is used throughout the whole process of resizing. In the new
Form_Circles_SimpleCoverAdh.cs, the cursor is shifted exactly to the circle border at the initial moment of resizing and
after it the resizing goes with zero shift between cursor and border. This is the first change in the StartResizing()
method. The second change is the calculation of the line segment along which the cursor is allowed to move throughout the
resizing. The inner end point of this segment is determined by the minimal allowed circle radius. There is no upper limit on
the circle radius, so the second point is set far away, even somewhere outside the screen. If you decide to put the upper
limit on the circle radius, then you have to use this restriction in calculation of the second end point.
new public void StartResizing (Point ptMouse, int iNode)
{
if (iNode != 0)
{
angleBeam = Auxi_Geometry .Line_Angle (Center, ptMouse);
AdjustCursorPosition (Auxi_Geometry .PointToPoint (Center, angleBeam,
Radius));
ptEndIn = Auxi_Geometry.PointToPoint (Center, angleBeam,MinimumRadius);
ptEndOut = Auxi_Geometry .PointToPoint (Center, angleBeam, 4000);
}
}
Two points – ptEndIn and ptEndOut – define the ends of the line segment along
which the cursor can move throughout the resizing (figure 12.5). Cursor movement
along straight segment was already demonstrated in the
Form_SpotsOnLinesAndArcs.cs (figure 11.24), so the code of the
Circle_SimpleCoverAdh.MoveNode() method must be easy to understand.
For any position of mouse cursor (ptM), the nearest point on the allowed segment
(ptEndIn, ptEndOut) is calculated. Distance from this point to the circle center is
Fig.12.5 Corrected way of
considered as a new radius and mouse cursor is moved into this point. For this enforced
cursor movement
cursor movement, the link between mover and the circle is temporarily cut.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
Move (dx, dy);
}
else
{
PointF ptNearest =
Auxi_Geometry .NearestPointOnSegment (ptM, ptEndIn, ptEndOut);
Radius = Convert .ToSingle (
Auxi_Geometry .Distance (Center, ptNearest));
AdjustCursorPosition (ptNearest);
bRet = true;
}
}
… …
How did the adhered mouse improve the process of resizing? When you press the border and starts moving it, you usually
try to move cursor somewhere along radius. Algorithm corrects this movement and the cursor moves along the best
possible trajectory for resizing.
World of Movable Objects 362 (978) Chapter 12 Simpler covers
Rings
File: Form_Rings_SimpleCoverAdh.cs
Menu position: Graphical objects – Basic elements – Rings – Simple cover and adhered mouse
The same technique and similar code are used to improve the ring resizing. The Ring_SimpleCoverAdh class is
derived from the Ring_SimpleCover class.
public class Ring_SimpleCoverAdh : Ring_SimpleCover
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut;
double angleBeam;
When one or another border is pressed with the left button, the resizing starts with calling the
Ring_SimpleCoverAdh.StartResizing() method. Mouse cursor is switched exactly to the border and from this
moment the cursor position
determines the new radius of
the caught border. Also the
range for the cursor allowed
movement along the radial
beam is calculated.
Ring has two borders for
resizing and the ranges of
possible change for two radii
are based on different
restrictions. For the inner
border, the range depends on
the minimal allowed inner
radius and the minimal
allowed width of the rings.
For outer border, the same
minimal allowed width is
taken into consideration,
while another end point is set
far away.
In the case of a circle, there
was one figure to show the Fig.12.6 Rings of the Ring_SimpleCoverAdh class
allowed cursor track
throughout the resizing. Ring has two borders for resizing, so two different figures demonstrate the allowed cursor track.
At each moment of resizing, the cursor is at the point where straight line crosses the ring border (figures 12.7).
Regular polygons
File: Form_RegularPolygon_SimpleCoverAdh.cs
Menu position: Graphical objects – Basic elements – Polygons (solid) – Regular (simple cover and adhered mouse)
Regular polygon has only one
movable border, so the case of
regular polygons with simple
cover and adhered mouse – the
RegPoly_SimpleCoverAdh
class – is closer in design and
needed code not to the rings but
to circles. When the mouse is
pressed somewhere near the
border to start resizing, the cursor
is moved to the nearest border
point. Throughout the resizing,
the cursor moves along radius
and the border is always under
the cursor. The inner end point
for cursor movement is defined
by the minimal radius of the
circle into which the polygon is
inscribed. There is no upper
limit on rectangle size, so only
the form border may stop the
cursor while it drags the polygon
border outside.
New class is derived from the Fig.12.8 Throughout the resizing, cursor moves along radius and the border is always
RegPoly_SimpleCover under the cursor
class and, similar to the previous
example, has only StartResizing() and MoveNode() methods of its own.
Strips
File: Form_Strips_SimpleCoverAdh.cs
Menu position: Graphical objects – Basic elements – Rounded strips
In the old example
Form_NnodeCovers.cs
(figure 7.1) there were three
different objects for demonstration
and explanation of the N-node
covers. For circles and rings we
now have variants with very simple
covers and adhered mouse used
throughout the resizing. Let us try
to do the same with the third object
from that old example – the rounded
strip. The view of the
Form_Strips_SimpleCoverAdh.cs
(figure 12.9) shows several objects
of the
Strip_SimpleCoverAdh
class. One strip is shown together
with an auxiliary line; it appears
only when some strip is in the
process of resizing and user wants
to see this auxiliary line. For better Fig.12.9 Auxiliary line shows the segment along which the cursor can move
understanding of the new cover, let together with the grabbed border point
World of Movable Objects 365 (978) Chapter 12 Simpler covers
us refresh the information about older version of similar strips. I think that the comparison of two pictures with covers
(figures 12.10) will help to understand the idea of the new simpler cover.
In the old version of the rounded strips (Strip_Nnodes class) there are three different parts of cover (figure 12.10a).
1. Two strip nodes along the straight parts of the border.
2. Two sets of circular nodes along the curved parts of the border.
3. One big strip node to cover the whole area.
Fig.12.10a Strip_Nnodes object and its cover Fig.12.10b Strip_SimpleCoverAdh object and its cover
In the new version of the rounded strips – the Strip_SimpleCoverAdh class – there are also three different parts of
cover (figure 12.10b). One of them – thin strip nodes along the straight parts of border – is exactly the same as in the older
version. Another part – the big strip node – is similar to what was used in the older version, but in the new class this strip is
slightly smaller. The third part is new: two sets of small circular nodes are substituted by two much bigger circles. Thus,
we have a cover consisting of five nodes, but their order must be also changed. Two strip nodes along the straight parts of
border are still the first, but two big circular nodes must be the last in the cover. Two circles cover not only the curved
border – the area where these nodes must work – but also a significant part of an object where these nodes must not work,
so those parts of two circular nodes are blocked from mover by the preceding big strip node.
public class Strip_SimpleCoverAdh : GraphicalObject
{
Form form;
Mover supervisor;
PointF ptC0, ptC1; // centers of semicircles
double m_angle; // from ptC0 to ptC1
float m_radius;
SolidBrush m_brush;
int delta = 3;
delta is the distance between the real strip border and the borders of
the nodes inside and outside. This distance can be changed, but with the
current value of three pixels it makes the sensitive area along the borders
equal to six pixels; from my experience it is just the good area for border
resizing. Code of the DefineCover() method uses two central points
of semicircles (C0 and C1) and the array of points pts[] which contains
the coordinates for corners of the straight part; all these important points
are marked at figure 12.11.
public override void DefineCover ()
{
Fig.12.11 Points used in cover design.
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) };
World of Movable Objects 366 (978) Chapter 12 Simpler covers
cover = new Cover (nodes);
cover .SetClearance (false);
}
Four nodes of this cover are used for resizing but they allow to change different sizes of an object: nodes along the straight
parts of the border (nodes 0 and 1) allow to change the width of the strip (and the radius of the end semicircles) while the
sensitive parts along the curves (not blocked parts of nodes 3 and 4) allow to change the length. The mechanism of these
changes is exactly the same as in the previous examples. Resizing can be started only somewhere in the vicinity of the
border. At the starting moment of resizing, the cursor is moved exactly on the border and the line segment for further cursor
movement is calculated. Throughout this movement, the position of the border is determined by the mouse position. Parts
of the StartResizing() method for two nodes along straight borders are similar; parts of code for the curves are also
similar, so I’ll show here the code for only one node of each pair.
public void StartResizing (Point ptMouse, int iNode)
{
corners = CornerPoints ();
PointF ptBase, ptCursor;
double dist;
switch (iNode)
{
case 0:
Auxi_Geometry .Distance_PointLine (ptMouse, corners [2],
corners [3], out ptBase);
angleBeam = m_angle + Math .PI / 2;
ptEndIn = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
2 * minRadius);
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam, 4000);
ptCursor = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
2 * m_radius);
break;
… …
case 4:
dist = Auxi_Geometry .Distance_PointLine (ptMouse, corners [1],
corners [2], out ptBase);
additionToLength = dist - Auxi_Geometry .Distance (ptC0, ptC1);
angleBeam = m_angle;
ptEndIn = Auxi_Geometry .PointToPoint (ptBase, angleBeam,
MinimumLength + additionToLength);
ptEndOut = Auxi_Geometry .PointToPoint (ptBase, angleBeam, 4000);
ptCursor = ptMouse;
break;
}
AdjustCursorPosition (ptCursor);
}
To calculate the segment end points, two things are needed: the main axis of the strip which is going through the centers of
semicircles and four corners of the straight part of the strip at the starting moment of resizing. When the border of the strip
is grabbed in the area of straight nodes, then the mouse movement is orthogonal to the main axis; when the curved border is
pressed, then the allowed movement is parallel to the main axis. The last situation is shown with an auxiliary line at
figure 12.9; the cursor is at the point where this line crosses the strip border.
The line segment along which the cursor is allowed to move is calculated in the
Strip_SimpleCoverAdh.StartResizing() method. Cursor can move only between the end points of this
segment. This is checked inside the Strip_SimpleCoverAdh.MoveNode() method and the current cursor position
determines one or another new size of the strip. Because of the similarity in code, I am showing the piece of code for one
straight border part (iNode = 0) and for one curve (iNode = 4).
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
double dist;
World of Movable Objects 367 (978) Chapter 12 Simpler covers
if (catcher == MouseButtons .Left)
{
if (iNode != 2)
{
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
if (iNode == 0)
{
dist = Auxi_Geometry .Distance_PointLine (ptNearest,
corners [2], corners [3]);
ptC0 = Auxi_Geometry .PointToPoint (corners [2],
m_angle + Math .PI / 2, dist / 2);
ptC1 = Auxi_Geometry .PointToPoint (corners [3],
m_angle + Math .PI / 2, dist / 2);
Radius = Convert .ToSingle (dist / 2);
}
… …
else if (iNode == 4)
{
dist = Auxi_Geometry .Distance_PointLine (ptNearest,
corners [1], corners [2]);
ptC1 = Auxi_Geometry .PointToPoint (ptC0, m_angle,
dist - additionToLength);
DefineCover ();
}
AdjustCursorPosition (ptNearest);
bRet = true;
}
else
{
Move (dx, dy);
}
… …
World of Movable Objects 368 (978) Chapter 12 Simpler covers
Coaxial rings
File: Form_Rings_Coaxial.cs
Menu position: Graphical objects – Basic elements – Rings – Coaxial
When an object of the Rings_Coaxial class is constructed, it consists of a single ring. Later the number of rings can
be changed, but there is always at least one ring.
public class Rings_Coaxial : GraphicalObject
{
Form form;
Mover supervisor;
PointF m_center;
List<RingData> dataRings = new List<RingData> ();
List<double> compensation = new List<double> ();
int delta = 4;
int iCaughtBorder;
PointF ptEndIn, ptEndOut;
double angleBeam, minWidthAllowed;
double [] fPart; // the sum of all parts = 1.0
// -------------------------------------------------
public Rings_Coaxial (Form frm, Mover mvr, RingData ring)
{
form = frm;
supervisor = mvr;
m_center = ring .Center;
m_rings .Add (ring);
}
Several pages back I wrote about the rings of the
Ring_SimpleCoverAdh class with a very simple
cover consisting of only four nodes but providing all
the possibilities for forward moving, resizing, and
rotation. Also the adhered mouse is used during their
resizing and the cursor nearly voluntarily moves along
the best trajectory for resizing. Exactly the same idea
of cover design is used in the Rings_Coaxial
class; only the existence of coaxial rings positioned
next to each other produces more possibilities for
resizing and rotation.
All nodes in the cover of the Rings_Coaxial
class are circular. There is a pair of nodes next to
each border; the first node of the pair is smaller than
the border (delta is the difference between them);
the second node of the pair is bigger than the border
(their difference is also delta). Thus, the sensitive
strip along any border has a standard width of
Fig.12.12 An object of the Rings_Coaxial class
2 * delta. Numbering of nodes starts from the
interior border of the set of rings and goes to the exterior border; every next node is bigger than the preceding.
World of Movable Objects 369 (978) Chapter 12 Simpler covers
public override void DefineCover ()
{
float [] radii = new float [dataRings .Count + 1];
for (int i = 0; i < dataRings .Count; i++)
{
radii [i] = dataRings [i] .InnerRadius;
}
radii [radii .Length - 1] = OuterRadius;
CoverNode [] nodes = new CoverNode [radii .Length * 2];
for (int i = 0; i < radii .Length; i++)
{
nodes [i * 2] = new CoverNode (i * 2, m_center, radii [i] - delta,
Cursors .SizeAll);
nodes [i * 2 + 1] =
new CoverNode (i * 2 + 1, m_center, radii [i] + delta);
}
nodes [0] .Behaviour = Behaviour .Transparent;
cover = new Cover (nodes);
cover .SetClearance (false);
}
Of all the nodes in the cover, only the very first one (inside the inner ring) is transparent. Though all the nodes in the cover
are circular, there is a difference in cursor shape over them; this difference is defined by the purpose of the nodes.
The nodes are numbered from the smallest inside the inner ring to the biggest one which covers the whole object. Each
border is first covered by some node with an odd number. These nodes are used for resizing. By default, the mouse cursor
over all circular nodes has the Cursors.Hand shape, so I do not change it for odd nodes. The inner area of any ring is
first covered by some node with an even number; for all such nodes I change the cursor to Cursors.SizeAll. Such
change of the cursor informs users that by pressing an object at such moment the forward movement is started.
Because odd and even nodes in the Rings_Coaxial class are used for different purposes, then you will see in many
parts of the code that there are different actions for odd and even nodes. The first of such places is in the
OnMouseDown() method of the form from which the Rings_Coaxial.StartResizing() method is called only
for odd nodes.
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 Rings_Coaxial)
{
Rings_Coaxial rsc = grobj as Rings_Coaxial;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode % 2 != 0)
{
rsc .StartResizing (e .Location, mover .CaughtNode);
}
… …
The process of resizing is similar to resizing of a single ring, but there are more variants for a set of rings.
• When some border between two rings is moved, then only the sizes of these two rings are changed. Any ring of
the RingData class has minimal allowed width, so the movement of any inner border has to consider these
limitations on both sides.
public void StartResizing (Point ptMouse, int iNode)
{
float rad;
angleBeam = Auxi_Geometry .Line_Angle (m_center, ptMouse);
iCaughtBorder = (iNode - 1) / 2;
World of Movable Objects 370 (978) Chapter 12 Simpler covers
… …
else
{
// between two rings
rad = dataRings [iCaughtBorder] .InnerRadius;
ptEndIn = Auxi_Geometry .PointToPoint (m_center, angleBeam,
dataRings [iCaughtBorder - 1] .InnerRadius + RingData.MinimumWidth);
ptEndOut = Auxi_Geometry .PointToPoint (m_center, angleBeam,
dataRings [iCaughtBorder] .OuterRadius - RingData.MinimumWidth);
}
AdjustCursorPosition (Auxi_Geometry .PointToPoint (m_center, angleBeam,
rad));
}
• When the interior or exterior border of the set of rings is moved, then the proportional change of all rings is
organized. For this, the width distribution is calculated by the WidthDistribution() method. The width
ratio between rings is stored in the fPart[] array.
public void StartResizing (Point ptMouse, int iNode)
{
float rad;
angleBeam = Auxi_Geometry .Line_Angle (m_center, ptMouse);
iCaughtBorder = (iNode - 1) / 2;
if (iCaughtBorder == 0) // inner border
{
rad = InnerRadius;
WidthDistribution ();
ptEndIn = Auxi_Geometry .PointToPoint (m_center, angleBeam,
RingData .MinimumInnerRadius);
ptEndOut = Auxi_Geometry .PointToPoint (m_center, angleBeam,
OuterRadius - minWidthAllowed);
}
else if (iCaughtBorder == dataRings .Count) // outer border
{
rad = OuterRadius;
WidthDistribution ();
ptEndIn = Auxi_Geometry .PointToPoint (m_center, angleBeam,
InnerRadius + minWidthAllowed);
ptEndOut = Auxi_Geometry .PointToPoint (m_center, angleBeam, 4000);
}
… …
The WidthDistribution() method also calculates the minimum allowed width for the whole set of rings –
minWidthAllowed. The maximum squeezing coefficient is easily calculated by comparison of the narrowest ring in a
set and the minimum allowed width for any ring. If any border is pressed with the left button, then some resizing starts.
Regardless of the pressed border, during the resizing mouse is allowed to move only along radius between two calculated
points. If outer or inner border of the set is moved, then the total width of the set is changed and the fPart[] array is used
to calculate the new width of each ring. If some inner border between two rings is moved, then the combined width of these
two rings does not change, but the new width of each of them is calculated.
Two different types of rotation are organized for the Rings_Coaxial objects; both start with the right button press and
with the calculation of compensation angles for all rings in a set.
public void StartRotation (Point ptMouse, int iNode)
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
compensation .Clear ();
foreach (RingData ring in dataRings)
{
compensation.Add (Auxi_Common.LimitedRadian (angleMouse - ring.Angle));
}
}
World of Movable Objects 371 (978) Chapter 12 Simpler covers
Another context menu can be called at any empty place; it has a single command to reinstall the default view of the
Form_Rings_Coaxial.cs
.Pay attention to one interesting thing: though rings in the set can be rotated individually, the whole set of rings is not a
complex object. The Rings_Coaxial object has a single cover for all its rings regardless of their number; such object
is registered in the mover queue with a simple Mover.Add() method. All commands of menu (figure 12.13) change the
order or number of the rings in view and yet all these commands with one exception do not require the call of the
RenewMover() method. When rings are added or deleted, there is still the same Rings_Coaxial object and only its
cover is changed but nothing in the mover queue.
The only mentioned exception is the command “Leave only pressed ring”, but it happens only because I decided to do it in a
simple way. If I would decide to delete all other rings one by one, then the RenewMover() method would be not needed.
Instead a new Rings_Coaxial object is organized; this object consists of the single pressed ring.
private void Click_miLeaveOneRing (object sender, EventArgs e)
{
rings = new Rings_Coaxial (this, mover, rings [iRingForMenu]);
RenewMover ();
}
Because now there is a new object on the screen, then the use of the RenewMover() method is mandatory.
• If any odd node is pressed, then it is the node on the border. In this case the layer inside the border is always
rotated; if there is a layer outside the pressed border, it is also rotated. There are also more variants as on the inner
side of the border there can be either circle or ring.
We already know that at the starting moment of any rotation one or several compensation angles must be calculated. This is
done inside the Circle_PlusRings.StartRotation() method; because there are different types of rotation with
either one or two levels involved, then there are one or two compensation angles with appropriate abbreviation.
• If the circle or any ring is pressed for rotation, then this angle is called compensation.
• If some border is pressed, then for the inner layer it is compensationIn and for another one it is
compensationOut. For outer border of the whole object the last one does not exist.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right)
World of Movable Objects 374 (978) Chapter 12 Simpler covers
{
double angleMouse = Auxi_Geometry.Line_Angle (dataCircle .Center, ptM);
if (iNode == 0)
{
dataCircle .Angle = angleMouse - compensation;
}
else if (iNode == 1)
{
dataCircle .Angle = angleMouse - compensationIn;
if (dataRings .Count > 0)
{
dataRings [0] .Angle = angleMouse - compensationOut;
}
}
else if (iNode % 2 == 0)
{
dataRings [iNode / 2 - 1] .Angle = angleMouse - compensation;
}
else
{
int iRingInside = (iNode - 3) / 2;
dataRings [iRingInside] .Angle = angleMouse - compensationIn;
if (iRingInside + 1 < dataRings .Count)
{
dataRings [iRingInside + 1] .Angle =
angleMouse - compensationOut;
}
}
bRet = true;
}
return (bRet);
}
Objects demonstrated in the last two examples are very similar in view, but I use two different sets of rules for their
rotation. There can be even one more set of rules when all the layers inside the pressed border are rotated; the same logic
can be applied to resizing. I cannot say which logic is the best. It is possible to implement all three of them and give users a
choice. This will slightly increase the code but considerably increase the flexibility. What is more important, any user will
have a chance to select whatever he prefers and needs at the moment. Only there is a problem of good explanation for all
these choices...*
*
Nearly half a century ago there was a Danish comedy Sla forst Frede. While working on all these possibilities of
individual and synchronous rotations, I remembered one item from that film – a pistol which could shoot forward and
backward depending on the previously pushed button. Funny scenes in that film were based on explanations and
misunderstandings.
World of Movable Objects 375 (978) Chapter 12 Simpler covers
comments. Menu on a circle allows to add new comments or to delete all existing comments. Menu on comment allows to
change its parameters (font and color) or to delete the pressed comment.
Circle_WithComments class is derived from the Sircle_SimpleCoverAdh class, so it has a very simple cover
of two nodes and uses the adhered mouse for resizing. The only new part in this class is the List of comments.
public class Circle_WithComments : Circle_SimpleCoverAdh
{
List<CommentToCircle> m_comments = new List<CommentToCircle> ();
All comments from this List are informed about any change of its associated circle. When circle is moved, then the
information is sent via the CommentToCircle.NewCircleSizes() method which allows to adjust comment
position to two changes simultaneously.
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
foreach (CommentToCircle comment in m_comments)
{
comment .NewCircleSizes (Center, Radius);
}
}
When circle is resized, then information to comments is sent via the CommentToCircle.CircleRadius property.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
Move (dx, dy);
}
else
{
bRet = base .MoveNode (iNode, dx, dy, ptM, catcher);
for (int j = 0; j < m_comments .Count; j++)
{
m_comments [j] .CircleRadius = Radius;
}
}
}
else if (catcher == MouseButtons .Right)
{
bRet = base .MoveNode (iNode, dx, dy, ptM, catcher);
}
return (bRet);
}
In many cases the circle rotation does not affect associated comments and this is the way the current example works. A bit
further I’ll demonstrate another example in which comments are informed about the circle rotation and adjust their
positions. In the chapter Data visualization I’ll introduce a class of circles which have two types of associated comments;
one of them ignores circle rotation while another synchronously goes around. There are always variants.
File: Form_Rings_WithComments.cs
Menu position: Graphical objects – Complex objects – Rings with comments (advanced)
Usually I deal with circles and rings in a similar way and if I demonstrate anything for circles, I try to organize similar
example with rings. Only I do not want to design the exact copy, so in the menu command to start the next example you
can see a word advanced in brackets. There were two different examples for rectangles with comments; the first one was
simpler while the second one included a big group of controls to show the values for all changing parameters. That second
World of Movable Objects 378 (978) Chapter 12 Simpler covers
example with rectangles was marked as advanced, so the new example with rings and comments is marked by the same
word because it also uses a group of controls to demonstrate all the changing parameters (figure 12.16).
Ring comments are designed as close as possible to circle comments. The main difference is that dominant element has two
radii, so the CommentToRing class has one extra field and all similar methods of this class have one extra parameter in
comparison with the CommentToCircle class.
public class CommentToRing : Text_Rotatable
{
PointF ptRing; // ring center
float radiusInner; // inner radius
float radiusOuter; // outer radius
double coef; // positioning coefficient
double angleToComment; // angle from the ring center to comment center
There is still one positioning coefficient, but it has three different areas for calculation.
• If comment is inside the inner border, then this is a coefficient from the [-1, 0] range with -1 corresponding to
the central point and 0 – to the inner border.
• If comment is between two borders, then coefficient belongs to the [0, 1] range with 0 corresponding to the
inner border and 1 – to the outer border.
• If comment is outside the outer border, then coefficient is greater then 1 and is equal to the distance between
comment and the outer border.
When comment is moved individually, its absolute position is changed and then coefficient and angle from the ring center
to comment must be recalculated. The only difference from the circle comment is in the code of
CoefficientUpdate() method.
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
World of Movable Objects 379 (978) Chapter 12 Simpler covers
CoefficientUpdate ();
angleToCmnt = -Math .Atan2 (Location.Y - ptRing.Y, Location.X - ptRing.X);
}
public void CoefficientUpdate ()
{
double distance = Auxi_Geometry .Distance (ptRing, Location);
if (distance <= radiusInner)
{
coef = -1 + distance / radiusInner;
}
else if (distance <= radiusOuter)
{
coef = (distance - radiusInner) / (radiusOuter - radiusInner);
}
else
{
coef = Math .Max (1.0, distance - radiusOuter);
}
}
When a ring is moved forward, its central point is changed; comment has to move synchronously after getting this new
value.
public PointF RingCenter
{
get { return (ptRing); }
set
{
ptRing = value;
Location = Auxi_Geometry .PointByRingCoefficient (ptRing, radiusInner,
radiusOuter, angleToCmnt, coef);
}
}
Ring can be resized by two borders, so two properties are used. Positioning coefficient of comment does not change, but
comment has to move along the radial line on which it resides.
public float RingInnerRadius
{
get { return (radiusInner); }
set
{
if (RingArea .MinimumAllowedInnerRadius <= value &&
value <= radiusOuter - RingArea .MinimumAllowedWidth)
{
radiusInner = value;
Location = Auxi_Geometry .PointByRingCoefficient (ptRing,
radiusInner, radiusOuter, angleToCmnt, coef);
}
}
}
public float RingOuterRadius
{
get { return (radiusOuter); }
set
{
if (value >= radiusInner + RingArea .MinimumAllowedWidth)
{
radiusOuter = value;
Location = Auxi_Geometry .PointByRingCoefficient (ptRing,
radiusInner, radiusOuter, angleToCmnt, coef);
World of Movable Objects 380 (978) Chapter 12 Simpler covers
}
}
}
The Form_Rings_WithComments.cs (figure 12.16) uses rings of the Ring_WithComments class which is derived
from the Ring_SimpleCoverAdh class.
public class Ring_WithComments : Ring_SimpleCoverAdh
{
int nVersion = 701;
List<CommentToRing> m_comments = new List<CommentToRing> ();
Such inheritance means that the adhered mouse technique is used for ring resizing and the cover of each ring consists of
only four nodes. For easier understanding of the code, I will remind that borders are covered by nodes with numbers one
(inner border) and three (outer border), while moving of the whole ring is provided by the node number two.
When ring is moved, then all comments are informed about the change. There is a
CommentToRing.NewRingSizes() method which allows to send all three parameters simultaneously.
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
foreach (CommentToRing comment in m_comments)
{
comment .NewRingSizes (Center, InnerRadius, OuterRadius);
}
}
Ring can be resized by one or another border and then comments can be informed about either inner or outer radius change,
but I simplified the code and use the same NewRingSizes() method in both cases.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 2)
{
Move (dx, dy);
}
else
{
bRet = base .MoveNode (iNode, dx, dy, ptM, catcher);
for (int j = 0; j < m_comments .Count; j++)
{
m_comments [j].NewRingSizes (Center, InnerRadius, OuterRadius);
}
}
}
else if (catcher == MouseButtons .Right)
{
bRet = base .MoveNode (iNode, dx, dy, ptM, catcher);
}
return (bRet);
}
Ring rotation does not affect the comments in this example, but you will see different reaction in the next one.
Rings and comments in the Form_Rings_WithComments.cs can be involved in different movements and throughout all
these movements values of their parameters are shown in the controls of two groups.
• When ring is moved or resized, then all its comments are also moved and the group Comment shows parameters
for the first of comments. Certainly it happens only if there are any comments associated with this ring.
World of Movable Objects 381 (978) Chapter 12 Simpler covers
• When comment is moved, then group Ring shows parameters for its associated ring. This ring is easy to find
because each comment is associated with one or another ring and keeps the id of this ring. This is a standard
practice for complex objects and it was demonstrated in several of the previous examples.
File: Form_CirclesRingsSpecialComments.cs
Menu position: Graphical objects – Complex objects – Circles and rings with special comments
Rectangles with comments can be looked at as an intermediate step in moving from simple rectangles to the plots used in
scientific and engineering programs. In the same way circles and rings with comments is a step in the direction of plots
used in financial applications. Further on in the chapter Data visualization I’ll demonstrate pie charts and ring sets with a
lot of comments which can be freely moved around the screen. The current example is organized to explain the work of
some classes which will be used later in some real applications. But first I want to give some explanations about special
features and limitations on the movements of the new objects.
Pie chart which is used in financial applications consists of a multicolored circle with movable partitions; different types of
comments can be associated with a whole circle or with its sectors. Parts of a pie chart can be involved in different
individual, related, and synchronous movements. Pie chart consists of many elements with a lot of different parameters;
while an application works, all those parameters can be modified through menu commands and special tuning forms. When
a new pie chart is added into program, it is done with another auxiliary form which allows to specify some parameters of the
new object. As I said, all parameters can be modified later after the new pie chart is added, so there is no need to organize
similar complex mechanism of tuning at the stage of new pie chart creation and usually an auxiliary form for new design
allows to specify a shorter list of parameters. The level of simplification depends on the developer’s view on the process.
To make the design of new object more obvious, an auxiliary form for its creation has to demonstrate this object and the
lighter version of object might use simpler version of allowed movements. While organizing such auxiliary form for pie
charts, I decided to impose some restrictions on comment movements and to exclude the tuning of some individual
parameters. At the same time some synchronization of comment movements was added; in the real applications the same
synchronization is organized via menu command. You will see all these things in the Form_PlotsVariety.cs example in
the chapter Data visualization, while I think that just now is the better moment to discuss some auxiliary classes and the
movements of their objects.
Design of circles and rings with comments is similar, so this example includes both variants (figure 12.17). At the same
time, the similarity in design allows me to write mostly about circles with comments and not to repeat the same explanation
in connection with rings.
Multicolored circle of the Circle_PartitionsAndComments class is resizable by border. Inner partitions are also
movable. To avoid sector disappearance, there is a minimal allowed sector angle (minSectorAngle) of 0.05 radian.
World of Movable Objects 382 (978) Chapter 12 Simpler covers
Each sector is associated with personal comment which is positioned on the central sector radius and can be moved only
along this radial line.
public class Circle_PartitionsAndComments : GraphicalObject
{
Form form;
Mover supervisor;
CircleData dataCircle;
CommentOnCircleRadius [] m_comments;
PointF ptEndIn, ptEndOut;
double compensation;
double minSectorAngle = 0.05; // in radians
Cover of this class uses the ideas which were already demonstrated with other circles:
• Partitions movability is provided by strip nodes along sector borders.
• Movability and resizability of the big circle are provided by two circular nodes with a small difference between
their radii. For the circular node which is used to move the whole object, shape of the mouse cursor is changed
from the default one to Cursors.SizeAll.
public override void DefineCover ()
{
double angleLine = Angle;
double [] sector_angle = SectorAngles ();
int nSectors = sector_angle .Length;
CoverNode [] nodes = new CoverNode [nSectors + 2];
for (int i = 0; i < nSectors; i++) // nodes on borders between sectors
{
nodes [i] = new CoverNode (i, Center, Auxi_Geometry .PointToPoint (
Center, angleLine, Radius));
angleLine += sector_angle [i];
}
nodes [nSectors] = new CoverNode (nSectors, Center, Radius - delta,
Cursors .SizeAll);
nodes [nSectors + 1] = new CoverNode (nSectors + 1, Center, Radius+ delta);
cover = new Cover (nodes);
cover .SetClearance (false);
}
The adhered mouse technique is used for circle resizing and this requires two additions in the code.
First, form and mover must be among the constructor parameters.
public Circle_PartitionsAndComments (Form frm, Mover mvr, PointF ptC,
float rad, double angleDegree, double [] fVals, string [] strs)
Second, when the mouse cursor is pressed anywhere in the border vicinity to start the resizing, cursor is moved exactly on
the border and after it throughout the whole process of resizing the mouse position is used as new border position. Cursor
movement throughout the resizing is allowed only along radial line (see figure 12.5), so the StartResizing()
method adjusts the initial cursor position and calculates the end points of line segment for further cursor movement.
public void StartResizing (Point ptMouse)
{
double angleBeam = Auxi_Geometry .Line_Angle (Center, ptMouse);
AdjustCursorPosition (Auxi_Geometry .PointToPoint (Center, angleBeam,
Radius));
ptEndIn = Auxi_Geometry .PointToPoint (Center, angleBeam, MinimumRadius);
ptEndOut = Auxi_Geometry .PointToPoint (Center, angleBeam, 4000);
}
Resizing of the Circle_PartitionsAndComments objects is organized in the same way as was described for the
Circle_SimpleCoverAdh objects somewhere 20 pages back (Form_Circles_SimpleCoverAdh.cs, figure 12.5).
Movement of partitions between sectors is identical to the one demonstrated with the Circle_SlidingPartitions
objects approximately 170 pages back (Form_Circles_SlidingPartitions.cs, figure 9.3). Thus, there is nothing new in the
World of Movable Objects 383 (978) Chapter 12 Simpler covers
changes of circle, but we now deal with a complex object, so we need to look at the individual and related movements of
comments.
Comments which are used in the new class belong to the CommentOnCircleRadius class. This class is derived from
the CommentToCircle class and in the name of the new class I tried to mention its most important new feature. For
better understanding of some properties, I want to show the whole chain of inheritance for the new class of comments. It is
useful to remember this chain, because when anything is done with some CommentOnCircleRadius object, then the
reaction is often determined not by some method of the new class, but by some method from one of the deeper levels.
CommentOnCircleRadius : CommentToCircle : Text_Rotatable : GraphicalObject
As nearly any other comment which is used in examples of this Demo application, the CommentToCircle class is
derived from the Text_Rotatable class, so it can be moved around the screen and can be rotated around its central
point. It is rotatable by default through inheritance, but in this particular case I prefer to deal with non-rotatable comments
and this feature is easily regulated by the Text_Rotatable.Rotatable property.
I also need comments to be visually associated with circle sectors on the one to one basis. If a comment is always
positioned on the central radial line of the associated sector, then there are no questions about this association. Each
comment is initially positioned on this line and is allowed to move only along this line. Position of any
Text_Rotatable object is determined by its central point, so movement of comment along the line means the
movement of its central point along the mentioned line. It is similar to movement of a small spot along straight segment.
Though comment might have solid sizes and there will be no cursor shift at the starting moment of such movement, I will
use the adhered mouse technique, so the form and mover are among the fields of the new class. There are several more
fields which are used as auxiliary throughout the movements.
public class CommentOnCircleRadius : CommentToCircle
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut, ptEndIn_Center, ptEndOut_Center;
When a circle with comments is initialized, central lines for all sectors are calculated and comments are placed on those
lines slightly outside the circle border. All comments are placed at the even distance from the circle center and they will
stay even all the time; this is one more difference from the objects of the CommentToCircle class which usually do not
correlate their positions with each other.
public Circle_PartitionsAndComments (Form frm, Mover mvr, PointF ptC,
float rad, double angleDegree, double [] fVals, string [] strs)
{
form = frm;
supervisor = mvr;
dataCircle = new CircleData (ptC, rad, angleDegree, fVals);
compensationCmntAngle = new double [dataCircle .SectorsNumber];
int nSectors = dataCircle .SectorsNumber;
double angleBorder = Angle;
double [] sector_angle = SectorAngles ();
m_comments = new CommentOnCircleRadius [nSectors];
for (int i = 0; i < nSectors; i++)
{
m_comments [i] = new CommentOnCircleRadius (form, supervisor, Center,
Radius, Auxi_Convert .RadianToDegree (angleBorder +
sector_angle [i] / 2), 20, strs [i]);
m_comments [i] .Rotatable = false;
m_comments [i] .ParentID = ID;
angleBorder += sector_angle [i];
}
}
Circle with comments is a complex object in which circle plays the main role and comments are positioned in relation to
this dominant element. Circle can be involved in four different movements; each time all comments have to react to the
circle change.
World of Movable Objects 384 (978) Chapter 12 Simpler covers
When circle is moved, all comments are informed about the new circle central point. Each comment of the
CommentToCircle class keeps its positioning coefficient, so the CommentToCircle.CircleCenter property is
used to retain the same relative position.
public override void Move (int dx, int dy)
{
Center += new Size (dx, dy);
for (int j = 0; j < m_comments .Length; j++)
{
m_comments [j] .CircleCenter = Center;
}
}
When circle is resized, then the adhered mouse technique is used to determine the new circle radius. After it all comments
are informed about this change via the CommentToCircle.CircleRadius property and each comment retains the
same relative position by using its own positioning coefficient.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else
{
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
Radius = Convert .ToSingle (
Auxi_Geometry .Distance (Center, ptNearest));
AdjustCursorPosition (ptNearest);
for (int j = 0; j < m_comments .Length; j++)
{
m_comments [j] .CircleRadius = Radius;
}
bRet = true;
}
… …
Similar things happen throughout the circle rotation and this is a significant change from the previous example of circles
with comments. In the Form_Circles_WithComments.cs I demonstrated CommentToCircle objects which did not
react to rotation of their dominant element. Any CommentToCircleRadius object has to stay on the central line of
associated sector, so all these objects have to rotate synchronously. The circle pressed by the right button is informed about
the rotation start through a special StartRotation() method.
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (e .Button == MouseButtons .Right)
{
if (grobj is Circle_PartitionsAndComments)
{
(grobj as Circle_PartitionsAndComments)
.StartRotation (e .Location);
}
… …
This Circle_PartitionsAndComments.StartRotation() method calculates the compensation angle
(compensation) which is needed for rotation of a circle itself and calculates an array of angle shifts from the starting
side of the first sector to all the comments (compensationCmntAngle[]).
World of Movable Objects 385 (978) Chapter 12 Simpler covers
public void StartRotation (Point ptMouse)
{
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptMouse);
compensation = Auxi_Common .LimitedRadian (angleMouse - Angle);
double shitToBorder = 0;
double [] sector_angle = SectorAngles ();
for (int i = 0; i < compensationCmntAngle .Length; i++)
{
compensationCmntAngle [i] = shitToBorder + sector_angle [i] / 2;
shitToBorder += sector_angle [i];
}
}
These angle shifts are not going to change throughout the circle rotation and are used to calculate the real angle to each
comment inside the Circle_PartitionsAndComments.MoveNode() method.
public override bool MoveNode (int iNode, 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 (Center, ptM);
Angle = angleMouse - compensation;
for (int j = 0; j < m_comments .Length; j++)
{
m_comments [j] .AngleToComment = Angle + compensationCmntAngle [j];
}
bRet = true;
}
return (bRet);
}
Change of sectors is done by moving the sector borders. This process was demonstrated with the
Circle_SlidingPartitions class in the Form_Circles_SlidingPartitions.cs (figure 9.3) and the resectoring in the
Circle_PartitionsAndComments class is identical. The only addition is the existence of comments which must be
always positioned at the central radial lines of their associated sectors. When any sector border is moved, then two sectors
are changed, so angles to comments of these two sectors have to change.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
int nSectors = dataCircle .SectorsNumber;
if (iNode < nSectors)
{
// border between two sectors
double angleMouse = Auxi_Geometry .Line_Angle (Center, ptM);
… …
if (min_angle_Resectoring + minSectorAngle <= angleMouse &&
angleMouse <= max_angle_Resectoring - minSectorAngle)
{
… …
m_comments [iSector_Clockwise] .AngleToComment =
dataCircle .SectorStartAngle (iSector_Clockwise) +
dataCircle .SectorAngle (iSector_Clockwise) / 2;
World of Movable Objects 386 (978) Chapter 12 Simpler covers
m_comments [iSector_Counterclock] .AngleToComment =
dataCircle .SectorStartAngle (iSector_Counterclock) +
dataCircle .SectorAngle (iSector_Counterclock) / 2;
}
… …
Comments in the Form_CirclesRingsSpecialComments.cs are purposely made
nonrotatable and the only type of their individual movement is the moving along
radius. To be absolutely correct, it is the central point of comment which is moving
along radius, while the pressed point of comment moves along the line parallel to
that radius (figure 12.18). When comment is pressed for moving, then its
StartMoving() method is called.
private void OnMouseDown (object sender,
MouseEventArgs e)
{
ptMouse_Down = e .Location; Fig.12.18 Two lines show the
if (mover .Catch (e .Location, e .Button)) tracks for two comment
{ points. Gray line shows
GraphicalObject grobj = mover .CaughtSource; the track for central
if (e .Button == MouseButtons .Left) comment point; black
{ line shows the track for
… … mouse cursor.
else if (grobj is CommentOnCircleRadius)
{
CommentOnCircleRadius cmnt = grobj as CommentOnCircleRadius;
cmnt .StartMoving (e .Location);
circleParent = (cmnt .ParentID == circle_7 .ID) ? circle_7
: circle_12;
ptIn_Mouse = cmnt .MouseWay_InnerPoint;
ptOut_Mouse = cmnt .MouseWay_OuterPoint;
ptIn_Auxi = cmnt .CenterWay_InnerPoint;
ptOut_Auxi = cmnt .CenterWay_OuterPoint;
bPossibleLineShow = true;
}
… …
At this moment the mouse point is known and the comment central point is easily obtained from the
Text_Rotatable.Location property. There is a limit on minimal distance (minDistCmntToCircleCenter)
between comment central point and circle center, so the limit point for movement of comment central point is easily
calculated (ptEndIn_Center). Then the end point for mouse cursor movement (ptEndIn) is calculated from
parallelogram.
public void StartMoving (Point ptMouse)
{
PointF ptL = Location;
distMouseToCmntCenter = Auxi_Geometry .Distance (ptMouse, ptL);
angleMouseToCmntCenter = Auxi_Geometry .Line_Angle (ptMouse, ptL);
double angleCommentCenter = Auxi_Geometry .Line_Angle (CircleCenter, ptL);
ptEndIn_Center = Auxi_Geometry .PointToPoint (CircleCenter,
angleCommentCenter, minDistCmntToCircleCenter);
ptEndOut_Center = Auxi_Geometry .PointToPoint (CircleCenter,
angleCommentCenter, 4000);
ptEndIn = Auxi_Geometry .PointToPoint (ptEndIn_Center,
angleMouseToCmntCenter + Math .PI, distMouseToCmntCenter);
ptEndOut = Auxi_Geometry .PointToPoint (ptEndOut_Center,
angleMouseToCmntCenter + Math .PI, distMouseToCmntCenter);
}
When the adhered mouse technique is used for moving of some object along the straight segment, then the whole procedure
is standard. On every change of mouse position, the nearest point on segment of its allowed movement is calculated
World of Movable Objects 387 (978) Chapter 12 Simpler covers
(ptNearest). Cursor is moved to this point and the new position for comment central point is calculated by using the
initial shift between cursor and comment central point.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
PointF ptNearest =
Auxi_Geometry .NearestPointOnSegment (ptM, ptEndIn, ptEndOut);
supervisor .MouseTraced = false;
Cursor .Position = form .PointToScreen (Point .Round (ptNearest));
supervisor .MouseTraced = true;
Location = Auxi_Geometry .PointToPoint (ptNearest,
angleMouseToCmntCenter, distMouseToCmntCenter);
CoefficientUpdate ();
bRet = true;
}
return (bRet);
}
The only addition in case of our comments is caused by the additional requirement for synchronous movement of all
comments. This is organized by using the CommentsSynchroMove() method.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is CommentOnCircleRadius)
{
CommentOnCircleRadius cmnt = grobj as CommentOnCircleRadius;
circleParent .CommentsSynchroMove (cmnt .PositionCoefficient,
cmnt .ID);
}
… …
In the case of our comments, their synchronous movement is organized by spreading the positioning coefficient from the
pressed comment to all its siblings.
public void CommentsSynchroMove (double coef, long idSample)
{
for (int i = 0; i < m_comments .Length; i++)
{
if (m_comments [i] .ID != idSample)
{
m_comments [i] .PositionCoefficient = coef;
}
}
}
These were explanations for circles and their comments. Work with rings is organized in identical way and is demonstrated
in the same example.
While organizing resizing of circles, rings, or more complex objects consisting of such parts, I prefer to calculate two end
points for the line segment along the radius and allow to move the mouse only along this segment. While organizing
rotation of the same elements, I do not use this technique of adhered mouse. It would be not a problem to use the same
mechanism of adhered mouse and set the way for the mouse strictly around the circle; this was already shown in the
Form_SpotOnLinesAndArcs.cs in the previous chapter. The code for such addition would be simple as it is a movement
around the closed circle and there are no problems of the gaps or limited arcs. Why the adhered mouse is not used for
rotation in these examples?
For circles and rings with big enough radii this will be OK; for really small objects there can be a problem of accurate turn
on the needed angle. When I need to turn a small object, I often grab it with the right button, then move the mouse
somewhere far away from the center of rotation, and start to move the mouse around only there; in this way I can turn even
a tiny object with high precision. It is not only while turning circles or rings; the same thing happens when I have to turn a
short text.
Anyway, if you want to use the same technique of adhered mouse and restricted cursor trail for rotation, you can easily do
it. The helpful code can be found in the mentioned example.
I use the adhered mouse more and more not only for circles and rings but with many different objects of the most popular
rectangular shape. I am still considering the possibility of including into the book different examples from the first chapters
which were demonstrated without this technique but which can be noticeably improved by using the adhered mouse.
In September of 2016 I finished writing another and definitely smaller book Elements of Total Movability. Nearly half of
this book is about the moving / resizing of simple objects of the most popular shapes (rectangle, circle, ring, and so on).
Everything is done with adhered mouse and I think that the explanation and demonstration is organized in a better way.
This new book can be downloaded together with its own accompanying program and all the codes.
World of Movable Objects 389 (978) Chapter 13 Individual controls
Individual controls
Controls are widely used in design of interfaces. Controls can be used as individual elements, in groups, and
they can be united with different graphical objects. Depending on the case, different technique can be used
for moving such objects. This chapter is only about the moving / resizing of solitary controls.
Applications are constructed of the elements of two different types: graphical objects and controls. In reality, all the screen
elements are graphical, but from the very beginning of the modern systems the controls were declared special by
manufactures and were designed in a special way. I would prefer to design applications exclusively on the basis of
graphical objects because the existence of controls as superior elements creates so many problems. Some controls can be
easily substituted with the graphical analogues; these changes have a lot of advantages without any visible negative effects.
At least, I do not know any such negative effects. The Trackbar class demonstrated in the Form_Trackbars.cs
(figure 10.8) in the chapter Complex objects is one example of switching from the standard control to its graphical
analogue. There are other controls that require more work for such substitution, but I think that this is the right way for
progress in programming and design of applications. But at the current moment a lot of applications are based on controls,
so it is important to look into the problems of moving and resizing such elements.
When you design and work with the applications composed of the fixed controls with the occasionally repainted
background, you have no problems with the controls. You simply got used to whatever is provided with them (all their
properties and events) and what you can get of them as a developer. Users also got used to the view of controls and their
behaviour throughout the last quarter of a century and demonstrate the classical Pavlov’s reflex: if they see a button on the
screen, they click it with a mouse; if they see some kind of a list, they scroll it and press the needed line. Everything works
as expected and this makes the reflex only stronger.
What none of users try to do is to move the buttons, for example, to another place, even if they think that another place will
be better. Well, I think that some users tried to do it when there was no one around to laugh at their attempts but
immediately found out that that was impossible. The reflex was fixed forever: you can click the controls but not move.
In reality this statement about the controls being not movable is absolutely wrong; they are movable, they are developed to
be movable and resizable, but these features are hidden from users and are occasionally used by developers to make an
impression. (Like some people in the passed centuries made their living on the ground of knowing which gradients to throw
into the flame to produce the colored smoke. Some of those people were burnt at the stake for such knowledge, but that is
an absolutely different story.) The popular dynamic layout is based on ability of controls to be moved and resized.
Controls can be easily moved and resized by using their properties; the demonstration of a button running away from the
approaching mouse is just a funny example of using these properties. (Write several primitive lines of code for one mouse
event and users will be amazed by the behaviour of those crazy controls.) This is an example of how developers can use
these features because such running away control is going to move according to the rules predefined by developer. I am
writing about the mechanism that allows USERS to move controls in any way they would like to do, so it is absolutely
different, though I use exactly the same properties. It is the question of who is managing the movements of controls:
whether the algorithm is absolutely predetermined by a developer (the control is moved for predefined number of pixels in
predetermined direction when the mouse arrives over it) or all the movements are determined only by users when they
decide to move a control one way or another. I think that users and only users must get the full control of moving and
resizing the controls.
*
There is some inconsistency in using the border of graphical objects for resizing only but the border of controls for both
actions. This is one of the consequences of having controls on the screen. Controls can be redesigned and treated like all
other objects; this will be really a big step and I think that it will happen some time in the future. There is absolutely
World of Movable Objects 390 (978) Chapter 13 Individual controls
Certainly, this frame around a control is used as a cover. It looks a bit strange to have a cover outside an object area, but
there are two remarks to this situation. First, the cover often goes outside a real object; in fact, it goes outside every time
when an object is resizable; the covering of the border makes sensitive an area on both sides of the border. Second, this
cover which is used for moving and resizing of a control belongs not to the control itself but to a SolitaryControl
object which wraps this control. This wrapper of
a control is registered in the mover queue as any
other object. Moving and resizing of this wrapper
are directly translated into the moving and
resizing of the associated control. The only
question in organizing such a frame is the
placement of the nodes for resizing, but several
applications use the same technique, so the
practice is already known. For example, when
you change the size of some control in Visual
Studio, you resize it by the small squares in the
corners or in the middle of the sides. So, the best
and expected places for the nodes to resize any
control are obvious, other details depend on the
purpose of the needed resizing.
Up till this moment I have demonstrated more
than 80 different examples (forms). A lot of them
use one or two small buttons to switch the cover
visualization ON / OFF and to return an area with
information into view. You can see the same two
buttons in the top left corner of the
Form_SolitaryControls.cs (figure 13.1), but
there are also six big controls which are used to
explain different variants of movable and
resizable controls. There is also a green group of
the familiar ElasticGroup class; controls of Fig.13.1 Individually moved controls and their covers
this group allow to change the sizes of covers. Let us exclude from discussion (only for some time) the controls inside the
group and consider only those eight individual controls which are outside the group.
SolitaryControl class has 10 different constructors; for six big controls of this example such constructor is used.
SolitaryControl (Control ctrl, // control to become movable
bool bMove, // declares an object to be movable or not
int nodesize, // size of the nodes in the corners
int frame, // frame width
bool bPrompts) // to show the prompts or not
Two small buttons are turned into SolitaryControl objects by using lighter version of constructor
SolitaryControl (Control ctrl)
This means that all omitted parameters get the default values, so it is equal to the variant
SolitaryControl (ctrl, true, 9, 6, false)
As you can see from the main variant of constructor with five parameters, none of them declare the type of resizing, but the
controls in this form can be resized in different ways and each of big controls shows the text with information about their
resizing. If there is no parameter with direct declaration of resizing type, then mover has to determine it and mover decides
about the type of resizing for particular control by analysing its sizes and the values of its MinimumSize and
nothing that demands the existence of those controls on the special level. Any one of them can be replaced with the
graphical object that looks exactly the same, behave exactly the same, but does not demand the special status. I designed
such objects before, other people were doing similar things, so this is not something absolutely unique and never heard
about. It is impossible for users to distinguish the current day controls from similarly designed graphical objects; they will
not see the difference at all. For programmers, such graphical objects are much better for design as they can be of an
arbitrary shape. Certainly, this needs some work to be done, so that the new graphical “controls” will be as easy in use for
design as our modern day controls, but I think that the word progress is a correct one to describe such work to be done.
World of Movable Objects 391 (978) Chapter 13 Individual controls
MaximumSize properties. If you need a resizable control, then set the needed ranges through those properties and after it
construct a SolitaryControl object. Mover is making a decision about possible control resizing according to such
rules.
• If the values of these MinimumSize and MaximumSize properties are not changed from their default values
(0, 0) or their values are the same as the size of a control, then this control is non-resizable. These are the buttons
in the top left corner and the button in the center of figure 13.1 below the group. This button has No resize text.
• If MinimumSize and MaximumSize properties provide a range for one direction only (width or height),
then this control is resizable in this direction only. The button in the middle of the left side can change only its
height (Resize.NS text); the TextBox control in the top right corner can change only its width (Resize.WE text).
• If MinimumSize and MaximumSize properties provide the ranges for both directions, then this control is
fully resizable; this is the case of the button in the middle of the right side (text Any).
There is one exception in the last case: if the second parameter in the above shown constructor is false, then this control
becomes unmovable and automatically non-resizable regardless of the size ranges provided by its properties! This is the
case of the ListView control closer to the bottom left corner. According to its properties, it has to be fully resizable, but
it is forced to be unmovable and, as a consequence, becomes non-resizable.
scUnmovable = new SolitaryControl (listviewResizeNotMove, false, nodesize,
frame, bPrompts);
The frame parameter of the SolitaryControl constructor determines the width of the sensitive frame around the
borders of a control; by pressing inside this area, the control can be moved. At figure 13.1, the covers are visualized and
these sensitive areas around controls are shown by the big red frames. The frame width cannot be less than 2 pixels; by
default it is 6 pixels. If the second parameter in the constructor – bMove – is set to false, then such control is unmovable.
The unmovable control automatically becomes non-resizable regardless of those two properties of a control. But this
control still has a sensitive frame around the borders; mover recognizes this area (and thus a control), so, for example, a
context menu can be called for such control.
Two big controls at figure 13.1 demonstrate the same type of cover which consists of a single red frame around the borders.
One of these controls – the button below the group – is movable; another – the ListView control closer to the bottom
left corner – is unmovable. Users are informed about the difference in their behaviour by different shapes of cursor above
those covers.
The process of making any solitary control movable / resizable differs from the same process for graphical objects in several
aspects.
1. The absence of DefineCover(), Move(), and MoveNode() methods. Simply forget about these methods
when you deal with the individual controls.
2. The resizing ranges are determined by the MinimumSize and MaximumSize properties of a control and by
comparison of these values with the sizes of a control.
3. SolitaryControl objects are registered with the mover in the same way as simple graphical objects by using
Add() or Insert() methods.
mover .Add (scWE);
mover .Add (scNS);
mover .Add (scNoResize);
Occasionally you can see a shorter form of the control registration, but this is only a shorter notation and nothing else.
mover .Add (ctrl);
mover .Insert (iPos, ctrl);
Even in such case the control is wrapped into the SolitaryControl object, but this is done by the mover somewhere
behind the curtains. Thus prepared SolitaryControl object is registered with the mover, but in such case this wrapper
gets the default parameters. This means, among other things, the resizing in any direction. Certainly, if the two mentioned
properties of a control allow to do it.
Depending on the type of the needed (organized) resizing, the covers at figure 13.1 show different number of nodes around
controls, but this is only on visualization. A cover for any SolitaryControl object consists of nine nodes. First eight
nodes are the small nodes next to corners and middle points on the sides. If you do not see part of these nodes next to some
of the controls at the figure, then it means that the parameters of those nodes were changed so as to make them invisible and
World of Movable Objects 392 (978) Chapter 13 Individual controls
not working, but the number of nodes did not change. These eight nodes are numbered from the top left corner and going
clockwise, so the node near the top left corner has number 0, in the middle of the upper side – 1, and so on. The last node in
the cover – node with number eight – is a big rectangular node which is wider than the control itself by the frame pixels
on each side. The Form_SolitaryControls.cs was designed especially for demonstration, so you can switch the
visualization of covers ON / OFF, but in real applications I never show the covers, so the easiness of moving and resizing
the controls depends on the design decision and the selection of several parameters.
The nodesize parameter of the SolitaryControl constructor determines the sizes of the corner nodes (these nodes
are always square) and the width of nodes in the middle of the sides. The default value of the nodesize parameter is 9
pixels; it is slightly bigger than the default width of the frame which is equal to six pixels. The enlarged size of the corner
nodes makes them preferable places for resizing. The corner nodes are used not only for Resizing.Any case but also
for two other types of resizing – Resizing.WE and Resizing.NS. These three types of resizing demonstrate
different cursor shapes over the corner nodes and the resizing works according to the demand.
Length of the nodes in the middle of the sides depends on the length of the appropriate control side: the longer the side, the
longer the node, so it will be easier to find it somewhere next to the big control. This difference in node sizes is perfectly
seen with the button closer to the bottom right corner of figure 13.1. All corner nodes have the same size; all four nodes in
the middle of the sides have the same width equal to the size of the corner nodes, but the length of nodes along horizontal
sides is obviously bigger than the length of nodes along vertical sides.
Values for nodesize and frame parameters are set individually and later they can be changed by two different
properties SolitaryControl.NodeSize and SolitaryControl.FrameWidth, but there is one restriction which
does not allow to set them absolutely independently: size of the nodes cannot be less than the frame width. When there are
two parameters with such relation between their possible values, one of these parameters has to play the dominant role; in
the SolitaryControl class, the
frame width is dominant. In the
Form_SolitaryControls.cs, two sizes of
covers are regulated via two controls
inside the group. The range for the
frame is set at design time and at any
moment you can give the frame
parameter any value from this range but
if you try to make it bigger than the
nodesize value, then the nodesize
is also increased.
The last parameter in the mentioned
SolitaryControl constructor
allows to show some graphical prompts
at the places of nodes which are used for
resizing. Prompts are shown by the
SolitaryControl.Draw() method
(figure 13.2); showing the prompts is
the only reason to have this method at Fig.13.2 Prompts can be shown to mark the places of nodes which can be
all. There are only two examples in the used for resizing
big Demo application where such
prompts for control resizing can be seen. The use of these prompts is not forbidden; but I do not think that they are needed.
When you know that everything is movable and that all controls have those nodes for resizing in the same places, then you
do not need prompts for their resizing.
Now I have to admit that there is a SolitaryControl constructor which allows to declare the needed type of resizing.
SolitaryControl (Control ctrl, // control to become movable
Resizing resize, // resize type
bool bMove, // declares an object to be movable or not
int nodesize, // size of the nodes in the corners
int frame, // the frame width
bool bPrompts) // to show the prompts or not
In reality there is a whole group of constructors which represent the shortened versions of this one; only first two parameters
are mandatory while others can be included in different combinations.
World of Movable Objects 393 (978) Chapter 13 Individual controls
There is one significant limitation on setting the resizing via the parameter of this constructor: thus specified resizing cannot
be more general than the resizing estimated in the standard way from comparison of the declared sizes and the values of the
MinimumSize and MaximumSize properties. If the sizes and two properties allow the full resizing of the control, then
you can limit the allowed resizing by passing as a parameter Resizing.NS, Resizing.WE, or Resizing.None. In
such case the control gets the type of resizing that is passed as a parameter. But if the properties of control allow it to be
resized only horizontally (Resizing.WE) and you try to change it with the Resizing.NS or Resizing.All
parameter, this has no effect on the resizing of such control.
The type of resizing for a control can be set not only at the moment of initialization of
SolitaryControl object but later with the help of the Resizing property. What can
require such change of resizing for a control in the working application? Consider a case of
fully resizable control. While an application is running, you change the size of this control to
whatever you prefer and do not want to change its width accidentally after it. You can Fig.13.3 Menu to change
change its resizing status to Resizing.NS and there will be no change of the width even if the resizing of
you press the corner node. The pale yellow button in the bottom right corner of figure 13.2 yellow button
can be used to demonstrate some possibilities. I purposely set different background color of this button to distinguish it
from others; this is the only control in the form on which and around which the context menu can be called (figure 13.3).
By setting the appropriate values for its MinimumSize and MaximumSize properties, the yellow button is designed to
be fully resizable. Then I called the context menu on this button and changed its resizing to Resizing.NS; you can see it
from the view of its cover and the information on the button (figure 13.4). With the help of the same menu, I can change
the resizing of this control to any other. This flexibility (all four choices) is possible only for the controls which are
originally fully resizable. If the comparison of the original sizes and its MinimumSize and MaximumSize properties
gives another level of original resizing, then some of the positions in menu will be disabled.
private void Opening_menuResizing (object sender, CancelEventArgs e)
{
ToolStripItemCollection items = menuResizing .Items;
Resizing resizing = scOriginallyAny .Resizing;
switch (scOriginallyAny .ResizingFromMinMax)
{
case Resizing .WE:
items ["miAny"] .Enabled = false;
items ["miNS"] .Enabled = false;
break;
case Resizing .NS:
items ["miAny"] .Enabled = false;
items ["miWE"] .Enabled = false;
break;
case Resizing .None: Fig.13.4 Button and its
items ["miAny"] .Enabled = false; cover after changing the
items ["miWE"] .Enabled = false; resizing to Resizing.NS
items ["miNS"] .Enabled = false;
break;
}
((ToolStripMenuItem) items ["miAny"]) .Checked = resizing == Resizing .Any;
((ToolStripMenuItem) items ["miWE"]) .Checked = resizing == Resizing .WE;
((ToolStripMenuItem) items ["miNS"]) .Checked = resizing == Resizing .NS;
((ToolStripMenuItem) items ["miNone"]).Checked = resizing == Resizing.None;
}
Two different properties of the SolitaryControl class allow to get two levels of resizing.
• ResizingFromMinMax property returns the original level calculated at the moment of initialization.
• Resizing property returns the currently used level. The second property allows also to change the level.
private void Click_miNS (object sender, EventArgs e)
{
scOriginallyAny .Resizing = Resizing .NS;
scOriginallyAny .Control .Text = "Originally - Any\nCurrently - NS";
Invalidate ();
}
World of Movable Objects 394 (978) Chapter 13 Individual controls
The change of the control resizing does not change anything in the view of this control. The only reason for including the
call for repainting into this method is the possibility of showing covers in the Form_SolitaryControls.cs; the cover view of
the SolitaryControl object depends on the type of resizing.
There is one restriction on the size of the controls that I implemented with the SolitaryControl class: controls cannot
be reduced to less than 10 pixels. This was done to avoid the accidental disappearance of controls in applications. I do not
think that users will appreciate such disappearance; if any control has to be deleted, this must be done in a different way.
There is absolutely nothing special in the movement of controls: press the mouse next to the border and relocate the control.
There is nearly nothing special in resizing the majority of controls. For example, you will not find anything strange in
resizing panels, buttons, and list views. But you may find something a bit strange in resizing, for example, the TextBox.
The problem is not in algorithm which is exactly the same for all controls. The problem was placed at the basic level of
those controls many years ago when their developers implemented a strict link between two properties of the controls:
between their font and size. These properties have to be absolutely independent, but somebody “too smart” at Microsoft
decided to organize a link between them, and the change of font often changes the size of control in addition. Specialists
from different branches of engineering know very well that if you organize two independent sources of control and change
for the same parameter, it is often the easiest way to organize a disaster. And the “geniuses” from Microsoft did it on
purpose and with a sound mind! The best way to describe such a thing is to quote the former Russian prime-minister: “We
wanted to do the best, but the result was as usual…” The worst results of that old decision become obvious when you
design some really complex forms with a lot of different controls inside. Yet, you have to work with what you get, and if
you work with Visual Studio, you have to be aware of some of the traps. I’ll write about one of such traps in the next
chapter.
For the majority of controls, there are no tricks with declaring minimum and maximum needed sizes, but for some classes of
controls you need to take into consideration the limitations enforced by the system in which you design your applications
(Visual Studio). In all my real applications (not Demo versions) users can change all the parameters of visualization: this is
one of the main features of user-driven applications. One of the things that users can change at any moment is the font for a
set of controls or for any control on an individual basis. For a majority of controls, there are no conflicts between changing
the font and declaring the ranges for the control sizes, but for several types of controls there are some strange situations.
Usually it happens for controls with a single text line; some hidden parameters are deep inside their structure and are out of
your control when you use such controls in your design.
For example, take the NumericUpDown control. You can declare the minimum and maximum width of the control via
the MinimumSize and MaximumSize properties, but the second number in both of them – the height – always has
zero value and cannot be changed. Do not worry about this value: whenever you change the font of the control, its height is
adjusted to the font, so you always see one line of text. I would not call the adjusting of the control size to the size of a font
a good decision; I did not ask for changing of the control size and I do not like when a system is doing anything by itself
without my direct command. But let us agree with this for a moment and decide that this is a normal and expected reaction.
Then different behaviour of another but very similar control looks at least strange to me.
An ordinary TextBox control demonstrates this strange behaviour but not always. Suppose that a TextBox control is
registered with the Resizing.Any parameter, so it can be resized by the corners. If the Multiline property of this
control is set to true, then the size can be changed in the range declared by the two size properties MinimumSize and
MaximumSize. As a developer, you set these two values in such a way that allow to change the height of this text box;
after it users will decide how many lines of text they want to see.
Suppose that you have another resizable TextBox control in which a single line of text has to be shown, so its
Multiline property is set to false. When users change the font for such control, its height is not changed at all! It is
not adjusted to the size of a font (and I think this is a correct decision), so users can change the height of such control as
they do it for buttons or list boxes. For users to get a chance of changing the height of such text box, its two properties have
to allow it by providing a wide enough range for the height of the control. So, in case of such TextBox control, the
values for two properties have to be declared in different way than in the mentioned NumericUpDown control. Though
visually these two controls with a single line of text are very much alike.
The mentioned things are only minor negative remarks to otherwise very important thing: all controls are easily turned into
movable and resizable.
Few controls are used as stand alone movable elements. Much more often they are used in groups or in combinations with
graphical objects. In the next chapter we are going to look at the combination “control + comment”.
World of Movable Objects 395 (978) Chapter 14 Control + graphical text
New things can be invented either by chance or after realizing that it is really needed. Movable pairs of controls with
comments appeared in my programs by the second way. When I began to use movable / resizable plots in my programs, I
immediately saw the conflict between movable graphical objects and unmovable controls, so controls had to become
movable. The result was the SolitaryControl class which we discussed in the previous chapter. Any control wrapped
into SolitaryControl blanket becomes movable but for some of them it is not enough.
There are around 25 – 30 different types of popular controls which are used in many applications; around 10 – 12 of them
are extremely popular. If I have to mention the most popular classes of controls by their names in Visual Studio, then there
will be Button, CheckBox, ComboBox, ListBox, ListView, NumericUpDown, RadioButton, TextBox, and
TreeView.* Some of them are very informative by themselves and do not need additional explanation. Button,
CheckBox, and RadioButton give enough information by their own text and usually nothing additional is needed.
ListView can tell a lot by the texts in its header but such control can be used without header in view or it can be not
enough, so in many cases ListView needs some comment. ComboBox may contain enough information in its strings,
but in many programs you can see two or three combo boxes with the same sets of strings and these controls are used to
define different parameters, so users need some explanation next to each combo box. Without some explanation next to
NumericUpDown or TextBox control, you will never understand what you are going to change with the help of this
control.
Thus, there are controls which can live by themselves and for them it is enough to have the SolitaryControl class.
There are also controls which can exist only in pair with additional information, so the next step was to invent such class.
There are two standard ways to show some text on the screen: it can be a Label control or a painted text. Visually these
cases are undistinguishable, so the question is only in the easiness of using one or another and your personal preferences as
a programmer. When you work in the realm of fixed design, the use of Label controls is easier: position all the needed
controls with the help of Visual Studio and do not think about the Paint message.
When the whole design is based on movable elements, the situation is different. All the screen elements are movable. All
graphical objects are movable by inner points; controls can be moved only by their borders. There is no problem with the
controls having visible borders (ordinary buttons, ListView, …), but Label controls are mostly used without visible
borders. If some text is organized as a Label control, then users have to know that this object can be moved only by its
invisible borders. It is not a huge problem, but it is obviously inconvenient. It is much easier if any text can be moved by
any point. I use Label control in one of the examples further on, but this is an exception which I need for demonstration;
all texts in my applications are painted. When I began to work on the class for control with comment, I knew from the very
beginning that it should be “control + graphical text”.
In many cases the information about control is placed somewhere above its area; usually it is lined with the control on one
of the sides or by the middle point. Occasionally this information appears at the side of the control and I decided that the
same three variants of lining will be enough. In this way the class of objects “control + graphical text” with limited
positioning of the text was born.
*
Very popular GroupBox and Panel are not included into this list because they can be used as containers for other
elements and will appear in the next chapter Groups.
World of Movable Objects 396 (978) Chapter 14 Control + graphical text
It is impossible to see the cover of any CommentedControl object because its visualization is blocked; this mechanism
of not showing the cover is explained in the section Visualization of covers. Cover of the CommentedControl class is
identical to the cover of the SolitaryControl class (figure 13.1). Control used in a pair with comment can be declared
with different types of resizing as was explained in the previous chapter. As any complex object, this one cannot be
registered with mover by the simple Mover.Add() or Mover.Insert() methods, but the
CommentedControl.IntoMover() method must be used.
Comment used in the CommentedControl class belongs to the CommentToRect class. As this class is derived from
the Text_Rotatable class, then rotation of comment is done automatically without mentioning it anywhere in the code
of a form.
In the chapter Complex objects in the Form_Rectangles_WithComments.cs (figure 10.1) I explained in details the
collaboration between rectangle and associated comment of the CommentToRect class. Inside the
CommentedControl class we have exactly the same collaboration between comment and control area. In the old
example, comments are always shown above the associated rectangle because we are free to decide about the order of
graphical objects on the screen. In the CommentedControl class the graphical comment has to deal with control;
controls are always shown above all graphical objects; this is defined by the system and cannot be changed in any way.
Such mandatory order of two parts inside the CommentedControl object – control above its comment – causes one
problematic situation which requires an absolutely unusual action in user-driven applications – the enforced relocation of an
element by the program. I’ll write about this special case a bit further.
Comment belongs to the CommentToRect class which is derived from the Text_Rotatable class, so position of
any comment is described by its central point. Comment has two mechanisms of positioning: either by the coordinates or
by coefficients that describe its relative position to the rectangular area of the control. These two descriptions of comment
position exist all the time but are used in different ways throughout different movements. If comment is moved
individually, then the coefficients are recalculated on the basis of the changing position; if the control is moved or resized,
then the new comment position is calculated on the basis of the unchanging coefficients.
The CommentedControl class has a lot of different constructors which allow to organize in different ways the initial
positioning of comment in relation to the control and specify for this comment the font, color, and angle. There are 50 (!)
different constructors in the CommentedControl class. Maybe it is a bit too much but it gives a chance to select the one
you really need in one or another case. You do not need to remember all these constructors (it is impossible), but it is useful
to have an idea about the main groups of constructors.
Any constructor of this class has to describe (directly or with default values) two main things: a cover around the control
and the position and view of the comment. The cover around the control is of the same type that was demonstrated for the
solitary controls in the previous chapter. There are not too many variants. You can specify the sizes of nodes and frame
and you can declare the resizing which gives not more choices than the one calculated from the sizes of a control and its two
properties MinimumSize and MaximumSize.
The majority of constructors are the variations on comment position and view. All constructors can be divided into several
groups depending on the set of parameters they use to describe the initial position of comment.
• Positioning coefficients in relation to the area of a control.
• Location point.
• Side of the control and additional space between the control and its comment.
• Side of the control and additional alignment (three variants) along this side.
• Side of the control and additional positioning coefficient along this side.
Variants in all these groups include the exact declaration of the comment font, color, and angle, or using the default values
in different combinations. The Form_CommentedControls.cs (figure 14.3) contains only four CommentedControl
objects, so I have a chance to demonstrate only few constructors. (Pay attention that the code below does not produce
comments as seen at figure 14.3 as after the initialization some of those comments were moved and their view was changed
via the commands of context menu.)
private void OnLoad (object sender, EventArgs e)
{
… …
ccName = new CommentedControl (this, textboxName, Side .E, "Name");
An object at the top uses nearly the minimum allowed set of parameters as three of them – form, control, and text of
comment – must be present in any constructor of this class. One more parameter specifies the side of the control where the
comment must be placed.
World of Movable Objects 399 (978) Chapter 14 Control + graphical text
ccSurname = new CommentedControl (this, textboxSurname, Resizing .WE,
Side .E, 4, "Surname");
The second from top object is designed around a control with identical sizes and parameters as the first one, but its resizing
is restricted to horizontal only, so the height of this control cannot be changed while the first one is easily reduced.
ccList = new CommentedControl (this, listView1, Side .N,
SideAlignment .Left, 3, "With ListView",
new Font ("Times New Roman", 10, FontStyle .Bold),
Color .Violet);
This is a constructor for the pair on the left. For comment positioning, the side of the control is specified and the comment
alignment along this side. There are also font and color which this comment has to have from the very beginning.
ccBtn = new CommentedControl (this, btnResizeAny, Side .E,
SideAlignment .Center, 4, "With Button",
new Font ("Times New Roman", 12, FontStyle .Bold | FontStyle .Italic),
30, Color .Blue);
Pair with the button uses nearly the same constructor, but in addition the comment angle is specified.
CommentedControl objects are registered in the mover queue by their IntoMover() method.
private void RenewMover ()
{
mover .Clear ();
ccBtn .IntoMover (mover, 0);
ccList .IntoMover (mover, 0);
ccSurname .IntoMover (mover, 0);
ccName .IntoMover (mover, 0);
… …
There are properties in the CommentedControl class which allow to change the visibility parameters of both control
and comment. For comment it is color and font; for control it is background color and font. Usually the change is
organized via the commands of context menus with each class having its own menu. Pay attention that when you press near
the border of some control from our pair “control + comment”, then you press the CommentedControl object, but if you
press on the comment of the same pair, then it is CommentToRect object. This is absolutely different from the case of
control with limited comment positioning.
In the Form_CommentedControls.cs, there is only one context menu which allows to change the font and color of the
pressed comment.
The color change is simple.
private void Click_miColor_Comment (object sender, EventArgs e)
{
ColorDialog dlg = new ColorDialog ();
dlg .Color = cmntPressed .Color;
if (dlg .ShowDialog () == DialogResult .OK)
{
cmntPressed .Color = dlg .Color;
Invalidate ();
}
}
Looks like the font change must be also simple. However, as you can see from the code below, I even included the warning
against using the simple and most obvious method of changing the font.
private void Click_miFont_Comment (object sender, EventArgs e)
{
FontDialog dlg = new FontDialog ();
dlg .Font = cmntPressed .Font;
if (dlg .ShowDialog () == DialogResult .OK)
{
// NOT cmntPressed .Font = dlg .Font; !!!
long idCmntCtrl = cmntPressed .ParentID;
World of Movable Objects 400 (978) Chapter 14 Control + graphical text
GraphicalObject grobj;
for (int i = mover .Count - 1; i >= 0; i--)
{
grobj = mover [i] .Source;
if (grobj is CommentedControl && grobj .ID == idCmntCtrl)
{
CommentedControl cc = grobj as CommentedControl;
cc .CommentFont = dlg .Font;
Invalidate ();
break;
}
}
}
}
I included the warning against changing font of the pressed comment with the CommentToRect.Font property; next
series of figures illustrates the cause of this warning. For this experiment I decided to use the CommentedControl
object based on the ListView; at figure 14.3 it is shown on the left.
Fig.14.4a Set the big comment font Fig.14.4b Place the comment so that Fig.14.4c Sharply decrease the font
(size 18) only small part of it is visible size
• First increase the comment font. This is needed because later I will need to decrease it. At the moment of
initialization this comment used the default font of the form and its size was 8, so I increase it to 18 (figure 14.4a).
• Now move this comment and release it when bigger part of it is closed by the ListView control and only the
small part is visible (figure 14.4b). (If this visible part will be too small, you will have no chances to leave the
comment in such position. Visible part of comment must be one or two pixels outside the invisible cover around
the control and the width of that frame is six pixels.)
• Call menu on the partly visible comment; you will have to click at the very edge of comment because its part closer
to the control, though it is visible, is blocked from mover by the invisible cover around this control. Through the
command of the opened menu sharply decrease the font; for example, select size 8.
Comment is positioned by its central point and when the font size is decreased, this comment shrinks to its central point. If
the comment from figure 14.4b shrinks, then it simply disappears under the control. Consequences depend on the code of
the Click_miFont_Comment() method.
If you use the easiest variant and change the font by the CommentToRect.Font property, then it is a disaster because
there is no way to return the hidden comment into view. It is hidden, so you cannot move it directly. When you try to move
the ListView, its associated comment moves synchronously and never looks out; the same happens when you resize the
ListView control. Theoretically there is a chance that you can squeeze the ListView to such tiny size that part of the
hidden comment will come out and you will have a chance to pull out the comment from its hiding place. But the
possibility of such happy end depends on the MinimumSize value which the control gets at the design time; if I am not
mistaken, this particular ListView will not give you such chance. Never use CommentToRect.Font property to
change the font of comment inside the CommentedControl object!
There is much better and absolutely reliable way to change the font of such comment. This comment keeps the id of its
parent. Search through the mover queue for CommentedControl object with such id and then use the
CommentedControl.CommentFont property. After changing the comment font, it will check the possibility of
comment being overlapped by the associated control; if it happens, then the comment is moved outside the control area
(figure 14.4c).
For the use of CommentedControl objects inside the Form_CommentedControls.cs such enforced relocation of
comment from underneath the associated control is enough. When CommentedControl objects are used inside some
World of Movable Objects 401 (978) Chapter 14 Control + graphical text
groups, further actions might be needed; we’ll return to such case further on in the chapter Groups in the example
Form_PersonalData.cs.
There are several situations when the forced relocation of comment in the CommentedControl object can be required.
The most obvious one is when you move comment and release it when it is totally covered by the associated control. Pay
attention that if it happens with some other control which is not united with this comment into the CommentedControl
object, then no action is needed and there is no forced relocation because you can easily move another control aside and the
comment will become visible and movable. If the comment is released under the associated control, then mover itself deals
with this situation and pushes the comment from under this control. Mover can take care of the situation when a comment
is moved and released, but there is one more case in which mover cannot help.
When you decide to change the font of a control, you use the CommentedControl.ControlFont property. It is
possible to get the control itself through the Control property, so you can change the same font through
Control.Font property, but do not do it! Though in many situations it can result in not more than not the best change
of the whole object, in some situations it can cause a disaster: the comment can disappear from view without any chance to
be seen again. It is nearly the same situation which was described on the previous page.
Suppose that you have situation from figure 14.4b and decide to change the control font. I have already explained in the
previous chapter that, unfortunately, some of the controls change their size according to the size of a font; if you increase
the font of a control, then the size of a control also increases. If you change the font through the
CommentedControl.ControlFont property, the comment is informed about the new area of the control and
changes its position so that it is seen in exactly the same way. If the change is done with the Control.Font property,
then the size of a control is changed, but there is no warning for the comment to adjust its position. Control itself does not
know that it is used as some part of a CommentedControl object, so there is no message to comment. Comment stays
at the same place, but the increased control entirely closes it from view. After it, there is no way to return the comment
back into view. Any move of the control results in synchronous move of the associated comment. If you find the way to
overlap the comment entirely with its dominant control, then there is no way to see this comment again. It is gone.
The situation with the possibility of comment disappearance is so special that I will write about it in the discussion of the
rules of user-driven applications in the second part of this book. At the moment I want to attract attention to the fact that
there were no such problems when we dealt only with graphical objects regardless of their shape, size, and number.
Groups
Individual controls and controls with an additional comment were discussed in the previous chapters. Such
objects are widely used for design of different forms but more often the design is based on bigger blocks
which consist of several controls or combinations of controls and graphical objects. These blocks can vary
from a simple pair of two elements to very complicated hierarchical groups of elements. The wide variety of
groups is discussed in this chapter.
All currently used applications are designed of the fixed elements; these can be either individual controls or some unions of
controls on the basis of a GroupBox or Panel. A list of popular controls is known for many years and includes around
25 – 30 different types. 10 – 12 of them are used very often; others you can see only occasionally in different applications.
Only one or two new controls were introduced throughout the last five - eight years; all other controls were developed 20 or
more years ago. They were put into life together with the rules of their design and use; the good solid books were published
about the rules of using those controls in applications; you rarely see now an application which risks to ignore those rules.
This situation can be looked at as a big advantage because everyone knows how to use these familiar elements. On the
other hand, the system of strict rules in design prevents any progress and can become a cause of stagnation in design of big
complex applications.
The design of applications on the basis of movable / resizable objects blows up this sluggish system and opens an infinitive
area of possibilities. The discussion of these new possibilities will be the subject of the second part of this book, but before
starting that discussion I want to show the variants of new elements that can be used in new design. Some of the elements
which are demonstrated in this chapter are widely used in design of user-driven applications. Others were designed
somewhere throughout a research work in response to new ideas but later they were replaced with the better versions. But
better means only my point of view at the current moment. From time to time I return back to the ideas which I used
several years ago but abandoned later. Those older ideas are not the dead ends in design; they can be used for development
of new elements in the future, so I would like to mention them here in parallel with those elements which are at high
demand now.
While writing about movability of controls in the chapter Individual controls, I purposely excluded two classes from the list
of the most popular controls. Objects of the GroupBox and Panel classes can be used as containers for sets of other
controls. In this way groups of elements are organized with the help of Visual Studio and I was using such groups
(certainly, with fixed elements inside) for many years. When I started to turn all elements in my applications into movable /
resizable, I had to do the same with the existing groups, so all pluses and minuses of turning GroupBox and Panel
objects into movable / resizable quickly became obvious. Let us look into their details because those minuses pushed my
work on and eventually brought me to invention of more powerful groups.
Ordinary panels
File: Form_OrdinaryPanels.cs
Menu position: Groups – Ordinary panels
In a standard design based on fixed
elements there are two ways to
organize a set of controls into a
group. The first way is to put
controls on a panel. The border of
such panel, if shown, makes it
obvious which of the controls are
included into a group.
Form_OrdinaryPanels.cs
demonstrates three movable panels;
two of them are also resizable
(figure 15.1). As any other control,
a panel can be moved / resized only
by its border. There is absolutely
nothing special about the moving
and resizing of panels; the whole
procedure is organized in the
standard way for all other controls. Fig.15.1 Panels are moved and resized as all other solitary controls
World of Movable Objects 403 (978) Chapter 15 Groups
Ordinary GroupBox
File: Form_OrdinaryGroupBox.cs
Menu position: Groups – Ordinary GroupBox
GroupBox objects are as easy to include into moving / resizing process as panels and other controls. This case has the
same negative feature as the case with panels: you cannot move a GroupBox by its inner points. But in addition an
ordinary GroupBox, when turned into movable, has one more negative feature of its own. Figure 15.2 demonstrates the
view of the Form_OrdinaryGroupBox.cs in which two GroupBox objects are organized: one is non-resizable, another is
resizable. The picture shows the covers of these two groups and makes the explanation of the problem easier.
GroupBox is a standard control and has the
same cover as any other control. A
GroupBox has a frame which is painted inside
its area. The main idea of this frame is to make
it obvious, which controls are included into the
group; for this purpose the frame works
perfectly. It is a very rare situation when the
background color of the GroupBox differs
from the background color of the form in which
it is used. When these two colors are the same
(and this happens nearly always), you cannot
see the real border of a GroupBox. The frame
is perfectly seen, but there is some distance
between the frame and the invisible border. In
the standard design based on fixed elements
there is no difference for users whether the
frame is exactly on the border or not; the frame
only informs about the filling of the group and
for this purpose the possible shift of the frame Fig.15.2 Ordinary GroupBox objects are moved and resized as all
for several pixels aside from the real border other controls controls
does not matter at all.
When you turn a GroupBox object into movable / resizable, it gets the cover next to its real borders. Visualized cover at
figure 15.2 perfectly demonstrates that on three sides the real border is close enough to the visible group frame while on the
fourth side – at the top – the distance between them is bigger and on that side the frame line is definitely not near real
border. Usually the covers are not shown at all while the frame is perfectly seen, so users try to grab a GroupBox for
moving or resizing somewhere next to the visible frame. On three sides where the cover is close to the visible frame anyone
can grab such group “by the frame”, but at the top such attempt fails. If you try to grab the group with the invisible cover
by its frame at the top, it would be impossible. You will find that you can grab such group if the cursor is moved
somewhere up where there is no indication of such possibility (except for the change of mouse cursor). This inability to
grab the movable object at the place which you think is absolutely right for it becomes very annoying; especially, if there
are several such objects in the form. That was one of the reasons why I quickly stopped using ordinary GroupBox objects
among movable elements and started to design other types of groups.
World of Movable Objects 406 (978) Chapter 15 Groups
Non-resizable groups
File: Form_RigidlyBoundRectangles.cs
Menu position: Groups – Non-resizable groups
From time to time it happens that you need to use a non-resizable group. Not too often but occasionally there is such a
requirement. You see it rarely enough (or ever at all) in the really complex forms, but in the relatively simple examples I
used such elements from time to time. A non-resizable group of elements is represented in the MoveGraphLibrary.dll by
the RigidlyBoundRectangles class. In the previous versions of this book there were around a dozen of examples
which used this class. While rewriting the whole book in 2014, I changed some of these elements to ElasticGroup
objects and in the new version you could see the identical RigidlyBoundRectangles objects only in the
Form_Rectangles_Standard.cs (figure 3.1) and Form_Rectangles_StandardToDisappear.cs (figure 3.2). These
examples are used at the very beginning of the text before introducing more flexible groups. There are few
RigidlyBoundRectangles objects in the forms which are not discussed yet and which you will see later, but as I
already mentioned, elements of this class are rarely used. Nevertheless, let us look into details of their design.
Suppose that you have a set of elements which need a synchronous move and nothing else. No resizing of elements and no
change of their relative positions. At the same time their initial relative positions can be arbitrary: elements can stay next to
each other, they can overlap, or they can be placed far away from each other. Their positions do not matter at all; whenever
any element of the set is moved, all others have to move synchronously. In a lot of situations the objects which require such
a synchronous move are controls; often enough they are accompanied by some comments. The design of the
RigidlyBoundRectangles class was caused by a request for synchronous move of several controls; later rectangles
were added as possible elements of this class. All rectangles that constitute a group move only synchronously and never
change their relative positions; that is where the name of the RigidlyBoundRectangles class came from.
public class RigidlyBoundRectangles : GraphicalObject
{
List<Control> m_controls = new List<Control> ();
List<TaggedRect> m_rects = new List<TaggedRect> ();
Delegate_Draw m_draw = null;
int m_framewidth = 6;
There are two Lists in this class: one for controls and another for rectangles of special class; at least one of these
Lists must be not empty.
The RigidlyBoundRectangles class has several constructors which define different initial sets of elements. It can
be a single control, a single rectangle, or an array of controls or rectangles. Later you can add any combination of controls
and rectangles to existing objects, so there are a lot of possibilities. When control is included into the
RigidlyBoundRectangles object, then the control itself is included into one List and the rectangular area around
its borders is included into another; this rectangular area is wider than the control area by six pixels on each side.
The second List contains not ordinary rectangles but rectangles with tags; this makes easier the whole work with
RigidlyBoundRectangles objects because any rectangle can be found by its tag.
public class TaggedRect
{
RectangleF rc;
string tag;
Well, tag can help only if it exists. If you add some rectangle with a tag, then this tag can be used, for example, for search
of this rectangle. If the tag was not specified at the moment of initialization, then it is null. You can give the same tag to
different rectangles, but I do not think that this is a good idea.
Cover for the RigidlyBoundRectangles objects is simple. Each rectangular area is covered by a single rectangular
node; the cover consists of a set of these nodes.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [m_rects .Count];
for (int i = 0; i < nodes .Length; i++)
{
nodes [i] = new CoverNode (i, m_rects [i] .Rectangle);
}
World of Movable Objects 407 (978) Chapter 15 Groups
cover = new Cover (nodes);
}
Moving of the whole object means the synchronous move of all rectangles and controls.
public override void Move (int dx, int dy)
{
for (int i = 0; i < m_controls .Count; i++)
{
m_controls [i] .Left += dx;
m_controls [i] .Top += dy;
}
for (int i = 0; i < m_rects .Count; i++)
{
m_rects [i] .Move (dx, dy);
}
}
The MoveNode() method for this class is extremely simple: regardless of the pressed node, the MoveNode() method
calls the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
Move (dx, dy);
}
return (false);
}
Some of the RigidlyBoundRectangles objects look extremely simple; others may contain a set of controls and
additional rectangles with interesting enough drawing, so they look not so simple. For mover all these objects are simple.
Each cover consists of a set of
rectangular nodes; this set is defined
at the moment of initialization and
is never changed. All
RigidlyBoundRectangles
objects are registered by the
Mover.Add() or
Mover.Insert() methods.
Three remarks before we start
looking at the example with the
RigidlyBoundRectangles
objects.
• There is no resizing of
controls which are used
inside such objects. You
can set the appropriate
values with
MinimumSize and
MaximumSize properties
and expect that it will help
to resize the controls Fig.15.3 Objects of the RigidlyBoundRectangles class
because it works in the
SolitaryControl and CommentedControl classes. You are going to be disappointed: there is no resizing
for elements inside the RigidlyBoundRectangles class; there is only forward moving.
• When any object consists of several (or many) parts and this object is movable, then it is natural to expect that this
object can be moved by any inner point. I prefer to organize an object moving in such way because it is natural
and expected by users. By default, there is no such thing in the RigidlyBoundRectangles class; an object
World of Movable Objects 408 (978) Chapter 15 Groups
is moved only by rectangles of which it exists. For example, you can declare that your object consists of all black
squares of the chess board; such object will be moved by any black square, but pressing the white squares will be
ignored. At the same time you may want your object to be movable by any inner point of the area regardless of the
positions of all the rectangles. In such case, use the RigidlyBoundRectangles.AddUnionRectangle()
method to add one more rectangle. This rectangle gets the "AutoUnion" tag and covers the united area of all
included rectangles, so there will be no gaps inside and such object will be moved by any inner point.
• Usually some drawing of the RigidlyBoundRectangles object is needed. Depending on the amount of
needed drawing, the code can be included into the OnPaint() method of the form or a special method can be
prepared and linked with the object. There is no direct correlation between the set of rectangles to move an object
and an area in which you draw something. In many cases you draw inside the area which you move but this is not
a mandatory rule.
Now we can look at the RigidlyBoundRectangles objects which are demonstrated in the
Form_RigidlyBoundRectangles.cs (figure 15.3). The RigidlyBoundRectangles class was originally invented for
moving of several controls, so all six objects in the current example use one or several controls.*
*
RigidlyBoundRectangles object without a single control can be seen, for example, in the Form_About.cs.
World of Movable Objects 409 (978) Chapter 15 Groups
gaps inside. There is no need to calculate the big rectangle manually; the RigidlyBoundRectangles class can do it
by using its own AddUnionRectangle() method.
private void OnLoad (object sender, EventArgs e)
{
… …
// rectsView
int cxL = numericUD_NodeSize .Left;
numericUD_Frame .Location =
new Point (cxL, numericUD_NodeSize .Bottom + spaces .VerMin);
checkShowPrompts .Location =
new Point (cxL, numericUD_Frame .Bottom + spaces .Ver_betweenFrames);
SizeF [] sizeStrs = Auxi_Geometry .MeasureStrings (this, strs);
rectsView = new RigidlyBoundRectangles (new Control [] {
numericUD_NodeSize, numericUD_Frame, checkShowPrompts });
rectsView .Add (Auxi_Geometry .RectangleToRectangleSide (
numericUD_NodeSize .Bounds, Side .E, sizeStrs [0], 4), "Node");
rectsView .Add (Auxi_Geometry .RectangleToRectangleSide (
numericUD_Frame .Bounds, Side .E, sizeStrs [1], 4), "Frame");
rectsView .Add (Auxi_Geometry .RectangleToRectangleSide (
checkShowPrompts .Bounds, Side .E, sizefStrs [2], 4), "Prompts");
rectsView .AddUnionRectangle ();
rectsView .DrawMethod = Draw_rectsView;
… …
The same set of controls is united into a group in the Form_SolitaryControls.cs (figure 13.1). Only in that example, it is
designed as an ElasticGroup with a frame, with easily movable inner elements, and standard group tuning. (The code
for ElasticGroup variant is simpler while its use is definitely better; that was my reason to switch in many examples
from RigidlyBoundRectangle objects to ElasticGroup objects.).
3. Several controls with and without comments. This group has a frame (figure 15.6).
If control has an associated comment, then the area for this comment is calculated and this
rectangular area is included into the List of rectangles. If there is a control without
comment, then it is added to the List of controls, but the rectangle of its sensitive invisible
frame is automatically included into the List of rectangles. It is a good idea to have a
frame around group, but this needs some extra calculations because there is no standard
frame and its area depends on the way it will be painted.
Fig.15.6 Controls,
private void OnLoad (object sender, EventArgs e)
rectangles for comments,
{
and rectangle for a frame.
… …
// rectsNew
cxL = numericUD_Balls .Left;
checkSameSize .Location =
new Point (cxL, numericUD_Balls .Bottom + spaces .Ver_betweenFrames);
buttonAdd .Location =
new Point (cxL, checkSameSize .Bottom + spaces .Ver_betweenFrames);
rectsNew = new RigidlyBoundRectangles (new Control [] { numericUD_Balls,
checkSameSize, buttonAdd });
cxL = numericUD_Balls .Left - spaces .Left_inFrame;
int cyT = numericUD_Balls .Top - spaces .Top_inFrame;
int cxR = buttonAdd .Right + spaces .Right_inFrame;
int cyB = buttonAdd .Bottom + spaces .Btm_inFrame;
Rectangle rcArea = Rectangle .FromLTRB (cxL, cyT, cxR, cyB);
rcArea .Inflate (3, 3);
rectsNew .Add (rcArea, "Area");
rectsNew .DrawMethod = Draw_rectsNew;
… …
There are some methods in the MoveGraphLibrary.dll which help to draw frames with and without titles; you can use one
of these methods or any of your own.
World of Movable Objects 410 (978) Chapter 15 Groups
private void Draw_rectsNew (Graphics grfx)
{
RectangleF rc = rectsNew .Rectangle ("Area");
rc .Inflate (-3, -3);
Auxi_Drawing .CurvedFrameWithTitle (grfx, rc,
Auxi_Colours .DefaultFramePen (this),
StringAlignment .Near, 4, strsNew [0],
Font, SystemColors .Highlight);
Auxi_Drawing .TextToControl (grfx, numericUD_Balls, Side .E, 4,
strsNew [1], Font, ForeColor);
Auxi_Drawing .TextToControl (grfx, checkSameSize, Side .E, 4,
strsNew [2], Font, ForeColor);
}
Group with the same view as shown at figure 15.6 can be seen in the Form_BallsInRectangles.cs (figure 11.13). Only
there it is developed as an ElasticGroup object and that variant is definitely better than the current one.
4. Several controls plus rectangle for drawing. This group has no frame.
Three previous examples of the RigidlyBoundRectangles objects are shown in the upper
part of figure 15.3. All three have no individual graphical objects; they use graphical texts but
only as comments associated with controls. Three other objects of the same class which are
shown in the lower part of figure 15.3 use individual graphical objects. One of them is used
without frame (figure 15.7).
There are three controls in this group and initially this RigidlyBoundRectangles object is
constructed on these controls. Then two rectangles are added. One of them is for title; another is
for drawing the needed graphical element which is selected in the combo box. This second
rectangle starts from the upper border of two small buttons and covers the area up to the combo Fig.15.7 Group has
box. Thus, the whole inner area is also covered by one of rectangles and the group can be moved several controls, a
by any inner point. rectangle for title, and
private void OnLoad (object sender, EventArgs e) another rectangle for
{ drawing.
… …
// rectsNoFrame
btnBorderClr .Location =
new Point (comboShape .Right - btnBorderClr .Width, btnInsideClr .Top);
rectsNoFrame = new RigidlyBoundRectangles (
new Control [] { btnInsideClr, btnBorderClr, comboShape });
rectsNoFrame .Add (new Rectangle (btnInsideClr .Left, btnInsideClr .Top,
btnBorderClr .Right - btnInsideClr .Left,
comboShape .Top - btnInsideClr .Top - 3),
"Picture");
rectsNoFrame .Add (new Rectangle (btnInsideClr .Left - 20,
btnInsideClr .Top - sizeTitleUnframed .Height,
sizeTitleUnframed .Width, sizeTitleUnframed .Height),
"Title");
comboShape .SelectedIndex = (int) shape;
rectsNoFrame .DrawMethod = Draw_rectsNoFrame;
… …
5. Several controls with and without comments plus rectangle for drawing. Group has a frame.
This variant is demonstrated by two groups (figure 15.8) which are initialized in similar way. A group is organized on a set
of controls; then big rectangle for a frame is calculated and added to the group. Areas for comments and for special drawing
are inside this big rectangle, so their possible addition to the group does not change the full area of an object. If it would be
easier for drawing, you can specify some inner rectangle with a name and then draw inside this named rectangle or you can
do without such naming.
This is how the left group is organized.
World of Movable Objects 411 (978) Chapter 15 Groups
private void OnLoad (object sender, EventArgs e)
{
… …
Control [] cntrls = new Control [] { comboUnicoloured, numericUD_Vertices,
btnUniColor, btnAddUnicoloured };
rectsUnicoloured = new RigidlyBoundRectangles (cntrls);
Rectangle rcFrame = Auxi_Geometry .FrameAroundControls (cntrls, spaces);
rcFrame .Inflate (3, 3);
rectsUnicoloured .Add (rcFrame, "Area");
rectsUnicoloured .DrawMethod = Draw_rectsUnicoloured;
… …
The RigidlyBoundRectangles objects work perfectly if
you need a movable but non-resizable group of elements. The
resizable groups are needed much more often, so I spent more
time on developing such groups. At the beginning, they were still
using the ideas of dynamic layout; next section demonstrates two
classes of such groups.
currently used font. The Space class provides me with the same values for all examples in the applications, so all the
forms in the WorldOfMoveableObjects application use the same default distances between the frames, the distances
between the frames and inner elements, etc.
Here is some code to show how the resizing works with the GroupBoxMR object in the
Form_GroupsWithDynamicLayout.cs. There are four controls in the group (figure 15.10). I made the decision about the
minimum size of the group (minW, minH) and also decided that the width of this group can be increased not more than
three times and the height – not more than four times from the minimum sizes.
private void OnLoad (object sender, EventArgs e)
{
… …
// groupboxMR
int minW = spaces .Left_inFrame + btnCommentClr .Width +
btnCommentFont .Width + 2 * spaces .HorMin + btnAdd .Width +
spaces .Right_inFrame;
int minH = spaces .Top_inFrame + 2 * btnAdd .Height + spaces .VerMin +
spaces .Btm_inFrame;
Rectangle rcFrame = Auxi_Geometry .FrameAroundControls (new Control []
{textNewComment, btnCommentClr, btnCommentFont, btnAdd }, spaces);
groupboxMR = new GroupBoxMR (this, rcFrame,
new RectRange (minW, 3 * minW, minH, 4 * minH),
"Resizable GroupBoxMR", MoveElements_GroupBoxMR);
… …
The GroupBoxMR class has Move() and MoveNode() methods but they are only responsible for correct change of
frame and title. Group does not know anything about the inner elements and thus group cannot change sizes and positions
of inner elements in response to the frame change. Such changes are done by the method which is passed as a parameter
during initialization; in the current example this is the MoveElements_GroupBoxMR() method. The demonstrated
group is simple, so my ideas about the positions and sizes of elements are also simple.
• Everything is calculated from the frame. Distance between frame and inner elements is determined by the Space
object.
• Two small buttons are always in the bottom left corner of the group with a fixed space between them.
• The third button is in the bottom right corner of the group.
• The TextBox control occupies the remaining part of the inner area. This is the only inner element which changes
its sizes according to the group change.
void MoveElements_GroupBoxMR ()
{
Rectangle rc = groupboxMR .RectAround;
btnCommentClr .Location = new Point (rc .Left + spaces .Left_inFrame,
rc .Bottom - (spaces .Btm_inFrame + btnCommentClr .Height));
btnCommentFont .Location = new Point (btnCommentClr.Right + spaces .HorMin,
btnCommentClr .Top);
btnAdd .Location = new Point (rc .Right - (spaces .Right_inFrame +
btnAdd .Width), btnCommentClr .Top);
textNewComment .Bounds = Rectangle .FromLTRB (btnCommentClr .Left,
rc .Top + Auxi_Geometry .RoundMeasureString (this, "Title",
groupboxMR .TitleFont) .Height / 2 + spaces .VerMin,
rc .Right - spaces .Right_inFrame,
btnCommentClr .Top - spaces .VerMin);
}
After using the GroupBoxMR class for some time, I understood that there was no reason to use the border (frame) of such
group both for moving and resizing. The group can be moved by any inner point, so there is no need to do the same by
some parts of the frame. The whole frame can be used for resizing only and this will be according to the idea of moving
and resizing for all the objects: move by inner points, resize by border points. The only question of such design was in
visualization: I needed to show in some way the difference between four variants of resizing, so I decided to use the dashed
parts of the frame in the middle of those sides which can stretch and squeeze. The only exception is again the upper line: if
World of Movable Objects 415 (978) Chapter 15 Groups
a group has a title, then the dashed segment is not included into the upper line of a frame (right group at figure 15.9). Thus
designed class is called Group.
public Group (Form form, // form in which the group is organized
Rectangle rcFrame, // frame rectangle
RectRange ranges, // ranges of possible resizing
Pen pen, // frame pen
string str, // title
StringAlignment align, // title alignment
int space, // space between title and line on its sides
Font fnt, // title font
Color clrStr, // title color
Delegate_NoParams onMoveResize) // method to change the size and position of inner elements
// during the moving / resizing of the group
There is not a big difference between the constructors of two classes. There are two additional Boolean parameters in the
GroupBoxMR constructor which are absent in the Group constructor and this is easy to understand.
• One of them gives a choice for the GroupBoxMR object either to be movable by inner points or not. For the
Group class, this is the only way to move the group, so this parameter is redundant.
• Another parameter allows to make a decision about showing special curves in the frame of the GroupBoxMR
object. In the Group class, parts of the frame are always shown as dashed line if the appropriate side can change
its size. Thus, this Boolean parameter is not needed and it is gone.
Figure 15.11 shows the cover for the Group object. There is nothing new or special
in this cover; it is a classical cover for rectangle which can be resized by any border
point and moved by any inner point.
I purposely included similar objects of the GroupBoxMR and Group classes into
the same form and filled these groups with the identical elements; this makes more
obvious the similarities and discrepancies between two classes. Both groups can be
moved by any inner point and resized by the borders. In both cases the resizing is
done “by the frame”, so there are no problems with it. A GroupBoxMR object can Fig.15.11 Group object and its
be resized only by several special places of the frame; a Group object – by any cover
point of the frame and this is definitely better.
In the Form_GroupsWithDynamicLayout.cs both classes – GroupBoxMR and Group – are represented by the objects
with only few elements inside, but these classes can be used in much more complex cases. Three or four years ago the most
complex tuning forms for different plotting classes from the MoveGraphLibrary.dll library were designed on the basis of
the Group class; I had no problems in using especially this class in the most complex cases. The group itself can include
a lot of inner elements, but for the mover it is a simple object, because there are no individual movements of inner elements.
All movements are described by the method passed as a parameter on initialization; registering of the groups of both classes
is simple.
mover .Add (group);
mover .Add (groupboxMR);
After the Group class was designed, it became for some time the main class to develop the groups in all my applications.
The Group class is still used in some of the tuning forms from the mentioned library, but the time of this class is over.
Those several objects exist only until the moment when I find the time to redesign those several forms from the library. An
amazing turn in using the class which not long ago looked like the best class for the resizable groups. What is the reason?
Dynamic layout is not used in my applications any more.
The second part of the book is about the user-driven applications. In this type of applications, there is no place for dynamic
layout. Whether you like it or not, but the dynamic layout means the work of application according to the designer’s view
on what is good and what is bad. But I think that only USERS have to decide what and how must be shown at the screen.
When a Group object is resized, its inner area is rearranged according to the developer’s view and taste. All changes are
predetermined in that method which was written by developer and passed as a parameter on the group initialization. Users
have no chances to make their own changes. If I decided that two small buttons had to stay in the bottom left corner of the
group next to each other and another button had to stay in the bottom right corner, then this decision is final and cannot be
changed by users. They can move and resize the group, but they cannot overrule my decision about the inner view of the
World of Movable Objects 416 (978) Chapter 15 Groups
group. In the user-driven applications this is absolutely wrong! Users must have the full control of the whole view and over
any detail. The groups for user-driven applications must be based on different ideas. When I thought out how to design
such groups, it was the final step in design of real user-driven applications.
The class of groups which fits perfectly with the ideas of user-driven applications and on which I base all my applications
now is called ElasticGroup. Very simple groups of this class already appeared in a couple of previous examples, but I
did my best not to use it before the detailed discussion. It is nearly time to look more carefully at the ElasticGroup
class, but before this we need to look at one more interesting type of relations between the elements of some group. This is
the case when one element plays the dominant role while all others are subordinates.
World of Movable Objects 417 (978) Chapter 15 Groups
*
The DominantControl class is the first in this book to demonstrate the design on the basis of dominant – subordinates
relation. However, it is not the only class of such type in the MoveGraphLibrary.dll; others and more powerful classes
with dominant – subordinates relation of elements are included into the library and are discussed further on in this chapter.
For example, the Dominant_SolitaryControl class entirely covers the use of the DominantControl class and
gives wider possibilities, but I think that for the introduction of such groups the DominantControl class is the best.
World of Movable Objects 418 (978) Chapter 15 Groups
An object of this class represents a dominant control with an access to a set of subordinates; all those subordinates belong to
the SubordinateControl class. Any subordinate control can be turned into the new dominant element; one of
further examples demonstrates it.
DominantControl is a complex object. While writing about complex objects, I prefer to say several words about
minor elements and only after it to write about the behaviour of the main object; I think that such order of explanation is
better for understanding of details.
In many aspects SubordinateControl is similar to SolitaryControl class. They have similar constructors; they
have identical covers, and, as a result, their individual moving and resizing are identical. This is absolutely expectable
because both classes are designed around the same controls. The difference is in the additional role of
SubordinateControl objects: each of them plays the subordinate role for some other control and for this it has to have
some link to the main element and coefficients to describe its relative position to the dominant element. The dominant
element is always a control which has a rectangular area, so these coefficients and their use are identical to what was
explained about comments to rectangle. The only difference with the CommentToRect class is in the base point: all
rotatable comments are positioned by the central point while any control is
positioned by its top left corner. Other details of SubordinateControl
class are better seen from the analysis of objects in the current example.
There are three different objects of the DominantControl class in the
Form_DominantControls.cs (figure 15.12). In two of these groups the
dominant control is obvious as it is the biggest control in a set. Two of three
groups have no frames, but I think that even without them the sets of related
controls make a clear-cut distinction between the groups. The most common
case of using a DominantControl, though it is not a mandatory rule, is the
situation with one big and several much smaller controls working as its
satellites. This case is shown at figure 15.13; this is the best case to start the
explanation.
The easiest way to construct a DominantControl object is to organize an
Fig.15.13 Group with dominant control
array of controls starting with the one which is going to be dominant.
ctrlsNumbered = new Control [] { listviewDom,
button1, button2, button3, button4 };
private void OnLoad (object sender, EventArgs e)
{
… …
dominWithNumbers = new DominantControl (ctrlsNumbered);
… …
Regardless of whether a control plays the role of dominant or subordinate element, its cover is the same as was shown for
SolitaryControl objects. If you visualize the covers in this example, then small nonresizable buttons with numbers 1,
2, and 3 will show only a narrow sensitive frame around their borders, while button 4 and big ListView control will
have also small nodes near the corners and in the middle of the sides.
The division on dominant and subordinate controls describes the difference in their individual and related movements.
• When dominant control is moved, all its subordinates move synchronously.
• When dominant control is resized, each of subordinates tries to retain its relative position to the dominant control.
An object of the SubordinateControl class has two coefficients that describe the position of its top left
corner in relation to the rectangle occupied by the dominant control.
• A subordinate control can be moved and resized individually and positioned anywhere in relation to the dominant
control. There is only one forbidden situation: if subordinate control is moved (or resized) and released at such a
place that it is fully inside the area of its dominant control (above or below – this depends on the Z-order of
controls), then this subordinate control is forcedly relocated and placed outside the area of dominant control. This
rule is longer to read and understand than to try; move any of the four buttons from the shown group to the area of
the ListView control, release it there, and you will see the result.
The exception of the possible arbitrary positioning of subordinate control happens only when the subordinate control is
released in the area of its dominant control. Pay attention to the underlined statement; if one of subordinates is released
World of Movable Objects 419 (978) Chapter 15 Groups
over another or if it is released inside the area of some control which does not belong to its DominantControl object,
then no enforced relocation is going to happen.
The enforced relocation of subordinate might happen in one more case. Consider the situation when a subordinate control is
placed mostly inside the area of the dominant control with only tiny part of it looking outside the right or bottom side of the
dominant control. Because a small part of subordinate control is visible, it can be placed in such a way without causing the
enforced relocation. Then start increasing the dominant control. Positioning coefficients of subordinate control are fixed
and its size is also not changing. While the dominant control is increasing, subordinate control will slightly move, but
because its sizes are fixed, the subordinate control may find itself inside the increased area of dominant element. If
subordinate control is entirely inside the dominant control area at the moment when dominant control is released, then this
subordinate element will be relocated.
Both situations with the possibility of the enforced relocation are checked when an object is released by a mover.
• When subordinate control is released, then the SubordinateControl.CheckLocation() method is
called. This method ignites the relocation if subordinate control is released at the forbidden ground.
• When dominant control is released, then the same relocation can be started by the
DominantControl.CheckSubordinates() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is DominantControl)
{
(grobj as DominantControl) .CheckSubordinates ();
}
else if (grobj is SubordinateControl)
{
(grobj as SubordinateControl) .CheckLocation ();
}
groupOfDominant .Update ();
Invalidate ();
}
… …
By analysing the set of rules for the DominantControl class, you can see that its rules for enforced relocation are
identical to the rules of the CommentedControl class. The difference between these two classes is in the number and
types of subordinates: the CommentedControl class has one painted text as a subordinate, the DominantControl
class has an arbitrary number of subordinates and all of them are controls.
Any control in a group can be made non-resizable or resizable in the same way as was explained for the
SolitaryControl class. Mover determines the type of resizing for particular control by analysing its sizes and the
values of its MinimumSize and MaximumSize properties. If those properties are not changed from their default
values (0, 0) or their values are the same as the size of a control, then this control is non-resizable. If there is a range for one
direction (width or height), then this control is resizable in this direction only; otherwise it is fully resizable.
At what place the forcedly relocated subordinate appears? By default, this place for relocation is next to the top right corner
of the dominant control. The enforced relocation allows to avoid situations with the inaccessible subordinates but it still
leaves the possibility of another strange and not good situation.
Position of the enforced relocation for any subordinate is described by two coefficients which determine the location of the
top left corner of subordinate in relation to the area of its dominant control. It is a common situation when dominant control
has several subordinates; all of them are constructed prior to dominant control and all of them get the same default values of
coefficients for enforced relocation. If in such organized group you grab one of the subordinate buttons and release it in the
area of dominant control and then repeat the same with another button, then both buttons will be relocated to exactly the
same position. If those subordinates have the same size, then the second forcedly relocated button will entirely close the
first one and you will have no idea that it is still at the screen but is closed from view. (You can check this situation with
World of Movable Objects 420 (978) Chapter 15 Groups
the group from figure 15.16.) First, this looks like the disappearance of one of the elements from the working application,
which is not good by itself. Second, the forced relocation to some place which is not determined by user is against the ideas
of user-driven applications. Beginning from version 6.10 of the MoveGraphLibrary.dll, the SubordinateControl
class was slightly changed to give users a control over the place of the forced relocation. This control is organized via the
SubordinateControl.SetForcedCoefficients() method; the use of this method is demonstrated both for
developers and users.
Developers work to produce the best possible view of their applications. As a developer, I would prefer to give all
subordinates of some group different coordinates for their enforced relocation; this will eliminate the problem of entire
overlapping of subordinates after enforced relocation. Suppose that as a developer I prefer the view of the group that is
shown at figure 15.13. Then I need to store the positioning coefficients for subordinate elements from this view as their
coefficients for enforced relocation; this is done immediately after initializing the group by using the
DominantControl.SaveSubordinatePositionsAsForced() method.
private void OnLoad (object sender, EventArgs e)
{
… …
dominWithNumbers = new DominantControl (ctrlsNumbered);
dominWithNumbers .SaveSubordinatePositionsAsForced ();
… …
If user has no objections to use the positions of subordinates from figure 15.13 as their positions for enforced relocation,
than he does not need to do anything else, but if he prefers other position for such case, then he can move a subordinate to
this preferable position and call a context menu on it. This menu – menuOnNumbered – contains a single command line
to declare the current relative position of the clicked button as its default position for enforced relocation. On clicking this
menu line, the SubordinateControl.SaveCurrentPositionAsForced() method is called.
private void Click_miResetForcedPosition (object sender, EventArgs e)
{
dominWithNumbers.Subordinates [iPressedSub].SaveCurrentPositionAsForced ();
}
The use of menu command for saving the current position of any subordinate control as its position for enforced relocation
works OK, but it is not the best or easiest way to do such a thing. The same thing can be organized automatically on
releasing any subordinate control; it was already demonstrated in one of the previous examples but it was long before the
detailed explanation of the DominantControl class, so at that moment I decided to postpone the explanation. Now is
the right moment to say some words about it.
In the Form_RectanglesWithComments.cs (figure 10.1), it is possible to add new comments to any rectangle by calling a
small auxiliary form. This form – the Form_NewComment.cs (figure 10.3) – looks exactly like a small group with a
frame in the current example. There are exactly the same four controls organized into a DominantControl object.
DominantControl dominGroup;
Control [] ctrls;
ctrls = new Control [] {textNewComment, btnCommentClr, btnCommentFont, btnAdd};
private void OnLoad (object sender, EventArgs e)
{
… …
dominGroup = new DominantControl (ctrls);
… …
}
When a SubordinateControl object is released, then its SaveCurrentPositionAsForced() method is
called; this method includes the call to SubordinateControl.CheckLocation() method. In this way, when a
subordinate control is moved by user to new position, then this new position automatically becomes the default place for
enforced relocation and no context menu is needed. Here is the code of the Form_NewComment.OnMouseUp()
method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (mover .Release ())
World of Movable Objects 421 (978) Chapter 15 Groups
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is DominantControl)
{
(grobj as DominantControl) .CheckSubordinates ();
}
else if (grobj is SubordinateControl)
{
(grobj as SubordinateControl) .SaveCurrentPositionAsForced ();
}
… …
There are two similar DominantControl objects in the Form_DominantControls.cs; these groups consist of one big
dominant control and several smaller buttons – subordinates. I purposely added the explained technique of setting the
positions for enforced relocation to one of the groups (figure 15.13) but not to another (figure 15.16). In this way it will be
easier for any reader to compare the cases and to decide about using this technique in the future.
In the group from figure 15.13, the dominant control is obvious, but this happens not always. There can be situations when
you work with a group of controls and need to change the dominant control from time to time. Look again at the rules of
resizing dominant and subordinate controls and take into consideration the positioning of subordinates. I already mentioned
that subordinates use two positioning coefficients in the same way as comments to rectangles. It is a very rare situation
when controls overlap; I never have overlapping controls in my programs and I do not remember other people using
controls with overlapping. Nearly always controls are positioned at some distance from each other. When you have some
main rectangle and several subordinate elements positioned on some distance from it, then resizing of the main element
does not change the distance between it and subordinates. The same rule works for DominantControl object.
Suppose that there are several controls which do not overlap but are positioned not far away from each other. When the
dominant control is increased, then all its subordinates retain the space from the dominant control; all subordinates are
moved aside, so there will be no overlapping. When any subordinate
control is increased, it is done on an individual basis without paying any
attention to positions of other elements in the group. The probability of
overlapping is high enough.
Suppose that you have several ListView controls, buttons, and
TextBox controls. During the work of an application, you need to
increase one control, then another, but you do not want them to overlap
because they show the important information. The best solution would be
to declare the control to be increased as the dominant control of such
group; then you can change the sizes of any control without overlapping
with others. This case is demonstrated with another
DominantControl object consisting of colored controls
(figure 15.14).
There are nine different controls in this group. The construction of the Fig.15.14 Any member of this group can be
group is done in the same easy way by providing an array of controls. declared a dominant control via the
The first control of this array automatically becomes a dominant control context menu.
of the group; in this case it is a ListView control in the top left corner.
ctrlsColored = new Control [] { listView1, listView2, listView3, panel1,
panel2, panel3, panel4, textBox1, textBox2 };
private void OnLoad (object sender, EventArgs e)
{
… …
dominColored = new DominantControl (ctrlsColored);
… …
At any moment you can declare any control a dominant element of this group by calling the context menu on this element
and using first command from the opened menu (figure 15.15).
World of Movable Objects 422 (978) Chapter 15 Groups
private void Click_miDeclareControlAsDominant (object sender, EventArgs e)
{
bool bShowPrompts = dominColored .ShowPrompts;
DominantControl groupChanged = dominColored .SwitchDominant (ctrlPressed);
if (groupChanged != null)
{
dominColored = groupChanged;
dominColored .ShowPrompts = bShowPrompts;
RenewMover ();
Invalidate ();
}
}
The DominantControl.SwitchDominant(ctrl) method organizes the group with the new dominant control only
if one of the subordinates is pressed and passed as a parameter. If the pressed control is already dominant in the group or if
some wrong control is passed as a parameter (the control which does not belong to this group), then nothing changes.
There is one more command in menu from figure 15.15 – Hide prompts; the small darkened rectangles close to the border
of the pressed control give a tip about this command. If you call the
same menu on any control when it is shown without such small
additions (like you see at figure 15.14), then the same menu line will
declare Show prompts.
I mentioned in several places of this book that neither movability nor
resizability of the screen elements in the real applications is ever
highlighted in any way. It is enough to know that all objects are
movable by inner points and resizable by border. I think that in such Fig.15.15 This menu on controls allows to
situation no visual prompts are needed. However, there can be special change the dominant control and to
situations when some other kind of visual prompt can be helpful. show / hide the prompts around the
Consider the case of the group from figure 15.14, there is no visual dominant control
difference between all those objects, but one of them is dominant and this means the different reaction on moving / resizing
of this particular control and all others. Maybe some users would like to see an indication of the dominant control. I am not
sure whether it is needed or not and I am demonstrating here some kind of visual prompt. It was not designed especially to
mark dominant control, but it can do it by the way.
The DominantControl class includes the possibility of showing small prompts at the places where the frame of the
control can be caught for resizing. Though all the controls can be resized in similar way regardless of whether it is
dominant or subordinate, these prompts are shown only around dominant control, so they can give a tip. Showing of those
small darkened areas is regulated by the DominantControl.ShowPrompts property. On initializing the new
DominantControl object, the false default value is imposed and the prompts are not shown. Especially for this
reason, when the dominant control in the group is going to be changed, then the value of this property is saved from the
existing group and applied to the new group after its initialization; see the code of the
Click_miDeclareControlAsDominant() method at the top of this page.
Demonstration of those small visual prompts inside the group was the only reason to include the drawing of this group into
the OnPaint() method.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
info .Draw (grfx);
groupOfDominant .Draw (grfx);
dominColored .Draw (grfx);
if (bShowCovers)
{
mover .DrawCovers (grfx);
}
}
DominantControl group consists exclusively of controls. There is nothing to
Fig.15.16 DominantControl
draw in such group, so the DominantControl.Draw() method is only about
object is the basis of
painting those small prompts,. If you use a group of the same class without prompts,
this group
then you do not need any drawing.
World of Movable Objects 423 (978) Chapter 15 Groups
Both representatives of the DominantControl class which are shown at figures 15.13 and 15.14 have no frame. But
at the beginning of this section I wrote about the switch of ruling direction (inner elements must set the position of the
frame) and mentioned that I wanted only to start the discussion with the frameless groups but not to abandon the frame
entirely. There is the third group in the Form_DominantControls.cs; this group has a frame (figure 15.16). To be
absolutely correct I have to admit that the content of this group is the DominantControl object, while the group itself
(if a group is recognized as a frame with all its inner elements) belongs to the ElasticGroup class. A
DominantControl object has no frame of its own but it can get a frame if it is used to organize an ElasticGroup
object.
ctrlsInFrame = new Control [] { textNewComment, btnCommentClr, btnCommentFont,
btnAdd };
groupOfDominant = new ElasticGroup (this, new DominantControl (ctrlsInFrame),
12, "ElasticGroup");
The ElasticGroup class is the main theme of the next section; here are several more words about the last example of
the DominantControl object.
Group at figure 15.16 is purposely organized as being identical to the Group object from figure 15.11. Group with such
content is used in several tuning forms from the MoveGraphLibrary.dll. The purpose of this group is to add new
comment to one or another screen object. You type the needed comment inside the TextBox control and then select the
color and font for this comment. Button Add will add the new comment to the screen when the group is used in the proper
way. The main element in this group is definitely the control in which the text is typed; three buttons are the auxiliary
elements. The needed size of the TextBox control depends on the situation; in some cases the comments are short; in
others they can be long enough and multilined. If the control is the main element of the group, then it makes sense to make
this control the “ruler” of the whole group, so I made it dominant while initializing this group. But the whole group itself is
not going to look exactly in the way I gave it at the beginning. If users agree with the developer’s ideas, they can leave the
group as it is, but each user can easily change the overall view of the group, the relative positions of elements, and their
sizes.
The DominantControl class demonstrates an absolutely new type of organizing a group: such groups are determined
only by users; developer does not interfere in the process of changing the group but only provides such type of group which
can be rearranged by user in any possible way.
The best representative of such design is the ElasticGroup class which is the theme of the next section.
World of Movable Objects 424 (978) Chapter 15 Groups
Elastic group
Elastic group got its name for the main feature by which it differs from any other group which you saw up till now. Elastic
group has no predetermined area into which a set of elements has to be squeezed. On the contrary, elements which
constitute the group move and resize independently; all such movements change the united area of all elements and this
united area with some additional spaces on the sides is looked at as the group area. If the frame of such group is visualized,
then you can watch how it automatically changes its position with all the moving / resizing of inner elements. In such case
a frame looks like an elastic string which keeps a bunch of pencils together. Such behaviour of a group gave the name to
the ElasticGroup class. This class has been already used in several of the previous examples and in nearly all of them
the group consists of a set of individually movable controls. Though it is one of the most primitive ways of using the
ElasticGroup class, let us have a quick look into such case before turning to other possible situations.
File: Form_SetOfObjects.cs
Menu position: Graphical objects – Basic elements – Set of objects
Let us return once more to the Form_SetOfObjects.cs (figure 9.5). This form
was used to demonstrate a whole variety of different graphical elements, but now
we are interested not in them but only in one object from that form – the group of
buttons which allow to add all those different elements into the form. All buttons
inside the group (figure 15.17) are individually movable, so you can place them
in any way you want: put them vertically in one column or horizontally in a row,
change their order in any possible way, set the distances between the buttons
according to your taste and requirements for good design. I hope you are aware
that the reaction on the click of any button does not depend on the position or
size of this button. For example, a click of the button with the colored circle on
Fig.15.17 A set of buttons organized
top will add a new circle into the form regardless of the location of this button.
into an ElasticGroup
Thus, the purpose of the group (to put into the form only the set of different
elements required by user) and composition of the group are entirely object
independent. Developer is responsible for the correct work of the form according
to its main purpose; you, as a user, are free to compose the group in any way you want. The unlimited freedom of
composition is provided by the ElasticGroup class.
Suppose that you decide to organize such a powerful group in one of your applications. How much work will be needed for
it? Let us check it with this group from the Form_SetOfObjects.cs. This group is named groupAdd; our checking is
easily done by founding all the places in the code where this field is mentioned.
Step 1. Declare a group.
ElasticGroup groupAdd;
Step 2. Construct a group.
ctrlsGroup = new Control [] { btnRectangle, btnRegularPoly, btnConvexPoly,
btnChatoyantPoly, btnCircle, btnRing, btnStrip,
btnPerforatedPoly, btnRegPoly_CircularHole,
btnRegPoly_RegPolyHole,
btnConvexPoly_RegPolyHole };
void DefaultView ()
{
… …
groupAdd = new ElasticGroup (this, ctrlsGroup,
new int [] { 12, 6, 12, 12 }, "Add");
… …
This is one of many variants to initialize such a group; all depends on the number of details which you want to specify. This
constructor determines several things.
• A set of buttons to be included into the group.
• An array of spaces between the inner elements and the frame of the group.
• The group title.
World of Movable Objects 425 (978) Chapter 15 Groups
File: Form_PersonalData.cs
Menu position: Applications – Personal data
The ElasticGroup class can be used in much more interesting ways than in the previous example; that is why it
became the main constructing element in all of my applications. The basic ideas of the ElasticGroup class sound
simple enough:
• Any element of a group can be moved individually.
• The frame of a group is automatically adjusted to the positions and sizes of all inner elements and always
surrounds them.
• A group can be moved by any point. There is no difference between inner points and frame points.
What makes the ElasticGroup class extremely flexible and powerful, is the list of objects which can be used as inner
elements:
• SolitaryControl objects
• CommentedControl objects
• CommentedControlLTP objects
• ElasticGroup objects.
• DominantControl objects
This list of classes means that an element can be a control, a control with comment, a group of controls of which one is
dominant and others are subordinates, and an element can be a group of the same class. Especially this recursive definition
of the group makes the ElasticGroup class the most valuable. I do not see any other types of relations between
controls which are not covered by the mentioned cases, so, from my point of view, any type of form can be organized with
the ElasticGroup objects.*
public class ElasticGroup : GraphicalObject
{
List<ElasticGroupElement> m_elements = new List<ElasticGroupElement> ();
int [] m_framespaces = new int [4]; // Left, Top, Right, Bottom
bool bTitleMovable = true;
RectangleF rcFrame; // only through calculation
SolidBrush brushBack;
bool bShowFrame;
*
I want to emphasize that this is only for the forms which consist of individual controls and controls with comments. If
there must be some graphical objects inside group, then this situation is not covered by the ElasticGroup class and will
be considered in the section Arbitrary groups.
World of Movable Objects 426 (978) Chapter 15 Groups
Pen penFrame;
There are several fields in the ElasticGroup class which are of special interest for our analysis. I’ll try to mention them
in the same order as they are shown in the code above.
• All inner objects of the group are included into the List as elements of the ElasticGroupElement class.
The particular class of each element is determined by some value from the ElementType_ElasticGroup
enumeration.
public enum ElementType_ElasticGroup { SolitaryControl, CommentedControl,
CommentedControlLTP, Group, DominantControl };
• There is an array of four integer values which determine the distance to the frame. On three sides – left, right, and
bottom – it is really the distance (in pixels) between inner elements and the frame. At the top it is the distance
between inner elements and the title. Certainly, if there is no title in the group, then it is the distance to the frame
as on all other sides. ElasticGroup class has minimum allowed distance to the frame – 6 pixels. There is no
upper limit for this space in the class itself, so any value can be set via the parameters, but if you change the group
view through its standard tuning form (I’ll write about this form later), then there is maximum limit of 20 pixels. I
do not think that anyone will need bigger distance between inner elements and frame.
• ElasticGroup objects have a title which can be moved along upper line of the frame and placed anywhere
between the left and right borders of the frame. At any moment the title movability can be switched ON / OFF.
• Group background color can be
changed at any moment. Groups can
be nested inside each other and it is
possible to spread the same
background color on inner groups. In
many cases you will prefer to have the
same color for all parts of the big
group; in other cases you will like to
distinguish the inner group from the
outer group (figure 15.18) and maybe
to have individual color for each inner
group.
• Frame is not a mandatory element. It
is a very useful element to make the
content of the group more obvious, but
groups work in exactly the same way
without frame in view. Title is still
movable along the invisible frame and
the distance between inner elements
and invisible frame is changed in the
same way as to the visible frame.
Figure 15.18 shows the view of the
Form_PersonalData.cs. This form
demonstrates the use of the ElasticGroup
class with all possible types of inner elements
except the CommentedControlLTP class.*
At the same time, this is not an artificial
example to show one or another discussed Fig.15.18 Form_PersonalData.cs demonstrates many variants of
feature. This form deals with the collection of
using the ElasticGroup class
personal data; it is a situation which is familiar
*
The reason for such exception is simple: I warned against simultaneous use of CommentedControl and
CommentedControlLTP classes, so I do not want to demonstrate such bad practice. There are many
CommentedControl objects in this example, so there is not a single CommentedControlLTP object which looks
identically but behaves differently. At the same time, the use of either one or another class of controls with comments is
only my advice and nothing else, so if you want to use these two classes in the same form and even inside the same group of
the ElasticGroup class, there is no restriction that will not allow you to do it.
World of Movable Objects 427 (978) Chapter 15 Groups
to everyone throughout his life, so everyone can make his own decision about the usefulness of the proposed design on the
basis of the movable / resizable elements. Certainly, each of us deals with the collection of personal data only occasionally,
but there are people (from HR departments) who work with similar forms day after day; it would be very interesting to hear
their opinion.
The Form_PersonalData.cs deals with personal data. Depending on the case, it can include only a small piece of data or
might require to include more than is shown here; you are free to add any other needed groups of information (all the codes
are available).
The ElasticGroup class has more than 50 constructors. Such a number is due to the wide variety of cases in which
the class can be used and due to my attempt to make the initialization in many cases as easy as possible and avoid the
request of parameters which can be set by default. When any object of the five classes mentioned a bit earlier is used as an
element of the ElasticGroup object, it is first casted (directly or indirectly) into the ElasticGroupElement class.
There are nearly 20 different constructors for this class, so together they provide such a variety, that you can find the easiest
one to use for any type of the needed group whether it consists solely of the elements of one type or of some combination.
In the Form_PersonalData.cs example, the biggest (outer) group – groupData – includes two SolitaryControl
objects to show the date and time, two CommentedControl objects to show name and surname, and five inner groups.
Overall there are 23 controls in this form. I would not call it an extremely complex form (from time to time I have to design
much more complicated forms), but at the same time I think that the majority of readers rarely have a chance to design a
form with such number of controls. There are many ways of using different
constructors to design such form. I tried to make the code as easy as possible for
understanding, so, for majority of elements, I use the constructors to develop the
inner elements and then combine them into bigger groups. The only elements
which avoided this preliminary construction are two controls for date and time Fig.15.19 Two elements represent the
(figure 15.19); they are so simple that they are organized during the construction SolitaryControl case
of the outer group.
private void OnLoad (object sender, EventArgs e)
{
… …
groupData = new ElasticGroup (this, new ElasticGroupElement [] {
new ElasticGroupElement (textDate, Resizing .WE),
new ElasticGroupElement (textTime, Resizing .WE),
… …
TextBox controls for name and surname have short comments; otherwise it
would be impossible to distinguish them. These pairs “control + comment” are
represented by two CommentedControl objects (figure 15.20).
private void OnLoad (object sender, EventArgs e) Fig.15.20 Two elements represent
{ the CommentedControl case
… …
CommentedControl ccName = new CommentedControl (this, textName,
Resizing .WE, Side .W, "Name");
CommentedControl ccSurname = new CommentedControl (this, textSurname,
Resizing .WE, Side .W, "Surname");
… …
groupData = new ElasticGroup (this, new ElasticGroupElement [] {
… …
new ElasticGroupElement (ccName),
new ElasticGroupElement (ccSurname),
… …
Four of the inner groups – Day of birth, Address, Contacts, and Professional status
– have similar design: each one consists of an array of CommentedControl
objects. First an array of CommentedControl objects is prepared; then it is
used for group construction. Here is the code to prepare the Contacts group
(figure 15.21).
Fig.15.21
World of Movable Objects 428 (978) Chapter 15 Groups
ElasticGroupElement constructor, then an ElasticGroup is constructed on this array and then this group is
transformed into an object of the ElasticGroupElement class.
One inner group at figure 15.18 – the Address group – is shown in different color, but this is only to indicate the possibility
of changing colors and other parameters of visualization. In the user-driven applications ALL the parameters of
visualization are under users’ control, so these parameters can be changed by users at any moment. There are two standard
ways to change parameters: either through context menu or via some tuning form; the Form_PersonalData.cs demonstrates
both of them.
Objects of two classes used in this form have their tuning forms: InfoOnRequest and ElasticGroup.
• Tuning form for the InfoOnRequest class (figure 5.4) was demonstrated in the chapter Texts as a tuning form
for ClosableInfo class. Since then information of the InfoOnRequest class together with this tuning form
was used in more than 70 examples and I think there will be more examples ahead.
• Tuning form for ElasticGroup objects will be discussed 11 pages ahead (figure 15.30).
Each class of objects has its unique set of parameters, so if the tuning commands are available through context menu, then
each class needs personal context menu. There are eight context menus in the Form_PersonalData.cs; as usual, the menu
selection is called from inside the OnMouseUp() method when the right button is released.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
if (grobj is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, null,
PointToScreen (ptMouse_Up));
}
else
{
MenuSelection (mover .ReleasedObject);
}
}
}
else
{
if (fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
}
The only menu that does not require any preliminary identification of the released object is the menu which is called at any
empty place – menuOnEmpty; all others require such identification and more often than not the identification of the parent
object for the pressed one. The MenuSelection() method has one parameter – the number of the released object in
the mover queue. In this way menu selection works only for objects registered in the mover queue. For the applications
with a lot of controls, and the Form_PersonalData.cs is one of them, the menu can be also called by direct click of a
control. Such click of a control cannot call the same MenuSelection() method, because the control itself is never
covered by a cover. The menu selection after the click of a control will de discussed a bit later.
World of Movable Objects 430 (978) Chapter 15 Groups
At the moment when the MenuSelection() method is called, the order of the released object in the mover queue is
already known and by this number the class of the released object is easily obtained. The MenuSelection() method is
based on the analysis of the class of the released object.
private void MenuSelection (int iInMover)
{
GraphicalObject grobjCompare;
GraphicalObject grobj = mover [iInMover] .Source;
groupParent = null;
if (grobj is ElasticGroup) // ElasticGroup
{
… …
}
else if (grobj is DominantControl) // DominantControlGroup
{
… …
ContextMenuStrip = menuOnDominantControl;
}
else if (grobj is SubordinateControl) // SubordinateControl
{
… …
ContextMenuStrip = menuOnSubordinate;
}
else if (grobj is SolitaryControl) // SolitaryControl
{
… …
ContextMenuStrip = menuOnSolitaryControl;
}
else if (grobj is CommentedControl) // CommentedControl
{
… …
ContextMenuStrip = menuOnCommentedControl;
}
else if (grobj is CommentToRect) // CommentToRect
{
cmntPressed = grobj as CommentToRect;
ContextMenuStrip = menuOnComment;
}
}
The MenuSelection() method is too lengthy to be shown here in full view and below you can see the sketch of this
method with the majority (but not all!) of menus that can be opened. Figure 15.23 shows group Address on which three of
those menus can be called. Nobody is going to use the group with such view. Its normal view is shown at figure 15.18 and
this is a purposely deformed group in order to prepare a figure with all three menus in full view and the clear indication of
the places where they can be called. Each menu appears with its top left corner exactly at the place of the right mouse click.
Three menus are never shown simultaneously but for many actions they are called one after another; this will be
demonstrated in the next section several pages ahead.
Users do not know the number of different menus, though it is not a bad practice to inform them about it in some short way.
Users know the relations between screen objects (this is found very quickly) and they need to know some simple
standard (!) rules about calling and using menus.
• If you need to change some object, you call menu by clicking this object.
• If you need to modify dominant element with its subordinates, you call menu on dominant element.
• If you need to modify all elements of a group, you call menu at any empty place inside the group.
In the current example any object belongs to one or another group, so, before calling the needed menu, this group must be
also identified. This is done in nearly every branch of the MenuSelection() method except the last one, but this is the
part to which I want to attract your attention.
World of Movable Objects 431 (978) Chapter 15 Groups
3. The enforced relocation of comment may require the frame update, so the group surrounding the involved
CommentedControl object must be identified.
4. Change of the inner group may require the change of the big group as the outer frame has to adjust to all changes
inside.
All these things have to be done as a reaction on the font change request, so the font change is more complicated than color
change.
private void Click_miCommentFont (object sender, EventArgs e)
{
FontDialog dlg = new FontDialog ();
dlg .Font = cmntPressed .Font;
if (dlg .ShowDialog () == DialogResult .OK)
{
long idCmntCtrl = cmntPressed .ParentID;
GraphicalObject grobj;
for (int i = mover .Count - 1; i >= 0; i--)
{
grobj = mover [i] .Source;
if (grobj is CommentedControl && grobj .ID == idCmntCtrl)
{
CommentedControl cc = grobj as CommentedControl;
cc .CommentFont = dlg .Font;
groupData .Update ();
Invalidate ();
break;
}
}
}
}
I have to admit that I simplified my work, that I did not identify the surrounding group but substituted items 3 and 4 with a
single call to update the outer group.
groupData .Update ();
Such simplification is possible because the ElasticGroup.Update() is a recursive procedure: it first updates all the
inner elements, which means the updating of all inner groups, and then calculates the new position for its own frame taking
into consideration already recalculated frames of the inner groups.
The CommentedControl objects are used in several groups of the Form_PersonalData.cs. Via the commands of menu,
parameters of object can be changed in such a way that its “parent” group has to be updated. Also, an object can be hidden
from view; this requires not only the updating of the group but also the renewal of the mover queue. For any of these
actions, not only the CommentedControl object must be identified but also the group to which it belongs. The object
itself can be identified in the standard way, but group identification requires some search through the mover queue. Two
things are used for this search:
• All members of the group precede in the mover queue their group, so the additional search is to be done only
through the remaining part of the queue.
• All elements of a group contain the id of their group; this is used for group identification.
private void MenuSelection (int iInMover)
{
GraphicalObject grobjCompare;
GraphicalObject grobj = mover [iInMover] .Source;
… …
else if (grobj is CommentedControl) // CommentedControl
{
ccPressed = grobj as CommentedControl;
for (int i = iInMover + 1; i < mover .Count; i++)
{
grobjCompare = mover [i] .Source;
World of Movable Objects 433 (978) Chapter 15 Groups
if (grobjCompare is ElasticGroup &&
grobjCompare .ID == ccPressed .ParentID)
{
groupParent = grobjCompare as ElasticGroup;
break;
}
}
ContextMenuStrip = menuOnCommentedControl;
}
… …
Similar technique is used for subordinate controls, only instead of the ParentID property, the dominant control can be
obtained via the SubordinateControl.Dominant property and then its id is used.
private void MenuSelection (int iInMover)
{
GraphicalObject grobjCompare;
GraphicalObject grobj = mover [iInMover] .Source;
… …
else if (grobj is SubordinateControl) // SubordinateControl
{
subPressed = grobj as SubordinateControl;
for (int i = iInMover + 1; i < mover .Count; i++)
{
grobjCompare = mover [i] .Source;
if (grobjCompare is DominantControl &&
grobjCompare .ID == subPressed .Dominant .ID)
{
dominantParent = grobjCompare as DominantControl;
break;
}
}
ContextMenuStrip = menuOnSubordinate;
}
… …
Suppose that you want to call a context menu on some CommentedControl object; this menu is shown in the middle of
figure 15.23. In the Form_PersonalData.cs it can be done in two different ways.
You can press near the control around which the needed CommentedControl is organized. Inside the OnMouseUp()
method mover gives the number of the released object in its queue and the MenuSelection() method is called with this
number as a parameter. The pressed object reveals the identification number of its parent which has to be some group.
Then the search through the mover queue is done until the group with such id is found (see code at the top of this page).
With this, the pressed object and its surrounding group are both identified; now the needed
menuOnCommentedControl can be opened because commands of this menu need both objects.
The same menu can be called directly on the control and in this case the whole process of identification of the needed
objects is different; this is perfectly seen from the code of the MouseDown_commentedcontrol() method which is
shown below. At first, only the pressed control is known, so mover queue is searched for the CommentedControl object
with such control. After the needed CommentedControl object is found, the remainder of the identification is the same
as in the previous variant.
In the Form_PersonalData.cs, the menuOnCommentedControl is mentioned in the ContextMenuStrip
property of every control which is used to design a CommentedControl object and the next method is marked as a
reaction on MouseDown event for each of these controls.
private void MouseDown_commentedcontrol (object sender, MouseEventArgs e)
{
Control ctrl = sender as Control;
GraphicalObject grobj;
if (e .Button == MouseButtons .Right)
{
for (int i = 0; i < mover .Count; i++)
World of Movable Objects 434 (978) Chapter 15 Groups
{
if (mover [i] .Source is CommentedControl)
{
ccPressed = mover [i] .Source as CommentedControl;
if (ccPressed .Control == ctrl)
{
for (int j = i + 1; j < mover .Count; j++)
{
grobj = mover [j] .Source;
if (grobj is ElasticGroup &&
grobj .ID == ccPressed .ParentID)
{
groupParent = grobj as ElasticGroup;
return;
}
}
}
}
}
}
}
Looking through context menus of the Form_PersonalData.cs, you can find that they allow to do three main things:
• To change the parameters of visualization.
• To hide / restore objects or groups.
• To change the movability of objects.
Changing of the visualization parameters (colors, fonts, transparency…) is an understandable and expected requirement.
Users have different preferences, so this allows everyone to have an application with the best (or personally preferable)
view. Changing of visibility and movability requires a special explanation; this is done in the next two sections.
Because of this duality, it is not enough for an object to have a single visibility parameter, but there must be two different
parameters to regulate visibility of object. Both are Boolean parameters and an object
is visible only when its personal visibility and “induced” visibility have the true
value.
Consider a situation with the Address group shown at figure 15.24; this is the view of
the group when all its inner elements are visible. Suppose that you work with
information on a group of people only from your country; in this case the name of the
country is excessive. Call context menu on the Country commented control; the
needed menu can be called directly on the control or anywhere close to its border.
Use the first command from the opened menu (figure 15.25) to hide the unneeded
control and its comment; this command is executed by the Fig.15.24
Click_miHideCommentedControl() method.
void Click_miHideCommentedControl (object sender, EventArgs e)
{
ccPressed .Visible = false;
groupData .Update ();
RenewMover ();
}
The pair “control + comment” will disappear and you will have an empty place Fig.15.25 Menu on
instead of this pair. Everything else is unchanged because other elements of this CommentedControl objects
group keep the frame at its place. You do not need an empty space inside the
group, so you can move the last pair in the group closer to others and receive much better view of the group (figure 15.26).
Now the group is smaller and you have inside the Address group only those controls which are needed.
Now suppose that at some moment you decide to hide the Address group because mostly you communicate with these
people by phone. Call menu on this group (figure 15.27) by clicking at any empty place inside the group and use the Hide
group command; this command calls the Click_miHideGroup() method.
private void Click_miHideGroup (object sender, EventArgs e)
{
groupPressed .Visible = false;
groupData .Update ();
RenewMover ();
}
For some time you work without Address group in view but at the end of the year
you need an access to the address information because it is the time for Christmas
cards. You cannot call a menu on the invisible group, so you have to open context
menu on the surrounding group – the Personal data group. At the top of the opened Fig.15.26
menu (figure 15.28) you see the list of the main elements with the checkmarks against those of them which are currently
visible. The Address line is not marked; you have to click this line to return the Address group into view. Which view of
the Address group you expect to see: with the Country information (figure 15.24) or without (figure 15.26)? I would prefer
the last one because it was the view of the group before it was hidden. But
how can program make a decision if all the CommentedControl objects
inside the Address group were invisible at the moment when you ordered
this group to be shown again? How can program decide to turn into visible
only four of those five controls inside the group?
The mechanism to make the correct decision is based not on one visibility
parameter but on two of them. Both parameters belong to the base
GraphicalObject class and are inherited by any other class of movable
objects as all of them are derived (directly or through others) from that class.
There are two properties to get / set these parameters.
Fig.15.27 Menu on group
• Visible property is used to deal with the object visibility
parameter which is changed directly. This property of the CommentedControl object is set to false when
you hide the commented control through its own menu.
• VisibleAsMember property is used to deal with the visibility which is changed indirectly, for example, when
the visibility of the surrounding group is changed.
World of Movable Objects 436 (978) Chapter 15 Groups
Any class inherits both properties from the base class, but for any class of complex objects you will have to write the new
versions of them. For example, here is the Visible property for the ElasticGroup class.
new public bool Visible
{
get { return (base .Visible); }
set
{
base .Visible = value;
bool bToMembers = base .Visible && base .VisibleAsMember;
foreach (ElasticGroupElement elem in m_elements)
{
elem .VisibleAsMember = bToMembers;
}
if (value == true)
{
Update ();
}
}
}
Any object of the group is shown only if both of its visibility properties
return true. Taking this into consideration, it is now easy to
understand how the Address group will be reinstated into view from
figure 15.26 in the above mentioned situation.
1. At the first step, the pressed commented control (Country) was
hidden via the direct command of its context menu; thus the
Visible property of the pressed commented control was set
to false.
2. At the second step the whole Address group was hidden through
a command of its menu; at this moment the Visible
property of the group is set to false and some information is
sent to all inner elements of the group. Both visibility
parameters of the group are used to determine the value which
has to be sent to the inner elements; when the group is hidden,
the code above shows that all inner elements will get the Fig.15.28 Menu on Personal data group
false value through their VisibleAsMember properties.
3. Now you reinstall the visibility of the Address group to true; simultaneously all the inner elements of the group
receive the true value into their VisibleAsMember properties.
4. Four of the five inner elements now have both of their visibility parameters set to true, so they appear in view.
The Country member has the value of its VisibleAsMember property turned into true, but its Visible
property is still set to false, so this commented control is not shown.
All objects have two visibility parameters and two properties to deal with them, but not all objects use both. If an object is
used only as a stand alone, then its VisibleAsMember property is never used. It gets its true value at the moment
of initialization and is never changed after it, so it has no effect on visualization; the visualization of such object is
determined only by its Visible property.
Groups use both properties. It does not matter, how many levels of nested groups you have; beginning from the second
inner level, the command from the outer group to all inner elements goes into the VisibleAsMember property,
regardless of whether it was initially a direct or an indirect change. Here is the VisibleAsMember property for the
ElasticGroup class. Compare this code with the previous piece and you will see that there is no difference beginning
from the next inner level.
new public bool VisibleAsMember
{
get { return (base .VisibleAsMember); }
set
{
World of Movable Objects 437 (978) Chapter 15 Groups
base .VisibleAsMember = value;
bool bToMembers = base .Visible && base .VisibleAsMember;
foreach (ElasticGroupElement elem in m_elements)
{
elem .VisibleAsMember = bToMembers;
}
if (value == true)
{
Update ();
}
}
}
Further on you will see that exactly the same mechanism of two visibility parameters works in the complex objects which
are used for different types of plotting.
Two more aspects of hiding and unveiling objects.
1. There is no visual indication in the groups that one or several of its inner objects are hidden. You might want to
add such indication and you can certainly do it, but I decided not to add such indication.
2. If you have a multilevel system of nested groups and some objects on different levels were hidden, then the
restoration of visibility for all of them will require several consecutive calls on different menus. To make this
process much faster, the ElasticGroup class has a VisibleAll property which restores the visibility of
all elements at all the inner levels of a group. It is easier to understand the use of this property by comparison, so
menu on the outer group (figure 15.28) includes two similar commands. To see the difference in their results, hide
some elements in one of the inner groups and then hide this inner group. Then use one of the commands from
menu on the outer group.
If you click the Show all data command, then the inner group appears but without its hidden elements.
If you click the Unveil all inner elements command, then the inner group appears with all its elements.
On movability
Changing the movability of the screen objects on a fly seems a bit strange after all the efforts to make everything movable,
but the requirement for such option becomes more and more important with the increase of the complexity of the forms
(applications). Look once more at the Form_PersonalData.cs (figure 15.18). With the group Professional status you are
not going to have any problems in trying to move the comments, the controls, or the whole group because all elements stay
clearly apart from each other and there is enough empty space inside the group to grab it by a mouse. However, there is
different situation with other groups.
Take a look at the Contacts group (figure 15.21) or Address group (figure 15.24). In these groups an empty space where
you can grab not some inner object but the group itself is very limited. You have to remember that each control is
surrounded by its invisible frame – the cover of this control which allows its moving and resizing. So when controls stay
not far from each other, they are visually separated by some empty space, but if you press at this space, then with a high
probability you are going to grab one or another of the nearest controls. Chances are high that, while trying to move a
group, you will accidentally grab and move some inner element. Usually, when you are moving the group, its inner
elements are already placed in such a way which you prefer, so you do not want to move inner elements. To avoid such
accidental move of inner elements instead of the needed group movement, the inner elements can be fixed, for example, via
command of menu.
Fixing / unfixing of elements can be organized through two different menus at two different levels: it can be done
individually via menu of the particular object or it can be done for all objects of a group through the group menu. There are
two different ways of regulating the movability of inner elements; these two ways are associated with different properties
and require slightly different use of menus. All differences are caused by two ways of declaring any object immovable.
Any object has a cover; each cover consists of nodes. For a movable object at least some of the nodes must have a
parameter Behaviour.Moveable; only by these nodes an object can be moved. The GraphicalObject class
includes two Boolean fields which regulate the movability of an object and its transparency for mover. By default, any
object is movable and non-transparent for mover. (If an object is transparent for mover then it is automatically unmovable.)
bool m_movable = true;
bool m_transparentForMover = false;
World of Movable Objects 438 (978) Chapter 15 Groups
These two fields are regulated by two properties Movable and TransparentForMover. Any class of objects
participating in moving process is derived from the basic GraphicalObject class, so any object inherits these fields
and properties. On any change of these two fields through the properties, the DefineCover() method of the particular
class is called.
public bool Movable
{
get { return (m_movable); }
set
{
m_movable = value;
DefineCover ();
}
}
public bool TransparentForMover
{
get { return (m_transparentForMover); }
set
{
m_transparentForMover = value;
DefineCover ();
}
}
The most widely used elements in the Form_PersonalData.cs are the CommentedControl objects, so let us consider
two ways of changing their movability. By default all these objects are movable and on any of them a context menu can be
called. This menu is shown at figure 15.25; the second command of menu allows to fix the pressed pair “control +
comment”. On clicking this command, the Click_miFixUnfixCommentedControl() method is called.
private void Click_miFixUnfixCommentedControl (object sender, EventArgs e)
{
ccPressed .Movable = !ccPressed .Movable;
}
This is an old type of this method. It was demonstrated in a couple of programs throughout the previous years; now this line
is commented in the code, but I want to explain how it worked. When the Movable property sets the movable field
to false, then the behaviour parameter for all nodes in the cover gets the Behaviour.Frozen value.* The
pressed commented control becomes unmovable, but the analysis of the possibility to move any element is over at such a
place. What can be done in such situation? A menu can be called again at the same place and for the same element; via this
menu the pressed element (commented control) can be returned back into movable. But while this object is unmovable,
nothing can be moved at the area which is occupied by this element. The same change of movability not on individual basis
but for all the inner elements of a group can be organized via the menu of a group (figure 15.27).
Fixing of the inner elements is usually practiced after the view of some group is prepared and you want to avoid an
accidental move of the inner elements. For the densely populated group, like Address or Contacts groups, the use of such
command solves the problem of accidental movement of elements but does not make easier the movement of the whole
group because the area by which the whole group can be moved is still very limited and does not change after fixing the
inner objects. The use of the TransparentForMover property looks more promising in such case. The new version
of the same method to fix the pressed CommentedControl object uses this property.
private void Click_miFixUnfixCommentedControl (object sender, EventArgs e)
{
ccPressed .TransparentForMover = !ccPressed .TransparentForMover;
}
An object of the CommentedControl class consists of two parts: control and text. Control is wrapped into a
CommentedControl object; a comment is turned into a CommentToRect object. Both of them are derived from the
GraphicalObject class; eventually the mentioned call will come to the TransparentForMover property of the
*
Such change of cover is demonstrated, for example, with the class Text_Rotatable_Demo in the section Rotatable
texts of the chapter Texts.
World of Movable Objects 439 (978) Chapter 15 Groups
base class, so the DefineCover() method for both parts of the commented control will be called. And the standard
way to make any object transparent for mover is to add such an ending to its DefineCover() method.
public override void DefineCover ()
{
… …
if (TransparentForMover)
{
cover .SetBehaviour (Behaviour .Transparent);
}
}
Objects of five different classes can be used as inner elements of the ElasticGroup; methods to define covers for all
five classes get such a standard ending. From my point of view, this is better way to declare the inner elements of any
ElasticGroup object unmovable; objects cannot be moved, but the moving of the whole group becomes much easier
because mover can look through any element and grab the group by this point. The “negative effect”, if you can call it
negative, is that it is impossible to return such unmovable object back into movable on an individual basis. If a cover is
transparent for mover, then it is transparent regardless of the pressed button, so the individual menu on such transparent
cover cannot be called. With the previous version of making elements unmovable, it was possible to call a menu on it and
not only to change its movability but also use other commands, for example, change the parameters. With the new version
of movability switch, you have first to use the menu of the surrounding group to return back the movability of all the inner
elements; only after it you can call a menu on the particular element.
In one situation a switch to inner elements which are transparent for mover turned out to be
very useful. From time to time there is a request to design a group with unmovable inner
elements. The reason for such a request can be different, it happens rarely enough, but one
such group can be seen in the tuning form for all ElasticGroup objects. The
Form_ElasticGroupParams.cs is shown and discussed in the next subsection;
figure 15.29 demonstrates only one group from this form. This group includes a bunch of
small buttons through which nearly all fonts and colors of the group under tuning can be
changed. It is not a problem at all to make all these buttons movable, but at the moment
they are unmovable. The group itself can be moved around by any inner point; design of
such group is extremely simple. Fig.15.29 Elements of this
There is an array of controls which constitute the group. group are fixed
Control [] ctrlsMedley = new Control [] { … };
I position all controls so that they look good enough. I have to take into consideration that font can be changed by user at
any moment, but otherwise this positioning is not a problem. After it the group is constructed.
groupMedley = new ElasticGroup (this, ctrlsMedley);
There is an additional drawing of the texts inside this group, but this is not important at the moment. The important thing is
that this development of a group uses some default steps and parameters. First, each control is wrapped into a
SolitaryControl object with a default frame of six pixels around the borders of a control. Then the whole set of
SolitaryControl objects are used as inner elements and are surrounded by a frame by adding a default space between
the elements and a frame. Thus organized inner elements are movable by default; to declare them unmovable, I have to use
one property of the class.
groupMedley .ElementsMovable = false;
In such way I receive a movable group with unmovable inner elements; it is exactly what I need, but there is one small flaw
in design: if you try to press the mouse close enough to any inner control then nothing happens. If the cursor is in the
vicinity of any control (inside its six pixels range), then nothing can be moved. At the same time, the cursor is the same at
any point whether it is next to any control or far away from them; there is no visual difference between the covers of the
inner elements and the remaining area inside the frame, so there is no indication why it is impossible to move this group in
some situations.
Switch to another property solves this problem; instead of the previous statement I use another one.
groupMedley .ElementsTransparencyForMover = true;
Now this group can be really moved by ANY inner point not covered by controls.
World of Movable Objects 440 (978) Chapter 15 Groups
Further on, in the second part of this book, I will write about the Plot class which is widely used for plotting in the
scientific and engineering programs; I will also write about several other interesting classes for plotting. Objects of the
ElasticGroup class contain mostly controls and are used for customization of applications; those classes for plotting
produce entirely graphical objects and are used for absolutely different purposes but their overall design and implementation
have a lot of similarities. Objects of all these classes are complex with different parts involved in individual, synchronous,
and related movements. Complex objects often use the change of movability for their parts, but in this aspect the
ElasticGroup differs from all others.
By making inner elements of any ElasticGroup object transparent to mover, I simplify the moving of the whole group
because in such situation pressing of any inner element goes through it but never misses the group itself.
In many plotting classes, the associated parts are often placed outside the main plotting area. For example, scales are mostly
placed somewhere at the side of the plotting area and comments are placed not inside the scales or plotting area but next to
them. In such case the transparency of those parts can cause the mouse press to go through and to find some other unrelated
object which might happen to be at the same place. For such situations I would prefer the unmovable element – part of a
plot – to block the mover from looking further on for any other object at the same place. Though the time for discussion of
the plots will come later, I want to mention this significant difference in changing their movability.
Transformation of standard objects into movable can produce some interesting and absolutely unexpected ideas.
The view of an ElasticGroup object is similar to the view of an ordinary GroupBox which is known for many years:
there are frame and title; the view of inner elements is familiar. An ordinary GroupBox allows to show the title either on
the left or on the right side. Several of my classes for groups (GroupBoxMR and Group are among them) used the same
variants for positioning the title plus the possibility of its central positioning, so it was the parameter which allowed three
choices. But one day I asked myself a very simple question: “Why only three possibilities? What about those users whose
sense of perfection requires to put the title somewhere else?” I slightly changed the cover of the ElasticGroup class;
now its title can be moved along the upper line of the frame from the left side to the right and can be placed anywhere inside
this range. (Certainly, the title can be moved in the same range even when the frame itself is not shown.) The movability of
title can be changed exactly in the same way as the movability of any inner element: it can be fixed and unfixed at any
moment. At the current moment, the title movability is changed via the group menu (figure 15.27, second command from
the bottom). Maybe it is a mistake and I will move this regulation into the group tuning form where all the group
parameters of visualization can be changed. On the other hand, all the changes of movability and the commands to hide or
show the elements are done via the context menus; the title movability is of the same type of parameters.
user-driven applications, so you can rearrange this form in any way you want. There is a small menu outside all groups; this
menu allows to reinstall the default view and to change the font for all elements except information. All visibility
parameters of information can be changed via its own tuning form which can be called by the double click inside the
information area.
I have explained before that the frame of any ElasticGroup object automatically surrounds the whole set of inner
elements regardless of their movements and the size change. A group can be always moved by any frame point or inner
point; by increasing the distance between the inner elements and the frame you enlarge the area where the group can be
grabbed for moving. Distances on four sides are regulated independently by changing the values in the four controls of the
Spaces to frame group.
The controls of the unnamed group allow:
• To switch the drawing of the frame ON / OFF.
• To change color and font simultaneously for all comments of the tuned group.
• To change the background color, foreground color, and font for all controls of the tuned group.
These two groups in the tuning form are the ElasticGroup objects with their ElementsMovable property set to
false. Thus, no elements inside these groups can be moved, but the groups themselves are movable.
Title group allows to change the text, font, and color of the title and to change the distance between the title and the line of
the frame on the sides of the title. Visibility parameters in any application have to be controlled by users only; the
Form_ElasticGroupParams.cs is designed for such purpose and all controls inside this form are always enabled. There is
one exception – the TextBox control to change the title of the modified group. There are situations when the title of the
group has to be exactly the one which designer gave it, so the constructor of this tuning form has one additional parameter
which allows to set this control either enabled or disabled.
Three elements of the Background group have such purposes.
• To change the background color of the tuned group.
• To spread (or not) the background color on the inner groups; this button is enabled only for the groups which have
inner groups.
• To change the transparency of the tuned group. This is an element of the Trackbar class which was described
previously (chapter Complex objects, section Track bars).
All three objects of this group are movable; the Trackbar object is resizable, so you can change the length of this track
bar. The group can be moved around the form exactly as other two groups; on moving or resizing of the inner elements, the
frame of the group adjusts itself to the positions of its inner elements. The group behaves exactly as any other
ElasticGroup but it cannot belong to this class. At the beginning of this section, while starting the discussion of the
ElasticGroup class, I listed the classes of objects which can be used as inner elements of this class. All those choices
are based on controls; no arbitrary graphical object can be used as an element of the ElasticGroup class. Though the
behaviour of the group is identical, it must be something else. This group is an object of the ArbitraryGroup class
which is discussed a bit further on. But before turning to another class of groups, I want to demonstrate the use of
temporary groups.
I have demonstrated how this rule is implemented in several of the previous examples, but the explanation was mostly
aimed at showing how USERS can control all the visibility parameters and change the view of a working application.
Certainly, any users’ actions are transformed into the changes of the involved objects, so it must be based on the design of
the associated classes. All properties and methods of the involved classes are described in the
MoveGraphLibrary_Classes.doc (see Programs and documents). I do not want to rewrite parts of that document into this
book, so I decided not to list in the text the properties and methods of the classes which I demonstrate. When you need to
see more information on the properties and methods which you see anywhere in the code, you can easily find this
information in the mentioned document. The only exception which I hope to make as short as possible is going to be this
subsection about the ElasticGroup class. This class became the most flexible and effective in design of absolutely
different groups from the tiny to very complicated and consisting of several levels. Objects of this class are used in
absolutely different applications; this is demonstrated in many examples of this book. I preferred first to demonstrate the
use of many properties of this class and only after it to give some relatively short description of them. It will give better
understanding of the ElasticGroup class and also of other involved classes because their design is based on exactly
the same principles. If users are given the full control over the tuning of applications, then it must be based on the same
level of design for all classes. After demonstration of the Form_PersonalData.cs, it will be easier to understand how all
those commands of context menus and tuning form of the ElasticGroup class are transformed into the properties of
this class.
The ElasticGroup objects consist of different combinations of controls; some of those controls are accompanied by
comments, so two groups of properties are aimed at controls and comments.
List<Control> Controls Gets the List<> of all controls of the group.
Color ControlsBackColor Sets background color for all controls.
Font ControlsFont Sets font for all controls.
Color ControlsForeColor Sets text color for all controls.
int ControlsNumber Gets the number of controls.
Color CommentsColor Sets color for comments at all levels.
Font CommentsFont Sets font for comments at all levels.
bool CommentsMovable Sets the movability of comments for CommentedControl
elements.
Inner elements can be of five different types, but they are all considered like elements of the ElasticGroupElement
class, so there are several properties to deal with these elements.
List<ElasticGroupElement> Elements Gets the List<> of elements.
Font ElementsFont Sets font for all inner elements.
int ElementsInvisibleNumber Gets the number of invisible inner elements.
bool ElementsMovable Sets the movability of inner elements. If elements are unmovable,
then their relative positions can not be changed by moving any of
them; the whole group is moved by any inner point.
int ElementsNumber Gets the number of elements.
bool ElementsTransparencyForMover Sets equal mover transparency for all inner elements.
int ElementsVisibleNumber Gets the number of visible elements.
The background color of the group, its transparency, and the spreading of the new background color on the inner groups are
regulated by two properties and one method.
Color BackColor Gets or sets the background color.
void BackColorSpreadInside () Spreads the background color on inner groups if there are any.
double BackColorTransparency Gets or sets the background transparency. Coefficient is always
inside the [0, 1] range.
Any group may have a frame; several properties deal with the frame parameters.
RectangleF FrameArea Gets frame area.
Color FrameColor Gets or sets frame color.
int [] FrameSpaces Gets or sets an array of frame spaces (left, top, right, bottom).
Pen Pen Gets or sets frame pen.
bool ShowFrame Gets or sets frame visibility.
World of Movable Objects 443 (978) Chapter 15 Groups
Title is a separate element of the group; visualization of a title is regulated by several properties. Title is not a part of the
frame; title appearance does not depend on the frame visibility.
bool HasTitle Checks title existance.
string Title Gets or sets title.
double TitleAlignmentCoef Gets or sets title positioning coefficient; value is inside [0, 1].
Color TitleColor Gets or sets title color.
Font TitleFont Gets or sets title font.
bool TitleMovable Gets or sets title movability.
int TitleSideSpace Gets or sets space between title and the frame on its sides. The
allowed range is [0, 12].
These are not all the properties of the ElasticGroup class. There are other properties to deal with the visibility and
movability of the whole group and its elements. Those properties are especially important and were discussed in two
special subsections.
I want to underline again that the above mentioned properties are not included here to show the full list of things that can be
done with the ElasticGroup objects. This is only to give you the better understanding of the implementation of the
basic rules of user-driven applications in the case of one of the classes included into the MoveGraphLibrary.dll.
Temporary groups
File: Form_TemporaryGroup.cs
Menu position: Groups – Temporary group of controls
The previous example Form_PersonalData.cs (figure 15.18) demonstrates the flexibility of the ElasticGroup class
and a lot of opportunities which it provides in design and use of applications. I would say that users of such program can
change the view of the running application in any possible way, but there is one limitation. Elements of each group can be
hidden and returned back to view at any moment, but the full list of elements for each group was predetermined by a
designer and no other elements can appear in any group. However, in some cases this may be not enough. There are
situations when there is a need to organize a group of any elements which are currently on display. The
Form_TemporaryGroup.cs demonstrates the use of the same ElasticGroup class for organizing such temporary
groups.
Figure 15.31 shows initial view of
the form. There are 20 buttons of
the same size lined in two rows.
The buttons are numbered to make
the tracking of their moving and
resizing easier. Each one of these
buttons can be moved and resized
individually like it was described
before in the chapter Individual
controls. Buttons are not big but
can be enlarged; the best way to
resize them is to move any corner
though the places in the middle of
the sides can serve in similar way.
Other parts of borders for all buttons
are used for moving. It is the
standard technique for moving and
resizing of individual controls. Fig.15.31 The initial view of the Form_TemporaryGroup.cs
There are no groups in view, but
this can be easily changed. A group is organized in a way which is standard for such operations. Press the left mouse
button at an empty point and drag the mouse without releasing the button. Point of the initial mouse press and the current
mouse position become the opposite corners of rectangle which is shown by a thin frame. If you release the button at the
moment when there are two or more buttons inside this rectangle, then the rounded controls are organized into an
ElasticGroup object. As any control in the form can be moved and resized individually, then there is no sense in
organizing a group with less than two buttons inside.
World of Movable Objects 444 (978) Chapter 15 Groups
All controls of the form are stored in some collection to which there is an access via the Controls property. When the
mouse is moved and released, I want to distribute all these controls between two Lists.
• The first List contains all controls which are not rounded by rectangle. These controls can be moved only
individually; this is mentioned in the name of their List
List<Control> controlsSingle = new List<Control> ();
• Second List contains the controls which happened to be inside the rectangular frame
List<Control> controlsInFrame = new List<Control> ();
There is no rotation of any object in this form; there are also no context menus, so there is no difference on whether you
would like to press left or right button to start rounding the controls.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (!mover .Catch (e .Location, e .Button))
{
bRectInView = true;
}
}
There are 20 buttons and one text in this form. If the mouse button is pressed at any empty place, then the flag
bRectInView which regulates the drawing of temporary frame is switched ON. Initially pressed point
(ptMouse_Down) is one corner of the frame; the current point of the moving mouse (ptMouse_Move) is an opposite
corner of the frame. If another group was organized before and is shown at the screen, then it still exists throughout the
time of the new group design.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
info .Draw (grfx);
if (bRectInView)
{
grfx .DrawRectangle (Pens .Blue,
Math .Min (ptMouse_Down .X, ptMouse_Move .X),
Math .Min (ptMouse_Down .Y, ptMouse_Move .Y),
Math .Abs (ptMouse_Move .X - ptMouse_Down .X),
Math .Abs (ptMouse_Move .Y - ptMouse_Down .Y));
}
if (controlsInFrame .Count > 0)
{
group .Draw (grfx);
}
}
When you press the mouse button at any empty place and start moving the mouse without releasing its button, then you see
a rectangle which changes its sizes according to mouse movement. This is not a group yet but only a temporary frame.
This temporary frame is not a movable object! It is a painted frame and nothing else.
There are movable objects in this form: 20 controls and information area. When the mouse button is released, the
Mover.Release() method informs if it is the end of some moving / resizing operation for one of those objects. Only
when the mouse is released without releasing any movable object, then it is the time to check the possibility of organizing
new group.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (mover .Release ())
{
… …
}
else
World of Movable Objects 445 (978) Chapter 15 Groups
{
if (bRectInView)
{
SetGroup (new Rectangle (Math .Min (ptMouse_Down .X, e .X),
Math .Min (ptMouse_Down .Y, e .Y),
Math .Abs (e .X - ptMouse_Down .X),
Math .Abs (e .Y - ptMouse_Down .Y)));
bRectInView = false;
}
}
Invalidate ();
}
SetGroup() is the method to decide about the possibility of new group. First, both Lists of controls are cleared and
positions of all controls are checked against the area of the temporary frame. If only a single control is caught inside, then
there is no sense in organizing a group; this control is also moved to the List of individually movable buttons
(controlsSingle).
private void SetGroup (Rectangle rc)
{
controlsSingle .Clear ();
controlsInFrame .Clear ();
foreach (Control control in Controls)
{
if (rc .Contains (control .Bounds))
{
controlsInFrame .Add (control);
}
else
{
controlsSingle .Add (control);
}
}
if (controlsInFrame .Count == 1)
{
controlsSingle .Add (controlsInFrame [0]);
controlsInFrame .Clear ();
}
RenewMover ();
}
Everything else is going to happen in the RenewMover() method but the crucial thing is whether there is anything in the
controlsInFrame list or not when this method is called.
First, all controls from the controlsSingle list are registered with the mover. If
there are controls in the controlsInFrame list, then a group must be organized
and has to include all these controls. The group (of the ElasticGroup class) has to
be registered by its IntoMover() method; the group is registered ahead of all the
controls which are not included into the group. I also added a couple of code lines to
highlight the area of the group and to make it slightly transparent; these two lines can be
commented if you do not like such effect.
private void RenewMover ()
{
mover .Clear ();
foreach (Control ctrl in controlsSingle)
{
mover .Add (ctrl);
}
if (controlsInFrame .Count > 0)
{
group = new ElasticGroup (this, controlsInFrame);
World of Movable Objects 446 (978) Chapter 15 Groups
group .BackColor = Color .LightYellow;
group .BackColorTransparency = 0.3;
group .IntoMover (mover, 0);
}
mover .Add (info);
}
There are some programs which allow to do similar action on the controls. Those programs (at least, those which I
remember) mark the selected elements with some additional signs and turn those programs into “Move” mode; after moving
the elements, the signs are taken out and the programs return to the normal mode of operation. I think it is a wrong
decision. As you can see in my example, there is no other mode except the normal one. At any moment, whether there is a
group or not, any control continues to work as it is supposed to do. If any methods are associated with the clicks of these
controls, they can be started at any moment. Substitute these buttons with real controls from some real application; those
controls can continue to
fulfil their tasks
regardless of their
positions and sizes, of
being inside some
temporary group or
outside. You receive
exactly the same
application, but users get
a chance to rearrange its
view.
The behaviour of the
temporary group can be
changed in different
ways; I demonstrated it in
some examples of earlier
programs but decided not
to include into this one in
order to keep the code as
simple as possible. For
example, this temporary
group is organized at the Fig.15.32 View of the form after several steps of moving and resizing
moment when the mouse
is released; after it the view of the group is easily changed by moving / resizing of inner controls, but the group content is
fixed (figure 15.32). However, it is easy to add another checking of controls at the moment when a group or some control
is released. If the group is released, then the controls which are still not in the group can be checked against the area of the
group and included into the group if they are found to be inside the boundaries. If a control is released, then it is enough to
have the same checking only for this control. In such way the content of a group can be changed not only by drawing the
new frame but by annexation of other controls. You can find such addition to the behaviour of a group in one of further
examples (chapter Medley of examples, Form_Village.cs, figure 21.11).
When you close the Form_TemporaryGroup.cs and then open it again, you will be surprised to see all controls in the
initial order. I hope that you will be surprised because now you are familiar with all rules of user-driven applications; you
must expect that this example obey those rules as all other examples do, and here one rule is violated. I admit that this was
done on purpose because I wanted to keep in the code only the lines related to the work of temporary group and did not
want to add anything else. For the same reason, the information of the UnclosableInfo class is used in this example.
World of Movable Objects 447 (978) Chapter 15 Groups
Arbitrary groups
The ElasticGroup class which is discussed in the previous section can be used for design of forms with any level of
complexity, but there is one significant limitation: inner elements of such groups must belong to one of the predetermined
classes.
• SolitaryControl an individual control
• CommentedControl a control with freely movable comment
• CommentedControlLTP a control with limited positioning of comment. Comment can be placed
on any side of associated control and lined either by the control side or by
middle point, so there are 12 available positions.
• DominantControl a group of controls one of which is dominant
• ElasticGroup allows to organize a system of nested groups
Elements of five classes can be used inside and I have no plans to change them. Such list of elements allows to organize
any kind of groups consisting of controls. The only possible inclusion of the graphical objects can be done in the form of
textual comments belonging to the CommentedControl and CommentedControlLTP objects. The
ElasticGroup class does not allow to include into a group an arbitrary graphical object. Though the ElasticGroup
class is an excellent instrument for form design, this limitation becomes a real problem when the design requires to unite
into a group both controls and graphical objects or only graphical objects.
If there is a request for some new class then this class will be designed. It was an interesting chain of inventions that let to
the ArbitraryGroup class.
For many years I was designing very complicated programs in different areas and all groups in those applications were
either Panel or GroupBox objects. In some cases it was not easy to organize groups only on those two classes, but I
could use only them, so each time I had to think and construct something on such limited basis. (I was not the exception; all
other programmers had to do the same.) After starting the work on movability of all screen objects, I relatively quickly
made the progress from movable Panel and GroupBox to GroupBoxMR and Group classes and from them to
ElasticGroup class. When this class began to work without problems, I thought at first that I got an ideal instrument for
design of any forms, but shortly enough ran into situation when graphical objects had to be inside my group.
If there would be a request only for one or two classes of graphical objects to be included into the groups, then I would add
these classes to the list of available inner elements in the ElasticGroup class and the problem would be closed. But this
is definitely not a solution because from time to time some new classes have to be used inside the groups. This problem
needed absolutely different solution and I began to work on a new class of groups.
I started with the rules which had to be implemented.
• Any element of a group can be moved and resized individually. This action is regulated only by the rules of the
class for each particular element.
• The frame of a group is automatically adjusted to the positions and sizes of all inner elements and always
surrounds them.
• A group can be moved by any inner point.
It is not surprisingly at all that these rules are the same as implemented in the ElasticGroup class; objects of that class
work exactly as I need all groups to work and the problem is only in applying the same rules to any set of elements.
ElasticGroup got its name from the behaviour of its frame which always keeps together the inner elements. The frame
of the new class has identical behaviour, but this is not the most important characteristic of the new class. The most
important is the fact that new group can include any element, so the new class is named the ArbitraryGroup.
For the ElasticGroup class with the fixed list of available inner elements, it is possible to define beforehand and put
into code the most crucial features of the group behaviour:
• Updating a group after any change of inner elements
• Drawing
• Synchronous movement of elements
• Registering in the mover queue
World of Movable Objects 448 (978) Chapter 15 Groups
With an open list of elements for the ArbitraryGroup class, none of these actions can be coded beforehand. Thus, the
methods to do all these things must be provided at the moment of the group initialization. Here is a constructor for the
ArbitraryGroup class.
public ArbitraryGroup (Form formSrc, // form in which the group is used
GroupVisibleParameters vispars, // visibility parameters
string strTitle, // group title; it can be empty
Delegate_Bounds onAreaElems, // method to calculate the area of inner elements
Delegate_Draw onDrawElems, // method to draw inner elements
Delegate_Move onSynchroMoveElems, // method for synchronous movement of the elements
Delegate_IntoMover onIntoMoverGroup) // method to register the group with the mover
Pay attention that three first methods deal only with the inner elements and not with the group while the fourth method (for
registering in the mover queue) has to register the group also. From the point of consistency, it is a mistake and it would be
better to change something, but I understood it a bit too late when the ArbitraryGroup class was already in use in
different projects, so I decided not to change anything at all in the constructor of this class.
All visibility parameters for a group are stored in the GroupVisibleParameters field. This class has several
constructors; one of them has the full list of parameters which can be regulated; in the current example I use this
constructor.
public GroupVisibleParameters (
Color clrBackground, group back color
bool bShowFrame, flag regulates the frame showing
Color clrFrame, frame color
int [] spaces, spaces between inner elements and the frame (L, T, R, B)
bool bTitle, flag regulates title existence
Font fontTitle, title font
Color colorTitle, title color
double coefTitlePos, title positioning coefficient ([0, 1])
int titlespace) extra space on the sides of a title
For better acquaintance with the ArbitraryGroup class, let us look at the example in which a group of this class is used.
File: Form_ArbitraryGroup.cs
Menu position: Groups – Arbitrary group
The group in this example includes three
controls and a Plot object
(figure 15.33). One control – button – is
used to construct a SolitaryControl
object and the button text declares it. Two
other controls have graphical comments
and are turned into the
CommentedControl objects; texts of
both comments announce about it. I tried
hard not to use the Plot class before
discussing its features, but then I decided
that the use of such inner element gives a
clear signal that for such set of elements
something else instead of the
ElasticGroup must be used.
There are four steps to organize an
ArbitraryGroup object:
1. Declare and initialize all inner
elements of the group.
2. Organize a
GroupVisibleParameters
object containing the
visualization parameters for a
group. Fig.15.33 An ArbitraryGroup object
World of Movable Objects 449 (978) Chapter 15 Groups
ElasticGroup class, each inner element is cast into an ElasticGroupElement object; any moving or resizing of
such object automatically causes the updating of the group to which it belongs. In the ArbitraryGroup class, there is
no casting of inner elements into anything else. Inner elements of our group belong to such classes (SolitaryControl,
CommentedControl, Plot), objects of which can be used either inside groups or as stand alone movable objects. There
is no indication that any movement of these objects can affect some group. Certainly, I could organize some checking
based on the identification numbers but I decided to simplify the code and call the ArbitraryGroup.Update()
method for the group whenever anything is moved by the mover. This update is done in an instant.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Update ();
group .Update ();
ShowNumbersInListView ();
Invalidate ();
}
}
The ArbitraryGroup objects have a special tuning form, but it may not be the best way of tuning for each group of this
class. The problem is that groups are really arbitrary and the parameters which may need tuning in one group are not used
at all in another. Or you can look at this problem from another point. There are parameters which are common for all
groups and these parameters are included into the standard tuning form for the ArbitraryGroup class. But there are
groups which require tuning of some special parameters; these parameters are not included into the standard tuning form, so
commands for their change has to be included into menu. I do not like the situation
when some parameters of an object are accessible via tuning form while other
parameters of the same object are accessible via commands of menu. I would prefer to
have the same access to all parameters.
In the current example, the group is modified through the commands of menu; all
commands are divided into four groups (figure 15.34). Commands of the first three
groups deal with the background color of the group, the frame, and the title. View of
any ArbitraryGroup object depends on these parameters, so you will find tuning
of the same parameters in the special tuning form for the ArbitraryGroup class
which will be demonstrated further on. Not any group contains controls, so the
commands from the last group of this menu are specific to this example and you will
not find anything similar in the tuning form for the ArbitraryGroup class. Fig.15.34 Menu on group
All general parameters of visualization for ArbitraryGroup objects are saved in a GroupVisibleParameters
field; an access to this field is through the ArbitraryGroup.GroupVisibleParameters property. For example,
here is the change of the title color.
private void Click_miTitleColor (object sender, EventArgs e)
{
ColorDialog dlg = new ColorDialog ();
dlg .Color = group .GroupVisibleParameters .TitleColor;
if (dlg .ShowDialog () == DialogResult .OK)
{
group .GroupVisibleParameters .TitleColor = dlg .Color;
Invalidate ();
}
}
While demonstrating the Form_PersonalData.cs (figure 15.18), I mentioned two situations when the enforced relocation
of the comment of the CommentedControl object is needed and explained a small additional piece of code for the case
when this happens inside the ElasticGroup object. Exactly the same reaction is needed when it happens inside a group
of the ArbitraryGroup class. In the current example all the needed reactions in such case are united into the
PossibleCommentRelocation() method; this method is called when the comment font is changed or when the
comment is simply released.
World of Movable Objects 452 (978) Chapter 15 Groups
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is CommentToRect)
{
PossibleCommentRelocation (grobj as CommentToRect);
}
}
… …
This PossibleCommentRelocation() method is written not for general case but is simplified according to reality of
this Form_ArbitraryGroup.cs. There is no search throughout the mover queue for the CommentedControl parent of
the pressed comment. Such search was needed in the Form_PersonalData.cs which uses many CommentedControl
objects; in the Form_ArbitraryGroup.cs we have only two objects of the CommentedControl class, so if any comment
is involved, then its parent is either one or another. Certainly, the identification is still done with the help of the
CommentToRect.ParentID property.
private void PossibleCommentRelocation (CommentToRect cmnt)
{
long idCmntCtrl = cmnt .ParentID;
CommentedControl ccParent =
(idCmntCtrl == ccListView .ID) ? ccListView : ccPanel;
ccParent .CommentEnforcedRelocation (mover);
group .Update ();
Invalidate ();
ShowNumbersInListView ();
}
After the identification of the comment parent, the CommentedControl.CommentEnforcedRelocation()
method checks possible blocking of comment by its associated control and, if it happens, moves the comment. The return
value of this method signals whether the enforced relocation happened or not, but I do not use this value because the update
of the group is needed in any case. The ArbitraryGroup.Update() method uses one of the methods passed on
initialization to calculate the area of inner elements and then calculates the frame position around them.
The ArbitraryGroup class has to work similarly to the ElasticGroup class and has to provide saving and
restoring of the groups, but because the set of inner elements is unpredictable, then these operations require more efforts
from programmer. In the ElasticGroup class, the set of available inner elements is known; each of those elements has
its own methods to save and restore, and the standard methods ElasticGroup.IntoRegistry() and
ElasticGroup.FromRegistry() will do everything.
There are similar standard methods ArbitraryGroup.IntoRegistry() and
ArbitraryGroup.FromRegistry(); there is also similar pair of methods to save and restore through binary files,
but all these methods save and restore the visibility parameters of the group itself but do nothing with its inner elements
because they are unknown. First, methods for each inner element must exist, and then methods for the particular form must
be written. For the Form_ArbitraryGroup.cs it looks like this.
private void SaveIntoRegistry ()
{
string strRegKey = Form_Main .strRegKey + strAddRegKey;
RegistryKey regkey = null;
try
{
regkey = Registry .CurrentUser .CreateSubKey (strRegKey);
if (regkey != null)
{
regkey .SetValue (nameSize, new string [] {
m_version .ToString (), // 0
World of Movable Objects 453 (978) Chapter 15 Groups
ClientSize .Width .ToString (), // 1
ClientSize .Height .ToString (), // 2
info .Visible .ToString (), // 3
listView1 .Columns [0] .Width .ToString (), // 4
listView1 .Columns [1] .Width .ToString (), // 5
listView1 .Columns [2] .Width .ToString (), // 6
listView1 .Columns [3] .Width .ToString (), // 7
listView1 .Columns [4] .Width .ToString (), // 8
},
RegistryValueKind .MultiString);
scHelp .IntoRegistry (regkey, "scHelp");
info .IntoRegistry (regkey, "Info");
scButton .IntoRegistry (regkey, "scButton");
ccListView .IntoRegistry (regkey, "ccListView");
ccPanel .IntoRegistry (regkey, "ccPanel");
plot .IntoRegistry (regkey, "Plot");
group .IntoRegistry (regkey, "Group");
}
}
… …
While restoring the group, you have to restore all inner elements and then restore the group by using the standard method.
private void RestoreFromRegistry ()
{
string strkey = Form_Main .strRegKey + strAddRegKey;
RegistryKey regkey = null;
try
{
bRestore = false;
regkey = Registry .CurrentUser .OpenSubKey (strkey);
if (regkey != null)
{
… …
scButton = SolitaryControl .FromRegistry (regkey, "scButton",
button1);
ccListView = CommentedControl .FromRegistry (this, regkey,
"ccListView", listView1);
ccPanel = CommentedControl .FromRegistry (this, regkey,
"ccPanel", panel1);
plot = Plot .FromRegistry (this, regkey, "Plot");
if (scButton == null || ccListView == null || ccPanel == null ||
plot == null)
{
return;
}
group = ArbitraryGroup .FromRegistry (this, regkey, "Group",
CalcArea_elements, Draw_elements,
SynchroMove_elements, IntoMover_group);
… …
In the Form_ArbitraryGroup.cs, the ListView element is used to show a set of positions and sizes for the involved
objects; this is done with the help of the ShowNumbersInListView() method.
private void ShowNumbersInListView ()
{
listView1 .Items .Clear ();
listView1 .Items .Add (new ListViewItem (StringsForListView ("Frame",
group .FrameArea)));
listView1 .Items .Add (new ListViewItem (StringsForListView ("Group",
group .RectAround)));
World of Movable Objects 454 (978) Chapter 15 Groups
listView1 .Items .Add (new ListViewItem (StringsForListView ("Button",
scButton .Control .Bounds)));
listView1 .Items .Add (new ListViewItem (StringsForListView
("SolitaryControl (around Button)", scButton .FrameArea)));
listView1 .Items .Add (new ListViewItem (StringsForListView ("ListView",
ccListView .Control .Bounds)));
listView1 .Items .Add (new ListViewItem (StringsForListView
("CommentedControl (with ListView)", ccListView .RectAround)));
listView1 .Items .Add (new ListViewItem (StringsForListView ("Panel",
ccPanel .Control .Bounds)));
listView1 .Items .Add (new ListViewItem (StringsForListView
("CommentedControl (with Panel)", ccPanel .RectAround)));
listView1 .Items .Add (new ListViewItem (StringsForListView (
"Pure plotting area", plot .PlottingArea)));
listView1 .Items .Add (new ListViewItem (StringsForListView (
"Plot (whole area)", plot .RectAround)));
}
There are ten lines in the list: two lines are associated with the group and two lines with each inner elements. The first line
in each pair shows the numbers for smaller rectangle; it is interesting to compare some numbers inside each pair.
• For the ArbitraryGroup object, the numbers are obtained from the FrameArea() and RectAround()
methods. First method returns the area of the frame while the second method adds to it the area of the title, so the
difference between two results is equal to half of the title height.
• For the SolitaryControl object, there are two rectangles with the same difference between their borders on
all sides. The smaller rectangle is equal to the area of the control itself. When a control is turned into
SolitaryControl object, then it gets a sensitive frame of equal width along all four sides; the
SolitaryControl.FrameArea() method returns this bigger rectangle.
• For the CommentedControl object, the smaller rectangle is also equal to the area of the control itself. The
CommentedControl.RectAround() method adds to it the area of comment, so the difference between two
results depends not only on the comment itself but also on the space between comment and control.
• For the Plot object, the smaller of two rectangles is the pure plotting area; it is returned by the
Plot.PlottingArea() method. The RectAround() method adds to it the areas of scales.
The main purpose of having the ArbitraryGroup class is to use it for the groups consisting of graphical objects and
controls. Groups with such mix of inner elements are of high demand in many cases. In the next chapters I will
demonstrate complex objects used for different types of plotting; groups of the ArbitraryGroup class are widely used
in the tuning forms for these objects. In the chapter Applications for science and engineering, there is a special section
Design of the tuning forms; it is mostly about the design of forms on the basis of ArbitraryGroup objects.
The main difference of this form from all other tuning forms provided by the library is in the general way of changing the
parameters through this form: the Form_ArbitraryGroupParams.cs is called like a modal dialog. This piece of code is
taken from one of the examples which will be discussed later.
private void Click_miModifyDataGroup (object sender, EventArgs e)
{
GroupVisibleParameters gvpCopy = groupData.GroupVisibleParameters .Copy ();
Form_ArbitraryGroupParams form = new Form_ArbitraryGroupParams (gvpCopy,
groupData .Title, PointToScreen (ptMouse_Up));
if (DialogResult .OK == form .ShowDialog ())
{
groupData .GroupVisibleParameters = gvpCopy;
groupData .Update ();
Invalidate ();
}
}
When anything is changed in this form, it does not
immediately affect the original group under tuning.
Instead, there is a Sample group in the tuning form; this
group demonstrates the changes. You must be aware that
this Sample group has nothing to do with the original
group which you are tuning; the group under tuning is
called arbitrary because it is impossible to predict the
exact set of inner elements in that group, while the
Sample group inside this tuning form always has those
three elements which are shown at figure 15.35. This
group is an artificial one just to show the proposed
changes after the OK button will be pressed.
The visibility parameters for an ArbitraryGroup
object are stored in the GroupVisibleParameters
field. There are no problems with some of these
parameters, for example, there is no ambiguity with the
background color of the group, with a frame color, with
font and color for a title, and with the spaces on the sides.
But the use of font and color for inner elements is not so Fig.15.35 This Form_ArbitraryGroupParams.cs can be
obvious, as the elements in each particular case are used for tuning of the ArbitraryGroup
absolutely unpredictable; the use of these parameters objects.
must be coded by developer in each particular case.
At the beginning, there can be some confusing moments in using the Form_ArbitraryGroupParams.cs, so I would like to
add some explanation here in order to lessen this possible confusion.
• There are four groups in this form. All inner elements of three groups – these are the groups with titles – can be
moved around thus allowing to rearrange the view of these groups. Elements inside the small untitled group never
change their relative positions and this group always has the same view as shown at figure 15.35.
• Three groups can be used to change the parameters of the original arbitrary group under tuning; the fourth group –
Sample – is not used for any tuning but only to show the results of changing the parameters in other three groups.
• There is a small context menu that can be called anywhere outside the groups. The only command of this menu
allows to change the font which is used in design of three groups. The Sample group is not affected by this change
of the font.
• The Form_ArbitraryGroupParams.cs is designed according to the rules of user-driven applications, so the view
of this group is saved in Registry at the moment of closing and the form is supposed to have the same view on
the next opening. However, the view of the form partly depends on the parameters of an arbitrary group for tuning
of which it is opened; on opening the form, these parameters have higher priority in determining the view of this
form than the parameters saved in Registry.
World of Movable Objects 456 (978) Chapter 15 Groups
As in many previous examples, the small button is used to visualize the covers; at figure 15.36 you see not one but two
such buttons. One
button – at the figure it
is shown in the top left
corner – is used to
visualize covers for
objects at the level of
the form. Another
button is used for the
same purpose on the
page with lines. There
are independent movers
on each page and there
are such buttons on each
page.
I have already explained
the difference in
initializing a mover with
or without parameter.
Usually a Mover
object is initialized with
a parameter which
represents the form in
which it is used. When
mover is initialized in
such a way, then it is
going to move objects
only inside the visible
area and does not allow
them to be moved Fig.15.36 Different types of moving for lines are visualized by several types of additional
across the borders; this marks
was explained in the
subsection Safe and unsafe moving and resizing in the very first chapter Requirements, ideas, and algorithm. In nearly all
the examples of this book the mover initialization is demonstrated for the forms; in the Form_ObjectsOnTabControl.cs
example you can see the same mechanism used in form and on tab pages. I named all movers in this example according to
the area (form or tab page) in which each of them works.
public Form_ObjectsOnTabControl ()
{
InitializeComponent ();
moverForm = new Mover (this);
moverLines = new Mover (tabLines);
moverTriangles = new Mover (tabTriangles);
moverRectangles = new Mover (tabRectangles);
moverPolygons = new Mover (tabPolygons);
moverCircles = new Mover (tabCircles);
ctrls = new Control [] { tabsFigures, btnHelp, btnCovers_Form, btnAdd };
}
If you want to change the clipping level for movements on any particular tab page, you have to initialize the appropriate
mover without any parameter. For example, such change in one line in the code above will allow to move triangles across
the borders.
moverTriangles = new Mover ();
Figure 15.36 demonstrates the default view of the Form_ObjectsOnTabControl.cs. User can change the relative
positions of all elements at any moment. There is no link between the position of information and other elements, but all
four controls are united into the DominantControl object, so the relative positions of controls have one restriction: the
dominant element – TabControl object – cannot block small buttons. At the same time user is free to change the sizes of
this TabControl and to move all controls around. Checking of positions according to the mentioned restriction and
World of Movable Objects 459 (978) Chapter 15 Groups
remembering the positions of all subordinates as their preferable positions are provided by a single
DominantControl.SaveSubordinatePositionsAsForced() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (moverForm .Release ())
{
if (e .Button == MouseButtons .Left)
{
groupDomin .SaveSubordinatePositionsAsForced ();
Invalidate ();
}
}
}
There are no other features at the level of the form which need special mentioning. Let us look into the code for moving
and resizing of objects on the tab pages. We will start with the page for lines; this page is shown at figure 15.36.
All pages of the tab control are organized in a similar way. There are movable / resizable objects of some particular shape
on the page, plus information about this page, plus the small button to visualize the covers for objects of this page.
Information on each page is organized as an UnclosableInfo object. If you do not want the information to be seen on
the tab page, simply move it to the border of the page and place it so that only a tiny part of the information area will be
visible. In such way it will not occupy any valuable area and at the same time you can move it back and return into full
view at any moment.
The moving / resizing of all objects on the page are organized with the help of local mover, but as you will see further on,
there are some variations from page to page.
All pages of our tab control are not the standard tab pages but special pages which allow to avoid flickering throughout the
moving / resizing of objects.
private MoveGraphLibrary .TabPageWithoutFlickering tabLines;
Initialization of the local mover is made with a page as a parameter; as a result the objects can not be moved across page
borders.
moverLines = new Mover (tabLines);
Objects are registered in the mover queue according to the general rule: controls at the beginning, then graphical objects.
The information (graphical!) is registered ahead of all other graphical objects (lines) because I want this panel to move
above all the lines; the painting is done in the opposite order to registering.
private void RenewMover_tabLines ()
{
moverLines .Clear ();
moverLines .Add (scCovers_Lines);
moverLines .Add (infoLines);
for (int i = 0; i < lines .Count; i++)
{
moverLines .Add (lines [i]);
}
}
Lines of the LineSegment_Regulated class are used in this example.
public class LineSegment_Regulated : GraphicalObject
{
PointF ptA, ptB;
Pen pen;
bool bResize;
bool bRotate;
bool bMoveForward;
Cover for such objects consists of three nodes; two small circular nodes at the ends provide the resizing, while the strip node
along the segment from one end to another provides forward moving and rotation.
World of Movable Objects 460 (978) Chapter 15 Groups
public override void DefineCover ()
{
cover = new Cover (new CoverNode [] {new CoverNode (0, ptA),
new CoverNode (1, ptB),
new CoverNode (2, ptA, ptB, Cursors .SizeAll)});
}
In many previous examples I have demonstrated that the best procedure to change the
set of available movements for an object is to change its cover. However, in the
majority of classes which I demonstrate in the Form_ObjectsOnTabControl.cs,
different technique is used: the possibility of each movement is regulated by a special
Boolean field, so for the LineSegment_Regulated class there are three of them.
Values of these Boolean fields can be changed via the context menu; figure 15.37
demonstrates such menu for a cyan line from figure 15.36. When line is initialized,
then all movements are allowed; for such line the first three commands in menu are
checked. Later I switched OFF resizing and rotation for the cyan line. For the
Fig.15.37 Context menu on the
LineSegment_Regulated class I decided to add special marks into the drawing;
cyan line from the
these marks make obvious (at least I hope so) the movements allowed or restricted for
previous figure
each line at any particular moment.
• Rotation of any line is organized around its middle point. If rotation of a line is allowed, then there is a small circle
around this center of rotation; these marks are shown for the green and yellow lines at figure 15.36.
• Lines can be resized by the end points. If a line is turned into non-resizable, then two short black lines are painted
at the ends orthogonally to the line itself; such marks are shown at the ends of the cyan line.
• Line can be moved forward by any inner point. If such movement of a line is forbidden, then several small black
marks are painted along this line as if the line is nailed to the background; yellow line from figure 15.36 cannot be
moved forward.
Any line can be involved in three different movements: an ordinary forward movement, resizing, and rotation. The allowed
movements for a line at each particular moment are signaled by the check marks in the context menu which can be opened
on a line, but the above mentioned combination of special visual marks gives the same information without opening a menu.
Thus, we can decide about the possible movements of lines just by looking at figure 15.36.
• Green line can be involved in all three movements.
• Yellow line can be rotated and resized but not moved forward.
• Blue line can be moved forward and resized but not rotated.
• Cyan line can be
only moved
forward.
Moving / resizing of
objects – in this case they
are lines – on a tab page is
organized in exactly the
same way as moving /
resizing of objects in the
forms. I will write about it
a bit more while looking
into another tab page
which contains polygons.
Figure 15.38 shows two
types of polygons that are
used on this tab page:
unicolor regular polygons
and multicolor polygons
which can be of different
shapes. For the simplicity
of initialization, the
Fig.15.38 The page of polygons
World of Movable Objects 461 (978) Chapter 15 Groups
multicolor polygons are organized also as regular polygons, but after it they can be reconfigured in any possible way.
To make my life as a developer a bit easier, all polygons on this page are organized into a List of RegulatedPolygon
objects, but each of them is initialized either as RegPoly_Regulated or ChatPoly_Regulated element.
List<RegulatedPolygon> polygons = new List<RegulatedPolygon> ();
public class RegulatedPolygon
{
RegulatedPolygon_Type figure;
RegPoly_Regulated regPoly;
ChatPoly_Regulated chatPoly;
Regular polygons of the RegPoly_Regulated class which are used on this tab page are similar to regular polygons
demonstrated in the previous examples, so a lot of things must be familiar to you. Any regular unicolor polygon is
described by its central point, number of vertices, radius of vertices, angle of the first vertex, and polygon color. For objects
of this RegPoly_Regulated class the possibility of three different movements – forward movement, resizing, and
rotation – are determined by the values of three Boolean fields.
public class RegPoly_Regulated : GraphicalObject
{
PointF center;
float radius;
int nVertices;
double angle;
SolidBrush brush, brushAnchor;
Pen penBorder;
bool bResize;
bool bRotate;
bool bMoveForward;
bool bRadiusRangeSet;
float fMinRadius, fMaxRadius;
static int nRadiusDisappearance = 5;
When regular polygon is resizable, then it is resizable by any border point. To provide such resizing, border segments are
covered by the thin strip nodes. When the resizing is not needed, then all these strip nodes are not needed also. Thus, the
switch of resizing flag between ON and OFF changes the number of nodes in the cover.
public override void DefineCover ()
{
CoverNode [] nodes;
if (bResize)
{
// first the strips along the sides; the last one is the convex polygon
nodes = new CoverNode [nVertices + 1];
PointF [] pts = Vertices;
for (int i = 0; i < nVertices; i++)
{
nodes [i] = new CoverNode (i, pts [i], pts [(i + 1) % nVertices]);
}
nodes [nVertices] = new CoverNode (nVertices, pts);
}
else
{
nodes = new CoverNode [] { new CoverNode (0, Vertices) };
}
cover = new Cover (nodes);
}
With the polygonal node which covers the whole area of a regular polygon the situation is different. Whether you need only
forward movement of a polygon, or its rotation, or both of these movements, in all these cases you have to include into the
cover a big polygonal node, so any change in the possibility of these movements has no effect on the cover. Even if you
strip a polygon of all possible movements, you still have to include into its cover this polygonal node; otherwise there will
be no chances to call a context menu on the polygon to reinstall the needed movements. Because two fields regulating the
World of Movable Objects 462 (978) Chapter 15 Groups
possibility of forward movement and rotation – bMoveForward and bRotate – are not used during the design of
cover, then they must be used inside the MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (cover [iNode] .Shape == NodeShape .Strip)
{
… …
}
else
{
if (bMoveForward)
{
Move (dx, dy);
}
}
}
else if (catcher == MouseButtons .Right && bRotate)
{
double angleMouse = Auxi_Geometry .Line_Angle (center, ptM);
angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
The value of the bResize field regulates the possibility of resizing “in general”, but when this value is set to true,
there are two additional ways to regulate a resizable polygon. First, there is a special radius of disappearance: whenever a
polygon is squeezed and released with its radius less than this radius of disappearance, then this polygon is deleted. This
variant is used at the tab page with polygons.
private void MouseUp_tabPolygons (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
RegulatedPolygon poly;
if (moverPolygons .Release ())
{
GraphicalObject grobj = moverPolygons .ReleasedSource;
if (e .Button == MouseButtons .Left && grobj is RegPoly_Regulated)
{
if (regpolyPressed .Radius < RegPoly_Regulated.DisappearanceRadius)
{
for (int i = polygons .Count - 1; i >= 0; i--)
{
if (polygons [i] .PolygonType ==
RegulatedPolygon_Type .RegPoly &&
polygons [i] .RegularPolygon .ID == regpolyPressed .ID)
{
polygons .RemoveAt (i);
RenewMover_tabPolygons ();
tabPolygons .Invalidate ();
break;
}
}
}
}
… …
World of Movable Objects 463 (978) Chapter 15 Groups
Another regulation of resizing is used for regular polygons of the same class in the Form_AddPolygon.cs which is
discussed slightly further on. I want those polygons to be resizable only inside some range, so after a standard initialization
those polygons get the range for resizing; also a flag indicating the use of such range gets the true value.
polyReg = new RegPoly_Regulated (new PointF (200, 200), fRadius_RegPoly,
nVertices, 0, clr_RegPoly);
polyReg .MinRadius = 20;
polyReg .MaxRadius = 400;
polyReg .RadiusRangeSet = true;
Those minimum and maximum allowed radii are used inside the RegPoly_Regulated.MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
double distanceMouse = Auxi_Geometry .Distance (center, ptM);
if (!bRadiusRangeSet)
{
… …
}
else
{
float rNew = Convert .ToSingle (distanceMouse * scaling);
if (fMinRadius <= rNew && rNew <= fMaxRadius)
{
radius = rNew;
DefineCover ();
bRet = true;
}
}
… …
Chatoyant polygons from figure 15.38 belong to the ChatPoly_Regulated class. This class has four different
Boolean fields to regulate the possibility of different movements.
public class ChatPoly_Regulated : GraphicalObject
{
PointF m_center;
PointF [] ptVertices;
Color clrCenter;
Color [] clrVertices;
bool bAuxiLines;
bool bResize;
bool bRotate;
bool bReconfigure;
bool bMoveForward;
Two of these fields – bResize to regulate the resizing and bReconfigure to regulate the change of configuration –
are used in the DefineCover() method. Depending on the values of these two fields, the number of nodes in the cover
is changed. Two other fields – bMoveForward for forward movement and bRotate for rotation – do not affect the
cover but are used inside the MoveNode() method.
public override void DefineCover ()
{
int nCircles = bReconfigure ? (VerticesNumber + 1) : 0;
int nStrips = bResize ? VerticesNumber : 0;
CoverNode [] nodes = new CoverNode [nCircles + nStrips + VerticesNumber];
if (nCircles > 0)
{
for (int i = 0; i < VerticesNumber; i++)
{
nodes [i] = new CoverNode (i, ptVertices [i], 6);
World of Movable Objects 464 (978) Chapter 15 Groups
}
nodes [VerticesNumber] = new CoverNode (VerticesNumber, m_center, 6);
}
int k0 = nCircles;
if (nStrips > 0)
{
for (int i = 0; i < VerticesNumber; i++)
{
nodes [k0 + i] = new CoverNode (k0 + i, ptVertices [i],
ptVertices [(i + 1) % VerticesNumber]);
}
}
k0 += nStrips;
for (int i = 0; i < VerticesNumber; i++)
{
PointF [] pts = new PointF [3] { ptVertices [i],
ptVertices [(i + 1) % VerticesNumber], m_center };
nodes [k0 + i] = new CoverNode (k0 + i, pts);
}
cover = new Cover (nodes);
}
Whatever is written up till this place in this subsection is only about different objects on the pages of tab control; the
previous text explains the design and movements of these objects, but nothing is written yet about the use of
ArbitraryGroup objects. Demonstration and discussion of such groups was the main purposes of including the
Form_ObjectsOnTabControl.cs into this chapter, so where are these groups? The groups of such class are used in the
auxiliary forms which are needed to define a new object or to change the colors of the existing object. Even for these cases
a lot depends on the shape of the currently demonstrated objects. No additional forms are needed to add a new line,
triangle, or rectangle; such objects are organized with randomly chosen parameters.
For polygons and circles the situation is more interesting and complicated. Pages with polygons and circles demonstrate
two different classes of objects each; it is not known beforehand, object of which class you want to add when you click the
button, so both possibilities must be available. Also the parameters of a new object must be set via several controls.
Throughout the process of setting these parameters, user needs to see a proposed view of the new object after each change,
so there is a need for a group consisting of a graphical object and several controls. An ArbitraryGroup class was
especially designed for groups with combination of graphical objects and controls. Let us consider the case of adding new
polygon. When the button is clicked in the situation shown at figure 15.38, then the Form_AddPolygon.cs is opened.
private void Click_btnAdd (object sender, EventArgs e)
{
int iTab = tabsFigures .SelectedIndex;
if (0 <= iTab && iTab < tabsFigures .TabCount)
{
TabPage page = tabsFigures .TabPages [iTab];
Rectangle rc = page .ClientRectangle;
switch (iTab)
{
… …
case 3: // Polygons
Form_AddPolygon formP = new Form_AddPolygon ();
if (DialogResult .OK == formP .ShowDialog ())
… …
The Form_AddPolygon.cs includes two groups which allow to design polygons of two different classes. To see how these
two groups of the ArbitraryGroup class are designed and used, let us consider the situation when the
Form_AddPolygon.cs is called for the first time; in such case there is no information in the Registry about this form,
so the initialized groups get default view (figure 15.39 shows nearly default view).
The group to design a new regular polygon – groupRegular – has to include a sample of such polygon, two controls to
set the color and the number of vertices, and a button to give a command for adding new polygon into the initial tab page.
World of Movable Objects 465 (978) Chapter 15 Groups
The control to set a number of vertices (numericUD_RegularVertices) is accompanied by a short comment; this pair
is organized as a CommentedControlLTP object (ccVerticesRegular).
ArbitraryGroup groupRegular;
SolitaryControl scRegularColor, scAddRegular;
CommentedControlLTP ccVerticesRegular;
RegPoly_Regulated polyReg;
The group design starts with the initialization of a
polygon. Initial polygon position does not matter
at all as it will be changed shortly after, but the
radius of polygon vertices is important. After the
polygon is designed, three controls are placed in
relation to this polygon; a
CommentedControlLTP object is
constructed on the basis of one control; two
others are wrapped into SolitaryControl
objects. When the group is initialized, then one
of its main methods – the
GroupUpdate_Regular() method – is used
to organize the frame at the proper position
around all inner elements. When the group is
designed, its location is changed to the proper
place. During this relocation, the frame and all
inner elements of the group are moved
synchronously, that is why the position of the
polygon at the moment of initialization is not
important. The second group –
groupChatoyant – is organized in similar Fig.15.39 Form_AddPolygon.cs includes two groups to prepare
way and then moved and placed side by side with new polygons of two classes
the first group. Then information of the
InfoOnRequest class is organized and the form size is calculated.
private void OnLoad (object sender, EventArgs e)
{
Spaces spaces = new Spaces (this);
btnHelp .Location = new Point (spaces.FormSideSpace, spaces .FormTopSpace);
scHelp = new SolitaryControl (btnHelp);
// Regular polygon
int nVertices = 5;
numericUD_RegularVertices .Value = nVertices;
float fRadius_RegPoly = 100;
Color clr_RegPoly = Color .Blue;
polyReg = new RegPoly_Regulated (new PointF (200, 200), fRadius_RegPoly,
nVertices, 0, clr_RegPoly);
polyReg .MinRadius = 20;
polyReg .MaxRadius = 400;
polyReg .RadiusRangeSet = true;
Rectangle rc = Rectangle .Round (polyReg .SquareAroundCircle);
numericUD_RegularVertices .Location = new Point (rc .Left –
numericUD_RegularVertices .Width, rc .Top);
ccVerticesRegular = new CommentedControlLTP (this,
numericUD_RegularVertices, "Vertices");
btnColor_Regular .Location = new Point (numericUD_RegularVertices .Left,
numericUD_RegularVertices .Bottom + spaces .VerMin);
scRegularColor = new SolitaryControl (btnColor_Regular);
Rectangle rcBtn = btnAdd_Regular .Bounds;
btnAdd_Regular .Location =
new Point (Convert .ToInt32 (polyReg .Center .X) - rcBtn .Width / 2,
rc .Bottom + 2 * spaces .VerMin);
scAddRegular = new SolitaryControl (btnAdd_Regular);
World of Movable Objects 466 (978) Chapter 15 Groups
GroupVisibleParameters visparams = new GroupVisibleParameters (BackColor,
true, Auxi_Colours .DefaultFrameColor (this),
new int [] { 10, 6, 10, 10 }, true,
Font, SystemColors .Highlight, 0.25, 4);
groupRegular = new ArbitraryGroup (this, visparams, "Regular polygon",
ElementsArea_groupRegular, DrawElems_groupRegular,
SynchroMove_groupRegular, IntoMover_groupRegular);
groupRegular .Location = new Point (spaces .FormSideSpace,
btnHelp .Bottom + spaces .Ver_betweenFrames);
… …
The behaviour of any ArbitraryGroup object is determined by four methods which are passed as the parameters on
initialization. Lets us look on these four methods for the group to define a new regular polygon – groupRegular.
ElementsArea_groupRegular() method calculates the united rectangular area of the inner elements. I want to
underline one feature of this method. In many examples I use the RectAround() method to calculate a rectangular area
around some graphical object. If I would do the same for the regular polygon from the Form_AddPolygon.cs
(figure 15.39), then you would see a strange and not good effect. While such polygon is under rotation, the rectangular area
calculated by its vertices is currently changing. If the frame of a group would depend on such constantly changing inner
rectangle, then this frame would be changing all the time; there would be a constant twitching of the frame. This is not
good at all and in order to avoid such twitching, instead of the square around the vertices I use the square around the circle
which is based on the vertices of a polygon; this square is returned by the polyReg.SquareAroundCircle property.
Throughout the rotation of a polygon, radius of its vertices is not changing, so the square around the circle does not change
and the frame of a group does not twitch.
private Rectangle ElementsArea_groupRegular ()
{
return (RectangleF .Union (RectangleF .Union (RectangleF .Union (
ccVerticesRegular .RectAround, btnColor_Regular .Bounds),
btnAdd_Regular .Bounds), polyReg .SquareAroundCircle));
}
DrawElems_groupRegular() method is used for drawing the inner elements. There is no drawing for
SolitaryControl elements, so only two inner elements are mentioned in this method.
private void DrawElems_groupRegular (Graphics grfx)
{
polyReg .Draw (grfx);
ccVerticesRegular .Draw (grfx);
}
SynhroMove_groupRegular() method is used for synchronous move of the inner elements. All four inner elements
of the group have their own Move() methods which are called with identical parameters.
void SynchroMove_groupRegular (int dx, int dy)
{
ccVerticesRegular .Move (dx, dy);
scRegularColor .Move (dx, dy);
scAddRegular .Move (dx, dy);
polyReg .Move (dx, dy);
}
IntoMover_groupRegular() method is used for registering the group and all its inner elements in the mover queue.
All movable elements must be registered individually; but their order in the queue is very important. First must be the inner
controls (two SolitaryControl objects), then elements which combine control with graphical addition (one
CommentedControlLTP object), then inner graphical objects (a polygon), and at the end the group itself. This is how
the elements must be positioned in the mover queue, but it is easier for me to use the Insert() method, so I start from
the last element and insert them all at the same mover position.
void IntoMover_groupRegular (Mover mv, int iPos)
{
mv .Insert (iPos, groupRegular);
mv .Insert (iPos, polyReg);
World of Movable Objects 467 (978) Chapter 15 Groups
mv .Insert (iPos, ccVerticesRegular);
mv .Insert (iPos, scRegularColor);
mv .Insert (iPos, scAddRegular);
}
Second group at figure 15.39 – the groupChatoyant which is used to add a chatoyant polygon – looks similar to what
was already shown previously in the chapter Polygons. As it was more than three hundred pages back, then I’ll remind you
about that old example. In the section Chatoyant polygons of the chapter Polygons I wrote for the first time about the
multicolor polygons which are originally designed as regular but with the possibility of their wide transformations. By
clicking a small button in the Form_Polygons_Chatoyant.cs (see figure 6.8), you open an additional
Form_AddChatoyantPolygon.cs; figure 15.40a shows this form (this is the copy of figure 6.11)
Fig.15.40a Fig.15.40b
Form Form_AddChatoyantPolygon.cs Form_AddPolygon.cs (one group)
Polygon movable movable
rotatable rotatable
resizable resizable
Small circles fixed fixed
Central color figure movable fixed
Commented control CommentedControl object CommentedControlLTP object
Font change allowed allowed
Both cases are developed for exactly the same purpose – creation of a new chatoyant polygon in the shape of a regular
polygon – so the same elements are needed:
• Multicolor polygon
• Small circles to represent and set the colors in vertices
• Small square to represent and set the color of the central point
• NumericUpDown control to set the number of vertices
• Button to add a new polygon.
The table with information makes the comparison of two cases easier. You can see from this table that two cases differ in
two aspects. First, a small area, by clicking which the central color is changed, is either movable (first case) or fixed
(second case). Second, control with comment is a CommentedControl object in the first case and a
CommentedControlLTP object in the second case. This gives you one more chance to compare and decide for yourself,
which class you prefer.
Let us return back from an auxiliary form to our example Form_ObjectsOnTabControl.cs. Graphical objects on each tab
page have their own context menu; figure 15.41 shows menu for polygons plus its submenu.
The first group of this menu consists of four commands to change the order of polygons. The same group of commands was
already demonstrated at figure 8.17 (menu on colored areas in the Form_FillTheHoles.cs from the chapter Transparent
nodes) and at figure 9.7 (menu on any colored figure in the Form_SetOfObjects.cs from the chapter Finishing strokes on
World of Movable Objects 468 (978) Chapter 15 Groups
graphical primitives); you can find the same commands in many other forms with different graphical objects. Interesting
thing in the current example is that the tab page with polygons is the only page which uses these commands; the tab pages
of triangles, rectangles, and circles use different technique to change the order of graphical objects on the screen; I will
write about it at the end of this section.
The second group of commands in the shown menu can look slightly different depending on whether the menu is called on
regular or chatoyant polygon. Regular polygons do not allow their reconfiguring, so the line with the corresponding
command is simply disabled for them. The enabled command for reconfiguration in menu at figure 15.41 makes it obvious
that this menu was called on a chatoyant polygon.
The line “Rotation center” in the last group of
commands is also enabled only for chatoyant
polygons; through this line a short submenu is
opened to select one of two choices. In the
previous examples, rotation of chatoyant
polygons was always organized around the
central point. In all those cases chatoyant
polygons were initialized as regular polygons
with the unlimited possibilities for further
reconfiguration. “Central point” which was
originally the center of a regular polygon and
was marked by a special color could be moved
anywhere inside and even outside the original
perimeter, but rotation was always organized
around this point. There is the
Form_Polyline.cs example (figure 4.2 in the
Fig.15.41 Context menu on polygons
chapter Rotation) in which the center of
rotation can be easily moved around the screen, but I do not remember other examples in which the center of rotation can be
decided by user. Maybe I have such an example somewhere else, but I cannot remember it. Anyway, the change of rotation
center by users can be easily organized for any object, so I decided to demonstrate it here. The algorithm for rotation works
regardless of the exact rotation center: radii for all the basic points and their compensation angles are calculated at the
starting moment of rotation and are not changed throughout the rotation.
There are two possibilities for rotation center of the ChatPoly_Regulated objects:
• It can be an original “central” point even moved to any position.
• It can be a geometrical center of rectangle which is calculated around all vertices and “central” point; this rectangle
is returned by the ChatPoly_Regulated.RectAround property.
new public RectangleF RectAround
{
get
{
RectangleF rc = Auxi_Geometry .RectAroundPoints (ptVertices);
return (RectangleF .FromLTRB (Math .Min (m_center .X, rc .Left),
Math .Min (m_center .Y, rc .Top),
Math .Max (m_center .X, rc .Right),
Math .Max (m_center .Y, rc .Bottom)));
}
}
With this possibility to select the rotation center, there is some small addition in the
ChatPoly_Regulated.StartRotation() method, but these are the changes which I have already explained a bit
earlier.
• First, the real rotation center is calculated.
• Then radii and angles for all vertices are calculated.
• If the rotation is set to go around the geometric center, then the distance and angle from geometric center to the
“central” point are also calculated.
World of Movable Objects 469 (978) Chapter 15 Groups
public void StartRotation (Point ptMouse)
{
ptGeometricCenter = Auxi_Geometry .Middle (RectAround);
PointF ptRotationCenter =
(rotationcenter == PolyRotationCenter .GeometricCenter) ?
ptGeometricCenter : m_center;
double angleMouse = Auxi_Geometry .Line_Angle (ptRotationCenter, ptMouse);
for (int i = 0; i < ptVertices .Length; i++)
{
radii [i] = Auxi_Geometry .Distance (ptRotationCenter, ptVertices [i]);
angles [i] =
Auxi_Geometry .Line_Angle (ptRotationCenter, ptVertices [i]);
compensation [i] = Auxi_Common.LimitedRadian (angleMouse - angles [i]);
}
if (rotationcenter == PolyRotationCenter .GeometricCenter)
{
radiiToCenter = Auxi_Geometry .Distance (ptGeometricCenter, Center);
angleToCenter = Auxi_Geometry .Line_Angle (ptGeometricCenter, Center);
compensationToCenter =
Auxi_Common .LimitedRadian (angleMouse - angleToCenter);
}
}
Values calculated in the StartRotation() method are used in the ChatPoly_Regulated.MoveNode()
method. For vertices, there is no difference from other classes of chatoyant polygons, but if rotation is going around the
geometric center, then position of the original “central” point must be calculated in a similar way.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
… …
else if (catcher == MouseButtons .Right && bRotate)
{
PointF ptRotationCenter =
(rotationcenter == PolyRotationCenter .GeometricCenter) ?
ptGeometricCenter : m_center;
double angleMouse = -Math .Atan2 (ptM .Y - ptRotationCenter .Y,
ptM .X - ptRotationCenter .X);
for (int j = 0; j < VerticesNumber; j++)
{
ptVertices [j] = Auxi_Geometry .PointToPoint (ptRotationCenter,
Auxi_Common .LimitedRadian (angleMouse - compensation [j]),
radii [j]);
}
if (rotationcenter == PolyRotationCenter .GeometricCenter)
{
m_center = Auxi_Geometry .PointToPoint (ptGeometricCenter,
Auxi_Common .LimitedRadian (angleMouse - compensationToCenter),
radiiToCenter);
}
DefineCover ();
return (true);
}
return (false);
}
There is one more interesting command in menu from figure 15.41. The third menu line from bottom slightly changes its
text and significantly the reaction on pressing this line depending on whether menu is called for regular or chatoyant
polygon. Any regular polygon has only one color, so in such case menu contains a Color command to call a standard dialog
to select this sole color. Chatoyant polygon uses a set of colors, so the Colors command of menu opens an auxiliary
World of Movable Objects 470 (978) Chapter 15 Groups
Form_Colors_ChatoyantPolygon.cs (figure 15.42). Though this form looks a bit similar to another auxiliary form which
allows to define a new chatoyant polygon (figure 15.40a), there are some features which made the design of the new form
much more interesting and exciting.
When chatoyant polygon is initialized as a regular polygon and still has this shape, then it is easy to place additional small
circles for setting colors; in the Form_AddChatoyantPolygon.cs (figure 15.40a) all these circles are placed at the radii
from central point to vertices. When you
have such predetermined shape of a figure
(regular polygon) and few other simple
objects like buttons and information, it is not
a problem to design such form according to
all rules of user-driven applications. When
the form is closed, positions and sizes of all
its elements are saved; the next time this
form is restored in exactly the same view.
The Form_Colors_ChatoyantPolygon.cs
deals with already existing polygons of an
arbitrary shape. Each time the existing
polygon has to be copied into the auxiliary
form; this causes problems in overall form
design and in placement of its elements.
Look at figure 15.38 at which the page of
polygons is shown; one chatoyant polygon is
stretched more horizontally while another
one – vertically. Certainly, when an
auxiliary form for color change is opened,
then not the exact size but only the shape of
the original polygon must be copied, so some Fig.15.42 Form_Colors_ChatoyantPolygon.cs
scaling is always used. No scaling will allow
both horizontally and vertically stretched figures to look good inside the form of the fixed size; the shape of the form must
be adjusted to the shape of the currently tuned polygon, but this means that the exact restoration of the form will not work.
The Form_Colors_ChatoyantPolygon.cs restores neither the form size from the previous session nor the exact positions of
elements. The rectangular area of the original polygon is calculated. In the opened form, polygon is scaled into similar
polygonal area with the lesser dimension equal to 300 pixels; some additional space is added on all four sides. For buttons
and information area, the positioning coefficients in relation to the form area are saved. The next time these coefficients are
used to place elements
in the form of the new
shape. It is possible
that for significant
change of the form
proportions from one
case to another these
positioning
coefficients will give
you not the best places
of elements, but they
guarantee that no
element will be lost
behind the form
borders; after it much
better view of the form
can be organized in
two – three seconds by
several movements
and resizing.
Another problem is
with those small
circles which are used
Fig.15.43 The page of circles
World of Movable Objects 471 (978) Chapter 15 Groups
to change the colors. It was easy to place them all on radii and slightly outside from vertices in case of regular polygon
(figure 15.40a). In such way they all look good; no circle is lost from view because of the poor background; no circle
movability is needed. An attempt to use the same technique of placing circles in case of an arbitrary polygon (figure 15.42)
can produce not the best result as some circles may have the same color as the background around them. For such case, the
movability of circles will be very helpful; this is done in the Form_Colors_ChatoyantPolygon.cs. You can see from
figure 15.42 that some of the small circles are still on the lines from central point to vertices; other circles are moved to the
places where their colors can be seen much better.
All colors of a chatoyant polygon can be changed only individually. For the situation with many different colors, it would
be nice to have, in addition to individual tuning, some quicker mechanism of changing several colors simultaneously. I
have included such mechanism and it is designed on the basis of an ArbitraryGroup object. Do you remember that
the main idea of including the Form_ObjectsOnTabControl.cs example into this section was to demonstrate a wide use of
the ArbitraryGroup class? But this quick setting of several colors is used for other graphical objects, so let us move
to another tab page.
Figure 15.43 shows one more view of the Form_ObjectsOnTabControl.cs; this is the tab page which contains circles.
Two different classes of circles are used on this page:
• Unicolor circles with a crooked line inside; a line is added only to make the rotation of such circles obvious.
• Multicolor circles with sliding partitions.
In the way similar to the page with polygons, all circles at this page are organized into a List of RegulatedCircle
objects, but each of them is initialized either as Circle_Unicolor_Regulated or
Circle_Multicolor_Regulated.
List<RegulatedCircle> circles = new List<RegulatedCircle> ();
public class RegulatedCircle
{
RegulatedCircle_Type figure;
Circle_Unicolor_Regulated circleUnicolor;
Circle_Multicolor_Regulated circleWithPartitions;
Unicolor circles have three Boolean fields to regulate forward movement, resizing, and rotation.
public class Circle_Unicolor_Regulated : GraphicalObject
{
PointF center;
float radius;
double angle;
Color color;
bool bMoveForward;
bool bResize;
bool bRotate;
This class has the simplest possible cover to provide all needed movements: it is enough to have a single node in the cover
of non-resizable circle while for resizable circle two nodes are used. If for some circle both forward movement and rotation
are switched OFF, then the cursor over the big node is changed.
public override void DefineCover ()
{
CoverNode [] nodes;
if (bResize)
{
nodes = new CoverNode [] {
new CoverNode (0, center, radius - delta, Cursors .SizeAll),
new CoverNode (1, center, radius + delta)};
}
else
{
nodes = new CoverNode [] {
new CoverNode (0, center, radius, Cursors .SizeAll) };
}
if (!bRotate && !bMoveForward)
{
World of Movable Objects 472 (978) Chapter 15 Groups
nodes [0] .Cursor = Cursors .Default;
}
cover = new Cover (nodes);
cover .SetClearance (false);
}
Inside the DefineCover() method those two Boolean variables – bForwardMovement and bRotate – are used
only to change the mouse cursor in one special case; inside the MoveNode() method the same variables are used to
regulate two different movements.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
if (bMoveForward)
{
Move (dx, dy);
}
… …
}
else if (catcher == MouseButtons .Right && bRotate)
{
double angleMouse = Auxi_Geometry .Line_Angle (center, ptM);
angle = angleMouse - compensation;
bRet = true;
}
return (bRet);
}
Multicolor circles of the Circle_Multicolor_Regulated class have the same three Boolean variables to regulate
forward movement, resizing, and rotation. There is one more Boolean variable to regulate the moving of partitions between
color sectors.
public class Circle_Multicolor_Regulated : GraphicalObject
{
PointF center;
float radius;
double angle;
double [] vals;
double [] sector_angle;
List<Color> clrs = new List<Color> ();
Rotation dirDrawing;
bool bShowInnerBorders;
Pen penInnerBorders;
bool bResize;
bool bRotate;
bool bMoveForward;
bool bMovePartitions;
For some unknown reason (maybe it was my laziness?) the cover of
this class was not simplified and it is still a classical N-node cover,
but the work of this class will not change if some day in the nearest
future I’ll substitute all those circular nodes along the border by a
sensitive ring. You can do it yourself because this was already
demonstrated in the previous examples of circles.
In many aspects the tab page with circles is similar to the page of
Fig.15.44 Form_AddCircle.cs contains two groups
polygons (figure 15.38). When you decide to add a new circle and
of the ArbitraryGroup class
World of Movable Objects 473 (978) Chapter 15 Groups
click the button, then a new form is opened; this Form_AddCircle.cs (figure 15.44) consists of two
ArbitraryGroup objects and looks (and works) in nearly the same way as analogous form for polygons.
Both big circles are movable, resizable, and rotatable; partitions in the multicolor circle are also movable. Controls inside
the groups are movable and resizable (small button is non-resizable), so you can change the default view of the form to
whatever you want.
Small circles around the big multicolor circle are objects of the Circle_Sample class. Three pages back I wrote about
the Form_Colors_ChatoyantPolygon.cs (figure 15.42) in which similar small circles of the same class were used as
normal movable objects. In the current form these small circles are unmovable, but their positions are changed with any
change of the multicolor circle. The small circles are purposely made unmovable at the moment of initialization; this is
done with the help of their Movable property.
private void ConstructSampleCircles ()
{
… …
for (int i = 0; i < sector_angle .Length; i++)
{
angle += sector_angle [i] / 2;
circle = new Circle_Sample (Auxi_Geometry .PointToPoint (ptC, angle,
dist), radiusSmall, circleMulticolor .GetSectorColor (i));
circle .Movable = false;
circles .Add (circle);
angle += sector_angle [i] / 2;
}
}
Small circles are normal objects which can be registered in the mover queue and then they can be easily detected by mover
when any of them is double clicked.
private void OnMouseDoubleClick (object sender, MouseEventArgs e)
{
if (mover .Release () && e .Button == MouseButtons .Left &&
mover .ReleasedSource is Circle_Sample)
{
Circle_Sample circle = mover .ReleasedSource as Circle_Sample;
long id = circle .ID;
int i;
for (i = 0; i < circles .Count; i++)
{
if (id == circles [i] .ID)
{
break;
}
}
… …
Groups in the Form_AddCircle.cs are normal ArbitraryGroup objects. Though each group is simple, it still requires
four special methods. Because groups are simple, those methods are also very simple. Here are the methods for the group
with unicolor circle (groupUnicolor).
groupUnicolor = new ArbitraryGroup (this, visparams, "Unicolor circle",
ElementsArea_groupUnicolor, DrawElems_groupUnicolor,
SynchroMove_groupUnicolor, IntoMover_groupUnicolor);
private Rectangle ElementsArea_groupUnicolor ()
{
return (RectangleF .Union (Rectangle .Union (scColor .RectAround,
scAddUnicolor .RectAround), circleUnicolor .RectAround));
}
World of Movable Objects 474 (978) Chapter 15 Groups
private void DrawElems_groupUnicolor (Graphics grfx)
{
circleUnicolor .Draw (grfx);
}
void SynchroMove_groupUnicolor (int dx, int dy)
{
scColor .Move (dx, dy);
scAddUnicolor .Move (dx, dy);
circleUnicolor .Move (dx, dy);
}
void IntoMover_groupUnicolor (Mover mv, int iPos)
{
mv .Insert (iPos, groupUnicolor);
mv .Insert (iPos, circleUnicolor);
mv .Insert (iPos, scColor);
mv .Insert (iPos, scAddUnicolor);
}
Let us return to the tab page with circles in the Form_ObjectsOnTabControl.cs
(figure 15.43). Menu can be called on any circle; this menu is similar to menu
on polygons but with an obvious difference in the upper group of commands:
instead of four commands, there is only one (figure 15.45). Use of this
command is explained a bit later; now let us look at one command from the Fig.15.45 Menu on circles
group at the bottom of menu.
When menu is called on a unicolor circle, it contains a Color command to change a single color with the help of a standard
dialogue. When menu is called on a multicolor circle, then it contains Colors command; pressing of this command opens an
auxiliary Form_Colors_Circle.cs. In this form color of any sector can be changed individually by double clicking on the
needed sector. There is also a mechanism for faster change of colors in a set of consecutive sectors; this is organized with
the help of an ArbitraryGroup object.
Fig.15.46a Default view of the Form_Colors_Circle.cs Fig.15.46b The same form after customization
Figure 15.46a shows a default view of the Form_Colors_Circle.cs, figure 15.46b shows the same form after
customization. Certainly, there can be an infinitive number of variants for customization of even such simple form. In
addition to rearranging the view of the group, it is possible to change the font (call a context menu at any empty place
outside the group) and to move the numbers of the sectors. This form was especially designed in such a way as to
demonstrate once more the use of the ArbitraryGroup class, but there are also other interesting features which I
would like to mention.
World of Movable Objects 475 (978) Chapter 15 Groups
• The group allows to apply the smooth change of colors for a range of sectors. The numbering of sectors goes in
the counterclockwise direction, but the circle itself can be rotated, so the starting sector with number zero can be
placed anywhere. Numbers From and To which determine the range of sectors to be changed can be set
independently, but the new coloring is always applied in the counterclockwise direction.
• The colored rectangle inside the group belongs to the ResizableRectangle class. I have used this class in
several examples while demonstrating sliders in resizable rectangle (figures 11.8 – 11.10) but more often I use this
class in tuning forms; further on in the chapter Applications for science and engineering I will demonstrate the use
of this class in design of different forms.
• The range for resizing of the colored rectangle is wide enough; each dimension can be changed between 20 and
400 pixels. This allows to stretch the rectangle either vertically or horizontally.
private void OnLoad (object sender, EventArgs e)
{
… …
rrColors = new ResizableRectangle (new Rectangle (100, 100, 40, 180),
new Size (20, 20), new Size (400, 400),
Show_Colors);
… …
• When the width of colored rectangle is bigger than its height, then the coloring of rectangle with the set of colors is
organized from left to right; when the height is greater than the width, then the coloring is done from top to bottom.
Controls with numbers From and To can be moved anywhere; if you swap the positions of these two controls, then
you will confuse only yourself.
• Numbers for sectors are organized as Text_Spotted objects. This class first appeared in the
Form_Texts_MoveAsSample.cs example in the chapter Texts to explain the synchronous rotation of texts by a
sample. In that old example, all objects of the Text_Spotted class were rotatable and for better explanation
were visualized with a set of specially colored spots. In the Form_Colors_Circle.cs, you do not see those spots on
Text_Spotted objects and all texts are non-rotatable.
textnum .ShowSpots = false;
textnum .Rotatable = false;
There is one more interesting feature of these texts which was only mentioned in description of that old example
but not explained in details. For any Text_Spotted object its rectangular area is calculated; then this area is
covered by a polygonal (rectangular) node which provides forward movement (and rotation, when it is needed) of
the text. By default any polygonal node has a Cursors.SizeAll shape of the cursor. The default cursor for all
circular nodes is Cursors.Hand, but because elements of the Circle_SlidingPartitions class are
usually big, then the cursor for its big circular node is changed to Cursors.SizeAll shape. In such situation
cursor over circle is the same as cursor over numbers of sectors. These numbers can be placed inside the circle
borders and then there will be no indication by the change of cursor that numbers can be moved independently of
circle. To signal about the possible independent move of numbers, I changed their cursor shape to
Cursors.Hand; this is done by the Text_Spotted.Cursor property when numbers are organized.
private void ConstructNumbers ()
{
numbers .Clear ();
Text_Spotted textnum;
PointF ptC = circle .Center;
double angle = circle .Angle;
double [] sector_angle = circle .GetSectorAngles ();
double dist = circle .Radius + fDistNumbersToCircle;
for (int i = 0; i < sector_angle .Length; i++)
{
angle += sector_angle [i] / 2;
textnum = new Text_Spotted (this,
Auxi_Geometry .PointToPoint (ptC, angle, dist),
TextBasis .M, i .ToString (),fntSelected, 0, ForeColor);
textnum .ShowSpots = false;
textnum .Rotatable = false;
textnum .Cursor = Cursors .Hand;
World of Movable Objects 476 (978) Chapter 15 Groups
numbers .Add (textnum);
angle += sector_angle [i] / 2;
}
}
• In the normal situation each Text_Spotted object moves individually, independently, and without any
restrictions. While using such objects for numbering the sectors of a circle in the Form_Colors_Circle.cs, I want
each number to be all the time only at the bisector of an associated sector and I want all numbers to be at the same
distance from the center of a circle. This means that when any number is moved, all other numbers have to move
synchronously and only along their radii. This is organized in two steps. First, when any number is caught for
movement by the left button, then a special variable (fDistMouseToNumbers) is calculated. This is a
difference between two distances of which one is calculated from circle center to the mouse and another from
circle center to the central point of the number.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is Text_Rotatable_Demo)
{
if (e .Button == MouseButtons .Left)
{
double fMouseRadius =
Auxi_Geometry .Distance (circle .Center,
e .Location);
fDistMouseToNumbers =
Auxi_Geometry .Distance (circle .Center,
numbers [0] .Location) - fMouseRadius;
}
}
}
ContextMenuStrip = null;
}
Throughout the movement of a caught number, the mouse position is known and the fDistMouseToNumbers
value allows to calculate the distance from circle center to the center of the moving number (and with it for all other
numbers because they move synchronously).
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
… …
else if (mover .CaughtSource is Text_Spotted &&
e .Button == MouseButtons .Left)
{
fDistNumbersToCircle = Auxi_Geometry .Distance (circle .Center,
e .Location) + fDistMouseToNumbers - circle .Radius;
PositionNumbers ();
}
… …
• The Form_Colors_Circle.cs is relatively simple, at least there are no complicated objects and not too many
objects at all, but all of them are movable and resizable. The same form with the same purpose could be organized
in the standard way (standard for all other applications!) by using only unmovable objects, but this would
noticeably worsen the form. If you try to combine fixed and movable elements in the same form, you will feel
immediately the discomfort of such design. These are the things which I am going to discuss in details further on
in the second part of this book.
World of Movable Objects 477 (978) Chapter 15 Groups
Discussion of different ArbitraryGroup objects in this section is over, but before moving to the next example I want
to mention one more feature of the Form_ObjectsOnTabControl.cs. There are five pages in tab control; initially there are
four graphical objects on each page, but the number of elements on any page can be easily changed by adding and deleting.
The new object is always placed on top of all existing; any object can be deleted regardless of its place in the queue, but
what about changing the order of elements? The movable objects can and often do overlap; in such case they close from
view some part of those objects which are underneath. The reordering of objects in this example is organized differently
depending on the page.
For lines, there is no reordering at all. It is easy to implement the reordering there, but from my point of view it is not
needed as all lines are thin and there is no significant overlapping of such elements.
The reordering of polygons is organized in a standard way which was already demonstrated in some of the previous
examples. Figure 15.41 shows that a context menu which can be called on any polygon contains four different commands
for reordering: two of them allow to move the pressed polygon one layer up or down; two others allow to move this polygon
to the top or to the bottom of a pile. I mentioned this set of four commands in menu as a “standard way” of reordering
graphical objects as this was used in some of the previous examples. Further on you’ll see the same four commands in the
examples with different types of plotting, for example, in scientific or financial applications. The forms in such applications
can work with significant number of plots, but the most frequently used command is “Put on top”, so this set of commands
is good enough.
Unfortunately, the same set of commands is not good for applications where it is often needed to move objects up or down
for different number of layers as this would require from user multiple calls of the same menu. To avoid such multiple calls
of menu and to make the whole process of reordering easier and more obvious, another solution can be used; in the
Form_ObjectsOnTabControl.cs it is demonstrated with triangles, rectangles, and circles. Instead of four mentioned
commands, menu on these objects contains a single command “Change levels…” which opens an auxiliary form.
Figures 15.47 demonstrate these forms: Form_Order_Triangles.cs, Form_Order_Rectangles.cs, and
Form_Order_Circles.cs. All three forms are organized in a similar way, but at figures 15.47 only one of them is shown in
full view with visible information while on two others the information is closed. Each form contains a
ResizableRectangle object with a set of elements on it. Rectangular area is movable and resizable, but it is resizable
only vertically. When such area is resized in cases of triangles and rectangles, the vertical size of small elements is also
changed; for the case of circles their radius is constant.
Siblings
Examples of this book demonstrate different types of relations between elements which are involved in movements.
• Groups with visible frames are often used for synchronous movement of inner elements. Those elements can be of
different classes and even of different origin, but their frame is usually the best indication that this set of elements
can be looked at as a single entity. Moving of a group by any inner point is the most expected behaviour of a
group; this is also the easiest way to move all elements synchronously.
• Relation between the parts of complex objects often belongs to the “parent – children” type. More often than not
the “parent” is set at the moment of initialization and its role is not changed throughout the lifetime of this element,
while “children” can be easily added or deleted at any moment. The exact movement of “children” in reaction to
some changes of the “parent” can vary from class to class, but the main idea is usually the same: position of each
“child” is determined by one or several coefficients at the moment of initialization; these coefficients are used to
determine the reaction of a “child” on any change of the “parent”.
• The case of a dominant control is very close to the previous one. Though I have demonstrated an example in
which any subordinate element can take the role
of a dominant element at any moment, I would
call it a rare case; more often is the situation
when the dominant element is declared from the
beginning and is not changed later. Usually the
dominant element is the biggest one in the group
and the behaviour of this dominant element is
slightly different from all subordinates.
In this section I want to discuss one more type of possible
relations between the elements. I decided to call them
“siblings”, because none of them differs from others; at the
same time they can be all involved in the individual
movements and also influence each others movements.
Only these involvements in individual or related
movements are determined not by the role of each element
in a group but by the part of each element at which the
movement is started. Visually some of examples in this
section are identical to what was demonstrated before; this
also shows that the same movements of objects can be
organized in absolutely different ways.
Rectangles
File: Form_Rectangles_Siblings.cs
Menu position: Groups – Siblings – Rectangles
In several cases I have already started the discussion of
new things with the examples of rectangles, so it is natural
to begin this section also with an example of rectangles Fig.15.48 Each rectangle can be moved, rotated, and resized.
(figure 15.48). Objects which are used in the Movement of any rectangle by its darker part
Form_Rectangles_Siblings.cs belong to the causes the synchronous movement of all siblings.
Rectangle_Sibling class.
public class Rectangle_Sibling : GraphicalObject
{
PointF [] pts = new PointF [4];
PointF [] ptsSmall = new PointF [4];
SolidBrush brush, brushSmall;
double angle;
double compensation; // only between MouseDown and MouseUp
PointF center; // only between MouseDown and MouseUp
bool bRotate;
List<Rectangle_Sibling> siblings = null;
World of Movable Objects 479 (978) Chapter 15 Groups
Rectangle has one basic point – it is its central point around which a rectangle can be rotated. It is natural for such rectangle
to be initialized by central point, two sizes, and angle.
public Rectangle_Sibling (PointF ptC, double w, double h, double angleDegree,
Color color)
{
bRotate = true;
w = Math .Max (minSide, Math .Abs (w));
h = Math .Max (minSide, Math .Abs (h));
angle = Auxi_Convert .DegreeToRadian (angleDegree);
double radius = Math .Sqrt (w * w + h * h) / 2;
double angle_plus = Math .Atan2 (h, w);
pts = new PointF [4] {
Auxi_Geometry .PointToPoint (ptC, angle + angle_plus, radius),
Auxi_Geometry .PointToPoint (ptC, angle - angle_plus + Math .PI,
radius),
Auxi_Geometry .PointToPoint (ptC, angle + angle_plus + Math .PI,
radius),
Auxi_Geometry .PointToPoint (ptC, angle - angle_plus, radius) };
PointsOfTheSmall ();
… …
The ninth part of each rectangle is a special part painted in similar but darker color than the whole rectangle. This smaller
rectangle is positioned next to the zero corner of the main object; numbering of corners is going counterclockwise from the
dark corner. Corners of this darker part are calculated by the PointsOfTheSmall() method.
private void PointsOfTheSmall ()
{
ptsSmall = new PointF [4] {
pts [0],
Auxi_Geometry .Line_ValueToPoint (pts [0], pts [1], 0.0, 1.0, 0.333),
Auxi_Geometry .Line_ValueToPoint (pts [0], pts [2], 0.0, 1.0, 0.333),
Auxi_Geometry .Line_ValueToPoint (pts [0], pts [3], 0.0, 1.0, 0.333) };
}
Cover of Rectangle_Sibling object is similar to the cover used in the Rectangle_AllMovements class from
the Form_Rectangles_AllMovements.cs (figure 4.4). Small circular nodes in the corners and narrow strip nodes along the
borders allow to resize a rectangle by any border point; the big polygonal node covering the whole area of an object allows
to move and rotate this object. The only new part in the cover of Rectangle_Sibling class is an additional rectangular
node which covers the darker rectangular part. Because this node is smaller than the big one and is positioned entirely
inside the area of the bigger node, then the smaller node must be placed ahead of the bigger one in the cover; otherwise
there will be no chances to press this node and use it in any way.
public override void DefineCover ()
{
CoverNode [] nodes;
nodes = new CoverNode [10];
for (int i = 0; i < 4; i++)
{
nodes [i] = new CoverNode (i, pts [i], radiusCorner);
}
for (int i = 0; i < 4; i++)
{
nodes [i + 4] = new CoverNode (i + 4, pts [i], pts [(i + 1) % 4]);
}
nodes [8] = new CoverNode (8, ptsSmall);
nodes [9] = new CoverNode (9, pts);
cover = new Cover (nodes);
}
Individual forward movement, rotation, and resizing of such rectangles were already described in the
Form_Rectangles_AllMovements.cs, so the code of this form and the code of the Rectangle_AllMovements class
World of Movable Objects 480 (978) Chapter 15 Groups
give all the needed samples to organize such movements. The new thing is the synchronous movements of siblings. To
organize such synchronous movements, the list of siblings is needed. There are two obvious places to keep such list.
• List of siblings can be organized at the upper level (at the level of the form).
• List of siblings can be kept inside the particular element which is caught by mouse (and thus by mover).
Because the only place where the required movement is defined in terms of the screen pixels is the MoveNode() method
of the caught object, then I prefer to use second variant.
When any rectangle is initialized, its list of siblings is empty; this list is needed only throughout the movement of rectangle,
so the list of siblings is populated only at the start of the movement; at the moment when an element is caught by mover.
In this example with rectangles I decided to demonstrate the synchronous participation of elements only in forward
movement, so the list of siblings is needed only if the left button is pressed on that special rectangular node with the number
eight (darker part of an object). Rotation of elements in this example is only individual, so list of siblings is not needed
when an element is caught by the right button.
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 Rectangle_Sibling)
{
Rectangle_Sibling rectPressed = grobj as Rectangle_Sibling;
iPressedNode = mover .CaughtNode;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode == 8)
{
long id = grobj .ID;
List<Rectangle_Sibling> rectsOther =
new List<Rectangle_Sibling> ();
for (int i = 0; i < rects .Count; i++)
{
if (id != rects [i] .ID)
{
rectsOther .Add (rects [i]);
}
}
rectPressed .Siblings = rectsOther;
}
}
else if (e .Button == MouseButtons .Right)
{
rectPressed .StartRotation (e .Location);
}
}
}
ContextMenuStrip = null;
}
The list of siblings is needed only on special occasion between the MouseDown and MouseUp events; this list must be
cleared when the mouse is released. Though the real clearing of this list is needed only in special case of ending a
synchronous movement, it is easier to clean this list on any release of any rectangle without further checks.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
World of Movable Objects 481 (978) Chapter 15 Groups
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Rectangle_Sibling)
{
(grobj as Rectangle_Sibling) .Siblings = null;
… …
The movements of any rectangle are described by its MoveNode() method. As it is organized in the majority of
previous examples, the exact movement is determined by the number of the caught node. The first eight nodes of the cover
for such rectangles are positioned in the corners and on the sides; these nodes are responsible for resizing. Of the main
interest in this case are two other nodes; one of them covers the special part of the full area (node number eight); another
one covers the whole area (node number nine).
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
if (catcher == MouseButtons .Left)
{
switch (iNode)
{
… …
case 8:
case 9:
Move (dx, dy);
bRet = true;
break;
}
… …
As you can see from this code, there is no difference in the MoveNode() method between the reaction on catching the
smaller ( i = 8) or the bigger ( i = 9 ) rectangular node. In both cases the caught object must be moved, so in both cases the
Move() method is called. But there is a difference for other rectangles, so this difference in reaction must be described
inside the Move() method.
Yes, the difference is really in the second part of the Rectangle_Sibling.Move() method; it is based on the fact
that the list of siblings for rectangle is not empty only if this rectangle was caught by its smaller rectangular node. If the list
of siblings is not empty, then the Move() method of each sibling from this list is called with exactly the same parameters;
these parameters describe the needed forward movement for the caught rectangle, so all siblings will make the synchronous
movement. At the same time the lists of siblings in all other rectangles are empty, so these calls do not spread anywhere
farther.
public override void Move (int dx, int dy)
{
SizeF size = new SizeF (dx, dy);
for (int i = 0; i < 4; i++)
{
pts [i] += size;
}
PointsOfTheSmall ();
if (siblings != null)
{
foreach (Rectangle_Sibling rect in siblings)
{
rect .Move (dx, dy);
}
}
}
I decided not to organize the synchronous rotations in this example; it can only make the code more complicated without
any other interesting things to look at. Such synchronous rotations were already demonstrated in case of the texts in the
example Form_Texts_MoveAsSample.cs (figure 5.8).
World of Movable Objects 482 (978) Chapter 15 Groups
For these sectors I use a cover with the minimal possible number of nodes and I also use the adhered mouse throughout the
resizing. It is easy to calculate the minimal number of nodes in the cover. To provide forward movement, rotation, and
resizing of the whole circle, two nodes are enough; this was already demonstrated with the Circle_SimpleCover class
(figure 12.1). To cut circle into sector, one or two transparent nodes are needed; the exact number of nodes depends on the
sector angle. To be absolutely correct, it was demonstrated not on circles, but on wide arcs (class
Arc_Wide_SimpleCover, figure 8.11), but even there it was used to cut out the part(s) of the big circle. I combine the
ideas from those two classes to write the CircleSector_Sibling.DefineCover() method. Covers for sectors of
the CircleSector_Sibling class are shown at figure 15.50. For better viewing, I even excluded the drawing of
sector border with wide pen.
In each case, there are two big circular nodes with
a small difference in size between them. Certainly,
the smaller node stays ahead; otherwise it will be
inaccessible. These two nodes are the last in the
cover. Depending on the sector angle, either one
(this is for sector angle greater than 180 degrees) or
two transparent nodes are needed.
Transparent nodes (one or two) are placed ahead of
non-transparent nodes in the cover. Inside the
MoveNode() method, different branches of code
describe the reaction on pressing non-transparent
nodes. The reaction on pressing the border in two Fig.15.50 Sector cover depends on sector angle
cases from figure 15.50 is the same, but the
numbers of these nodes are different: for the left case it is node number three; for the right case it is node number two. The
number of the pressed node is the first parameter of the MoveNode() method and different values of this parameter might
look like a problem. It is not a real problem, as the node for resizing is always the last one in the cover, so slightly different
expression can be used
if (i == cover .NodesCount - 1)
The variable number of transparent nodes is not a real problem but some inconvenience. I decided to eliminate this
inconvenience by adding an extra node in the right case. Instead of one transparent polygonal node, there are two identical
nodes. Because of this identity, their borders have the same coordinates and for the yellow sector at figure 15.50 you see
only one transparent node. This additional node does not require any extra calculations because the points for this polygon
are already calculated; this extra transparent node has no effect at all because its area is the same as of another transparent
node. Now for both cases the inner circular node (for forward movement and rotation) has number two and the outer
circular node (for resizing) has number three. In both cases we now have four nodes in the cover though you see only three
in the right case from figure 15.50.
The DefineCover() method is a bit lengthy to include its code into the book and there is no sense in doing it as the
calculation of transparent nodes was already explained earlier (look at figure 8.7 with some additional comments). To
shorten the piece of code, I’ll omit the calculations of corner points for polygonal nodes from figure 15.50 and show only
the main carcass of the DefineCover() method.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [4];
if (Math .Abs (angleSector) <= Math .PI)
{
// two transparent rectangles on the sides
… …
PointF [] ptsA = new PointF [] {… …};
PointF [] ptsB = new PointF [] {… …};
nodes [0] = new CoverNode (0, ptsA, Behaviour .Transparent);
nodes [1] = new CoverNode (1, ptsB, Behaviour .Transparent);
}
else
{
// two identical transparent polygons
… …
PointF [] pts = new PointF [] {… …};
World of Movable Objects 484 (978) Chapter 15 Groups
nodes [0] = new CoverNode (0, pts, Behaviour .Transparent);
nodes [1] = new CoverNode (1, pts, Behaviour .Transparent);
}
nodes [2] = new CoverNode (2, Center, m_radius - delta, Cursors .SizeAll);
nodes [3] = new CoverNode (3, Center, m_radius + delta);
cover = new Cover (nodes);
cover .SetClearance (false);
}
When sectors are constructed, there is no mentioning of siblings. The only influence of the future siblings is in calculation
of angles for all sectors because the sum of their angles must produce the full circle of 360 degrees.
private void PrepareSectors (PointF ptC, float rad)
{
int nVals = Convert .ToInt32 (numericUD_Sectors .Value);
double fSum = 0;
List<double> vals = new List<double> ();
double fVal;
for (int i = 0; i < nVals; i++)
{
fVal = rand .NextDouble ();
vals .Add (fVal);
fSum += fVal;
}
List<double> angles = new List<double> ();
foreach (double val in vals)
{
angles .Add (360.0 * val / fSum);
}
int jClr = rand .Next (100);
double angleStart = 0.0;
sectors .Clear ();
for (int i = 0; i < nVals; i++)
{
sectors .Add (new CircleSector_Sibling (this, mover, ptC, rad,
angleStart, angles [i],
Auxi_Colours .ColorPredefined (jClr +i)));
angleStart += angles [i];
}
}
All sectors of the circle in the Form_CircleComposedOfSectors.cs are individual objects, so each of them must be
registered in the mover queue.
private void RenewMover ()
{
mover .Clear ();
mover .Add (scHelp);
mover .Add (scRenew);
mover .Add (ccSectors);
info .IntoMover (mover, mover .Count);
foreach (CircleSector_Sibling sector in sectors)
{
mover .Add (sector);
}
if (bAfterInit)
{
Invalidate ();
}
}
I purposely demonstrated the covers of individual sectors (figure 15.50) and did not include the standard button for cover
visualization into the form itself. If I show the covers in the form (figure 15.49), then there will be a mess of lines over the
World of Movable Objects 485 (978) Chapter 15 Groups
circle because a lot of transparent nodes from different sectors will overlap. I want to remind that transparent nodes from
other objects have no effect on behaviour of any object which mover grabs for moving. For mover, any mess of transparent
nodes does not mean anything at all and only the first non-transparent node matters. Because the non-transparent parts of
sectors do not overlap but stay side by side without any gaps and in such way produce a circular sensitive area, then any
mouse press inside this area will start some movements. Let us look into these processes.
When any sector is pressed with a mouse (it means caught by the mover), then all other sectors are organized into a List
of siblings for the pressed one. In this example, and this is the difference with the previous one, a List of siblings is
needed regardless of whether a sector is caught by the left or by the right button because any possible movement of the
caught sector must be coordinated with all siblings.
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 CircleSector_Sibling)
{
CircleSector_Sibling sectorPressed = grobj as CircleSector_Sibling;
long id = grobj .ID;
List<CircleSector_Sibling> sectorsOther =
new List<CircleSector_Sibling> ();
for (int i = 0; i < sectors .Count; i++)
{
if (id != sectors [i] .ID)
{
sectorsOther .Add (sectors [i]);
}
}
sectorPressed .Siblings = sectorsOther;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode == 3)
{
(grobj as CircleSector_Sibling).StartResizing (e.Location);
}
}
else if (e .Button == MouseButtons .Right)
{
foreach (CircleSector_Sibling sector in sectors)
{
sector .StartRotation (e .Location);
}
}
}
}
ContextMenuStrip = null;
}
Rotation is organized in the standard way: at the starting moment the compensation angle is calculated and then this
compensation is used throughout the whole process of rotation. Though it is absolutely standard procedure, there is one
unique feature in this example. In all other examples the rotation starts when the right button is pressed inside an object;
then compensation angle is calculated for the pressed object and this object is rotated. In the current example of sectors –
siblings one sector is pressed, but rotation starts for all siblings, so for all sectors except the pressed one the compensation
angle is calculated for the mouse pressed outside an object to be rotated. For calculations, there is no difference at all, but I
want to underline that this is the only example of such type.
The set of siblings is needed only between the MouseDown and MouseUp events, so the second event must return this
set of siblings into null.
World of Movable Objects 486 (978) Chapter 15 Groups
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is CircleSector_Sibling)
{
(grobj as CircleSector_Sibling) .Siblings = null;
}
… …
Any sector can be involved in three different types of movement; in all three cases there is a synchronous movement of all
sectors.
• If any sector is pressed inside by the left button, then the whole circle must be involved in the forward movement
as an entity.
• If the arc of a sector is pressed by the left button, then the radius of this sector is going to change, but the radii of
all other sectors must be changed in exactly the same way. The possibility of resizing sectors by their arcs is
signaled to users by the wide line on the border of a circle (figure 15.49).
• If any sector is pressed inside by the right button, then the whole circle must be involved in rotation.
Moving of any object is described by its MoveNode() method; let us look at some details of all three possible movements.
Forward movement of sector starts when its first non-transparent node (iNode == 2) is pressed by the left button. As
usual, for forward movement of an object its MoveNode() method simply calls the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 2)
{
Move (dx, dy);
}
… …
The Move() method moves the pressed sector and, as we need the synchronous movement of all siblings, calls the same
method for all other sectors.
public override void Move (int dx, int dy)
{
m_center += new Size (dx, dy);
if (siblings != null)
{
foreach (CircleSector_Sibling sector in siblings)
{
sector .Move (dx, dy);
}
}
}
All sectors will be moved for the same number of pixels (dx, dy), so we’ll have the forward movement of the whole
circle. There is no infinitive chain of CircleSector_Sibling.Move() calls because only for the pressed sector the
List of siblings is not empty.
Resizing of sector starts when its last node (i == 3) is pressed. There is no checking of the node number in this case
because there are only two non-transparent nodes in the cover and the checking was already done in the previous case. For
resizing, the adhered mouse is used. When the mouse is pressed in the vicinity of border, then the cursor is shifted exactly
World of Movable Objects 487 (978) Chapter 15 Groups
on the border line and throughout the resizing the cursor position gives the exact radius value. The pressed sector gets the
new radius and all siblings are changed in the same way.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else // iNode == 3
{
PointF ptNearest =
Auxi_Geometry .NearestPointOnSegment (ptM, ptEndIn, ptEndOut);
m_radius = Convert .ToSingle (Auxi_Geometry .Distance (m_center,
ptNearest));
AdjustCursorPosition (ptNearest);
bRet = true;
if (siblings != null)
{
foreach (CircleSector_Sibling sector in siblings)
{
sector .Radius = m_radius;
}
}
}
… …
There is nothing unusual in rotation of any sector; the compensation angle was already calculated at the starting moment of
rotation and this compensation is used throughout the whole rotation. But we need the synchronous rotation of all sectors,
so all siblings must be informed about rotation.
public override bool MoveNode (int iNode, 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 (m_center, ptM);
angleStart = angleMouse - compensation;
bRet = true;
if (siblings != null)
{
foreach (CircleSector_Sibling sector in siblings)
{
sector .MoveNode (2, 0, 0, ptM, MouseButtons .Right);
}
}
}
return (bRet);
}
Siblings are informed about rotation with the help of the MoveNode() call. The first three parameters in this call to
siblings are not important; the only needed things are the mouse position and the mentioning of the right button. Because
the MouseButtons.Right parameter is included into the call to all siblings, then each sibling uses that part of the
MoveNode() method which is responsible for rotation. The individual compensation angles for all sectors were already
calculated, so each sector is rotated by using its own compensation angle and combined movements of all sectors produce
the rotation of the whole circle.
World of Movable Objects 488 (978) Chapter 15 Groups
Circles composed of individual sectors behave in exactly the same way as those circles which were designed as a single
object painted in different colors. Then why did I introduce another way of organizing similar circles? Further on I am
going to demonstrate slightly different type of multicolor circles. Such circle cannot be designed as a single object but only
as an object composed of similar parts. But before demonstrating such new circles, I need to show a sector which can be
involved in one more type of movement.
Slices of a pie
File: Form_Slices_Individual.cs
Menu position: Graphical objects – Basic elements – Sector of a circle – Individual slices
Sectors in the previous examples can be moved, resized, and rotated; rotation is always organized around the apex point of a
sector. In the example of a circle consisting of sectors, all the apices are positioned at the same point. However, there are
many different types of plots and it is possible that in some of the pie charts (multicolor circle but with additional textual
information) users will occasionally like to move some sectors out of the pie and / or enlarge them; both things attract more
attention to those special sectors (pie slices). If users would like to move and enlarge the pie slices then developer has to
provide such possibility. As a preliminary step in design of a pie with such features, let us design the slices for such pie.
When such sectors are united together to compose a circle, then each sector can be involved in four different movements.
• The whole circle can be moved together.
• Any sector can be moved away from the circle center or returned back to reinstall the undisturbed picture of a
circle. On both movements, no angles are changed and the sector apex moves along the bisector.
• Sector can be resized by its arc. This can be a resizing of a single sector or such resizing may cause the resizing of
the whole circle; both cases will be discussed in the next subsection when such sectors will compose a circle.
• Sector can be rotated by any inner point.
First two movements are started by the left button and this causes a problem of distinguishing these movements. I already
used one possible solution in the case of siblings–rectangles in which individual and synchronous movements were started
at different parts of object area; I am going to use the same technique with the circle sectors.
Sector is divided into two parts which provide two different movements started by the same left button. There is no
evidence that one of these movements is required more
often than another, so both parts have approximately equal
area. An easy way to have nearly equal areas is to have the
radius of inner part as 3/4 of the outer radius. To
distinguish two parts of a sector visually; the inner part of a
sector is painted with the required color while the outer part
is painted with the lighter version of the same color. If a
sector is involved in both movements, then inner part is
used to start the movement of the whole circle, while the
outer part of a sector is used to move it individually along
its bisector. It is possible that for some reasons the
individual movement of a sector is prohibited; in this case
there is no outer (lighter) part and the whole sector is
painted in one color.*
Figure 15.51 demonstrates the Form_Slices_Individual.cs
in which two objects of the Slice_Individual class
are used; only one of these slices – the green one – can be
moved along its bisector. Rotation and resizing of both
Fig.15.51 Objects of the Slice_Individual class
sectors in this form are organized only individually without
any effect on another sector.
public class Slice_Individual : GraphicalObject
{
protected Form form;
*
In both examples of siblings–rectangles and siblings–sectors the part responsible for synchronous forward movement is
painted with darker color but the proportions between the parts are different. In rectangle, the darker part occupies 1/9 of
the area (figure 15.48) while in circle sector it is approximately 1/2 part. There is no special meaning behind any of these
numbers; you can easily change one or both of them in any way you prefer.
World of Movable Objects 489 (978) Chapter 15 Groups
protected Mover supervisor;
protected PointF m_center;
protected PointF ptApex;
protected float m_radius;
protected double angleStart;
protected double angleSector;
SolidBrush brush;
SolidBrush brushLight;
Pen penMovableBorder;
bool bMovableAlongBisector;
float coefInner = 0.75f;
protected static float fMinRadius = 20;
int delta = 3;
int minDistanceCenterApex = 5;
In many aspects the Slice_Individual class is similar to the previously demonstrated classes of circle sectors, but
there are some new features which required some new fields in the class description. Some of these new fields get their
values at the moment of initialization; others are used only throughout the movement.* The most unusual feature of such
slice is the need of two points to describe its position. A circular pie combined of slices has a central point (m_center);
initially apexes of all slices are also positioned in this point. When any slice is moved outside, then position of its apex is
described by ptApex and to describe the movement and position of a slice both variables are needed. In the
Form_Slices_Individual.cs, there are only individual slices, but there is an unmovable point ptCenter which plays the
role of the pie center, so there are still the same two points and we can analyse all possible slice movements.
public Slice_Individual (Form frm, Mover mvr, PointF ptC, PointF ptAp,
float rad, double angleStart_Degree,
double angleSector_Degree, Color color, bool bMoveAlongBisector)
{
form = frm;
supervisor = mvr;
m_center = ptC;
ptApex = ptAp;
m_radius = Math .Max (fMinRadius, Math .Abs (rad));
angleStart = Auxi_Convert .DegreeToRadian (angleStart_Degree);
angleSector = Math .Min (Math .Max (-Math .PI * 2,
Auxi_Convert .DegreeToRadian (angleSector_Degree)), Math .PI * 2);
brush = new SolidBrush (color);
Color clrLighter = Auxi_Colours.IntermediateClr (0.33, color, Color.White);
brushLight = new SolidBrush (clrLighter);
penMovableBorder = new Pen (Color .DarkGray, 3);
bMovableAlongBisector = bMoveAlongBisector;
}
Cover for such slices is designed in the way similar to sectors from the previous example, but there is one extra node
because a sector can be involved both in individual and synchronous movements. Thus we receive such order of nodes.
Nodes 0 and 1 Two transparent nodes are used to cut out the unneeded part of a circle. For sectors not bigger than 180
degrees, there are two different rectangular nodes on sector sides. For sectors bigger than 180 degrees,
there are identical polygonal nodes.
Node 2 Circular node to cover the inner part by which the synchronous move of sectors is started. There is no
synchronous movement of sectors in the current example, so the left press on this node is simply ignored.
Node 3 Circular node to cover nearly the entire area of a circle; radius of this circle is several pixels (delta) less
than the slice radius. If the slice can be moved individually along its bisector, then approximately half of
its inner part is blocked by the previous circular node. If there is no individual movement of this slice,
then this node is entirely blocked by the previous node.
*
If you were using the same Slice_Individual class prior to version 701, then pay attention that the last Boolean
parameter in constructor has changed its meaning to the opposite one, so the new version with the true value will
produce the same effect as the previous version with the false value.
World of Movable Objects 490 (978) Chapter 15 Groups
Node 4 Circular node to cover the sector arc. Radius of this node is several pixels (delta) bigger than the slice
radius, so only a thin strip (2 * delta) along the arc is left unblocked by the previous circular node.
Because calculations of transparent nodes are the same as in several classes demonstrated earlier, I’ll show here only the
carcass of the Slice_Individual.DefineCover() method.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [5];
if (Math .Abs (angleSector) <= Math .PI)
{
// two transparent rectangles on the sides
… …
PointF [] ptsA = new PointF [] {… …};
PointF [] ptsB = new PointF [] {… …};
nodes [0] = new CoverNode (0, ptsA, Behaviour .Transparent);
nodes [1] = new CoverNode (1, ptsB, Behaviour .Transparent);
}
else
{
// two identical transparent polygons
… …
PointF [] pts = new PointF [] {… …};
nodes [0] = new CoverNode (0, pts, Behaviour .Transparent);
nodes [1] = new CoverNode (1, pts, Behaviour .Transparent);
}
float rInner = bMovableAlongBisector ? (m_radius * coefInner)
: (m_radius - delta);
nodes [2] = new CoverNode (2, ptApex, rInner, Cursors .SizeAll);
nodes [3] = new CoverNode (3, ptApex, m_radius - delta, Cursors .SizeAll);
nodes [4] = new CoverNode (4, ptApex, m_radius + delta, Cursors .Hand);
cover = new Cover (nodes);
cover .SetClearance (false);
}
On initialization, the apex point is positioned in the center of rotation. Later a slice can be moved along its bisector. There
is one restriction and one artificial help for such movement.
• Sector cannot move “over” the central point; thus, when a circle is combined of such sectors, no sector can overlap
with another one.
• When you try to reinstall the normal view of a circle by moving the apex point of any sector back into center, you
are not required to organize the exact equivalence of two points. It is enough to release a sector with its apex being
close enough to the central point; this is treated as an attempt to reinstall a circle and such apex point is adjusted to
the central point. The “close enough” vicinity is defined by the minDistanceCenterApex field; by default
it is equal to five pixels, but you can change this value.
Two sectors that you see in the Form_Slices_Individual.cs (figure 15.51) are used only for demonstration of individual
movements. Originally both sectors were initialized with apexes based on the central point; later one sector was rotated and
moved away from central point in order to prepare this figure.
private void OnLoad (object sender, EventArgs e)
{
… …
sliceGreen = new Slice_Individual (this, mover, ptCenter, ptCenter, 280,
-60, 60, Color .FromArgb (77, 255, 77), true);
sliceViolet = new Slice_Individual (this, mover, ptCenter, ptCenter, 200,
150, 25, Color .Violet, false);
… …
Different movements often require the calculation and storing of some parameters at the starting moment of such
movement; the call to special methods is always included into the OnMouseDown() method of the form. Any class of
objects involved in rotation has a StartRotation() method. Nearly always the StartResizing() method is
World of Movable Objects 491 (978) Chapter 15 Groups
called when the resizing starts. For our slices the call of the StartResizing() method is mandatory because I am
going to use the adhered mouse and at the start of resizing the cursor must be shifted exactly on the border. Surprisingly,
but there is another special method StartRadiusMovement() to be called when the movement of a slice along
bisector has to start; this mandatory call is due also to my idea of using the adhered mouse.
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 Slice_Individual)
{
Slice_Individual sector = grobj as Slice_Individual;
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode == 3)
{
sector .StartRadiusMovement (e .Location);
PrepareAuxiLines_AlongBisector (sector, e .Location);
Invalidate ();
}
else if (mover .CaughtNode == 4)
{
sector .StartResizing (e .Location);
PrepareAuxiLines_Resizing (sector, e .Location);
}
}
else if (e .Button == MouseButtons .Right)
{
iNodePressed = mover .CaughtNode;
sector .StartRotation (e .Location);
}
}
}
ContextMenuStrip = null;
}
Rotation was already discussed in several examples; for each of the
involved in rotation objects, the difference between the angle to the
mouse and the angle of the caught object has to be calculated in the
StartRotation() method. An unusual thing in case of the
Slice_Individual class is due to the kind of rotation in which such
sectors are involved. If prior to rotation the slice was moved aside from
the central point, then ptApex will be rotated around m_center, so
distance (rApex) and angle (angleBisector) from the central point
Fig.15.52 Slice moving along bisector.
to the apex point must be calculated and saved. Parallel tracks for arc corners and
public void StartRotation (Point ptMouse) mouse cursor.
{
double angleMouse = Auxi_Geometry .Line_Angle (m_center, ptMouse);
angleBisector = Auxi_Common .LimitedRadian (angleStart + angleSector / 2);
compensation = Auxi_Common .LimitedRadian (angleMouse - angleBisector);
rApex = Auxi_Geometry .Distance (m_center, ptApex);
}
Moving of a slice along its bisector can be started only when the sector is divided into two parts for different types of
movement and when outer part of such sector is pressed, so it is started by pressing the node number three. I use the
adhered mouse technique throughout this movement. The bisector line is going from center through the apex point; apex
has to move only along this line; the angle of this line is angleBisector. The adhered mouse and the arc end points
World of Movable Objects 492 (978) Chapter 15 Groups
will move in parallel to this line. For better visual control of this movement, you see four parallel lines – these are the
tracks for mentioned points.
To use the adhered mouse technique, I need to calculate the end points of the line segment along which the adhered mouse
can move; this is the second from bottom line at figure 15.52. Only one end point of this line is crucial because another one
is placed somewhere in infinity. To calculate crucial end point for mouse movement, I need the angle of bisector line, the
mouse position at the starting moment, and distance and radius from the apex point to the mouse at the same starting
moment. The crucial end point of the cursor track is in the same relative position to the central point as the mouse position
to the apex point at the starting moment. There is one non-standard aspect of using adhered mouse for this movement: there
is no adjustment of the cursor position at the starting moment but only throughout this movement.
public void StartRadiusMovement (Point ptMouse)
{
angleBisector = Auxi_Common .LimitedRadian (angleStart + angleSector / 2);
angleApexToMouse = Auxi_Geometry .Line_Angle (ptApex, ptMouse);
distanceApexToMouse = Auxi_Geometry .Distance (ptApex, ptMouse);
ptEndIn = Auxi_Geometry .PointToPoint (m_center, angleApexToMouse,
distanceApexToMouse);
ptEndOut = Auxi_Geometry .PointToPoint (ptEndIn, angleBisector, 4000);
}
When the end points of segment are known, then the mouse movement along this segment is easy. At any moment the
cursor takes the nearest position on the allowed segment and then the apex point is easily calculated because the relative
positioning of apex and mouse does not change.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 3)
{
// outer part of circle
PointF ptNearest =
Auxi_Geometry .NearestPointOnSegment (ptM, ptEndIn, ptEndOut);
AdjustCursorPosition (ptNearest);
ptApex = Auxi_Geometry .PointToPoint (ptNearest,
angleApexToMouse + Math .PI, distanceApexToMouse);
bRet = true;
}
… …
Slice resizing also uses the adhered mouse, but this is nearly a classical case of using this technique with initial mouse shift
to the nearest border point.
public void StartResizing (Point ptMouse)
{
angleBeam = Auxi_Geometry .Line_Angle (ptApex, ptMouse);
AdjustCursorPosition (Auxi_Geometry .PointToPoint (ptApex, angleBeam,
m_radius));
ptEndIn = Auxi_Geometry .PointToPoint (ptApex, angleBeam, fMinRadius);
ptEndOut = Auxi_Geometry .PointToPoint (ptApex, angleBeam, 4000);
}
This slice resizing is similar to arc resizing in the Form_Arcs_FullyChangeable.cs (figure 11.27). Three auxiliary lines at
figure 15.53 show the tracks for arc end points and cursor; all three go along the radii. The crucial end point of the cursor
track is calculated from the known angle to the mouse (angleBeam) and the minimum allowed slice radius
(fMinRadius). The mouse position is adjusted to the slice border at the starting moment of resizing; after it the changing
distance between apex and mouse gives the exact value of the slice radius.
World of Movable Objects 493 (978) Chapter 15 Groups
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == 4)
{
// border
PointF ptNearest =
Auxi_Geometry .NearestPointOnSegment (ptM, ptEndIn, ptEndOut);
AdjustCursorPosition (ptNearest);
m_radius = Convert .ToSingle (Auxi_Geometry .Distance (ptApex,
ptNearest));
bRet = true;
}
… …
The last touch is done when a slice is released by mover with apex point being
in the immediate vicinity of the central point; in this case its position is adjusted
by the Slice_Individual.AdjustToCenter() method which moves
the apex point exactly on the central point.
We have the working class of slices; now we can move on and design a pie
composed of such sectors–siblings.
File: Form_Pie.cs
Menu position: Groups – Siblings –Pie
Fig.15.53 Lines show the tracks for arc
An object that you can see in the Form_Pie.cs combines the ideas that were end points and mouse cursor
introduced in the previous examples. A multicolor circle (figure 15.54) is throughout slice resizing.
composed of sectors of the PieSlice class.
public partial class Form_Pie : Form
{
List<PieSlice> slices = new List<PieSlice> ();
The PieSlice class inherits everything from the Slice_Individual class and adds the ability to have siblings.
public class PieSlice : Slice_Individual
{
List<PieSlice> siblings = null;
When the pie is organized, it looks like an ordinary multicolor circle. Slices with an angle less than 180 degrees are painted
in two hues of the same color; slices with bigger angle are unicolored. Slices individually and the whole pie can be
involved in different movements. Some of them were already demonstrated in the previous example, but there are more
opportunities when there are siblings around.
If a slice is painted in two colors, it is an indication that such slice can be moved individually along its bisector by pressing
the light (outer) part. When the apex point of a slice is placed on the central point of a pie, then this feature of a slice (fixed
– unfixed) can be changed via the command of context menu. For a slice which is moved aside from the center, this
command in menu is disabled.
By pressing the darker part of any slice, the whole pie can be moved around.
There are no questions that resizing starts by border, but there are two possible resizings – of a single slice and of the whole
pie – and there is a problem to distinguish the commands for these movements. Everyone would prefer to use the
commands which are obvious and absolutely natural; unfortunately, this is the situation without such obviousness and any
solution will look artificial.
If all slices have the same radius and together look like a circle (this is an initial pie view), then an attempt to move any
border point will look like a command for pie resizing. If a slice is somewhere aside from others, like blue slice at
figure 15.54, then pressing of its border looks like a command for individual resizing. What about green, orange, or cyan
World of Movable Objects 494 (978) Chapter 15 Groups
slices from the same figure? When you press the border of one of these slices, do you expect an individual resizing or
zooming of the whole pie? There can be several ideas about solving this problem.
• Part of an arc can be used for resizing the particular sector; the remaining part can be used for resizing the whole
set of sectors. This will make the code much more complicated (different nodes on the arc have to react in
different way) and there is a problem of adding some visual marks to the border in order to distinguish those parts.
This idea was rejected.
• Using of some additional key from the keyboard in combination with the mouse. I try to avoid the request for
anything additional to the mouse and up till now I solved all the problems without using the keyboard. Also users
have no idea about the possible additional key. This idea was also rejected.
• Here is the solution which is implemented
now in the Form_Pie.cs: if the apex point
of a sector is based on the central point of
the pie (the slice is not moved outside)
then its resizing changes proportionally
the radii of all sectors; if the sector is
moved outside, then it is an individual
resizing of this sector.
The last movement to discuss is rotation. The
easiest would be to start the synchronous rotation
of all slices regardless of the point of the mouse
press and in such way rotation was organized in the
previous versions. Now rotation is made consistent
with forward movement, so the whole pie rotates
when the darker part of any slice is pressed, while
pressing of the lighter part rotates a single slice.
This decision required an addition of one command
to menu on slices; I’ll explain this at the end of this
section.
When the rules for all the needed movements are
set, it is time to look into the code. Any movement
is started by catching one or another object by a
mouse, so everything starts from the Fig.15.54 This multicolor pie is composed of slices
OnMouseDown() method. Some of the
movements are going to be individual but in a lot of cases the movement of one sector affects all other siblings, so this
method starts with organizing a List of siblings.
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 id = grobj .ID;
List<PieSlice> slicesOther = new List<PieSlice> ();
for (int i = 0; i < slices .Count; i++)
{
if (id != slices [i] .ID)
{
slicesOther .Add (slices [i]);
}
}
slicePressed .Siblings = slicesOther;
… …
World of Movable Objects 495 (978) Chapter 15 Groups
Three different movements can be started by pressing a slice with the left button; these three movements are started at nodes
2 (dark part), 3 (light part), and 4 (arc). The movement started at node number two does not require any calculations at the
starting moment, but two other cases require. By pressing node three, you start moving the pressed slice along its bisector;
the StartRadiusMovement() method is the same as in the previous example and it is perfectly illustrated by
figure 15.52.
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;
… …
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtNode == 3)
{
slicePressed .StartRadiusMovement (e .Location);
}
else if (mover .CaughtNode == 4)
{
if (slicePressed .Apex == slicePressed .Center)
{
foreach (PieSlice slice in slices)
{
slice .SaveRadius ();
}
}
slicePressed .StartResizing (e .Location);
}
}
… …
By pressing node four, you start the resizing. The StartResizing() method is the same as was used in the previous
example and it is illustrated by figure 15.53, but if the pressed slice has its apex point on the central pie point, then all slices
will be resized, so the initial radii for all slices must be saved.
foreach (PieSlice slice in slices)
{
slice .SaveRadius ();
}
Preliminary calculations are over at the starting moment of any movement; the movement itself is described by the
PieSlice.MoveNode() method. As I said, three different movements are started by pressing three different nodes with
the left button.
When the dark part of a slice is moved (node 2), then the whole pie is moved without any distortions. For the pressed slice
it means that the MoveNode() method simply calls the Move() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 2)
{
// inner circle
Move (dx, dy);
}
… …
World of Movable Objects 496 (978) Chapter 15 Groups
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
if (siblings != null)
{
foreach (PieSlice sector in siblings)
{
sector .Move (dx, dy);
}
}
}
As you can see from the code of the PieSlice.Move() method, it first calls the Move() method of the base class. This
method was not discussed with the previous example because such movement was purposely banned in the previous
example. The Slice_Individual.Move() method synchronously changes positions for apex and central point.
public override void Move (int dx, int dy)
{
m_center += new Size (dx, dy);
ptApex += new Size (dx, dy);
}
After moving the pressed slice, the PieSlice.Move() method calls the same method for all siblings and they are also
moved. The stack of calls is not overfilled because for all other slices except the pressed one the List of siblings is empty.
When the light part of a slice is moved (node 3), then only the pressed slice is moved along its bisector. There is nothing
new in this movement and only the corresponding part of the base Slice_Individual.MoveNode() method is used.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == 3)
{
// outer circle
base .MoveNode (iNode, dx, dy, ptM, catcher);
bRet = true;
}
… …
Resizing (node 4) starts with calling the corresponding part of the base method which provides the resizing of the pressed
slice. If its apex is positioned on the pie central point, then all siblings are zoomed with exactly the same coefficient. That
is where the initial radii for all slices are used.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else // iNode == 4
{
// border
base .MoveNode (iNode, dx, dy, ptM, catcher);
bRet = true;
if (siblings != null && m_center == ptApex)
{
float coef = radiusResizingStart / m_radius;
foreach (PieSlice sector in siblings)
World of Movable Objects 497 (978) Chapter 15 Groups
{
sector .Zoom (coef);
}
}
}
… …
// ------------------------------------------------- Zoom
public void Zoom (float coef)
{
m_radius = radiusResizingStart / coef;
}
Rotation is organized by the right button. Rotation of the pressed slice is provided by the base class, while for synchronous
rotation of all siblings there is an additional piece of code in the PieSlice.MoveNode() method. I mentioned before
that synchronous rotation takes place only if rotation starts at the dark part of any slice (node 2), in this case for each sibling
the MoveNode() method is called with the MouseButtons.Right parameter.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
}
else if (catcher == MouseButtons .Right)
{
base .MoveNode (iNode, dx, dy, ptM, catcher);
bRet = true;
if (siblings != null && iNode == 2)
{
foreach (PieSlice sector in siblings)
{
sector .MoveNode (2, 0, 0, ptM, MouseButtons .Right);
}
}
}
return (bRet);
}
Individual slice rotation might be useful in some cases, but… It is easy to destroy the ideal
position of slices side by side but it is not so easy to reinstall it manually. Eyes can work
perfectly and control the movements of the slices but there can be still some gaps between
the sides of the neighbouring slices. To place all slices at correct angles, use the last Fig.15.55 Menu on slice
command from context menu which can be called at any slice (figure 15.55).
The second command from the same menu allows to fix / unfix slices. The text of this command depends on whether the
pressed sector is currently fixed or unfixed, but the command is available only if the apex point of pressed slice is
positioned in the pie central point.
The Renew button eliminates all previous changes of the pie and produces a new pie in the form of ideal circle with new set
of angles.
World of Movable Objects 498 (978) Chapter 15 Groups
Fig.15.56 On the left is the ElasticGroup object; two others are of the Dominant_SolitaryControl class.
World of Movable Objects 499 (978) Chapter 15 Groups
File: Form_ElasticVsDominant.cs
Menu position: Groups – ElasticGroup vs. dominant-subordinates
It is one of those very rare examples in this Demo application where I use the classical dynamic layout. The reason is
simple – there is a single element in this form – a tab control. I can easily make this tab control resizable, declare a mover
at the form level, register this single element in the mover, and then you will have a chance to move and resize this control
as any other. At the same time the pages of this tab control are occupied with a lot of different elements and for better
viewing you will need to use the tab control of the big size. As big as possible, so you will resize this tab control up to the
borders of the form. In this case, there is no difference between resizing by the control borders or form borders. I always
say that dynamic layout is fairly good for a case of a single object.
Because of the big number of inner elements on the pages of this tab control, the current example is better used on the wide
screen. If you have not a big screen, reduce the sizes of inner elements in a standard way.
Tab control in this example consists of only two pages to demonstrate two already mentioned classes. Each page is
organized in the same way and has some information of the standard InfoOnRequest class plus three main objects.
• On the left is an ElasticGroup object.
• In the middle is the copy of the same group but its design is based on some new class.
• The right group is included to demonstrate some features of the new class which are not covered by the second
object.
Do not expect any reaction from clicking all these buttons or using other controls. This example has no other functionality
than to demonstrate the possibilities of resizing, moving elements around individually or together, and changing some
visibility parameters, especially the one – font – which affects the sizes. Let us start with the page which demonstrates the
Dominant_SolitaryControl objects (figure 15.56).
Several words about the ElasticGroup object which appears
on the left of this tab page. This group is taken from another
application which allows to organize a photo collection, to add
different information about pictures, and so on. Each picture can be
associated with some place. The place can be described in different
ways. There is a name which can be long enough and there is much
shorter abbreviation which can be used for more compact
displaying of information, like Wisconsin is often substituted by
WI. Places can be added, deleted, and their information can be
changed. The shown group is used to do all these changes.
Inner elements of ElasticGroup objects are movable and
resizable; there is also a standard tuning form for this class, so any
group can be easily modified according to the taste and current
preferences of any user. The change of the group view from Fig.15.57 Inner ElasticGroup object is used
figure 15.56 to figure 15.57 was done in a couple of seconds. I’ll
to add data into the ListView and to
skip the code lines for original relational positioning of elements
change this data.
inside the group and show only the lines for group construction.
ListView control and a small button are organized into a DominantControl object (dominState); several other
elements constitute inner ElasticGroup object (groupAddChangeState); then these two elements are united into
bigger ElasticGroup object (groupProvinces). The code below is for default view of the group as it is shown at
figure 15.56.
private void DefaultView_pageDSC ()
{
… …
DominantControl dominState = new DominantControl (
new Control [] { listProvince, btnDelete_Province });
CommentedControl ccNameState = new CommentedControl (this,
textName_Province, Side .N,
SideAlignment .Left, "Name");
CommentedControl ccAbbrevState = new CommentedControl (this,
textAbbrev_Province, Side .N,
SideAlignment .Left, "Abbreviation");
World of Movable Objects 500 (978) Chapter 15 Groups
ElasticGroup groupAddChangeState = new ElasticGroup (this,
new ElasticGroupElement [] {
new ElasticGroupElement (ccNameState),
new ElasticGroupElement (ccAbbrevState),
new ElasticGroupElement (new SolitaryControl (btnAdd_Province)),
new ElasticGroupElement (new SolitaryControl (btnChange_Province))});
groupProvinces = new ElasticGroup (this, new ElasticGroupElement [] {
new ElasticGroupElement (dominState),
new ElasticGroupElement (groupAddChangeState)},
"State, province, land");
… …
This group is from the application which is designed for my own use. It means that all parts (or nearly all because you
cannot exclude laziness from consideration) work in the way I want them to work. There is one feature of this group which
I do not like but which is impossible to change under the current rules of the ElasticGroup class. Two parts of the big
group – the DominantControl object and inner group – do not exchange the information about their boundaries. They
are independent inner elements of the bigger group and coexist without knowing about each other; if you want to enlarge
the ListView and the small group is in its way, you have to move this group also, otherwise they will overlap and
produce a mess on the screen. Moving of inner group is not a big problem, but even this can be avoided by switching from
using the outer ElasticGroup to the Dominant_SolitaryControl class.
This is a class for dominant – subordinate relation in which the dominant element is a control.
public class Dominant_SolitaryControl : SolitaryControl
Subordinates of four different classes can be used; second part of their names clearly indicates the classes from which they
are derived.
• Subordinate_SolitaryControl
• Subordinate_CommentedControl
• Subordinate_CommentedControlLTP
• Subordinate_ElasticGroup
New classes of subordinates are based on the classes which were already discussed in details. All those base classes have a
lot of constructors – from 10 for SolitaryControl class to 50 for CommentedControl class. On the contrary,
the classes of subordinates based on them have very few constructors. To be exact, each of these new classes has two
constructors: one allows to specify a set of parameters which is a copy from one variant of constructor for the base class;
another uses an object of the base class.
Four mentioned classes of subordinates are never mentioned throughout the construction of any new
Dominant_SolitaryControl object. You can see them later when, for example, a menu has to be called on any of
them, but at the moment of construction elements of their base classes like SolitaryControl or ElasticGroup are
organized; then these elements are organized into Lists and these Lists are used as parameters for constructor of the
main object. I skip again the code lines for positioning of controls and show only the lines with construction of elements for
the group (dominSC) from figure 15.58.
private void DefaultView_pageDSC ()
{
… …
CommentedControl ccNameStateD = new CommentedControl (this,
textName_ProvinceD, Side .N,
SideAlignment .Left, "Name");
CommentedControl ccAbbrevStateD = new CommentedControl (this,
textAbbrev_ProvinceD, Side .N,
SideAlignment .Left, "Abbreviation");
ElasticGroup groupAddChangeStateD = new ElasticGroup (this,
new ElasticGroupElement [] {
new ElasticGroupElement (ccNameStateD),
new ElasticGroupElement (ccAbbrevStateD),
new ElasticGroupElement (new SolitaryControl (btnAdd_ProvinceD)),
new ElasticGroupElement (new SolitaryControl (btnChange_ProvinceD))});
List<ElasticGroup> groups = new List<ElasticGroup> ();
World of Movable Objects 501 (978) Chapter 15 Groups
groups .Add (groupAddChangeStateD);
List<SolitaryControl> scs = new List<SolitaryControl> ();
scs .Add (new SolitaryControl (btnDelete_ProvinceD));
dominSC = new Dominant_SolitaryControl (listProvinceD,
scs, null, null, groups);
… …
Lists of future subordinates are sent as parameters into the
Dominant_SolitaryControl constructor. Only there the
subordinates of new classes are constructed and on initialization
each of them gets the rectangular area of the dominant element.
Position of any subordinate (the location of its top left corner) is
described by a pair of coefficients in relation to this rectangular
area of a dominant element. Whenever the dominant element is
moved or resized, each subordinate tries to retain the position
according to these two coefficients. If the relocation of any
subordinate brings it to such position that this subordinate is
entirely inside the area of the dominant element, then this
subordinate is forcedly relocated outside of it. For different
classes of subordinates there are some common features but there
are also some minor differences in the checking of the situation
for enforced relocation and the place for such relocation.
• The forbidden area is not exactly inside the boundaries of Fig.15.58 This Dominant_SolitaryControl
the dominant control but is slightly wider; I try to avoid object works similar to the previous
situation when any subordinate is allowed to look outside group
by one or two pixels only. In such case the subordinate is not blocked entirely, but at the same time it is hardly
seen and has to be grabbed by one or two pixels in order to move it to better position. If the forbidden area is
widened a bit, then minimal visible part of any subordinate is big enough to make the grabbing of this element
easier.
• For the Subordinate_CommentedControl and Subordinate_CommentedControlLTP objects
not their whole area is checked against the forbidden area but only the area of their controls.
• For the Subordinate_SolitaryControl objects the place for the enforced relocation is next to the top
right corner of the dominant element; for subordinates of other classes this place is next to the bottom left corner of
the dominant element.
• Whenever any subordinate is moved and released in such a way that the new position does not require the enforced
relocation, then this new position is automatically remembered as a default place for this particular subordinate in
case of the future enforced relocation.
The Dominant_SolitaryControl object from figure 15.58 is designed as close as possible to the ElasticGroup
from figure 15.56. What are the differences between two cases?
• There is no frame around the Dominant_SolitaryControl object, but usually it is obvious what belongs
to the group.
• When you have a group with a frame, such group can be moved around by any inner point. In the second case, the
whole group is moved around synchronously with the movement of the dominant element. This is the standard
feature for the dominant – subordinates group.
• There is no standard auxiliary form for tuning of the
Dominant_SolitaryControl objects, so such
tuning must be organized via the commands of context
menu(s). Such menu depends on situation. I decided that
in the current example such menu has to demonstrate only Fig.15.59 Menu on dominant element – the
variants of possible font change for dominant and ListView control from the previous
subordinate elements. Figure 15.59 demonstrates the figure
view of this menu when it is called on the ListVeiw
control from the group shown at the previous figure. There is one more group of the
Dominant_SolitaryControl class on the same tab page but its dominant control does not require the
change of font, so when the same menu is called on it, only the second command of menu is enabled.
World of Movable Objects 502 (978) Chapter 15 Groups
Form_DominantControls.cs (figure 15.12). In that example, it is enough to check one class of dominant elements and
one class of subordinates, so there is a direct call of one or another method when elements of these two classes are released.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is DominantControl)
{
(grobj as DominantControl) .CheckSubordinates ();
}
else if (grobj is SubordinateControl)
{
(grobj as SubordinateControl) .CheckLocation ();
}
groupOfDominant .Update ();
… …
There is similar problem of forbidden areas and possibility of enforced relocation for the
Dominant_SolitaryControl class but it is multiplied by the greater number of classes which can be used for
subordinate elements. Instead of checking all these classes one by one, I delegate the whole burden to mover which can use
one of its methods.
private void MouseUp_pageDSC (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
pageCurrent = sender as TabPageWithoutFlickering; // pageDSC;
if (mover_pageDSC .Release ())
{
GraphicalObject grobj = mover_pageDSC .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
mover_pageDSC .CheckDominantsSubordinates ();
groupProvinces .Update ();
pageCurrent .Invalidate ();
}
… …
In the current version of MoveGraphLibrary.dll the Mover.CheckDominantsSubordinates() method checks
the possibility of needed enforced relocation (and provides this relocation if needed) when any object from the next list is
released:
DominantControl
SubordinateControl
Dominant_SolitaryControl
Dominant_CommentedControl
Subordinate_SolitaryControl
Subordinate_CommentedControl
Subordinate_CommentedControlLTP
Subordinate_ElasticGroup
All these classes are included into MoveGraphLibrary.dll and it is not a surprise that the Mover class from the same
library takes the responsibility for their relocation in very special situations. Certainly, if you design other classes with
dominant – subordinates relation, then Mover class will not check those new classes automatically and you will have to add
your own code for checking similar situations with the needed enforced relocation.
In all three big objects from figure 15.56 some small controls with comments are used. User-driven applications allow to
change visibility parameters for any element; for comments it means changing of font and color. When a pair “control +
World of Movable Objects 504 (978) Chapter 15 Groups
comment” is used inside some ElasticGroup object, then those parameters can be changed via the tuning form of this
group, but such form changes simultaneously the parameters for all comments inside a group. If you need individual
change, and providing it is among the rules of user-driven applications, then such change is organized via the commands of
menu. (Though menu is often selected according to the class of the pressed element, it is easy to add more details by using
the id of the pressed element.) Pairs “control + comment” from figure 15.56 look similar, but they belong to different
classes.
private void MouseUp_pageDSC (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, e .Location);
pageCurrent = sender as TabPageWithoutFlickering; // pageDSC;
if (mover_pageDSC .Release ())
{
GraphicalObject grobj = mover_pageDSC .ReleasedSource;
… …
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
if (grobj is ElasticGroup)
{
elgrPressed = grobj as ElasticGroup;
pageCurrent .ContextMenuStrip = menuOnElasticGroup;
}
else if (grobj is CommentToRect)
{
cmntPressed = grobj as CommentToRect;
pageCurrent .ContextMenuStrip = menuOnComment;
}
else if (grobj is Subordinate_CommentedControlLTP)
{
sccPressed = grobj as Subordinate_CommentedControlLTP;
pageCurrent .ContextMenuStrip = menuOnSubordinateCC;
}
… …
• When you press some comment inside the group, you press a CommentToRect object and
then one menu is called (menuOnComment, figure 15.61a). This menu includes commands
only for individual change of the pressed comment. If you need similar change of all
comments in the group, call the tuning form for this group. Fig.15.61a
• When you press some comment shown at figure 15.60, then you press a
Subordinate_CommentedControlLTP object. This menu (menuOnSubordinateCC) has the same
commands for individual change of the pressed comment, but there is no tuning form for simultaneous change for
all pairs, so I added such command into menu (figure 15.61b). Three elements of the mentioned class are based on
tiny CheckBox controls; four others are based on TextBox controls; two
parts look differently, but when you use the last command from this menu,
then you spread the view of the pressed element on all seven siblings. If
you want to divide changes for these two subsets of commented controls,
you need to add more detailed checking into the
Click_miUseAsSampleForSiblings() method. Fig.15.61b
When the ElasticGroup object from figure 15.57 was substituted by a Dominant_SolitaryControl object
from figure 15.58 not only the frame around the group disappeared but also a title which was included into the original
frame. In order not to lose this valuable information, I included its text into the header of the ListView in the second
case, but a separate title in a bigger font looks better and would be more informative. This can be easily solved by
switching from the Dominant_SolitaryControl class to the Dominant_CommentedControl class. The use
of this class is demonstrated on the second page of our tab control (figure 15.62).
Left group at figure 15.62 was also taken from the mentioned application to work with photo archive. The main element of
this group is a ListView control which is accompanied by a set of smaller auxiliary controls. This is a classical
situation of dominant – subordinates relations when all elements are represented by controls. The set of controls is turned
World of Movable Objects 505 (978) Chapter 15 Groups
Fig.15.62 The left group is an ElasticGroup object; two other groups belong to the
Dominant CommentedControl class.
into the DominantControl object and an ElasticGroup is organized with this object as its single inner element.
This is so simple that one line of code is enough.
private void DefaultView_pageDCC ()
{
… …
groupBirds = new ElasticGroup (this, new DominantControl (ctrlsBirds),
"Groups of birds");
… …
The original ListView has no header; it does not need any header
because there is only one column and the title of the group clearly
indicates what is inside. I could easily turn this group into a
Dominant_SolitaryControl object, add a header to the
ListView, and put there the title of the left group. Instead, I use
Dominant_CommentedControl class which differs only in one
aspect: its dominant element is derived from the
CommentedControl class, so such group has a movable and
tunable title. Technically this title is associated with the ListView
control because this pair constitutes a CommentedControl
Fig.15.63 Dominant_CommentedControl
object, but visually this title belongs to the whole group, especially, if
object
the font for the title is enlarged as shown at figures 15.62 and 15.63.
Dominant_CommentedControl class is derived from the CommentedControl class.
public class Dominant_CommentedControl : CommentedControl
The new class allows to use subordinates of the same four types as Dominant_SolitaryControl class and the
construction goes in exactly the same way. Subordinates are organized into four lists some of which can be empty; these
lists are used as parameters for Dominant_CommentedControl constructor. In case shown at figure 15.63, there are
three elements in the list of solitary controls and one element in the list of groups. In the code below, I skip all lines for
setting positions, sizes, and visibility parameters; there are only lines for construction of subordinates and of the final object.
World of Movable Objects 506 (978) Chapter 15 Groups
private void DefaultView_pageDCC ()
{
… …
List<SolitaryControl> scs_2 = new List<SolitaryControl> ();
scs_2 .Add (new SolitaryControl (btnUp_BirdsDomin));
scs_2 .Add (new SolitaryControl (btnDown_BirdsDomin));
scs_2 .Add (new SolitaryControl (btnDelete_BirdsDomin));
ElasticGroup elgr_2 = new ElasticGroup (this,
new DominantControl (new Control [] { text_BirdsDomin,
btnAdd_BirdsDomin, btnChange_BirdsDomin }));
List<ElasticGroup> groups_2 = new List<ElasticGroup> ();
groups_2 .Add (elgr_2);
CommentedControl ccMain = new CommentedControl (this, listBirdsDomin,
Side .N, SideAlignment .Left, 4, "Groups of birds",
new Font ("Times New Roman", 12, FontStyle .Bold | FontStyle .Italic),
Color .Blue);
dominCC = new Dominant_CommentedControl (ccMain,
scs_2, null, null, groups_2);
… …
Because this new class is derived from the CommentedControl class, then its title cannot be hidden under the main
(dominant) control of the group; if you try to release the title in this forbidden area, it is automatically moved outside.
While turning the original ElasticGroup object (the left group from figure 15.62) into the new
Dominant_CommentedControl object, I made a small improvement which is impossible to do in the
DominantControl but is easily obtained in the new class. Three controls at the bottom of the original group clearly
constitute a small group, but in the DominantControl all the subordinates are equal and can be only individual
controls. However, in the new variant they are united into ElasticGroup and this group is a subordinate to the main
control.
This code demonstrates an interesting example of using both the old and the new classes of dominant elements.
• First, the “classical” DominantControl object is organized of those three controls.
new DominantControl (new Control [] { text_BirdsDomin,
btnAdd_BirdsDomin, btnChange_BirdsDomin }));
• On the next step this object is wrapped into an ElasticGroup object.
ElasticGroup elgr_2 = new ElasticGroup (this, new DominantControl (… …
• During the last step this group is used as a subordinate of the Dominant_CommentedControl object.
When you have a ListView as a dominant element, there is at
least a chance to put some general information about a group into
the header of this control. In many cases the information in the
header must be more specific and has to inform about the
particular columns of the list, but in some situations this trick can
work (figure 15.58). Controls like Panel have no headers and
this trick does not work; for such cases the switch to
Dominant_CommentedControl class is definitely a nice
solution. With this class, you are not restricted to have the general
title only at the top but have much more flexibility throughout the
design and work (figure 15.64).
One of the rules of user-driven applications declares that “All
parameters of visibility must be easily controlled by users”. The
change of fonts and colors does not interfere with the work of a
program and does not damage any data that an application gives
out to users. On the other side, the usefulness of any application
greatly depends on whether the information is provided in the
form that user likes or dislikes; in the last case the usefulness of Fig.15.64 Another
even a good program can tend to zero. Thus, user needs an easy Dominant_CommentedControl object
way to change all visibility parameters in any way he prefers.
World of Movable Objects 507 (978) Chapter 15 Groups
In the Form_ElasticVsDominant.cs, there are six different menus through which users can change the visibility
parameters. Some menus are used on different classes of objects on two tab pages but the view of these menus is the same
so I do not need to show them again. There is one more menu which I would like to mention. Menu on some object allows
to change the pressed object (and occasionally siblings).
Menu on the group allows to change all elements inside the
pressed group. Menu at empty places allows to change all
objects in the form (or on tab page); this is the case of menu
shown at figure 15.65. I purposely included such difference
between two commands that the first of them does not affect Fig.15.65 Menu at empty places
Helps on both pages.
In this form, I did not cover all the cases of changing the parameters but rather marked with different menus all or nearly all
the situations where such menus are needed. If you want, you can easily add a set of other commands to these menus; at the
moment all menus contain from one to three commands each; the majority of these commands is used to change a font
either for a single pressed element or for a whole group of elements.
Two classes of dominant elements demonstrated in this section – Dominant_SolitaryControl and
Dominant_CommentedControl – are very similar in design and behaviour; they differ only in absence / existence of
a comment for dominant element. Maybe one day I will combine them together and include a List of comments as
another type of subordinates into the new class. I did not do it yet only because I never ran into situation where more than
one comment would be needed. But maybe I will organize a single combined class to cover all the examples of this section.
World of Movable Objects 508 (978) Chapter 15 Groups
To demonstrate the use of new class, there is the Form_DominantComControlLTP.cs with two
Dominant_CommentedControlLTP objects (figure 15.66).
Dominant_CommentedControlLTP dominTexts, dominTicks;
One object has three subordinates, so two of them are added after the initialization.
ctrlsTexts = new Control [] { checkTexts, btnClr_Texts, btnFont_Texts,
btnFlipDrawingDirVer };
ctrlsTicks = new Control [] { posintTicksLen, btnFlipHorTicks };
private void OnLoad (object sender, EventArgs e)
{
… …
CommentedControlLTP cc =
new CommentedControlLTP (this, checkTexts, "Show texts");
… …
dominTexts = new Dominant_CommentedControlLTP (this, cc,
new SolitaryControl (ctrlsTexts [1]));
dominTexts .AddSubordinate (new SolitaryControl (ctrlsTexts [2]));
dominTexts .AddSubordinate (new SolitaryControl (ctrlsTexts [3]));
… …
Another object has only one subordinate, but I purposely use different control for dominant element in order to demonstrate
that the new class can be used not only with the CheckBox as dominant part.
cc = new CommentedControlLTP (this, posintTicksLen, "Ticks");
dominTicks = new Dominant_CommentedControlLTP (this, cc,
new SolitaryControl (ctrlsTicks [1]));
In the current Form_DominantComControlLTP.cs example, there is no checking of overlapping of dominant and
subordinate elements. Subordinates are controls while
the bigger part of dominant element is its graphical
part. Controls always move atop all graphical objects,
so if you want to place a subordinate partly or entirely
above dominant element, you are free to do it.
Subordinates of any
Dominant_CommentedControlLTP object are
placed in the mover queue ahead of their dominant
element, so there are no problems in moving them
regardless of their position. However, if you want to
implement some logic which prevents the overlapping
of dominant and subordinate parts, it is easy to do. At
first I did not want to add anything else to this Fig.15.66 Two objects of the
example but then I decided to demonstrate this easy
Dominant_CommentedControlLTP class
instrument to deal with overlapping.
In the previous examples where I wrote about overlapping, the detection of that overlapping was easy. When you deal only
with controls, then area of each element is determined by its visible border, and the overlapping is obvious. The bigger part
of dominant element of the new class consists of the graphical text and the visible part of this text is smaller than the area
calculated by the MeasureString() method. While checking the overlapping possibility, I use the results from this
method, so the overlapping of some button and graphical text is signaled if the button is not visually above the text but
somewhere near it. Do not forget about this small remark while checking the work of this example. Because the border of
rectangle calculated by the MeasureString() method is not visible, I added a small indicator to signal about
overlapping. When such indication is needed, a small red spot appears in the bottom right corner of the text.
The overlapping prevention starts in the OnMouseDown() method when any Subordinate_SolitaryControl
object is caught by mover. At this moment three values are calculated:
• area of dominant part (rcParent)
• coefficients describing the relative position of the caught subordinate (coefsReturn)
• point of special mark for indication of overlapping (ptSpot)
World of Movable Objects 510 (978) Chapter 15 Groups
private void OnMouseDown (object sender, MouseEventArgs e)
{
if (mover .Catch (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Subordinate_SolitaryControl)
{
Subordinate_SolitaryControl sub =
grobj as Subordinate_SolitaryControl;
rcParent = (sub .ParentID == dominTexts .ID) ?
(dominTexts as CommentedControlLTP) .RectAround
: (dominTicks as CommentedControlLTP) .RectAround;
coefsReturn = Auxi_Geometry .CoefficientsByLocation (rcParent,
sub .Control .Location);
ptSpot = new PointF (rcParent .Right, rcParent .Bottom);
}
}
}
Throughout the movement of subordinate, the flag of possible overlapping – bRedSpot –is calculated.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Subordinate_SolitaryControl)
{
Subordinate_SolitaryControl sub =
grobj as Subordinate_SolitaryControl;
Rectangle rc = Rectangle .Round (rcParent);
bRedSpot = Rectangle .Intersect (rc, sub .Control .Bounds) !=
Rectangle .Empty;
}
Invalidate ();
}
}
If needed, a small red spot arrives at the previously calculated point (figure 15.67).
There is no visual overlapping of the text and the button at the bottom of this figure, but
if the red spot appeared, then it means that that button crossed the border of dominant Fig.15.67
element.
private void OnPaint (object sender, PaintEventArgs e)
{
… …
if (bRedSpot)
{
Auxi_Drawing .FillCircle (grfx, ptSpot, 3, Color .Red);
}
}
If overlapping occurs when a subordinate element is released, then it must be returned to that initial position from which the
movement started. In any case there will be no overlapping, so the bRedSpot flag gets the false value.
private void OnMouseUp (object sender, MouseEventArgs e)
{
if (mover .Release ())
{
bRedSpot = false;
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is Subordinate_SolitaryControl)
{
World of Movable Objects 511 (978) Chapter 15 Groups
Subordinate_SolitaryControl sub =
grobj as Subordinate_SolitaryControl;
Rectangle rc = Rectangle .Round (rcParent);
if (Rectangle .Intersect (rc, sub .Control .Bounds) !=
Rectangle .Empty)
{
sub .ControlLocation = Auxi_Geometry .LocationByCoefficients
(rc, coefsReturn);
sub .UpdateCoefficients ();
RenewMover ();
}
}
Invalidate ();
}
}
World of Movable Objects 512 (978) Chapter 15 Groups
Big tab control is the only object in this form, so, as in the previous example, there is no mover at the form level, the tab
control is not resizable by its borders but its sizes are changed according to the form resizing. Each page of the tab control
has its own mover and all objects on the pages are movable.
Several features of the Form_ReferenceBook_Groups.cs work for all tab pages.
• Each page has its own information – a short Help – about the class of objects demonstrated on the page. This
explanation is an object of the UnclosableInfo class. Area can be big enough; you can move it closer to the
border in such a way that the main part of it will be outside and only a very thin strip of several pixels will be seen.
In such position the area will occupy only a tiny part of the page but can be returned into view at any moment.
Right click on the area with information opens a standard form for its tuning.
World of Movable Objects 513 (978) Chapter 15 Groups
• Half of the pages contain a small standard button to show the covers of demonstrated elements.
• The general menu of the form contains only one command to change a font; this font is used to change some
elements at different pages but not all elements. The majority of pages allow to use their own context menus and
many elements are modified through the commands of those menus. Objects of more complex classes, like
ElasticGroup or ArbitraryGroup, are modified with the help of their own tuning forms.
Now let us go quickly along the pages. The table below contains figures of all pages; next to each figure is the name of the
class demonstrated on the page and some information about the main features of this class.
SolitaryControl class
Individual controls are resized by special parts of the sensitive frame
around the border and are moved by other parts of the same frame.
Special places are next to the corners and in the middle of the sides.
Their existence and the cursor over them depend on the type of the
allowed resizing which is determined by the control size and the
values of its MinimumSize and MaximumSize properties.
Small graphical prompts can be shown at the places of resizing;
switch the covers OFF and use a check box on the page to switch
those prompts ON.
A disabled control can be moved by any inner point; this feature is not
Fig.15.69a demonstrated here but is used in other examples.
CommentedControl class
A “control + text” pair in which a comment can be placed anywhere
except entirely under the associated control; in this case the comment
is forcedly relocated outside the area of control.
When control is moved, its comment moves synchronously.
Throughout the resizing of a control, its comment retains the same
relative position.
Fig.15.69b Comment can be moved and rotated individually by any inner point.
CommentedControlLTP class
A “control + text” pair in which a comment can be placed only at one
of the 12 predetermined positions (three on each side of a control).
Such pair can be moved synchronously not only by the border of a
control but also by any point of comment.
There is no rotation of comments; they are always shown horizontally.
The relative position of comment can be changed, for example,
through a special additional form. Use context menu on comment to
Fig.15.69c call this form.
Fig.15.69d
World of Movable Objects 514 (978) Chapter 15 Groups
RigidlyBoundRectangles class
Any combination of rectangles can be united for synchronous
movement; some of rectangles can include controls; others can be
used for painting. There is no resizing for objects of this class.
There are no regulations for size or placement of rectangles: they can
stay side by side, far away from each other, or overlap in any possible
way. To exclude any inner gaps in the complex structure, an
additional rectangle over the united area of elements is often used.
Fig.15.69f
GroupBoxMR class
Group can be moved by any inner point and by the straight parts of
the frame line. The curves of the frame are the places to resize the
group; the form of these curves slightly depends on the type of
resizing. There are four different types of resizing; the one in use is
determined by one of the parameters passed at the moment of
initialization. Another parameter is a function which describes the
change of the inner elements during the group resizing.
Context menu can be called on a group to change its visualization
parameters.
Fig.15.69g
Group class
Group can be moved by any inner point and resized by any border
point. The dashed lines are included into frame on those sides which
can change their length throughout the resizing; dashed line is not
used for upper line if there is a title. There are four different types of
resizing; the one in use is determined by one of the parameters passed
at the moment of initialization. Another parameter is a function which
describes the change of the inner elements during the group resizing.
Context menu can be called on a group to change its visualization
parameters.
Fig.15.69h
World of Movable Objects 515 (978) Chapter 15 Groups
DominantControl class
Group can include only controls; one of them is a dominant element;
all others are subordinates. On moving / resizing of a dominant
control, all subordinates retain their relative positions to dominant
element. Subordinates can be resized and moved individually but they
cannot be released entirely inside the area of dominant element; in
such case a subordinate is forcedly relocated outside. There is a
default relative position for the enforced relocation of subordinates but
it can be also changed by users for each subordinate individually.
Fig.15.69i
Dominant_CommentedControlLTP class
Group of elements with dominant – subordinates relation. Dominant
element belongs to the CommentedControlLTP class while
subordinates are only SolitaryControl elements. There is no
overlapping checking though it can be easily added; this is
demonstrated in separate example. The whole group is moved only
by the main part, so it is a useful class when the text area of the main
Fig.15.69j part is bigger than all subordinates together.
Dominant_SolitaryControl class
Dominant_CommentedControl class
Two classes with dominant – subordinates relation between their
elements differ only by the type of the dominant element and use the
same four classes of subordinate elements. The first class has a
solitary control as a dominant element and can be easily used instead
of the DominantControl class. The second class has a
commented control as a dominant element; this comment can be used
as a title for the whole group. An arbitrary number and any
combination of subordinates of following classes can be used:
SolitaryControl, CommentedControl,
Fig.15.69k CommentedControlLTP, and ElasticGroup.
ElasticGroup class
A group can be moved by any inner point; the frame is automatically
adjusted to the united area of inner elements. Inner elements can
belong to the SolitaryControl, CommentedControl,
CommentedControlLTP, DominantControl, and
ElasticGroup classes.
The MoveGraphLibrary.dll includes a special tuning form for such
groups. To use this tuning form, call a context menu anywhere inside
the group.
Fig.15.69l
World of Movable Objects 516 (978) Chapter 15 Groups
ArbitraryGroup class
A group can include any combination of controls and graphical
objects. Because of this uncertainty of inner elements, the behaviour
and view of each particular group is described by four methods passed
as parameters on initialization. Methods describe the synchronous
move of all inner elements, the adjusting of a frame to all changes of
inner elements, the drawing of these elements, and the correct
registering of a group together with all its inner elements in the mover
queue.
The MoveGraphLibrary.dll includes a special tuning form to modify
Fig.15.69m the visibility parameters of a group, but because of the uncertainty of
inner elements, this form may be not the best in all the cases.
Useful objects
Example which you will see in this chapter was included into the book during one of the updates and the
biggest problem was in finding the correct place for it. There is already one example with different filename
viewers in the chapter Complex objects, so logically it would be correct to put them side by side. On the other
hand, the new example uses movable controls and groups which are discussed and explained after that
mentioned chapter, so it would be too early to include the new example into that chapter. At last I decided to
organize a new chapter and put it at the place where you can see it now. At this moment any reader is already
familiar with movable controls, with objects of the DominantControl class, and with moving / resizing
of objects on the pages of tab controls.
Two graphical buttons are used to change the filename in view; these buttons are slightly changed when they are pressed or
released. The view of a button is determined by its state. This value is ButtonState.Pushed for the pressed button
and ButtonState.Normal otherwise. Button state is used as a parameter for a standard procedure of drawing
controls inside the FilenameViewer_Demo.Draw() method..
public void Draw (Graphics grfx)
{
grfx .FillRectangle (brushBack, rcFrame);
ControlPaint .DrawBorder3D (grfx, rcFrame, Border3DStyle .Sunken);
ControlPaint .DrawButton (grfx, rcBtnLeft, stateLeft);
… …
ControlPaint .DrawButton (grfx, rcBtnRight, stateRight);
… …
When a FilenameViewer_Demo object gets a filename, it organizes a List<string> of all possible views of this
filename; this List starts with the shortest name of a file without any path and ends with the longest variant which
includes the full path to the file.
private void FileNameViews (string name)
{
if (!String .IsNullOrEmpty (name))
{
int iFirstDivider;
List<string> names = new List<string> ();
names .Insert (0, name); // full name
if ((iFirstDivider = name .IndexOf ("\\")) > 0)
{
string strCheck = name .Substring (iFirstDivider + 1);
while ((iFirstDivider = strCheck .IndexOf ("\\")) > 0)
{
names .Insert (0, ".." + strCheck .Substring (iFirstDivider));
strCheck = strCheck .Substring (iFirstDivider + 1);
}
names .Insert (0, strCheck); // shortest name
}
namesList = names;
}
}
The currently selected view of a filename is determined by the iSlidingView field. On selecting a new filename, the
longest variant with the full path to the file is shown. On pressing the left button, the longer view is selected; pressing the
right button changes the view to the shorter form.
Tab control in the Form_FilenameViewersPlus.cs has three pages (figure 16.3); the first page is titled Standard viewer;
the page itself, fields on this page, and methods used on this page include in their names abbreviation stand or standard.
Two FilenameViewer_Demo objects are demonstrated on the first page of tab control; initially these objects are
prepared but they are empty.
FileNameViewer_Demo standviewerSimple, standviewerCommented;
private void OnLoad (object sender, EventArgs e)
{
… …
standviewerSimple =
new FileNameViewer_Demo (this, new Point (60, 100), 400);
standviewerCommented = new FileNameViewer_Demo (this, new Point (60, 200),
500, Font, ForeColor, SystemColors .Control,
Side .N, SideAlignment .Left, "Input file",
Font, 0, Color .Blue);
… …
Use the Select filename button in the form to select any file in any directory. Nothing is done with the selected file; only its
name is used. The name of nearly any file includes several subdirectories, so it is long and only some part of it can be seen
World of Movable Objects 521 (978) Chapter 16 Useful objects
Filename viewers on the first two pages of tab control are used in cases when a single filename is shown. From time to time
there is a task to show the names for a set of files; such demonstration requires some nontrivial solution. Suppose that you
try to show a collection of filenames in a standard ListView. What is the weakness of such solution?
Usually the filenames with the full path are very long. If you try to show them on the screen in full view, they will take
nearly the whole width of a screen; if you slightly enlarge the used font, then even the whole screen width will be not
enough. There is a scroll bar in the ListView; this scroll bar can be used to scroll the long lines, but are you sure that
in this case a standard scroll bar can be useful?
I need to put together into a ListView control several filenames from different directories and subdirectories; filenames
with the paths can significantly vary in length. As a result, when you scroll such ListView to look at the ends of the
longest lines (to see the real filenames which are at the end of a path), then for the shorter filenames you see only a small
part of the name or even nothing at all. This can be really an interesting view of information: you see some lines in the
ListView, while other lines around them are empty.
I was thinking about this problem for some time and then I came to some solution which uses a rail with a ball similar to
one I showed in the FilenameScrolling class. Only in the previous example a rail and a ball were parts of the
FilenameScrolling object, while in the next example a rail with a small circle constitutes an object of the
HorRailCircle class. In the following example I am going to use a HorRailCircle object in pair with a
ListView control, but it can be used in similar way with any other object, so first let us look at the HorRailCircle
class. In some aspects the HorRailCircle class is similar to the Trackbar class, but the HorRailCircle is
simpler. Visually a HorRailCircle object consists of a thin horizontal rail along which a colored circle can be moved
left and right (figure 16.5). Up till now I use only horizontal rails for such objects;
if I see the need of similar object with a vertical rail, then the updating of this class
will be done in minutes. But as long as I need only horizontal rails, I do not see Fig.16.5 HorRailCircle object
any reason in making this class even a bit more complicated.
I never use a stand alone HorRailCircle object but only in pair with some bigger object; usually it is a control which
plays dominant role. Three parameters are needed to initialize a HorRailCircle object:
• A rectangular area of its dominant element.
• Initial shift from this rectangular area of the dominant element; negative value means position above the rectangle,
while positive value puts the rail below the rectangle.
• Coefficient to describe the ball on the rail; it changes from 0 on the left end to 1 on the right end.
public HorRailCircle (Rectangle rcMaster, int dyInitialShift, double coefInit)
A HorRailCircle object cannot be moved around the screen to arbitrary position; it is moved around only as a result
of moving / resizing of the dominant element. There are two movements in which a HorRailCircle object can be
involved individually: it can be moved vertically, thus changing its shift from the dominant element, and the ball can be
moved along the rail. For these two movements, it is enough to have two nodes in the cover, so the cover is really simple.
public override void DefineCover ()
{
CoverNode [] nodes =
new CoverNode [] { new CoverNode (0, ptBall, nRadius, Cursors .SizeWE),
new CoverNode (1, rcBigArea, Cursors.SizeNS)
};
cover = new Cover (nodes);
}
An object is movable up and down, but the rail is thin enough (2 pixels), so I enlarged the height of the rectangular area by
which the whole object can be moved (rcBigArea) and made its height equal to the ball diameter.
World of Movable Objects 525 (978) Chapter 16 Useful objects
One more feature of HorRailCircle objects must be mentioned before looking at its role in viewing multiple
filenames. This is a graphical object and its dominant element is going to be a control. Controls are shown atop all the
graphical objects; if I would allow to leave a HorRailCircle object inside the area of its dominant element, then it will
vanish under it forever without any chances to see and use it again. To avoid this, the area of dominant control is declared a
forbidden area for HorRailCircle object; if released in this area, the HorRailCircle object is forcedly relocated
outside. This is an already familiar and easy to deal with situation for objects involved in dominant – subordinate relation.
Figure 16.6 shows the third page of
the tab control. The biggest element
on the page is the ListView to
show the names of files. One small
button allows to call the standard
OpenFileDialog and to add
any number of new names to the
list; another button allows to delete
a selected name from the list. These
three controls are united into a
Dominant_SolitaryControl
object. There is also a
HorRailCircle element for
which the ListView plays a role
of dominant element. All four
elements constitute a group.
Because one of the elements is
graphical, then the
ElasticGroup class cannot be
used but only an
ArbitraryGroup. Figure 16.6
shows the default relative positions
of the elements inside this group, Fig.16.6 In this case the HorRailCircle object is used to organize scrolling of
but some parameters of one column in the ListView
visualization were already changed
throughout the tuning forms for group and information.
ctrlsFiles = new Control [] { listFiles, btnAdd, btnRemove };
private void OnLoad (object sender, EventArgs e)
{
… …
dominFiles = new Dominant_SolitaryControl (ctrlsFiles);
railballForList = new HorRailCircle (listFiles .Bounds, 16, 1.0);
GroupVisibleParameters visparams =
new GroupVisibleParameters (Color .Yellow, true,
Auxi_Colours .DefaultFrameColor (this),
new int [] { 10, 10, 10, 10 }, true,
Font, SystemColors .Highlight, 0.0, 4);
visparams .Transparency = 1.0;
groupFiles = new ArbitraryGroup (this, visparams, "Add / delete files",
ElemsArea_groupFiles, DrawElems_groupFiles,
SynchroMove_groupFiles, IntoMover_groupFiles);
… …
I have already mentioned the problems of showing long filenames; here is how I decided to deal with these problems.
• The most informative part of the filename is its short name. I do not want to take this name out of view at any
moment, so the short name is always shown in the first column of the ListView control.
• The long name with a whole path is shown in the second column. The scrolling of information in this particular
column (not the horizontal scrolling of the whole ListView !) is ruled by moving the ball along the rail in the
HorRailCircle object. When the ball is at the left end of rail, then each filename is shown from the
beginning; when the ball is at the right end, then the tail of each name is in view. At any moment maximum of
each filename can be viewed in the second column; the exact part to be seen depends on the column width and
World of Movable Objects 526 (978) Chapter 16 Useful objects
selected font. When the ball is on the left end, then all the names are shown from the beginning starting on the left
border of the column. When the ball is on the right end, then the ends of all filenames are at the right border of the
column. The length of filenames can significantly vary, so each name must be scrolled at its own speed when the
ball is moved along the rail. For this, the OwnerDraw property of this ListView is declared true and the
DrawSubItem event is used.
private void DrawSubItem_listFiles (object sender,
DrawListViewSubItemEventArgs e)
{
ListView list = (ListView) sender;
Graphics grfx = e .Graphics;
if (e .ColumnIndex == 0) // Files
{
e .DrawDefault = true;
}
else
{
Rectangle rc = e .Bounds;
rc .Inflate (-1, 0);
grfx .SetClip (rc);
int nNameLength = Auxi_Geometry .RoundMeasureString (this,
e .SubItem .Text, list .Font) .Width;
int cxText = Auxi_Geometry .ValToCoor_Linear (rc .Left,
rc .Right - (nNameLength + 2), 0.0, 1.0,
railballForList .ViewCoefficient);
Auxi_Drawing . Text (grfx, e .SubItem .Text, list .Font, 0,
list .ForeColor, new Point (cxText, rc .Top), TextBasis .NW);
grfx .ResetClip ();
}
}
First, the full length of the filename is calculated. If the positioning coefficient from the ball is zero, then the beginning of
this string must be on the left side of the column. If the coefficient is one, then the end of the string must be on the right
side of the column and the beginning of the string is easily calculated from its length. The real coefficient at any moment
gives the starting position for the string. The string is painted from this position, but for the period of this operation the area
of painting is clipped to the size of the column, so only the part of the string between the column borders is seen.
Coefficient is the same for all lines of the ListView, but the length of each string (of each filename) is unique, so the
corresponding part of each name is seen.
The thing which allows to decide about the part of each file name in view is the call to the
HorRailCircle.ViewCoefficient property; it changes the view of the second column for each change of the ball
position. There can be a very unusual situation when you have a very short filename which fits entirely into column; this
short line will be in view all the time but moving from the left border to the right border according to the ball position. You
can easily avoid this movement by adding a simple comparison of the column width and the filename length into the above
code; because this is a very unusual situation, I decided not to do it.
Three standard mouse events are used to organize all moving and resizing on the tab page; several special moments must be
mentioned in the code of these events.
MouseDown event. When a ball is caught for movement, it is allowed to go only between the left and right ends of the
rail. The clipping area for mouse cursor is set at the moment when the ball is caught; this is done in the same way as was
explained with similar ball on the previous tab page.
MouseMove event has two special situations.
• If the dominant element is moved or resized, then all its subordinates must be informed about the change of area of
their dominant element. Two small buttons are informed automatically because they are the members of the
Dominant_SolitaryControl object; the HorRailCircle element must receive this information
through its ParentRect property.
• If the ball is moving, then the redrawing of the ListView control must be called.
World of Movable Objects 527 (978) Chapter 16 Useful objects
private void MouseMove_pageSetOfFiles (object sender, MouseEventArgs e)
{
if (moverSetOfFiles .Move (e .Location))
{
TabPage page = sender as TabPage;
page .Update ();
GraphicalObject grobj = moverSetOfFiles .CaughtSource;
if (grobj is Dominant_SolitaryControl)
{
railballForList .ParentRect = listFiles .Bounds;
}
else if (grobj is HorRailCircle &&
moverSetOfFiles .CaughtNodeShape == NodeShape .Circle)
{
listFiles .Update ();
listFiles .Invalidate ();
}
groupFiles .Update ();
page .Invalidate ();
}
}
MouseUp event. The only special situation is the release of any subordinate inside the area of its dominant element. The
positions of two small buttons are checked and the enforced relocation is performed, if needed, by the
Mover.CheckDominantsSubordinates method. For the HorRailCircle object, similar method of this class
must be called. The HorRailCircle object can be placed anywhere above or below ListView; if released under the
ListView, it can reappear also above or below the control; the exact place of emersion depends on whether the point of
release was closer to the upper or lower border of the control.
private void MouseUp_pageSetOfFiles (object sender, MouseEventArgs e)
{
Cursor .Clip = Rectangle .Empty;
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (moverSetOfFiles .Release ())
{
GraphicalObject grobj = moverSetOfFiles .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
moverSetOfFiles .CheckDominantsSubordinates ();
if (grobj is HorRailCircle &&
moverSetOfFiles .CaughtNodeShape == NodeShape .Polygon)
{
(grobj as HorRailCircle) .SaveCurrentPositionAsForced ();
}
groupFiles .Update ();
pageSetOfFiles .Invalidate ();
}
… …
The use of HorRailCircle object allowed me to solve the problem of showing long filenames without using too much
screen space; now I have similar elements in several of my programs. When you start using elements of such type in
different situations, they can also give a clue to solving some other problems. In some programs I have big ListView
controls with a lot of columns which I need to scroll independently. For each of the columns that I need to scroll I can use
its own HorRailCircle element associated with the rectangular area of this particular column. I do not want to insert
such example into this version of the book; maybe later. From the point of the scroll organization, there is nothing new or
special; on the other hand, there are some problems which have nothing to do with moving or resizing, so I continue to think
if this book needs such example or not. But I want to underline that such simple objects in view and design as
HorRailCircle can be very useful in absolutely different situations.
World of Movable Objects 528 (978) Chapter 16 Useful objects
User-driven applications
This is the first chapter in the second part of the book. All previous chapters were about HOW to turn
different objects into movable / resizable. Further on I will write more about turning into movable / resizable
some of the most complex objects which are used for scientific, engineering, and financial plotting, but
beginning from this place and especially in this chapter I am going to write much more about WHAT FOR all
objects have to be turned into movable and WHAT USERS get from the new versions of applications entirely
designed on movable objects.
Let us quickly remember what was explained and discussed in the first part of the book.
• The idea and the algorithm for turning any screen object into movable / resizable was introduced.
• The method of turning objects into movable was demonstrated on a variety of objects starting from the simplest
and then dealing with more and more complex: rectangles, polygons, circles, rings, different figures with holes…
• Then came the time of real complex objects with the parts involved in individual, synchronous, and related
movements.
• Movement restrictions were considered.
• After graphical objects came the time for movable / resizable controls.
• Next was the logical step from solitary elements to the groups. The wide variety of groups with different
behaviour was discussed.
I think that everyone will agree that this required a lot of work to be done. Some of the examples look interesting and can
be implemented in applications just in the way they are demonstrated. Others can be used in one or another application
after some minor adjustments. But do you really think that turning of one or another object from the currently used
applications into movable was the real goal of all this work?
I never tried to hide the fact that I started my work on the movability of objects not as an attempt to invent the general
algorithm for all kinds of objects. I will write about it in more details further on because these details are very important for
understanding of the new applications, but the triggering thing of this work was the demand for movable / resizable plots in
scientific programs. I work on design of scientific and engineering applications throughout all my professional life. It
became obvious to me several years ago that:
• There is a long period of stagnation in design of scientific / engineering applications. Not in one or another
specific branch but in the whole segment of such applications. (I worked in different areas and saw the
similarities.)
• The only chance to end this stagnation and move on is to use absolutely different main idea of design.
• The control over applications has to be passed from developers to users.
• The only way to do this is to turn all plots into movable / resizable.
I had to invent an algorithm and I had to check it on some simple elements, like rectangles, but the main purpose was the
movability of the scientific plots. When the movability of the plots became good enough to be used in real programs and
some time after it while I was constantly improving the algorithm, I was thinking more and more that that idea of
movability could not be bound only by the original task of scientific plots. Applying the designed technique to other objects
and the spread of the idea of movability on all the screen objects turned out to be much more important than the algorithm
itself.*
From my point of view, the consequences of designing the applications on the basis of movable / resizable objects are much
more important than any algorithm which is used. You can use my algorithm or you can use any other, for example, your
own, if you spend some time and produce one. You may use any algorithm to add movability, but if you start to design
*
The historical parallel of the consequences which are much more important than the algorithm itself. Newton and Leibniz
worked on the invention of calculus independently. It looks like Leibniz had a slightly better approach, but Newton’s
claims for priority sounds much louder because he demonstrated excellent samples of using the new instrument. I am not
saying a word about the priority of one or another. The most important thing is the widespread use of calculus; it is much
more important than the identification of stones in the base of this mathematical instrument.
World of Movable Objects 530 (978) Chapter 17 User-driven applications
applications on the basis of movable / resizable objects, you will eventually come to the same ideas and principles which
constitute what I call the user-driven applications.
Applications based on movable / resizable elements differ so much from what we got used to throughout many years that
for many people it is difficult to accept them at first. This is the nature of all human beings: when we see something new
which differs not more than for 10-15 per cent from usual things, we can estimate it without any bias. But if we are
introduced to something that is nearly 100 per cent different from familiar things, then we have big problems not only in
understanding it. We often reject such things even without trying. It is not the prejudice about one or another idea; such
reaction against absolutely new thing (technique, idea, or device) is deep inside the human psychology.
I have a clear understanding of this psychological problem while introducing the world of new programs. A lot of
programmers have looked at the new applications trying to understand how this new programs work and how these ideas
can be used. I have also met specialists who simply refused to spend a minute of their extremely valuable time to look at
something that they never saw. It was a bit funny to watch such a reaction, but nobody can be forced into looking at the
new things if they do not want to do it.
I think that you are aware how difficult it was for people to accept the idea that not the Sun was going around the Earth but
vice versa. Each day observations show to everyone that the Sun is going around the Earth; it is an AXIOM, and axioms are
usually not discussed or doubted; they are simply accepted without any second thought.
In programming we have such type of axiom which was never doubted since the beginning of the programming era: any
application is used according to the developer’s understanding of the task and the scenario that was coded at the stage of the
design. After reading this, you will definitely announce a statement: “Certainly. How else can it be?” Well, the Sun was
going around the Earth for centuries. There were no doubts about it. Yet, it turned out to be not correct.
All programs of standard design work strictly according to their developers’ scenario. User-driven applications do not obey
this rule. When you develop any program for your personal use, then the design under the underlined axiom is not a
problem: if at some moment you need something different from a program, you simply redesign and change it. The
problem begins when the number of users starts growing. People have different ideas and preferences; they have different
demands, especially to interface.
Computers were invented as pure calculators. Powerful, then more powerful and even more, but for many years they were
only pure calculators and nothing else. When the main and the only goal of a program is to give out the results of some
calculations, then the only important thing is the correctness of results. The results depend on the initial equations and their
coding, so there is nothing wrong if such program works strictly according to the developers’ ideas. As this was the only
type of programs for decades, then the main rule of their design became an axiom.
The invention of graphical display noticeably improved the process of data and results analysis (people are much better in
understanding the graph than the column of numbers) but did not change the main idea of the programs. Then with the
hardware improvement the era of new programs started; the problems began to grow like an avalanche. Even in the
specialized scientific / engineering programs the part of calculations was shrinking very quickly. Visualization became the
dominant part even in such specialized applications; there are also a lot of programs without any calculations at all but with
the requirements for a wide variety of data and results presentation and for different types of man – machine interfaces. The
questions of interface became the most important in design of programs. When an application is used by hundreds,
thousands, or millions of users, then it is impossible to imagine that all of them will agree with the proposed view and
decisions imposed by designers. Yet, the new programs are still developed on the basis of the same axiom which is
absolutely inappropriate for them.
The interface of our programs has hardly changed throughout the last 15 – 20 years. The most popular programming
languages change, the environment for the programmers’ work is absolutely different, but the results look the same. Only
by the view of some controls and several other tiny details a good specialist can distinguish the 1994 application from the
2012 program. But these changes are simply seasonal adjustments and nothing else. Why are there no real changes for so
many years? I think that to uncover the roots of the problem you have to look 25 years back because the programming
philosophy under which the interface design continued to exist throughout all these years was incorporated at that time.
Somewhere 25 years ago in one of the hot discussions on the interface design, its future changes, and the vector of
development, I heard such a declaration of the most popular view: “The clients will have to like whatever we will give
them”. Rude expression? Maybe, but only from the point of political correctness. If you skip from all manuals to all
famous programs the standard phrases about the priority of the clients’ interests (it is only a mantra which has to be
declared; nothing else) and look at what those clients really get, then you will immediately see that it is an exact declaration
of how a lot (a majority) of designers continue to think.
The history of different branches of science and engineering starts at different moments and go on with different speeds, but
looks like all of them make the same steps in their evolution. Compare that statement about the programs with the famous
declaration from Henry Ford: "Any customer can have a car painted any color that he wants so long as it is black".
World of Movable Objects 531 (978) Chapter 17 User-driven applications
The automakers turned away from that famous motto years ago. I think that it is time for programmers to change their view
on the clients. Not in declarations but in reality. I think that programmers had to change their view on the clients long ago
but they did not do it up till now. Users must decide about the view of the programs they work with. Not to beg from
developers to change one or another feature when the next version is distributed, but to make the decisions about the view
of applications and their performance just sitting in front of the computer. Not to make only those changes which
developers included into the predetermined list of allowed adjustments, but any changes they, USERS, would like to do.
I hear too often that good specialists in design (they call themselves experts) know much better than any user how the
program must look and work, what kind of interface must be organized. Each time I hear such a statement, I ask a simple
question: “Better for whom?” Does a user have the right to decide what is better personally for him? Or is he looked at as a
total idiot who cannot estimate his own feelings and say a word about his own preferences? My work is aimed at not only
giving each user this chance but giving him an easy way of changing any program according to his personal wish.
There is an obvious and very funny flaw in that underlined experts’ statement. Specialists in design declare that because of
their huge experience they know better than any user, what he really needs. They look at the users of their programs as the
crowd that must be directed and instructed up to the tiny details; from such point of view users are too dull to decide for
themselves the questions of organizing the screen view of applications. But the same experts in design spend a lot (a bigger
part) of their work time as the users of other applications; by developers of these applications those users-“experts” are
considered as dull as everyone else. Do you see the funny side of it?
How many times were you really mad even with the most popular applications because in one or another situation the
program was not working according to your expectations but did something different? I am sure that huge efforts were
made to design those applications in the best way so that you would be always satisfied with their work. And still you are
mad with their response on your commands from time to time. The developers are bad? No, they are excellent. You
wanted to do something stupid? I doubt. Both sides are correct, but they have different views, preferences, experience, and
habits. It is an absolutely wrong idea to declare that only one view on design is correct and to force the opponent to change
his view and to agree with your vision of the best design (in such way the developers always have the upper hand).
To solve the problem of a single interface enforced on users with different views, the adaptive interface was introduced.
Did it solve the problems? No, only soften them a bit. The popular dynamic layout* did not change anything at all except
for some extremely simple situations. If you have a resizable form with a single control inside, then with the resizing of the
form this control changes its size so as to fill the same percent of the form area or keep the constant spaces between the
control and the borders of the form (classical anchoring). This is very easy to implement and the results will satisfy nearly
everyone, though I mentioned two possibilities even for this extremely primitive case. But what about two controls inside?
They can resize both, or only the first one, or only the second. And the programmer has two choices: either to put into code
the variant he personally prefers (or the marketing department of his company declares to be the best) or to add an
instrument which allows users to select one of three cases. For the primitive case of only two controls you get three
possible variants. What if you have not two but three, four, or more controls inside? What are you going to do in case of 30
or 40 controls? Do you think that you will be capable to organize a selection between all possible variants and will like to
introduce users to such system? I doubt. The only solution which is left for you is to put some variant into the code and
then explain to users that this is the best variant which they will like. That is how all popular (and less popular) programs
are designed. Does it differ from that declaration which was announced 25 years ago? “The clients will have to like
whatever we will give them”.
In the chapter Groups I wrote about the Form_PersonalData.cs. This is a real application to deal with a lot of personal
data. This form contains 23 controls accompanied by 16 pieces of comments and organized into several groups. If you
have to design this application, what else except the dynamic layout can you use to organize a good view for such program?
Do you think that dynamic layout would allow you to design it with the interface which thousands or millions of users will
like and admire? You can push thousands of people to work with such a program if it is sold in a package with computers,
but there are no chances that all users will like the interface. They can adjust to it if there are no other choices, but are you
going to call this interface friendly only because the size of several controls is changed according to the size of the form?
The same program designed as a user-driven application gives users an unlimited freedom in changing its view; figure 17.1
shows only two variants, but the number of variants is infinitive.
The common thing for any form of adaptive interface (it can be called a dynamic layout or by any other name) that
somewhere inside the program there is a model of users’ behaviour. This model is either fixed at the moment of design or
can be refined throughout the work of an application; the second case is slightly better. The most important thing is that this
*
Several years ago Charles Petzold wrote in [4] that “dynamic layout is … an important part of future Windows user-
interface design philosophy”. Petzold is not only a good author, but he knows very well what is going to be cooked at the
Microsoft’s kitchen. That statement is definitely not the Petzold’s speculation on the item but the Microsoft’s decision. An
extremely wrong one.
World of Movable Objects 532 (978) Chapter 17 User-driven applications
We move real objects in our real life all the time because we need them to be positioned slightly different at different
moments. Our decisions about the relocation and placement of the real objects depend on many different things, but
whenever we think that another place is better, we move the thing (if it is not too heavy) without thinking an extra second.
Just move it and that is all; if we do not like its new position, we will move the thing again. Absolutely the same thing
happens to the screen objects: if they are easily movable, we will move them around the screen and place them at the most
suitable position as we estimate it at each moment. I see this happening often enough when people work with the user-
driven applications.
Certainly, some people understood the need of movable (by users!) screen objects years ago. Because of the importance of
this task, the movable objects appeared from time to time in different programs. The main problem was that it happened
occasionally when some clever programmers designed such an algorithm for a particular class of objects. I think that
because of their complexity those algorithms were never applied to other objects. I was also doing such things 20 or 25
years ago; my old algorithm was designed for some objects and was too complicated to be used in other situations. There
were no algorithms that could be easily used for different types of objects and there were no attempts to design applications
totally on the basis of movable objects. Those movable objects in the older applications were only good solutions for
particular cases. I want to emphasize and underline again two things:
• There were no algorithms that allowed to turn into movable / resizable the objects of an arbitrary shape and origin.
• There were no systems or applications totally designed on movable / resizable elements.
Only an algorithm that can be easily applied to any object allows to design the applications entirely on movable / resizable
elements. I emphasize it again, especially for those who continue to grumble “We have seen the movable objects before;
there is nothing new in it.”
1. The novelty is not in moving one or another object or even the objects of some special class. ALL objects in an
application must be movable without exceptions.
2. All kinds of objects must be turned into movable / resizable by the same algorithm. This algorithm must be easy to use
with all classes, with all shapes of objects, and under all the circumstances. Otherwise the algorithm is unsuitable.
That is why I have designed all those examples (be sure, I have more) which were demonstrated and discussed in the
first part of the book. To show that a wide variety of graphical objects, controls, and different kind of groups can be
turned into movable and resizable.
3. When such algorithm is applied to all objects in an application and when an application is designed according to several
rules, only then it turns into a different type of program – a user-driven application.
User-driven applications have some general rules. To become acquainted with these rules, let us have a look at a simple
program of data selection.
Selection of years
File: Form_YearsSelection.cs
Menu position: Applications – Selection of years
Let us consider a well known case when you have a limited set of items of which you need to select some subset. Such
selection can be implemented for different types of objects; for this example I decided to stop on the selection of years. The
design of such program is simple (figure 17.2). All years from the predetermined range are represented by the lines of one
ListView control. Any set of lines in this control can be selected. When lines are selected, press the Select button to
copy them into another ListView control. By repeating this procedure, you fill the second ListView with some
subset of years. If you need to delete some lines from the second ListView control, select those lines and press the
Exclude button.
I do not think that anyone will have any problems on the implementation of such task. Developers will have no problems
with writing the code for such application, but the questions can be (and they certainly will be) brought up by the users on
proposed design: a lot of users will like to see the full list of items on the left side and the selected items on the right;
another big group of users will prefer those lists to be positioned in the mirror way. There will be a small number of those
who will prefer to see both lists in one column; they have the right to demand such positioning. In addition, there is an old
problem of the best position for OK button. Three big groups of designers prefer to place it in the right-top corner, in the
right-bottom corner, or in the middle at the bottom of a form. Members of each group would never agree with anything else
except their opinion.
The task is so primitive that every developer will simply design this form in the way he personally prefers and will ignore
any other suggestions. (“The clients will have to like whatever we will give them.”) As a huge respect to users, the
World of Movable Objects 534 (978) Chapter 17 User-driven applications
dynamic layout can be applied to this form; the sizes of both ListView controls will be changed according to the used
font or resizing of the form. I am sure that you can remember either designing similar form or using it in one or another
application and there was nothing else except the things that I have mentioned.
But are you sure that this is really the right way to design even such simple programs?
Let us consider the task itself. It is only about the selection of years and nothing else. This selection works correctly
regardless of positions and sizes of all controls. As a designer, you are responsible for correct work of all the operations
(selection, deselection, and saving). If you will insist that application must look exactly as you prefer, then stop talking
about the user-friendly interface of your programs. If you will agree that it would be nice to allow each user to change the
view of the program in such a way as he personally wants, then ask yourself a very simple question: “What has to be
changed to give users the full control over the view?”
The answer is obvious and simple: the screen elements must become movable and resizable by users.
Why was it not done yet? Sluggishness of minds; I do not see any other reason. No ruling boss from some big corporation
has declared the possibility and novelty of such thing, so nobody tried to do such things. It is a pity to see that the
programmers’ community lost the sense of imagination and the ability to think independently and only waits for the
commands from the big corporation to turn left or right and to march in new direction. Until the next big turn is announced.
In the first part of this book I have already demonstrated how different elements can be turned into movable and resizable;
now it is time to apply that knowledge to the design of some real and at first simple application.
Our program (figure 17.2) has two obvious groups, one
solitary control (OK button), and my standard pair of
elements to show some information and to hide it. Groups
can be organized in different ways; here each of them is
designed as an ElasticGroup object which has a single
DominantControl element inside.* Each group includes
a pair of controls of which a ListView is obviously a
dominant control and a button is subordinate. The simplest
constructor of the DominantControl class needs only
an array of controls of which the first one is automatically
turned into dominant.
Only a small button with the question sign is non-resizable;
other five controls in the Form_YearsSelection.cs are
resizable. If you want, you can resize any button even in such
a way as to see its text going vertically. I do not care what
any user wants to do with the view of this application. As a
developer, I have to provide users with an instrument to select
the needed data (years). User can change the view of the
program in any way he wants; lists and buttons work properly
regardless of their sizes and positions.
To grant such flexibility, several things must be done.
1. Declare and initialize a mover.
Mover mover;
mover = new Mover (this); Fig.17.2 Selection of years
2. Construct all movable objects.
private void OnLoad (object sender, EventArgs e)
{
… …
scHelp = new SolitaryControl (btnHelp);
DominantControl domin = new DominantControl (ctrlsAll);
domin .SaveSubordinatePositionsAsForced ();
*
This is not the only possible solution. Earlier variant of the same task was demonstrated in the article [6] and it was based
on two Group objects; it was designed especially for those who like to use the ideas of dynamic layout. That article is
accompanied by its own Demo program of which the whole project is available, so you can download it (see section
Programs and Documents) and look at the code of another slightly different implementation.
World of Movable Objects 535 (978) Chapter 17 User-driven applications
groupAll = new ElasticGroup (this, new ElasticGroupElement (domin), 12,
"All years");
groupAll .Location =
new Point (btnHelp .Right + spaces .Hor_betweenFrames,
spaces .FormTopSpace);
domin = new DominantControl (ctrlsSelected);
domin .SaveSubordinatePositionsAsForced ();
groupSelected = new ElasticGroup (this,
new ElasticGroupElement (domin), 12, "Selected");
groupSelected .Location = new Point (groupAll .FrameArea .Right +
spaces .Hor_betweenFrames, groupAll .FrameArea .Top);
btnOK .Location =
new Point (groupSelected .FrameArea .Right - btnOK .Width,
groupAll .FrameArea .Bottom - btnOK .Height);
scOK = new SolitaryControl (btnOK);
info = new InfoOnRequest (this, btnHelp, RenewMover,
new Point (spaces .FormSideSpace,
groupAll .FrameArea .Bottom + spaces .Ver_betweenFrames),
txtInfo);
… …
3. Register all movable objects with a mover.
private void RenewMover ()
{
mover .Clear ();
groupAll .IntoMover (mover, 0);
groupSelected .IntoMover (mover, 0);
mover .Insert (0, scOK);
mover .Insert (0, scHelp);
info .IntoMover (mover, mover .Count);
if (bAfterInit)
{
Invalidate ();
}
}
4. Use three standard mouse events to organize the whole moving / resizing process. All three methods for mouse
events are very simple. One special situation has to be checked inside the OnMouseUp() method: if a
subordinate control is released inside the forbidden area – in the area of its dominant control – then such
subordinate is relocated outside this area; mover takes care of this situation.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
mover .Catch (e .Location, e .Button);
ContextMenuStrip = null;
}
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Update ();
Invalidate ();
}
}
private void OnMouseUp (object sender, MouseEventArgs e)
{
if (mover .Release ())
{
if (e .Button == MouseButtons .Left)
World of Movable Objects 536 (978) Chapter 17 User-driven applications
{
mover .CheckDominantsSubordinates ();
groupAll .Update ();
groupSelected .Update ();
Invalidate ();
}
… …
The task of selection of years is a simple one; the implementation is also simple, but even this example allows to see how all
basic rules of user-driven applications work.
Rule 1. All elements are movable.
There are no exceptions; all objects, regardless of their shape, size, or complexity must be movable. If for some object you
do not see a good solution in an instant, THINK a bit more and you will find a good solution. Users have to get the full
control of an application (form); this means that each and all objects must be under their control. Users are going to use an
application at the best level to fulfil their work; the movability of elements increases their chances to do this work exactly in
such a way as they want, so give them this chance. If you decide to give them nearly everything but this and that, then they
will bump into these hillocks on the road again and again. Each time with an adequate thought about you as a designer.
I prefer not to demonstrate any examples in which this main rule of user-driven applications is broken, but there happens to
be one example in which all objects are movable except one. The Form_TrickWithControls.cs will be discussed further
on in the chapter Some interesting possibilities, but through the menu command Miscellaneous – Trick with controls you
can start this example and estimate for yourself what kind of problems even a single unmovable object can organize for the
work of normally designed application.
Rule 2. All parameters of visibility must be easily controlled by users.
Rules 1 and 2 are the projections of the full users’ control over a program on the different sets of parameters. The first rule
deals with the locations and sizes; implementation of this rule is based on the covers of all objects, on registering all objects
in the mover queue, and on using some methods of the Mover class in the code of three mouse events as shown above The
second rule deals mostly with the colors, fonts, and some auxiliary things. Mover plays a very important role in detecting
the element to be changed and in starting all these changes. I prefer to start all such changes from the MouseUp event.
Objects of two classes which are used in the current example – ElasticGroup and InfoOnRequest – have special
tuning forms, so they are opened when the corresponding object is clicked with the right button.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
… …
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is ElasticGroup)
{
groupPressed = grobj as ElasticGroup;
groupPressed .ParametersDialog (this, GroupParametersChanged,
PointToScreen (ptMouse_Up), false);
}
else if (grobj is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, InfoParamsChanged,
null, PointToScreen (ptMouse_Up));
}
}
… …
The above shown code leaves without tuning only one object – the OK button. Rule 2 does not allow any exceptions, so I
had to add some mechanism to change the font of that button. I could add a small menu with one command to be called on
this button but I decided to use slightly different way. The same menu of one command can be called at any empty place
and the font is set not only for OK button, but the same font is applied to button, to both groups, and to the information.
World of Movable Objects 537 (978) Chapter 17 User-driven applications
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
… …
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
}
private void Click_miFont (object sender, EventArgs e)
{
FontDialog dlg = new FontDialog ();
dlg .Font = btnOK .Font;
if (dlg .ShowDialog () == DialogResult .OK)
{
btnOK .Font = dlg .Font;
groupSelected .Font = dlg .Font;
groupAll .Font = dlg .Font;
info .Font = dlg .Font;
Invalidate ();
}
}
Rule 3. Users’ commands on moving / resizing of objects or on changing the parameters of visualization must be
implemented exactly as they are; no additions or expanded interpretation by developers are allowed.
Changing of the visualization parameters by users is not an unknown thing and is implemented in a lot of applications. But
in the standard applications, especially those that are built on the ideas of dynamic layout, users change one parameter, for
example, font, and a lot of related things are changed automatically, because this is the nature of dynamic layout. With the
user-driven applications you, as a designer, have to stop thinking in the way like this: “You changed the font. I am smart, I
know what you really wanted to do, so I will do it for you: I will adjust the sizes of this, this, and that object. Be happy,
because I save you several clicks.” This is an absolutely wrong way of design for user-driven applications. The developer
must not interfere with the users’ commands and add anything of his own.
It can be a bit strange at the beginning of new design to control yourself and not to add anything of your own to the users’
commands. You may be a designer with many years of practice; you really know what must be done to make the view of an
application better in different situations. But this is another world; if you gave users the full control of an application, it
must be really full, so you do not leave anything for yourself as a second control circuit. Whatever is gone is gone.
Eventually you will find that nobody needs your even excellent experience on adjusting the forms to their needs. The
situation in which you have to apply all your skills in design (the higher – the better) is the construction of the default view
of every form. The highest credit to your skills is the big percentage of users who will not change anything at all but will
work exactly with your proposed design. And in addition to your perfectly designed forms, there is the possibility for users
to move, resize, and tune all the screen elements at their wish at any moment.
Do not forget about rule 3 even in situations when the probability of some needed additional actions after the change of
visibility parameters is nearly 100 per cent. This often happens after the font change of some elements. On my computer
the default font is small (size 8); when the Form_YearsSelection.cs is started for the first time, this small font is used for
all controls. Suppose that I decide to enlarge the size of information in all controls; I call menu at any empty place and
enlarge the font up to size 12.
For both ListView controls it means that fewer lines are seen in the same rectangular area, but all these lines are perfectly
seen. Not so good is the situation with buttons. Text on the buttons will be seen only partly. This still visible part is big
enough to understand the text, but the view is obviously damaged because the lower part of the text is cut out. The solution
is obvious and simple – press the corner of a button and increase its size.
World of Movable Objects 538 (978) Chapter 17 User-driven applications
The apologists of dynamic layout may insist that this situation perfectly demonstrates the advantages of dynamic layout. I
need to say that this only demonstrates absolutely different basic idea of user-driven applications. User is not an additional
element for a program designed by somebody. User is the only ruler of all the things going on the screen of his computer.
If user ordered to change the font, then only font must be changed and nothing else.
If you want to help users in such situation, include two different commands in menu. One command will work as it is
organized now. Another command – Font (plus controls resizing) – will change the font and change the controls
proportionally. Adding of such command is easy but it makes a program much more flexible. I demonstrate such pair of
commands in several examples further on. When some application has a complicated default view with many screen
elements which must be carefully positioned in relations to each other, I often include a command to reinstall the default
view. In some examples you will see two commands; one will reinstall the default view using the default font; another will
organize the default view but using the font you prefer.
When 15 lines up I wrote about the font increase which damaged the text view by cutting its lower part, it was written in
assumption that the original button size was not changed. But suppose that first the buttons were enlarged and only after it
the font was increased. In this case there would be no text damage and automatic resizing is definitely not needed. User
can change the button size at any moment. You, as a developer, do not know the chain of user’s actions, so there is a high
enough probability that the automatic resizing after the font change would be not needed. Do you still insist that it must be
hard coded?
This is a very simple example which shows the importance of this rule. Do not add anything to user’s request; execute only
the ordered command.
Rule 4. All parameters must be saved and restored.
Saving the parameters for restoring them later and using the next time is definitely not a new thing and is practiced for many
years. But only the passing of the full control to users and the significant increase in the number of parameters that can be
changed, turned this saving / restoring of parameters from the feature of the friendly interface into a mandatory thing. If
user spent some time on rearranging the view to whatever he prefers, then the loss of these settings is inadmissible. And as
the full users’ control means the possibility of changing any parameter, then saving / restoring of all the parameters must be
implemented.
It does not mean that you have to write many lines of code if you are going to organize a form with dozens of objects inside.
You can check the code of this form and of other forms with much more elements inside and see for yourself that the
process of saving / restoring is not boring at all or time-consuming. Rule 4 means that each class has to have its own
methods for saving and restoring; then those methods are simply mentioned in any form where the objects of this class are
used.
In the Form_YearsSelection.cs, objects of three different classes are used: SolitaryControl, ElasticGroup, and
InfoOnRequest. All these classes have their methods for saving, so there is one code line per each object which has to
be saved.
private void SaveIntoRegistry ()
{
… …
scHelp .IntoRegistry (regkey, "scHelp");
info .IntoRegistry (regkey, "Info");
groupAll .IntoRegistry (regkey, "All");
groupSelected .IntoRegistry (regkey, "Selected");
scOK .IntoRegistry (regkey, "scOK");
… …
At the moment of saving, the InfoOnRequest object can be visible or hidden, so the current status of this element is also
saved. Restoring of the form view uses the corresponding methods of the same classes.
private void RestoreFromRegistry ()
{
… …
scHelp = SolitaryControl .FromRegistry (regkey, "scHelp", btnHelp);
info = InfoOnRequest .FromRegistry (this, regkey, "Info",
Convert .ToBoolean (strs [3]), btnHelp, RenewMover);
groupAll = ElasticGroup .FromRegistry (this, regkey, "All", ctrlsAll);
groupSelected = ElasticGroup .FromRegistry (this, regkey, "Selected",
ctrlsSelected);
World of Movable Objects 539 (978) Chapter 17 User-driven applications
scOK = SolitaryControl .FromRegistry (regkey, "scOK", btnOK);
… …
Four mentioned rules were born in different ways. Rule 1 – the main rule of user-driven applications – fixes in words the
whole process of transformation from the standard applications into something totally different and based on movable
elements. It took me several months to understand the entire process; the understanding came step by step, and on each
stage I had no idea, what would be the next. But it turned out to be very logical; I will talk about it further on while writing
about the scientific / engineering applications because this is the area where all these things started. Rules 2, 3, and 4
became obvious to me and were formulated later when I started to design more and more user-driven applications.
Now all these main rules are formulated and further on we will see how these rules work in complicated forms and
applications which contain a lot of different elements. Before introducing the new examples, I want to look once again at
one example which was already discussed in the chapter Groups a hundred pages back.
Personal data
File: Form_PersonalData.cs
Menu position: Applications – Personal data
Let us return once more to the application in which users can look through the sets of personal data (figure 17.3). The code
of the Form_PersonalData.cs was already used in the chapter Groups for the detailed explanation of the
ElasticGroup class. This form is organized according to all the rules of user-driven application, so let us look on the
details of how these rules work in this program.
Rule 1 All elements are movable.
Certainly, ALL elements are movable. I would not even try to predict the number of different users’ opinions about the best
view of such a form. I think that it will differ not too much from the number of users. Or it will be much higher than the
number of users because my experience shows that when there is a chance to rearrange the view of the form (application) at
any moment, then each user has different opinions about the preferable view each day of the week and also depending on
the time of the day and the weather outside. The movability of each and all parts of the form allows to rearrange its view in
an arbitrary way. When the elements are movable, this feature is used all the time. Yet, I have this extraordinary remark on
one of my articles.
“Is there more than anecdotal evidence to suggest that end users really want to be configuring the fine
points of a user interface layout, rather than accomplishing the task that the application is intended to
support? The HCI literature would seem to provide strong evidence to the contrary.”
Anonymous review from the UIST-2009.*
It is interesting and really funny to watch the same reaction and hear the same critical remarks from specialists of interface
design about the implementation of movability for all the elements. “Users do not need it. They have to work with the
application and not to play with moving objects around.” The funniest thing in this remark is in the fact that it is always
pronounced before the speaker ever tried such an application but never after. I never heard a single complaint about the
uselessness of movability from anyone who work with such applications or even from those who only tried them.
Programmers and users prefer to try and then give out their opinion. “The biggest specialists” have different procedure;
looking at a working application is never considered by them as a mandatory thing for making their conclusion. “We have
our Holy books; they are always right; whatever is against them is a heresy. And whoever is pronouncing a heresy must be
punished.” I am not joking at all; look at the quotation several lines up. That quotation is from the review on my proposal
for one of the well known annual symposiums. Never mind that the author of that review did not bother himself with
looking at an application and trying it even for a couple of minutes. He must be too busy and too good specialist for such a
waste of time. I really like this review; I admire it. I have it printed out in big letters and hanged on the wall at my work
place. “The H[uman] C[omputer] I[nterface] literature provides strong evidence … that you are wrong.”
Unfortunately, the review is anonymous. I would like to thank heartily the author of such an amazing review on the work at
which he never even looked.
The rules of “good design” which can be found in many books and manuals declare that if you have in a form several
controls with comments, then all the comments must be placed on the same side of the controls. You can see from
*
The progress of science can be based only on the exchange (and battle!) of the different ideas, proposals, theories. Hiding
the name of an opponent in the scientific discussion – it is one of the most stupid and ridiculous things with which I met
throughout the years of my scientific career. It is also the best illustration that the pure transfer of procedure from one area
(political vote) into absolutely different area (scientific discussion) can easily turn the whole situation into the theater of the
absurd. "Something is rotten in the state of Denmark."
World of Movable Objects 540 (978) Chapter 17 User-driven applications
figure 17.3 that I have not only slightly violated but absolutely ignored this rule. I think that with the spread of user-driven
applications a lot of currently used rules of design, even those that are looked at as axioms, will be ignored, forgotten, or
revised. Another world, different rules. The majority of currently used rules were declared 20 and plus years ago when
everyone was working with a single screen. Now we have much bigger screens; a lot of people use several screens. But the
biggest blow to those rules comes from the movability of the screen elements.
I do not see anything annoying or strange if in one group the comments are positioned on one side of the TextBox
controls while in another group they are positioned differently. Look at the central part of figure 17.3; there are two groups,
which look like opposite pages of the opened book. Why not to have the comments to controls of these groups on the outer
sides of them? I saw some marvelously designed books with purposely enlarged margins which contained illustrations and
comments to the main text. When you look at such opened book, you see two columns with the main text in the middle and
the comments on both sides of it. A book with such positioning of information looks beautiful; it is a pleasure to look at it
and to read it. If it is good for books, why is it going to be wrong for the screen? By the way, in the realm of old design
(fixed elements) you have no chances to check for yourself if the announced rule is right or wrong. As a designer, you
would construct the forms according to this rule;
maximum that you allow yourself is to try either the left
side positions or the right side positions for all the
comments but nothing more. (It is my assumption; I can
be wrong.) As a user, you would have to work with
whatever you have been given; in nearly all the programs
this rule is implemented. Only now, with all the
elements easily movable around the screen, you get the
chance to check that rule for yourself and to decide about
its usefulness. You can put all comments on one side of
controls, or on another side, or you can place the
comments as they are shown at figure 17.3 and decide
about the best view personally for you.
There is another thing related to the same problem of
multiple TextBoxes with comments: the alignment
of all those comments. The preference on this item is so
individual that I do not remember even reading about any
rule; the alignment of comments was enforced by the
designers without any second thought. (“You will have
to like what I like.”) I was not arguing about the
alignment of comments years ago, I’ll not do it now
when I give you an instrument to change this alignment
Fig.17.3 Another view of the Form_PersonalData.cs
easily, quickly, and in any possible way. If you have a
very specific view on the good looking design, you can place those comments even in the chess order around the
TextBoxes. I will not say a word against it. You took the application; you work with it; you change it in the way which
is the best for you. The application will work correctly regardless of the placement of those comments. As a designer, I am
responsible for the correct work of an application. I provide you with a view which I consider to be good, but you are
welcome to change it in any way you want. I have to guarantee that the program will work correctly regardless of the view
which you will organize.
Some people can say that these things – side of comments and so on – are the minor items which can be simply ignored. I
do not agree with this. And I think that users of such programs, especially those who have to work with similar applications
many hours a day (HR people), will not agree that these things can be simply ignored.
A bit more about the positioning of the elements which is strongly related to their movability.
One of the highlighted groups at figure 17.3 – the Address group – represents the standard set of controls to deal with the
address information. While the shown order of controls in this group is natural for western countries, it is absolutely
unnatural for other parts of the world where the address is often written in the opposite order beginning from the country
information. When all the screen elements are easily movable, it will take a couple of seconds to rearrange the view to
whatever the user would like it to be (to whatever order he got used to throughout many years of his life). I cannot think out
any other good solution for this problem of the habitual order of information. Certainly, you can hard code several variants
plus some instrument for the selection of the needed one. It is definitely a lot of extra work, but are you sure that you will
put into code all the needed variants? Or you can simply ignore the specific local preferences in the address information.
(Let them work with what is given.) You can decide for yourself if it is a good solution or not. What would you prefer if
you have to work with such data day after day: movable elements which you can position in the way you like or a set of
elements fixed by somebody according to his preferences?
World of Movable Objects 541 (978) Chapter 17 User-driven applications
In the chapter Movement restrictions, I have described the use of clipping and different levels of moving objects across the
borders of the forms. Moving temporarily unneeded elements across the border in order to rearrange the whole view is
widely practiced in the user-driven applications. There is an easy way to put the needed level of restrictions on such
movement, so you never lose your objects if you do not want to do it. Temporary relocation of objects across the form
border is not a trick in the user-driven applications but a very reliable, often used, and very easy and quick way of adjusting
the view of applications. This method can be used with any stand alone object (it can be a single control or a whole group
with a lot of elements inside), but it is not the good idea to try this technique with any inner part of the surrounding group.
If it is a part of the ElasticGroup, then the group frame will go after its runaway element not looking at the distance.
For the ElasticGroup class, there exists much better solution: any inner element can be turned into invisible and
returned back to life only when it is needed again. This technique was already discussed in the section Elastic group.
The movability of elements adds so many new things to the design of applications! I work on the design of really big and
sophisticated programs for more than 30 years. New languages and faster computers opened new possibilities from time to
time; it was never a revolutionary step but only some improvement to the already known procedures. With the invention of
the algorithm for turning elements into movable / resizable, the situation is different. There are no recommendations on
what can be or has to be done in one case or another; you go step by step in exploring the new continent and often you find
the things which you could not even imagine a month or two ago.
Complex applications have a lot of screen objects. These objects are not only individually movable, but a lot of them are
complex objects which include movable parts. To all these individual and synchronous movements users can add the
moving of the arbitrary groups of objects. After series of movements user can understand that the default view was much
better than the one he organized himself. It is a good idea to give users a chance to reestablish with one click a default view
of a group or of a whole form. In the standard applications, you do not even think about such problem because those
applications always have a default view. For user-driven applications, the existence of such command is not another rule
but simply a sign of good design. A sign of your respect to the users of your programs.
Usually the restoration of default view is ordered via a context menu at any empty place. Depending on the complexity of
the form, it can be done in two different ways. If the form is simple with only few inner elements, then the set of default
positions, sizes, and other parameters can be saved on opening the form; this set of saved parameters can be used at any
moment when user decides to return to the default view. With the really complex forms, I prefer to use another technique.
I have mentioned it before (rule 4) and I am going to write about it further on that all the parameters of a form and all the
parameters of all elements are saved in the Registry at the moment when the form is closed. When the form is opened
the next time, there is a checking, if the Registry contains the set of parameters for this form. If NOT, then the method
to organize the default view is called. So, when I want to restore the default view of the already opened form, I organize it
in such a way:
1. Set the flag for clearing the Registry, set the DialogResult property for reentering, and call the closing
of the form.
private void Click_miDefaultView (object sender, EventArgs e)
{
bClearRegistry = true;
DialogResult = DialogResult .Retry;
Close ();
}
2. While the form is closing, the value of the bClearRegistry flag is checked. For the case when the default
view must be restored, the entry of the current form in the Registry is deleted.
private void OnFormClosing (object sender, FormClosingEventArgs e)
{
if (bClearRegistry)
{
DeleteRegistryEntry ();
}
else
{
SaveIntoRegistry ();
}
}
3. The form is closed and in the Form_Main.cs the returned value from the form.ShowDialog() method is
checked. If this value was previously set to DialogResult.Retry, then the form is immediately opened
World of Movable Objects 542 (978) Chapter 17 User-driven applications
again. Because the form entry in the Registry does not exist, then on this opening the form gets its default
view.
private void Click_miPersonalData (object sender, EventArgs e)
{
Form_PersonalData form =
new Form_PersonalData (PointToScreen (new Point (200, 70)));
while (DialogResult .Retry == form .ShowDialog ())
{
form = new Form_PersonalData (form .Location);
}
}
The form appears at the same location where it was closed an instant before. If the form size before closing differed from
the default size, then the eyes will catch this difference. If the form had the default size, then it looks like only the inner
view was changed and no user will understand that the form was closed and opened again. (Users are interested in
reestablishing the default view and for them it doesn’t matter at all whether the form was closed and opened again or
whether the inner elements were simply moved around and resized. But for developers these two processes certainly
differ.)
User gets the default view of the form. He can either use it or make another attempt on better rearranging the form. There
is no limit on repeating the above described procedure, so I am sure that user will move the elements of the form again and
again.
The idea of movability penetrates all the layers of design; the movable big parts allow to change quickly the whole view of
an application or a form; the same feature of the smaller or tiny parts allow to reach the aesthetic perfection in parallel with
the most effective work of applications. Further on I will write how the idea of movability spreads from the main form of
any application into the tuning forms and into all auxiliary forms which are used in applications.
Rule 2 All parameters of visibility must be easily controlled by users.
Form_PersonalData.cs (figure 17.3) is not the simplest one but definitely not among the most complex forms which I
have to design and work with. It is just an ordinary form with an average number of elements; each element has its own
visualization parameters. The outer Personal data group includes five inner groups and several other elements. In total
there are six groups; each group has a set of its own parameters; the number of the tunable parameters for each group can be
estimated by the view of the group tuning form (figure 17.4). Thus, for the Form_PersonalData.cs we have more then 100
visibility parameters which regulate
only the view of the elements on the
screen. What is YOUR answer on
such a simple question: “Which part
of these parameters must be
controlled by users?” Try to answer
this question not as a developer of
such program but as a USER.
There are only three possibilities for
providing the tuning of your
applications:
• Give users no possibilities
for tuning. It is a funny Fig.17.4 Tunable form of the outer group
way to look at it as some
level of tuning; better to call it not providing any tuning at all.
• Give users a chance to change some of the parameters.
• Declare each and all parameters tunable. Not only declare but give users an easy instrument to change any
parameter of visualization.
I do not think that anyone can seriously consider the first possibility with no tuning at all. Certainly, if you look at yourself
as the our day Malevich and produce the modern (screen) version of the Black Square, then such work does not need any
tuning at all. On the other hand, such work must be considered as a product of the contemporary art but not as a
programming product. For all other applications some tuning is always needed.
World of Movable Objects 543 (978) Chapter 17 User-driven applications
The second option opens a wide area for discussion of what can be given to users for tuning and what not. Such discussions
will never end. On the constant demands from users of any real application you will have to increase the number of tunable
parameters steadily drifting to the third variant. So why not to avoid these long discussions and not to select the third option
just from the beginning?
Passing the control over ALL the visualization parameters to users looks absolutely natural for user-driven application. I
have to mention again that there are no complaints about such total control from users, but there are, funny to repeat, from
the “experts”. Experts have two main objections.
Objection 1 Users do not need all this setting of parameters; they need only the working program and will be happy with
it.
I try to explain again and again, that the work of a program does not conflict with the possibility of tuning the
parameters. All tunings do not change the goal of the program but only make it much more flexible and
provide a lot of possibilities to rearrange the view of a program to whatever each user would like to see.
Objection 2 Too many parameters for tuning; users will be lost in them.
I agree that the number of tunable parameters can be really big in some programs, but this is the reality of the
big programs. By not allowing users to change those parameters you do not decrease their number; all
parameters still exist, but in the standard applications they are set at the developers wish, and that is all. Give
users an instrument for easy changing of ALL these parameters and let users decide, when, how, and to what
extent they would like to use this instrument. The decision must be given to users and not imposed on them.
Users do not change the parameters all the time. Usually they set them at the beginning of the work and then there is only
an occasional additional tuning when something needs to be changed. But at the beginning it is often the only way to get
the good looking application. I spend a lot of time, trying to design the complicated forms of my scientific applications in
really good view. The scientific applications are among the most complicated programs; I will write about them in the next
chapter. Some programs on which I worked for several years in the department of Mathematical Modelling included the
tasks which were discussed in the department for years but were never developed because of the design problems; the
movability of all elements opened the opportunity to solve the problems and to design such programs. When the
applications are ready and look perfectly at my own computer, I give these applications to colleagues and see with horror
the initial view of the same programs on their computers. I spent a lot of time on thinking out those applications up to the
tiny details; the programs looked very good on my computer but on others… Even if I would try to, I would not have a
chance to destroy the views of my forms more than I see when they are used for the first time on different computers with
different screen resolutions and different default fonts.* From my point of view, the modern PCs are like a bus in which
each passenger has his own steering wheel. Nobody knows whose wheel is the main one at one moment or another; it looks
like they take this role at random, but definitely there are several of them battling for control.
With the applications constructed of movable / resizable elements, I am not worried any more about this strange effect on
different computers. When a user-driven application starts at any computer for the first time, it may have not the best view,
but each user can redesign any form to a good looking one in seconds. This may happen (I am sure it will happen
occasionally) with the forms from the Demo application for this book, but the figures from the book can show you what I
prepared and what I would like you to see. You can rearrange each form to the identical view or you can change them in
any way you want. It is a user-driven application in which the ideas are the main things and the views are under your full
control.
The “experts in design” deny the idea of giving the full control to users as the biggest heresy; they want neither to discuss
the possibility nor even to hear about it. I never heard a single complaint about this thing from users, but an outcry started
from those who teach for years how to design the good looking system. It looks like those people simply try to defend their
monopoly on the sacred knowledge; nothing else. What are they afraid of? Are they afraid that users will organize the view
of applications in such way that they really need and prefer? But what is wrong with this? Programs must have the best
design (view) for each user personally and not for the average user as a designer imagine him. I hope you understand the
difference between these two things.
Do not look at users of your applications as too dull to do all the tuning. You, as a developer, write the program; the
construction of a tuning part in this program is only a small part of your work, so you deal with this tuning relatively small
part of your time. Users are working with your programs for a considerable amount of time (I hope you are a good
programmer and users like your applications), so users will spend some time on tuning these applications according to their
demands. If you exclude some possibilities of tuning from this process, users will be still looking for opportunities of
*
It is incredible even to imagine, what this Windows system can do to the good looking application. “We wanted to do the
best, but the result was as usual…”
World of Movable Objects 544 (978) Chapter 17 User-driven applications
tuning, but they will have to spend much more time in their efforts to go around the artificial limitations that you put there.
What for? Give them the whole power of tuning. Users will be thankful to you.
There are some problems in making everything tunable. Some of these problems are caused not by the number of tunable
parameters but by the way these changes are processed by the Windows system. The biggest problem may happen with the
change of some font. These changes and especially some negative consequences are more related to the next rule.
Rule 3 Users’ commands on moving / resizing of objects or on changing the parameters of visualization must be
implemented exactly as they are; no additions or expanded interpretation by developer are allowed.
The dynamic layout is used in applications for years and throughout these years we got used to some of its remarkable
features. The dynamic layout is some kind of designing philosophy; we, as users of different programs, deal with the
implementation of this philosophy in each particular case. This can differ a lot from application to application because it
depends on the interpretation of the ideas of dynamic layout by each designer. Our expectations can be different from the
developer’s view; this is the cause of many problems. I still do not understand why the command to change the size of the
font has to change the size of the form also. I did not ask for it, I want to see the same information at the same place but in
bigger letters; that is all. Why somebody decided even without asking me that I would like to enlarge the form and
controls? From my point of view, it is really obtrusive and I cannot understand, why it is continued to be called a friendly
interface. It is exactly like washing my car at my expense at the gas station if somebody would decide that any filling of the
gas tank must be accompanied by the total car wash. I really wonder how many readers will call such gas station friendly.
User-driven applications give the FULL control to users, so a designer of such applications has no rights to add anything of
his own to the users’ commands. Thus, the command on changing the font of a single element or a group will do exactly
what was ordered and not a bit more, but… the result can be slightly different from our expectations which are based on our
previous experience.
• Let us return to the Form_PersonalData.cs (figure 17.3) and change the font of the comment belonging to the
CommentedControl object. I want to remind that these are the pairs “control + comment”; such objects are
widely used in the mentioned form. The comment of any CommentedControl object belongs to the
CommentToRect class which is derived from the Text_Rotatable class. These objects are positioned by
the central point; on changing the font, the anchor point – the central point of the text – is not moved and the area
of the text either expands in all directions or shrinks. If the changed comment is positioned close to its related
control and you significantly increase the font of comment, then chances are high that the enlarged comment will
be partly covered by the associated control; the text will need some moving to reestablish the good view of this
pair.
• When you increase the font of a whole group, the result depends on the type of controls inside this group. Some
controls will preserve their sizes and will not organize any troubles for you; others will automatically change their
sizes. If several text boxes are positioned close to each other, then chances are high that after the increase of their
font they will overlap; you will have to move some of those controls to restore a good view.*
While working with user-driven applications, you can occasionally observe such or similar annoying results after changing
the font, but in reality such problems occur rarely enough. First, the significant increase of the font is a very rare thing; for
the small changes of the size the effect is not as bad as I mentioned. Second, you do not change the font too often. Usually
you change it once and make some needed movements of the elements. After it, there are no requirements on changing the
font, though from time to time you continue to move and resize elements.
When you try to introduce and use something new, you have to take into consideration the features of the elements on
which you and others build the applications. I want to cut the strong link between the size of controls and the size of the
font which these controls use. I also do not want to include into my applications any actions which are still out of users’
control. For this reason, I never change the font for the whole form throughout its Font property which, depending on
the setting of the form AutoScaleMode property, might also change the size of a form. Instead, I change the font of
each control on an individual basis; the consequences depend on the type of those controls. For example, any single lined
TextBox will change its height according to the size of the font and you cannot do anything with it; this reaction is fixed
deep inside the system by the developers of those controls; programmers have no access to it and have no control over such
*
I know very well about these mentioned consequences of changing a font. Especially because of this problem, from time
to time I give users of my applications one more option; you can find it in some of the examples. In addition to “Change
font…” and “Default view” I include into a context menu a combined command “Default view with current font”. This
command produces a good view of the form, as I estimate it, for the font already set by user. There are no comments partly
closed by the controls in such view; there are no overlapped controls; everything looks good enough. At the same time
users are free to change this view as any other, so this is only one more option for users. Nothing mandatory; users are still
in full control of an application.
World of Movable Objects 545 (978) Chapter 17 User-driven applications
action. The majority of the TextBoxes in the Form_PersonalData.cs is of such a type (their Multiline property is
set to false); the width of such element is under users’ control but not the height. However, you can give users the
control over the height of such element, but only if you set the Multiline property of the TextBox to true. In the
Form_PersonalData.cs there are two multi-line TextBox controls in the group Professional status; at figure 17.3 only
one of them is seen and another one is hidden. If the height of such control is not big, then some negative effect can be
observed on the significant increase of the font for such TextBox: at first moment the increased symbols are seen through
the same area of a control only partly; then user have to resize this control with a mouse. Everything comes with a price,
but I think this is a very small price for exchange of the full control over the view.
About the violation of rule 3. I would prefer to have no exceptions from rule 3, but at the moment there is one which is
related to the CommentedControl class. (User-driven applications can use a lot of different classes; from time to time
I add new useful classes into the MoveGraphLibrary.dll, so similar situation might occur with other classes later.) Any
CommentedControl object consists of two parts: control and comment. Any moving of a control causes the
synchronous movement of its comment, so there are no problems at all. Comment can be moved individually and released
at any place. Controls are always shown atop all the graphical objects; when the comment is moved across the area of any
control, the text of the comment disappears from view because it is closed by the control. What will happen if a comment is
released at such moment when it is entirely closed from view by a control? If this is not the control of the same pair (of the
same CommentedControl object), then the comment is left where it was released. User can move the control aside
and open an access to the comment. If the control belongs to the same CommentedControl object, then there will be no
way to relieve the comment if it is left there because comment always moves synchronously with the move of the associated
control. If the comment is left under the associated control, then it will stay invisible and inaccessible forever. To avoid
this situation, the system violates rule 3 and in this particular case pushes the comment from underneath the control thus
making the comment accessible.
The described situation happens at the moment when mover releases an object (a comment or a control), so in this case the
mover is responsible for checking the possibility of such situation and avoiding it. But similar situation can happen as a
result of the things not related to mover in any way; then a designer has to think about it. Consider a case of a
CommentedControl object with the comment shown in some big font and positioned mostly under its control with only
a tiny part of the comment in view. (This case is illustrated with figures 14.4 in the chapter Control + graphical text.)
Suppose that you call a context menu on this comment and order the significant decrease of the font of the comment. As a
result, the area of the text shrinks and the comment disappears under the control. This situation is especially checked in the
Click_miCommentFont() method.
private void Click_miCommentFont (object sender, EventArgs e)
{
FontDialog dlg = new FontDialog ();
dlg .Font = cmntPressed .Font;
if (dlg .ShowDialog () == DialogResult .OK)
{
long idCmntCtrl = cmntPressed .ParentID;
GraphicalObject grobj;
for (int i = mover .Count - 1; i >= 0; i--)
{
grobj = mover [i] .Source;
if (grobj is CommentedControl && grobj .ID == idCmntCtrl)
{
CommentedControl cc = grobj as CommentedControl;
cc .CommentFont = dlg .Font;
groupData .Update ();
Invalidate ();
break;
}
}
}
}
Font is changed by the CommentedControl.CommentFont property, but after changing the font, the new area of
comment is compared with the area of associated control and if needed, then comment is relocated.
I mentioned the situations when change of the font can lead to such new positioning of elements that will not look good and
will require a move of some elements by user. The standard reaction from an experienced programmer would be to think
World of Movable Objects 546 (978) Chapter 17 User-driven applications
about such possibilities beforehand and to add a small bit of “improvement” from his own. My advice: do not do it. Such
even small addition is against the full users’ control of the applciations. Users are not fool or dull; you are one of them, and
you are as smart as anyone else. Users do with the applications exactly what they want to do. If you think that you are
capable of organizing the good view of any program with which you work, then users of your applications are capable of
managing the programs which you develope for them. If you think that they are too dull to do such a thing, then look into
the mirror: you are one of users. We are all users; there are no exceptions.
I understand that it will be a bit strange for the experienced developers not to include into the code some “obvious
imporvements”, but the world of user-driven applications is a different universe with different rules. Developers have to
think about and to avoid the fatal situations like disappearance of objects without any chance to return them back. Avoiding
such situations demonstrates your designer’s level and this is a credit to you. But do not apply your skills to the automatic
move of the slightly overlapping objects; user can easily do it himself. If you want, you can offer both variants and let users
make their choice; I will demonstrate such things in one of further examples.
Rules 1, 2, and 3 are formulated as separate, but in real applications they all work together. I think you can see it even from
the above text: while writing about the movability, I have to mention the tuning; while writing about the tuning, I have to
mention the special and very accurate way of working with some of the elements and again have to add some words about
moving.
Rule 4 All parameters must be saved and restored.
There are many objects in the Form_PersonalData.cs; parameters of any element can be changed, so all these parameters
must be saved until the next opening of the form. I have estimated before that there are more than 100 visibility parameters;
there are also coordinates and sizes for all the elements. Each element has a parameter which determines its visibility status.
Elements inside groups can be hidden individually or together with a group, so, for correct unveiling of a group, each
element uses two parameters to describe its visibility. Each object can be declared movable or not. For saving / restoring of
all these parameters I prefer to use the Registry though exactly the same thing can be organized via a binary file. All
classes included into the MoveGraphLibrary.dll have two pairs of methods to save and restore the objects of these classes;
one pair of methods for Registry, another – for a binary file.
For saving the view of any form in order to restore it in the same view the next time, I have to store the size of the form,
maybe several other general parameters, and all the used objects. For the Form_PersonalData.cs, there are only three (!)
such objects: the outer group, information, and the small button to turn hidden information back into visible. I mentioned
before that saving / restoring of all the needed parameters is not too complicated; I do not think that the code below is too
long or too complicated to save around 100 parameters of visibility and all those locations and sizes for approximately 40
elements that can be seen at the figure of a form.
private void SaveIntoRegistry ()
{
string strRegKey = Form_Main .strRegKey + strAddRegKey;
RegistryKey regkey = null;
try
{
regkey = Registry .CurrentUser .CreateSubKey (strRegKey);
if (regkey != null)
{
regkey .SetValue (nameSize, new string [] {
m_version .ToString (), // 0
ClientSize .Width .ToString (), // 1
ClientSize .Height .ToString (), // 2
info .Visible .ToString (), // 3
bShowAngle .ToString (), }, // 4
RegistryValueKind .MultiString);
groupData .IntoRegistry (regkey, "Data");
scHelp .IntoRegistry (regkey, "scHelp");
info .IntoRegistry (regkey, "Info");
}
}
catch
{
}
finally
{
World of Movable Objects 547 (978) Chapter 17 User-driven applications
if (regkey != null) regkey .Close ();
}
}
Restoring of the form is as easy as saving, but you must be careful in one thing: to restore some ElasticGroup object,
the array of inner controls is used and this array must include those controls exactly in the same order as they were used for
the construction of the group. For this reason, the array of controls is organized at the very beginning even before the
groups are first designed.
public Form_PersonalData (Point ptMouseScreen)
{
… …
ctrls = new Control [] {
textDate, textTime, textName, textSurname,
textDay, textMonth, textYear,
textHomePhone, textOfficePhone, textMobilePhone, textEMail,
textStreet, textTown, textProvince, textCountry, textZipCode,
textCompany, textPosition,
listProjects, btnDelete, btnMoveUp, btnMoveDown };
… …
Later the same array – ctrls – is used for group restoration; in this way there will be no mistakes.
private void RestoreFromRegistry ()
{
string strkey = Form_Main .strRegKey + strAddRegKey;
RegistryKey regkey = null;
try
{
bRestore = false;
regkey = Registry .CurrentUser .OpenSubKey (strkey);
if (regkey != null)
{
string [] strs = (string []) regkey .GetValue (nameSize);
if (strs != null &&
strs .Length == 5 && Convert .ToInt32 (strs [0]) == 701)
{
ClientSize = Auxi_Convert .ToSize (strs, 1);
scHelp =
SolitaryControl .FromRegistry (regkey, "scHelp", btnHelp);
info = InfoOnRequest .FromRegistry (this, regkey, "Info",
Convert .ToBoolean (strs [3]), btnHelp, RenewMover);
bShowAngle = Convert .ToBoolean (strs [4]);
groupData =
ElasticGroup .FromRegistry (this, regkey, "Data", ctrls);
if (scHelp != null && info != null && groupData != null)
{
bRestore = true;
}
}
}
}
catch
{
}
finally
{
if (regkey != null) regkey .Close ();
}
}
World of Movable Objects 548 (978) Chapter 17 User-driven applications
I have mentioned several times that saving / restoring of the view can be organized not only through Registry but also
through the binary files. The Form_PersonalData.cs is
such an example which demonstrates both ways.
Registry is used to save the current view of the form
at the moment when the form is closed. This example
can be modified in many different ways for special
purposes; those views will be absolutely different and it
would be very helpful to have an instrument to save and
restore different views. For such purpose saving in files
with different names is the best solution.
If you need to save some view of the
Form_PersonalData.cs, call menu on its main (big)
group and use its Save view command. The auxiliary
Form_PersonalData_SaveView.cs is opened
(figure 17.5). The form is very simple and is used only
to set the name under which the needed view is saved.
All objects in the Form_PersonalData.cs have special
Fig.17.5 This Form_PersonalData_SaveView.cs is used to
methods to save them in binary files, so the whole
save different views of the Form_PersonalData.cs
procedure is simple.
private void Click_miSaveView (object sender, EventArgs e)
{
Form_PersonalData_SaveView form =
new Form_PersonalData_SaveView (namesView);
if (DialogResult .OK == form .ShowDialog ())
{
… …
bw = new BinaryWriter (fs);
bw .Write (m_version);
bw .Write (ClientSize .Width);
bw .Write (ClientSize .Height);
bw .Write (info .Visible);
scHelp .IntoFile (bw);
groupData .IntoFile (bw);
info .IntoFile (bw);
… …
When you need to restore some view, the procedure is nearly identical
and also very simple. Call menu on the main (big) group in the
Form_PersonalData.cs and use the Restore view command. The
auxiliary Form_PersonalData_RestoreView.cs is opened (figure 17.6).
Select the needed name and press the Restore button. If some class has
IntoFile() method, then it usually has FromFile() method. Fig.17.6 An auxiliary form to restore view of
(There is no sense to have only one of these methods; it makes sense to the Form PersonalData.cs
have either both or none.)
private void Click_miRestoreView (object sender, EventArgs e)
{
Form_PersonalData_RestoreView form =
new Form_PersonalData_RestoreView (namesView);
if (DialogResult .OK == form .ShowDialog ())
{
… …
bool bShow = br .ReadBoolean ();
scHelp = SolitaryControl .FromFile (br, btnHelp);
groupData = ElasticGroup .FromFile (this, br, ctrls);
info = InfoOnRequest .FromFile (this, br, bShow, btnHelp, RenewMover);
… …
World of Movable Objects 549 (978) Chapter 17 User-driven applications
Rule 5 The above mentioned rules must be implemented at all the levels beginning from the main form and up to
the farthest corners.
Rules 1 – 4 are very important and are mentioned several times throughout the book. Whenever they are mentioned, I also
demonstrate how they are implemented, so I hope that at some moment on the way they are remembered. Rule 5 is
mentioned in this book not so often. The first reason is that a lot of examples from the Demo application accompanying this
book have only one (main) form and there are no chances even to mention rule 5. Even when there is a chance to mention
this rule, I often write its text without mentioning that this is one more rule in line with others. This
Form_PersonalData.cs is just one of those cases where I can demonstrate and comment rule 5.
Usually you design the main form of an application with maximum possible accuracy and a lot of thinking even about its
tiny details. When the main form requires an additional form for some auxiliary operation, then such form is often very
simple, can be produced even in minutes and you try to do it as quickly as possible in order to return to the main task. You
design it just to use for your own work on the main application. The auxiliary form gives you the needed result and you
immediately return to the main problems. In many cases you simply forget that it is not finished yet. It happened several
times to me that only during the final checking of the main application I called those auxiliary forms and was amazed that
some elements were unmovable or not tunable.
My amazement perfectly demonstrated to me the users’ feelings when they work with some user-driven application in
which everything is movable, resizable, and tunable, and then at some moment they open a tiny part of the same application
in which these things do not work. In some fairy tales, they have some tiny and usually locked room, but if you open this
room, then BIG problems start. Absolutely the same thing happens if you forget to apply standard rules to some tiny part of
perfectly designed user-driven application.
So, apply the same rules to all corners of your program.
The Form_PersonalData_SaveView.cs (figure 17.5) and the Form_PersonalData_RestoreView.cs (figure 17.6) are
simple, but all elements in them are movable, resizable, and tunable. Information objects in these forms belong to different
classes, but both classes have identically looking tuning forms. Fonts for all controls can be changed via the command of
menu which can be called at any empty place. There is a single comment to control in the
Form_PersonalData_SaveView.cs (figure 17.5), but a small menu can be called to change the view of this comment.
Both forms are saved and restored, so all rules of user-driven applications are implemented in these simple auxiliary forms.
World of Movable Objects 550 (978) Chapter 17 User-driven applications
By looking into the details of this group (you can do it, for example, with the help of Visual Studio), you can find some
important details. All buttons in this group are standard RadioButton objects but stripped of any associated text; just a
RadioButton squeezed to the size of a circle. Also, the buttons of each row are placed on a separate panel. Panels have
no visible borders and their color is the same as the color of the group itself. There is no visual indication of panels, but
their existence eases the selection inside each data type. These panels were needed and were helpful years ago when
interface provided the selection both by mouse and with the keyboard. I am not sure that the second variant is still needed,
so I could easily get rid of these invisible panels in the current example, but those invisible panels do not do any harm, so I
just made an exact copy of the old variant.
Version at figure 17.8 uses the old standard design without any changes, so the group on this page of tab control
(pageStandard) is an ordinary GroupBox object. The only difference from the old version is that in the current
application this group is turned into movable. There are two movable objects on this tab page and only one of them –
information of the UnclosableInfo class – is tunable, so the code for this tab page is really simple.
Mover moverStandard;
SolitaryControl scStandard;
UnclosableInfo infoStandard;
moverStandard = new Mover (pageStandard);
private void OnLoad (object sender, EventArgs e)
{
… …
scStandard = new SolitaryControl (groupboxStandard);
infoStandard = new UnclosableInfo (this,
new Point (groupboxStandard .Right + 2 * spaces .Hor_betweenFrames,
groupboxStandard .Bottom - 20), strsAboutStandard);
… …
private void RenewMover_pageStandard ()
{
moverStandard .Clear ();
moverStandard .Add (scStandard);
moverStandard .Add (infoStandard);
}
private void MouseDown_pageStandard (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
moverStandard .Catch (e .Location, e .Button);
}
private void MouseUp_pageStandard (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (moverStandard .Release ())
{
if (e .Button == MouseButtons .Right &&
Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up) <= 3 &&
moverStandard .ReleasedSource is UnclosableInfo)
{
infoStandard .ParametersDialog (this, ParamsChanged_pageStandard,
null, pageStandard .PointToScreen (ptMouse_Up));
}
}
}
private void MouseMove_pageStandard (object sender, MouseEventArgs e)
{
if (moverStandard .Move (e .Location))
{
pageStandard .Update ();
pageStandard .Invalidate ();
}
}
World of Movable Objects 552 (978) Chapter 17 User-driven applications
The easiness of design can be looked at as a huge advantage, but in this case it comes with some negative effects. There is
no tuning of visibility parameters for this group; this is not a simple mistake in my design, but the consequences of using a
standard GroupBox object.
A collection of circular buttons looks like a table, so in further explanation I will use the word table to describe the general
view of all these buttons. I want to design this group according to the rules of user-driven applications, so in general I
would need to add a menu to change the parameters of visibility. Changing of colors is easy, but changing of the font(s) for
texts inside the group may cause some problem. In the minimal case, all the texts inside the group are changed
simultaneously; better case allows to change the fonts for rows and columns independently; the best case allows individual
change of each word with the possibility of spreading the selected font on a subgroup of words (rows or columns) or on all
words in the group. In one of further variants I will demonstrate the implementation of such maximum flexibility, but now
let us consider the problems even with the middle case of changing the font for a subset of texts.
If you increase the font for the headers, those headers will be too close to each other or even overlap. You will like to move
those headers and put them farther away from each other, but the headers must correspond with the columns of circles in the
table. After changing the font for headers, you will like to increase the width of the table which will require two things:
• You need some visible borders of the table to use them for resizing.
• All rows must change the width synchronously.
Both things can be done if you put the existing set of panels under buttons on another bigger panel, make the borders of this
bigger panel visible, and implement some kind of dynamic layout on changing the sizes of this panel. You can also use the
ResizableRectangle class to link the changes of this panel with the movement of the texts.
If you want to use dynamic layout, then there is a better solution: instead of the GroupBox object you can use the
Group object which gives exactly the same view of a group but allows to move it by any inner point. Even in this better
case with a Group object, the existence of panels inside the group excludes their area from being used for moving the
whole group. To inform users that there is something special inside the group and that because of this something a part of
the group cannot be used for moving, this part must be, at least, painted in some color different from the rest of the group.
Each problem can be solved individually, but looks like the solving of one problem brings into light another one which must
be solved in its turn. All these problems are the hurdles that have nothing to do with the main task; they are simply the
results of using panels and standard GroupBox or Group.
For all the mentioned reasons I stopped using Panel, GroupBox, and Group objects several years ago. I demonstrate
this variant as a reminder about the original task and as a technically possible variant, but I am not going to use it in any of
my programs. Then what other options do I have?
If you are reading this part of the book, then with a
high probability you are familiar with the previous
text, so you have a good understanding of moving
and resizing individual controls, of controls with
comments, and different groups. You are also
familiar with the rules of user-driven applications, so
you understand the requirements to new realization
of the same task. You can think out some variant
which you would propose if you were asked to
implement this task. On the following pages you
will see several variants of the same task and you
can compare them with your proposed version. I am
going to demonstrate my variants on separate pages
of tab control and there will be nothing else, but
while thinking about your variant(s), keep in mind
that this is not a stand alone task but only a group
which has to be used with other elements
(figure 17.7) which are already redesigned
according to all the rules of user-driven applications.
Figure 17.9 demonstrates the same group organized
as an ElasticGroup object with a set of Fig.17.9 This variant uses a group of the ElasticGroup class
CommentedControl objects inside. What are with a set of CommentedControl objects inside
the advantages of this design?
World of Movable Objects 553 (978) Chapter 17 User-driven applications
• ElasticGroup object can be moved around the screen by any inner point.
• Tuning of such group is organized with a special tuning form which is provided by MoveGraphLibrary.dll.
• Controls inside the group can be resized and placed in any needed way.
• Each pair of a ComboBox with comment constitutes a CommentedControl object; comment can be placed
in an arbitrary way in relation to the associated control. For example, if you prefer to see comments to the right
from controls, you can organize such view in several seconds.
• In addition to simultaneous tuning of inner elements
throughout the tuning form, there is a context menu
(figure 17.10) which can be called on any comment;
menu allows the individual tuning of comments. It is
possible not only to change the font and color of any
comment individually but to spread the visibility
parameters and positioning of any comment on all
other comments inside the group. This means that
you can put the combo boxes in an arbitrary way (in
the way you prefer!), then place a comment to one of
the combo boxes in the way you like, set its visibility
parameters, and at the end with one command change Fig.17.10 Menu on comments at the page with
the view of comments to all other elements inside the ComboBox controls
group in identical way.
With all these possibilities for tuning, there can be an infinitive
number of views; figure 17.11 shows one of them. Without reading
about possible transformations and without trying them in the
working program, it will be really hard to believe that figures 17.9
and 17.11 demonstrate the same group and that transformation from
one variant to another can be done in several seconds.
This variant of group design is very easy to implement as it is based
on using only the ElasticGroup and CommentedControl
classes. Fig.17.11 One of the infinitive variants for the
view of this group
Mover moverCombos;
ElasticGroup groupCombos;
moverCombos = new Mover (pageCombos);
strsRow = new string [] { "Countries", "Provinces", "People", "Animals",
"Birds", "Flora", "Events" };
private void OnLoad (object sender, EventArgs e)
{
… …
CommentedControl [] ccs = new CommentedControl [ctrlsCombo .Length];
for (int i = 0; i < ccs .Length; i++)
{
ccs[i] = new CommentedControl (this, ctrlsCombo[i], Side.W, strsRow[i]);
}
groupCombos = new ElasticGroup (this, ccs, "Information in lists");
… …
I see only one disadvantage in this solution with the ElasticGroup and ComboBox controls and my opinion in this case
can be very subjective, but from my point of view variant from figure 17.8 is more informative because it gives not only the
information about the current set of parameters but also the entire picture of all possibilities, while in the second case you
have to open each ComboBox to see your choices. Variant from figure 17.9 is easy in implementation and the result
works according to all rules of user-driven applications but does not show the full picture. This means that I have to think
about another variant which can combine the advantages of two previous cases.
Before start working on another variant, let us summarize the good features that would be nice to see in it. Maybe in
writing down the requirements one by one we will get a very accurate definition of a new object to be designed.
World of Movable Objects 554 (978) Chapter 17 User-driven applications
• The group, if it is going to be a group, must be movable by any point. If it is going to be something different, then
this something must be easily moved as one object.
• It is better to see simultaneously the whole set of selected parameters and all the possibilities for each and all
parameters. This means that a view of a table is preferable, so let it be a table.
• This table must be movable by any inner point and be resizable by its borders. A table might have a title which can
be moved individually and placed arbitrary in relation to the table; at the same time this title must retain its relative
position throughout any moving or resizing of a table. This sounds like an object of the
ResizableRectangleWithComment class which is included into the MoveGraphLibrary.dll. An object of
this class consists of a resizable rectangle (ResizableRectangle class) accompanied by a comment of the
CommentToRect class.
• Radio buttons from the first variant worked perfectly for setting the needed parameters. Unfortunately, we cannot
use them here, at least, in the same way as they were used in the first variant where they needed panels underneath.
This means that we need a graphical
analogue of radio buttons to be used in
exactly the same way; the graphical
analogue does not need panels.
• We need some mechanism for setting the
parameters of visibility; we shall work on it
after the main thing will be designed.
Figure 17.12 demonstrates the view of the third
variant which is produced according to this list of
requirements. I call it the variant with false radio
buttons because the RadioButton controls are
substituted by graphical images which also imitate
the clicks.
Let us start the design with a class of false radio
buttons. This graphical object must look like a
standard radio button. I am going to use mover to
switch these “buttons” ON and OFF, so this must be
an object derived from GraphicalObject; it
has to have a cover and so on. At the same time
these false buttons are not going to be moved
individually by mouse as any normal movable
object; each button is going to stay in the middle of
the appropriate cell and the moving of buttons will Fig.17.12 Variant with false radio buttons
be a result of moving or resizing a table. Because
buttons are not moved individually, their class is named UnmovableRadioButton.
public class UnmovableRadioButton : GraphicalObject
{
Point ptCenter;
int rBig = 8;
int rSmall = 4;
bool bSwitched;
Cover for this circular object is simple – it consists of a single circular node which covers the entire object; the behavior
field is set to Behaviour.Frozen.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, ptCenter, rBig, Behaviour .Frozen));
}
Move() method for non-resizable circle was already demonstrated in the previous examples, so there is nothing new.
This method is primitive as only the central point must be changed.
public override void Move (int dx, int dy)
{
ptCenter .X += dx;
World of Movable Objects 555 (978) Chapter 17 User-driven applications
ptCenter .Y += dy;
}
More interesting though even simpler (!) is the MoveNode() method of this class. Any class derived from the
GraphicalObject has to have such method, but we have objects which are never moved individually, so this method is
empty. (Maybe there were similar cases among the previously demonstrated classes but I can’t remember them.)
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
return (bRet);
}
In such way the false buttons are not movable but recognizable by mover, so a mouse click can be used to change their
status.
private void MouseUp_pageFRB (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover_FRB .Release ())
{
GraphicalObject grobj = mover_FRB .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is UnmovableRadioButton && fDist <= 3)
{
Point pt = (grobj as UnmovableRadioButton) .Center;
int iRow, jCol;
CellForPoint_pageFRB (pt, out iRow, out jCol);
SwitchInfoView_pageFRB (iRow, jCol);
pageFalseRadioBtns .Invalidate ();
}
}
… …
As I mentioned before, the table and its title from figure 17.12 are organized into a complex object of the
ResizableRectangleWithComment class, but this is not the only movable object on this tab page. I decided to give
users more possibilities in changing the view of the table and organized movable partitions between the cells with the
textual information and the rest of the table with the buttons. With these movable partitions, user can change the width of
the first column with the names of rows; the remaining part of resizable rectangle is equally divided between three columns
with buttons. In a similar way the height of the headers can be changed and the remaining area is divided between other
seven rows. In the chapter Movement restrictions I have already demonstrated vertical and horizontal sliders inside a
resizable rectangle. That is exactly the type of elements which I need inside this resizable table, so I am going to use the
same SliderInRectangle class.*
Mover mover_FRB; // abbreviation FRB for pageFalseRadioBtns
ResizableRectangleWithComment rr_FRB;
SliderInRectangle horSlider_FRB, verSlider_FRB;
Resizable rectangle gets its minimum and maximum sizes at the moment of initialization; I also set limitations on the size of
cells in order to avoid their accidental disappearance.
private void OnLoad (object sender, EventArgs e)
{
… …
*
It is not a problem at all to make the borders of all rows and columns of the table movable individually, but I decided that
it was not needed in this case in which all cells contain exactly the same type of information – circles of the same size. If
there would be the cells with different elements inside that would require personal setting of width and/or height, then I
would make all the partitions movable individually. This was already demonstrated in the
Form_SlidersUnchangeableOrder.cs in the chapter Movement restrictions.
World of Movable Objects 556 (978) Chapter 17 User-driven applications
int wRowName = 4 * minWidth_Col;
int wCol = 3 * minWidth_Col;
int hTitle = minHeight_Row * 5 / 3;
int hRow = minHeight_Row * 4 / 3;
int nTitledRows = strsRow .Length;
int nHeaders = strsHeader .Length;
rr_FRB = new ResizableRectangleWithComment (this,
new RectangleF (5 * spaces .FormSideSpace,
2 * spaces .FormTopSpace,
wRowName + wCol * nHeaders,
hTitle + hRow * nTitledRows),
new SizeF (minWidth_Col * (nHeaders + 1),
minHeight_Row * (nTitledRows + 1)),
new SizeF (maxWidth_Col * (nHeaders + 1),
maxHeight_Row * (nTitledRows + 1)),
DrawMainRect_pageFRB, ChangeMainRect_pageFRB,
0.2, -12, "Information in lists", Font, 0, ForeColor);
RectangleF rc = rr_FRB .ResizableRectangle .Rectangle;
horSlider_FRB = new SliderInRectangle (rc, LineDir .Hor,
(double) hTitle / rc .Height, penSlider);
verSlider_FRB = new SliderInRectangle (rc, LineDir .Ver,
(double) wRowName / rc .Width, penSlider);
… …
The order of elements in the mover queue is important and in this case this order is obvious: all elements that reside in the
area of the table must precede the table in the queue, so I begin with all the false buttons (there are 16 of them), then come
two sliders, and then the table with its associated title – comment.
private void RenewMover_pageFRB ()
{
mover_FRB .Clear ();
mover_FRB .Add (info_FRB);
for (int i = 0; i < btnsCountries .Length; i++)
{
mover_FRB .Add (btnsCountries [i]);
}
… …
mover_FRB .Add (horSlider_FRB);
mover_FRB .Add (verSlider_FRB);
rr_FRB .IntoMover (mover_FRB, mover_FRB .Count);
}
Object of the ResizableRectangleWithComment class is registered by its own IntoMover() method, but the
order of registering provided by this method is obvious: comment can be placed atop its associated rectangle and must be
movable regardless of its position, so comment must stay ahead of rectangle in the mover queue. Thus we receive that at
the page with the false radio buttons the last element in the mover queue is the table. If you need to move the table, you can
press anywhere inside except two inner partitions (one vertical and one horizontal), all strings, and all radio buttons. Looks
like a significant part of the table is blocked by other elements, but figure 17.12 shows that there is a lot of empty space and
it is not a problem at all to move this table.
What interesting features must be mentioned in connection with moving / resizing in the variant of false radio buttons?
• When a slider (either horizontal or vertical) is caught for movement, the range of its possible movement must be
calculated; the limits of such movement depend on the current size of the table and the minimum / maximum
restrictions on the cell size.
private void MouseDown_pageFRB (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover_FRB .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
World of Movable Objects 557 (978) Chapter 17 User-driven applications
if (mover_FRB .CaughtSource is SliderInRectangle)
{
Rectangle rcMain = rr_FRB .ResizableRectangle .Rectangle;
SliderInRectangle slider =
mover_FRB .CaughtSource as SliderInRectangle;
if (slider .Direction == LineDir .Hor)
{
slider .Limit_Top = Math .Max (rcMain .Top + minHeight_Row,
rcMain .Bottom - maxHeight_Row * strsRow .Length);
slider.Limit_Bottom = Math.Min (rcMain.Top + maxHeight_Row,
rcMain .Bottom - minHeight_Row * strsRow .Length);
}
else
{
slider .Limit_Left = Math .Max (rcMain.Left + minWidth_Col,
rcMain .Right - maxWidth_Col * strsHeader .Length);
slider .Limit_Right = Math.Min (rcMain.Left + maxWidth_Col,
rcMain .Right - minWidth_Col * strsHeader .Length);
}
}
}
}
pageFalseRadioBtns .ContextMenuStrip = null;
}
• Throughout the movement of a slider, positions of all radio buttons must be constantly recalculated; this is done by
the RelocateButtons() method.
private void MouseMove_pageFRB (object sender, MouseEventArgs e)
{
if (mover_FRB .Move (e .Location))
{
if (e .Button == MouseButtons .Left &&
mover_FRB .CaughtSource is SliderInRectangle)
{
RelocateButtons ();
}
pageFalseRadioBtns .Invalidate ();
}
}
private void RelocateButtons ()
{
btnsCountries [0] .Center = CellMiddle_pageFRB (1, 1);
btnsCountries [1] .Center = CellMiddle_pageFRB (1, 2);
btnsCountries [2] .Center = CellMiddle_pageFRB (1, 3);
… …
Variant with the false radio buttons (figure 17.12) is a good one, but I think that several improvements can be made.
• It is much easier to change a parameter if not a small radio button inside a cell is used for such change but the
whole cell. Certainly, in such case the whole area of a cell must be used to give visible tip about the selected value
of parameter.
• It is better to have an individual setting of visualization parameters for each piece of textual information plus the
possibility of spreading these parameters on a group of elements. This was already done with the comments in the
second variant (the page with the ComboBox controls), but there is some significant difference: in that previous
variant the comments can be placed anywhere in relation to their associated controls, while in the case of a table,
and I am going to use the same resizable table with the sliders inside, the comments are placed inside the cells and
have to be moved inside these cells. (The last statement means that the text of comment can cross the cell borders,
but the central point of comment must always be inside the cell.) This sounds like the class
CommentToRectLimited which was introduced in the chapter Movement restrictions (see
Form_CommentToRectLimited.cs, figure 11.11) and this is the class which I am going to use in the next variant.
Figure 17.14 demonstrates the last variant in this example – variant with the colored cells. As you see, there are
opportunities for playing with each piece of textual information.
Mover mover_CC; // abbreviation CC for pageColoredCells
ResizableRectangleWithComment rr_CC;
SliderInRectangle horSlider_CC, verSlider_CC;
CommentToRectLimited [] cmntlimRows, cmntlimCols;
Fig.17.15a Menu on title Fig.17.15b Menu on any string Fig.17.15b Menu on any cell
World of Movable Objects 561 (978) Chapter 17 User-driven applications
Variant with colored cells uses three context menus. One of them can be called on the title; this menu is identical to the
same menu from the previous variant (figure 17.15a). Another menu unites two context menus which in the previous
variant were called on headers and row strings. In the current variant, all strings can be rotated, so there is one more
command for spreading the parameters of the pressed string on the siblings (figure 17.15b). There is also a small menu
which can be called on any cell in the table (figure 17.15c).
The Form_SettingsVariants.cs demonstrates not only the new way of design, but also allows to compare old and new
variants (figure 17.16). We have three different variants of solving this task according to the rules of user-driven
applications. Variant with ComboBox objects occupies minimal screen space, but this comes at a high price of being the
worst variant from the point of showing information. I would prefer to see in the program the variant with the colored cells,
but opinions vary. The most important thing is that any of three variants turns the full control over the view of application
to users and then each user personally decides about the preferable view in the boundaries of proposed design.
Old design with unmovable Movable group with resizable Resizable and movable The same movable and
and nonresizable elements. and movable inner elements. table; the movable partitions resizable table. Each piece
The tuning is done via the allow to change the size of of text can be tuned
standard tuning form of the cells. Visibility parameters individually; parameters of
ElasticGroup class. are set for each group of visibility can be spread on
textual information. siblings.
Fig.17.16 Small pictures are set side by side for better comparison of all variants
One more remark about the evolution of this Form_SettingsVariants.cs example. Years ago the design was based on
controls. The first step in direction of user-driven applications still used controls. The second step eliminated controls but
reproduced them in similar graphical elements. The final step got rid of this similarity. Now there are only graphical
elements of several classes. I did not do it on purpose; it is simply the logic of design which pushes you in this direction.
This is not the only example which demonstrated me the inner logic of user-driven applications. Controls are helpful
elements from time to time but they cause problems which outweigh their good features. If there is some way to substitute
controls with graphical elements, then such solution is usually the best.
World of Movable Objects 562 (978) Chapter 17 User-driven applications
Block diagram
File: Form_BlockDiagram.cs
Menu position: Miscellaneous – Block diagram of this application
There is no sense in writing a book about the new type of applications without an accompanying program which illustrates
all the proposed ideas. Because there is a wide variety of screen objects and different situations of using them, there are
many different cases that must be demonstrated and explained. The accompanying Demo program gives an access to all
examples via the system of menu and submenus in the main form. There are some auxiliary forms (examples) without a
direct access from the main form, but even the number of examples which are accessible through that menu system is now
over 130. Such a huge number of examples is helpful in explanation but causes some problems. First, when the way to any
example goes through two or three levels of submenus, this is not good for users of such program. Second, it is simply
difficult to remember the exact path to the needed example. If from time to time I have this problem myself, then it means
that users of the program (readers of the book) may have a real hurdle on the way to the example they want to look at.
Something must be done and this something must be in providing a different and easier way to all the examples.
Those 130 and plus examples have a direct access through the commands of menu. Unfortunately it is impossible to look
simultaneously at all branches of the main menu, so it is impossible to see the whole picture of all possibilities. Well, it is
impossible to see them all through the standard menu, but maybe it is possible to do through some other class or classes of
objects? This was the main idea when I decided to turn the main menu and its submenus into some kind of a block diagram
which shows all the possibilities and gives an easy direct access to any example. This decision added one more example to
already big set of them, but this is a very small price for the new possibilities. When the design of the
Form_BlockDiagram.cs was finished, I found out that I stopped using the access through the standard menu system but
started to use mostly the new way. Looks like I had to design and add this example long ago.
Figure 17.17 demonstrates nearly default view of the Form_BlockDiagram.cs with only minor changes. In the real
program only one chain of submenus can be opened at any moment, so neighbouring chains can appear at the same place
without causing any problems on viewing. In my block diagram all submenus are demonstrated simultaneously and such
view requires a lot of screen space. Number of different examples in this Demo application is so big that when I tried to
position all submenus in a traditional order from left to right but without overlapping, the resulted diagram, even shown in
the smallest font, was bigger than my wide screen. In order to fit this example into the sizes of my screen, I had to relocate
some elements. It was easy to do because all elements are movable. Figure 17.17 shows the result of my work to improve
World of Movable Objects 563 (978) Chapter 17 User-driven applications
the default view of this example. Some details on this figure are hardly seen, but you can copy this figure from the text into
some other program and then the details mentioned in the next several phrases will be easily seen.
Two types of connections are used in my block diagram.
• Connection between two submenus has unmovable end points. The starting point has to be at the level of some
string in the first submenu and cannot be moved anywhere else. Another end has to touch another submenu and it
really does not matter at which point the line will touch it, but I decided that the best point will be at the level of
the first string in this submenu.
• Connection between an element inside upper menu and some submenu has both ends movable. Such line starts
from some command of the upper menu. The text of this command occupies a rectangular area inside the upper
menu and the line can start at any point at the bottom of this rectangle. Thus, the upper end of this connection can
be moved along the bottom of the text. Another end of this line comes to the upper border of some submenu. It
does not matter at what point it comes; it is obvious that this is a connection not to some command inside submenu
but to the whole submenu, so this end of line can be moved along the upper border of submenu.
There exist special programs for block diagram design; they give a lot of possibilities for this type of activity. This example
is not of that type. I call it a block diagram because of its view, but it is limited to the purposes of this particular task, so let
us formulate them.
• The form must show the view of the main menu and all levels of its submenus as if they are opened
simultaneously.
• Diagram must show the connections between the information blocks according to the links between real submenus
in the Form_Main.cs.
• By a double click on the appropriate string inside any block, a corresponding form (example) must be called on as
if you go to the same example through the real menu system.
Other requirements are simply the result of implementation of all the rules of user-driven applications.
• All blocks of this diagram are movable.
• Configuration of the lines which demonstrate the real links between the blocks can be changed by users.
• Parameters of visualization can be changed.
• All changes are saved so that the diagram can be restored in exactly the same way on the next opening.
Elements of three main types are used to construct such diagram.
• The upper menu – analogue of the main menu from the Demo application – is represented by an object of the
UpperMenu_BD class. Abbreviation BD is for Block Diagram.
• All other rectangles in this diagram represent submenus; these are the objects of the Submenu_BD class.
• Lines between blocks are classical polylines. Some of them have fixed ends; the end points of other lines are
movable. All previous versions of the same example used one class of polylines. The new version uses two
classes of lines which differ only by fixed or not fixed end points.
Before we start the discussion of this example, we’ll look at several needed classes and one preliminary example which is
designed to explore the work of polylines of the Polyline_Unmovable class. One class of polylines was already
demonstrated in the Form_Polyline.cs (figure 4.2), but that old class is inappropriate for the new example. Next table
shows the differences between two classes of polylines and it is easy to see from their comparison, why another class had to
be designed. The only common feature for both classes is the movability of joints; everything else is different.
File: Form_Polylines_Unmovable.cs
Menu position: Graphical objects – Basic elements – Lines – Manual design of polyline
The Form_Polylines_Unmovable.cs example demonstrates polylines of the Polyline_Unmovable class both with
movable and unmovable end points (figure 17.18). A set of fields which is needed for any class of polylines is obvious:
some collection of connected points and a pen to draw the line. There can be also special colors to mark the end points and
joints; the use of these additional marks is regulated by two Boolean fields. This is a standard set of parameters which is
needed for any polyline, so I organized a special class of data which any polyline can use.
public class PolylineData
{
List<PointF> pts;
Pen pen;
bool bMarkJoints, bMarkEnds;
Color clrJoints, clrEnds;
In the old Polyline class the number of points (and segments) is determined at the moment of initialization, so in that
class an array of points is used. In the new classes of polylines (the Polyline_Unmovable class is only the first of
them) the number of points can be changed, so there is a List<PointF>. The Polyline class was discussed at the
beginning of the book, so it used only the basic things. For movement of points in the Polyline_Unmovable class I
decided to use the adhered mouse technique.
public class Polyline_Unmovable : GraphicalObject
{
Form form;
Mover supervisor;
PolylineData linedata;
bool bHeadMovable, bTailMovable;
PointF ptLimit_Left, ptLimit_Right;
The set of nodes in the covers of Polyline and Polyline_Unmovable classes is identical: small circular nodes on the
end points of segments and thin strip nodes along the segments. The set of nodes is the same, but the behaviour, as you can
see from the table on the previous page, is absolutely different. This means that the devil is in the details; in case of covers
a lot of interesting things can be hidden in the Behaviour property of the nodes.
In the Polyline_Unmovable class the ends of segments can be movable and for this purpose they are covered by
small circular nodes. The segments are not movable but must be detected by mover, so they have to be covered by some
nodes. To avoid the segments movability, the behaviour of these nodes is set as Behaviour.Frozen.
Movability of each end point is determined by the value of the associated Boolean field. For the unmovable end point, the
behaviour parameter of its circular node is determined as Behaviour.Nonmoveable and the size of this node is zero,
so it will not block any part of an element to which it is connected. In my examples the movable ends of
Polyline_Unmovable objects can go only left or right, so the cursor shape over these circular nodes is
Cursors.SizeWE.
public override void DefineCover ()
{
List<PointF> pts = linedata .Points;
int nPoints = pts .Count;
CoverNode [] nodes = new CoverNode [nPoints + nPoints - 1];
if (bHeadMovable)
{
nodes [0] = new CoverNode (0, pts [0], Cursors .SizeWE);
}
else
{
nodes [0] = new CoverNode (0, pts [0], 0, Behaviour .Nonmoveable);
}
for (int i = 1; i < nPoints - 1; i++)
{
nodes [i] = new CoverNode (i, pts [i]);
}
int j = nPoints - 1;
World of Movable Objects 565 (978) Chapter 17 User-driven applications
if (bTailMovable)
{
nodes [j] = new CoverNode (j, pts [j], Cursors .SizeWE);
}
else
{
nodes [j] = new CoverNode (j, pts [j], 0, Behaviour .Nonmoveable);
}
for (int i = 0; i < nPoints - 1; i++)
{
nodes [nPoints + i] = new CoverNode (nPoints + i, pts [i], pts [i + 1],
Behaviour .Frozen);
}
cover = new Cover (nodes);
}
Polyline of the Polyline_Unmovable class is never moved as a whole object, so its Move() method is empty!
public override void Move (int dx, int dy)
{
}
Only circular nodes are involved in any movement but they are regulated differently: any joint can be moved without
restrictions, while the end point can be moved only along the previously determined horizontal segment.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0 || iNode == linedata .PointsNumber - 1)
{
PointF pt = Auxi_Geometry.NearestPointOnSegment (ptM, ptLimit_Left,
ptLimit_Right);
linedata .MovePoint (iNode, pt);
AdjustCursorPosition (pt);
bRet = true;
}
else if (iNode < linedata .PointsNumber)
{
linedata .MovePoint (iNode, ptM);
bRet = true;
}
}
return (bRet);
}
As you see from the code of the MoveNode() method, the new position of the caught joint is equal to the current mouse
position. This is possible, because at the starting moment of such movement when a circular node over joint is pressed by
mouse, the cursor is shifted exactly on the joint point and throughout the whole movement the difference between mouse
cursor and the caught joint is zero. The initial mouse shift is done by the
Polyline_Unmovable.StartMoving_Joint() method.
public void StartMoving_Joint (int iPoint)
{
AdjustCursorPosition (linedata .Points [iPoint]);
}
Similar adjustment of the cursor position is done when the node over a movable end point is pressed. Simultaneously the
limits for moving of this end point are declared, so this is done by another method.
World of Movable Objects 566 (978) Chapter 17 User-driven applications
public void StartMoving_EndPoint (int iPoint, PointF ptLeft, PointF ptRight)
{
AdjustCursorPosition (linedata .Points [iPoint]);
ptLimit_Left = ptLeft;
ptLimit_Right = ptRight;
}
Three polylines in the Form_Polylines_Unmovable.cs (figure 17.18) demonstrate different cases of movable and
unmovable end points.
Blue line between two cyan rectangles has two movable end points. Each end point can be moved along the side of the
connected rectangle. Both rectangles of the Rectangle_Standard class are movable and non-resizable.
Green line has two unmovable end points. They are not movable individually, but they are connected to the movable
rectangles, so they are connected to the same points when those rectangles are moved.
Violet line is initialized with unmovable end points. If you try to press and move its end without any additional marks, then
at the first moment you will see some movement, but when you continue to move the mouse farther on, you will see that the
fixed end point is still on its place and you move the line by some other point. This is a classical process of adding new
joint to such line; I’ll explain it several lines below. Another end of this violet line can be moved anywhere, but this is some
kind of a trick. A small circle of the Spot_Unrestricted class is placed on another end of violet line and when this
spot is moved, the end point of the line goes with it.
Maybe the most
interesting thing happens
to polyline when you
need to add a new joint.
The most natural way to
do it would be to press
the line at the point where
you want to put new joint.
If you simply press and
release the line, then a
new joint appears at this
point and the existing
segment is divided into
two segments which
reside on the same line. I
think that the purpose of
adding new joint is not
only to divide some
segment in two but to
change the line
configuration, so the
natural movement will be
to press the line at some Fig.17.18 Different variants of polylines with movable and unmovable end points
point and continue to
move this point without releasing the line. Press-and-move is absolutely natural in this situation, but all segments are
covered by the unmovable strip nodes, so something a bit unusual must be done to organize the needed movement.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtSource is Polyline_Unmovable)
{
linePressed = mover .CaughtSource as Polyline_Unmovable;
int iNode = mover .CaughtNode;
NodeShape shape = mover .CaughtNodeShape;
if (shape == NodeShape .Circle)
World of Movable Objects 567 (978) Chapter 17 User-driven applications
{
… …
}
else if (shape == NodeShape .Strip)
{
int iSegment = iNode - linePressed .PointsNumber;
PointF ptBase =
Auxi_Geometry .NearestPointOnSegment (e .Location,
linePressed .Points [iSegment],
linePressed .Points [iSegment + 1]);
mover .Release ();
int iPoint = iSegment + 1;
linePressed .InsertPoint (iPoint, ptBase);
Invalidate ();
mover .Catch (e .Location, e .Button);
}
}
}
}
ContextMenuStrip = null;
}
Here is a sequence of steps to organize this press-and-move process on the unmovable segments.
• When a strip node of some polyline is pressed, then the number of the pressed segment is easily determined by the
number of the pressed node. The nearest point on this segment is calculated; after it the caught object is
immediately released!
int iSegment = iNode - linePressed .PointsNumber;
PointF ptBase =
Auxi_Geometry .NearestPointOnSegment (e .Location,
linePressed .Points [iSegment],
linePressed .Points [iSegment + 1]);
mover .Release ();
• The new joint is organized at the calculated point; the number of this joint is determined by the segment number.
int iPoint = iSegment + 1;
linePressed .InsertPoint (iPoint, ptBase);
Invalidate ();
• Because the number of points has changed, then the new cover is organized and the new joint is covered by a
circular node as any other. Mouse cursor is not farther away from the center of this joint than the radius of its
node, so when mover gets the command to catch again, then it catches the same line by the new circular node and
can go on with moving this joint because all joints are movable.
mover .Catch (e .Location, e .Button);
All these things happen inside the OnMouseDown() method of the form and without any interruption you can move this
line as if there was a joint all the time.
Let us look at different movements which change configurations of three lines in the current example. Movements of all
joints are provided only by the Polyline_Unmovable.MoveNode() method (the code is shown two pages back), so
these movements are not mentioned anywhere in the code of the Form_Polyline_Unmovable.cs. One end of the violet line
is unmovable. Other five ends of our lines are movable either individually or forcedly, but to analyse these movements, we
need to look at the process of line design.
Violet line is constructed in pair with a small spot which is placed at its tail.
private void OnLoad (object sender, EventArgs e)
{
… …
pts = new List<PointF> ();
pts .Add (new PointF (700, 400));
pts .Add (new PointF (800, 540));
World of Movable Objects 568 (978) Chapter 17 User-driven applications
lineHeadFixed = new Polyline_Unmovable (this, mover, pts,
new Pen (Color .Magenta), false, false);
spotWandering =
new Spot_Unrestricted (lineHeadFixed .Tail, 5, lineHeadFixed .Color);
… …
When this spot – spotWandering – is moved, it orders the tail of the violet line to move to the same position.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is Spot_Unrestricted)
{
lineHeadFixed .MoveTail (spotWandering .Center);
}
Invalidate ();
… …
The head of the green line is connected with the right side of one rectangle while the tail is connected with the left side of
another rectangle. Visually there is no difference between head and tail, but they are distinguished by the point number.
Rectangles are non-resizable, so the shifts of the line ends on their sides – dyHeadFixed and dyTailFixed – are
determined at the moment of initialization and never change later.
private void OnLoad (object sender, EventArgs e)
{
… …
rectHeadOnRight = new Rectangle_Standard (
new Rectangle (270, 70, 140, 160), Color .Lime);
rectTailOnLeft = new Rectangle_Standard (new Rectangle (700, 210, 110, 60),
Auxi_Colours .TransparentColor (Color .Lime, 0.5));
dyHeadFixed = rectHeadOnRight .RectAround .Height - 20;
dyTailFixed = 10;
pts = new List<PointF> ();
pts .Add (new PointF (rectHeadOnRight .RectAround .Right,
rectHeadOnRight .RectAround .Top + dyHeadFixed));
pts .Add (new PointF (rectTailOnLeft .RectAround .Left,
rectTailOnLeft .RectAround .Top + dyTailFixed));
lineEndsFixed = new Polyline_Unmovable (this, mover, pts,
new Pen (Color .Green), false, false);
lineEndsFixed .MarkJoints = true;
… …
When those two green rectangles are moved, then the same two shifts are used to calculate the new positions of the green
line ends.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Rectangle_Standard)
{
… …
else if (grobj .ID == rectHeadOnRight .ID)
{
lineEndsFixed .MovePoint (0,
new PointF (rectHeadOnRight .RectAround .Right,
rectHeadOnRight .RectAround .Top + dyHeadFixed));
}
else if (grobj .ID == rectTailOnLeft .ID)
{
World of Movable Objects 569 (978) Chapter 17 User-driven applications
lineEndsFixed .MoveTail (
new PointF (rectTailOnLeft .RectAround .Left,
rectTailOnLeft .RectAround .Top + dyTailFixed));
}
… …
Blue line is also connected to the sides of two rectangles and when those rectangles and line are initialized, then similar
shifts for the ends of line are determined.
private void OnLoad (object sender, EventArgs e)
{
… …
rectHeadOnBottom = new Rectangle_Standard (new Rectangle (50, 50, 90, 30),
Color .Cyan);
rectTailOnTop = new Rectangle_Standard (new Rectangle (120, 260, 120, 140),
Auxi_Colours .TransparentColor (Color .Cyan, 0.5));
dxHeadMovable = 10;
dxTailMovable = rectTailOnTop .RectAround .Width / 2;
List<PointF> pts = new List<PointF> ();
pts .Add (new PointF (rectHeadOnBottom .RectAround .Left + dxHeadMovable,
rectHeadOnBottom .RectAround .Bottom));
pts .Add (new PointF (rectTailOnTop .RectAround .Left + dxTailMovable,
rectTailOnTop .RectAround .Top));
lineEndsMovable = new Polyline_Unmovable (this, mover, pts,
new Pen (Color .Blue), true, true);
lineEndsMovable .MarkEnds = true;
… …
When rectangles on the ends of the blue line are moved, then the connected ends of the blue line move in the same way as
the ends of the green line. Each end of the blue lines can be also moved individually along the side of the connected
rectangle; the limits of such movement are set when the circular node on one or another end is caught for movement.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
if (mover .CaughtSource is Polyline_Unmovable)
{
linePressed = mover .CaughtSource as Polyline_Unmovable;
int iNode = mover .CaughtNode;
NodeShape shape = mover .CaughtNodeShape;
if (shape == NodeShape .Circle)
{
Rectangle rc;
if (iNode == 0)
{
rc = rectHeadOnBottom .RectAround;
linePressed .StartMoving_EndPoint (0,
new PointF (rc .Left, rc .Bottom),
new PointF (rc .Right, rc .Bottom));
}
else if (iNode == linePressed .PointsNumber - 1)
{
rc = rectTailOnTop .RectAround;
linePressed .StartMoving_EndPoint (iNode,
new PointF (rc .Left, rc .Top),
new PointF (rc .Right, rc .Top));
}
… …
World of Movable Objects 570 (978) Chapter 17 User-driven applications
Shifts for the end points constantly change throughout their individual movement, but the new values of these shifts are
calculated only when the end points are released.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iReleased, iNode;
NodeShape shapeReleased;
if (mover .Release (out iReleased, out iNode, out shapeReleased))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Polyline_Unmovable && grobj.ID == lineEndsMovable.ID)
{
if (iNode == 0)
{
dxHeadMovable = lineEndsMovable .Points [0] .X –
rectHeadOnBottom .RectAround .Left;
}
else if (iNode == lineEndsMovable .PointsNumber - 1)
{
dxTailMovable = lineEndsMovable .Tail .X –
rectTailOnTop .RectAround .Left;
}
}
… …
This is all about the movements of elements and their parts. A small context menu can
be called on polyline (figure 17.19). The first command is used to call an auxiliary
form to modify the line; this Form_Tuning_Line.cs was already mentioned in the
chapter Movement restrictions (see figure 11.21).
Fig.17.19
Two commands allow to change the polyline geometry.
Make straight line This command turns polyline into a straight line by deleting all joints and leaving a single segment
between end points.
Delete joint This command is enabled only when menu is called on a joint; in this case it allows to delete the
pressed joint.
The last command allows to mark the joints by the small dark spots. Such spots are helpful when the angle between
neighbouring segments is very sharp or when it is closer to 180 degrees.
After such acquaintance with the Polyline_Unmovable class we can return back to the Form_BlockDiagram.cs
(figure 17.17) and look into some details of its design and work.
Four main classes are used to design this block diagram.
• The most numerous elements are rectangles which represent the submenus, so they belong to the Submenu_BD
class.
• Rectangular area at the top belongs to the UpperMenu_BD class. Texts inside this area can be moved left and
right, but their movements are limited by the neighbours and by the rectangle borders.
• Texts from the UpperMenu_BD object can be connected by polylines with Submenu_BD objects. These
polylines belong to the Link_Uppermenu_Submenu class. Both ends of such line are movable along the side
of rectangle with which it is connected; for upper end it is a rectangle of particular text (command).
• Hierarchy of submenus is represented by polylines of the Link_Submenu_Submenu class. Line of this class
connects a pair of Submenu_BD elements and both ends of such line are fixed at those points on submenus where
they were placed at the moment of initialization.
World of Movable Objects 571 (978) Chapter 17 User-driven applications
When the whole upper menu is moved, then coordinates for rectangles of all inner texts are changed synchronously and the
upper ends of all connections are moved by using the corresponding values from the dxShift[] array.
public override void Move (int dx, int dy)
{
rcMenu .X += dx;
rcMenu .Y += dy;
for (int i = 0; i < rcs .Length; i++)
{
rcs [i] .X += dx;
rcs [i] .Y += dy;
}
for (int i = 0; i < m_links .Length; i++)
{
if (m_links [i] != null)
{
m_links[i].MovePoint (0, rcs[i].Left + dxShift[i], rcs [i].Bottom);
}
}
}
Polylines between submenus – objects of the Link_Submenu_Submenu class
Hierarchy of submenus is demonstrated by polylines which connect them. Positions of the end points are determined only
by rectangular areas and fonts used in two connected submenus. These points cannot be moved individually, so a
PolylineData object contains all the needed data to describe such line.
public class Link_Submenu_Submenu : GraphicalObject
{
Form form;
Mover supervisor;
PolylineData linedata;
Submenu_BD subOnHead, subOnTail;
Cover for this class is standard for all polylines: circular nodes on the ends of segments and strip nodes along segments.
There are no variations with movable or unmovable end points; both ends are unmovable, so the nodes over them are
unmovable and their size (radius) is zero. Cover of the Link_Submenu_Submenu class is a simplified version of the
cover from the Polyline_Unmovable class.
public override void DefineCover ()
{
List<PointF> pts = linedata .Points;
int nPoints = pts .Count;
CoverNode [] nodes = new CoverNode [nPoints + nPoints - 1];
nodes [0] = new CoverNode (0, pts [0], 0, Behaviour .Nonmoveable);
for (int i = 1; i < nPoints - 1; i++)
{
nodes [i] = new CoverNode (i, pts [i]);
}
int j = nPoints - 1;
nodes [j] = new CoverNode (j, pts [j], 0, Behaviour .Nonmoveable);
for (int i = 0; i < nPoints - 1; i++)
{
nodes [nPoints + i] = new CoverNode (nPoints + i, pts [i], pts [i + 1],
Behaviour .Frozen);
}
cover = new Cover (nodes);
}
Each Link_Submenu_Submenu object can exist only as a connection between two submenus. Positions of the end
points are strictly determined by areas of those submenus and their fonts. The only allowed movement which has to be
described in the MoveNode() method is the movement of joints. When such movement starts, the cursor position is
adjusted to the joint point and throughout this movement the cursor determines the exact position of the caught joint.
World of Movable Objects 575 (978) Chapter 17 User-driven applications
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
linedata .MovePoint (iNode, ptM);
bRet = true;
}
return (bRet);
}
Polylines from upper menu to submenus – objects of the Link_Uppermenu_Submenu class
Lines from upper menu to submenus are similar to lines between submenus but there are two differences. First, the head of
such line is connected not to the whole upper menu but to some text in this menu, so the number of this text must be among
the fields of the new class. Second, end points are movable. They are movable only along the horizontal segment; this
segment [ptLimit_Left, ptLimit_Right] is determined at the starting moment of such movement.
public class Link_Uppermenu_Submenu : GraphicalObject
{
Form form;
Mover supervisor;
PolylineData linedata;
UpperMenu_BD menuOnHead;
int iHeader;
Submenu_BD subOnTail;
PointF ptLimit_Left, ptLimit_Right;
The cover is similar to the previous case, but circular nodes over end points have normal size and the cursor has the shape
which clearly informs about possible movement left and right.
In the auxiliary example Form_Polylines_Unmovable.cs, the shifts for end points were determined only at the finishing
moment of their movement. In the current example these shifts are calculated throughout the movement and no special
calculation is needed when the end points are released. The consequences of such change will be seen a bit later.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
PointF pt = Auxi_Geometry .NearestPointOnSegment (ptM,
ptLimit_Left, ptLimit_Right);
linedata .MovePoint (0, pt);
AdjustCursorPosition (pt);
menuOnHead .RenewShift (iHeader);
bRet = true;
}
else if (iNode == linedata .PointsNumber - 1)
{
PointF pt = Auxi_Geometry .NearestPointOnSegment (ptM,
ptLimit_Left, ptLimit_Right);
linedata .MoveTail (pt);
AdjustCursorPosition (pt);
subOnTail .RenewShiftForUpperConnection ();
bRet = true;
}
… …
World of Movable Objects 576 (978) Chapter 17 User-driven applications
Now let us see how these four classes work together. I tried to organize some good enough default view. It is good enough
from my point of view, but you are welcome to change this view to whatever you want. Construction of the default view
starts by placing the most numerous elements – submenus. In the current version, there are 20 of them. All their inner
strings are known; separators are marked as “Separator” strings. I move through the list of submenus from left to right
trying to place them in a strict order but occasionally making some adjustments in an attempt to pack them better on the
screen. Here is the initialization of one of submenus.
public void DefaultView (Font fnt)
{
… …
cxL = submBasicElements .RectAround .Left;
submTexts = new Submenu_BD (this, new PointF (cxL,
submBasicElements .RectAround .Bottom + dySpaceBetweenSubs),
new string [] { "Text_Horizontal class...",
"Information on request...",
"Separator",
"Rotatable texts...",
"Texts with variable positioning...",
"Comments to points..." }, fnt);
… …
After submenus comes the time for upper menu. The problem is not in placing the main (wide) rectangle and the texts
inside it; the problem is in such placing of those texts that all their connections will be easy to describe when the time for
their initialization will come. The current version of upper menu contains eight inner elements.
public void DefaultView (Font fnt)
{
… …
string [] strsUpper = new string [] { "Covers", "Graphical objects",
"Movement restrictions", "Controls", "Groups",
"Applications", "Miscellaneous", "About" };
Size [] sizes = Auxi_Geometry .RoundMeasureStrings (this, strsUpper, fnt);
int minSpace = 10;
float [] cxLeft = new float [sizes .Length];
cxLeft [0] = submCovers .RectAround .Left; // Covers
… …
cxLeft [7] = cxLeft [6] + sizes [6] .Width + 2 * minSpace; // About
menuUpper = new UpperMenu_BD (this, spaces .FormSideSpace, cyT_menuUpper,
1400, 10, strsUpper, cxLeft, fnt, true, true);
… …
At the next step all connections from upper menu are organized. By default they are all straight lines, but these are
polylines which can be reconfigured at any moment later. Only one text inside upper menu has no such connection.
public void DefaultView (Font fnt)
{
… …
RectangleF [] rcHead = menuUpper .Headers;
linesFromUpperMenu [0] = new Link_Uppermenu_Submenu (this, mover,
(rcHead [0] .Left + rcHead [0] .Right) / 2,
penLinesFromUpperMenu,
menuUpper, 0, submCovers);
… …
linesFromUpperMenu [6] = new Link_Uppermenu_Submenu (this, mover,
(rcHead [6] .Left + rcHead [6] .Right) / 2,
penLinesFromUpperMenu,
menuUpper, 6, submMiscellaneous);
… …
The last step is the initialization of all connections between submenus. It is similar to the lock assembling when you keep
several elements in their places with your fingers, then you place the cover and after it mechanism can work by itself
without any outer help. Certainly, it works only if you have assembled it in the right way. In the case of block diagram, it
means that for each connection the correct submenus at the ends are declared and the number of position in the first
World of Movable Objects 577 (978) Chapter 17 User-driven applications
submenu from which this connection starts. There are 13 connections of the Link_Submenu_Submenu class in the
current version. If all submenus of lower level would be placed strictly at the same horizontal coordinate as we have in real
menus, then it would be possible to organize these connections in default view as straight lines. But the submenu of basic
elements (submBasicElements) has eight submenus at the next level and in this way all of them do not fit even into my
big enough screen. I had to move some of those submenus and then I decided to give their links a real polyline view. By
default, each of them consists of three segments: horizontal, vertical, and horizontal. Of other links between submenus,
some are straight and others consist of three segments.
public void DefaultView (Font fnt)
{
… …
linesBetweenSubmenus .Clear ();
linesBetweenSubmenus .Add (new Link_Submenu_Submenu (this, mover, penLines,
submGraphicalObjects, 0, submBasicElements));
linesBetweenSubmenus .Add (new Link_Submenu_Submenu (this, mover, penLines,
submGraphicalObjects, 1, submTexts, 0.66));
… …
Our block diagram is ready and can work as any other user-driven application. Movements of elements of four different
classes can change the view of this diagram. There is also one interesting movement which is started not by pressing these
objects but by pressing at any empty place.
Connection between upper menu and some submenu can be pressed for movement at any segment end or anywhere inside
some segment. If one of circular nodes is pressed, then the Link_Uppermenu_Submenu.StartMoving() method is
called. The cursor is adjusted exactly to the position of the segment point; if it is one of the line end points, then the limits
for its movement are calculated. If a segment is pressed anywhere between its end points, then the new joint is organized
and the movement of this joint can go on.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
NodeShape shape = mover .CaughtNodeShape;
if (grobj is Link_Uppermenu_Submenu)
{
linePressed_UpSub = grobj as Link_Uppermenu_Submenu;
if (shape == NodeShape .Circle)
{
linePressed_UpSub .StartMoving (iNode);
}
else if (shape == NodeShape .Strip)
{
int iSegment = iNode - linePressed_UpSub .PointsNumber;
PointF ptBase = Auxi_Geometry .NearestPointOnSegment (
e .Location,
linePressed_UpSub .Points [iSegment],
linePressed_UpSub .Points [iSegment + 1]);
mover .Release ();
int iPoint = iSegment + 1;
linePressed_UpSub .InsertPoint (iPoint, ptBase);
Invalidate ();
mover .Catch (e .Location, e .Button);
}
}
… …
World of Movable Objects 578 (978) Chapter 17 User-driven applications
Pressing of any connection between submenus is similar; the only difference is in absence of circular nodes over the end
points.
Interesting movement starts when the left button is pressed at any empty place outside all the movable objects. This block
diagram is really big. It can fit into big screen if the smallest font is used, but in all other cases it will be bigger than the
screen. Viewing of such big object can be organized in two different ways: either using the scroll bars inside the form or
moving of the form which can be bigger than the screen. In the Form_BlockDiagram.cs and a couple of other examples in
this Demo application I use the second variant. It was already used in one of the previous examples
(Form_PlotAnalogue.cs, figure 10.6) in which I only mention this possibility without any explanation but promised to do
it later.
When the left button is pressed at any empty place, then a special flag (bFormInMove) is set to true and the shift from
the mouse cursor to the top left corner of the form is calculated. This shift will be used throughout the movement of the
form.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
bFormInMove = true;
sizeMouseShift =
new Size (PointToScreen (ptMouse_Down) .X - Location .X,
PointToScreen (ptMouse_Down) .Y - Location .Y);
}
}
ContextMenuStrip = null;
}
A lot of elements can be moved in this block diagram, but none of them has to be mentioned in the OnMouseMove()
method because everything is done by the MoveNode() methods of the involved classes. In the similar example
Form_Polylines_Unmovable.cs, its OnMouseMove() method includes the calculation of line end point when a rectangle
linked with this end point is moving. In the Form_BlockDiagram.cs the same calculations are called from inside the
Submenu_BD.Move(), UpperMenu_BD.Move(), and UpperMenu_BD.MoveNode() methods. The
OnMouseMove() method of the form would contain a single call for repainting, but there is a small additional part to
calculate the form position when it is involved in movement started at some empty point.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
else
{
if (bFormInMove)
{
Location = PointToScreen (e .Location) - sizeMouseShift;
}
}
}
When the mouse is released and the movement of the form is over, the value of the flag which indicates this movement must
be returned to false.
World of Movable Objects 579 (978) Chapter 17 User-driven applications
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
bFormInMove = false;
}
This block diagram is used to give a better understanding of the whole system of examples and an easier access to any of
them. This access is provided by a double click on those menu items (in the upper menu or in any submenu) which are
associated with the examples.
private void OnMouseDoubleClick (object sender, MouseEventArgs e)
{
int iReleased, iNode;
NodeShape shapeReleased;
if (mover .Release (out iReleased, out iNode, out shapeReleased) &&
e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is UpperMenu_BD)
{
int iText = iNode - 2;
if (iText == menuUpper .Texts .Length - 1)
{
Form_About form = new Form_About ();
form .ShowDialog ();
}
}
else if (grobj is Submenu_BD)
{
long id = grobj .ID;
Submenu_BD sub = grobj as Submenu_BD;
int iLine = sub .LineByPoint (e .Location);
if (id == submCovers .ID) // submCovers
{
if (iLine == 0)
{
Form_Nodes form = new Form_Nodes ();
form .ShowDialog ();
}
else // iLine == 1
{
Form_NnodeCovers form = new Form_NnodeCovers ();
form .ShowDialog ();
}
}
… …
For better visualization, the items associated with examples (in some menu commands such items are called leaves) can be
marked with different color and this brings us to the whole system of visualization possibilities in the
Form_BlockDiagram.cs. As usual, those possibilities are available through a system of context menus. There are six
different context menus; two of them – on connections between upper menu and submenus (figure 17.20) and on
connections between submenus (figure 17.21) – are similar in design and differ only in some small details.
A short explanation under the figure 17.20 “menu on connections between upper menu and submenus” sounds a bit strange,
but it is only a shorter version of much longer and much more correct statement “context menu on connections between
upper rectangle representing the upper menu in the main form of Demo application and rectangles below which represent
the submenus of the main menu in the same form”.
I hope that there is no problem in understanding a small difference between two similar submenus which are shown in the
right half of figure 17.20. Any line can be modified via a special tuning form which can be called by the first command of
menu. One of visualization parameters or the whole set of those parameters can be spread from the pressed line on all other
lines of the same type (connections between upper menu and submenus) or on all connections of the block diagram. But
connections between submenus do not have additional end marks because the end points of such connections cannot be
moved individually. Thus, this parameter of the “upper” connections cannot be spread on another class of connections and
there is one command less in the corresponding submenu. There is no such problem when menu is called on connections
between submenus; in this case the visualization parameters can be spread individually or all together either on connections
of the same type or on all connections in diagram (figure 17.21).
Four other context menus can be called on upper menu (figure 17.22a), on submenus (figure 17.22b), on the titles of
submenus (figure 17.22c), and at any empty place of the form (figure 17.22d).
Filling of these menus with commands is done according to the same rules which are used in many other examples. Title of
any submenu can be hidden through its own menu, but no menu can be called on invisible element, so this title can be
returned back in view only through the menu on its owner – submenu; this is the last command at figure 17.22b. Fonts can
be changed individually through menus on elements, while the unified font can be set through menu at empty places. This
is one of examples where the default view can be reinstalled with default font or with any other font.
The Form_BlockDiagram.cs is included into the chapter which discusses the rules of user-driven applications and without
any doubts this example has to demonstrate the use of these rules. All elements are movable. Al elements are tunable.
There are few tiny elements in this example which by a miracle ran away from this rule. They are really tiny elements –
these are the spots on the joints and end points of connections. To make them tunable, you can easily add commands into
menus on the lines (figures 17.20 and 17.21). Another way is to use slightly different form for line tuning in which the
colors and sizes of additional spots can be modified.
Changing the color of the leaves is the only possibility to change colors for upper menu and submenus. Application can be
seen on many computers; users could set their preferable colors for menus through the standard system way and I decided to
show analogues of menus in the same palette.
World of Movable Objects 581 (978) Chapter 17 User-driven applications
As usual, all visibility parameters are saved on closing the form and the same view is reinstalled when you call this form
again. Only there is some difference in saving / restoring of menus and connections between them.
Elements of the UpperMenu_BD and Submenu_BD classes can be used as stand alone objects, so each class has a pair of
its own IntoRegistry() and FromRegistry() methods; these methods are used in exactly the same simple way as
was demonstrated with the dozens of classes earlier.
Connections of the Link_Uppermenu_Submenu and Link_Submenu_Submenu classes cannot exist as stand alone
objects. They also do not have any additional fields of their own which values need to be saved. Whatever is needed to
reinstall them is inside their field of the PolylineData class, so when any connection has to be saved, then this data is
saved.
private void SaveIntoRegistry ()
{
… …
for (int i = 0; i < linesFromUpperMenu .Length; i++)
{
linesFromUpperMenu [i] .Data .IntoRegistry (regkey,
"UpSub_" + i .ToString ());
}
for (int i = 0; i < linesBetweenSubmenus .Count; i++)
{
linesBetweenSubmenus [i] .Data .IntoRegistry (regkey,
"SubSub_" + i .ToString ());
}
… …
Restoration of the block diagram starts with upper menu and all submenus. If all of them are restored, then the data for all
polylines is restored. After it the block diagram can get the same view it had at the end of the previous session.
private void RestoreFromRegistry ()
{
… …
PolylineData [] dataUpSub = new PolylineData [7];
for (int i = 0; i < dataUpSub .Length; i++)
{
dataUpSub [i] =
PolylineData .FromRegistry (regkey, "UpSub_" + i .ToString ());
if (dataUpSub [i] == null)
{
return;
}
}
PolylineData [] dataSubSub = new PolylineData [Convert.ToInt32 (strs [4])];
for (int i = 0; i < dataSubSub .Length; i++)
{
dataSubSub [i] =
PolylineData .FromRegistry (regkey, "SubSub_" + i .ToString ());
if (dataSubSub [i] == null)
{
return;
}
}
linesFromUpperMenu [0] = new Link_Uppermenu_Submenu (this, mover,
dataUpSub [0], menuUpper, 0, submCovers);
linesFromUpperMenu [1] = new Link_Uppermenu_Submenu (this, mover,
dataUpSub [1], menuUpper, 1, submGraphicalObjects);
… …
This chapter gives a detailed analysis of the rules for user-driven applications and demonstrates several examples of
applying these rules. Examples show the transformation of familiar old programs into applications of the new type. Next
chapter is about more complicated tasks and the examples in the next chapter are the applications which are currently used
throughout the scientific work.
World of Movable Objects 582 (978) Chapter 18 Applications for science and engineering
For many years the computers were used only as the most powerful calculators. Later graphical displays appeared and were
immediately used for better presentation of data and results in scientific and engineering applications. The history of
graphical presentation of different functions includes several decades. All programming libraries on all types of computers
provide the drawing of functions, but programmers continue to write new and newer methods of their own because the
existing standard methods are too general while specialized methods are often better in each particular case.
I started to work on the programs with the presentation of functions before the PC era; in computer history it is the analogue
of the Middle Ages. The hardware and software changed throughout the years, the new programming languages were born,
the new ideas in programming pushed a lot of people (and me also) into rethinking of some basic principles, but for many
years I work mostly on scientific and engineering applications in different areas. The core of all applications in all these
areas is still the same: “Draw me the function”.
There are two milestones in design of scientific and engineering applications during the PC era. The first important moment
happened when the textual mode of monitors was abandoned and programs began to work exclusively in graphical mode.
Then the increasing processor speed allowed to use more complicated (and time consuming) calculating algorithms in
parallel with the real-time graphical presentation of results. For several years the hardware progress ignited the new ideas in
design of scientific and engineering applications making possible the things which could not be implemented on slower
computers.
Somewhere 15 years ago the design of scientific / engineering applications went into the period of stagnation. The new
versions of the big and well known programs are still distributed among the clients every year or two; managers of these
projects continue to declare each version as the biggest achievement in the history of mankind. “You have to buy the new
product if you want to do anything at all!” All these words do not worth the paper they are printed on. If there is really
anything new in those programs, then it is only in the algorithmic part (the core of each program) but not in their design.
This is really a strange situation: the continuing progress in hardware; the C# language which is much better than C++ for
programmers’ work on big and small scientific / engineering applications, and yet there is nothing new or helpful for users
of these applications. Users do not care about the language or the programming environment that designer used throughout
his work; they care only about the result and what they get. And this is absolutely correct because users must do their own
work and the only important thing for them is this: “In what way the new version of this program is better for my work than
the previous version?”
I worked on the design of big scientific / engineering applications for different areas and it became obvious to me that the
problem was not in some particular area but it was a general problem. This general problem has its roots in the main idea of
development: up till now all applications are designer-driven.
Users of scientific and engineering applications are often better or much better (!) specialists in their particular area than the
designers of the programs.** Users of these programs (scientists and engineers) try to solve some very complicated
problems and there is a paradoxical (some times even anecdotal) situation: better specialists have to work inside the range of
*
The first version of this book was written during my long vacation from the Heat and Mass Transfer Institute of the
Belarussian Acdemy of Sciences where I worked as a senior researcher in the department of Mathematical Modelling.
Though I stopped working there in the year 2012, I do not want to change such phrases in the text and will keep them in the
original form even in the new edition.
**
It is funny to watch when the developers of scientific and engineering programs try to reject even this obvious fact.
Automakers do not insist that they are better drivers than car racers, specialists in ballet shoes never claim that they are
better dancers than soloists of the ballet groups, but when I write that users of scientific applications are often much better
specialists in their specific areas than developers of the programs they have to use, then there is often an outcry from the
developers of those applications: “We do not agree with this.” The same point of view anounced by the manufactures of
applications again and again: “We know better than anyone else what is really needed. You have to like whatever is given
to you.”
World of Movable Objects 583 (978) Chapter 18 Applications for science and engineering
problem understanding enforced by lesser specialists. The only way for users to get something new from the programs is to
explain to manufacture that they really need this and that features. If users are successful in their explanations, then maybe
in a year or two they will see some additional parts in the application, but nobody knows beforehand how those users’
explanations will be interpreted by designers. Users have more problems while using the programs in the most difficult
cases, so they try to explain especially these situations to developers. It often happens that developers are simply not
qualified enough to understand those situations and related problems, so they understand the explanations up to the limits of
their knowledge. Then they try to solve the problems in applications according to their understanding which is often far
from what they were told.
Many years ago an adaptive interface was born and eventually became the axiom of design for all the complex applications.
You can find in many books the description of the adaptive interface as a solution to “user – designer” problems because, as
it is written in the books, adaptive interface gives user a chance to select the best solution personally for him. What is never
written and carefully hidden is the fact that user gets a chance to select only between the choices that were predetermined by
a designer according to his understanding of each situation and of his vision of the best solutions in each particular case.
Adaptive interface can soften the problem but not solve it. When I understood that the main problem of all the scientific /
engineering applications is in their being absolutely designer-controlled, I also understood that this hurdle could not be
removed with the help of any known or will be thought out in the future form of adaptive interface. Only something
different will allow to solve this main problem and move forward. Then I came to the idea of user-driven applications.
Such applications have to pass the control over the screen elements to users, thus the algorithm of moving the screen objects
became crucial and had to be invented. When I thought out such algorithm, I immediately tried to apply it to the objects
which I was using all the time in my programs – the scientific plots. Maybe it was a mistake and I had to begin with much
simpler objects (in the way I demonstrate this algorithm in the first part of this book), but an attempt to use this algorithm
from the very beginning on the complicated objects unveiled to me some basic features of user-driven applications and
helped to understand a lot of very important things about the whole area.
I began to solve this problem. First, in some primitive way in order only to move controls outside the plotting areas. When
controls became movable and I had the first, from the current point of view very primitive movable groups, then I could
organize scientific applications of the new type and in reality it was a new paradigm – user-driven applications.
Step 5 – redesign of tuning forms.
The transformation of the main part of my scientific applications from rigid construction into user-driven was definitely not
the end of the road. Everything became movable / resizable in the main form of an application and it looked fine. The
positions and sizes of all elements are now easily changed with a mouse, so each user can rearrange the view of an
application to whatever he wants at any moment. But positions and sizes are not the only parameters of the view. All the
plots, scales, and comments have their parameters of visualization; there are so many of these parameters that they are
usually changed in some auxiliary tuning forms. After playing with the improved plots for some time, I decided to change
some of the visual parameters, opened one of the tuning forms, and tried to move the elements there. It was the most
natural thing to do after all those moving / resizing in the main form, but it did not work: all the tuning forms were still old-
fashioned. Certainly, they were old-fashioned; they were the same tuning forms which I used all the time without any idea
that there was something wrong. My amusement at the first moment when I tried and could not move the elements in those
tuning forms showed me immediately what I had to do. I began to redesign all the tuning forms on the basis of movable /
resizable elements.
It was not some kind of whim; there was one more thing which required redesign of those tuning forms according to the
rules of user-driven applications. The tuning forms for all kinds of scales include a sample of a number or a text; this
sample shows the positioning of all numbers (texts) along the scale which is currently under tuning. This sample is
movable; its movement and rotation are copied by all the numbers (texts) along the scale. When you have any movable
element inside the tuning form, then the mentioned conflict between movable and unmovable elements immediately breaks
out in this form and you have to redesign this form by making all its elements movable.
The next comment came from my colleagues who began to use more and more new user-driven applications in parallel with
other programs with which they work for years and which were designed in the old style. Colleagues caught themselves on
trying to move the objects in those programs in exactly the same way as they moved them in my applications. Certainly, it
does not work (all other programs are designed in an old way), but each time it happens like a mini shock to users: why this
object does not want to move when every other is movable? It takes some part of a second to understand the reason and to
remember that this is a different type of program in which everything is still fixed…
Then I found myself in similar situation. I was working on some part of the program which had to accompany another
article. I was checking for many hours different parts of an application in order to find any possible mistakes. I was
switching from one form to another and while I was doing something in the main form, I tried to move it simply by some
inner point in the same way as I move any graphical object. Certainly, the form did not move in such a way and my first
thought was about some mistake which I had to find. The next instant I remembered that that was not one of my graphical
objects but the form itself which could be moved only by the caption bar. After 20 years of working with Windows I tried
to move a window by its inner point! When it happened again, I started to think that this is not the problem of overworking
but the demonstration of the basic rule for user-driven applications. You quickly got used to the movable / resizable objects
and expect that all objects in all the applications have to work in such a way because it is the most natural way of dealing
with all of them.
Movable / resizable objects do not want to coexist with the fixed objects in any way. If there are movable objects in an
application, they will insist, require, and demand that all other objects around them have to be turned into movable
and resizable. This requirement is not limited by the form in which these movable objects are placed but spreads on all the
related forms, on all the forms of the same application, on other applications… The expanding universe of the movable
objects. (Only in this case I know exactly when and how the Big Bang happened. And it definitely took not the fractions of
a second…)
Step 6 – unexpected consequences.
Whatever is mentioned several lines before is definitely not the new thing for me: colleagues mentioned with laugh their
attempts to move windows by the inner points (not by their title bars!); several times I had tried to do the same. Then some
time ago when I have already finished one of the articles and was rereading the text once more before publishing it on the
web, the idea came into my head that an attempt to move the forms by any inner point was not a problem at all. If the logic
of applications demands the moving of the forms by any inner point, then it can be done in seconds by adding several
primitive lines. It does not even require the use of mover or any special class, so my algorithm has nothing to do with this
solution. Simply change the location of the form synchronously with the mouse move, if the form was pressed by a mouse
at any inner point. To do such a thing, you need a very simple addition into the code of the same three mouse events:
MouseDown, MouseMove, and MouseUp. The code is so primitive!
World of Movable Objects 586 (978) Chapter 18 Applications for science and engineering
In the current version of Demo application this moving of a form by any inner point is used in several examples
• Form_PlotAnalogue.cs (figure 10.6) was discussed in the chapter Complex objects.
• Form_BlockDiagram.cs (figure 17.17) was discussed in the previous chapter User-driven applications.
• Form_Function.cs is discussed in the next section of the current chapter (figure 18.3).
• Form_PlotsVariety.cs is discussed further on in the chapter Data visualization (figure 19.1).
These examples have one common feature: they are populated with a lot of different plots. All those plots can be moved
around by pressing them at any inner point, so it is absolutely natural if the forms can be moved by pressing them at any
inner point. The plots used in these forms are absolutely different, but the technique of moving the forms by inner points is
identical; this easy technique was explained seven pages back in the Form_BlockDiagram.cs example.*
You can try this technique on any of your forms or in other forms of this Demo application and decide for yourself whether
such moving of the standard forms can be useful. For 20 and plus years I was moving windows only by the caption bar;
then the work with the user-driven applications pushed me into thinking that there can be a very useful addition even for
such a well known thing. This is how user-driven applications affect all the things around.
*
Four mentioned examples demonstrate the synchronous move of all objects together with their form. Similar movement of
all objects but without moving their form is practiced for many years in applications of several types, for example, in
programs dealing with maps. This type of moving all objects is demonstrated in the Form_FamilyTree.cs (figure 21.55).
*
Plot and Scale classes are included into the MoveGraphLibrary.dll. Detailed description of methods and properties
of these classes can be found in the MoveGraphLibrary_Classes.doc (or MoveGraphLibrary_Classes.pdf).
World of Movable Objects 587 (978) Chapter 18 Applications for science and engineering
schematic view (figure 18.2) and then return to real plot from figure 18.1. The situation to be discussed is the resizing of
the main plotting area by its circular nodes on the corners. Cover of each scale analogue is equal to the rectangular area
bounded by the main line and two ticks at the ends; for better understanding, the fourth side of rectangle is shown with a
dotted line. To refresh your memory about the cover of yellow rectangle at this figure, you can look at the cyan rectangle
from figure 3.1.
Circular node at the bottom left corner of yellow rectangle is not blocked by any element, so you can press anywhere near
this corner and easily resize the main plotting area.
Now let us look at the top left corner. The length of the scale analogue in the PlotAnalogue class is equal to the
corresponding dimension of the plotting area (figure 18.2). Even when such “scale” is placed along the side of the plotting
area (upper scale on the figure), some part of the corner node of the plotting area still looks out from under this “scale”. In
the top left corner of yellow rectangle, the right half of circular node is blocked by the scale, but the left half is not blocked,
so it is still possible to resize the area by the node in this corner. Maybe such resizing is not very convenient as only half of
the circular node is still available and can be sensed by mover, but at least there is a chance. If you move the green vertical
scale closer to the plotting area and position it on the side of yellow rectangle in the way similar to the upper scale, then it
will block one more quarter of this circular node. There will be still an unblocked part of this node – the top left quarter, but
it is small and really hard to find. I want to mention that the existence of this unblocked quarter of circular node is due to
our use of the scale analogues. The scheme is always a simplification of the real thing; the scheme at figure 18.2 ignores
some important details of calculating a cover for real scales. The details are small, but it is well known that the devil is in
the details.
When a real scale is placed along the border of the plotting area, it leaves nearly no chances for resizing of the main area by
this side and by two corners. Here is a small copy of the lower part of plotting area from figure 18.1, so you do nod need to
jump back and forth in order to compare explanation with
that figure. Sensitive area of any scale includes the main
line with ticks and numbers. These numbers can be
positioned in different ways along the scale, but the
sensitive area of a scale is always a single rectangle. (Because this cover consists of a single rectangular node, I can
mention in this part of the text either cover or node.) In this case, the upper border of the scale cover is determined by the
upper coordinate of the ticks while the lower border of the cover is determined by the lower coordinate of the numbers. Left
and right limits of the cover are determined by the end points of the main line and the outer points of numbers at the ends.
These numbers are often centered to the end points of the scale, so the outer borders of numbers are usually outside the ends
of the main line. This is just the case with our scale, so the scale cover in this case spreads to the left and to the right of the
main line. The cover also spreads up and down from the main line. Thus, the cover of this scale spreads in all directions
from both ends of the scale main line; because this line is positioned exactly on the border of the main plotting area, then the
scale cover spreads in all directions from the corners of the main plotting area and the result is obvious: the scale cover
entirely blocks several nodes of the main area – both circular nodes at the corners and the thin node along the bottom side of
the main area. Thus, the plotting area cannot be resized by moving its bottom side.
For some time this situation looked like a deadlock in the design of plots: I had a nice working object which looked
absolutely fine in all situations except when the scale was placed just on the border of the plotting area. But such placement
of a scale is so common (it is used more often than any other) that this inconvenience marred otherwise excellent solution.
There is nothing to do with blocking of the node along the side, but I tried to find some solution for corner nodes. They are
small but very useful for resizing; they are used much more often than those nodes along the borders. It would be nice to
divide the cover of the main area into two parts (corners separately from borders and the main area) and to register them
separately in the mover queue with the scales registered in between. It would be just a solution, but… The algorithm does
not allow to divide a single cover into two parts and to register them separately. Whether it is a simple cover consisting of a
single node or a complex cover of many nodes, it does not matter; any single cover (!) must be registered as a single entity.
In order to produce any even relatively good solution, I tried to use some tricks like making holes in the cover of a scale, so
that a scale cover would not close the corner nodes of the main area. But such solution had some other negative effects as
these holes were in the cover of a scale regardless of the scale position. Even with the scale staying apart from that special
place on the border, the cover of a scale still had holes. It was not a catastrophe but it was definitely a problem which
bothered me for a long time. Then I came to an excellent, elegant, and easy to implement solution which solved the whole
problem. As often happens, when you think out such neat solution, you cannot understand why you did not see it from the
very beginning. Because it is so obvious.
Let me try to explain this problem once more because the found solution for this problem can be useful in other cases.
Suppose that you have a complex object consisting of the main part and several related elements; all of them are movable.
The whole object must be registered in the mover queue together with all its related elements. These elements can be
moved across the main part, so they are registered ahead of the main part. Elements can be placed in such a way that they
World of Movable Objects 589 (978) Chapter 18 Applications for science and engineering
block very important places for resizing of the main part. It would be nice to divide the cover of the main part into two
halves and register everything in such order: one half of the main element, auxiliary elements, and then another half of the
main element. In such case the part of the main element will be always ahead in the queue and will provide the resizing
regardless of positions of auxiliary elements. It is needed but it is impossible in such way because no cover can be
registered in parts. Is there any solution?
A complex object consists of several parts; each part has its own cover. When a complex object is registered with the
mover, those covers of the parts are placed into the mover queue one after another. I have a problem with some part
blocking the nodes of another part. How to let that overlapped part of the cover work? Well, there is no way to let that part
of the cover work, but what about adding something auxiliary with the identical behaviour and let this new part work ahead
of everything else? What about including into the Plot class another element with a cover that duplicates those four
nodes in the corners and do nothing else but allow to resize the main rectangle at any moment? From this auxiliary element
only the work of its cover is needed, so the element can be invisible and in this way it will not destroy the view of the main
object. (Maybe it took me so long to think out this solution because all the time I was solving the problems of turning
visible objects into movable and here the solution was in adding something auxiliary and always invisible in order to
organize better resizing of visible objects.) The invisible element which is placed on the corners of the main area belongs to
the RectCorners class; this object is always placed ahead of all the scales in the mover queue. Here is the code of the
Plot.IntoMover() method from which you can see the order of all movable parts in the queue. Scales are also
complex objects, so they are registered by their Scale.IntoMover() method.
new public void IntoMover (Mover mover, int iPos)
{
mover .Insert (iPos, this);
foreach (CommentToRect comment in Comments)
{
if (comment .Visible)
{
mover .Insert (iPos, comment);
}
}
foreach (Scale scale in HorScales)
{
if (scale .Visible)
{
scale .IntoMover (mover, iPos);
}
}
foreach (Scale scale in VerScales)
{
if (scale .Visible)
{
scale .IntoMover (mover, iPos);
}
}
mover .Insert (iPos, m_rectcorners);
}
Comments associated with any rectangle (objects of the CommentToRect class) gets the value of rectangle at the moment
of initialization and later they are informed about any change of this rectangle. Object of the RectCorners class also
gets the value of the main rectangle at the moment of initialization and then is always informed about any changes. But this
is not enough and there is one more parameter in the RectCorners constructor – method by which the RectCorners
object informs its “parent” (it can be a Plot object or any other) when any node of its cover is moved.
public RectCorners (Rectangle rect, Delegate_Rect inform)
Cover of the RectCorners object consists of four circular nodes in the corners of the received rectangle. I am not sure
if in any of the previous examples there is a cover consisting of separated nodes. Up till this moment we always dealt with
visible objects and tried to move them by any inner point and to resize by border points. For this we had to cover all points
of the object area, so in all those covers the nodes either overlapped or stayed side by side. In the case of RectCorners,
we have an object consisting of several always separated parts and the cover of such object is constructed of separate nodes.
World of Movable Objects 590 (978) Chapter 18 Applications for science and engineering
public override void DefineCover ()
{
int radius = 6;
cover = new Cover (new CoverNode [4] {
new CoverNode (0, rc.Left, rc.Top, radius, Cursors .SizeNWSE),
new CoverNode (1, rc.Right, rc.Top, radius, Cursors .SizeNESW),
new CoverNode (2, rc.Right, rc.Bottom, radius, Cursors .SizeNWSE),
new CoverNode (3, rc.Left, rc.Bottom, radius, Cursors .SizeNESW) });
if (!Movable)
{
cover .SetBehaviourCursor (Behaviour .Frozen, Cursors .Hand);
}
}
When the mouse cursor is pressed near some corner of the plotting area, then one of these nodes is caught and moved. If
the proposed movement is allowed (there is the same limit on minimal size in the RectCorners class as in the Plot
class), then “parent” is informed about the new value of rectangular area through that method which was passed as
parameter during the RectCorners initialization. Parent – in our case it is a Plot object – changes its main area and
informs all the related elements about this change. We used the InformRelatedObjects() method in the
PlotAnalogue class where all comments were informed about the change of rectangle. With the Plot class it is
absolutely the same, but the list of elements to be informed is much wider. There are comments associated with the main
plotting area, there are horizontal and vertical scales, and there is the same RectCorners object! It has to be informed
because there are changes of the plotting area which are not initiated at the corners and the
InformRelatedObjects() method is the same for all cases. Thus, we have a two way reporting between the Plot
and its RectCorners part about any change of the plotting area.
Fig.18.3 This collection of plots reminds me about the predefined functions which are included into application
Any number of different plots can be shown simultaneously in the Form_Functions.cs. Figure 18.3 demonstrates all the
predefined functions which are included into this example; I use this view as a reminder for myself about those functions.
The form with so many plots in view may look like an artificial example, but I watched not once how the scientists from our
World of Movable Objects 591 (978) Chapter 18 Applications for science and engineering
department of Mathematical Modelling used some of the programs designed on the basis of movable objects. When
application is totally controlled by user, then at any moment the screen shows only those objects which user wants to see.
Any unneeded object is immediately deleted or hidden, but I saw not once that there were more plots on the screen than you
see at this figure.
All plots from figure 18.3 are simple enough: each plot (with one exception) has a single graph, so each of the plotting
areas has one horizontal and one vertical scale; there is also only one comment in each plot – the name of a function.
The coexistence of many plotting areas on the screen is not the only source of having a lot of different movable elements.
In real applications, the use of multiple scales is not an unusual thing. It is a rare situation to see more than one horizontal
scale, but the number of vertical scales can easily go up to four or five. This happens in cases when scientists need to
compare the behaviour of different functions or the distribution of some components, for example, different gases along the
same time period. The value ranges for those functions can be so far from each other that each of them requires its personal
vertical scale with its specific range. The use of many scales noticeably increases the number of different elements on the
screen.
With several functions shown in one plotting area, there is often a requirement for some comments to distinguish and
identify these functions; as a result, there will be several comments associated with the plotting area. The same situation
happens when you need to comment several special points of even a single function. So the plotting area with multiple
comments is not an exception.
Consider also a scale from figure 18.4. This is a standard view of a time scale used to show some data on geology or
paleontology. There is a standard part of scale consisting of the main line, ticks, and sub-ticks along the line, plus there are
numbers (years). There is also a lot of additional information, like the names of periods and eras; existence of these
comments make the data on a plot much more informative. This is the standard scale to be used along the area of a plot, so
this scale is going to stretch or shrink synchronously with the change of the plotting area. All those words along the scale
are the comments which are automatically relocated according to the change of the scale length, but they can be also moved
and rotated individually. I do not think that any specialist is going to change the order of periods, but inside the standard
and well known order the words can be placed individually.
Fig.18.4 A time scale used in geology or paleontology often requires a lot of different comments
I mentioned some situations in which the number of comments even for a single plot or scale can be big enough. All these
examples demonstrate that the number of screen elements can vary from few to really big numbers. And this sets two big
requirements for the Plot class:
• Regulation of movability
• Regulation of visibility
When I write about the regulation of movability and visibility, I do not mean their regulation by developer. These
applications are totally controlled by users, so we are going to discuss how movability and visibility of the screen elements
can be controlled by users.
Only objects registered in the mover queue can be moved, so there is always a simple (the easiest) way of making any
object unmovable: excuse it from this queue. However, this solution has some negative sides.
1. As you can see from the code of the Form_Functions.cs and from many other examples of the accompanying
Demo application, the decision on opening one or another menu depends on the mover sensing an object. If you
exclude some object from the mover queue, this possibility is over as mover deals only with the elements which
are in its queue. In such a way mover can help to turn an object into unmovable, but after it the mover will never
help you to reinstall the movability. You can add another technique of checking the request for menu for such
objects; it can be based on comparison of the clicked point and the screen areas of all objects, but this means the
significant complication of the code.
2. When the change of movability requires not the primitive switch between being movable and unmovable but
requires the switch between other variations of movability, then the exclusion from the mover queue does not work
at all.
Even these two remarks must give you an idea of the solution: with the change of movability, an object must be still kept in
the mover queue, but the cover of this object must be changed.
Before looking into two possibilities of changing a cover, I want to remind you about one of the previous examples in which
the change of movability was already demonstrated. It is done with regular polygons of the RegPoly_CoverVariants
class in the Form_RegularPolygon_CoverVariants.cs (figure 6.1). In that example, the free movement of any polygon
around the screen can be limited to movement only along one or another direction. This is a very special case of
movements limitation which is organized without any change of cover but simply by a slight change of the Move()
method of the used class. This is an easy solution in a very special case, so let us remember it as a possibility but not more.
In general, the demand for change of movement restrictions requires the change of cover.
There are two main ways of changing the cover in order to affect object movability.
• To change the cover design by changing the set and type of nodes.
• To keep the same set of nodes but to change their behaviour.
The first approach can be seen in the same example of the RegPoly_CoverVariants class where three different
types of resizing are provided by three different types of cover. Another example in which the change of movability is
achieved by the change of cover is the Trackbar class which is used and demonstrated in the Form_Trackbars.cs
(figure 10.8). Both mentioned classes are simple; maybe that was the reason why for changing their movability I used the
first way. For classes of objects involved in development of scientific / engineering plotting, the second way of changing
covers is used.
At the beginning of the book, in the section Algorithm, I wrote about one node parameter which is described by the
Behaviour enumeration. When a node is used as a starting point for some movement, then this parameter of the node is
set to Behaviour.Moveable. When such node is pressed and moved, then transformation of the movement of the
invisible node into the visible movement of an object is described by the MoveNode() method of this object (class). If
you need to turn an object into unmovable, it is enough to change the behavioral parameter of the nodes of its cover into the
Behaviour.Frozen. Node with such parameter is recognized by mover, but the MoveNode() method for this node
is not called.
Suppose that you have placed a scale somewhere on the border of the plotting area in the Form_Functions.cs; you move
this area around the screen but you do not want to change accidentally the relative position of the scale. Change of
movability for any scale can be included into the commands of menu which can be called on this scale. However, the
procedure for the scales in the mentioned example is slightly different. Fixing of scales was included into several of my
scientific applications and I was told by their users not once that nobody wanted the individual fixing for each scale. Users
prefer to place all scales at the proper places around the plotting area and then fix all the scales with one command, so this
command Fix scales can be found in the menu of the plotting area; this menu is shown 10 pages ahead at figure 18.7.
When you need to unfix the scales, it is done by a single command in the same menu. By clicking any of these commands,
the Movable property of all scales associated with the pressed plotting area gets its opposite value (true / false).
The Movable property belongs to the base class GraphicalObject from which all other classes of movable objects
are derived. On setting the value of the Movable property, the DefineCover() method of the involved class is
called. Here is this method for the Scale class.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [1];
World of Movable Objects 593 (978) Chapter 18 Applications for science and engineering
if (Movable)
{
Cursor cursor = (LineDirection == LineDir .Hor) ? Cursors .SizeNS
: Cursors .SizeWE;
nodes [0] = new CoverNode (0, Frame, cursor);
}
else
{
nodes [0] = new CoverNode (0, Frame, Behaviour .Frozen, Cursors .Hand);
}
cover = new Cover (nodes);
cover .SetClearance (false);
}
The cover of a scale always consists of a single polygonal (rectangular) node. As was described before, on fixing the scale
its behavioural parameter is turned into Behaviour.Frozen. There is also some change of the cursor which is shown
above this node. When the scale is movable, the cursor above it signals about the possible direction of movement. For
horizontal scales it is a bi-directional arrow pointing North – South; for vertical scales it is a similar arrow pointing West –
East. Over any unmovable scale the cursor always has the Cursors.Hand shape.
Other objects that require the switch of movability are the comments, but for them it is done on an individual basis, so
whenever you need to change the movability of any comment, you have to call menu on this particular comment.
CommentToRect class is derived from the Text_Rotatable class and inherits the DefineCover() method of
the base class. When the command to change comment movability is pressed, then the
Text_Rotatable.DefineCover() method is called.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, Corners));
if (!Movable)
{
cover .SetNodeBehaviourCursor (0, Behaviour .Frozen, Cursors .Default);
}
if (TransparentForMover)
{
cover .SetBehaviour (Behaviour .Transparent);
}
}
Cover for any comment consists of a single rectangular node. For known sizes of the text, its angle, and middle point, the
corners of the rectangular area are calculated by the Text_Rotatable.Corners property; then the standard Cover
constructor with the array of these points as a parameter is used. When comment is fixed, the behavioural parameter of this
node is turned into Behaviour.Frozen and the cursor above it gets the Cursors.Default shape. This is the
difference from the unmovable scale.
Though I use this change of behaviour and cursor for a long time and in many applications, I cannot come to the final
decision about the shape of a cursor over the unmovable objects. Personally, I would prefer to change it into the
Cursors.Default. I do not need any visual indication or any other reminder about the possibility of doing something
even with the temporarily unmovable objects. I know that in my applications ALL objects are movable; they can be
temporarily turned into unmovable, but there is always a possibility of calling a context menu on them as on any other
object. When you work with the user-driven applications for some time, you come to the same understanding of the
situation and do not need any kind of reminder any more. On the other hand, there are always users who are not familiar
with the user-driven applications (like the majority of readers of this book!); for them such a reminder by the change of the
cursor shape is very helpful. Maybe this would be the best solution for everyone: it is possible (and very easy) to make this
default cursor a parameter of the class, so that the default cursor over the temporarily unmovable objects can be decided by
each user. It will be just one more tunable parameter.
The previous part of this subsection appeared in the first version of the book and at that time I was sure that that would be
enough to explain the change of movability. However, while I was busy with writing the book, users of several applications
prepared the requests for needed improvements of those applications; the first task on my return back to programming was
the work on changing the movability of the Plot objects back and forth. This thing can be developed in different ways.
Users of those applications are excellent specialists in their area (thermodynamics); they do not want and do not need to
World of Movable Objects 594 (978) Chapter 18 Applications for science and engineering
keep in mind all possible variations on the programming side of applications they work with. But as a programmer I could
organize more variants than those scientists could imagine, so before fixing one or another variant of movability change, I
wanted to give them a simple example on which they could check the possibilities and decide what they really need in real
applications.
The change of movability by changing the cover is the standard way which can be used for many classes. When you change
movability of a simple object, then all the changes and results are going to be as they were described before. When the
same technique is applied to complex objects, then there can be variations in results, and developers can give users more
possibilities. The Plot class is an excellent example to look into many details and to estimate, what you can do as a
designer.
While working on design of several complex classes, I was thinking more about the movability of the smaller parts than
about the same feature of the main objects. I wrote about it in the chapter Groups while discussing the ElasticGroup
class. Groups with their inner elements and plotting areas with their auxiliary parts have a lot of similarities in behaviour
and design. I have mentioned before that some groups in the Form_PersonalData.cs (figure 15.18) are so densely
populated that they need special commands for fixing inner elements; otherwise those inner elements are often moved
instead of moving the whole group. I thought that exactly the same problems would occur with the plots, so I included into
my scientific programs the commands to fix scales and comments. There are different variants in different applications as
comments and scales can be fixed either individually or all simultaneously, but it was always about immobilizing the small
parts of some big object. I never included into programs the commands to fix the main plotting areas. However, such
request came from users of one scientific program. The DataRefinement application is the theme of one of the next
sections, so here are only several words about it.
The main plotting area in the Form_DataRefinement_Zoom.cs (see figures 18.35 and 18.36 ahead) includes relatively
small circles which represent the (x, y) pairs of data; users can move these circles thus changing the view of a function.
Moving of those spots is one of the main features of that program, but I never thought about changing the movability of the
main plotting area on which those spots reside. However, such a request came from users. They were moving the spots all
the time, but occasionally they pressed the button several pixels aside (those circles, as you will see further on, are small
enough). The whole plotting area is also movable and when the mouse is pressed not on the small spot but at any empty
place near by, then the whole area is moved. It is the unneeded movement and the area can be easily moved back, but such
accidental movement of the plotting area by mistake became annoying. Several minutes was enough to add a menu
command for fixing / unfixing the plotting area, but even this simple fixing can be organized in different ways. I explained
different possibilities to the users of the DataRefinement application and put into code the variant they preferred. Several
days later I got the call from user of another application. He saw this useful addition in the DataRefinement application and
asked for similar change in the program he worked with. But this user saw only one working variant of fixing and did not
hear my explanation of possible variants; it was possible that for him another variant would be preferable. It became
obvious to me that I had to prepare some small demonstration for users to decide, what kind of mechanism to fix / unfix the
parts of the plots in their applications they would like to see. For the purpose of demonstration, I prepared the
Form_PlotPartsMovability.cs which allows to play with movability of a Plot object and its parts.
File: Form_PlotPartsMovability.cs
Menu position: Movement restrictions – Plots, scales, and comments
Figure 18.5 demonstrates a view of the Form_PlotPartsMovability.cs which contains a single object of the Plot class.
This object is initialized by using the simplest constructor, so the only needed parameter in this constructor is a rectangle for
the main plotting area. Plot gets one horizontal and one vertical scale; all parameters of visualization are set by default.
Comments are needed for discussion of movability, so I added one comment to each scale and two comments to the main
plotting area. Texts of comments declare their associations; it is also underlined by using different colors: comments of the
main area are blue, comment for vertical scale is violet, and comment for horizontal scale is green.
private void OnLoad (object sender, EventArgs e)
{
… …
plot = new Plot (this, new Rectangle (100, 40, 400, 300));
Font fnt = new Font ("Times New Roman", 12,
FontStyle .Bold | FontStyle .Italic);
plot .AddComment (0.3, 0.1, "Comment for main area", fnt, 0, Color .Blue);
plot .AddComment (0.8, 0.6, "Another comment for main area", fnt, 20,
Color .Blue);
plot .HorScales [0] .AddComment (0.6, 32, "Comment for HOR scale", fnt, 0,
Color .Green);
World of Movable Objects 595 (978) Chapter 18 Applications for science and engineering
plot .VerScales [0] .AddComment (-30, 0.7, "Comment for VER scale",
fnt, -70, Color .DarkViolet);
… …
Three auxiliary groups are organized as the
RigidlyBoundRectangles objects, so they
are movable (by any inner point) but non-
resizable. All visibility parameters of groups can
be changed via the commands of their context
menu. Menu can be called on any group, but all
changes organized through the commands of this
menu spread on all three groups, so their visibility
parameters are always identical. For better
understanding of the code, I would like to
mention the order of rectangles in these groups.
Each small rectangle is covered by its own node
with the same number and these numbers are
used to decide about the reaction on the mouse
click inside a group. The system of nodes inside
all groups is identical; each group has 11 nodes.
Nodes 0 – 3 small rectangles from top to
bottom.
Nodes 4 – 7 comments to these rectangles in
the same order from top to bottom.
Node 8 title area.
Node 9 frame.
Node 10 slightly enlarged (by three pixels
on each side) frame area.
RigidlyBoundRectangles object is
moved by any point of all its rectangles, so it may
look like a strange thing to have in the cover two
nearly identical rectangular nodes which differ
only by three pixels on each side. In reality the
existence of these two nodes makes two different
operations easier: node 9 gives coordinates to
draw the frame, while node 10 allows to move the Fig.18.5 An example to look into details of movability for Plot
whole group not only by inner points but also by object and its parts
any point near the frame.
Three groups are used to regulate the movability of the main plotting area and all subordinates (scales and comments).
There are four possibilities to change the movability; for each case you can click either the small rectangle or its comment.
Each change is described by two code lines, so instead of organizing 12 short methods, I included those lines directly into
the code of the OnMouseUp() method. The reaction depends on the number of the released node; the mentioned node
numbers will help you to understand the code
GraphicalObject is the base class for all classes of graphical objects. This base class contains a field for movability
(m_movable) and the Movable property to regulate this field.
public bool Movable
{
get { return (m_movable); }
set
{
m_movable = value;
DefineCover ();
}
}
World of Movable Objects 596 (978) Chapter 18 Applications for science and engineering
All classes simply inherit this property from the base class. After writing this phrase, I decided to check myself and found a
single class (!) of graphical objects which has its own new Movable property. The discussion of this class is still ahead,
but I am more than sure that this single exception was organized on purpose.
There are going to be several interesting situations with the movability of plot and its parts in the
Form_PlotPartsMovability.cs ; for easier identification I’ll name these variants by letters.
Case A. Example starts with the situation when
everything is movable (figure 18.6a). Plotting area, both
scales, and all four comments are movable. The view of
plotting area with all subordinates does not depend at all
on movability, so there is no sense in showing it again and
again. Mouse cursor changes its shape over elements
when their movability changes, but cursor is never shown
on the figures. All further changes will be illustrated only Fig.18.6a Initial situation – everything is movable (case A)
by the changes of the groups because each new case is
organized by another selection inside the groups.
As I said, any class of graphical objects inherits Movable property; all four variants inside a group deal with this
property. Four variants can be divided into two pairs.
The first pair of commands – movable / unmovable – calls the Movable property of the base class. Change of
movability always requires some change in the cover, so this property sets the new value for movability of the pressed
object (true for movable object; otherwise false) and calls its DefineCover() method.
Case B. In any complex object, and Plot is definitely a
complex one, covers of the parts are independent. Click
the second from the top rectangle inside the group for the
main area (figure 18.6b). Code below shows that the new
value – false – is sent to the Plot.Movable property
(in reality to the GraphicalObject.Movable
property) which calls the creation of the new cover for the Fig.18.6b Only the main plotting area is unmovable (case B)
Plot object and nothing else.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObject, iNode;
if (mover .Release (out iObject, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
long idPressed = grobj .ID;
if (e .Button == MouseButtons .Left)
{
if (grobj is RigidlyBoundRectangles && fDist <= 3)
{
if (idPressed == rigrectsMainArea .ID)
{
switch (iNode)
{
… …
case 1:
case 5:
plot .Movable = false;
iSelected_MainArea = 1;
break;
… …
We have such result of this action:
• Main plotting area unmovable
• Both scales movable
World of Movable Objects 597 (978) Chapter 18 Applications for science and engineering
There can be some variants for restoring those hidden areas back into view. The easiest way is to restore all of them
without selection. Add the Restore areas line to the menu which is called at empty places (menuOnEmpty); on clicking
this command, the Click_miRestoreAreas() method has to be called. As usual, when a set of visible and movable
objects is changed, the RenewMover() method must be called.
private void Click_miRestoreAreas (object sender, EventArgs e)
{
for (int i = areasHidden .Count - 1; i >= 0; i--)
{
areasHidden [i] .Plot .Visible = true;
areas .Add (areasHidden [i]);
areasHidden .RemoveAt (i);
}
RenewMover ();
Invalidate ();
}
If you want to organize some selection among the previously hidden areas, then it will be more complicated, but the main
idea of the whole mechanism is obvious and simple: the change of a single Visible parameter of any Plot object
allows to hide or restore the plotting area.
Changing the visibility of plots by switching the Visible property can be explored in many different ways. In one of
my scientific applications, users get a chance to organize any number of different pages with any number of plots on each
page. Each page is like a Form_Functions.cs, only each page has an identification – the name which user declares when he
wants to organize a new page. Each page contains the unique set of plots which user has organized and placed on it. No
limitation on the number of pages; no limitations inside any page; plots are easily moved and copied from one page to
another. The switch between the pages is through the dynamically changing list of their names. And each user organizes a
unique combination of pages with unique set of plots on any of them. This is a classical example of the user-driven
scientific instrument and there are a lot of analogues between this instrument and well known Word program.
In Word you may use any number of documents which are stored in separate files. Any document is identified by the file
name; documents can be opened and closed; there is an easy to use method of copying any part of text from one document
into another. The file system provides saving and restoring of documents.
The scientific instrument which I mentioned is organized in similar way. When this instrument was prepared, scientists
began to organize separate pages for different experiments; then some plots were taken from those pages and included into
new pages which were associated with speeches on conferences, with articles, and so on. All plots have a lot of tuning (I’ll
write about it several pages ahead), so it is possible to make a copy of some plot and then absolutely change its view. This
is a user-driven application; everything is movable and tunable. As a developer, I had to provide the unlimited number of
pages and unrestricted number of plots on each of them plus tuning, saving, and restoring. Such instrument was given to
scientists and after it each of them can use this instrument in any way he wants.
Hiding and restoring the plots can be one of the major elements of a big scientific application, but the play with hiding and
unveiling the scales and comments can be even more interesting (from the programming point of view) and has more
variants. This mechanism is similar to what was explained in the case of the ElasticGroup class and its inner
elements. Hiding and unveiling of some parts of the plots will be discussed a bit later because it is entirely based on the
identification mechanism from which I want to start. The identification system was discussed in the chapter Complex
objects; let us look at it once more.
World of Movable Objects 602 (978) Chapter 18 Applications for science and engineering
Identification
Any command in the Form_Functions.cs starts through some line of one or another context menu. If you click the right
button nearly anywhere inside the form and the distance between the points of the MouseDown and MouseUp events is
small, then the chances are very high that one of the context menus will be opened. Any menu is opened when the right
button is released, so here is that part of the OnMouseUp() method which leads to the menu opening.
private void OnMouseUp (object sender, MouseEventArgs e)
{
bFormInMove = false;
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
MenuSelection (grobj);
}
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
iAreaTouched = -1;
ContextMenuStrip = menuOnEmpty;
}
}
}
If the right click happened at some empty place, then the menuOnEmpty is called; otherwise the MenuSelection()
method is called. Method has a single parameter – the released object.
private void MenuSelection (GraphicalObject obj)
{
if (obj is ElasticGroup)
{
groupPressed = obj as ElasticGroup;
ContextMenuStrip = menuOnGroup;
}
else
{
Identification (obj);
if (iAreaTouched >= 0)
{
if (obj is Plot)
{
ContextMenuStrip = menuOnPlot;
}
else if (obj is Scale)
{
ContextMenuStrip = menuOnScale;
}
else if (obj is CommentToRect)
{
ContextMenuStrip = menuOnComment;
}
World of Movable Objects 603 (978) Chapter 18 Applications for science and engineering
}
}
}
If the released object belongs to the
ElasticGroup class, then the
menuOnGroup is called. Other
options include menus for Plot,
Scale, and CommentToRect
classes. Figure 18.7 shows one plot
and all menus which can be called on
its parts. If the screen space allows,
any menu is opened with its top left
corner at the same point where an
object was pressed with a mouse, so
there must be no problems in
association of menus with objects.
Menu on comment is shown at the
top of this figure. Comments can be
associated with scales and plotting
areas; the menu view does not
change, but when the first command
of this menu is used, then the
appropriate tuning form is opened.
Menu for scales is demonstrated even
twice: one for horizontal scale,
another for vertical scale.
Menu for plotting area includes the
biggest number of commands. There
are also two small submenus which
are not shown at this figure.
Menu at the bottom of figure 18.7 is
not associated with any particular
object; this menu is called at any
empty place and its commands are
applied to all objects of the form.
This is also the only menu which
does not require preliminary
identification of the pressed object
because there is no such object.
Before calling any other menu, the
pressed object must be fully Fig.18.7 Context menus can be called on plotting areas, on scales, on comments,
identified and this process always and at empty places
reminds me several lines from
Mother Goose Rhymes:
This is the cat,
That killed the rat,
That ate the malt
That lay in the house that Jack built.
There are no more than four levels of identification in the Form_Functions.cs, so we do not need to remember longer
version of the same rhyme.
Form_Functions.cs has a List of areas.
List<AreaOnScreen> areas = new List<AreaOnScreen> ();
Each area is an object of the AreaOnScreen class which unites a Plot object with a List of functions that are shown in
this plotting area.
World of Movable Objects 604 (978) Chapter 18 Applications for science and engineering
public class AreaOnScreen
{
long id;
Plot plot;
List<FunctionDesigned> funcs = new List<FunctionDesigned> ();
Commands from menu on areas need to know only the number of the pressed area (iAreaTouched) in their List; this is
the easiest identification in the current example.
private void Identification (GraphicalObject obj)
{
long id = obj .ID;
iAreaTouched = -1;
… …
if (obj is Plot)
{
for (iAreaTouched = areas.Count - 1; iAreaTouched >= 0; iAreaTouched--)
{
if (id == areas [iAreaTouched] .Plot .ID)
{
break;
}
}
}
… …
For any pressed scale, three pieces of information have to be found.
• Is it vertical or horizontal scale? This is important because scales are divided between two Lists inside the
Plot object. Depending on the scale orientation, one of two flags – bVerScaleTouched or
bHorScaleTouched – is set to true.
• The order of the touched scale in the List of scales of detected type (iScaleTouched). The
Form_Functions.cs has a slightly simplified version for this case because any plot in this form has only one
horizontal and one vertical scale. In general, there can be more than one scale of each type.
• The order of the plotting area to which this scale belongs (iAreaTouched).
private void Identification (GraphicalObject obj)
{
long id = obj .ID;
iAreaTouched = -1;
bVerScaleTouched = false;
bHorScaleTouched = false;
… …
else if (obj is Scale)
{
for (iAreaTouched = areas.Count - 1; iAreaTouched >= 0; iAreaTouched--)
{
if (areas [iAreaTouched] .Plot .VerScaleOrder (id) >= 0)
{
bVerScaleTouched = true;
break;
}
else if (areas [iAreaTouched] .Plot .HorScaleOrder (id) >= 0)
{
bHorScaleTouched = true;
break;
}
}
}
… …
World of Movable Objects 605 (978) Chapter 18 Applications for science and engineering
On one end are those who do not want to do anything but require the program always to visualize the plots in the best way.
Such users are sure that developer always has to have the same understanding of what is “the best” exactly like they have.
“Do whatever you want but always show me the plots in such a way as I (user!) expect them to look. And do not bother me
with any manual tuning; it is your (programmer’s!) job.” Luckily, the percentage of such users among those who work with
scientific and engineering applications is
minimal.
On the other end are those who prefer to have
everything under their control and to have an
access to any parameter. Whether to use this
control or not they prefer to decide themselves
but they always prefer the systems in which
they can use this control at any moment. I
think that this view is the right one, but there is
one small addition. The full and detailed
control must be really easy. And this is some
kind of a problem when you have so many
parameters to control and tune.
The list of tunable parameters for plots is well
known for years. I think that this list has
hardly changed at all for my plots throughout
the last 20 years, but the way of tuning
absolutely changed during the same period.
The first significant change happened when
the ideas of user-driven applications were
applied not only to the main area of
applications but to all the tuning forms also.
From that moment every new idea in
organizing the groups and moving / resizing of
objects was immediately applied to the tuning
forms.
Figure 18.8 demonstrates the default view of
the Form_PlotParams.cs. This is the tuning
form for the main plotting area of the Plot Fig.18.8 The default view of the tuning form for the main plotting
objects; this form also includes an instrument area (Form_PlotParams.cs)
to switch ON / OFF the visibility of associated
scales. This form allows:
• To change the visualization parameters of the main plotting area.
• To add, delete or modify the comments for this area.
• To switch ON / OFF the visualization of any scale.
• To change the order of lines which are used for drawing graphs inside the plotting area. By double click on the
area of lines or by opening a menu at the same area you can proceed to another tuning form in which the lines can
be modified and added.
Before looking into some details of changing the parameters of visualization, I would like to remind the order of drawing
the parts of the plotting areas. Scales are always shown atop the main area and comments are shown atop of their “parent”
which can be some scale or main area, but there are more details in the plots, so here is the full list of all the painted
elements in the order of their drawing.
Main plotting area
Background, if it is not transparent
Horizontal grid
Vertical grid
Borders
Horizontal scales in such order for each
Main line
Ticks and sub-ticks
Numbers
Comments
World of Movable Objects 609 (978) Chapter 18 Applications for science and engineering
scales. Any scale can be hidden from view through its own menu; all scales of plotting area can be hidden or restored
simultaneously via the commands in the menu on that plotting area (see figure 18.7), but this list of scales is the only place
to restore the visualization of scales on an individual basis. This is very important in case of multiple scales, because it is
really the case when the visualization of scales needs to be applied on an individual basis.
The group to add/delete/modify comments (figure 18.13) includes more elements than any other group of this tuning form.
The group consists of two main parts. Each part has one dominant control associated with several small buttons, so each
part is designed as a DominantControl object.
The first part contains:
• List of all the comments associated with the main plotting area. Checkbox in each line allows to switch ON / OFF
the visibility of each comment.
• Button to replace the text of the selected comment with the new
one.
Fig.18.13 A group to add/delete/modify
• Button to add the new comment. comments
Several remarks on using the Comments group.
1. The only button in this group which is always enabled is the button to add the new comment. Other four buttons
are enabled only when one of the comments is selected in the upper List.
2. When the new comment is added, two things happen: the comment itself appears in the middle of the plotting area; line
with the text of the new comment appears at the end of the List.
3. When the color or the font for comment is changed, it is also stored as the parameter to be used for the next new
comment.
4. Usually the comments are short and are shown in one line. But there is no such limitation and any comment can be
shown in several lines. When the text of the new comment is typed inside the area at the bottom of Comments group, it
can be typed in several lines and will have the same view on the screen, but in the List of comments inside the same
group it will appear in one line.
Group Comments is the most densely populated in this form
(figure 18.8). In other groups, it is not a problem to find an
empty place inside in order to move a group around the screen.
In the group Comments, there are only controls inside and
those controls are placed near each other. Each movable
control is surrounded by an invisible but sensitive frame and Fig.18.14a Menu on group Fig.18.14b Menu on
together these frames block nearly the whole are of the group. Comments other groups
In order to make the moving of this group easier, there is an
extra command in menu which is called on this group. When inner controls are placed in the proper way, then the Fix
group elements command (figure 18.14a) makes the frames of inner elements transparent for move and the group can be
moved by any point which is uncovered by controls. If you need to rearrange such group, then slightly different command
of the same menu unfixes all inner elements.
The Plot class has a significant number of methods to draw the functions. For drawing of Y(x) or {X(r), Y(r)} functions
special class of lines is used – the MarkedLine class. Parameters of such
lines include color, type of line, and additional markers which can be placed
along the curve. Special group (figure 18.15) in the tuning form allows to make
some actions with the line samples.
Methods of the Plot class which are used for drawing of graphs reference the Fig.18.15 The group to deal with
needed line not directly but by its number in the inner List. Suppose that some lines for drawing functions
World of Movable Objects 611 (978) Chapter 18 Applications for science and engineering
line is used for drawing; if this line is changed, then the view of the graph changes. Line samples in the group Lines for
functions are shown from left to right according to their order in the inner List. By moving a sample to the new position,
the order of lines is changed. If the lines at the head of the List are changed, then with the high probability some of the
drawn functions have to be shown by different lines and this change is immediately seen in the plotting area. (The
explanation is much more complicated than the real process of changing the view of the drawn function. Move the left
sample to any other position and you will see the change in your plotting area. This will immediately explain the whole
process.)
Other actions with the lines can be started only via the context menu which is called at the area of samples. The minimum
number of lines associated with any plotting area is eight. If there are more than eight samples in the area and you want to
delete some of them, open a context menu on the unneeded line and delete it. If you want to modify some samples or add
new lines, the same menu allows to open an auxiliary tuning form (figure 18.16). The same tuning form can be opened by
the left double click in the area of samples.
As I have already mentioned, the Plot class has a number of methods to draw Y(x) functions and parametric functions
{X(r), Y(r)}. Each function is painted by a line of the MarkedLine class. An object of this class has two main parts –
line and markers; of these parts at least one must be shown. So any graph can be shown in three different ways:
• By a line
• By a line with markers
• By markers without any line.
The auxiliary Form_PlotColorParams.cs is shown at
figure 18.16. The order of lines can be also changed
here as in the main tuning form, but more changes are
allowed in this form. By clicking any sample line, you
make it currently tunable; the small red triangle marks
such line. For this line, it is possible to set:
• Line color
• Line width
• Line style
• Type of markers
• Color of markers
• Size of markers.
All changes made in this form are not going into the
plotting area immediately but only when this auxiliary
form is closed with the OK button.
Fig.18.16 Tuning of lines (Form_PlotColorParams.cs)
Let us return one level back (or up) to the main tuning
form of the plotting area (figure 18.17a). This tuning form allows to do a lot of things, but you do not need all these
possibilities all the time.
• The setting of the border lines or grids can be useful from time to time, but if you work with some complex
scientific program day after day, then you set the preferable parameters and after it you will need to change them
extremely rare.
• It is also possible that your plotting areas do not have more than one scale of each type and you always want them
to be visible; in such a case the list of scales also becomes superfluous.
• There is an easy way to change the transparency of the plotting area, but I doubt that scientists are going to use this
possibility too often. They mostly do not like to change the background of the plotting area either. This makes the
whole Main area group redundant.
Taking all these things into consideration, it leaves only two groups which are used often enough: Lines and Comments. If
only two groups are used often enough and other four very rarely, then there is no sense in using the tuning form in its
default view as it is really a waste of the valuable screen space. Whenever you open the tuning form on top of the working
scientific application, you overlap a lot of valuable information which you have to analyse. The less of the plots is closed
by the tuning form, the better, and there is an easy way of shrinking the tuning form exactly to what you really need and not
a pixel more. Everything is movable and resizable! Move the unneeded groups outside the tuning form and rearrange
World of Movable Objects 612 (978) Chapter 18 Applications for science and engineering
whatever is left in the way you prefer. I decided to rearrange the form according to what I have written several lines above;
the result is shown at figure 18.17b.
Fig.18.17a Default view of the tuning form Fig.18.17b One of possible views for the same tuning form
I purposely put two figures side by side; this allows to avoid going back and forth. The left figure (18.17a) was already
shown before – it is the default view of the tuning form. The right one (figure 18.17b) is the same tuning form. I moved
three temporarily unneeded groups and one ListView control over the right border and dropped them there. Then I
moved ONE control inside the Comments group and slightly changed its size to make two halves of the group even.
Though the Comments group includes seven different controls, its design makes the rearranging easy to do. I mentioned
earlier that this group consists of two parts organized as DominantControl objects, so the TextBox for typing the new
comment moves synchronously with two subordinate buttons. The last touch was the enlarging of the Lines group. All
these changes took only two or three seconds. The new variant shows only the needed parts of the form but occupies only
half of the original space on the screen.
This is a perfect demonstration of the power and effectiveness of user-driven applications when there is a request of
rearranging a form for your particular needs. Here are four main characteristics of this process.
1. There is no fixed list of possible views of the form. The designer (in this case it is me) is not giving you the finite
list of possible solutions from which you can select one or another. Instead, you get an instrument which gives you
an infinitive number of possibilities. Any solution is determined by your demand at the current moment and by
your taste of proportions, relative positions, and so on.
2. Going from one solution to another is really quick, just seconds.
3. Whatever is done is saved; none of your work on rearranging the form is going to be lost.
4. You can always return to the default view by a single command. At any moment you can call the context menu
outside the groups, order the default view to be restored, and immediately return to the view shown at
figure 18.17a.
Let us return back to the Form_Functions.cs (figure 18.3). Any plotting area in this form has one horizontal and one
vertical scale. Scales have a significant number of parameters that can be tuned in special forms. There are different ways
to open these forms:
• By double clicking the scale
• Through the context menu on the plotting area (figure 18.7).
• Through the context menu on the scale itself (figure 18.7).
The tuning forms for horizontal and vertical scales are designed in similar ways, but there are some differences that make
obvious what type of scale the opened form is related to. Figure 18.18 shows the default view of the tuning form for
horizontal scale – the Form_HorScaleParams.cs.
World of Movable Objects 613 (978) Chapter 18 Applications for science and engineering
I have explained in the chapter User-driven applications how the movability of any element begins to demand the spread of
the same feature on all the neighbours, on all the related tuning forms, and so on. The scales are movable, so their tuning
form must be designed according to the rules of user-driven applications. According to these rules, the group Line and ticks
must be movable and tunable in case users would like to rearrange it.
Line and ticks group is movable and resizable automatically, as it is an object of the ElasticGroup class. I have
already described this class in details (see chapter Groups) and mentioned not once that this class is widely used throughout
all my applications. The group can be moved around; in this way the whole tuning form is rearranged. If you do not need
it; the group can be moved outside of view across the right or bottom border of the form. There are eight elements inside
the group; all of them can be moved around; the frame is adjusted automatically; this is a standard feature of the
ElasticGroup class.
Moving of inner elements changes the group view, but what about the
content? What to do if you never want to change, for example, the
number of subintervals, the length of subticks, and the length of ticks? In
this case three controls with comments are not needed while these
elements occupy half of the group area. It would be nice to hide these
elements and to squeeze the group. You can move any of the inner
elements out of view (across the form border) in the same way as the
whole group, but this would be a very strange thing to do. When you
move the whole group, it disappears from view with all its inner elements
and frame. If you move only some inner element, then the lines of the
frame will go after it and you will have a frame starting somewhere on Fig.18.20 Menu for the Line and ticks group
the screen and going to the form border. It is really a strange view, so the
Line and ticks group uses another technique to change its content. If you want to hide some of the inner elements, call
context menu on this group (figure 18.20). Of the
eight inner elements of this group (figure 18.19)
I would say that this is the main group of tunable parameters associated with a scale. The controls of this group are used for
tuning all the time, so there is no instrument of taking out of view some of the inner elements. Of all inner elements of this
group only several buttons are movable; these are four buttons which are seen at the bottom of figure 18.21. All other
elements are composed into three obvious groups; two of them regulate the values and appearance of the end values; the
third group regulates the number (or step which also determines the number) of the inner numbers along the scale. The grid
in the main plotting area corresponds to these inner numbers, so these parameters – step or number of values along the scale
– have some effect on the view of the plotting area. The main idea of my work is to make everything movable and under
the users’ control. There are few situations when screen elements have to be placed in some relative position and their
relative position has to be kept without any change; this is one of those rare situations.
Rules of user-driven applications have to be applied at all levels, but sometimes the application of these rules creates nearly
an infinitive chain of tuning forms. I am not giving an advice to ignore those rules and produce something much simpler; I
only do not want to hide or skip from discussion some complicated cases. Plots are complex objects not only from mover
point of view; Plot object contains a lot of parameters which require tuning, so the main area and its scales have their
own special tuning forms and figure 18.18 demonstrates such form for horizontal scale. The same rules are applied to the
Form_HorScaleParams.cs, so double click on any of its groups opens the tuning form for the pressed group
(Form_ElasticGroupParams.cs, figure 15.30) while double click on information area opens another tuning form
(Form_InfoOnRequestParams.cs is identical to the Form_ClosableInfoParamcs.cs from figure 5.5).*
Changing of several parameters in
the Numbers group (and also in the
Line and ticks group) is organized
in a slightly different way from
changing all other parameters in
the tuning form. Usually, when
you make any changes of
parameters in the tuning form, it
has an immediate effect on the
object under tuning. This works in
the tuning forms for the main
plotting area and for scales. The
exceptions of this rule are those
several controls in which you have
to type the new values. After
typing each new value, you have
to press the Enter button if you
want to introduce this value.
Pressing of this button tells the
program that the new value is
ready, can be checked, and
applied, if found to be correct.
Values along the scales can be
Fig.18.23 Tuning form for vertical scales
from absolutely different ranges,
so only users can decide about the format to show those numbers. Click button Format inside the scale tuning form to open
another auxiliary form – the Form_NumbersFormat.cs (figure 18.22) in which the needed format can be selected.
Programmers know that there are special formats to show the numbers. For many years I worked on developing different
applications for scientists and engineers; nearly all of them were programmers themselves, so they knew the difference
between the basic F and G formats and they could easily pick up the needed format from the list of abbreviations. In the
Form_NumbersFormat.cs those abbreviations are still shown for those who are familiar with them, but each variant is also
demonstrated by two samples, so the selection is easy for everyone.
*
While rewriting this part of the text for new version of the book, I was thinking about one more figure which would
illustrate the whole tree of tuning forms which can be called on the plot, scales, and then on their tuning forms… I tried to
show them all simultaneously and to add some lines which will show the links between them. Plot with scales was
accompanied by six different tuning forms with a lot of interconnections. When I looked at the resulting figure which I
could hardly fit into a page, I dropped this idea. Throughout real work with applications you would never open all these
tuning forms simultaneously, but I think that it gives a good explanation of how the full users’ control is organized in the
real complex applications. It is a good figure for a blackboard during lecture, but looks like I am not skillful enough to
include it into this book.
World of Movable Objects 616 (978) Chapter 18 Applications for science and engineering
Figure 18.23 demonstrates the tuning form for vertical scales. Horizontal and vertical scales have a lot of common features
(they are objects of the same Scale class), so two groups – Comments and Lines and ticks – are identical in two tuning
forms. Group Numbers has the same set of inner elements, but three groups of inner elements are positioned vertically for
better association with vertical scale. Line for the sketch is also vertical. In all other aspects the work of two tuning forms
is identical.
There are two more ways to change the view of tuning forms for scales (figures 18.18 and 18.23); both are through the
commands of context menu which can be called at any empty place outside the groups.
First, the font can be changed. This font is applied to all texts in view, like comments for the controls and the titles of the
groups, and to the controls in which any information is shown or typed.
Another command of the same menu allows to reinstall the default view of the form. This is done by taking into
consideration the font which user could already change.
One more remark about scale tuning. The Numbers group contains three check boxes which allow to switch ON / OFF the
visualization of different numbers along the scale; the range of possibilities is between showing all the numbers and not
showing any numbers at all. The Line and ticks group has three check boxes which allow to switch ON / OFF the main line
and different ticks; the range of possibilities is between showing all the lines and hiding everything. If you switch OFF all
six check boxes in two groups, you will receive a paradoxical situation when a scale is not hidden but at the same time not
visible. To avoid this, the program does not allow to switch OFF all six check boxes simultaneously. With this single
exception, all other variants of switching these six check boxes ON and OFF are available, so you have 63 different
combinations. If a scale must disappear from the screen, it can be hidden, for example, via the context menu, but not by
switching OFF all its parts one after another.
These are not the trivial questions. First, the answers can depend on the purpose of application, so the implementation can
vary from one program to another. Second, the user’s view on the problem can change (and often do change) over time.
Solution according to the rules of user-driven applications would require to give users all the choices to decide, but I am not
sure that in this case this is the best solution. Is it a paradox? I am agitating all the time for the full control passed to users,
but here I oppose this thing.
Well, users have the full control over each plot. What we are discussing now is the parallel way of imposing a whole set of
visibility parameters by a single command. If I am going to introduce users to another tuning form in which I combine a lot
of possibilities from several already existing tuning forms, it would be something huge and, from my point of view,
absolutely unneeded. As I have already underlined, the full control is still there, it works, and nobody is going to take a bit
from it. The faster transfer of parameters from one plot to another can be looked at as an experimental parallel way which
can be constructed after consultations between the particular user and designer.
In real life it works in such a way. I implement in the program some variant of faster tuning according to the users’ request
at the moment. After working with this program for some time they may come to different opinion. Then either I change
that part in the first program or develop another variant for the fast tuning of plots in another program. Programs are
different and users’ requests can be different, so they rarely ask me to unify these tasks. But if they ask, I do. My programs
are for scientists. I never work with that motto “They will have to like whatever they are given”.
You can see an example of the fast tuning in the Form_Functions.cs. Figure 18.7 shows a typical view of context menu
opened on a plotting area. Two commands of this menu allow to use any plot as a sample (Use area as sample) and to use
the sample for changing the visibility parameters of the plot (Change to sample view).
The first command simply organizes an exact copy of the touched plot.
private void Click_miUseAsSample (object sender, EventArgs e)
{
Plot plotSrc = areas [iAreaTouched] .Plot;
plotSample = plotSrc .Copy (this);
}
The copy has the same coordinates of the plotting area and all the auxiliary parts as the touched plot, though not all of these
parts are going to be used when the sample is used later by the second command.
Menu line of the second command becomes enabled only after a sample was organized; at figure 18.7 this line is disabled.
When it is enabled, the Click_miChangeToSampleView() method can be called.
private void Click_miChangeToSampleView (object sender, EventArgs e)
{
Plot plotDest = areas [iAreaTouched] .Plot;
if (plotDest .HorScales .Count != plotSample .HorScales .Count ||
plotDest .VerScales .Count != plotSample .VerScales .Count)
{
MessageBox .Show (
"Sample area and touched area have different number of scales",
"Use sample area", MessageBoxButtons .OK,
MessageBoxIcon .Exclamation);
}
else
{
plotDest .CloseTuningForms ();
Rectangle rc = new Rectangle (plotDest .PlottingArea .Location,
plotSample .PlottingArea .Size);
Plot plotNew = Plot .Copy (this, rc, plotSample);
plotNew .CopyComments (plotDest);
plotNew .HorScales [0] .CopyComments (plotDest .HorScales [0]);
plotNew .VerScales [0] .CopyComments (plotDest .VerScales [0]);
areas [iAreaTouched] .Plot = plotNew;
RenewMover ();
}
}
In the case of the Form_Functions.cs, the quick tuning of the plots works under three conditions:
World of Movable Objects 618 (978) Chapter 18 Applications for science and engineering
• The sample and the plot to be changed must have the same number of horizontal and vertical scales. In this form it
is always true as each plot has exactly one scale of each direction.
• Size of the main plotting area is changed to the size of a sample.
• Comments of the area are not changed.
After setting these rules, the full procedure is going through such steps.
1. The tuning forms of the plot to be changed must be closed.
Plot plotDest = areas [iAreaTouched] .Plot;
plotDest .CloseTuningForms ();
2. The new plot is organized at the place of the changeable one, but it is the copy of the sample and has its size of the
main plotting area.
Rectangle rc = new Rectangle (plotDest .PlottingArea .Location,
plotSample .PlottingArea .Size);
Plot plotNew = Plot .Copy (this, rc, plotSample);
3. All comments from the modified plot are copied into the new plot.
plotNew .CopyComments (plotDest);
plotNew .HorScales [0] .CopyComments (plotDest .HorScales [0]);
plotNew .VerScales [0] .CopyComments (plotDest .VerScales [0]);
4. The modified plot is substituted by the new one.
areas [iAreaTouched] .Plot = plotNew;
5. Movable objects on the screen were changed, so the mover queue must be reorganized.
RenewMover ();
World of Movable Objects 619 (978) Chapter 18 Applications for science and engineering
Analyser of functions
File: Form_Functions.cs
Menu position: Applications – Analyser of functions
30 pages back, while introducing the Plot class, I already mentioned the Form_Functions.cs as an example in which a
lot of Plot objects are used. All plots at figure 18.3 represent the predefined functions which are included into this
example only for the purpose of easier acquaintance. Whenever you want to analyse any other function, it must be defined
in some textual form and interpreted by a program. Before going into the details of the Form_Functions.cs and a couple of
related forms, I’ll write several words about the interpreter used in Demo application.
There are many different situations when you want (or you need) to look at the graph of a function described by some
equation. Maybe you are helping your kids to do some homework, maybe you are teaching students, maybe you try to
imagine the view of some function or the result of its change by one or another parameter, maybe you need to compare
several functions and this comparison is better done by their simultaneous drawing. There can be different variants and at
the base of them all there is the same simple task: to turn function expression into a graph.
Any program implementing such task must allow to type the text of expression and then, if this expression is correct, some
interpreter calculates it for different values of argument and draws the graph. I do not want to write about tiny details of
such interpreter which transforms normal mathematical expression into another form that is more suitable for calculations.
In the 1920s, the Polish mathematician Jan Lucasiewicz invented the notation in which all brackets were omitted and the
operator came before operands; in honour of the author this notation was called Polish notation. In the late 1950s, the
Australian computer scientist Charles Hamblin changed the order of operands and operators and thus invented the reverse
Polish notation. Any interpreter of math expressions is based now on using such notation. In some books published 30 and
more years ago you can find the explanation of such interpreters with the pictures demonstrating the turn of cars at the
railroad station. Each car contains some element of the original expression and, depending on the order of elements, a car
can move straight through the station or can turn for some time into the dead-end only to be taken out of there at correct
moment. In this way the original order of cars is changed and on the output you get the original expression in reverse Polish
notation. Writing such an interpreter became a standard exercise for those who take courses in programming and just learnt
about queues and stacks, so I am not going into the details of coding such interpreter but I am going to use one.
An interpreter of the FunctionInterpreter class is included into the MoveGraphLibrary.dll. This interpreter has
only few methods, but it is enough for its use. Interpretation always starts with analysis of some string which has to be
transformed into a List of elements of the Elem class, so the first method to be used is the
FunctionInterpreter.Analyse() method.
bool Analyse (string strIn, ref List<Elem> elems, out int iError,
out int kErrPlace)
The original function text can include:
• Names of standard functions: { sin, cos, tg, sh, ch, th, ln, lg, exp, sqrt, mod, arcsin, arccos, arctg }.
• Binary operations: { +, –, *, /, ^ }. Symbol ^ is used for degree function.
• Unary operation: –
• Brackets: ( and )
• Variable: { x, p, r, X, P, R }
• Numbers.
If there is any mistake in the text of a function, then the type of error and its place are returned. If the text is correct, then it
is transformed into a special view – a collection of elements List<Elem>; this List can be used for calculations and
drawing. Calculation is done by another method of the FunctionInterpreter class.
bool Calculate (List<Elem> PolishForm, double fArg, ref double fVal)
The Boolean value returned by this method only signals about the correctness of calculation; the result of calculation is
obtained via one of the parameters (fVal).
The results of calculations can be stored in different ways and used further on. To simplify the drawing, the Plot class
includes a couple of methods which use that List<Elem> prepared by the interpreter. The first of these methods is used
to draw Y(x) functions. The function to be shown is calculated for each pixel of the range determined by the horizontal
scale of the plotting area.
World of Movable Objects 620 (978) Chapter 18 Applications for science and engineering
void DrawYofX (Graphics grfx, PlotAuxi auxi, List<Elem> polishY)
Another method is used for drawing of parametric functions {X(p), Y(p)}.
void DrawParamFunc (Graphics grfx, PlotAuxi auxi, List<Elem> PolishX,
List<Elem> PolishY)
Both drawing methods use a parameter of the PlotAuxi class. An object of this class contains several values which are
needed to draw some graph inside the Plot.
• While demonstrating tuning forms of the Plot class, I already mentioned that there is an inner List of lines (of
the MarkedLine class). PlotAuxi contains the number of the line from this List which is used for drawing
of particular graph.
• Not in the current example but in general any plotting area may have several horizontal and vertical scales, so
PlotAuxi contains the numbers of scales which are used for drawing of particular graph.
• Interpreter calculates the function for all needed points and then each pair of consecutive points has to be
connected by a line; thus, any graph consists of a set of segments. There are no questions when both ends of
segment are inside the plotting area, but there can be situations when one or both end points are outside the plotting
area. PlotAuxi decides whether to draw such segments or not. (Even with both end points of a segment being
outside of the plotting area, the segment between them can cross this rectangular area, so some decision about
those cases must be made.)
• For each parametric function, there is a range for parameter and its step; all these values are kept inside the
PlotAuxi.
We do not need to discuss the details of interpreter code; it has to work and it will work. As I said, writing the interpreter
itself is simply an exercise in programming. For our discussion it is much more interesting to look at the design according
to the rules of user-driven applications. I am a designer of this program but I cannot predict the way people are going to use
this analyser and I have no right to put any restrictions on the users’ possible behaviour. There can be any number of the
plots on the screen, each plotting area might contain any number of graphs, those plotting areas can be arranged in any
possible way, there are a lot of visibility parameters, and all of them must be under the full users’ control. User can add,
delete, or change anything, while I have to provide all these possibilities and in the most simple and natural way. In other
words, I design an instrument – analyser of functions – and only users decide WHAT, WHEN, and HOW to show. Let us
look at the design of this program.
First, we need to formulate several main requirements and mark the way of their fulfilment.
• Easy and obvious process to define new functions.
The text of any function can be defined by typing it inside the text box; when the typing is over, the function must
be shown for visual evaluation. There can be mistakes in the text of a function; in this case some information
about the possible problem is needed. It is better to organize the function definition in some auxiliary form.
Declaration of Y(x) functions and parametric functions {X(r), Y(r)} are slightly different, so it is easier to organize
two separate forms but similar in design. Only the approved (!) function is returned into the main
Form_Functions.cs and is included into the list of functions under some name. Those names are also provided by
user, so each user can give the names which are the best for him and which will not cause any misunderstanding.
(Though even this is not a problem as there must be an easy way to rename any function.)
• An arbitrary number of plotting areas can be used in analyser; such areas can be added and deleted at any moment.
In order to add a new plotting area with the graphs of the needed functions, it is enough to select the names of
these functions in the list and press the button. To delete any plotting area, call menu
on this area and use the Delete area command. All the created areas can be also deleted by a single command
from menu which can be called at any empty place.
• Any number of functions can be shown in any plotting area.
The number of functions (graphs) to be shown in any plotting area is unlimited in this program, but all functions in
the particular area are shown inside the same ranges. The Plot class allows to use areas with multiple
horizontal and vertical scales and it is often done in the real scientific applications, but in this analyser I purposely
set the restriction on the scales number. This is more like an application for teaching and I decided that multiple
scales can be too confusing in such case.
World of Movable Objects 621 (978) Chapter 18 Applications for science and engineering
• Easy change of positions and sizes for all the plotting areas.
Movability and resizability of the plotting areas are provided by the Plot class. Positioning of those plots on
the screen has no limitations and is very individual for each user.
In this analyser of functions the screen area works like a desk surface on which you position the sheets with the
graphs. In real life you can put on your desk several sheets of paper, look at them, then move them aside, and
place on the desk another set of sheets with different functions. In the proposed program you can simulate the
same process but in a more efficient way. Any set of plotting areas can be declared as a “view” with its unique
name; an arbitrary number of views can be organized; an elementary list with the names allows to switch between
the views.
• Easy change of all visibility parameters for all the plotting areas.
As usual, all the needed commands are in several context menus; to change some object, user has to call menu on
this object. This rule is slightly changed in case of plotting areas and their related elements – comments. In the
previous examples, the visibility parameters of any comment is changed via the commands of menu on this
comment. The comments do not exist on the screen by themselves but only as auxiliary elements for scales or
plotting areas, so all changes of comments are now done in the tuning forms for scales and plotting areas; you will
see it a bit later.
Form_Functions.cs was designed according to all these requirements. The set of predefined functions shown at
figure 18.3 is included only for easier acquaintance with this example. The main purpose of this program is to show a
function which is described by the text provided by user. These functions can be of two different types which are prepared
in two different auxiliary forms. Both forms are called by pressing buttons inside the Form_Functions.cs. Because the
List of predefined functions is not needed for such work, it can be hidden by the command of menu which can be called
inside the group. I am going to use several buttons of this group, so it is better to have at hand the figure of this group
(figure 18.24). Let us start with the Y(x) functions. Explanation is easier on the basis of some real task. Suppose that you
want to discuss with your students the polynomial functions and want to demonstrate such function
y = (x – 3) * (x – 1) * (x + 1) * (x + 2) * (x + 4)
I think that the best way to explain polynomial functions is to show how it changes with an addition of each new member.
• Two controls – TextBox control for function expression and Label control for possible information about
mistake – can be united into a DominantControl object. You will see this in similar Form_FuncXrYr.cs.
(I would never show two different solutions in similar forms of a real application, but this is a Demo program and I
have nearly a unique chance to show both variants to users. It is the best opportunity for readers of the book to
compare two cases and make their decision about the preferable way.)
• Information can be shown as a comment of the CommentedControlLTP object based on the TextBox
control. This variant is used in the Form_FuncYx.cs, though it is against one of the design rules which I
mentioned several times.
private void OnLoad (object sender, EventArgs e)
{
… …
groupFunction = new ElasticGroup (this, new ElasticGroupElement [] {
new ElasticGroupElement (new CommentedControlLTP (this, textFunction,
Side.S, SideAlignment.Left, 2, "", Font, Color .Red)),
new ElasticGroupElement (btnShowFunc),
new ElasticGroupElement (btnAddFunc),
new ElasticGroupElement (btnChangeFunc),
new ElasticGroupElement (new CommentedControl (this, textName,
Resizing .WE, Side .W, "Name"))},
"Function");
… …
I always advise not to use CommentedControlLTP
objects and CommentedControl objects in the same form
because they are moved in different ways and this is very
confusing for users, but this is exactly the situation which I
organized in this form. I decided to break the mentioned rule
because in this case the comment of the
CommentedControlLTP object appears rarely enough:
when the expression is correct, as at figure 18.25, there is no
comment.
Another unusual feature of this example is the use of two
information areas. One of them contains information about
possible tuning and about function preparation; this is an
InfoOnRequest object which is paired in a standard way
with a small button. The second area is of the
UnclosableInfo class. In the previous version of this
example the same information was organized as another
ElasticGroup object. If you do not like the idea of two
information areas, you can unite them into one. There are
always variants in design; throughout the years my estimation
of better design can change and the same program may have
different views while accompanying one or another article.
Let us start our work with polynomial function. Type the text
Now type the name of the new function (it is already done at figure 18.25) and press button. You will return
to the Form_Functions.cs, the name of the new function will be included into the List of names (figure 18.24), and in the
middle of the form you will see the new plotting area with the graph of the new function. Both the text and the name of the
function are shown as comments to the new plotting area; in our case they are nearly identical, so you can hide one of them
by calling a menu on comment. This menu is shown several pages ahead at figure 18.29c; menu includes commands to
hide and to delete the pressed comment. I always advise not to delete but to hide comment; visually the result is the same,
but the hidden comment can be returned back if there is any need.
We continue to prepare our polynomial function. We can do it in exactly the same way making each new function longer
and the degree of our polynomial higher, but we can make this process a bit easier. Select in the List of names
(figure 18.24) already prepared function and press the button. Program will understand that you want to
change some Y(x) function and will open the Form_FuncYx.cs, but the text boxes for function text and name will contain
the information about the selected function and you will have to type only small additions. (Maybe it is not so crucial in
case of our simple polynomial function but it can be very helpful if you want to prepare several functions in order to
demonstrate how the exponent can be substituted by the sum of several simple functions.)
In this way I prepared five
polynomial functions with
increasing degree. Five
plotting areas appeared
throughout this process in
the Form_Functions.cs. I
set the same relatively small
size for these areas
(figure 18.26). Then I
selected in the List of
functions five lines with
their names and ordered all
of them to be shown in one
plotting area. Later I made
some changes of fonts and
colors, so that any
polynomial is shown now
with the same color both in
its own area and in
company. The last touch
Fig.18.26 A set of functions and plotting areas for discussion of polynomial functions
World of Movable Objects 624 (978) Chapter 18 Applications for science and engineering
Each function has a name. Function of Y(x) type has one text and the reverse Polish notation of this text stored as the
List<Elem>; parametric function has two pairs of text and Polish notation. Names of all prepared functions are shown in
the Form_Functions.cs in one List. Any combination of functions can be shown in one plotting area. When you select
the needed functions and press the button, then new AreaOnScreen object is organized.
This object includes the plotting area and the list of functions to be shown in this area. When the area is organized and its
plotting area (with graphs) is shown; then the shown comments depend on the number of functions.
• If there is only one function, then this plot gets two (or three) comments – the name and the text(s) of the function
– and all these comments are shown. Comments on the screen belong to the Plot object and even if you do
anything with them, it has no effect on the name or text(s) of the function which are stored in the
FunctionDesigned object. If you think that the text is exclusive and it is enough to have only name on the
screen, I advise not to delete this comment (it is possible via menu command) but only to hide it through the tuning
form of this plotting area.
• If there is more than one function, then only their names appear as comments of this plotting area. If you need to
show the text of the function as comment, you need to organize another area with this function only, then you open
tuning forms for both plotting areas and copy comment from one tuning form into another.
Changes of the screen comments do not affect names and texts of functions in the associated FunctionDesigned
objects. If you want to change the name or the view of the function, select this function in the List and press the
button; depending on the type of this function, either the Form_FuncYx.cs (figure 18.25) or the
Form_FuncXrYr.cs (figure 18.27) will be opened. All fields in the opened form – name and text(s) of the function and
parameter values for parametric function – will be shown.
Figures 18.3 and 18.26 show absolutely different views of the same Form_Functions.cs. There are a lot of different plots
in each of these views, so some efforts were used to prepare them. There can be an infinitive number of other views and it
would be nice to save some of them for later use. Form_Functions.cs includes an easy to use instrument to save and
restore any view. All needed commands can be found in menu which is called at any empty place (figure 18.7). Three
commands – Save view As, Select another view, and Rename view – call simple auxiliary forms which are very similar in
design.
Form_Functions_SaveViewAs.cs (figure 18.28a) to save the view under some new name
Form_Functions_SelectView.cs (figure 18.28b) to select another view
Form_Functions_RenameView.cs (figure 18.28c) to rename a view
Fig.18.28a Saving new view Fig.18.28b View selection Fig.18.28c View renaming
Context menus which are used in the Form_Functions.cs were already shown at figure 18.7 and will be discussed in the
next section.
I also demonstrate function analyser as a stand alone program. From time to time I change the view of this program and
some design details in the Form_Functions.cs differ from Function Analyser though the main idea is the same.
World of Movable Objects 626 (978) Chapter 18 Applications for science and engineering
*
I have included into the Form_Functions.cs a simple mechanism to visualize, if needed, the sizes of the plotting area
which is currently under moving or resizing. This is a special feature which can be required by users in some cases but not
too often, so the small additional parts of code are not mentioned in further samples of code.
World of Movable Objects 627 (978) Chapter 18 Applications for science and engineering
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, bShowAngle))
{
GraphicalObject grobj = mover .CaughtSource;
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
bFormInMove = true;
Point pt = PointToScreen (ptMouse_Down);
sizeMouseShift = new Size (pt.X - Location.X, pt.Y - Location.Y);
}
}
ContextMenuStrip = null;
}
The moving / resizing of already caught object is provided by the Mover.Move() method. One small addition in the
OnMouseMove() method allows to improve the view when any control or group is moved around the screen; another
provides the moving of the form by any inner point.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is ElasticGroup || grobj is SolitaryControl)
{
Update ();
}
else if ((grobj is Plot || grobj is RectCorners) &&
ShowAreaDimensions && e .Button == MouseButtons.Left)
{
… …
}
Invalidate ();
}
else
{
if (bFormInMove)
{
Location = PointToScreen (e .Location) - sizeMouseShift;
}
}
}
Any moving / resizing is over when the mouse button is released; at this moment the Mover.Release() method must
be called. The return value of this method informs if any movable object was released or not; another very important
parameter for making the decision on the following actions is the released mouse button.
• If an object was released by the right button, it is often an impulse for opening a context menu. Menu depends on
the class of the released object
• The release of the right button without any previously caught object is often a call to open a special menu at an
empty spot.
private void OnMouseUp (object sender, MouseEventArgs e)
{
bFormInMove = false;
ptMouse_Up = e .Location;
World of Movable Objects 628 (978) Chapter 18 Applications for science and engineering
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
MenuSelection (grobj);
}
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
iAreaTouched = -1;
ContextMenuStrip = menuOnEmpty;
}
}
}
Usually each class of objects has its own menu, so menu selection is easily done by the class of the released object, but
menu view can depend on the particular object, so the full identification is often needed. Details of menu selection and
object identification were already discussed 25 pages back in the subsection Identification.
Plotting areas have many parameters of visualization; these parameters can be changed in the tuning forms. The tuning
forms for the plotting areas and scales are often opened on a double click (the MouseDoubleClick event). The release
of any object during the double click is checked by already mentioned Mover.Release() method; if an object was
released, then its identification must be done.
private void OnMouseDoubleClick (object sender, MouseEventArgs e)
{
if (mover .Release () && e .Button == MouseButtons .Left))
{
Point ptScreen = PointToScreen (e .Location);
GraphicalObject grobj = mover .ReleasedSource;
Identification (grobj);
if (iAreaTouched >= 0)
{
if (grobj is Plot)
{
Plot plot = m_areas [iAreaTouched] .Plot;
plot .ParametersDialog (this, RenewMover, ParametersChanged,
ptScreen);
}
else if (grobj is Scale)
{
Scale scale = grobj as Scale;
scale .ParametersDialog (this, RenewMover, ParametersChanged,
ptScreen);
}
}
… …
Another way of opening the tuning dialogs is often provided via the context menus. Different classes have their own
menus, so it is not a rare situation to have around 10 different menus in a form. Menus often allow to change the visibility
and movability of the elements. Hiding of an object can be organized via its own menu, but the restoration of this object
back into view must be provided via the menu of the “parent” of this object. The menu of the “parent” often includes the
hide / unveil and fix / unfix commands for all the “children” or some groups of “children”; this makes much easier the
identical change of parameters for all the “siblings”. The same type of commands for objects of the upper level (for all the
World of Movable Objects 629 (978) Chapter 18 Applications for science and engineering
plots in the form) can be included into the menu that is opened at empty places of a form. Figures 18.29 demonstrate the
context menus for plot (a), scale (b), and comment (c). More complex objects have more commands in their menus in order
to deal not only with their own parameters but also with all subordinate objects.
the examples of this Demo application, but it is often required by users of scientific applications and is often included into
such programs. I would say that this is one more demonstration of the fact that even very experienced designers with many
years of practice have different view on the applications than users. As programs are designed for users, then these
programs must implement the required things even if, from the designer’s point of view, such request is at least strange.
Throughout the years I had to include into articles and other documents a lot of pictures to illustrate the work of my
applications. I never considered it a problem to organize some view which I liked, to grab it from the screen, and to include
into a document. If I like the view, then it is OK for an article because I am an author. I would say that organizing a good
view of some picture became especially easy in the new applications with all those movable and resizable screen elements,
but… There is a catch. In the old days of standard design, the positions of all screen elements were determined at the
design stage; if some elements had to be lined then they were lined during the design and would stay lined forever. With
the movability and resizability of all screen objects, you can change the view in any way you want, but if you want to line
some objects that do not stay next to each other, there can be a problem. There is similar problem if you need to change
some plotting area in such a way as to give it the exactly required size (in pixels) or ratio of its sides.
Solution to these problems is obvious: add some instrument to inform user about the sizes and positions of the screen
objects and give user an easy way to regulate the appearance of this information. Such instrument is often required by users
of scientific applications. It is included into many of my scientific / engineering applications, but the Form_Functions.cs is
the only example in this Demo application which demonstrates it.
The command in menu (figure 18.30) – Show moving area dimensions – allows to regulate the visualization of this
information, but it is much more interesting to look at its implementation when the information has to be shown.
I did not design any special class to show the needed information; I use the Text_Horizontal class which is widely
used throughout the examples of this book. The Text_Horizontal class is used by approximately 20 examples in the
Demo application, but its use in this example Form_Functions.cs is different from other places. Everywhere else the
Text_Horizontal objects are used in the standard way of all movable objects: you grab it with a mouse, move it to
another location, and release it there. To be used in such a way, any Text_Horizontal object has to be registered with
a mover. In the Form_Functions.cs this special object of the
Text_Horizontal class is used in a unique way: it moves with a mouse,
but it is never caught by the mouse and is never included into mover queue!
The needed information appears at the moment of the MouseDown event
when some plot is caught. Then information moves synchronously with the
mouse during the MouseMove event and disappears when the MouseUp
occurs.
Demonstration of the sizes for an object under change is definitely not a new
thing; for example, it is used for many years in Visual Studio; it is also used
in other similar programs. The only difference is in the placement of this
additional information: in Visual Studio it is shown somewhere at the side, Fig.18.31 While the plotting area border is
while in the Form_Functions.cs I want to show it next to the mouse cursor, moved, information about the
so it would be easier to resize a plot by some border or corner and to see position and sizes of the area is
nearly at the same place the numbers of the changing size (figure 18.31). shown next to the mouse
cursor.
Three variables are used to organize the demonstration of the plot position
and sizes:
bool bShowAreaDimensions; this flag is switched ON / OFF via the command of menu (figure 18.30)
Plot plotUnderChange; to remember the Plot object for which the information is shown
Text_Horizontal infoPlotSize; information about position, sizes, and ratio of the caught plotting area
Everything starts when a plotting area is caught for moving or resizing. There is one thing not to be missed: when any
plotting area is caught for moving, then the grabbed object is definitely of the Plot class, but when the same area is
caught for resizing, then it can be either a Plot object or a RectCorners object.* In the first case the
plotUnderChange is determined directly from the caught object; in the second case several additional lines of code are
needed.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, bShowAngle))
*
The need and the use of the RectCorners class were explained in the section The Plot class of the current chapter.
World of Movable Objects 631 (978) Chapter 18 Applications for science and engineering
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
if ((grobj is Plot || grobj is RectCorners) && ShowAreaDimensions)
{
if (grobj is Plot)
{
plotUnderChange = grobj as Plot;
}
else
{
long id = grobj .ID;
for (int i = 0; i < areas .Count; i++)
{
if (id == areas [i] .Plot .RectCorners .ID)
{
plotUnderChange = areas [i] .Plot;
break;
}
}
}
string str = InfoSizesText ();
infoPlotSize = new Text_Horizontal (this,
new Point (e .Location .X + 16, e .Location .Y + 16), str,
Font, Color .Black, true, Color .Yellow);
CheckInfoLocation (e .Location);
Invalidate ();
}
… …
When the Plot object is determined, then the needed information is prepared by the InfoSizesText() method and
the needed object of the Text_Horizontal class – infoPlotSize – is initialized. By default I try to show this
information slightly below and to the right of the mouse cursor; an additional CheckInfoLocation() method adjusts
the position of this information in case the moving or resizing happens very close to the border of the form.
An object with the information about the position and sizes of the plotting area - infoPlotSize - belongs to the
Text_Horizontal class, so its visualization is done by the Draw() method of this class.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (infoPlotSize != null)
{
infoPlotSize .Draw (grfx);
}
… …
Throughout the moving / resizing of a plotting area, the position of this additional information is adjusted to the mouse
position.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if ((grobj is Plot || grobj is RectCorners) && ShowAreaDimensions)
{
infoPlotSize .Text = InfoSizesText ();
World of Movable Objects 632 (978) Chapter 18 Applications for science and engineering
infoPlotSize .Location =
new Point (e .Location .X + 16, e .Location .Y + 16);
CheckInfoLocation (e .Location);
}
Invalidate ();
… …
When the plotting area is released, then the existing additional information must be erased.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (plotUnderChange != null)
{
plotUnderChange = null;
infoPlotSize = null;
Invalidate ();
}
}
… …
What is missing in the above implementation? Definitely, it is the tuning of that additional information. It is not a problem
at all to tune a Text_Horizontal object, but there is a problem with this one. The problem is a small one, but its roots
are in the unique status and behaviour of this object. In all other cases similar texts exist on the screen all the time, they are
registered in the mover queue in the normal way and, for example, a context menu can be called on such text. The
infoPlotSize object is not registered in the mover queue and, what is more important, it does not exist when a plot is
not caught. So, when no plotting area is moved or resized, then the infoPlotSize object does not exist. You cannot
call a menu on the nonexistent object! So, you cannot call a menu with commands to tune this additional information.
This tuning can be easily organized in some other way, for example, by adding a command (or several commands) into the
menu called at empty places. Some people would agree with this; others would dislike. For this reason I decided not to
include this tuning into the Form_Functions.cs example. This tuning has nothing to do with the discussion of movability;
it is more about the preferable way of applications design; you can organize it in any way you want.
As this section is about the typical design of scientific applications and this typical design is discussed on the basis of the
Form_Functions.cs example, then one more thing could be added to this form. I decided not to do it only in order not to
overload the code, but this thing is used in my other similar applications. When you have a big plotting area with the lines
of different functions inside, you often need to know the exact (x, y) values for some points on the graph. Scales with the
grids can give you the approximate values and all depends on the needed accuracy. Track bars with sliders (see section
Track bars in the chapter Complex objects; figure 10.8) can provide very accurate values, but each slider shows either x, or
y value and track bars are the additional elements on the screen and additional lines across the plotting area.
There is one more way to get the needed information. It is shown in exactly the same way as that additional information
about the moving / resizing of plotting areas, only in the new case similar information is provided while the mouse cursor is
moved across the plotting area. You can do it as another exercise on top of the Form_Functions.cs example; I will give
some tips.
bool bShowXYUnderMouse; this flag regulates the showing of additional information
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
… …
}
else if (mover .Sensed)
{
if (mover [mover .SensedObject] .Source is Plot)
{
World of Movable Objects 633 (978) Chapter 18 Applications for science and engineering
if (page .ShowXYUnderMouse)
{
Plot plot = mover [mover .SensedObject] .Source as Plot;
double arg = plot .CoorToArg (e .X);
double val = plot .CoorToVal (e .Y);
if (infoValueUnderMouse == null)
{
infoValueUnderMouse = new Text_Horizontal (this,
new Point (e .Location .X + 16, e .Location .Y + 16),
InfoValuesText (page, plot, arg, val), Font,
Color .Black, true, Color .LightCyan);
}
else
{
infoValueUnderMouse .Location =
new Point (e .Location .X + 16, e .Location .Y + 16);
infoValueUnderMouse .Text = InfoValuesText (page, plot,
arg, val);
}
Invalidate ();
… …
Information about the values under cursor is shown when nothing is caught by mover and the cursor is moving across the
Plot object. In the very first chapter of the book – Requirements, ideas, and algorithm – I already mentioned that mover
“senses” the movable objects under the mouse cursor and provides the standard set of information about them. This feature
is not used too often, but for showing this particular information it is very useful. One more tip about showing this
information with (x, y) values: it is enough to use only the MouseMove event, but do not forget all the cases when such
information must be erased.
In the next few sections I am going to demonstrate how these rules of typical design are applied in development of real
scientific applications.
World of Movable Objects 634 (978) Chapter 18 Applications for science and engineering
DataRefinement application
File: Form_DataRefinement.cs
Menu position: Applications – Data refinement
Science and engineering cannot progress without new more and more sophisticated programs. Such applications require
huge efforts in development; at the same time the number of users of each particular application is relatively small.
Definitely not millions and rarely even thousands. I saw information that one huge program into development of which I
put big efforts and which was mostly redesigned by me ten years ago was sold in more than 130 countries. This program
has thousands of users, but this is not common for scientific and engineering applications. I would call it an unusual case.
Much more common is a situation when there are hundreds, dozens, or only several users of each program. But these
applications are needed and have to be developed because the research work cannot go on without them.
With such relatively small number of users, it is even more important to develop applications in such a way that EVERY
user has a chance to change it in the best way personally for him. User-driven applications allow to do it; that is why I say
and write all the time that a switch to user-driven applications is the only way out of stagnation for scientific and
engineering programs.
The idea of the program which I am going to demonstrate here was discussed in the department of Mathematical Modelling
years before I came to work there. Colleagues were thinking about such application for a long time but never developed it
because they understood, how ineffective it would be if based on the standard procedure of retyping new values again and
again in some kind of a table. It took me several days to understand all the requirements and to give them the first version
which worked according to all their requests and expectations. Later the program was redesigned and changed according to
new ideas which were born throughout its use.
Scientists put in front of programmers the same tasks again and again until the new programming ideas would produce a
really elegant solution. A lot of purely numerical problems were solved years ago, but the combinations of calculations and
manual adjusting of algorithms usually waits until some moment when the new development in interface would allow to
solve it easily. One of such tasks is the data refinement. A lot of researchers receive the needed data from the analog-
digital converters. This data is going to be analysed by the well known mathematical methods, but the data is often obtained
together with an additional noise and the programs for calculations go crazy while trying to solve the equations with such
input values. Experimental data has to be previously refined by a researcher. It is nearly impossible to do when such data is
shown in an ordinary table, but the needed adjustments are absolutely obvious when the same data is represented in
graphical form. The best solution would be to give scientists some easy to use instrument to adjust such graphs; this is
where the movability of different elements is of high demand.
There are two main things that a researcher would like to do with the input data.
• Each data file consists of a huge amount of data. When you look at the graph of any data file, you immediately see
that it consists of several or many easily distinguishable segments; the view of function in each segment is
absolutely different from the neighbours. Later these segments can be and will be approximated by different
relatively simple functions; the first task is to set the boundaries of those segments.
• Data is received with a lot of noise. In many cases, even after division of the data file into a set of segments, it is
still impossible to use the sets of (x, y) value pairs from any segment as an input for further calculations because
this data is marred with the noise. The second goal of this application is to reduce the noise manually.
The authentic DataRefinement application includes much more options than you see in this example.* I excluded nearly all
the parts which deal with storing and showing some intermediate results because they can be interesting only for specialists
working with the data. I also excluded all the parts which deal with saving of produced data into files. For the real
application, this is the main and the most valuable result, but the majority of those who are going to read this book and try
the application will be very nervous if my program would try to write anything on their computers. I do not want the
readers of this book to become nervous. The main goal of this demonstration is to show how to design scientific
applications on the basis of movable objects and how users are going to work with such programs. All these things which I
want to show can be seen on the crippled version of the DataRefinement program.
Writing of results is excluded from this example, but it is impossible to demonstrate anything without reading some data.
There are three data files in special subdirectory (figure 18.32); if you want to see how the DataRefinement works, you
have to use one of them. You can also prepare your own data files in the needed format and use them to check the
*
The DataRefinement application was commissioned by Dr.Stanislav Shabunya from the Heat and Mass Transfer Institute.
Dr.Shabunya also provided the main ideas and methods of calculation used by this program. In one of my previous articles
[6], I have mentioned the first version of this application by showing one picture with several phrases of explanation.
World of Movable Objects 635 (978) Chapter 18 Applications for science and engineering
All sliders in this plotting area look and move exactly like sliders from the Form_SlidersUnchangeableOrder.cs
(figure 11.10), but there is a fundamental difference between these cases. In the old
example each slider is a separate object which has its own cover and is individually
registered in the mover queue. All sliders at the plotting area in the
Form_DataRefinement.cs constitute a single object.
All movable lines (sliders) in the Form_DataRefinement.cs belong to a single object
of the SegmentedData class. When the form is opened and there is no opened
data file, then an object of this class is only declared. Fig.18.34 Menu which is called
SegmentedData segmenteddata; inside a plot
This object is initialized when the data file is opened and the data is read.
private void Click_miOpen (object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog ();
dlg .Filter = strReadFilter;
dlg .FilterIndex = 1;
if (dlg .ShowDialog () == DialogResult .OK)
{
string filename = dlg .FileName;
if (filename .EndsWith (".bin"))
{
double [] fxRead;
double [] fyRead;
if (ReadBinaryData (filename, out fxRead, out fyRead))
{
fxOriginal = fxRead;
fyOriginal = fyRead;
segmenteddata = new SegmentedData (plot .PlottingArea,
fxOriginal, fyOriginal);
AdjustPlot ();
}
}
RenewMover ();
Invalidate ();
}
}
The newly constructed SegmentedData object has two Lists of data, a List of segments, and a List of
coordinates on which the lines (sliders) must appear.
public class SegmentedData : GraphicalObject
{
Rectangle rc;
Pen pen;
List<double> args = new List<double> ();
List<double> vals = new List<double> ();
List<Segment> segments = new List<Segment> (); // minimum number is 2
List<int> cxLine = new List<int> ();
This object is associated with the data, but visually it is represented by lines on the borders of segments. Each segment has
two borders, so each segment is bordered by two lines, but there is a special situation with the first and last segments. The
left border of the firsts segment is always on the left border of the plotting area; the right border of the last segment is
always on the right border of the same area. These two lines cannot be moved from the plot borders. At the same time the
plotting area is usually painted with its own border. There is no sense in drawing one line atop another especially when
these two end sliders cannot be moved, so these two lines of SegmentedData object are never shown and they are not
covered by nodes. The number of the sliders inside the plotting area is variable; these sliders can be added and deleted, but
at least one slider inside the area always exists.*
*
Menu on sliders has only one command which is used to delete the pressed slider. Because the last slider cannot be
deleted, then menu is not called on slider if it is the only slider inside plotting area.
World of Movable Objects 637 (978) Chapter 18 Applications for science and engineering
Each movable slider is covered by a simple thin rectangular node which can be moved only left or right. Movable sliders
are shown atop the plotting area, so the SegmentedData object is placed ahead of the plotting area in the mover queue.
The area itself is resizable by any border point. I do not want the cover of any slider to block any part of the sensitive
border of the plot, so the node on each slider is a bit shorter than the slider line and the end points of such node are moved
inside the plotting area from its upper and lower borders. Those borders of plotting area are covered by similar thin
rectangular nodes, so the same value – halfsense – is used to determine the width of nodes over sliders and to shorten
those nodes at both ends.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [cxLine .Count - 2];
for (int i = 0; i < nodes .Length; i++)
{
int cx = cxLine [i + 1];
Rectangle rcNode = new Rectangle (cx - halfsense, rc .Top + halfsense,
2 * halfsense, rc .Height - 2 * halfsense);
nodes [i] = new CoverNode (i, rcNode, Cursors .SizeWE);
}
cover = new Cover (nodes);
}
All sliders together, regardless of their number, represent a SINGLE object of the SegmentedData class. This is seen
very well from the RenewMover() method of the form. For mover, the SegmentedData object is a simple one and
it is registered by a simple Mover.Insert() method.
private void RenewMover ()
{
mover .Clear ();
plot .IntoMover (mover, 0);
if (segmenteddata != null)
{
mover .Insert (0, segmenteddata);
}
… …
Sliders can be moved only left or right and their movements are restricted by the borders of the plotting area and the
neighbouring sliders. The restriction can be organized in different ways. I have written in the chapter Movement
restrictions, that usually the limits of node movements are used inside the MoveNode() method. But the code of the
SegmentedData.MoveNode() method does not show any restrictions at all, so there must be something different in
this case.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
cxLine [iNode + 1] += dx; // +1, because the left line is not covered by the node
bRet = true;
}
return (bRet);
}
Sliders exist only inside a rectangular plotting area. If there is a single slider, then its area of existence (area of possible
movement) is equal to this rectangular plotting area. If there are more sliders, then area of existence for each of them is
some smaller part of the plotting area; it is also a rectangular area but with lesser width determined by the neighbouring
sliders. Each slider is covered by a thin node of several pixels width and in many similar cases I would ignore the possible
shift of one or two pixels and use the cursor position as the proposed slider position. However, in this case I need to be
accurate and take into consideration the possible initial discrepancy between the mouse and the line which is caught for
moving; this difference – dx_LineToMouse – is calculated when the slider is caught for movement. As each node (and
associated line) can be moved only inside some rectangular area, then it is possible to set an ordinary Cursor.Clip
restriction on the mouse movement; this restriction is used between the MouseDown and MouseUp events.
World of Movable Objects 638 (978) Chapter 18 Applications for science and engineering
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (mover .CaughtSource is SegmentedData)
{
int iNode = mover .CaughtNode;
int iLine = iNode + 1;
dx_LineToMouse =
e .Location .X – segmenteddata .LineCoordinate (iLine);
int cxL =
segmenteddata .LineCoordinate (iLine - 1) + 1 + dx_LineToMouse;
int cxR =
segmenteddata .LineCoordinate (iLine + 1) - 1 + dx_LineToMouse;
Rectangle rcClip = new Rectangle (cxL, segmenteddata .Area .Top,
cxR - cxL, segmenteddata .Area .Height);
Cursor .Clip = RectangleToScreen (rcClip);
}
}
ContextMenuStrip = null;
}
When mover in the Form_DataRefinement.cs is initialized, then its standard clipping inside the visual part of the form is
organized by default. Objects of different classes can be caught for moving in this form, but the smaller clipping rectangle
is enforced only when the SegmentedData object is caught by mover; for the plot itself or for any of its movable parts
the clipping is unchanged. When any object is released, then the class of the released object is not even checked and simply
the standard clipping of the cursor is reinstalled; the mover clipping is unaffected by all these things.
private void OnMouseUp (object sender, MouseEventArgs e)
{
Cursor .Clip = Rectangle .Empty;
… …
Was there any other way to organize the restrictions for slider movements? Certainly, it can be done in a classical way. I
have explained a bit earlier that there are two lines on the left and right borders of the area; because these lines must always
stay where they are, these two lines were not covered by nodes. (These two lines are not painted, but this is not important
for our discussion of the movement restrictions.) You can cover these two lines with similar rectangular nodes, but better to
make them thinner (for example, 2 pixels wide), and set their behavioural parameter to Behaviour.Transparent. In
such a way they will be not movable, but, being transparent, they will not block the resizing of the plotting area by those
borders. Then you can delete the special clipping from the OnMouseDown() method of the form but include a simple
check for movement of nodes into the SegmentedData.MoveNode() method. Those two transparent nodes at the
ends will be not considered in the MoveNode() method because they are transparent; all other nodes will use the
positions of two neighbours to set the restrictions on their movements. It will be more standard way of organizing the
movement restrictions, but I do not think that it will be better in this particular case. Sliders will be moving correctly
according to the restrictions, but cursor will easily leave a caught slider and move farther on when a slider will be stopped at
the border of an area or by the neighbour. For this reason I decided to demonstrate in the Form_DataRefinement.cs a
slightly different way of organizing the movement restrictions. I want to underline that it is possible because of the
simplicity of the case and rectangular area of the movements.*
Several interesting situations may occur when the caught object is released. The first is the release of a slider after
movement. As a rule, the data in the input file consists of a huge number of (x, y) pairs, so each pixel along the horizontal
scale of the plotting area corresponds to dozens, hundreds, or even more data pairs. Any slider has to be associated with one
particular (x, y) pair, so in such case it is only the question of selection of one of these pairs which correspond to the
particular horizontal coordinate (pixel). But there are situations when there are gaps (in horizontal coordinate) between the
*
There is one more possible way of design for this example. The original Form_DataRefinement.cs was developed
several years ago when I only started to use the adhered mouse technique. Current example in the book is only a simplified
version of the original program. If I would have to develop the same program now, I would use the adhered mouse
technique to move all sliders.
World of Movable Objects 639 (978) Chapter 18 Applications for science and engineering
neighbouring (x, y) pairs; the released slider cannot be left anywhere in between but must be moved to the horizontal
coordinate of the nearest (x, y) pair. This is done by the SegmentedData.AdjustLineToValue() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
Cursor .Clip = Rectangle .Empty;
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is SegmentedData)
{
(grobj as SegmentedData) .AdjustLineToValue (
e .Location .X - dx_LineToMouse, mover .CaughtNode);
Invalidate ();
}
}
… …
Two movable objects in the Form_DataRefinement.cs – a Plot object and a SegmentedData object –are associated
with two context menus. Two first commands of menu on plot (figure 18.34) – adding a slider or opening the
Form_DataRefinement_Zoom.cs – use as a parameter the number of the pressed segment (iClickedSegment), so this
number must be calculated prior to menu opening. This is done inside the OnMouseUp() method. Coordinates of all
sliders are stored in one List inside the SegmentedData object, so the coordinate of mouse cursor is compared with
those values.
private void OnMouseUp (object sender, MouseEventArgs e)
{
Cursor .Clip = Rectangle .Empty;
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
… …
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
if (grobj is Plot && segmenteddata != null)
{
for (iClickedSegment = segmenteddata .Segments .Count - 1;
iClickedSegment >= 0; iClickedSegment--)
{
if (segmenteddata.LineCoordinate (iClickedSegment) < e.X &&
e.X < segmenteddata.LineCoordinate (iClickedSegment+1))
{
break;
}
}
if (iClickedSegment >= 0)
{
ContextMenuStrip = menuOnPlot;
}
… …
If there is a call for menu on slider, then there is a check of another type. The minimum allowed number of segments is
two, so the only slider in view cannot be deleted. Menu on slider contains a single command to delete the pressed slider; if
there are only two segments, then menu is not called.
World of Movable Objects 640 (978) Chapter 18 Applications for science and engineering
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release ())
{
… …
else if (e .Button == MouseButtons .Right)
{
… …
else if (grobj is SegmentedData && segmenteddata.Segments.Count >2)
{
iClickedSliderNode = mover .CaughtNode;
ContextMenuStrip = menuOnSlider;
}
… …
Sliders exist only inside the plotting area. Individual movements of sliders were already discussed, but there are also the
related movements. When the plot is moved or resized, sliders have to change their positions in the proper way.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
if (segmenteddata != null)
{
if (plot .PlottingArea != segmenteddata .Area)
{
if (plot .PlottingArea .Width == segmenteddata .Area .Width &&
plot .PlottingArea .Height == segmenteddata .Area .Height)
{
// plot is simply moved, so sliders must be moved also
int dx = Convert .ToInt32 (plot .PlottingArea .Left –
segmenteddata .Area .Left);
int dy = Convert .ToInt32 (plot .PlottingArea .Top –
segmenteddata .Area .Top);
segmenteddata .Move (dx, dy);
}
else
{
segmenteddata .Area = Rectangle.Round (plot .PlottingArea);
}
}
}
Invalidate ();
}
}
The above code shows two different related movements for SegmentedData object.
• When the plotting area is moved, then sliders are moved synchronously by the SegmentedData.Move()
method.
public override void Move (int dx, int dy)
{
rc .X += dx;
rc .Y += dy;
for (int i = 0; i < cxLine .Count; i++)
{
cxLine [i] += dx;
}
}
World of Movable Objects 641 (978) Chapter 18 Applications for science and engineering
• When the plotting area is resized, then positions of all sliders must be recalculated; this is done by the
SegmentedData.Area property.
public Rectangle Area
{
get { return (rc); }
set
{
rc = value;
SetLineCoordinates ();
DefineCover ();
}
}
Data file may include many thousands of (x, y) pairs. The full view makes it obvious, how to divide the big piece of data
into segments, but positioning of borders between segments is not too accurate here as each pixel of horizontal scale can be
associated with many real (x, y) values. The view of any data file in the plotting area in this form is used only for the rough
dividing of this file into segments; more accurate partitioning and all further work on the data files is done inside the
Form_DataRefinement_Zoom.cs which is called through the Zoom command of context menu (figure 18.34). Opened
form allows to work with the same data files with much higher accuracy. (In original application, this is also the place
where specialists apply the needed methods of calculation to the data.)
plotFullView – gives a view of the whole data file with its general segmentation. At any moment one of segments can
be selected for detailed view, for accurate changing of data, and for calculations; this segment of data is shown at another
plot - plotSegment. These two plotting areas with objects of several classes residing in these areas are the most
interesting objects in the form; I want to discuss them a bit later after mentioning some features of other objects.
Of the two ListView controls, the first one includes the list of segments and highlights the line of the currently selected
segment; the second one shows all the (x, y) pairs of data for the selected segment.* As in all user-driven applications,
everything in the Form_DataRefinement_Zoom.cs is movable and resizable. The number of segments on which the data
file is divided can vary from few to dozens. When the number of segments is small, there is no sense to make the list of
segments big, but if there are a lot of segments, then it makes sense to enlarge this list. Anyway, it is the users’ choice to
place the list anywhere in the form and set its size. The same thing happens to the list of (x, y) pairs for the selected
segment.
The group of controls is of special interest for those specialists who work with the data; for our discussion only the
questions of organizing this group can be important. Group belongs to the ElasticGroup class; as seen from the
figure, there is even one group inside another. The code to initialize a group can be written in different ways; everything
can be done in one statement, but for better understanding of the code I first design the inner group and then use it as one
element in the outer group.
ctrlsGroup = new Control [] {comboMethods, // 0
panelButtons, textbox_X0, textbox_Y0,
textbox_X1, textbox_Y1, // 1 - 5
posintOutputPoints, btnApply, listResults }; // 6 - 8
private void OnLoad (object sender, EventArgs e)
{
… …
ElasticGroup groupInner = new ElasticGroup (this,
new ElasticGroupElement [] {
new ElasticGroupElement (ctrlsGroup [1]),
new ElasticGroupElement (new CommentedControlLTP (this,
ctrlsGroup [2], strsGroup [5])),
new ElasticGroupElement (new CommentedControlLTP (this,
ctrlsGroup [3], strsGroup [6])),
new ElasticGroupElement (new CommentedControlLTP (this,
ctrlsGroup [4], strsGroup [7])),
new ElasticGroupElement (new CommentedControlLTP (this,
ctrlsGroup [5], strsGroup [8])),
}, strsGroup [1]);
group = new ElasticGroup (this, new ElasticGroupElement [] {
new ElasticGroupElement (ctrlsGroup [0]),
new ElasticGroupElement (groupInner),
new ElasticGroupElement (new CommentedControlLTP (this,
ctrlsGroup [6], strsGroup [9])),
new ElasticGroupElement (ctrlsGroup [7]),
new ElasticGroupElement (ctrlsGroup [8])
}, strsGroup [0]);
… …
The inner group - groupInner - does not need any mentioning as a global variable because a group of any level of
complexity can be saved and restored by a single call. While an ElasticGroup is saved, all sizes, positions, and
parameters of visualization for all its elements are stored; later they are used to reinstate the view of a group. But there are
some important details of the inner view of some controls which are not saved automatically on saving a group, these details
must be saved and restored separately; this happens, for example, with the width of columns for any ListView control.
*
At one moment I though about turning these two SolitaryControl objects into CommentedControl objects by
adding one word explanations but then I dropped this idea. They can be helpful in Demo application but have absolutely no
sense in real application. It is like explanations on the bookshelves: they are very helpful in public libraries, but you don’t
use them in your home library even if there are thousands of books. (If you begin to feel that you need such signs at the
bookshelves at home, then it is the best indication that you don’t need books any more…)
World of Movable Objects 643 (978) Chapter 18 Applications for science and engineering
Previously I have demonstrated the groups in which the currently unneeded inner elements were temporarily hidden; in
other cases inner elements can be fixed to prevent their accidental move. The use of inner elements in the Method and
settings group depends on the selected method, but there is no sense of hiding couple of controls and then restoring them
back when another method is selected. Both groups – inner and outer – are not too densely populated, so it is not a problem
to move any of them by some empty spot, but the command for fixing and unfixing the inner elements is included into menu
which can be called on any group. Another command in this menu allows to open a tuning form for the pressed group.
Usually the number of context menus in application strongly correlates with the number of different classes of the screen
elements, while the number of commands in those menus depends on the complexity of objects and the possibility of their
tuning. There are six different menus in the Form_DataRefinement_Zoom.cs but four of them consist of a single
command.
• Menu on any slider contains a single command to delete the pressed slider.
• Menu on the plotting area of the whole data file includes a single command to add new slider.
• Menu at empty places includes a single command to change the font.
Two big ListVew controls do not need commands for hiding and restoring. The List of segments is in use all the
time; if anyone does not want to keep the List of dots in view, then this List can be simply moved out of view across
the border. The really needed thing is the possibility of changing the font for these controls; this is done via the only
command which is included into menu that can be called at any empty place of the form.
Tuning of both plotting areas and their scales is started by double click. Usually setting of the ranges is one of the main
things that these forms allow to do, but in the Form_DataRefinement_Zoom.cs this is not allowed! The plotting area has
to show one segment at a time; so in each case the X range is strictly defined by positioning of the sliders. In reality slightly
more than a single segment is shown at a time, but this is purposely organized for better visualization and for making easier
the decision on moving the sliders. For these purposes, small parts of the function beyond the sliders are also shown but in
a different way. Sliders are often placed at the places where the shape of the function changes, so it is very useful to see the
function on both sides of a slider. Spots corresponding to (x, y) pairs inside the shown segment can be moved up and down,
so the vertical range of the plot is made slightly wider than the range [minimum, maximum] defined by the values of this
segment. To prevent the manual (through the tuning forms) change of the range for the plotSegment, some of the
parameters are declared immutable; this makes them unchangeable through the tuning forms.
private void OnLoad (object sender, EventArgs e)
{
… …
plotSegment = new Plot (this, RectangleF .FromLTRB (cxL, cyT, cxR, cyB));
plotSegment .HorScales [0] .ImmutableParams =
Immutable .ValueLT | Immutable .ValueRB | Immutable .ValuesSwap;
plotSegment .VerScales [0] .ImmutableParams =
Immutable .ValueLT | Immutable .ValueRB | Immutable .ValuesSwap;
… …
The biggest opportunities in the Form_DataRefinement_Zoom.cs come not with the variety of available commands, but
with what users can do manually with the mouse help. All these possibilities are based on using two classes of movable
objects.
The first is the Spots_UpDown class. Such object, as it is obvious from the name of the class, is a collection of spots
which can be moved up and down.
public class Spots_UpDown : GraphicalObject
{
Rectangle rc;
ValuesOnBorders borderValues;
int nRad;
Color clrSpots;
Pen penLines;
double [] fX;
double [] fY;
PointF [] pts;
The Form_DataRefinement_Zoom.cs always shows only one segment in the plotting area plotSegment. On
initialization, an object of the Spots_UpDown class gets the area of this plot, the boundary values, and the arrays of X
and Y values belonging to segment.
World of Movable Objects 644 (978) Chapter 18 Applications for science and engineering
private void SelectedIndexChanged_listSegments (object sender, EventArgs e)
{
… …
iSegment = listSegments .SelectedIndices [0];
… …
double [] fX = new double [segment .SourceDots];
double [] fY = new double [segment .SourceDots];
for (int i = 0; i < segment .SourceDots; i++)
{
fX [i] = srcData .Args [segment .LeftNumber + i];
fY [i] = srcData .Vals [segment .LeftNumber + i];
}
spots = new Spots_UpDown (Rectangle .Round (plotSegment .PlottingArea),
plotSegment .ValuesOnBorders, fX, fY);
… …
Based on the plotting area and the values on borders, the positions of all (x, y) pairs belonging to segment are calculated.
These pairs are shown as the small circles, but their size is enough to make their grabbing by the mouse and moving easy.
The number of circles in segment can vary from few to several thousands. When the number of circles is huge, then circles
on the neighbouring (x, y) pairs can overlap (figure 18.35), but even in such case you can see some data areas in which the
pattern is obviously broken. This is the clear indication of some noise being added to the signal. There can be other
situations with the individual circles staying far away from each other and the function is seen as a very rough terrain
(figure 18.36). In this case it is obvious, which of the circles must be moved up or down to make the function line smoother
and the calculations easier and more accurate.
Fig.18.36 This segment contains not too many circles; they do not overlap and it is more obvious, which of them have to
be moved up or down to make the function smoother.
The design of a cover for such object is easy enough. Each (x, y) pair is covered by a small circular node which allows to
move this particular point up and down. There is no need to move the whole collection of points by itself, so no other nodes
are needed.*
public override void DefineCover ()
{
int rad = Math .Max (minNodeRad, nRad);
CoverNode [] nodes;
if (Movable)
{
nodes = new CoverNode [pts .Length];
for (int i = 0; i < pts .Length; i++)
*
The cover of any object is determined by its view and the requirements for its movements. I am slightly jumping ahead,
but in the next example I will demonstrate a similar looking collection of points which are involved in different movements;
this difference in movements will be the cause of different cover.
World of Movable Objects 645 (978) Chapter 18 Applications for science and engineering
{
nodes [i] = new CoverNode (i, pts [i], rad, Cursors .SizeNS);
}
}
else
{
nodes = new CoverNode [1];
nodes [0] = new CoverNode (0, pts [0], rad, Behaviour .Transparent,
Cursors .Default);
}
cover = new Cover (nodes);
cover .SetClearance (true);
}
Code of the Spots_UpDown.DefineCover() method shows two absolutely different variants. The interesting one is
the first part with all (x, y) pairs from the data file covered by small circular nodes; this variant is shown at figures 18.35
and 18.36. What is the idea of having the second variant? In that variant only the first point is covered by a node and even
this node is transparent, so none of the points can be really moved. Turning of all points into unmovable is done via the
menu on the plotting area for current segment. What is the purpose of such change?
Figure 18.36 represents a case of relatively small number of points inside the selected segment; in such case each point can
be easily distinguished from the neighbours, each point can be easily moved without accidental catch of the wrong point,
and all the operations of redrawing go really quickly. Figure 18.35 demonstrates a case with much bigger number of points
in segment; nodes on the neighbouring points overlap, selection of particular point becomes much harder, and some
operations of changing the borders of segments take more time, but the case from this figure is relatively good. There are
worse cases when an input file can include hundreds of thousands of points; then the number of points from input data
associated with each horizontal pixel of the screen is huge. Selection of particular point for moving it up or down becomes
nearly impossible (unless one point stays far aside from the neighbours), but any operation on moving the borders of
segment requires a lot of calculations and a lot of time for these calculations and redrawing. For such cases, scientists asked
for a special command to declare all these points unmovable. That was the reason to have this special case of a cover, but
this is really a special case and discussion further on is only about the standard case.*
Moving of any node up or down is aimed on making the whole function smoother, so there is no sense in moving any point
out of view; this is the only restriction on individual movement of points.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float cyNew = pts [iNode] .Y + dy;
if (rc .Top <= cyNew && cyNew <= rc .Bottom)
{
pts [iNode] .Y = cyNew;
fY [iNode] = Auxi_Geometry .CoorToVal_Linear (rc .Top, rc .Bottom,
borderValues .Top, borderValues .Bottom,
Convert .ToInt32 (pts [iNode] .Y));
bRet = true;
}
}
return (bRet);
}
*
It is the reality of design the applications for science and research: the research is often unpredictable and the programs
have to copy the same pattern. While working on the first version of this DataRefinement program, I got from the
colleagues several data files to work with and to use them for program testing; each file contained not more than several
thousand (x, y) pairs; I was sure that the size of all data files will be of the same order and the design was oriented on such
amount of data. However, when the program began to work smoothly and was intensively used, scientists started to use it
with the input files containing hundreds of thousands or even millions of input points. Then I had to think about those
situations and the second variant of cover was added.
World of Movable Objects 646 (978) Chapter 18 Applications for science and engineering
In many situations you press some object and move; such movement is defined by the Move() method of the involved
class. For the Spots_UpDown class, there is no such movement, so it looks like this class does not need the Move()
method. But this collection of points resides atop the plotting area and when this area is moved, then all points must move
synchronously; the Move() method is very useful in such a case.
public override void Move (int dx, int dy)
{
rc .X += dx;
rc .Y += dy;
Size size = new Size (dx, dy);
for (int i = 0; i < pts .Length; i++)
{
pts [i] += size;
}
}
The second interesting class which is used in the Form_DataRefinement_Zoom.cs is the SliderLocal class. An
object of this class is represented by a vertical line which can be moved only left or right between the defined boundary
positions.
public class SliderLocal : GraphicalObject
{
int iArg;
int cx, cyT, cyB;
Pen penLine;
int cxLeftBound, cxRightBound;
Depending on the number of the selected segment, you can see either one or two objects of this class in the plotting area for
selected segment. The first segment has only one slider marking its right end, so this slider is shown closer to the right
border of the plotting area; the last segment has a single slider to mark its left end, so it is shown closer to the left side of the
area; for any other segment, two vertical lines mark the borders of segment. I want to underline again the difference
between the sliders in two plotting areas of the Form_DataRefinement_Zoom.cs.
The upper plot at figure 18.35 shows the whole data file; though you see several sliders in this area, all of them together
represent a single object of the SegmentedData class. This object has a cover consisting of several narrow rectangular
nodes to cover all the sliders in the plotting area. The number of nodes is equal to the number of sliders; sliders can be
added and deleted (but at least one always exists), so the number of nodes in the cover of this object varies.
The lower plot at the same figure demonstrates currently selected segment. Depending on the order of segment, there can
be one or two sliders in view. Each of these sliders is a separate object of the SliderLocal class, so at each moment
there are either one or two objects of this class. Cover for each object consists of exactly one node – a narrow rectangle to
cover the line.
As I mentioned, a cover for a SliderLocal object consists of a single rectangular node.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, new Rectangle (cx - 3,
Math .Min (cyT, cyB), 6, Math .Abs (cyB - cyT)),
Cursors .SizeWE));
}
Individual slider movements left and right are restricted by the cxLeftBound and cxRightBound values which are
defined at the moment of initialization.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
int cxNew = cx + dx;
if (cxLeftBound <= cxNew && cxNew <= cxRightBound)
{
cx = cxNew;
bRet = true;
World of Movable Objects 647 (978) Chapter 18 Applications for science and engineering
}
return (bRet);
}
Sliders are supposed to live only inside the plotting area, so when the plot is moved, the existing sliders (there can be either
one or two) have to move synchronously. This is the same related movement which was described in some of the previous
examples by the InformRelatedElements() method. A slider is described by its two end points, so the Move()
method of such object is really primitive.
public override void Move (int dx, int dy)
{
cx += dx;
cyT += dy;
cyB += dy;
}
As you can see, the Form_DataRefinement_Zoom.cs contains a couple of simple movable objects, but the team-work of
these objects produces very interesting results. Is it not amazing that one or two rectangles which can be moved left and
right plus a set of small circles which are moved up and down can produce together a very efficient instrument for data
analysis?
In nearly all complex examples of this Demo application the movable objects are already prepared before calling the
RenewMover() method of a form; in such a way this method becomes very simple and easy to understand. The situation
with the Form_DataRefinement_Zoom.cs is slightly different. Some of the movable objects are constructed inside the
RenewMover() method and mover queue depends on the result of this construction. This peculiarity depends on the fact
that there are additional conditions on whether to include some objects into mover queue or not.
• The number of the selected segment determines the amount of sliders of the SliderLocal class to be included
into the mover queue: if it is the first or the last segment of the data file, then there is only one slider in view (and
in work); otherwise there are two sliders. It is not enough to determine the number of sliders at the moment when
the form is called for the first time; the number of sliders can be changed throughout the work by adding or
deleting the sliders and by changing the current segment.
• Small circles can be declared movable or not. Cover of the Spots_UpDown object is included into mover
queue in any case. I have already explained what kind of cover makes the spots unmovable, but there is one more
difference in filling the mover queue depending on the movability of the spots. If all the spots are declared
unmovable, then the ListView control with the corresponding values is turned into invisible and is not
included into mover queue.
In the Form_DataRefinement_Zoom.cs, there are movable objects of different origin; their order in the mover queue is
regulated by some rules (controls ahead of graphical objects) and relations between the graphical objects of several classes.
Sliders and spots are graphical objects which live inside the plotting area; they are visualized atop this area, so they must be
placed ahead of the plotting area in the mover queue. But is there any difference in the order of spots and sliders? It looks
like it does not matter but it is not so. A slider is always placed at exactly the same x coordinate as some spot (see
figure 18.36). Slider is a thin line of one pixel width, while a diameter of such spot is six pixels. But the width of the node
in the cover of a slider is exactly the same as the diameter of the node in the cover for a spot. If slider stays in the mover
queue ahead of the spots, then the node for this particular spot will be entirely closed by the node of the slider; there will be
no chance to move this spot. Because of this, spots have to precede sliders in the mover queue; you can see this from the
code of the RenewMover() method.
private void RenewMover ()
{
mover .Clear ();
plotFullView .IntoMover (mover, 0);
mover .Insert (0, srcData);
info .IntoMover (mover, 0);
plotSegment .IntoMover (mover, 0);
if (spots != null)
{
Rectangle rc = Rectangle .Round (plotSegment .PlottingArea);
int iSegment = listSegments .SelectedIndices [0];
Segment segment = srcData .Segments [iSegment];
if (iSegment < srcData .Segments .Count - 1)
World of Movable Objects 648 (978) Chapter 18 Applications for science and engineering
{
rightSlider = new SliderLocal (segment .RightNumber,
Convert .ToInt32 (spots [spots .SpotsNumber - 1] .X),
rc .Top, rc .Bottom, Color .Green);
mover .Insert (0, rightSlider);
}
else
{
rightSlider = null;
}
if (iSegment > 0)
{
leftSlider = new SliderLocal (segment .LeftNumber,
Convert .ToInt32 (spots [0] .X),
rc .Top, rc .Bottom, Color .Green);
mover .Insert (0, leftSlider);
}
else
{
leftSlider = null;
}
mover .Insert (0, spots);
if (bAfterInit)
{
RenewSliderRulers ();
}
}
… …
Controls must precede all graphical objects in the mover queue, but controls must also obey some rules of order among their
kin. There are three solitary controls; they must be at the head of the mover queue, but even the order of these elements is
decided by their order in the full collection of controls of this form.
A group consists mostly of controls, but there are also some graphical elements, so a group must be placed after all solitary
controls. Thus we get this second half of the RenewMover() method.
private void RenewMover ()
{
… …
group .IntoMover (mover, 0);
scSegments .IntoMover (mover, 0);
if (bMovableSpots)
{
listDots .Visible = true;
scDots .IntoMover (mover, 0);
}
else
{
listDots .Visible = false;
}
mover .Insert (0, scHelp);
if (bAfterInit)
{
Invalidate ();
}
}
Figures 18.35 and 18.36 demonstrate that not only the selected segment is shown in more details at plotSegment, but
this plotting area shows some additional parts of data beyond the borders of the selected segment. (Certainly, these parts are
shown only if it is not the beginning or the end of the data and there is anything beyond the sliders.) Those (x, y) pairs
which happen to be inside extra ranges are saved separately and their values are used for two purposes.
World of Movable Objects 649 (978) Chapter 18 Applications for science and engineering
• The y values of these additional (x, y) pairs are considered while determining the vertical range of the plotting area.
• The x values of these additional (x, y) pairs are used to calculate the boundaries of the possible movements for
sliders.
For example, if there are some values beyond the left slider, then it can be moved to the left up to the border of the plotting
area. If one slider is moved in direction of another, then the limit of its movement is determined in such way that they
would be never placed at the same point.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is SliderLocal)
{
SliderLocal slCaught = grobj as SliderLocal;
int cxLeftLimit, cxRightLimit;
if (leftSlider != null && leftSlider .ID == slCaught .ID)
{
if (fxLefter .Count > 0)
{
cxLeftLimit = Convert .ToInt32 (
plotSegment .PlottingArea .Left);
}
else
{
cxLeftLimit = Convert .ToInt32 (spots [0] .X);
}
leftSlider .StartMovement (cxLeftLimit, Convert.ToInt32 (
spots [spots .SpotsNumber - 2] .X));
}
… …
The call to the SliderLocal.StartMovement() inside the OnMouseDown() method is similar to the
StartResizing() method which was demonstrated many times in the examples of the first part of this book. At the
moment when an object is caught and the movement is going to start, the boundaries of possible movement are calculated
and saved as some fields of an object under movement.
public void StartMovement (int cxL, int cxR)
{
cxLeftBound = cxL;
cxRightBound = cxR;
}
These boundaries are saved and used throughout the movement while an object, in this case it is a slider, is moved left or
right. These saved values are used inside the SliderLocal.MoveNode() method to check the possibility of
proposed movement, the code of this method was shown three pages back.
With the movement of any circle, it is much easier. It can be moved only up or down; the boundaries of the possible
movement are determined beforehand (these are the borders of the plotting area), so, when any circle is caught for moving,
only the corresponding line in the List of points must be highlighted.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
World of Movable Objects 650 (978) Chapter 18 Applications for science and engineering
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is Spots_UpDown)
{
int jNode = mover .CaughtNode;
listDots .Items [jNode] .Selected = true;
listDots .EnsureVisible (jNode);
}
}
}
ContextMenuStrip = null;
}
The OnMouseDown() method is often used to determine the boundaries of the possible movement; the significant
changes are often done when an object is released; for this we have to look into the OnMouseUp() method.
• If any point is released, then the y value of the associated (x, y) pair is calculated, based on the new position of the
point. But this is not all as the vertical range of the plotting area depends on the values from the shown segment.
If needed, the range is changed.
• The release of a slider causes bigger changes. A slider can be released at any x coordinate, but it has to be placed
exactly at some point. This new point, nearest to the position of release, must be determined; the position of the
slider must be adjusted. Any move of a slider to another point means the redistribution of points between two
segments. This changes the information in both ListView objects; the view of the plotting area with the whole
file must be also changed.
There are enough movable objects in the Form_DataRefinement_Zoom.cs: information, three SolitaryControl
objects, group with all its inner elements, two plotting areas, sliders in both plotting areas, and a number of circles.
Movements of all these elements are described in the MoveNode() methods of their classes. If an object is involved in
individual movement like moving of a group or a solitary control, then no additional mentioning of such movement is
needed in the form code. The situation with moving of plotting areas is different as each of them is associated with other
graphical objects.
The plotting area to show the whole data file – plotFullView – is associated with an object srcData of the
SegmentedData class; the related movements of these two objects are organized in exactly the same way as was
explained a bit earlier in the case of the Form_DataRefinement.cs form.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Plot || grobj is RectCorners)
{
if (plotFullView .PlottingArea != srcData .Area)
{
if (plotFullView.PlottingArea .Width == srcData .Area .Width &&
plotFullView.PlottingArea .Height == srcData .Area .Height)
{
// plot is simply moved, so sliders must be moved also
int dx = Convert.ToInt32 (plotFullView.PlottingArea .Left –
srcData .Area .Left);
int dy = Convert.ToInt32 (plotFullView.PlottingArea .Top –
srcData .Area .Top);
srcData .Move (dx, dy);
}
else
{
srcData.Area = Rectangle.Round (plotFullView.PlottingArea);
}
}
… …
World of Movable Objects 651 (978) Chapter 18 Applications for science and engineering
The plotting area to show the selected segment – plotSegment – is associated with sliders of the SliderLocal
class and spots of the Spots_UpDown class, so there is more code to describe their related movements. The code for
simple movement of this plotting area is similar to the previous code; all the associated objects must be moved
synchronously; this means calling their Move() methods. Those methods for the Spots_UpDown and
SliderLocal classes were shown a bit earlier; both are simple.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Plot || grobj is RectCorners)
{
… …
else if (spots != null && plotSegment .PlottingArea != spots .Area)
{
if (plotSegment .PlottingArea .Width == spots .Area .Width &&
plotSegment .PlottingArea .Height == spots .Area .Height)
{
// area is simply moved, so dots must be moved also
int dx = Convert.ToInt32 (plotSegment .PlottingArea .Left –
spots .Area .Left);
int dy = Convert.ToInt32 (plotSegment .PlottingArea .Top –
spots .Area .Top);
spots .Move (dx, dy);
if (leftSlider != null)
{
leftSlider .Move (dx, dy);
}
if (rightSlider != null)
{
rightSlider .Move (dx, dy);
}
}
… …
When the area plotSegment changes its sizes, then positions of all the spots must be recalculated; this is done by
calling the Spots_UpDown.Area property. Only after this calculation, the new positions for sliders can be determined
because they must be adjusted to the new positions of spots.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is Plot || grobj is RectCorners)
{
… …
else if (spots != null && plotSegment .PlottingArea != spots .Area)
{
… …
else
{
// area is changed, so dots must be recalculated from the current values
spots .Area = Rectangle .Round (plotSegment .PlottingArea);
if (leftSlider != null)
{
leftSlider .AdjustXCoor (leftSlider .ArgNumber,
Convert .ToInt32 (spots [0] .X),
Convert .ToInt32 (plotSegment .PlottingArea .Top),
Convert.ToInt32 (plotSegment.PlottingArea.Bottom));
World of Movable Objects 652 (978) Chapter 18 Applications for science and engineering
}
if (rightSlider != null)
{
rightSlider .AdjustXCoor (rightSlider .ArgNumber,
Convert .ToInt32 (spots [spots .SpotsNumber - 1] .X),
Convert .ToInt32 (plotSegment .PlottingArea .Top),
Convert .ToInt32 (plotSegment .PlottingArea.Bottom));
}
}
}
if (leftSlider != null && rulerLeft != null && rulerLeft .Visible)
{
rulerLeft .Location = Point .Round (new PointF (
leftSlider .XCoor, plotSegment .PlottingArea .Top - 4));
}
if (rightSlider != null && rulerRight != null &&
rulerRight .Visible)
{
rulerRight .Location = Point .Round (new PointF (
rightSlider .XCoor, plotSegment .PlottingArea .Top - 4));
}
Update ();
… …
At the end of the previous code you can see the possible change of location for two
more objects – rulerLeft and rulerRight – which belong to the
SliderRuler class; such object is shown at figure 18.37. An object is
represented by a group of four controls, but you cannot move them. Is it strange?
Fig.18.37 An object of the
The group is strictly linked with a SliderLocal object and does not need any
SliderRuler class
movement by itself; it only gives users more flexibility in positioning such slider.
There were no such groups in the older versions of the DataRefinement application and sliders could change their positions
only when they were moved. However, there are situations when these local sliders cannot enlarge the range of the selected
segment because there are no (x,y) pairs in those intervals which are shown beyond the sliders of the plotSegment. Or
there can be only one or two (x,y) pairs in the extra range beyond a slider and to move this slider a bit farther, for example,
for 10 positions aside, you have to repeat the movement again and again. These situations happen when the used data file
has relatively small number of (x,y) pairs; the files with huge number of data add more problems. For such files, it can be
difficult to position a slider at the particular pair; one of the reasons can be that several or many pairs are associated with the
same horizontal pixel of the screen and the program itself determines the exact pair with which to associate the slider at the
selected coordinate. SliderRuler allows users to declare the number of the pair or an argument at which a slider must
be positioned; in addition to standard moving of sliders, this makes the positioning much more flexible.
One feature of user-driven applications is not realized in the Form_DataRefinement_Zoom.cs: not all the parameters of
visualization can be tuned by users. Well, everything is prepared for this thing, but there is no access to some of the
parameters. Sliders and spots are drawn with predefined colors which cannot be changed in this working version. You can
easily improve the design by adding to menu on sliders a command to change their color. Another menu can be added for
spots to change their radius and color and to change the color of the line. If you do not want to add menus, you can
organize another group of the ElasticGroup class; several controls inside the new group will do the job of setting the
visualization parameters. All the needed properties to change the colors can be found in the Spots_UpDown and
SliderLocal classes.
At the moment the drawing of sliders and dots are done with the predefined colors.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (listSegments .SelectedIndices .Count > 0)
{
if (spots != null)
{
spots .Draw (grfx);
World of Movable Objects 653 (978) Chapter 18 Applications for science and engineering
}
… …
}
if (leftSlider != null)
{
leftSlider .Draw (grfx);
}
if (rightSlider != null)
{
rightSlider .Draw (grfx);
}
… …
There are no questions about the drawing of sliders; these straight lines are drawn with a predefined pen. If you look into
the Spots_UpDown.Draw() method, it also looks simple; there is no problem in drawing a set of segments between
the known points and to mark these points with the small circles.
public void Draw (Graphics grfx)
{
for (int i = 0; i < pts .Length - 1; i++)
{
grfx .DrawLine (penLines, pts [i], pts [i + 1]);
}
if (bShowEnlargedSpots && Movable)
{
for (int i = 0; i < pts .Length; i++)
{
Auxi_Drawing .FillCircle (grfx, pts [i], nRad, clrSpots);
}
}
}
But if you check the value of the bShowEnlargedSpots, you will find that it is always false, so those small circles
on figure 18.36 are not drawn by the Spots_UpDown.Draw() method.* Then who is responsible for drawing those
circles?
Does not their view look familiar to you? Such small red circles filled with white color inside did not appear in the last big
and complex examples, but they were used frequently enough in the first part of the book when I was explaining the design
of covers. Yes, this is simply the visualization of cover for the Spots_UpDown class! I have mentioned that covers are
never shown in the real applications, but here is a refutation of this statement; it is obvious from the OnPaint() method
of the form.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
for (int i = 0; i < mover .Count; i++)
{
if (mover [i] .Source is Spots_UpDown)
{
mover [i] .DrawCover (grfx);
}
}
… …
A strange and maybe funny (or amazing) example of using the MovableObject.DrawCover() method, but it works
well enough in this case. Certainly, if you add an instrument to change the visualization of points, then you have to take this
part of the OnPaint() method out and rely on drawing the small circles inside the Spots_UpDown.Draw()
method.
*
Another indication is the color of those spots: by default and in the constructor they are declared blue
(clrSpots = Color .Blue), but there are no blue spots at figures.
World of Movable Objects 654 (978) Chapter 18 Applications for science and engineering
The scientists who work with this application use both types of movable objects (sliders and circles) to organize the needed
segments and to exclude the noise from data for further calculations. Then one of the methods is selected for
approximation; parameters for each method can be also changed. The calculated results are shown immediately at the same
plotSegment area and some results of calculations appear in a small ListView control inside the group; both things
help to estimate the accuracy of approximation and to compare the used methods. The discussion of the used methods is
definitely out of the scope of this book, but the use of movable objects for design of such applications is demonstrated.
Nearly every serious task could be solved with some older methods. The standard question is, how effective the new
method is in comparison with the old one. (Tycho Brahe was extremely accurate in all his measures, but when the calculus
was invented, astronomers received absolutely new methods for their work.)
The data refinement can be organized without movable objects, but it would be so ineffective that there are big doubts if it
can be used in such a way. On the contrary, the design of this application on the basis of movable objects turned it into very
effective and useful program.
You can look through the long lists of data and by comparison of the values decide, which of them must be adjusted, but it
is a very tiresome and laborious work. Even then it would be a huge problem to decide, for how much this or that value
must be changed. By moving the spots on the plotting area up or down, the same thing can be done easily. Human eye is a
perfect instrument to estimate the smoothness of a curve. Calculations which are done in parallel with such movement of
spots immediately show the accuracy of manual adjustment.
Those short additions of data beyond the boundaries of the shown segment help to decide about the best positions of the
borders for segments. Again, the calculations going in parallel with such movement and the visualization of these results
immediately signal about the wrong or right movement of the borders.
Both things allow to refine input data quickly and accurately. Users do not need to adjust manually the positions of
hundreds or thousands of original (x,y) pairs. Methods of approximation will do the job perfectly, but some refinement of
the noisiest parts of input files must be done prior to calculations. This is the main purpose of the DataRefinement
application.
World of Movable Objects 655 (978) Chapter 18 Applications for science and engineering
• The first step is to read and show the textual information from the head of the input file and some part of real data.
This part must be big enough for users to make a decision about the data columns to be turned into graphs.
• Any column can be selected as an array of X values. Usually X values represent the time during an experiment, so
these X values cannot decrease.
In general, the last limitation is not crucial, as Y(x) plots can be produced for an arbitrary array of X values.
However, scientists who commissioned this application use it only for the plots along the time scale and
this check of X values helps to find out some mistakes in the input data or the wrong selection of column
for X data.
• Any other column can be selected as an array of Y values.
• An arbitrary number of graphs can be shown in one plotting area. Selected columns are turned into Y(x) functions
and are shown in the plotting area as Y(x) graphs. The plotting area has a single X scale for all graphs and
individual Y scale for each graph (function); later these Y scales can be united.
• Plots can be tuned in any possible way to produce the best view for further use in any document.
To make the overall work and the use of this application similar to all other programs used in our department, I designed it
on the classes which are familiar to my colleagues through the work of other applications. These classes were already
discussed in this book: SolitaryControl, ElasticGroup, ArbitraryGroup, FilenameScrolling, and
Plot. To demonstrate the work of Simple data viewer for TXT files I need some input file; so I put one into the
…\WorldOfMoveableObjects\DataFiles_for_SimpleViewers subdirectory, but anyone can use this
program with other data files; the simple rules of their design were already mentioned. Figure 18.38 demonstrates the first
form of the application after the input file was opened (use File – Open menu position), the columns to be used as Y values
were selected, and the button was pressed.*
*
As often happens with the real scientific applications, the most picturesque figures have nearly no sense from the point of
science but are the best for explanation of the programming features.
World of Movable Objects 657 (978) Chapter 18 Applications for science and engineering
This Form_DataViewerTXT.cs consists of three main parts: two groups and a plot. The first group - groupData –
reminds about the name of the source data file, shows the textual information from the head of the selected file, and contains
the initial part of data. This initial part is shown in the same system of columns as the source data file has. Two inner
controls of this group are turned into SolitaryControl objects. An element to remind about the filename belongs to
the FilenameScrolling class, so the whole group can be only of the ArbitraryGroup class.
private void OnLoad (object sender, EventArgs e)
{
RestoreFromRegistry ();
if (!bRestore)
{
Spaces spaces = new Spaces (this);
// groupData
name_viewer = new FilenameScrolling (this, new Point (24,
SystemInformation .MenuHeight + 24), 500);
Rectangle rc = Rectangle .Round (name_viewer .RectAround);
textHeaders .Bounds = new Rectangle (rc .Left,
rc .Bottom + spaces .Ver_betweenFrames, rc .Width, 90);
scHeaders = new SolitaryControl (textHeaders);
rc = textHeaders .Bounds;
listData .Bounds = new Rectangle (rc .Left,
rc .Bottom + spaces .Ver_betweenFrames, rc .Width, 320);
scData = new SolitaryControl (listData);
GroupVisibleParameters visparams = new GroupVisibleParameters (
BackColor, true, Auxi_Colours .DefaultFrameColor (this),
new int [] { 12, 8, 12, 12 }, true, fntSelected,
SystemColors .Highlight, 0.0, 4);
groupData = new ArbitraryGroup (this, visparams, "Data from file",
ElementsArea_groupData, DrawElems_groupData,
SynchroMove_groupData, IntoMover_groupData);
… …
The second group – groupSelection – is used to select
columns of values for Y(x) graphs and to set the compression
coefficient (figure 18.39). As I mentioned before, an amount
of data lines can be huge and easily go into hundreds of
thousands. At the same time the width of a screen is less than
2000 pixels; if you draw such Y(x) graphs without preliminary
compression, then dozens or even hundreds of (x, y) pairs of
data are shown with the same x coordinate.*
This group contains only controls; some of them with
comments, so each element is turned either into
SolitaryControl or into CommentedControl object; Fig.18.39 This group allows to select the columns of
both classes can be wrapped into ElasticGroupElement; values for Y(x) graphs and to set the
after it an ElasticGroup is constructed. compression of input data.
*
There are no problems in drawing those graphs without any compression (with compression equal to 1). You will not see
any flickering as double buffering is used here as in any other form of the Demo application, but you may see some delay in
moving anything around the screen. At the same time the compression does not hide any significant changes in the input
data and even the compressed graph gives you the needed information about the most crucial moments of some rapid
changes. It is a common practice to get the data from analog-digital converter once in a second; at the same time the
experiment can go for hours and easily exceed 24 hours; as a result, the number of data lines can be close to or above
100000. The compression coefficient of 60 is a common thing for the first view of such graph; later on all interesting pieces
of graph can be shown with much higher accuracy.
World of Movable Objects 658 (978) Chapter 18 Applications for science and engineering
private void GroupSelectionDefaultView ()
{
… …
List<CommentedControl> ccs = new List<CommentedControl> ();
for (int i = 0; i < ctrlsSelect .Length - 2; i++)
{
ccs .Add (new CommentedControl (this, ctrlsSelect [i], Resizing .WE,
Side .W, strInGroup [i], fntSelected));
}
List<ElasticGroupElement> elems = new List<ElasticGroupElement> ();
for (int i = 0; i < ccs .Count; i++)
{
elems .Add (new ElasticGroupElement (ccs [i]));
}
elems.Add (new ElasticGroupElement (ctrlsSelect [ctrlsSelect.Length - 2]));
elems.Add (new ElasticGroupElement (ctrlsSelect [ctrlsSelect.Length - 1]));
groupSelection = new ElasticGroup (this, elems, "Data selection");
groupSelection .TitleFont = fntSelected;
}
I mentioned before that the Plot class can be used with multiple horizontal and vertical scales. The use of several
horizontal scales is rare enough, while the plots with one horizontal scale and several vertical scales are needed much more
often; the Form_DataViewerTXT.cs demonstrates such a case.
When all the needed columns are selected and the button is pressed, then an array of X data is filled and
arrays of Y data are included into a List.
double [] fxData = null;
List<double []> fyDataMult = new List<double []> ();
Data reading is done by the ReadSeveralFunctions() method. Each Y(x) function uses its own Y scale; maximum
and minimum values on each scale are determined by the range of values from the corresponding Y array. The new number
of vertical scales can differ from the number of scales from previous case; the unneeded scales are deleted, the new scales
can be added; the ranges of the existing scales are adjusted. Because the number of scales can change and all the scales are
registered in the mover queue individually, then this queue must be renewed by calling the RenewMover() method.
private void Click_btnShow (object sender, EventArgs e)
{
… …
// change Plot
double minVal, maxVal;
plot .SetGrid (LineDir .Ver, fxData [0], fxData [fxData .Length - 1],
GridOrigin .ByMinLines, 4);
Scale scale = plot .HorScales [0];
scale .ImmutableParams =
Immutable .ValueLT | Immutable .ValueRB | Immutable .ValuesSwap;
scale .AddComment (0.5, 20, "Column " + iColumnX .ToString (), fntSelected,
0, ForeColor);
for (int j = plot .VerScales .Count - 1; j >= nColumnsY; j--)
{
plot .VerScales .RemoveAt (j);
}
// change existing vertical scales
for (int j = 0; j < plot .VerScales .Count; j++)
{
Auxi_Common .Range (fyDataMult [j], true, out minVal, out maxVal);
plot .VerScales [j] .Grid (maxVal, minVal, GridOrigin .ByMinLines, 4);
}
RectangleF rcPlot = plot .Underlayer .Area;
for (int j = plot .VerScales .Count; j < nColumnsY; j++)
{
Auxi_Common .Range (fyDataMult [j], true, out minVal, out maxVal);
World of Movable Objects 659 (978) Chapter 18 Applications for science and engineering
scale = new Scale (this, maxVal, minVal, GridOrigin .ByMinLines, 4,
false, false, rcPlot, LineDir.Ver, 1 + (j - 1) * 34,
Side .W, new Lining (TextBasis .W));
scale .Font = fntSelected;
plot .AddVerScale (scale);
}
for (int j = 0; j < nColumnsY; j++)
{
scale = plot .VerScales [j];
scale .Comments .Clear ();
scale .AddComment (0.5, -30, "Column " + iColumnsY [j] .ToString (),
fntSelected, 60, ForeColor);
}
RenewMover ();
}
With all arrays and all scales prepared, the drawing of all Y(x) functions in the same plotting area is not a problem at all.
The Plot.DrawYofX() method uses a PlotAuxi parameter which allows to specify the numbers of horizontal and
vertical scales that are used for drawing.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
plot .Draw (grfx);
if (fxData != null && fxData .Length >= 2)
{
for (int i = 0; i < fyDataMult .Count; i++)
{
plot .DrawYofX (grfx, new PlotAuxi (i, 0, i,
SegmentLocation .Anywhere), fxData, fyDataMult [i], -1);
}
}
groupSelection .Draw (grfx);
groupData .Draw (grfx);
}
The Form_DataViewerTXT.cs uses the same classes which were already discussed (Plot, ElasticGroup, and
ArbitraryGroup) and it is designed in a standard way, so that it can be used without any special instructions at all. You
can see a very short information about this form in the bottom left corner of figure 18.38, but it was specially added when I
prepared the original application for being used in Demo. There is no such information in the version which scientists use,
but there is some information available through the About command of upper menu; I’ll write about it a bit further in the
special subsection.
There are three main objects in this form – two groups and a plot. Tuning of each of them starts with a double click
(different tuning forms are opened for the main plotting area and for each scale), but there is also a small menu on each of
these three objects. Menus on two groups include commands to call their tuning forms. These commands duplicate the use
of double click, so menus were purposely organized for other commands.
wide, but the plotting area in the Form_DataViewerTXT.cs is restricted by the coexistence of two groups.
Consider a plot from figure 18.38 as only a sketch which gives you an idea about the selected functions. A single
command from menu on the plot opens another form with the same plot (figure 18.41).
Form_OnePlot.cs shows
the same set of Y(x)
functions which were
selected previously in the
Form_DataViewerTXT.cs.
Tuning of the plotting area
and scales is done via their
standard tuning forms which
can be opened with a double
click. There are also context
menus on the plotting area
(figure 18.42a), on scales
(figure 18.42b), and on
comments (figure 18.42c).
Several menu commands
call the same tuning forms;
some commands duplicate
the actions which can be
done inside tuning forms.
Many of these commands
were already described
while I wrote about the
Form_Functions.cs, but
there are several new
commands which were not
used in previous examples. Fig.18.41 The Form_OnePlot.cs provides all the possibilities for tuning of the graphs.
This is the first example
with multiple vertical scales. The plotting area is shown with the grids associated with one of those scales. By default, it is
the first vertical scale, but this can be changed at any moment by calling menu on the needed scale and using its command
Select grid of this scale.
• Scales can be associated with the same physical parameter, but the problems can occur because of the big
difference in ranges. For example, if concentrations of two gases differ by two orders and you unite their scales,
then for one of them the graph will look like a straight horizontal line.
• By default any vertical scale is organized with the values growing from bottom to top, but there is a small button in
the scale tuning form which allows to swap the end values, so values along the scale can grow in any direction. If
you have one vertical scale with the range [10, -10], which means 10 on top and -10 at the bottom, and another
scale with the range [-10, 10], then try to explain to yourself what you expect to see after uniting these scales.
Certainly, I can include into the program some decision for such case, but any decision by a program means in
reality a decision by developer (!); it means that the program (developer) makes a decision instead of user. The
majority of scientists (or all the TRUE scientists) hate to work with the programs which are making their own
decisions. I do not like to use the programs that are making their own decisions instead of me and I avoid using
such technique in my programs.
Examples demonstrated in this chapter were mostly developed for a group of very bright scientists and I always prefer to
think that users of my programs have brains and know how to use them.
One of the rules of user-driven applications declares that whatever is done by a user must be saved with a possibility of
restoration in the same view when the program is used again. Maybe user is working on some special view of the graphs
and has to interrupt his work at one moment to continue it later. At the closing of the form, its view is automatically saved
in the Registry; the next time, even with another input data, an application will try to organize the view of this form as
close as possible to the saved one. But if you need to continue your work on exactly the same input data with all the
changes that were already made, then you have to save this data through the Save command of the upper menu. Data used
in this plotting and all parameters of visualization for the plotting area and the graphs are saved in a binary file. If you want,
you can type a comment into the special area; this comment will be saved with everything else. With the help of a small
menu on the filename viewer, you can select the filename from it and include it into comment in a standard way.
Suppose that you used the Save menu and saved your plot in a binary file. What are you going to do with it? This data
viewer works only with the TXT input files.
Originally this data viewer was designed both for TXT and BIN files. Later I found out that it was much better to have not
one but two separate data viewers to deal with two types of input files. To demonstrate the whole cycle of dealing with data
originally obtained from analog-digital converters, I included the Simple data viewer for BIN files into the Demo
application; these two viewers can be called via two consecutive commands in the same Applications menu. The viewer for
BIN files – Form_DataViewerBIN.cs - does not need any selection of data from the huge source files; this viewer gets
already selected data and shows it in the same way as the Form_OnePlot.cs (figure 18.41). These two forms are so much
alike that there is no need to show another figure with the second form; the tiny difference between two forms is only in
their upper menu while all other menus on a plotting area, on scales, and on comments are identical. The input data BIN
file can be found in the same directory as for TXT viewer (figure 18.43).
Fig.18.43. Data files for TXT and BIN simple data viewers are saved in the same subdirectory
Text_Rotatable Object of this class can be rotated around its central point; positioning is done by the same point.
Objects of both classes can be used directly but more
often they are used indirectly. Text_Rotatable
class is used as a base class for all different types of
comments. Text_Horizontal class is the base class
for unclosable information (UnclosableInfo class)
and for information which can be hidden and unveiled
(InfoOnRequest class). There is hardly any form in
Demo application without using one or both of
mentioned text classes.
In nearly all the examples of Demo application, objects
of the Text_Horizontal and Text_Rotatable
classes and objects derived from them are used as
auxiliary elements. They are often used as subordinate
elements in some complex objects in which the dominant
element is either control or some big graphical object. In
the Form_AboutDataViewerTXT.cs texts play all
possible roles: there is a simple object of the
Text_Horizontal class, there is information of the
InfoOnRequest class which is derived from the
Text_Horizontal class, and there are complex
objects in which dominant and subordinate elements are
texts. Lets us start with these new objects.
An object of the Text_Hor_Dominant class
consists of one main text and any number of subordinate
texts. All those texts do not need rotation, so all of them
are based on the Text_Horizontal class. I’ll
remind the basic features of this class. Fig.18.44 Except for one small button, all elements in this
form belong either to the Text_Horizontal
• Horizontal text consists of one or more lines.
class or to one of its derivatives.
• Object occupies a rectangular area. Height is
determined by the number of lines and used font; width is determined by the longest line.
• Text location is determined by its top left corner.
• Object is non-resizable but movable. Cover consists of a single rectangular node. Object is moved by any inner
point.
New Text_Hor_Dominant class has only one field of its own – the List of subordinates.
public class Text_Hor_Dominant : Text_Horizontal
{
List<Text_Hor_Subordinate> m_subordinates =
new List<Text_Hor_Subordinate> ();
Class inherits the DefineCover() and MoveNode() methods from the base class and only the Move() method is
new.
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
InformRelatedElements ();
}
This is a classical design of a complex object: whenever a dominant element is moved, all subordinates must be informed
about the change.
private void InformRelatedElements ()
{
for (int i = 0; i < m_subordinates .Count; i++)
{
World of Movable Objects 663 (978) Chapter 18 Applications for science and engineering
m_subordinates [i] .DominantArea = base .RectAround;
}
}
The very first example in the chapter Complex objects demonstrated rectangles with comments. Those comments were
rotatable and their location was determined by central point, but the main thing in that old example was the mechanism of
comment positioning in relation to dominant rectangle. In case of the Text_Hor_Dominant class, exactly the same
positioning technique for subordinates is used. Any subordinate has the rectangle of the main element and coefficients to
describe relative position to this rectangle; on any movement of dominant or subordinate, only one of these parameters is
changed.
public class Text_Hor_Subordinate : Text_Horizontal
{
RectangleF rcDominant;
double xCoef; // calculated as LT corner in relation to rcDominant
double yCoef;
On initialization, a Text_Hor_Subordinate object gets the rectangle of the dominant text and the point of
subordinate top left corner; two positioning coefficients are easily calculated from those values.
public Text_Hor_Subordinate (RectangleF rcParent, Form form, PointF ptLeftTop,
string txt, Font fnt, Color clr, bool show_frame, Color back)
: base (form, ptLeftTop, txt, fnt, clr, show_frame, back)
{
rcDominant = rcParent;
Auxi_Geometry .CoefficientsByLocation (rcDominant, ptLeftTop,
out xCoef, out yCoef);
}
From this moment any movement of a dominant or subordinate part can change only one of the parameters while another
one is calculated according to this change.
• If the dominant element is moved, then it sends the bounds of its new area to each subordinate; the new position of
subordinate is calculated from this new area by using the unchanged coefficients. This is done by the
Text_Hor_Subordinate.DominantArea property.
public Rectangle DominantArea
{
get { return (rcDominant); }
set
{
rcDominant = value;
Location =
Auxi_Geometry.LocationByCoefficients (rcDominant, xCoef, yCoef);
}
}
• If a subordinate text is moved, then its new position is used to calculate the new coefficients in relation to the
unchanged area of the dominant text. This is done by the Text_Hor_Subordinate.Location property
new public Point Location
{
get { return (ptLT); }
set
{
base .Location = value;
Auxi_Geometry .CoefficientsByLocation (rcDominant, ptLT,
out xCoef, out yCoef);
}
}
Any class of movable objects has to override three abstract methods from the GraphicalObject class. For the
Text_Hor_Subordinate class, two of these methods are inherited from the Text_Horizontal class, while the
third one – the Move() method – has to be written because the new coefficients must be calculated on moving an object.
World of Movable Objects 664 (978) Chapter 18 Applications for science and engineering
public override void Move (int dx, int dy)
{
base .Move (dx, dy);
Auxi_Geometry .CoefficientsByLocation (rcDominant, ptLT,
out xCoef, out yCoef);
}
Now it is time to look at the design and work of the Form_AboutDataViewerTXT.cs (figure 18.44). The upper text is a
simple object of the Text_Horizontal class.
private void DefaultView ()
{
Spaces spaces = new Spaces (this);
int cxL = spaces .FormSideSpace;
textGeneral = new Text_Horizontal (this, new PointF (cxL,
spaces .FormTopSpace), strGeneral, fntOrdinary, false);
… …
Next three groups have similar view: a piece of text
with overhanging short title. Title is shown in bigger
font and different color (figure 18.45). I purposely
used two fonts to highlight the difference between
dominant and subordinate parts of each object. Fig.18.45 An object of the Text Hor Dominant class
private void DefaultView ()
{
… …
float fHanging = 30;
thdData = TitledInfo (new PointF (cxL, textGeneral .RectAround .Bottom +
spaces .Ver_betweenFrames),
strData_Title, fHanging, strData);
thdSettings = TitledInfo (new PointF (cxL, thdData .RectAround .Bottom +
spaces .Ver_betweenFrames),
strSettings_Title, fHanging, strSettings);
thdTuning = TitledInfo (new PointF (cxL, thdSettings .RectAround .Bottom +
spaces .Ver_betweenFrames),
strTuning_Title, fHanging, strTuning);
… …
Because three objects have similar view, each of them is prepared by the TitledInfo() method.
public Text_Hor_Dominant TitledInfo (PointF ptLT_title, string strTitle,
float fHang, string strText)
{
Text_Horizontal sub = new Text_Horizontal (this, ptLT_title, strTitle,
fntTitles, clrTitles, false, BackColor);
Text_Hor_Dominant domin = new Text_Hor_Dominant (this,
new PointF (ptLT_title .X + fHang, sub .RectAround .Bottom),
strText, fntOrdinary, clrOrdinary, false, BackColor);
domin .AddSubordinate (sub);
return (domin);
}
Determination of the dominant and subordinate parts in such pair of texts is not so simple as it seems. It reminds me of the
famous problem of whether a dog wags the tail or the tail wags the dog. Title immediately attracts the attention because it is
shown with bigger font and it is brighter, but I think more about movability. When the dominant part is moved, then
subordinates move synchronously. According to this logic, the ordinary information is a dominant part, while its title is a
subordinate. Does it look strange to you that the most noted part of a set – a brightly colored title – plays the role of a
subordinate? You can easily change the code and declare the title a dominant part in order to check another variant and
compare them, but in my version a title is subordinate. This is obvious from the code of the TitledInfo() method.
Three pieces of information – institution, author, and date – are united into another Text_Hor_Dominant object in
which the longest piece is used as dominant element. No part of this object is shown in different font or color and this
added a bit of complexity into the change of fonts and colors for elements of this form.
World of Movable Objects 665 (978) Chapter 18 Applications for science and engineering
There is a small menu which can be called on any text (except one) in the Form_AboutDataViewerTXT.cs. (The only
exception is the form information which is modified through the special tuning form which is called by double click.) Menu
includes two commands to change font and color. The easiest way would be to change font and color only for the pressed
text, but I organized it in more interesting way which required a bit of extra code.
Only for the purpose of tuning, all texts are divided into two groups: three titles constitute one group while all other texts are
included into another group. Elements of each group have the same font and color, so there are two fonts and two colors.
Three titles belong to the Text_Hor_Subordinate class and it would be very easy to make a decision by analysing
the class of the released object. Unfortunately, there is one more object in which elements of the same
Text_Hor_Subordinate class must be shown in ordinary font and color; this required an additional checking. In any
complex object, subordinate keeps the id of its dominant element; this makes the additional checking very easy.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (grobj is Text_Horizontal)
{
if (e.Button == MouseButtons.Right && fDist <= 3 && id != info .ID)
{
if (grobj is Text_Hor_Subordinate &&
grobj .ParentID != thdInstitute .ID)
{
bChangeTitles = true;
}
else
{
bChangeTitles = false;
}
ContextMenuStrip = menuOnText;
}
}
… …
After the belonging of the pressed text to one or another group is determined, the change of font and color for all elements
of the group is easy.
The Form_AboutDataViewerBIN.cs – an auxiliary form with short information about data viewer for BIN files – is
designed in similar way.
World of Movable Objects 666 (978) Chapter 18 Applications for science and engineering
When the Form_GraphManualDefinition.cs is opened for the first time, it shows two big empty objects and the small one
(figure 18.46). Big objects are the plotting area and the table; the small one is a rectangle with a bright circular spot and a
short information Press and place next to it. You can ignore this small object and define your function in an old way by
typing the new values into the table. Or you can use this small object of the
SpotNest class and make the definition of a function much easier. Just do what the
information tells you to do (figure 18.47). Press the colored spot and move it to the new
location inside the plotting area where you want to place a tiny spot to represent the new Fig.18.47 SpotNest object
(x, y) pair. If you release the spot anywhere outside the plotting area, then nothing will happen as it is a wrong place. If
you release a spot anywhere inside the plot, then the new (x, y) pair will appear in the table and the new spot in the plotting
area. If it is the first spot dropped in the plotting area, then a small colored spot appears at the point of release; otherwise
the spots in the plotting area are connected into a broken line and the new spot is automatically included into this line by
adding one more segment to it.
The SpotNest is relatively simple but interesting class. Parameters of its constructor define the place, size, and view of
an object.
public SpotNest (Form form,
PointF ptLT, // top left corner of rectangle
Font font, // text font
Color clr, // text color
bool show_frame, // flag to decide the showing of a frame
Color clrBack) // background color
The specified font defines the size of the text; an extra space is added to the left of the text; this is the place for the colored
patch. The whole rectangular object can be moved around by any inner point; the only exception is the patch which can be
moved individually. The patch must be shown atop the rectangular area and must be caught ahead of the area on which it
resides. The cover for such object is obvious: two nodes of which the small circular one must be the first.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, ptSpot, nSpotRadius),
new CoverNode (1, rc)};
cover = new Cover (nodes);
}
The patch has an interesting behaviour: it can be moved anywhere around the screen, but whenever it is released, it must be
returned home to its “nest”. The MoveNode() method is easy as there are no restrictions on moving both nodes.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
switch (iNode)
{
case 0:
ptSpot = ptM;
bRet = true;
break;
case 1:
Move (dx, dy);
break;
}
}
return (bRet);
}
The patch – the colored circle – is relatively small. In the case of the patch I can ignore the difference between the cursor
and the center of a circle at the moment when the circle is pressed by the mouse. From this moment and up till the spot
release the changing mouse position (ptM) is used as the real place of the patch (ptSpot). But, as you will see shortly,
World of Movable Objects 668 (978) Chapter 18 Applications for science and engineering
not all even small objects allow such a substitution; some of the small objects may demand to take into consideration any
tiny discrepancy between the mouse position and the real position of an object.
Return of the patch to the “nest” works in such a way. Two PointF fields are defined in constructor:
• ptNest describes the position at home (the “nest”); its relative position inside the SpotNest object never
changes
• ptSpot describes the current position of the patch and it can be anywhere
When a patch is moved around by a mouse, it can be at any location, so the ptSpot value is used for drawing a patch.
public void Draw (Graphics grfx)
{
grfx .FillRectangle (brushBack, rc);
if (bFrame == true)
{
ControlPaint .DrawBorder3D (grfx, Rectangle .Round (rc), borderstyle);
}
Auxi_Drawing .Text (grfx, text, font, 0, clrText,
Point .Round (rcText .Location), TextBasis .NW);
Auxi_Drawing .FillCircle (grfx, ptSpot, nSpotRadius, clrSpot);
}
When the patch is released, then some of the actions depend on the point of its release, for example, a new spot must be
added to the graph if the patch was released inside the plotting area. But the patch itself must be returned home
immediately by the SpotNest.SpotReturnHome() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iReleased;
if (mover .Release (out iReleased, out iNode, out shapeReleased))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is SpotNest && shapeReleased == NodeShape .Circle)
{
spotNest .SpotReturnHome ();
… …
This method SpotNest.SpotReturnHome() is primitive; it simply assigns the ptSpot the “nest” value. The
patch is back and ready to be used again.
public void SpotReturnHome ()
{
ptSpot = ptNest;
}
Though the SpotNest object is a small one, it is an object in the normal user-driven
application, so all its parameters of visibility must be controlled by users. Small menu
on this object allows to change four parameters of visibility (figure 18.48). Only one
parameter is excluded from tuning in the current version – the area transparency. This
SpotNest object can be placed not only outside the plotting area (see figure 18.46) Fig.18.48 Menu on the
but also atop the plotting area (figure 18.49); especially for such a case some SpotNest object
transparency of this object can be helpful. Because of this, the SpotNest object is initialized with some transparency; this
coefficient is not changed when the background color is changed. You can easily add the tuning of transparency for the
SpotNest object; this is demonstrated, for example, in text tuning in the Form_Text_Horizontal_Class.cs.
The SpotNest object is used to add new spots on the graph. The first time you drop the patch inside the plotting area, a
small spot will appear at the place. Beginning from the second spot and further on all of them will be linked in order by
segments (figure 18.49). All (x, y) pairs are ordered according to the increase of their x component; the same x value for
the consecutive spots is allowed. In some cases the positioning of the spots on the plotting area is just the best way to define
World of Movable Objects 669 (978) Chapter 18 Applications for science and engineering
the function. In other cases more accurate definition of values may be required; for this purpose you can use editing of
numbers in the table. New (x, y) pairs can be also added through the table; the new segments will be inserted according to
the increase of their x components. There is one more way of adding new spots to the graph: press the graph with the left
button and the new spot will appear at this place; I will write about it a bit further.
Usually the tuning form of the horizontal scale allows to change both end values and in a real application I would certainly
allow such thing. However, if you change the end values in such a way that the X numbers would decrease from left to
right, then I should have also to change the logic of entering the new values into the table. This would only make the code
more complicated but would have nothing to do with the discussion of the objects movability and the design of such
application, so I blocked this feature by declaring some of the parameters for the scale immutable.
plot .HorScales [0] .ImmutableParams = Immutable.ValueLT | Immutable.ValueRB |
Immutable.ValuesSwap;
The next interesting object in the
Form_GraphManualDefinition.cs
is the graph (class
SpotsOnPlot) which is painted
as a set of colored spots connected
by segments. This is how it looks at
figure 18.49, but there is a way to
change the view of this graph. I will
return to this a bit later; now let us
look at the cover of the
SpotsOnPlot object.
The main part of the
SpotsOnPlot class is a
collection of points at which the
spots must be placed. This
collection is associated with the Fig.18.49 A graph can be determined in parallel by adding/moving/deleting the
Plot object which has its spots and by editing/adding the values in the table
boundary values, so each spot of the
SpotsOnPlot object gets the associated (x, y) pair.
public class SpotsOnPlot : GraphicalObject
{
Plot plot;
List<PointF> pts = new List<PointF> ();
List<double> args = new List<double> ();
List<double> vals = new List<double> ();
The design of cover for the SpotsOnPlot class is determined by several ideas about the use of this class.
• All spots must be moved individually, so each of them must be covered by a small circular node. No other
movement is needed for this object, so it looks like no other nodes are needed either, but there are other things that
have to be considered.
• Moving the patch of the SpotNest object and dropping it somewhere inside the plotting area is not the only
way of adding new spots to the graph. I want also to add the new spots by pressing any segment at any needed
place. This can be organized by analysing the geometry of this set of spots, but I prefer to use mover for such task.
If each segment would be covered by a thin strip node, then mover can inform about the mouse pressing at the
needed place. These strip nodes are not going to be used for any movement of an object (!); they are artificial but
very important pieces of the whole design.
• Spots can be moved outside the plotting area, though the graph is not shown outside this area. The problem of
painting up to the borders of rectangular area (see the right end of the graph at figure 18.49) is easily solved by
using some standard clipping throughout the time of drawing. Much more important is to organize it so that the
circular and strip nodes outside the plotting area will be not detected by mover. The obvious solution to this is to
cover everything outside the plotting area by a transparent node; it is impossible to do with one node, but four
transparent nodes can cover everything outside the rectangular area. The use of such cover is based on the correct
order of transparent and non-transparent nodes in the cover of the SpotsOnPlot class.
1. Four transparent nodes to cover everything outside the plotting area.
World of Movable Objects 670 (978) Chapter 18 Applications for science and engineering
*
In this case it is possible to design a dominant element as a standard Panel with the lines painted on it and then such
group can be organized as an ElasticGroup; this was done in the older versions. But panel, as any other control, can
be moved only by its border. I prefer to use elements which can be moved by any inner point. For this reason, panel is
substituted by the ResizableRectangle object and instead of the ElasticGroup class the ArbitraryGroup
class is used.
World of Movable Objects 677 (978) Chapter 18 Applications for science and engineering
… …
groupGrids = new ArbitraryGroup (this,
new GroupVisibleParameters (this, true), "Grids",
ElemsArea_groupGrids, DrawElems_groupGrids,
SynchroMove_groupGrids, IntoMover_groupGrids);
groupGrids .Location = new PointF (groupBorders .FrameArea .Left,
groupBorders .FrameArea .Bottom + spaces .Ver_betweenFrames);
… …
For ElasticGroup objects, a many things related to the synchronous movement of the involved elements are provided
automatically; this is possible because all the standard controls have rectangular areas and it is easy to calculate them. For
ArbitraryGroup objects, nothing can be defined in general; for each object the needed things are described by four
methods provided on initialization of a group.
• The ElemsArea_groupGrids() method is used to calculate the combined rectangular area of all inner
elements. Spaces between the inner elements and the frame are set at the moment of initialization (and can be
changed in tuning form); so the frame position and thus the cover of the group can be easily calculated when this
area is known.
private RectangleF ElemsArea_groupGrids ()
{
return (RectangleF .Union (RectangleF .Union (RectangleF .Union (
rrVerGrid .Rectangle, rrHorGrid .Rectangle),
btnVerGridClr .Bounds), btnHorGridClr .Bounds));
}
• The DrawElems_groupGrids() method is used to draw the inner elements. Only two
ResizableRectangle elements require drawing; the needed methods to draw the samples of line styles can
be found in the MoveGraphLibrary.dll.
void DrawElems_groupGrids (Graphics grfx)
{
Draw_VerGrid (grfx);
Draw_HorGrid (grfx);
}
void Draw_VerGrid (Graphics grfx)
{
Auxi_Drawing .LineStyles_WithEmptyPosition (grfx, rrVerGrid .Rectangle,
iVerGridStyle, bShowVerGrid, LineDir .Ver);
}
void Draw_HorGrid (Graphics grfx)
{
Auxi_Drawing .LineStyles_WithEmptyPosition (grfx, rrHorGrid .Rectangle,
iHorGridStyle, bShowHorGrid, LineDir .Hor);
}
• The SynchroMove_groupGrids() method describes the synchronous movement of all inner elements. For
buttons, their location must be changed directly; for ResizableRectangle elements, their Move()
method is used.
void SynchroMove_groupGrids (int dx, int dy)
{
rrVerGrid .Move (dx, dy);
rrHorGrid .Move (dx, dy);
btnVerGridClr .Left += dx;
btnVerGridClr .Top += dy;
btnHorGridClr .Left += dx;
btnHorGridClr .Top += dy;
}
World of Movable Objects 678 (978) Chapter 18 Applications for science and engineering
• The IntoMover_groupGrids() method guarantees the correct order of registering for the group and its
inner elements. As usual, controls must be registered ahead of the graphical objects; the group itself must be the
last one.
void IntoMover_groupGrids (Mover mv, int iPos)
{
mover .Insert (iPos, groupGrids);
mover .Insert (iPos, rrVerGrid);
mover .Insert (iPos, rrHorGrid);
mover .Insert (iPos, btnVerGridClr);
mover .Insert (iPos, btnHorGridClr);
}
Group for tuning the border lines - groupBorders - has some similarities in design and behaviour with the previous
group, but it also has some differences. This group contains three inner elements (figure 18.56) of which the bigger one
belongs to the ResizableRectangle class and plays the role of the dominant element in this group
(rrBorderStyles). One of subordinate elements is again a small non-resizable button;
another – rrSketch – belongs to the ResizableRectangle class but is used as
non-resizable element; this is easily organized by using another constructor of the class.
ArbitraryGroup groupBorders;
ResizableRectangle rrBorderStyles, rrSketch;
Fig.18.56 Group for
private void DefaultView () tuning borders
{
… …
// groupBorders
Rectangle rcBorderStyles = new Rectangle (200, 200, 79, 52);
rrBorderStyles = new ResizableRectangle (rcBorderStyles, new Size (40, 34),
new Size (200, 120), Draw_BorderStyles);
rrBorderStyles .AreaCursor = Cursors .Hand;
Rectangle rcSketch = new Rectangle (rcBorderStyles .Right + 12,
rcBorderStyles .Top, 32, 32);
rrSketch = new ResizableRectangle (rcSketch, Draw_Sketch);
rrSketch .AreaCursor = Cursors .Hand;
btnBorderClr .Location = Point .Round (new PointF (
rrSketch .Rectangle .Right + 12, rrSketch .Rectangle .Top));
groupBorders = new ArbitraryGroup (this,
new GroupVisibleParameters (this, true), "Borders",
ElemsArea_groupBorders, DrawElems_groupBorders,
SynchroMove_groupBorders, IntoMover_groupBorders);
groupBorders .Location = new PointF (
groupComments .FrameArea .Right + spaces .Hor_betweenFrames,
groupComments .FrameArea .Top);
… …
Group Main area for setting the background color and transparency of the area consists of the resizable graphical object (of
the Trackbar class) and a small button. Though this group looks similar to the previous groups, here the bigger object is
not declared dominant; the small button can be placed anywhere and even entirely in the area of the track bar
(figure 18.57). In real application, it is definitely a sign of poor design when similar
objects behave differently. This is Demo application used for explanations, so I
purposely made this group slightly different in order to show different possibilities. In
all other aspects the groups are similar.
ArbitraryGroup groupArea;
Trackbar trackbarTransparency; Fig.18.57 Group for tuning the
background color and
private void DefaultView () transparency of the area
{
… …
// groupArea
TrackbarSlider slider = new TrackbarSlider (Side .N, new PointF (cxL, cyT),
new PointF (cxR, cyT), fTransparency);
World of Movable Objects 679 (978) Chapter 18 Applications for science and engineering
trackbarTransparency = new Trackbar (this, slider, true, nSpaceToTicks,
nTicksLen, 11, Draw_Transparency);
trackbarTransparency .AddComment (0.5, hHalf, "Transparency", Font, 0,
ForeColor);
trackbarTransparency .AddComment (0.0, hHalf, "0", Font, 0, ForeColor);
trackbarTransparency .AddComment (1.0, hHalf, "1", Font, 0, ForeColor);
groupArea = new ArbitraryGroup (this,
new GroupVisibleParameters (this, true), "Main area",
ElemsArea_groupArea, DrawElems_groupArea,
SynchroMove_groupArea, IntoMover_groupArea);
groupArea .Location = new PointF (groupGrids .FrameArea .Left,
groupGrids .FrameArea .Bottom + spaces .Ver_betweenFrames);
… …
The last group in the form – group Colors and filling – also belongs to the
ArbitraryGroup class. This group was not used anywhere before, but in the
next chapter you will see the use of such group in the tuning form for bar charts.
There are no controls in this group (figure 18.58); it includes two
ResizableRectangle elements; each of these rectangles has inner movable
elements. Small colored rectangles can be moved inside their area; two small
circles can be moved along the rail. Fig.18.58 Group for changing the
order of colors and filling
ArbitraryGroup groupColors; the bar chart area
ResizableRectangle rrSamples, rrFilling;
List<RectInsideRect> samples = new List<RectInsideRect> ();
TwoEndFiller filler;
private void DefaultView ()
{
… …
// groupColors
rrSamples = new ResizableRectangle (rc, new Size (w / 3, h / 2),
new Size (w * 2, h * 2), Draw_RR_Samples);
rrSamples .AreaCursor = Cursors .Hand;
CreateRectSamples ();
int h_rr = spaceAroundFiller + hFiller + hTicks + spaceAroundFiller;
RectangleF rcFilling = new RectangleF (rc .Left, rc .Bottom + 12,
rc .Width, h_rr);
rrFilling = new ResizableRectangle (rcFilling, new Size (w / 3, h_rr),
new Size (w * 2, h_rr), Draw_RR_Filling, Change_RR_Filling);
groupColors = new ArbitraryGroup (this,
new GroupVisibleParameters (this, true),
"Colors and filling",
ElemsArea_groupColors, DrawElems_groupColors,
SynchroMove_groupColors, IntoMover_groupColors);
groupColors .Location = new PointF (groupComments .FrameArea .Left,
groupComments .FrameArea .Bottom + spaces .Ver_betweenFrames);
… …
Before discussing in more details the main three methods related to the mouse events of the
Form_DesignOfTuningForms.cs, I want to attract attention to one aspect of initializing the elements of this Colors and
filling group. Each ResizableRectangle object of this group includes movable objects.
• The upper rectangle (rrSamples) includes a whole set of colored samples of the RectInsideRect class;
these small rectangles can be moved inside the area and released anywhere thus allowing to change their order.
The allowed area of moving these samples is defined by their “parental” rectangle; whenever the rrSamples
object is moved or resized, its changed rectangular area must be sent to each sample and the new height and
position of each sample must be calculated. (The width of the samples is fixed.) Notification of all the samples
about the change of their area is made from inside the OnMouseMove() method of the form.
World of Movable Objects 680 (978) Chapter 18 Applications for science and engineering
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is ResizableRectangle)
{
long id = grobj .ID;
ResizableRectangle rr = grobj as ResizableRectangle;
Point pt;
if (id == rrSamples .ID)
{
foreach (RectInsideRect sample in samples)
{
sample .Areal = rrSamples .Rectangle;
sample .Height = sample .Areal .Height - 6;
}
SamplesStandardPlaces ();
}
… …
• The lower rectangle (rrFilling) includes an object of the TwoEndFiller class; it is shown as a colored
strip with two movable balls at the ends. Whenever this “parental” rectangle is moved or resized (only its width
can be changed), the inner element changes its size / position, but there are no traces of these changes in the
OnMouseMove() method. Instead, the rrFilling object is initialized with an additional parameter – the
Change_RR_Filling() method which changes the size and position of the inner element according to the
changes of the “parental” rectangle. This is another way of organizing the same thing.
void Change_RR_Filling ()
{
Rectangle rc = rrFilling .Rectangle;
filler .Rectangle = Rectangle .FromLTRB (rc .Left + spaceAroundFiller,
rc .Top + spaceAroundFiller,
rc .Right - spaceAroundFiller, rc .Bottom - spaceAroundFiller);
}
Now, when all the groups of the Form_DesignOfTuningForms.cs are initialized, it is time to look into some details of
moving all its elements.
How many movable elements are there in the form? You can estimate it by running this example or you can try to do it by
looking at figure 18.53. If I am not mistaken, there are (33 + N) movable elements, where N is the number of the colored
samples in the Colors and filling group. Thus, we receive 40 movable objects, but only two special situations are mentioned
in the OnMouseDown() method of the form.
• Catching of the dominant ResizableRectangle objects inside the groups Borders and Grids.
• Catching of colored samples inside the Colors and filling group.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (mover .CaughtSource is ResizableRectangle)
{
ResizableRectangle rr =
mover .CaughtSource as ResizableRectangle;
long id = mover .CaughtSource .ID;
if (id == rrBorderStyles .ID)
{
Auxi_Geometry .CoefficientsByLocation (rr .Rectangle,
btnBorderClr .Location, out coefBtnX, out coefBtnY);
World of Movable Objects 681 (978) Chapter 18 Applications for science and engineering
Auxi_Geometry .CoefficientsByLocation (rr .Rectangle,
rrSketch .Location, out coef_X2, out coef_Y2);
}
else if (id == rrVerGrid .ID)
{
Auxi_Geometry .CoefficientsByLocation (rr .Rectangle,
btnVerGridClr .Location, out coefBtnX, out coefBtnY);
}
else if (id == rrHorGrid .ID)
{
Auxi_Geometry .CoefficientsByLocation (rr .Rectangle,
btnHorGridClr .Location, out coefBtnX, out coefBtnY);
}
}
else if (mover .CaughtSource is RectInsideRect)
{
(mover.CaughtSource as RectInsideRect) .StartMove (e.Location);
}
}
ContextMenuStrip = null;
}
What is so special about these elements? Why other elements are not mentioned?
Moving / resizing of the majority of elements is described in the MoveNode() and Move() methods of their classes,
so these movements do not need any special mentioning anywhere else. For example, group Comments has nine movable
elements: seven elements inside, the group itself, and its title. Two inner elements are resizable; the group is resizable as a
result of moving its inner elements; all possible movements and resizing are described in the mentioned methods of the
classes DominantControl, SubordinateControl, and ElasticGroup. In other groups several other classes
are involved in movements, but nearly everything is described in their methods.
There are two types of special situations which have to be considered at the starting moment of a movement.
• The most common is the case of the related elements when position of one is described by some coefficient in
relation to position of another; the positioning coefficient has to be calculated at the starting moment and kept
unchanged throughout the whole movement. This is the case of the dominant elements in the Borders and Grids
groups. When the dominant element is part of the DominantControl class, as in the Comments group, then
this is automated and is done behind the curtain. To emulate the same behaviour for a graphical object (in our case
it is a ResizableRectangle object), some code to calculate the needed positioning coefficient(s) must be
added, so the Auxi_Geometry.CoefficientsByLocation() method is used.
• The second situation is when the initial position of the mouse cursor is crucial. The colored samples can move
only inside the “parental” rectangle, but each sample is not a line of one pixel width, so the initial shift of the
cursor from the location of the caught sample must be calculated and stored. This is done by calling the
RectInsideRect.StartMove() method; as usual, the parameter of this method is the mouse location.
The above mentioned positioning coefficients are calculated in the OnMouseDown() method but only to be used inside
the OnMouseMove() method. While the dominant element played by the ResizableRectangle object is moved
or resized, the position of its subordinate(s) is determined by those coefficients.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is ResizableRectangle)
{
long id = grobj .ID;
ResizableRectangle rr = grobj as ResizableRectangle;
Point pt;
… …
else if (id == rrBorderStyles .ID)
World of Movable Objects 682 (978) Chapter 18 Applications for science and engineering
{
btnBorderClr .Location = Auxi_Geometry .LocationByCoefficients
(rr .Rectangle, coefBtnX, coefBtnY);
pt = Auxi_Geometry .LocationByCoefficients (rr .Rectangle,
coef_X2, coef_Y2);
rrSketch .Move (pt .X - rrSketch .Left, pt .Y - rrSketch .Top);
}
else if (id == rrVerGrid .ID)
{
btnVerGridClr .Location = Auxi_Geometry .LocationByCoefficients
(rr .Rectangle, coefBtnX, coefBtnY);
}
else if (id == rrHorGrid .ID)
{
btnHorGridClr .Location = Auxi_Geometry .LocationByCoefficients
(rr .Rectangle, coefBtnX, coefBtnY);
}
}
groupBorders .Update ();
groupGrids .Update ();
… …
The shown code of the OnMouseDown() and OnMouseMove() methods is about the situation when the movable
object belongs to the ResizableRectangle class and plays the role of a dominant element. Another special situation
to be considered occurs when a subordinate element is released; the decision to be made at this moment depends on some
general ideas of the form design.
The Form_DesignOfTuningForms.cs allows to organize and compare different types of dominant – subordinates relation.
The variant of dominants and subordinates all being controls is realized by the DominantControl class and can be
seen in the Comments group. For the DominantControl class, mover takes care of the special situation with
subordinate being released inside the area of dominant control; mover solves this problem, so there is nothing to worry
about.
In other groups of current example the role of dominant element is played by some graphical object and in such case the
release of subordinate control in the area of dominant element is not fatal at all. Controls are always shown atop all
graphical objects and are placed in mover queue ahead of graphical objects. Thus, if control is left atop its dominant
graphical object, then you can simply press the sensitive frame around control border and move this control to any other
position. Group Main area demonstrates this possibility without any addition; this is the only group in which a subordinate
control can be left atop and entirely inside the area of graphical dominant.
In other groups, there are some additions to simulate the behaviour of the DominantControl class, but there are some
differences which demonstrate that there are more variants than the two mentioned above. I have slightly changed the logic
and decided to call the forced relocation of subordinate elements inside the Borders and Grids groups in cases of even
partial overlapping of dominant and subordinate elements. Subordinate elements in these two groups belong to different
classes; this requires different checking of the involved elements.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (e .Button == MouseButtons .Left)
{
if (grobj is ResizableRectangle)
{
… …
if (id == rrSketch .ID)
{
if (rrBorderStyles .Rectangle .IntersectsWith (
rrSketch .Rectangle))
World of Movable Objects 683 (978) Chapter 18 Applications for science and engineering
{
rrSketch .Move (
Convert.ToInt32 (rrBorderStyles.Rectangle.Right +
8 - rrSketch .Location .X),
Convert.ToInt32 (rrBorderStyles .Rectangle .Top –
rrSketch .Location .Y));
groupBorders .Update ();
}
}
… …
}
… …
else if (grobj is SolitaryControl)
{
Control ctrl = (grobj as SolitaryControl) .Control;
Rectangle rcControl = ctrl .Bounds;
if (ctrl == btnBorderClr &&
rrBorderStyles .Rectangle .IntersectsWith (rcControl))
{
btnBorderClr .Location = Point .Round (
new PointF (rrBorderStyles .Rectangle .Right + 8,
rrBorderStyles .Rectangle .Top));
groupBorders .Update ();
}
else if (ctrl == btnVerGridClr &&
rrVerGrid .Rectangle .IntersectsWith (rcControl))
{
btnVerGridClr .Location = Point .Round (
new PointF (rrVerGrid.Rectangle.Right + 8,
rrVerGrid .Rectangle .Top));
groupGrids .Update ();
}
else if (ctrl == btnHorGridClr &&
rrHorGrid .Rectangle .IntersectsWith (rcControl))
{
btnHorGridClr .Location = Point .Round (
new PointF (rrHorGrid.Rectangle.Right + 8,
rrHorGrid .Rectangle .Top));
groupGrids .Update ();
}
}
… …
The Colors and filling group has neither dominant nor subordinate elements. There are two ResizableRectangle
objects inside this group; for better visualization, they are shown with sunken border (figure 18.58). Each of these
rectangles plays the role of a “parent” to other movable objects; the sizes and positions of the inner elements are regulated
by the changes of the “parental” rectangles. There is an unlimited flexibility in changing the sizes and positions of two
main rectangles in the group, but their overlapping can produce a real mess on the screen. To avoid this, I check the
possibility of overlapping for those two rectangles and put them side by side if it happens; the place to do this checking is
inside the same OnMouseUp() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
long id = grobj .ID;
if (e .Button == MouseButtons .Left)
{
if (grobj is ResizableRectangle)
World of Movable Objects 684 (978) Chapter 18 Applications for science and engineering
{
… …
else if ((id == rrSamples .ID || id == rrFilling .ID) &&
rrSamples .Rectangle .IntersectsWith (
rrFilling .Rectangle))
{
rrFilling.Move (Convert.ToInt32 (rrSamples .Left –
rrFilling .Left),
Convert.ToInt32 ((rrSamples .Bottom + 12) –
rrFilling .Top));
groupColors .Update ();
}
… …
These are the details of organizing the Form_DesignOfTuningForms.cs. As you will see in the next chapter, similar or
identical groups are used in the tuning forms of different plotting classes. I prefer to use the ElasticGroup class for
design of different forms and applications but in many cases when a group has to include some graphical objects, the use of
that class becomes impossible. For such cases I use the ArbitraryGroup class and try to organize the behaviour of
such group in identical way with the behaviour of any ElasticGroup object.
Any tuning form is designed to modify complex objects with a lot of parameters, so the tuning form itself is often not a
simple one and requires some tuning. Three classes used for the Form_DesignOfTuningForms.cs design –
ElasticGroup, ArbitraryGroup, and InfoOnRequest – have tuning forms of their own which can be called by a
double click. Tuning of the groups can be also started through context menus. This way of calling the same forms looks
like some redundancy, but I had to organize those menus for other commands and then I simply added commands for tuning
into those menus. The need for those menus on all the groups was caused by the complexity of those objects and my view
on implementation of rules of user-driven applications.
Suppose that you changed one group in preferable way and want other groups to look similar. You can change groups one
by one, but spreading of the same view on all other groups would be much easier. For this purpose menu on any group
includes command Use as sample. Groups of the ElasticGroup and ArbitraryGroup classes have identical
properties and methods to get and to set visibility parameters, so parameters can be spread from any group on all others.
The set of visibility parameters which can be spread from one group on another is described by the
GroupVisibleParameters class. The maximum set of parameters which can be spread is collected from the pressed
group by the GroupVisibleParameters property. The spreading of parameters is done by the
VisibleParametersAdjustment() method, but there is a word adjustment in its name because spreading can
depend on some conditions.
• Background color, spaces to the frames, and the flag regulating the frame appearance are changed unconditionally.
• Frame color is changed only if the frame is shown.
• All title parameters are changed only if there are titles in the current group and in the group from which the new
parameters are copied. Parameters include font, color, positioning coefficient, and the side spaces between title
and frame.
Data visualization
Data can be visualized through many different types of plots. Graphs of Y(x) functions were demonstrated in
the previous chapter. This chapter shows some other very popular plots turned into movable and resizable.
The world of data is infinitive. There are many forms of data visualization; there are many systems and libraries that allow
to show data in different ways. Throughout my professional life I worked on different scientific / engineering problems and
had to use mostly the standard plots of the Y(x) type or something similar. There were also special plots, like sonograms
which are among the most interesting things I was working on. With the invention of an algorithm for turning objects into
movable / resizable, I was especially looking for unfamiliar objects to test the algorithm on the objects of the unusual shape
or in strange situations. The variety of plots widely used outside the familiar to me scientific areas turned out to be a good
range for my ideas. The plots which I am going to discuss in this chapter are more often used for economical and financial
analysis than anywhere else. For some time I was using the term “financial plotting” for the objects which will appear in
this chapter, but this is not a correct term though it can give a main idea. The area of financial analysis and the discussions
of stock market use some types of plots which you will never see anywhere else. From my point of view, all these plots are
shouting to be turned into movable / resizable because they perfectly fit with the ideas of user-driven applications and can
be made movable / resizable to the huge benefit of their users. And there are millions and millions of people who daily
analyse the huge amount of economical and financial data. For such users the turn of all the used plots into objects which
anyone can rearrange according to his personal preferences means a lot in understanding of this huge amount of data and in
the outcome of their analysis.
I do not want to introduce you to the whole variety of plots for financial and economic analysis that can be turned into
movable and resizable. This chapter is limited to only three types of plots: bar charts, pie charts, and ring sets.
File: Form_PlotsVariety.cs
Menu position: Applications – Variety of plots
Figure 19.1 demonstrates the default view of the Form_PlotsVariety.cs. The BarChart and the PieChart classes
are represented at this view by one object each; the RingSet class is represented by two objects in order to show that an
object of this class can consist of any number of rings. Before discussing the Form_PlotsVariety.cs, let us look into the
details of these three mentioned classes.
Bar charts
There are many different types of bar charts; I am
going to demonstrate and discuss only the “classical”
one. Such bar chart consists of a set of rectangular
bars; the length of bars is proportional to the values
that they represent (figure 19.2).
From many aspects the BarChart class is similar
to the Plot class that was described in the previous
chapter. Because of these similarities, I try to keep
them in behaviour and in use as close as possible, so
that users and programmers will not need to learn a lot
of new things if they are already familiar with one of
them. Whenever possible, I also try to design their
tuning forms on the basis of the same elements and
groups; this will be shown a bit later.
A BarChart object consists of the main
Fig.19.2 Bar chart
rectangular plotting area (class Underlayer), plus
one horizontal and one vertical scale. The scale along which the bars go belongs to the Scale class. Another scale,
orthogonal to the first one and showing the textual information, belongs to the TextScale class. You cannot determine
beforehand the types of horizontal and vertical scales of any bar chart as it can be easily turned around at any moment. The
angle of each turn is fixed to 90 degrees but any number of turns in both directions can be made. The main area and each of
the scales can be associated with an arbitrary number of comments. All comments belong to the CommentToRect class.
public class BarChart : GraphicalObject
{
RectCorners m_rectcorners;
Underlayer m_underlayer;
TextScale scaleText;
Scale scaleNum;
List<CommentToRect> m_comments =
new List<CommentToRect> ();
A BarChart object can be moved and resized in exactly
the same way as any Plot object: move it by any inner
point of the main plotting area and resize the main area by its
borders. The scales of a bar chart can be positioned anywhere
in relation to the main plotting area, so there is the same
problem with the resizing in case when a scale is placed over
the border. The same problem is solved in exactly the same
way: you can see a RectCorners field in the
BarChart class, so a bar chart can be resized by any corner
regardless of the positions of its scales.
For clear understanding of the possibilities of the bar chart
tuning, it is better to have two figures not far from each other:
one for the bar chart itself (figure 19.2) and another one of
the tuning form for this particular bar chart (figure 19.3).
This figure shows a nearly default view of the
Form_BarChartParams.cs. You can see that five groups
from this form were already demonstrated in the last example
of the previous chapter where I explained the main ideas of
designing such tuning forms but did not write anything about
the results of tuning. In the case of a real tuning form for bar Fig.19.3 BarChart tuning form
World of Movable Objects 687 (978) Chapter 19 Data visualization
charts, the interaction of elements is organized exactly as it was described in the Form_DesignOfTuningForms.cs, but for
now the more important thing is the reaction on clicking and selection of all these elements as each action in the tuning form
has an immediate effect on the view of the currently modified bar chart.
As I have already mentioned, the tuning forms for the Plot and BarChart classes are designed as close as possible,
both forms include four identical groups which have the same effect on objects under tuning. These groups were already
described in the subsection Tuning of plots, scales, and comments of the chapter Applications for science and engineering; I
will repeat some information about tuning by these elements and a bit about the possibilities of rearranging the view of
these groups.
Borders around the main plotting area can be shown in any color and any available standard style
of lines; the border width is always 1. Border style is selected by clicking one of the line samples.
A small sketch with the four darkened rectangles (figure 19.4) represents four parts of the border
line which can be painted; these parts of the border can be switched ON and OFF independently.
By clicking any rectangle, you switch the flag responsible for drawing the corresponding part of
Fig.19.4 Tuning of
the border between ON and OFF.
borders
Reminder. Scales are painted after the borders, so if the line of a scale is positioned on the border
and this line is drawn, which is a common situation, then this part of the border is invisible.
There are three elements inside the Borders group; the biggest one with the line samples plays the role of a dominant
element and two others are its subordinates. Rectangle with the line samples is resizable. While this rectangle is moved or
resized, two smaller elements keep their relative position to this rectangle. Two small elements can be moved and placed
anywhere in relation to the rectangle with the samples, but if you try to release any
subordinate in such a place that it overlaps the dominant rectangle, then this small element is
automatically relocated slightly outside of it next to the top right corner of the big rectangle.
Vertical and horizontal grids can be shown in any color and any available standard style of
lines. If shown, the width of the lines is always 1; it is possible to switch the grids OFF by
clicking an empty strip at the end of the samples area (figure 19.5). The selection of styles
and colors for two grids is done independently.
Inside each pair of elements for tuning vertical and horizontal grids there is a dominant –
subordinate relation with bigger rectangle being dominant. An attempt to place a button
anywhere overlapping the rectangular area of samples will cause the automatic relocation of Fig.19.5 Tuning of grids
this button slightly outside the dominant rectangle.
The pair of elements inside the Main area group (figure 19.6) allows to set the background color and transparency of the
main plotting area. Change of background color has an immediate effect on the
tuned bar chart, but the reaction on transparency change is slightly different.
When the small slider is moved along its rail, the transparency is determined by
the current position of this slider and several areas inside the tuning form change
their background according to the currently recalculated transparency; these are
dominant rectangular areas inside groups Borders and Grids (figures 19.4 and
19.5). The area of the original plot under tuning will change its background Fig.19.6 Background color and
color only when the slider of the track bar is released. transparency
This group is similar to the previous one in overall design as it has one bigger
graphical element and a small button to change a color parameter, but the logic of this group is slightly different and there is
no dominant – subordinate relation between the elements. Thus, the button can be moved and released anywhere even
overlapping partly or entirely with the area of the track bar. The width of the track bar can be changed by moving its sides;
the Transparency comment is also movable
The group Comments (figure 19.7) was shown several times earlier in the tuning
forms for Plot and Scale classes and it is going to appear in a couple of
more tuning forms further on in this chapter. This group allows to add, modify,
and delete comments. Group consists of two obvious parts. The biggest control in
each part plays the role of a dominant element; small buttons are subordinates.
The organized logic of overlapping for these elements is slightly different from the
logic for groups Borders and Grids. Partial overlapping of dominant and
subordinate controls is allowed in Comments group; only when a small button is
released entirely inside the sensitive area of the dominant control, then this button
is forcedly relocated. Fig.19.7 Group for tuning comments
World of Movable Objects 688 (978) Chapter 19 Data visualization
Now it is time to look at those small parts of the Form_BarChartParams.cs which are not used in
the tuning forms of other plotting classes.
Any BarChart object has exactly two scales of which one is numerical and another is textual. In
the Scales group, it is possible to decide about the visibility of these scales (figure 19.8). The scales
can be also hidden from view via their context menu, but this group in the tuning form is the only Fig.19.8 Scales can
way to reinstall the visibility of scales. be shown or hidden
With the base level set to the value on one of the borders (figure 19.9), you see the rectangular
bars going in one direction only from that border. By changing the base level to something
between the two border values, you can receive the rectangular bars going from that level in both Fig.19.9 Setting the
directions. In some cases such view can be more informative. base level
The last group to discuss is the Colors and filling group (figure 19.10). Two
words in the name of the group reflect the existence of two different parts inside
the group. Usually any group has to unite the elements to modify the related
parameters; two parts of this group changes the non-related parameters though
both deal with colors. The BarChart class and its tuning form were included
into the MoveGraphLibrary.dll years ago and originally two parameters were
regulated in two separate groups. There are several pros and cons in organizing
this tuning in one or two groups and while working on one of the previous Fig.19.10 Group to deal with colors
versions, I decided to do this tuning inside one group. and filling of the area
Each part of this group is represented by a resizable rectangle; the one with the colored samples inside can be resized in
both directions; for another one only its length can be changed. Each rectangle has movable elements inside; to avoid the
mess on the screen, the overlapping of these two rectangles is not allowed; if it happens then the forced relocation is called.
The rectangular plotting area of the BarChart is divided into segments according to the structure of the associated data;
the bar chart from figure 19.2 shows data for five days, so the main area has five segments. Each segment has the same
width. A segment includes several colored bars of equal width and may have an empty space at both ends. The number of
sets is the same for all segments and is also determined by the structure of the associated data; the bar chart from
figure 19.2 has four sets. Inside the Colors and filling group you can see a narrow blue strip with two red spots at the ends
(figure 19.10). The whole element is used to change the filling of the chart main area with the colored bars by regulating
the filling of each segment; for this, the red balls can be moved by a mouse left and right. All the sets (all the colored bars)
have equal width, so it is possible that several pixels at the border of each segment will be not used even if the filling is set
(at least, demanded) from one end to another. Human eye is a perfect instrument which can see even a couple of pixels gap
between colored bars. To avoid this, the width of the whole main area can be changed by its tiny resizing.
A bar chart is represented by a series of the colored rectangular bars; the set of
colored samples allows to add, modify, and delete the colors and to change their
order. This set (figure 19.10) may include more samples than you see in the bar
chart (figure 19.2); only the needed number of colors from the left end of
samples is used for drawing at any moment. The samples can be moved by a
mouse from place to place; in this way the order of colors is changed. It is easy
to include extra color samples into the area of the tuning form and select the best
combination of colored bars in the bar chart by changing the order of samples. Fig.19.11 Menu on samples
While the order of colors is changed by the mouse; all other operations are done
via the context menu (figure 19.11); this menu can be called inside the rectangle which frames the samples.
The view of this menu partly depends on whether it is called directly on one of the colored samples or anywhere else inside
the rectangle. The first and the last positions of menu are always enabled. Commands Modify color and Delete color are
available only if the menu is called exactly on one of the samples; in this case these commands are applied to the pressed
color.
Another menu can be called anywhere inside the form but outside any group. This is a standard menu for all the tuning
forms with two standard commands in it. The tuning form consists of many movable elements; some of these elements are
resizable. With all these possibilities, the form is very flexible for transformations imposed by any user. If you make a lot
of changes and do not like the result at all, you can reinstall the default view of the form by a single command from this
menu. Another command allows to change the font which is used in the form.
Two scales of a BarChart object belong to two different classes. The numeric scale belongs to the Scale class which
was already discussed in the previous chapter. The textual scale belongs to the TextScale class and was not used
anywhere before, so it is time to look at it and its tuning form.
World of Movable Objects 689 (978) Chapter 19 Data visualization
easily switched OFF. This limitation allows to avoid an awkward situation when the ticks are left as the only part of the
scale in view and then their length is set to zero. In this case the scale theoretically exists in view (it is not declared as
hidden), but the area occupied by visible parts is zero, so mover does not sense such object. Thus, the tuning form of a scale
cannot be opened, the parameters of a scale cannot be changed; the scale becomes a ghost without any chances to return
back to life. I am not sure that any program needs such ghosts.
Two inner parts of this unnamed group are organized as Dominant_CommentedControlLTP objects and the whole
group belongs to the ArbitraryGroup class.
Groups in this tuning form belong to two different classes but they behave similarly. Each group can be modified through
its tuning form. If needed, the visibility parameters of one group can be easily spread on other groups; this is done via the
command of menu which can be called on any group.
There is also a sketch to regulate position and lining of all segment texts. Work and behaviour of this sketch is identical to
the one which was described while I wrote about the tuning of numerical horizontal scales (figure 18.18).
There are three check boxes inside the tuning form to regulate the visibility of the scale parts. Those three parts – main line,
ticks, and segment texts – can be shown in different combinations and the only forbidden case is the one with all of them
hidden. I already mentioned that this would be an awkward situation with a scale which theoretically exists but which has
zero area and cannot be sensed by mover. This combination is prohibited and the tuning form does not allow to switch OFF
all three check boxes. If you need to switch OFF the whole scale, it is easily done via the tuning form for the main area
(figure 19.3).
Let us return once more to comments and segment texts. Texts and comments of the textual scale are similar, but these are
elements with different purposes. They behave differently in some situations; the understanding of such differences is
important. Texts are always positioned along the line of a scale; the line can be invisible, but the lining for all segment texts
and their angle (the same for all) is still regulated by the single Sample from the tuning form (figure 19.13). The textual
scale can be placed outside the main plotting area or atop this area. Each text has its own length and they can be lined in
different ways (by a corner, by the middle point, etc.), but their anchor points are always lined in the same way.
Figure 19.14 demonstrates similarities and differences of
segment texts and comments. There are two sets of words
which are shown in the same font and with the same angle.
Each set consists of the same words and only positioning
of one word is different. To make the explanation easier,
these two sets are shown in different colors.
Five words shown in black color are segment texts of the
textual scale. The scale was moved to the upper part of the
plotting area and then the main line and ticks were made
invisible. All segment texts of some scale are always lined
in the same way; as we have the case of horizontal textual
scale, then its segment texts are always on one horizontal Fig.19.14 Comments and texts of the TextScale object
level. The relative sizes of the colored bars inside the bar can look similar, but they behave differently
chart are determined by the data; in this case the highest bar partly covers one of the texts. Certainly, the whole scale can be
moved even higher, so that the bar will not cover the text, but there is also another solution.
You can see green comments which nearly duplicate the original view of segment texts. By putting these two collections of
words next to each other I want to show that visually they are indistinguishable; they use the same font, they are rotated on
the same angle and I could easily set them the same color. One word of the green set is moved out of the line; this can be
done only with the comments because they can be moved individually. Everything comes with a price. The individualism
of comments requires more efforts in their placement and rotation; on the contrary, the easiness of placement and rotation
for all segment texts strips them of any chance to go out of line.
Users have the full control over everything in user-driven application, so there are really interesting things that users can do
here. Data for a bar chart arrives from some outside source; it can be a result of calculations or it can come from a database.
The segment texts for scale in this case were proposed by the author of an application which turned to be me. If for any
reason users do not like these texts, they can turn OFF the scale and substitute it with their own comments. The behaviour
of texts and comments in response to any movement, resizing or rotation of the bar chart is identical. There is no way to
find out that the textual scale was substituted by some comments which were not there originally!
World of Movable Objects 691 (978) Chapter 19 Data visualization
Pie charts
Pie charts were among the first really complex nonrectangular objects to which I tried to apply the ideas of movability. Old
versions of moving and resizing pie charts and rings [11] look odd in comparison with the currently used variants, but the
awkwardness of those old versions was exactly the thing that pushed me to think about different solutions and eventually
resulted in development of the N-node covers. This type of covers allowed to include into normally movable and resizable
a whole variety of non-rectangular objects. Now the pie charts can be moved by any inner point and resized by any border
point.
A PieChart object consists of three main parts:
• Circle divided into sectors.
• Comments associated with the whole circle.
• Texts associated with sectors.
public class PieChart : GraphicalObject
{
PointF ptCenter;
float fRadius;
double [] vals;
List<CommentToCircle> circlecomments = new List<CommentToCircle> ();
List<CommentToCircleSector> m_sectorcomments =
new List<CommentToCircleSector> ();
A PieChart object is initialized by a central point, radius of a circle,
and an array of values. The number of values determines the number of
sectors; the ratio of values determines the angles of sectors as their sum
must be equal to 360 degrees.
Figure 19.15 shows a PieChart object consisting of seven sectors.
Though all texts associated with sectors are positioned in different ways,
it is obvious with which sector each of them is associated. The only
comment associated with the whole pie chart can be seen in the bottom
left corner of this figure.
Cover which makes a circle movable by any inner point and resizable by
any border point was already explained with the
Circle_SimpleCover class (figure 12.1). Such cover consists of
two coaxial circular nodes with a small difference between their radii.
• First node is a bit smaller than the pie chart itself; this node is Fig.19.15 Pie chart
used for moving the pie chart around the screen.
• Second node is slightly bigger than pie chart. Only a narrow ring of this node (its width is 2*delta) is not
blocked by the first node. This part of the node works like a sensitive strip along the pie chart border, so pie chart
can be resized by any border point.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, ptCenter, fRadius - delta, Cursors .SizeAll),
new CoverNode (1, ptCenter, fRadius + delta)};
cover = new Cover (nodes);
cover .SetClearance (false);
}
As for any other complex object, the most important things are individual and related movements of the parts of an object.
Comments associated with the whole circle belong to the CommentToCircle class.
List<CommentToCircle> circlecomments = new List<CommentToCircle> ();
The number of such comments is not limited. All related movements of the CommentToCircle objects when dominant
circle is moved or resized were demonstrated in the Form_Circles_WithComments.cs (figure 12.15) and were discussed
in the chapter Simpler covers. Here are the rules of such movements.
World of Movable Objects 692 (978) Chapter 19 Data visualization
Texts associated with the sectors belong to the CommentToCircleSector class. As with many other classes of
comments, this one is derived from the Text_Rotatable class, so it is a comment which can be moved anywhere
around the screen and rotated without any mentioning in the code. A pie chart has a List of comments associated with
the sectors.
List<CommentToCircleSector> m_sectorcomments =
new List<CommentToCircleSector> ();
It would be not a problem to link any number of texts with each sector, but I couldn’t think out any case when more than
one comment per sector would be needed, so the number of sector texts is equal to the number of sectors. Sector texts have
similar system of rules for synchronous and related movements. The first two rules are exactly the same, but the third one is
different.
3. Sector text rotates synchronously with a circle.
This rule defines only the movement (rotation) of the central point of comment. There is more about changing of
orientation (angle) of the text involved in rotation of the whole pie chart; I will write about it a bit further.
Sector angles are determined by the array of values associated with the pie chart. Each sector has its starting angle and the
angle of a circle (sector angle) which it occupies.
A CommentToCircleSector object has four fields to describe its position; three fields are the same as in the case of a
CommentToCircle object, but the last one is different.
public class CommentToCircleSector : Text_Rotatable
{
PointF ptCenter_Circle; // center of a circle
float fRadius_Circle; // radius of a circle
double coef; // positioning coefficient
double angleFromSectorStart; // angle from the side of a sector
Two of identical parameters describe location and radius of dominant circle; the third parameter is positioning coefficient
which determines the comment position in relation to circle border. Each comment of this class has to be visually
associated with some sector, so the fourth parameter stores the angle shift from the starting side of this sector to comment.
When circle is moved or resized, then reaction of sector text is identical to reaction of the circle comment. Only reaction to
rotation is different and in such case this angle from the sector side allows to keep the same relative position of the text to
sector.
Figure 19.16 demonstrates the Form_PieChartParams.cs which is used for tuning the pie charts. This form consists of
four groups; three of them belong to the ElasticGroup class; the group with the coloured samples belongs to the
Group class which has a different behaviour. It is not a good idea to have in the same form the groups with different
behaviour, but in this case the work with coloured samples required different things from what is the best for tuning all
other parameters.
In all my programs, I prefer to use the ElasticGroup class as the basis for design of forms. In such groups, inner
elements can move independently while the frame is only an additional element of visualization to make the content of a
group more obvious. Such groups have no predetermined idea of its general view; the frame is adjusted to current positions
of all inner elements. This tuning form was designed several years ago. While preparing the new version of this book, I
changed this form but only slightly. I have some ideas on its improvement, but I decided not to make significant changes.
Thus, one group still belongs to the Group class. This class was demonstrated in the
Form_Groups_WithDynamicLayout.cs (figure 15.9) and here is a rare example of using this class in some real
application.
Narrow group without a title includes the samples of colored rectangles. The colors and their order are the same as the
colors of sectors in the pie chart. Different sectors may have the same colors, so in addition there is information inside the
samples about the per cent of the pie chart occupied by the corresponding sector.
World of Movable Objects 693 (978) Chapter 19 Data visualization
Samples can be moved by a mouse to other positions inside the group. If a sample is released at another position, then the
order of colors in the pie chart is changed. It is not the reordering of sectors but only the reordering of colors! The sectors
are defined by the array of values at the moment when the chart is initialized. Each sector gets its angle according to the
associated value from the array. Each sector also gets a comment; the comments can be moved and rotated. Nothing of
these is going to change, when the colored
samples are moved. Sectors are painted
with different colors; only the order of
colors is changed on such movement.
Colors can be changed. Double click the
sample you want to change; this will open
the standard dialog for color selection.
The unnamed narrow group with coloured
samples inside is the only group in this
form on which no menu can be called; on
other groups a small menu can be opened.
One command of this menu allows to fix or
unfix the inner elements of the pressed
group; another command allows to call the
tuning form for the group. Those three
groups belong to the ElasticGroup
class, so the called tuning form is the
standard tuning form for this class; the
Form_ElasticGroupParams.cs was
already discussed in the chapter Groups.
Comments group is the standard group for
Fig.19.16 Tuning form for PieChart objects
tuning the comments associated with one or
another plotting class; the group was already discussed in the previous chapters.
The smallest group, also without title, consists of only two controls. This group allows to change the visualization of
borders in the pie chart: the borders can be either painted or not; if painted, their color can be selected.*
Sector texts group is the biggest in the form. As its title tells, the group allows to tune the texts associated with sectors.
Inside the group, there is nearly the standard but slightly simplified set of controls to deal with the comments. Select any
line with comment; then you can change its color, font, or text in the standard way. But the simplified version does not
allow to delete or add sector texts. These actions are not allowed because they make no sense. Each sector is associated
with some text. It can be shown or hidden (one way to do this is to use the check box in the line with comment) but it
always exists, so you cannot delete it. Each sector is always associated with exactly one text, so there is no sense in adding
more texts.
Comments to a circle and texts for sectors can be rotated individually, but the sector texts are also involved in synchronous
rotation with a pie chart. When a circle is rotated, the centers of all sector texts rotate synchronously with it, but what has to
happen with the angles of all those texts? Texts at figure 19.15 are shown at different angles. How these angles have to
change if you start the pie chart rotation? Do they have to keep the same angles regardless of the turn of a circle? Or do
they have to keep the same angle in relation to their sectors? Does it mean that the turn of a circle for 180 degrees has to
turn the text of each sector for the same 180 degrees? If you turn the pie chart from figure 19.15 for 180 degrees and all
sector texts for the same angle, then all of them will be shown upside down. Do you expect such a result? Different
reactions of the sector texts on rotation of a circle can be organized; two additional check boxes in the group allow to
regulate this process.
If the check box Fix angles on rotation is switched ON, then the angles of all sector texts are not changed during the
rotation of a pie chart and you will be watching a graphical big dipper. If the same check box is switched OFF, then each
text does not change its angle to the “parent” sector and you can see some of the texts upside-down or at very interesting
angles.
*
Instead of ElasticGroup object the same two inner elements can be united into a
Dominant_CommentedControlLTP object as shown only four pages back in the Form_TextScaleParams.cs
(figure 19.13). From my point of view, a frame around two small elements (figure 19.16) is a bit redundant, though the
existance of the frame makes the association between two elements more obvious. Maybe the existance of two variants of
design on two figures not far away from each other will make their comparison easier.
World of Movable Objects 694 (978) Chapter 19 Data visualization
To avoid looking at the texts upside-down, there is an additional “Easy to read” option, but it can be switched ON only
when the angles are not fixed.
I recommend switching ON the tuning form and turning a pie chart around with different combinations of these check
boxes. This will explain all the possibilities much better and in shorter time than reading of the previous sentences.
Each sector is associated with some value. Sector texts can be shown in different ways; I will explain these variants a bit
later but some variations include showing those values or per cents; two buttons in the Sector texts group allow to regulate
the view of these numbers. Programmers know that there are special formats to show the numbers. For users the format
selection must be organized in some obvious way, so this is done in an auxiliary Form_NumbersFormat.cs which was
shown and discussed in the previous chapter (figure 18.22).
There are other possibilities for tuning the pie charts; they are available not through the tuning form but via the context
menus of the applications in which the pie charts are used. I will return to these possibilities a bit later.
Ring sets
Rings and ring sets have a lot of common with the pie charts; I often worked in parallel on these two types of plotting.
Whenever anything new was introduced with one of these types, the same changes had to be done immediately on the other
because they are very much alike. Pie charts have the minimum allowed radius to prevent their accidental disappearance on
squeezing. For the same reason, there is a limit on minimum size of the inner radius of a ring and the minimum allowed
width of the rings. I have already demonstrated an example with coaxial rings (Form_Rings_Coaxial.cs, figure 12.12) and
movements of all parts in the new example will be similar to the old one. There are two main differences between two
examples.
• In the old example with the Rings_Coaxial class, neighbouring rings always stay side by side (the outer
border of one ring is the inner border of the next one). Current example allows spaces between neighbouring rings.
• There were no texts at all in the old example. New rings have a lot of different comments which are organized in
exactly the same way as in pie charts, so there are sector texts and there are comments associated with rings.
Spaces allowed between the rings of the RingSet class cause one situation which requires special explanation. In the
Rings_Coaxial class, there is a single border between neighbouring rings. This border is covered by a node which
provides the movement of this border and thus two rings are changed simultaneously: while one ring widens, it squeezes the
neighbour. In the RingSet class, border of each ring is covered by nodes independently and it is impossible to change
width of two rings simultaneously. All borders are covered by nodes starting from the most inner border and going outside.
Thus, the nodes of the inner neighbour precede the nodes of the next one. If there is no gap between neighbouring rings,
then nodes of the inner ring prevail and such border can be moved only inside. When the gap between two rings appears,
then there is no more nodes overlapping and the outer ring can be squeezed by its inner border.
A RingSet object may have any number of rings, but at the moment
of construction it has only one ring, so it is initialized by a central point,
two radii (inner and outer), and an array of values. The number of values
determines the number of sectors; the ratio of values determines sector
angles as their sum must be equal to 360 degrees. Thus initialized
RingSet object contains a single ring of the RingArea class
(figure 19.17). Later the rings can be added or deleted. The rings are
added only on the outside, but any ring can be deleted. No overlapping of
the rings is allowed, but the gaps between the rings are (figure 19.18).
All rings of the RingSet object are concentric; whenever any ring is
moved forward, all other rings move synchronously to continue being
concentric. Each ring is rotated only individually; there is no
synchronous rotation of all rings.
An object of the RingSet class consists of three main parts:
• A List of rings; every ring is divided into sectors.
• Comments associated with the whole set of rings. Fig.19.17 Any ring set starts with one ring;
then other rings can be added.
• Texts associated with sectors.
Positioning of comments is similar to how it is done in the pie charts, but there are some differences.
World of Movable Objects 695 (978) Chapter 19 Data visualization
Comments associated with the whole set of rings belong to the CommentToRing class. For positioning of such
comments two radiuses of the ring are needed: the inner and the outer. When the RingSet object consists of a single
ring, these two parameters are obviously the parameters of this ring; when there are several rings united into an object, then
the most inner and outer of all the radii are used. CommentToRing class was already discussed in the chapter Simpler
covers; I want to remind the rules for calculation of its positioning coefficient.
• If comment is inside the inner border, then this is a coefficient from the [-1, 0] range with -1 corresponding to
the central point and 0 – to the inner border.
• If comment is between two borders, then coefficient
belongs to the [0, 1] range with 0 corresponding to the
inner border and 1 – to the outer border.
• If comment is outside the outer border, then coefficient is
greater then 1 and is equal to the distance between
comment and the outer border.
When the RingSet object is resized, it tries to keep the
positioning coefficients of all general comments unchanged. In this
way, the comments outside all the rings are kept at the constant
distance from the outer border. The comments inside are moved to
the new positions in such a way that the coefficient calculated with
the new rings sizes is unchanged.
Texts associated with the sectors belong to the
CommentToRingSector class.
1. When the rings are moved forward, all these comments
move synchronously. Fig.19.18 A RingSet object may contain any
number of rings
2. Texts rotate synchronously with their ring.
3. Sector texts react to resizing of their ring exactly in the same way as the general comments to the resizing of the
whole set of rings.
Tuning form for RingSet objects (figure 19.19) is similar to the tuning form for PieChart objects. The only
difference is one extra control inside Sector texts group; this control allows to select the ring for which the set of texts is
currently shown.
Figures 19.17 and 19.18 demonstrate some
interesting examples of sector texts. At
figure 19.18 all texts of the outer ring are
turned on the same angle. Certainly, each
comment can be rotated individually, but
that would be a long and tiresome work if
you have many sectors and want to turn all
their texts on exactly the same angle.
Originally, all sector texts are shown
horizontally (zero angle). You can unfix
the angle on rotation (in the tuning form)
and turn the ring. All texts will turn exactly
on the angle of the turn. What if you want
these angles to be different? What if you
want to turn the ring for 150 degrees
(sectors look better in such a way) and their
texts only for 15 degrees? There must be
some way to do it.
Figure 19.17 shows even stranger view.
All sector texts are positioned along their
radial lines. I hope you did not expect me
to do it manually in order to produce such a
view? Fig.19.19 Tuning form for RingSet objects
World of Movable Objects 696 (978) Chapter 19 Data visualization
Both figures 19.17 and 19.18 were prepared in the Form_PlotsVariety.cs; a lot of things can be done with the objects of
this form not via tuning forms of these objects but via different context menus. All these things are included into
application in accordance with the well known rule that users can do in the user-driven applications whatever they want.
Let us look at the work of this rule in the Form_PlotsVariety.cs.
Fig.19.20 A typical bar chart Fig.19.21 Tuning form for numeric scale
Fig.19.22 Tuning form for textual scale Fig.19.23 Tuning form for the main area
On any of these objects a context menu can be called. Menu on the main plotting area of the bar chart has two submenus of
which I decided to show one (figure 19.24). Menus for scales (figure 19.25) and comments (figure 19.26) are much
simpler.
Menus often include several commands which can be achieved in some other ways. This does not mean that one of these
ways must be excluded; it is not the question of redundancy but the question of providing customary paths for different
users.
The first three commands in the menu on the main area (figure 19.24) open one or another required tuning form; the same
form can be opened by a double click on the main area or the scale. If you need to modify a comment, then there are not
two but three different paths. Comment is modified inside the tuning form of its parent, so there are the same two ways;
plus you can go through the menu of comment itself (figure 19.26). I would say that the last way through the menu of
comment is the most natural and at least the most reliable, as it is impossible to distinguish the belonging of comment by its
view or position. In a lot of cases the parent of comment is obvious from the position of comment but not always. Three
comments on the left side of figure 19.20 more likely belong to the vertical scale than to anything else, but they can also
belong to the main area. The comment at the top of the same figure 19.20 can belong to the main area, but what would you
say if I move the same comment and position it below the scale at the bottom? Would you continue to insist that this
comment belongs to the main area or the nearest scale – the textual one – will look like an obvious “parent”?
World of Movable Objects 699 (978) Chapter 19 Data visualization
Rotation of the bar charts is organized only through the main menu (second group of commands at figure 19.24). The only
unchangeable thing throughout the rotation is the size and position of the main plotting area, but everything else goes
around according to the selected direction of rotation.
Fig.19.24 Menu on the bar chart area with one of its submenus
Fig.19.26 Menu on comment
Hiding and unveiling of comments can be organized in different ways in different situations. The same menu of comment
(figure 19.26) is opened on any comment regardless of its association with the main area or with one of the scales. But by
the command from this menu a comment can be only hidden. There is no way to call a menu on the hidden object, so a
hidden object can be unveiled only through the parental tuning form or through the menu of the parent. In the first case –
via the tuning form –comments can be hidden and unveiled on an individual basis by checking or unchecking each line in
the list of comments (figures 19.22 and 19.23). Commands of menus (figures 19.24 and 19.25) which regulate hiding and
unveiling of comments can be applied only to the whole set of associated comments.
All complex applications can be (and often are) used for grabbing the view of the results from the screen into some kind of
document. The wide variety of tuning of all the screen objects makes it easier to get the screen image exactly in the view
which is needed for documents. For this reason I include into all complex applications different commands for taking the
information from the screen into the Clipboard; submenu from figure 19.24 shows the possibilities. All commands for
taking any part of the screen into the Clipboard are available only via the menus.
Several more interesting commands from the menu on the main area of a bar chart.
Copy bar chart. Do you need a second copy of the data you are analysing now? Possibly NOT, but this command may help
you in one interesting situation. Tuning of parameters for visualization allows to change the view, but it is difficult to
predict whether the changed view will be better or not. If the final result of several changes comes with the realization that
the original view was better, this does not improve the user’s spirit. Maybe the better way is first to copy the whole bar
chart and start changing one of the copies. In this way it is much easier to estimate if the changes are making the view
better or worse. The number of copies is unlimited; the unneeded charts can be deleted at any moment.
Another command allows to save the bar chart into the file, but I will return to this possibility later.
There can be a lot of different plots on the screen; they can stay apart or they can overlap. The whole set of charts configure
a multi level system with each chart occupying its own level. A small submenu not shown at figure 19.24 but available via
the Change plot level command allows to move the charts up or down through the levels. I saw many times that scientists
often changed the order of charts while working with a big number of plots. If such commands are very helpful in analysing
a lot of plots in scientific applications, then maybe the similar commands will be useful in programs for economical and
financial analysis.
Pie charts and rings have less tuning forms than bar charts. There are no scales in these plots, so any object of the
PieChart or RingSet class is associated with exactly one tuning form. But several plots from figure 19.1 make it
obvious that those round plots have more variations in their views than a bar chart. If there are more variants and all these
possibilities have to be tuned somewhere (this is a user-driven application!), then there must be another way of changing the
parameters. One obvious way to do this is through the context menus and the menus for round plots provide all the needed
possibilities of tuning. In order to avoid multiple jumping between different pages, I want to show next to each other the
PieChart object (figure 19.27), its tuning form (figure 19.28), and the menu on the pie chart together with its submenus
World of Movable Objects 700 (978) Chapter 19 Data visualization
(figure 19.29). It is impossible to see all submenus simultaneously in the program, but I think that such a view in the book
gives the best understanding of all the available commands.
Fig.19.27 Pie chart sample Fig.19.28 Tuning form for this pie chart
Some commands in this menu look identical to those that were already shown with the bar charts, but others are definitely
new. One of them is the “Sector texts looking to center”; such command allowed to prepare in two clicks the view of the
ring at figure 19.17. If the Fix angles on rotation in the tuning form is not checked but “Easy to read” angle is checked,
then throughout the rotation of the pie chart its sector texts are always positioned along the radial lines and never turn upside
down.
I have explained earlier that changing the order of colored samples in the tuning form does not change the order of values
but only the order of colors to paint sectors. From comparison of figures 19.27 and 19.28 you can see that values are shown
from the Red sector in the clockwise direction. The “Switch drawing direction” command of menu (figure 19.29) does not
change the order of values and their association with the colors but changes the direction of drawing.
the first submenu. Parameters of any sector text can be used as a sample for all siblings (texts of other sectors); these things
are available through commands of the second submenu.
Sectors are associated with some values. An array of values is used for initialization of a pie chart; the number of sectors is
equal to the number of values in that array. Values cannot be negative; the situation of all zeros is also forbidden; the sum
of the array must be positive. Together all sectors fill the whole circle; the angle of each sector corresponds to the
percentage of its value in the whole sum. Sectors can be also associated with some texts. Comments to sectors can show in
different combinations those texts, values, and percents. Each comment for a sector can be shown in its own way; sector
texts at figure 19.27 demonstrate all seven available variants. The first submenu from figure 19.30 allows to select the
view of the comment. The order of views for the comments in sectors at figure 19.27 is the same as the order of views in
the first submenu at figure 19.30; everything starts from the red sector and goes clockwise. Comment for Red sector shows
only the associated text; comment for Orange sector – only the value, and so on.
underneath, and press the button to substitute the old text with the new.
• Radius Comment can be moved at any moment.
• Angle Comment can be rotated at any moment.
• Text view Change of the view is done through the menu and submenu (figure 19.30).
Suppose that you have really a big number of sectors; figure 19.17 represents such a case for the ring, but the tuning of pie
charts and rings are organized identically. Suppose that you changed the view of one comment and decided that it would be
nice to spread the same view on all the siblings. You do not need to repeat the same procedure again and again; instead you
can use the commands from the second submenu (figure 19.30). When any command of this submenu is selected, the
corresponding parameter from the pressed comment is spread on all the siblings (on all other sector texts).
Such combination of individual and group tuning is very powerful. For example, you set the same color for all sector texts,
but then you saw that one of them is poorly visible on its sector (for example, black text on the blue sector at figure 19.27).
In this case you can change the color of this particular text; in the mentioned pie chart the White comment on Blue looks
much better. If you need to attract the attention to one particular text; you can set the same font to all texts and then enlarge
the needed one.
If you have an idea that users of the complex financial applications are too dull to get and use all these possibilities, you
better ask THEM about it but not decide instead of them. I do not understand why a TV remote control needs anything
except the single red button. I can never understand the need of all other buttons, but I do not demand that all the remote
controls have to contain only one button and nothing else. I have a feeling that other people have much better
understanding of such devices and use all (or bigger part) of all those buttons at one moment or another. The same thing
with the variety of visibility parameters for all the screen objects. Give the choice to users; do not think that you are
cleverer than anyone and that you have to decide for others, what they can understand and what they can do. Give them the
World of Movable Objects 702 (978) Chapter 19 Data visualization
control over everything and an easy way of performing this control. Users will decide (individually!) what they really need
and how they are going to use all those choices.
I am not finished yet with the context menus in the
Form_PlotsVariety.cs; figure 19.31 shows the menu that is opened at
any empty place.
The last command from this menu can be of some interest. If there are
too many plots in the form, you can get rid of them one by one (via their
menus), or you can select the last line in this menu and return to the
default view of the form with only four plots (figure 19.1). Only do not
expect the new pie chart and rings to be exactly the same as on that
figure as their associated values are picked at random at the moment of
initialization. Fig.19.31 Menu at any empty place in the
Form_PlotsVariety.cs
The “Form default view” is a standard command which can be found in
menus of many forms of this application, but in the Form_PlotsVariety.cs it was the reason to add several more commands
into other menus of this form and to organize one situation which is not repeated anywhere else in the whole Demo
program.
In the section DataRefinement application of the previous chapter, I have already mentioned that I do not use writing to the
files anywhere in this Demo program. Too many readers are absolutely sure that whenever the program is writing anything
on their computers, this is definitely a virus and nothing else. My big Demo program has to demonstrate the possibilities of
user-driven applications, but I do not want readers of this book to become nervous; for this reason, saving of information
(objects) into the files is not used anywhere throughout the program. Better to say that up till this moment I could not
demonstrate the saving of objects into files because I purposely avoided such actions. However, the more I worked with the
Form_PlotsVariety.cs, the more I felt the need of the possibility to save and restore objects of this form via the files. It is
not done somewhere behind the curtains, but at least it can be done if user wants.
There are so many chances to add new and modify existing objects in this example, but then you have (or had before) only
two choices: either to keep these objects permanently or to delete them. With only these two choices, I had a big problem
even in preparation of the book with the parallel checking of the program. I prepared some objects for demonstration, I
made some pictures of the views, and I could not use the “Form default view” command without losing those objects.
Now there is a chance to save the objects, to delete them from view, but to have an opportunity to restore them later.
Commands for saving the bar charts and pie charts into file can be seen in menus at figures 19.24 and 19.29; saving of the
RingSet objects is done in exactly the same way. It is a standard procedure where you can select the location and the
name of the file, but the default name includes the type of an object, for example, “PieChart”, and the long number which is
nothing else but the original id of this object. You can use any names of your own; the file includes a short indication of the
object which is stored inside so the restoration is not going to produce an elephant instead of monkey, even if you change
the name.
private void Click_miPiechartSave (object sender, EventArgs e)
{
… …
fs = new FileStream (binFile, FileMode .Create, …);
bw = new BinaryWriter (fs);
bw .Write ("PieChart");
elems [iElement] .PieChart .IntoFile (bw);
… …
I have mentioned before that each object has a pair of methods for saving / restoring via the binary files; this code uses the
PieChart.IntoFile() method. The “Read plot from file” command (figure 19.31) opens the standard dialog to
select the file for reading. When the file is selected, then its content is first checked for the quick identification of the object
type (class); after it the appropriate method of this class is called. This piece of code shows the restoration of a pie chart;
there are similar pieces for bar charts and rings.
private void Click_miReadPlotFromFile (object sender, EventArgs e)
{
OpenFileDialog dlg = new OpenFileDialog ();
…
fs = new FileStream (filename, FileMode .Open, FileAccess .Read);
br = new BinaryReader (fs);
string str = br .ReadString ();
World of Movable Objects 703 (978) Chapter 19 Data visualization
… …
else if (str == "PieChart")
{
PieChart piechart = PieChart .FromFile (this, br);
if (piechart != null)
{
piechart.Move (ptMouse_Up.X - Convert.ToInt32 (piechart.Center.X),
ptMouse_Up.Y - Convert.ToInt32 (piechart.Center.Y));
elems .Insert (0, new SingleElement (piechart));
bRenew = true;
}
}
… …
RenewMover ();
… …
Two important remarks on restoration of plots from files.
1. Plot is stored in a file with the whole set of its parameters; this set includes the unique identification numbers for
all its parts. The FromFile() method of any class restores an object but gives it the new id; it happens at all
levels, so all the identification numbers of the restored complex object and its inner parts will be different. Thus,
you can copy objects from file without producing a mess in the identification system.
2. Plots are restored with the same coordinates that they had on the moment of saving into files. It is possible that the
original object was moved around the screen between the moments of storing and restoration; it is also possible
that it was not moved at all. In the last case you get the copy exactly at the same place and there will be no
indication of two different objects at the same spot. To avoid this, the restored object is positioned at the place,
where the menu with the command for restoration was called. The top left corner of the menu is used for
positioning of the restored plot; for a bar chart it will be the top left corner of the main area; for pie charts and rings
it will be the central point. It is possible that the original object was moved and the menu was called in such a way
that the position of the restored object will put them on top of one another, but the probability of such a thing is
negligibly small.
It would be an unusual thing to have in my big application some form in which users will be allowed to do everything with
the existing objects but not to add objects of their own. The first three commands of menu at empty places (figure 19.31)
close this possible flaw in design and allow to add new objects to the form; the description of these possibilities is in the
following section.
If a bar has only one individual movement, then it is enough to have one node in its cover. Previous version of the
SingleBar class had two nodes in its cover. One of them was redundant and I had to give additional explanation why I
preferred to keep that redundant and unused node in the cover. In the current version of cover, there is only one node, so the
explanation must be different.
I have mentioned an earlier example with single side resizable rectangles. Those objects of the
Rectangle_ResizeByOneSide class are also movable, so their cover has to include two nodes: one for moving and
another for resizing. That class has a classical cover design which provides the object movement by any inner point.
SingleBar was designed as a copy of that class and had the same cover. But objects of the SingleBar class are not
movable individually, so the behaviour of the bigger node had to be changed. It would be enough to change the behaviour
of this node to Behaviour.Nonmoveable, but I want the whole big rectangle of the bar chart to be movable by any
inner point and this is achieved if the behaviour parameter of each single bar is turned into Behaviour.Transparent.
That was a solution in the previous version of the SingleBar class, but that was an obvious remnant of the older
examples.
The change of nodes from movable to transparent and back again is very useful when object movability is regulated by user.
If this movability is never changed and an object is never moved individually, then there is no need to cover such object by
any node. It is useful to cover even an unmovable object by some node if there is a menu which has to be called on this
object. In the Form_DefineNewBarChart.cs, the color of any bar is regulated not through the menu command. No menu
is called on bars and there is no reason to keep a redundant node in the cover of each bar. New version of the SingleBar
class does not have such node in its cover, so it has a single node over the movable side of the bar. There is no rotation of
bar chart in this auxiliary form; bar chart is always shown with its bars growing from the bottom line up (figure 19.32), so
only the top of each bar is movable.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, new RectangleF (rcMax .Left,
cyTop - halfsense, rcMax .Width, 2 * halfsense),
Cursors .SizeNS));
}
Code of the SingleBar.MoveNode() method is very simple as there is a single node which can be moved only up
and down inside the rectangular area.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
float cyNew = cyTop + dy;
if (rcMax .Top <= cyNew && cyNew <= rcMax .Bottom)
{
cyTop = cyNew;
bRet = true;
}
}
return (bRet);
}
Movement of a bar is only forced by movement of the whole bar chart, but such movement will require the use of the
SingleBar.Move() method.
public override void Move (int dx, int dy)
{
rcMax .X += dx;
rcMax .Y += dy;
cyTop += dy;
}
Now we can move to the BarChart_ForDefinition class. Big rectangular area has to be resized by any corner and
side, so a standard cover for resizable rectangles can be used.
World of Movable Objects 706 (978) Chapter 19 Data visualization
public override void DefineCover ()
{
cover = new Cover (m_underlayer .Area, Resizing .Any);
}
Bar chart is a complex object; when its main part is moved, all related elements have to be informed about this movement.
public override void Move (int dx, int dy)
{
m_underlayer .Move (dx, dy);
InformRelatedElements ();
}
The full list of related elements which must be informed includes the text scale and all bars inside the main area.
private void InformRelatedElements ()
{
scaleText .RelatedRect = m_underlayer .Area;
for (int i = 0; i < bars .Count; i++)
{
bars [i] .MaxRectangle = BarMaxRect (i);
}
}
Text scale gets the new rectangle of the main area and uses its positioning coefficient to adjust its position. Single bars have
no such coefficients and each bar gets a unique rectangle of its own maximum allowed area. Calculations are done by the
BarMaxRect() method and the result depends on several parameters: number of sets, number of segments, and the
filling of the main bar chart rectangle by segments.
The MoveNode() method includes many cases because all sides and corners can be moved. There are some limitations
on minimum allowed area, but if the proposed change is allowed, then the area is changed and all related elements are
informed through the same InformRelatedElements() method.
In a standard way for all complex objects, the BarChart_ForDefinition objects are registered by the
IntoMover() method.
new public void IntoMover (Mover mv, int iPos)
{
if (iPos < 0 || mv .Count < iPos)
{
return;
}
scaleText .IntoMover (mv, iPos);
mv .Insert (iPos, this);
foreach (SingleBar bar in bars)
{
mv .Insert (iPos, bar);
}
}
The unusual here is only the order of two elements: the main area is registered ahead of the text scale. There is a very
simple explanation to this unusual order. The Form_DefineNewBarChart.cs is an auxiliary one with only minimal tuning
of some elements. The scale is positioned below the bar chart (figure 19.32) and is declared unmovable.
public BarChart_ForDefinition (Form form, RectangleF rc, int sets,
int segments, double [] nums,
List<Color> colors, string [] strs)
{
… …
scaleText = new TextScale (form, strs, rc, LineDir .Hor, 1.0);
scaleText .Movable = false;
}
This is a very unusual thing in my programs to turn usually movable element into unmovable, but this scale is used only to
inform about the selected segment texts. If at some moment I decide to make this scale movable, I will delete the extra line
World of Movable Objects 707 (978) Chapter 19 Data visualization
from constructor and I will also change the IntoMover() method. In the current version, the unusual decision about
movability causes the unusual order in the mover queue.
Bars of any bar chart are associated with some values. For better visual estimation, a real bar chart is usually shown
together with numeric scale. There is no such scale in the Form_DefineNewBarChart.cs and each bar is associated with
some value between zero at the bottom and the upper value which is specified in the special control inside the group. When
the new value is entered into this TextBox (do not forget to press Enter!), each bar automatically gets the new associated
value depending on position of its upper side. When the upper line of any bar is moved, there is a small temporary area
sliding synchronously with the mouse cursor and showing the currently changing value of this bar.
The rules of user-driven applications have to be applied not only to the main
form of any program but at all the levels. From the Form_Main.cs of the
Demo application we went to the Form_PlotsVariety.cs; from there we
called an auxiliary Form_DefineNewBarChart.cs. Even here all the
declared rules must be applied.
The Form_DefineNewBarChart.cs has a group of the ElasticGroup Fig.19.33 Menu on the group in the
class, so there is a menu to call the standard tuning of this group Form_DefineNewBarChart.cs
(figure 19.33). Other commands of the same menu allow to fix / unfix the
inner elements of the group and give some other options. Another menu (figure 19.34) can be called anywhere outside the
group.
Menu on the group (figure 19.33) contains a command to reinstall the default view
of this group; the second menu (figure 19.34) includes similar command to recover
the default view of the whole form. Similar commands are used throughout all the
complex groups and forms of all the demonstrated examples. In the user-driven Fig.19.34 Menu outside the group
applications, it is possible to change the views in so many ways... Some in the
modifications can improve the default view (there are users who are much better Form_DefineNewBarChart.cs
designers than the authors of applications); some changes may have an opposite
effect. With these menu commands, users receive an opportunity to recover the initial view of the form or of its part;
whether users would like to do it or not, it is up to them.
The possibility to change font becomes one of the most important menu commands and one of the most valuable for a lot of
users. The increasing percentage of the middle aged and older users simply cannot use the applications designed
predominantly by the younger developers because of the trend in eyesight change of which younger people rarely think at
all. You can find the commands for changing fonts in nearly every corner of all of my examples, so there is no surprise in
having such a command for this form (figure 19.34). But the last command in the same menu can be a bit surprising. I
continue to insist, and I have mentioned it several times, that the font change must be only the font change and not a bit
more. A designer is not allowed to adjust the view according to the new size of the font. This is against the law of user-
driven applications: developers are not allowed to interpret users’ commands but have only to provide the reaction on
whatever was asked.
If somebody asks another font, then the font is changed. It can partly corrupt the whole view, but there is an opportunity to
move and resize all objects, so the view can be improved by some moving / resizing. At the same time there can be a
request for the same general view but with the changed font. This can be (and must be) a separate command; that is what
the last command in the menu from figure 19.34 allows to do. I did not spread the use of such commands throughout the
Demo application, but it is implemented in the Form_DefineNewBarChart.cs and a couple of similar forms.
Let us close the small form to add the new bar chart and return back to the Form_PlotsVariety.cs in which other types of
plots can be also added. First three commands from menu at figure 19.31 open the ways to three different forms to define
new bar charts, pie charts, and rings. Pie charts and rings are close in design and the forms to define new objects of these
two types are also similar (figures 19.35a and 19.35b). Each form includes a group of controls to set the main parameters
and a graphical element to make these changes more obvious. These graphical elements are simplified versions of pie
charts and rings; simplification is mostly in the way of moving sector texts.
In the real PieCharts and RingSet objects, sector texts can be moved freely around the screen. In these auxiliary
forms, sector texts can be moved only along the central lines of sectors and all texts move synchronously. I can remind that
movable and resizable circles and rings with also movable partitions and sector texts moving synchronously along the
central sector lines were demonstrated in the Form_CirclesRingsSpecialComments.cs (figure 12.17). Two new auxiliary
forms use circles and rings of those two classes – Circle_PartitionsAndComments and
Ring_PartitionsAndComments – which were already discussed with that old example.
World of Movable Objects 708 (978) Chapter 19 Data visualization
In the form for new bar charts, a small temporary rectangle is used to show the needed information throughout the resizing
of each bar. There are two reasons to use such small rectangle: at each moment only one value is changed, but the total
number of values can be big enough as it is the multiplication of sets and segments. (If you prepare some bar chart with
information for four companies throughout a whole year, then there will be 48 bars; see figure 19.20.)
Fig.19.35a The form to define new pie chart Fig.19.35b The form to define new ring
For pie charts and rings, the number of sectors is usually not so big, while moving of any partition changes two values
simultaneously. For both reasons I decided to include into the group one more control and information in two lines of this
ListView is changed when the partition is moved.
Fig.19.36 Any number of different plots can appear in the Form_PlotsVariety.cs. All these elements can be moved,
rotated, resized, tuned, hidden, and unveiled again.
World of Movable Objects 709 (978) Chapter 19 Data visualization
there was not a word about the big form itself in which an unlimited number of complex objects and elements can be
moved, rotated, hidden, unveiled, tuned, resized, and so on. Is it really a complex application without anything special in it?
Surprisingly, it is so! When all these types of plots (and there can be much more different classes involved) are designed
according to the rules of user-driven applications, then all these classes can be used in many places without any tricks or
special procedures for one or another case.
At the very beginning of the book I explained that three mouse events – MouseDown, MouseMove, and MouseUp – are
used in all possible situations. These events have to use one Mover method each; nothing else is needed. If there are
complex objects for which some tuning forms are called, then the MouseDoubleClick event is also used and relies on
one of the already used Mover methods. Just to be sure that nothing special is missed in discussion of the
Form_PlotsVariety.cs, let us make a small tour of this form. It is worth doing it because this is one of the complex forms
and a lot of real applications are designed in exactly the same way. Just to remind you about the field of discussion, here is
a view of this form with an increased number of plots (figure 19.36). Do not try to find any sense behind the shown
objects; I simply added several plots and moved them around the screen; several of the smaller elements were also moved
around when they were unlucky to be found under the mouse cursor. As we liked to say many years ago while playing in
the yard: “Do not blame me if you could not hide”. If anything happened to be under the cursor, why not to move it or
rotate?
Three classes of plots can be seen in the Form_PlotsVariety.cs: BarChart, PieChart, and RingSet. Instead of
mentioning all three classes in many parts of the code, I combined them under the SingleElement class.
public class SingleElement
{
MedleyElem elemtype;
PieChart m_piechart = null;
RingSet m_ringset = null;
BarChart m_barchart = null;
Objects of the SingleElement class can be shown in one of those three views but they are organized into a single
List of elements.
public partial class Form_PlotsVariety : Form
{
List<SingleElement> elems = new List<SingleElement> ();
Whenever any new plot is organized, it is included into this List.
private void DefaultView ()
{
… …
RingSet rs = new RingSet (this, new Point (ClientRectangle .Width / 4,
ClientRectangle .Height / 4), 80, 30,
new double [] { 1, 2, 4, 8, 16 });
elems .Add (new SingleElement (rs));
… …
PieChart chart = new PieChart (this, new Point (ClientRectangle .Width / 5,
ClientRectangle .Height * 2 / 3),
Math .Max (ClientRectangle .Width,
ClientRectangle .Height) / 4, fRandVal);
elems .Insert (0, new SingleElement (chart));
… …
BarChart barchart = new BarChart (this, rc, fVals,
Side .S, Auxi_Common .strDays, TextsDrawingDirection .LTtoRB,
Side .E, 1000.0, 0.0, GridOrigin .ByStep, 200.0);
elems .Insert (0, new SingleElement (barchart));
… …
}
Any number of plots can be organized inside the form; in addition there is an informational panel of the
InfoOnRequest class (info) and a small button (btnHelp) to return this information back into view if it was
previously hidden. All plots and these two objects are included into mover queue. According to the rule, button must be the
first element in queue as it is the only control in the form; I also decided to show information atop all the plots.
World of Movable Objects 710 (978) Chapter 19 Data visualization
private void RenewMover ()
{
mover .Clear ();
for (int i = elems .Count - 1; i >= 0; i--)
{
elems [i] .IntoMover (mover, 0);
}
info .IntoMover (mover, 0);
mover .Insert (0, scHelp);
if (bAfterInit)
{
Invalidate ();
}
}
This method uses the SingleElement.IntoMover() method, but two pages back I showed that the
SingleElement class is not derived from the GraphicalObject class. It means that no SingleElement
object can be registered directly in the mover queue. Instead, BarChart, PieChart, or RingSet element is
registered depending on the type of particular object.
public void IntoMover (Mover mover, int iPos)
{
switch (elemtype)
{
case MedleyElem .PieChart:
m_piechart .IntoMover (mover, iPos);
break;
case MedleyElem .RingSet:
m_ringset .IntoMover (mover, iPos);
break;
case MedleyElem .BarChart:
m_barchart .IntoMover (mover, iPos);
break;
}
}
This also means that if anything is analysed inside the methods for three mouse events and mover is asked about the class of
the pressed, moved, or released object, then these three classes are checked.
The Form_PlotsVariety.cs is one of those very rare examples (there are only four of them) in the Demo application in
which the movement of the form by any inner point is used. This technique adds several lines of code into the
OnMouseDown() method. Such movement of a form was explained in the chapter User-driven applications.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, bShowAngle))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is PieChart && mover .CaughtNode == 1)
{
(grobj as PieChart) .StartResizing (e .Location);
}
}
else
{
if (e .Button == MouseButtons .Left)
{
bFormInMove = true;
sizeMouseShift =
new Size (PointToScreen (ptMouse_Down) .X - Location .X,
PointToScreen (ptMouse_Down) .Y - Location .Y);
World of Movable Objects 711 (978) Chapter 19 Data visualization
}
}
ContextMenuStrip = null;
}
There are a lot of elements in this form which can be involved in rotations, but there is no trace of anything like
StartRotation() method for any of the classes. Yet, without using such method, there would be an obvious
twitching of an object at the starting moment of rotation. There is no twitching of objects in the Form_PlotsVariety.cs, so
somewhere the appropriate method for each involved class is used correctly when an object is caught by the right button.
The place where the needed StartRotation() method is called is the Mover class itself. Exactly for three
involved classes – Text_Rotatable (all comments are derived from this class), PieChart, and RingSet – mover has
all the needed information and starts their rotation correctly. For these three classes everything is automated, but if you
decide to add objects of other classes into this example and those objects need rotation, then you will have to add several
lines as it was described in the chapter Rotation.
The OnMouseMove() method is also extremely simple. In a standard situation there would be a single call to the
Mover.Move() method, but moving of the form by any inner point adds several lines.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
else
{
if (bFormInMove)
{
Location = PointToScreen (e .Location) - sizeMouseShift;
}
}
}
The OnMouseUp() method is not more complicated. I want to emphasize that standard moving and resizing of any
object in the form is done by mover automatically, so only special situations must be described. The most common sign of
such special situation is the release of the mouse button at the same place where it was pressed; then the reaction depends on
whether it was left or right button.
• When some graphical object is released by the left button at the same place where it was pressed, then it often
means the command to put this object on top of others.
• If the right button was pressed and released without movement, then it is the call of context menu.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if ((grobj is PieChart || grobj is RingSet || grobj is BarChart) &&
fDist <= 3)
{
Identification (grobj);
PopupElement (iElement);
}
}
else if (e .Button == MouseButtons .Right && fDist <= 3)
{
MenuSelection (grobj);
}
World of Movable Objects 712 (978) Chapter 19 Data visualization
}
else
{
if (e .Button == MouseButtons .Right && fDist <= 3)
{
ContextMenuStrip = menuOnEmpty;
}
}
bFormInMove = false;
}
Regardless of whether it is the change of objects order or menu calling, the identification of the pressed object is required.
Method of identification can be a lengthy one if there are objects of several classes. If there are complex objects, and this is
just the case in the Form_PlotsVariety.cs, then the whole chain of related elements must be identified, but the process itself
is straightforward as any element has its own id and any “child” keeps the id of its “parent”. The identification process was
already discussed in the previous chapters.
Menu selection is easy as it depends on the class of the pressed object which is provided by mover. Some changes in the
opened menu depend on the results of the previous identification. Usually each class of objects has its own context menu.
In the Form_PlotsVariety.cs, there are nine menus for objects of different classes plus one menu for empty places. Part of
these menus can be seen at figures 19.24 – 19.26 and 19.29 – 19.31. Some commands from these menus were discussed;
others are obvious and do not need any explanation.
Objects with a lot of visibility parameters need special tuning forms. Plots have a lot of such parameters, so objects used in
the Form_PlotsVariety.cs are tuned through the six different auxiliary forms. All tuning forms can be called through the
commands of context menus but they can be also opened with a left double click on objects. Three tuning forms are called
directly from inside the OnMouseDoubleClick() method; calls to three others are hidden inside the
ElementMainParametersDialog() method.
private void OnMouseDoubleClick (object sender, MouseEventArgs e)
{
if (mover .Release () && e .Button == MouseButtons .Left)
{
Point ptScreen = PointToScreen (e .Location);
GraphicalObject grobj = mover .ReleasedSource;
if (grobj is InfoOnRequest)
{
info .ParametersDialog (this, RenewMover, ParamsChanged, ptScreen);
}
else
{
Identification (grobj);
if (iElement >= 0)
{
BarChart chart;
if (grobj is BarChart || grobj is PieChart || grobj is RingSet)
{
ElementMainParametersDialog (iElement, ptScreen);
}
else if (grobj is Scale)
{
chart = elems [iElement] .BarChart;
chart .NumScale .ParametersDialog (this, RenewMover,
ParamsChanged, null, chart .Title, ptScreen);
}
else if (grobj is TextScale)
{
chart = elems [iElement] .BarChart;
chart .TextScale .ParametersDialog (this, RenewMover,
ParamsChanged, null, chart .Title, ptScreen);
}
}
World of Movable Objects 713 (978) Chapter 19 Data visualization
}
}
}
The saving of all objects and all the parameters of visualization are paired with the procedures of restoration, so users are
not going to lose a bit of their efforts in changing the view of this form. Everything is organized according to the rules of
user-driven applications.
World of Movable Objects 714 (978) Chapter 20 Some interesting possibilities
Visualization of covers
Visualization of covers is both an interesting and a strange title, because I think that covers must not be visualized at all and
the good design must avoid any type of cover visualization. Looking at the examples of the accompanying Demo
application, you can see that the visualization of covers can be found only in those of them which are used for explanation
of the cover design. Examples from chapters 2 – 12 explain the covers for graphical objects while chapters 13 – 15 are
about covers for controls and groups. Outside those chapters and examples you are not going to see the button which
switches the visualization of covers ON and OFF. After the design of covers is explained, you do not see the covers any
more, because their visualization is not needed any more. You do not see covers anywhere in the real applications from the
second part of this book; you do not need to show covers in any real applications you will write yourself. However, if at
one moment or another there will be a request for cover visualization, you have to know how to organize it. That is why I
decided to explain it in this section, but while reading it, do not forget that all the questions of visualization are important
only when there is such a request. The normal situation is when the covers are not shown.
If you check the codes of examples in which the covers can be shown, you will find that mostly it is done in two ways. The
first one can be seen, for example, in the Form_SolitaryControls.cs (figure 13.1) where the covers for all objects in the
form are shown by a single method Mover.DrawCovers().
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (bShowCovers)
{
mover .DrawCovers (grfx);
}
… …
}
Another solution can be even more popular. Ii is demonstrated, for example, in the
Form_RegularPolygon_CoverVariants.cs (figure 6.1) where each cover is painted by a separate call to a similar method
of the MovableObject class.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
for (int i = mover .Count - 1; i >= 0; i--)
{
… …
if (bShowCovers)
{
mover [i] .DrawCover (grfx);
}
}
}
The Mover.DrawCovers() method is used in the situations when you do not care too much about the order of drawing
the covers and draw them on top of all objects. The case of MovableObject.DrawCover() is much better for
situations when you want to draw each object with its own cover, so that you will see exactly the picture which mover has
to analyse while picking up the objects for moving and resizing. What is common for both cases, that not the movable /
resizable objects are responsible for painting their own covers but the mover. It is really a strange thing, because the good
programming practice (at least, how it is understood today) requires the Draw() method to be included into the list of
methods of any visualized class so that any object of such class can draw itself. Cover belongs to an object and all the
demonstrated classes have their own Draw() methods, but there is not a single DrawCover() method in any of these
World of Movable Objects 715 (978) Chapter 20 Some interesting possibilities
classes. If the whole process of cover visualization is organized in such an unusual way, then it was definitely done so on
purpose.
Cover belongs to an object but is needed and used only for the purpose of moving / resizing an object. It is an additional
layer of parameters for object and it never interferes with other parameters. Cover is defined by other parameters (for
example, by sizes) and strongly depends on them, but it is a one way relation: cover never rules other parameters. The main
thing was already mentioned: cover is an additional, auxiliary parameter. Though cover is designed at the moment when an
object is initialized, you may look at the situation with its cover in such a way: it is activated when an object is registered
with a mover. You can look through all the examples and you will not find a single object with any kind of indication of
either it is registered with any mover or not. The movability of objects can be regulated and changed (for example, via
menu commands; this was demonstrated in several examples), but no object knows if it is movable / resizable at the moment
or not!
Any object derived from the GraphicalObject class can be used as unmovable or movable; this status can be easily
changed while the application is running, for example, by including an object into the mover queue or excluding an object
from this queue. Mover is the only one who knows the list of movable objects at any moment; because of this, mover is
responsible for drawing of their covers.
Since the invention of the whole algorithm, only mover was responsible for drawing the covers. As the real applications
never use the cover drawing, I had no problems at all with this situation. However, while preparing more and more
complex examples for one of the articles, I ran again and again into situations when I needed to draw the covers not for all
objects from the mover queue but only for several of them. It looks like the use of the MovableObject.DrawCover()
method can easily solve this problem, but in some situations it is not so. There are many cases when the number and the
order of movable objects in the mover queue can easily change; it is difficult to calculate the order of the needed object in
the mover queue and then it is not obvious, which object of the MoveableObject class must be asked to draw its cover.
As a result, I added the GraphicalObject.DrawCover() method. Every movable / resizable graphical object is
derived from the GraphicalObject class, so now any object can draw its own cover. There are even two different
GraphicalObject.DrawCover() methods: one of them uses the appropriate color from mover, another receives the
color for drawing as a parameter..
Several examples in the Demo application demonstrate the use of the GraphicalObject.DrawCover() method; one
of them is the Form_CircleSector_OneMovableSide.cs (figure 8.9).
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
for (int i = sectors .Count - 1; i >= 0; i--)
{
sectors [i] .Draw (grfx);
if (bShowCovers)
{
sectors [i] .DrawCover (grfx, mover);
}
}
}
The most interesting questions about the cover visualization are:
1. How to change the parameters of visualization?
2. How to visualize only part of the cover?
3. How to select only some of the covers for viewing?
Four different classes can be involved in the visualization of covers: Mover, MovableObject, Cover, and
CoverNode.
Covers consist of the CoverNode objects, so the visualization of cover is in reality the visualization of all its nodes or
only part of them. The shape of any node can be a circle, a strip, or a polygon; any of them has inner area and a border, so
the choices for drawing nodes are limited:
• To fill or not the inner area of the node with some color.
• To draw or not the border of the node.
The default colors for this drawing are:
World of Movable Objects 716 (978) Chapter 20 Some interesting possibilities
If you need to draw all covers, there is the Mover.DrawCover() method. Nodes of each cover are drawn in the
reverse order of nodes; the covers are also drawn in reverse order (from the end of mover queue to the beginning), so this is
the exact situation with the covers and nodes as mover sees it and analyses.
This chapter is about the cover visualization, but there are situations when you do not want the cover for one or another
class to be visualized even on request. It is not about hiding your ideas from anyone else; it is only for convenience. For
example, I have explained in details the cover design for the Plot class. Those objects can be moved by any inner point
and resized by any border point; these are the only things users need to know to work with them, so there is absolutely no
sense in visualizing their covers. If for any reason you want to exclude the visualization of cover for any class, add a short
ShowCover property to this class.
public override sealed bool ShowCover
{
get { return (false); }
}
In such way the ShowCover property is used in the majority of classes included into the library. However, it is not used
for the Text_Rotatable class, but that was done only for the purpose of explanation; all the comments in the
MoveGraphLibrary.dll which are derived from the Text_Rotatable class have such property.
World of Movable Objects 718 (978) Chapter 20 Some interesting possibilities
means that at such moment their Enabled property is set to true. All mouse clicks inside an active control is handled
by the operating system and never come to any mover. But when a control is disabled, then such mouse click is ignored by
the operating system; this click goes to something which is watching carefully all the mouse activities and is waiting for
such an easy prey. Mover never sleeps!
Let us look once more at the figure of the Form_SolitaryControls.cs with the covers of all elements in view. This figure
was already shown in the chapter Individual controls, but I want to repeat it here (figure 20.2). Red frame around each
control shows the boundaries of a big rectangular node which is mostly blocked by the control itself, but this statement is
correct only for active controls. When the control is active, which is the standard situation, then only the narrow strips
between the boundaries of this control and the borders of the big node (red frame) are used for moving the whole control.
This frame could be organized as a set of four narrow rectangles along the borders; for movement of an active control, there
is no difference at all whether you have four nodes over this narrow area, or one big node to cover the whole area inside the
frame. The second variant requires less code lines and is easier, so it was implemented this way.
But for the case of disabled control there is a huge difference. Any mouse press on the disabled control is ignored by the
standard program, but if you have a mover and all those covers, then such press is going directly into that rectangular node.
This is maintained by mover as any other press of any other node, so it is a standard command to move a control.
Nothing else is changed at all when the Enabled property of a control is switched between true and false. The
resizing of particular control is not affected at all. If a control is an element inside some group, then this group behaves in
the same ordinary way regardless of the enabled / disabled status of any control inside.
File: Form_TrickWithControls.cs
Menu position: Miscellaneous – Trick with controls
Fig.20.3 Objects in the Form_TrickWithControls.cs demonstrate different situations when a switch of controls into
disabled mode can be useful.
While designing the Form_TrickWithControls.cs (figure 20.3), I purposely constructed it only of the parts which were
already used in other forms of the Demo application and were already discussed in some of the previous examples:
• Groups on the left and in the middle differ by the background color and have slightly different behaviour of their
inner elements. These groups are the copies of the group from the Form_Main.cs.
• The group on the right was already used in the Form_PersonalData.cs.
• A set of solitary controls at the bottom was used in the Form_SolitaryControls.cs.
World of Movable Objects 720 (978) Chapter 20 Some interesting possibilities
When I was thinking about the design of the Form_Main.cs, I had to take into consideration several things.
• That was going to be the first form of a big Demo application. When anyone starts to read the book and in parallel
looks at this application, then at the beginning this person is not familiar with any details of the proposed
mechanism of moving and resizing. I wanted to demonstrate from the very first moment that moving and resizing
could be applied to any objects. It is not only about graphical objects of arbitrary forms; this ANY must include
controls which play significant role in many applications.
• While looking for the first time at the form which is occupied with different objects and knowing only that
everything is movable, it is natural to expect that everything is movable in the same way. If the majority of the
shown elements are moved by any inner point, then it is natural to expect that all objects are moved by any inner
point. All graphical objects are moved by any inner point, so something has to be done with the controls in view.
• The easiest way would be to substitute those controls with their graphical pictures. It would work perfectly, but
this would be a fraud. Though the whole area of programming is full of frauds, you cannot start an introduction of
the new ideas with a fraud. At least, if you are a serious researcher.
• There was one more thing about those controls in the Form_Main.cs. They were put on the screen to show some
basic information about the book. This information is to be observed but not to be changed by users of the Demo
program. Those controls can be either of the Label or TextBox class; in the last case the controls have to be
at least read-only.
Group with the read-only TextBox objects is demonstrated in the middle of figure 20.3. This group shows the needed
information without giving a chance to change it, but inner controls of this group can be moved only by their borders. It
differs from the moving of graphical objects in the Form_Main.cs, so the use of read-only controls does not help at all.
The solution comes with declaring of those TextBox objects disabled. Information is shown but cannot be edited by
users. At the same time these controls can be moved around the screen by any inner point, so their movement is identical to
all graphical objects.
The switch to disabled controls allowed to fulfil all the requirements for showing the information and moving the controls.
At the same time the switch to disabled controls does not require any changes in the code. The best indication of this
amazing thing is in the fact that the design of two similar groups in the Form_TrickWithControls.cs goes along the same
lines and is based on using the same methods.
ElasticGroup groupDisabled, groupReadOnly, groupAddress;
public Form_TrickWithControls ()
{
… …
ctrlsDisabled = new Control [] { textTitle_0, textPages_0, textFigures_0,
textExamples_0, textWritten_0, textUpdated_0 };
ctrlsReadOnly = new Control [] { textTitle_1, textPages_1, textFigures_1,
textExamples_1, textWritten_1, textUpdated_1 };
… …
private void OnLoad (object sender, EventArgs e)
{
… …
groupDisabled = ConstructGroup (ctrlsDisabled, "Disabled controls",
Color .LightCyan, 0.3);
groupReadOnly = ConstructGroup (ctrlsReadOnly, "Read-only controls",
Color .LightYellow, 0.2);
… …
private ElasticGroup ConstructGroup (Control [] ctrls, string title,
Color clrBack, double fTransparency)
{
string [] strsGroup = { "Title", "Pages", "Figures", "Examples",
"Finished", "Updated" };
CommentedControl [] ccs = new CommentedControl [ctrls .Length];
for (int i = 0; i < ccs .Length; i++)
{
ccs [i] =
new CommentedControl (this, ctrls [i], Side .W, strsGroup [i]);
World of Movable Objects 721 (978) Chapter 20 Some interesting possibilities
}
LineCommentsOnLeft (ccs);
ElasticGroup group = new ElasticGroup (this, ccs, title);
group .BackColor = clrBack;
group .BackColorTransparency = fTransparency;
return (group);
}
If two groups have the same set of inner elements and these two groups are constructed in the same way, then those inner
elements must behave identically. Yet, as you can find by a simple checking in a moment, the text boxes inside the
groupDisabled can be moved around the screen by any inner point, while the same text boxes in the
groupReadOnly can be moved only by their borders which is a standard behaviour for numerous controls in a lot of
other examples. To organize an unusual way of moving controls inside the groupDisabled, all these controls have
their Enabled property set to false. I have done it in Visual Studio while constructing the
Form_TrickWithControls.cs, but this kind of action by a designer of the program (me!) leads to a very interesting
question.
You remember that rules of user-driven applications demand that users must receive the full control over applications. If a
switch of controls between being enabled and disabled can be helpful in using an application, then the control over such
change of status must be also passed to users. What happens with a working program in which user starts to change the
controls behaviour? Definitely nothing extraordinary; this can be easily done without any negative side effects. To
demonstrate such possibility, one more group is used in the Form_TrickWithControls.cs.
The group from figure 20.4 was already used in the Form_PersonalData.cs. In
the current example this group gets new title which changes when status of inner
controls is changed between enabled and disabled. A context menu can be called
at any empty place inside this group; this menu contains a single line with a
command to make the controls of this group either enabled or disabled. To use or
not to use this possibility is up to the users.
The group is used to enter the user’s address; the possibilities of working with this
group were already discussed in the chapter Groups. Even in the standard
situation when all the text boxes inside this group are enabled, these elements can
be easily moved around by their frames. I do not have any problems with moving
such text boxes around the screen or resizing them, but… I never insist that Fig.20.4 Group with changeable
whatever is a good thing for me must be good enough for anyone else. movement of controls
I see here the direct analogue with the moving of plotting areas in the complex
scientific applications. While designing such applications and checking their behaviour, I concentrate only on the
correctness and easiness of all movements. If I do not need to move any screen object, I do not touch it and it stays where it
was left before. I have no problems with accidental movement of any objects because I think only about movements and
nothing else. For users of those scientific applications the situation is absolutely different. Users are concentrated on their
research work, on the analysis of data which is shown inside those plots. Scientists do not pay attention to the tiny details of
moving those plots. They often need to move them, so they press the mouse to move a plot, but occasionally something else
can be moved. Such unexpected action distracts users’ attention from what they are involved in – the analysis of data – that
is why scientists demanded an easy way to fix / unfix those plots. From my point of view, the accidental movement looks
very awkward; from the scientists’ point of view it is very embarrassing. Those applications are not for me to demonstrate
some level of skills in their development. The applications are for scientists to do their work, so those instruments must be
designed according to the scientists’ requirements.
Exactly the same thing happens with a switch between enabled and disabled controls. As a developer involved in checking
of the program, I do not have any problems with moving and resizing of the text boxes; I do it only when I want to do it.
But whoever is going to work with such a group in a real program, will be concentrated only on the data he is working with.
If any special accuracy is needed for moving elements inside the group, then it can be considered ridiculous by the users of
such application. If some users might prefer to switch controls into disabled mode for easier moving, then why not to give
them such a possibility? I am not going to tell anyone that he has to do it only one way or another. Try both and choose
whatever is the best for you.
There is nothing extraordinary in organizing this possibility for users. The group is declared and initialized in the standard
way.
World of Movable Objects 722 (978) Chapter 20 Some interesting possibilities
public Form_TrickWithControls ()
{
… …
ctrlsAddress = new Control [] { textStreet, textTown, textProvince,
textCountry, textZipCode };
… …
private void OnLoad (object sender, EventArgs e)
{
… …
CommentedControl [] ccsAddress = new CommentedControl [] {
new CommentedControl (this, textStreet, Resizing .WE, Side .W, "Street"),
new CommentedControl (this, textTown, Resizing .WE, Side .W, "Town"),
new CommentedControl (this, textProvince, Resizing .WE, Side .W,
"Province"),
new CommentedControl (this, textCountry, Resizing.WE, Side.W, "Country"),
new CommentedControl (this, textZipCode, Resizing.WE, Side .W,
"Zip code") };
LineCommentsOnLeft (ccsAddress);
groupAddress = new ElasticGroup (this, ccsAddress, "Use menu inside");
groupAddress .BackColor = Color .LightSeaGreen;
groupAddress .BackColorTransparency = 0.5;
… …
The only additional thing is the use of context menu which can be called on the groupAddress. There is only one
command in this menu; this command allows to change the Enabled property for all controls inside this group.
private void Click_miSwitchEnabledInside (object sender, EventArgs e)
{
bool bNew = !ctrlsAddress [0] .Enabled;
foreach (Control ctrl in ctrlsAddress)
{
ctrl .Enabled = bNew;
}
string strTitle = ctrlsAddress [0] .Enabled ? "Controls are enabled"
: "Controls are disabled";
groupAddress .Title = strTitle;
Invalidate ();
}
I have already mentioned that the change of the Enabled property for any control does not affect anything else except
the possibility of moving this control by any inner point. A control can be resized in different ways; this is determined by
the values of its MinimumSize and MaximumSize properties. Resizing of a control is going in the same way
regardless of whether a control is enabled or disabled at the moment.
A control can be deliberately made unmovable by passing the appropriate parameter on the initialization of a
SolitaryControl object.
scResizeNotMove = new SolitaryControl (listviewResizeNotMove, false);
scResizeNS = new SolitaryControl (btnResizeNS);
A set of SolitaryControl objects is organized inside the
Form_TrickWithControls.cs to demonstrate that switching of the Enabled
property can be used not only inside some group but with any individual control. The
change of this property is organized via another context menu which can be called at Fig.20.5 Menu at empty places
any empty place of the form (figure 20.5). Text of the first command in this menu
depends on the current value of the Enabled property of solitary controls and informs in what way it can be changed.
World of Movable Objects 723 (978) Chapter 20 Some interesting possibilities
cases of resizing for controls. It looks like the number of nodes in the cover and their positions depend on the needed type
of resizing, but this is only an illusion. In reality there are always nine nodes and their positions are the same as it is shown
with the right control for which any resizing is allowed. Whenever some of the nodes are not needed (for the cases of lesser
resizing), then those unneeded nodes are squeezed to zero size but they still exist and there are always nine of them.
Such nine nodes exist in the covers of both CommentedControl and CommentedControlLTP classes, but the
objects of these classes are moved differently, so they have different additions.
• The CommentedControl class has nothing else in its cover except what is shown at figure 20.6. The
comment used in this class is an element of the CommentToRect class. Two parts – control and comment – are
elements of the complex object; as in any complex object, its parts are registered in the mover queue individually
but one after another.
• The CommentedControlLTP class is not a complex object. In this class the comment is not an individual
element but the fixed part of an object. The comment area is covered by an additional node which is included into
the cover of the CommentedControlLTP class.
As you see, covers of those two classes are organized differently, so how am I going to design a new class which is going to
work one way or another depending on the user’s wish? The situation with the proposed ControlWithComments
class is even more interesting: there can be an arbitrary number of comments at any moment and their behaviour can be
changed individually, so some of comments can behave like those in the CommentedControl class and move
individually while others can behave like parts of the CommentedControlLTP object and the movement of such
comment has to move the entire object – control and all the comments – synchronously. Have you any ideas about the
design of such class?
The ControlWithComments class includes comments that can be moved individually. This is a classical example of
the complex object, so all those comments must be registered in the mover queue individually. If a comment is declared
fixed, then an attempt to move it will result in synchronous movement of the whole object which consists of the “parent”
control and all its associated comments.
One solution would be to change the number of nodes in the cover of the whole object every time when the status of any
comment is changed between fixed and unfixed. It is possible, but I think it is a bad solution as it will also require some
complicated mechanism of identification for additional nodes under the fixed comments.
I think that there is a better solution: to have additional nodes under all comments. The number of these nodes does not
change when the status of any comment is changed. This number of nodes is changed only when comments are added or
deleted, so at each particular moment the number of nodes is known.
public override void DefineCover ()
{
rcFrame = FrameArea;
Cover coverTMP = new Cover (m_control .Left, m_control .Right - 1,
m_control .Top, m_control .Bottom - 1,
resize, Movable, nodesOut, m_framewidth);
CoverNode [] nodes = new CoverNode [9 + m_comments .Count];
for (int i = 0; i < 9; i++)
{
nodes [i] = coverTMP [i];
}
for (int i = 0; i < m_comments .Count; i++)
{
nodes [9 + i] = new CoverNode (9 + i, m_comments [i] .Frame);
}
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviour (Behaviour .Transparent);
}
}
As you see, the first nine nodes in the cover are those standard nodes that are shown at figure 20.6; after it there are
additional nodes for the areas of comments. The cover for each comment (of the CommentToRect class) consists of a
single rectangular node; the area of this node is exactly the same as the area of the corresponding node belonging to the
World of Movable Objects 725 (978) Chapter 20 Some interesting possibilities
ControlWithComments object. I want to emphasize that the area of each comment is covered by two nodes of equal
size. Further work with these nodes depends on their behaviour and their order in the mover queue. The behaviour will be
discussed a bit later. The order is determined by the ControlWithComments.IntoMover() method which shows
that comments are registered in the mover queue ahead of the “parent” object.
new public void IntoMover (Mover mover, int iPos)
{
if (iPos < 0 || mover .Count < iPos)
{
return;
}
if (Visible && VisibleAsMember)
{
mover .Insert (iPos, this);
for (int i = 0; i < m_comments .Count; i++)
{
mover .Insert (iPos, m_comments [i]);
}
}
}
When comment is unfixed, then it has a normal node which is detected and caught by mover; after it the pressed comment
can be moved or rotated. The equal node belonging to the main element is positioned in the mover queue behind the
comment node, so that node of the main element is unreachable for mover if comment is unfixed.
When comment is fixed, then its cover is changed in such a way that it becomes transparent for mover. When the mouse is
pressed on such comment, then mover looks through this transparent cover and catches the next available node from the
next object in the mover queue. In this queue, the comments to any ControlWithComments object are followed by
their “parent” object which has the nodes duplicating the areas of all its comments. Thus, when the fixed comment is
pressed, then mover goes through it, because it is transparent for mover, but cannot miss a node which belongs to the main
element – ControlWithComments object. In this way instead of the fixed comment its ControlWithComments
“parent” is caught. And this caught node is responsible for synchronous movement of control and all its comments.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
int newWidth, newHeight;
switch (resize)
{
case Resizing .Any:
switch (iNode)
{
… …
case 8:
default:
Move (dx, dy);
break;
}
break;
… …
For such synchronous movement of the whole object, the order of the pressed fixed comment and the order of the caught
node underneath do not matter at all. But for other things, for example, for changing the parameters of the pressed
comment, the identification of the comment is very important. Each comment has a node underneath and the order of these
nodes is the same as the order of comments. Because first nine nodes in the cover are always around control, then the
numbering of those extra nodes starts from nine, so the identification of the comment above the pressed node is very easy.
The work of the ControlWithComments objects is demonstrated in the Form_ControlsAndComments.cs
(figure 20.7). As you see, there are big and small controls; there are controls with several comments, with one comment, or
without comments, so all possible cases are considered.
World of Movable Objects 726 (978) Chapter 20 Some interesting possibilities
First, let us look at the constructors of the ControlWithComments class. Previously designed similar classes have a
lot of constructors: CommentedControl has around 50 constructors and CommentedControlLTP has 13
constructors. These big numbers are the result of wide use of those classes and my attempts to minimize the number of
parameters for many special situations. The ControlWithComments class has only six constructors, but you are free
to add other variants
which you need. These
six constructors represent
three cases with
absolutely different sets
of parameters, while
inside each pair the only
difference is in a
parameter describing the
possibility for control
resizing. The resizing
possibility is
automatically calculated
from MinimumSize
and MaximumSize
properties of the control;
an additional
Resizing parameter in
constructor allows to
decrease the resizing Fig.20.7 The Form_ControlsAndComments.cs
possibility from the
automatically calculated.
The minimum number of parameters can be seen in the constructor of an object without comments; in the
Form_ControlsAndComments.cs this is the case of the ListView.
private void OnLoad (object sender, EventArgs e)
{
… …
cwcListview = new ControlWithComments (this, listView1);
… …
Initial position of comment can be defined in two different ways. The first one – by declaring Side and
SideAlignment parameters – is demonstrated in the case of a CheckBox control.
cwcCheckbox = new ControlWithComments (this, checkBox1, Resizing .None,
Side .E, SideAlignment .Center, 2,
"On the right side", Font, 20, Color .Blue);
Another way is to declare two coefficients of initial positioning; this is demonstrated in the case of a Button.
cwcButton = new ControlWithComments (this, button1, 0.2, -12,
"Button_Magenta", Font, 0, Color .Magenta);
There are no constructors for an object with several comments; for such case you have to initialize an object with one
comment and then to add other comments. The button used in the Form_ControlAndComments.cs has four comments;
the first one is described in the constructor; others are added by using the
ControlWithComments.InsertComment() method.
cwcButton .InsertComment (0, Side .E, SideAlignment.Top, 5,
"Button_Blue", Font, 0, Color .Blue);
cwcButton .InsertComment (0, Side .E, SideAlignment .Center, 5,
"Button_Red", Font, 0, Color .Red);
cwcButton .InsertComment (0, Side .E, SideAlignment .Bottom, 5,
"Button_Green", Font, -15, Color .Green);
In similar way panel with three comments is organized, but in this case comments are added with the
ControlWithComments.AddComment() method and positions of additional comments are described by coefficients.
World of Movable Objects 727 (978) Chapter 20 Some interesting possibilities
cwcPanel = new ControlWithComments (this, panel1, 0.5, -20, "At the top",
Font, 0, Color .Blue);
cwcPanel .AddComment (-40, 0.4, "Left", Font, 70, Color .Red);
cwcPanel .AddComment (0.8, 30, "Bottom", Font, -10, Color .Magenta);
Originally any comment is defined as movable; its status can be changed later.
This Form_ControlAndComments.cs is designed in such a way that the number of controls is defined by developer and
cannot be changed by user while the number of comments associated with any control can be easily changed by users and
all comments can be tuned. Tuning is allowed in three different ways:
• Synchronously for all comments in the form
• Synchronously for all comments of a control
• Individually for the pressed comment
Any comment has four different parameters that can be changed:
• Font
• Color
• Status of movability (fixed or unfixed)
• Text
Text can be changed only individually; three other parameters can be
changed at any level of generality. Because we have three levels of
changing the parameters of comments, there are going to be three different
context menus. In reality, there are four, but I’ll explain such increase in
the number of menus a bit later.
The synchronous tuning of all comments in the form can be started via the
menu called at any empty place – menuOnEmpty (figure 20.8); in Fig.20.8 Menu at empty places
addition to the comments tuning, this menu includes a couple of general
commands for the form. There is nothing special in the commands of this
menu; similar commands were used in other forms, for example, in the
Form_PersonalData.cs.
Another menu – menuOnControl (figure 20.9) – can be called at any
control of this form except the small check box. I simply can’t imagine a
situation when a CheckBox needs more than one comment; I also can’t
imagine the use of a CheckBox without any comment at all. For these
reasons, the first and the last command of menuOnControl cannot be
applied to the check box, while all other commands can be found in another
menu which can be called directly on comment. Nearly all commands of
Fig.20.9 Menu on controls
this menu on controls work in the same way as the commands of the
previous menu. The only new command is used for adding the comments;
this command calls an additional Form_NewComment.cs (figure 20.10)
which was already used in the example Form_Rectangles_WithComments.
Two menus with commands for tuning all comments in the form (figure 20.8)
or only all comments of the pressed control (figure 20.9) include similar
commands to change the font, color, and movability status of the selected
group of comments. When these commands are applied to any set of
comments, then they are applied to each member of the set. Thus, it looks like
for individual tuning of any comment the same commands must be applied
and without any problems at all. Well, yes, the same commands are applied,
but there is a catch which makes the individual tuning much more interesting. Fig.20.10 Form_NewComment.cs
Any comment can be fixed or unfixed at any moment. Such change of status has no visual effect but it is very important for
organizing all actions with comments. Regardless of whether a comment is fixed or unfixed, the same parameters can be
tuned. As was shown many times in the previous examples of this book, when you have in the form different classes of
movable objects, then more often than not each class is associated with its own context menu, and for such situation mover
is very helpful. You press the right button on the needed object; mover analyses the class of the pressed object and calls an
World of Movable Objects 728 (978) Chapter 20 Some interesting possibilities
appropriate menu. Thus, when the movable comment is pressed, this object of the CommentToRect class is easily
identified and only several extra lines are needed to identify its “parent” – the ControlWithComments object.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iWasobject, iWasNode;
ptMouse_Up = e .Location;
double dist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release (out iWasobject, out iWasNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
}
else if (e .Button == MouseButtons .Right)
{
if (grobj is CommentToRect)
{
cmntPressed = grobj as CommentToRect;
bool bRelocated = RelocatedAfterDisappearance (cmntPressed);
if (!bRelocated && dist <= 3)
{
long idParent = cmntPressed .ParentID;
if (idParent == cwcPanel .ID)
{
cwcParent = cwcPanel;
}
else if (idParent == cwcButton .ID)
{
cwcParent = cwcButton;
}
else if (idParent == cwcCheckbox .ID)
{
cwcParent = cwcCheckbox;
}
else if (idParent == cwcListview .ID)
{
cwcParent = cwcListview;
}
iCmntPressed = cwcParent.CommentOrderByID (cmntPressed.ID);
ContextMenuStrip = menuOnComment;
}
}
… …
Any fixed comment is transparent for mover and cannot be identified in the same easy way when it is pressed by a mouse.
When the mouse is pressed on the fixed (transparent) comment, then mover goes through this comment and finds
underneath its “parent” ControlWithComments object. At the same time mover knows the number of the node under
this comment; by this number the comment is identified.
else if (grobj is ControlWithComments && iWasNode >= 9 && dist <=3)
{
iCmntPressed = iWasNode - 9;
ContextMenuStrip = menuOnCmntPlace;
}
Do not be surprised that there is no “parent” detection inside the OnMouseUp() method when the fixed comment is
pressed. In this case, not the CommentToRect object is pressed (this comment is transparent for mover!) but the
ControlWithComments object which was already identified in the OnMouseDown() method.
World of Movable Objects 729 (978) Chapter 20 Some interesting possibilities
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button, bShowAngle))
{
GraphicalObject grobj = mover .CaughtSource;
if (grobj is ControlWithComments)
{
cwcPressed = grobj as ControlWithComments;
cwcParent = cwcPressed;
}
}
ContextMenuStrip = null;
}
As you can see from the OnMouseUp() method, for unfixed and fixed comments two
different menus are used – menuOnComment and menuOnCmntPlace – though
they have identical view (figure 20.11). There are more command lines in this menu than
I would include into similar menu in any real application; I did it on purpose because I Fig.20.11 Menu on comment
want to demonstrate two different approaches.
When you have some object with one, two, or three tunable parameters, then the simplest way is to organize a small menu
with a corresponding number of command lines: one line per each parameter. It is especially good when you need to
change font and color as for each of them a standard dialogue is called. In the majority of applications, the comments to the
controls on the screen are so essential that those texts are determined by developer and never changed later. Thus, for such
comments only their font, color, and status of movability can be changed (fixed or movable); for such situation I would use
three separate command lines in context menu.
If users are allowed to change the comment text, then one more command is
needed for editing of the text. Such editing can be organized just at the place of
the existing text, but I do not like it because the text to be changed disappears
under the control in which you have to type the new text. Instead, I prefer to
have a small additional form for changing all the parameters (figure 20.12).
Thus, the menu at figure 20.11 includes commands for both variants of tuning
the comments; in real application you would leave only one or another; if you
prefer to use for tuning the Form_ModifyComment.cs, then the first three Fig.20.12 Form_ModifyComment.cs
commands in that menu are redundant.
Auxiliary forms Form_NewComment.cs (figure 20.10) and Form_ModifyComment.cs (figure 20.12) are similar in view
and possibilities. The only difference between them is an additional check box in the second form, but this addition
demands the use of different classes in design. The Form_NewComment.cs contains only controls, so they are organized
into a DominantControl object. Because Form_ModifyComment.cs contains a CommentedControlLTP
object, the elements of this form are organized into a Dominant_SolitaryControl object.
looks awkward. To avoid such situations, no control is allowed to settle inside the area of another control and this is
guaranteed by enforced relocation.
Rules of moving controls in the Form_SetOfControls.cs can be formulated in three short statements.
• There is no synchronous movement of controls.
• Each control can be moved individually.
• There are situations when enforced relocation is applied; in such case the new control position is determined by
positions of other objects.
Combination of these rules looks like a boundary situation between individual controls and groups; to distinguish this case
from groups, I call it the set of controls.
At the bottom of figure 20.13 you see information of the InfoOnRequest class; as usual, this information is paired
with a small button which can be seen in the top left corner. All other elements in the form compose a single object of the
SetOfCommentedControl class.
SetOfCommentedControls socc;
This object incorporates nearly all elements in the form, but this class is not mentioned anywhere in three methods for the
main mouse events. Explanation of this fact is simple: the SetOfCommentedControl class is not derived from the
GraphicalObject class, so this object cannot be recognized by mover.
public class SetOfCommentedControls
{
List<CommentedControl_General> ccgs = new List<CommentedControl_General> ();
A SetOfCommentedControl object can include one or more CommentedControl_General elements. One
way to organize a SetOfCommentedControl object is to initialize it with a single
CommentedControl_General object and then to add others.
ctrls = new Control [] { listFirst, listSecond, btnCombine, textNewName,
comboLabelsPolicy, panel1 };
private void OnLoad (object sender, EventArgs e)
{
… …
socc .AddCommentedControl (new CommentedControl_General (this, ctrls [1]));
socc .AddCommentedControl (new CommentedControl_General (this, ctrls [2]));
socc .AddCommentedControl (new CommentedControl_General (this, ctrls [3],
Side .W, SideAlignment .Center, 4, "New name",
Font, 0, ForeColor));
socc .AddCommentedControl (new CommentedControl_General (this, ctrls [4],
Side .W, SideAlignment .Center, 4,
"If there are two labels\nfor the same photo", Font, 0, ForeColor));
socc .AddCommentedControl (new CommentedControl_General (this, ctrls [5],
Side .N, SideAlignment .Left, 2, "For pictures",
Font, 0, ForeColor));
… …
An attempt to register SetOfCommentedControl object in the mover queue by using Mover.Add() or
Mover.Insert() methods will cause mistakes; instead the SetOfCommentedControls.IntoMover() method
must be used. This method registers all the movable elements, so only the CommentedControl_General and
CommentToRect classes are mentioned in the code of three mouse events.
Two special situations have to be considered when any object is released.
The first is the release of comment under the associated control. Such comment is forcedly relocated outside the control
area; this situation was discussed in details in some of the previous examples when it happened in CommentedControl
objects. In the current example, I decided to make the code easier and do not even identify the
CommentedControl_General object in which it happens. If comment is relocated for this reason, then new covers
for all elements of the set are calculated and the mover queue is renewed. All these things are done by the
RelocatedAfterDisappearance() method.
World of Movable Objects 732 (978) Chapter 20 Some interesting possibilities
Second special case is checked when any CommentedControl_General object is released. If control is released
inside the area of another control, then it is relocated. It may happen that after relocation it will overlap with some other
control from the set, so another relocation is required. As any control can be associated with an arbitrary number of
comments, then it is a relocation not of the control only, but of the CommentedControl_General object. The need
for relocation is checked and is done, if needed, by the SetOfCommentedControls.CheckOverlapping()
method. Method is called inside the loop until it is needed.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iWasobject, iWasNode;
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release (out iWasobject, out iWasNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is CommentToRect)
{
RelocatedAfterDisappearance (grobj as CommentToRect);
}
else if (grobj is CommentedControl_General)
{
ccgPressed .CheckCommentsLocation ();
bool bOverlap = false;
do
{
bOverlap = socc .CheckOverlapping (
grobj as CommentedControl_General);
} while (bOverlap == true);
RenewMover ();
}
… …
The Form_SetOfControls.cs example has the same set of menus as the previous one. There are menus on controls, on
comments, and at empty places. Menus are nearly the same as in the previous example; the main difference is with menu
on comments which has only two commands. In the previous example, I purposely added three redundant commands which
allowed individual change of comment parameters. In the current example, there is only a command to open auxiliary form
in which any combination of the same parameters can be changed.
World of Movable Objects 733 (978) Chapter 21 Medley of examples
Medley of examples
This chapter has no general idea which unites the included examples. It is really a medley of small programs which are far
away from each other, but when they are shown one after another, it is one of the best demonstrations of the novelties that
movability of objects brings into different applications.
• The first example deals with a standard Calculator which is well known to everyone. What happens when
movability is added to the objects of such simple and well known application? Is this addition of movability a
single step without consequences or maybe there are other things which spring out of this new feature?
• Village is a small exercise in drawing.
• Book by chapters is another attempt to give quick and direct access to all numerous examples of the huge Demo
application. The previous example of such type was designed as a block diagram which duplicated the main menu
with all its submenus. Here is another type of block diagram which copies the structure of this book with its
chapters.
• A whole set of examples illustrates the design of family tree without restrictions. This is one of the few examples
which I demonstrate not only as a part of Demo application but also as a stand alone program. Since the moment
when I wrote about this example in an article [19], it attracted attention of programmers all round the world.
• The last example of this book includes graphical objects of different shapes. These objects use the best resizing
technique demonstrated in different chapters of the book; there are also some new things which were not used in
other examples.
File: Form_OldCalculator.cs
Menu position: Applications – Facelift of the old calculator
The Calculator application, if there are no aberrations of my memory, appeared with the very first version of the Windows
system, so regardless of when you personally became familiar with this system, the Calculator was already there. Similar
World of Movable Objects 734 (978) Chapter 21 Medley of examples
applications exist for all other operating systems. The main thing is that everyone is familiar with this application and can
make his own opinion on the usefulness of turning it into a user-driven application without waiting for some authorized
opinion.
I am not a priest to bestow my blessing by a simple hand waving; I have to
do a bit of the code writing for it. I have no access to the original
Calculator, so at figure 21.1 is the view of the Form_OldCalculator.cs.
The default view of this form copies the simpler view of the original
calculator from Microsoft.
I use the standard Calculator from Microsoft three or four times a year,
when I need to divide two big numbers and too lazy at the moment to do it
with a pen on a sheet of paper. Maybe it is a result of my rare use of this
program, but on those rare occasions when I start a standard Calculator, I
have a problem with finding in it the needed numbers because they are
definitely not at the places where I would put them as a designer. It is a big
problem if you use this Calculator as often as I do. On each occasion you
can spend several extra seconds for such a search, get the needed result, and
then forget about the search problems for the next several months.
Fig.21.1 Copy of the standard Calculator
Then I decided to develop my own version of the Calculator which allows to with very limited functionality
eliminate those search problems. The design is quickly done in Visual but with the movability of all
Studio; after it some coding must be done. elements
1. First, mover must be declared.
Mover mover;
2. Then mover must be initialized. There is upper menu in this form, so I use that mover constructor which has an
additional parameter to restrict objects movement at the top. The original view of the form includes some controls
which I never need in real Calculator, for example, I never use the memory operations. The best way to get rid of
the unneeded controls is to move them across the border, so the appropriate clipping must be used. The level
Clipping.Safe allows to move elements across the right and bottom sides of the form.
public Form_OldCalculator ()
{
InitializeComponent ();
mover = new Mover (this, SystemInformation .MenuHeight);
mover .Clipping = Clipping .Safe;
… …
3. The Form_OldCalculator.cs contains exclusively controls. Registering of all elements in the mover queue is
simple.
public Form_OldCalculator ()
{
… …
foreach (Control control in Controls)
{
mover .Add (control);
}
… …
4. The whole process of moving objects is organized with three mouse events. All three methods have the simplest
possible view.
private void OnMouseDown (object sender, MouseEventArgs e)
{
mover .Catch (e .Location, e .Button);
}
private void OnMouseUp (object sender, MouseEventArgs e)
{
mover .Release ();
}
World of Movable Objects 735 (978) Chapter 21 Medley of examples
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Update ();
}
}
With all these things implemented, I can move the
unneeded controls over the border and change the places
of other elements; in this way different view can be
organized (figure 21.2). This application allows to
perform only the basic arithmetical operations, but this is
the only thing that I need from it. Certainly, I do not
want to rearrange the view of this Calculator every time
I am going to use it, so there are two simple methods for
storing the whole view in the Registry and restoring it
back from the Registry for the next use. Fig.21.2 The changed view of the same Calculator
private void SaveIntoRegistry ()
{
… …
int nControls = Controls .Count;
string [] strs = new string [nControls * 2 + 2];
for (int i = 0; i < nControls; i++)
{
strs [i * 2] = Controls [i] .Left .ToString ();
strs [i * 2 + 1] = Controls [i] .Top .ToString ();
}
strs [nControls * 2] = ClientSize .Width .ToString ();
strs [nControls * 2 + 1] = ClientSize .Height .ToString ();
regkey .SetValue (nameLocation, strs, RegistryValueKind .MultiString);
… …
private void RestoreFromRegistry ()
{
… …
string [] strSizes = (string []) regkey .GetValue (nameLocation);
if (strSizes != null && strSizes .Length == (Controls .Count * 2 + 2))
{
int nControls = Controls .Count;
if (strSizes .Length == (Controls .Count * 2 + 2))
{
for (int i = 0; i < nControls; i++)
{
Controls [i].Location = Auxi_Convert.ToPoint (strSizes, i * 2);
}
ClientSize = Auxi_Convert .ToSize (strSizes, nControls * 2);
}
}
… …
The above shown code restores the positions of controls but not their sizes if they were changed. If you want controls to be
resizable, it can be easily organized by setting the needed ranges through the MinimumSize and MaximumSize
properties of those controls. To save and restore the results of resizing for all controls, all elements must be transformed
into SolitaryControl objects. Some changes might look like the code samples below. I included these variants into
the Form_OldCalculator.cs in the form of comments.
1. A List of elements is declared.
List<SolitaryControl> sc_S = new List<SolitaryControl> ();
World of Movable Objects 736 (978) Chapter 21 Medley of examples
If wrapping into SolitaryControl objects is done by mover, then it is up to you to provide the level of restoration
which you want. You may not do anything at all, or save only the positions of all controls, or save both the positions and
sizes. Depending on your decision, the code of the SaveIntoRegistry() and RestoreFromRegistry()
methods must be changed. You (and users) get whatever you provide. But do not forget one thing. Even if you do not do
anything, all controls are resizable and users will change their sizes. If some sizes are changed and then all those changes
are lost, users will not like this situation.
If you organize SolitaryControl objects yourself, then it is very easy to save and restore these objects. When
SolitaryControl objects are saved, then all their parameters are saved and can be restored. It is not only about the
sizes; you can add more things, for example, change font and / or color; everything will be saved and restored. But this will
lead us much farther from the original Calculator. To explore these things, let us take the next example.
File: Form_Calculator.cs
Menu position: Applications – Calculator (on controls)
Previous example with a copy of original Calculator was prepared years ago in order to demonstrate the addition of
movability to a widely known program. After that example was ready, I had no intention of doing anything else with the
Calculator but one day during a conversation
with one of the colleagues I heard a complaint:
having a poor vision, the colleague had big
troubles with the standard Calculator program,
because even the combined efforts of several
specialists did not reveal any way to increase
the font used by this application. Another
mystery from Microsoft. So I sat down and
developed a Calculator which that colleague
could use. As it was not for me, then the
program had to work as a normal Calculator
with operations and functions. Certainly,
changing of the font was not the only thing
that I allowed to do in this program; it was
designed according to all the rules of user-
Fig.21.3 The default view of the Form_Calculator.cs
driven applications. Figure 21.3 demonstrates
the default view of the Form_Calculator.cs.
There are still only controls in this form. There are no buttons for memory operations, but there are some buttons for often
used functions. And there are two controls (not one!) to show the input values and results; I decided that it will be more
informative for the work. All controls of the form are divided into several groups; three groups play special role and in
default view the buttons of these groups have their texts shown in different colors.
• Buttons with digits have their texts shown in Blue. From the point of mathematics, dot and sign are definitely not
numbers, but they are used as parts of notation while writing numbers, so in this program the two corresponding
buttons are included into this group.
Control [] controlDigit = new Control [] { button_0, button_1, button_2,
button_3, button_4, button_5, button_6, button_7,
button_8, button_9, btnSign, btnDot };
• Operations are shown in Red, but only six small buttons positioned in one line belong to this group. Other three
Red buttons in the upper part are not the members of this group.
Control [] controlOperation = new Control [] { btnPlus, btnMinus,
btnMultiply, btnDivide, btnDegree, btnEqual };
• Buttons for eight functions used in this program have their texts shown in Violet. I could easily add more
functions, but the person for whom I designed this program said that this set of functions would be enough. You
can add more functions into this program, only carefully check the code and make changes in all the places where
this group is used.
Control [] controlFunction = new Control [] { btnLn, btnLog, btnExp,
btnSin, btnCos, btnTg, btnInverse, btnSqrt };
For the purpose of better showing the results, there are two different information controls; one of them is used to show the
current number, another shows the whole expression.
World of Movable Objects 738 (978) Chapter 21 Medley of examples
Calculator is designed to fulfil its main purpose. But this is a user-driven application with all the possibilities of moving,
resizing, tuning, saving, and restoring. Let us start with moving.
Moving can be applied to individual controls, to the controls of those predetermined groups
(numbers, operations, and functions), and to the temporary group of controls. Moving of a
single control is organized in a standard way: press the left button somewhere close to the
border of a control and move it to the new location. Area around the control border is used
both for moving and resizing, but the change of a cursor tells you what action can be started
at each particular point.
A group around numbers, operations, or functions (figure 21.4) can be organized via the
menu commands; I will write about menus a bit later. Thus organized “standard” group
includes only the controls of that predefined set and nothing else even if some other
controls happen to be inside the frame when the group is called. So the Functions group
always includes exactly eight controls with the names of the functions; two other controls
inside the frame at figure 21.4 (one with the digit and another with the equal sign) do not
belong to the group.
In the Form_TemporaryGroup.cs (figure 15.31), I have demonstrated a temporary group Fig.21.4 Part of the form
of controls which can be organized at any moment; the same idea of a temporary group is with the
used in Calculator. Press the left button at any empty place and move the mouse cursor Functions group
without releasing the button. Throughout such movement, you will see the rectangular
frame; two opposite corners of this rectangle are the initial press point and the
current mouse position. If at the moment of the mouse release there is more than
one control inside this rectangle then a temporary group of these controls is
organized (figure 21.5). Temporary group can include all or some controls from
one of the predefined groups, controls from different predefined groups, or even
the controls which do not belong to any of them. For example, the temporary
group from figure 21.5 includes seven controls from the Numbers group, two
controls from the Operations group, and one information control.
Any group, regardless of whether it is a predefined one or a temporary one,
belongs to the ElasticGroup class, so it can be moved by any inner point.
The frame adjusts its size and position to any changes of inner elements. I would
say that any group organized in Calculator serves like a temporary one and only
for the purpose of moving some group of elements. Though it is always an
ElasticGroup object, there is no command to modify this group. Tuning of
ElasticGroup objects is used for two purposes: to modify the view of the
Fig.21.5 Temporary group group itself and to modify inner elements. In the Form_Calculator.cs, there is
no modification of the group view, while elements modifying is organized not
through tuning form but in different way which is described a bit further on.
The total number of controls in the Form_Calculator.cs is 31 (figure 21.3). Controls are divided between two Lists.
The names of these Lists make it obvious that one of them includes all controls of the currently used group; all other
controls go into another List.
List<Control> controlsSingle = new List<Control> ();
List<Control> controlsInGroup = new List<Control> ();
The distribution of controls between two Lists is done either at the moment of calling an appropriate command from the
Settings menu (figure 21.6, commands of the third group) or when a mouse is released with a drawn rectangle in view. In
both cases the SetGroup() method is called but with different parameters.
When the menu command is called and one of the predefined groups is ordered to appear, then the SetGroup() method
gets a predefined set of controls as a parameter. Controls from this set go into the controlsInGroup; all other controls
go into the controlsSingle.
private void SetGroup (List<Control> ctrlsToFrame)
{
controlsSingle .Clear ();
controlsInGroup .Clear ();
… …
foreach (Control control in Controls)
World of Movable Objects 739 (978) Chapter 21 Medley of examples
{
if (ctrlsToFrame .Contains (control))
{
controlsInGroup .Add (control);
}
else
{
controlsSingle .Add (control);
}
}
… …
In case when a rectangle is painted by a mouse and then the mouse is released, another version of the SetGroup()
method is called. In this case the painted rectangle is used as a parameter and all the controls are checked for a chance of
being inside this rectangle.
private void SetGroup (Rectangle rc)
{
controlsSingle .Clear ();
controlsInGroup .Clear ();
foreach (Control control in Controls)
{
if (rc .Contains (control .Bounds))
{
controlsInGroup .Add (control);
}
else
{
controlsSingle .Add (control);
}
}
if (controlsInGroup .Count == 1) Fig.21.6 The Settings menu
{
controlsSingle .Add (controlsInGroup [0]);
controlsInGroup .Clear ();
}
RenewMover ();
}
At the end of this method, after all the controls are
checked and the distribution between two Lists is
done, the number of controls inside the
controlsInGroup is checked. This number is not
allowed to be less than two; if there happened to be a
single control inside, it is also moved to the
controlsSingle. Group can be eliminated by the
Delete frame menu command (figure 21.6), but this
checking at the end of the SetGroup() method
shows one more way to do it. Draw a new rectangle
which contains no controls or a single control. In each
of these situations no group will be organized, but if
some group is shown at this moment, then it will
disappear. It is not even needed to draw an empty
rectangle; it is enough to click with the left button at
any empty place. Fig.21.7 The Standard positioning menu command allows to
There is one more way to rearrange the positions of open an auxiliary form to select some standard
elements, but this method can be applied only to the positioning of elements for predefined group
controls of three predefined groups. Submenu from figure 21.7 shows three commands which can be applied to the
Numbers group; identical submenus are opened when the menu lines Operations or Functions are selected.
World of Movable Objects 740 (978) Chapter 21 Medley of examples
Fig.21.8a The form Form_BtnsPlacement_Numbers.cs is used to Fig.21.8c Selection of standard positioning for
select some standard positioning of controls inside the Functions group
Numbers group
Each variant of the standard positioning is shown as a sketch with an additional check box to select this variant. Each
sketch consists of a set of rectangles, representing the real controls. These tiny rectangles are linked with the lines to show
the order of controls in the group. Otherwise it would be impossible to distinguish similar variants in which controls can be
positioned either by rows or by columns. When the Form_BtnsPlacement_Numbers.cs is closed by clicking the OK
button, the buttons of the Numbers group in Calculator are lined according to the selected variant. It does not matter,
whether the Numbers group has or has not the frame at this moment; the standard positioning works regardless of it.
Each auxiliary form for selection of standard positioning has a small group containing two controls; at figures 21.8 these
groups are shown in different colors. Change of values inside these controls has no effect on the sketches view, so when
these values are changed, then there is no reaction inside the auxiliary form. But if any standard positioning is selected and
the OK button is pressed, then controls of the group are positioned in the main form of the Calculator according to the
selected case and the distances between controls are determined by those two values.
Let us continue with the Calculator, but after it I will return again to the discussion of sketches from figures 21.8.
The resizing of all elements in the Form_Calculator.cs can be done on an individual basis in a standard way for control
resizing. Now suppose that you have resized one control from the Numbers group in the way you want this control to look.
What are you going to do next? Are you going to keep it different from other buttons with the numbers or to change the
sizes of other 11 buttons in the Numbers group in exactly the same way? Are you going to do it manually? Some people
may like to organize the steadily growing size of the buttons according to the increase of the numbers on them or organize
other extraordinary views (nothing is prohibited in user-driven applications!), but somehow I have a feeling that the
majority of users will prefer to see the controls of a group all having the same size. I also feel that the manual standardizing
of controls in a big group one after another is not going to be a very popular operation.
Certainly, there is a better way which was already used in the Form_PlotsVaraiety.cs and described in the previous
chapter: an element can be used as a sample for all its siblings. The variations are only in choosing the parameters for
spreading and also in decision about the group of controls on which to spread the selected parameter.
World of Movable Objects 741 (978) Chapter 21 Medley of examples
Figure 21.9 shows menu which can be called on any control from the
Numbers group. Nearly all elements in the Calculator are buttons.
There are only three parameters which can be changed for such element:
size, text font, and text color.* Font and color can be changed via the
commands of this menu; the new parameters are automatically spread
on all the controls of the group. Two commands of menu allow to Fig.21.9 Menu on controls of the Numbers
spread the sizes of the changed control either on all buttons of the
group
Numbers group or also on all buttons of the Operations group. Controls
of these two groups initially have the same size and I have a feeling that some users will prefer to keep this equality in sizes.
Similar menus can be called for the buttons of the Operations and Functions groups.
While describing different types of plotting in the previous chapters, I showed that the tuning of many classes was started
from different context menus. In the case of graphical objects, the selection of the appropriate menu is done by analysing
the class of object pressed and released by mover. In the Calculator, there are two different mechanisms of calling the
context menus, but even the one which relies on mover is slightly different. In the Form_Calculator.cs, many objects need
different menus but belong to the same SolitaryControl class, so the menu selection starts with the number of the
caught object in the mover queue.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double dist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
if (mover .Release ())
{
… …
else if (e .Button == MouseButtons .Right && dist <= 3)
{
bMenuByMover = true;
ContextMenuSelection (mover .ReleasedObject);
}
… …
All buttons were transformed into SolitaryControl objects and then they were distributed between three groups.
Menus for elements of these groups are slightly different, but the class of the pressed object is the same, so the class itself
cannot help in the right selection of menu. Instead, the number of the pressed object in the mover queue helps to find the
control which is the kernel of the pressed object. The belonging of this control to one of the predefined groups determines
the menu to be called.
private void ContextMenuSelection (int iInMover)
{
GraphicalObject grobj = mover [iInMover] .Source;
if (grobj is SolitaryControl)
{
Control ctrl = (grobj as SolitaryControl) .Control;
if (listDigits .Contains (ctrl))
{
controlTouched = ctrl;
ContextMenuStrip = menuOnDigitBtn;
}
else if (listOperations .Contains (ctrl))
{
controlTouched = ctrl;
ContextMenuStrip = menuOnOperationBtn;
}
else if (listFunctions .Contains (ctrl))
{
*
At the very end of writing and checking this book I understood that I forgot about one more possible tunable parameter:
the control background color. It is easy to do but I do not want to make such changes at the last moment. If you want, you
can add the command to change the background color of controls into this menu. Maybe I will add this command into the
next version.
World of Movable Objects 742 (978) Chapter 21 Medley of examples
controlTouched = ctrl;
ContextMenuStrip = menuOnFunctionBtn;
}
}
}
The second mechanism of calling the context menus for elements in Calculator is specific. Menu can be mentioned in the
ContextMenuStrip property of the control at the design stage; this will be enough for automatic call of this menu. I
mentioned that this is a specific way of calling a context menu because it can be used with controls but not with graphical
objects. As my programs deal mostly with different graphical object, then this way of calling menus can be rarely found in
my applications. In the big Demo program accompanying this book, the ContextMenuStrip property of controls is
used only in two examples: in the Form_PersonalData.cs and in the Form_Calculator.cs.
When menu is called by pressing some button, there is no problem in identifying this button, so the method itself is very
simple. But because there are two different mechanisms of calling menus, I had to add into the code of the form an
additional field (bMenuByMover) to prevent calling the menu twice at the same moment. Here is the method for calling
menu on any control from the Numbers group; similar methods exist for the Functions and Operations groups.
private void MouseDown_numbers (object sender, MouseEventArgs e)
{
if (e .Button == MouseButtons .Right)
{
controlTouched = sender as Control;
ContextMenuStrip = menuOnDigitBtn;
}
}
Suppose that after moving and resizing the controls you have organized the perfect view of the Calculator and you do not
want to damage this view even by a single accidental move of any element. A lot of previous examples include different
commands to fix / unfix their elements. In some of the examples such type of command can be applied to one group of
elements or another; in the Calculator the fixing / unfixing is applied to all controls simultaneously via the menu at any
empty place. The command switches the value of the bMovable field which is used in the construction of all
SolitaryControl objects.
private void RenewMover ()
{
mover .Clear ();
foreach (Control ctrl in controlsSingle)
{
mover .Add (new SolitaryControl (ctrl, bMovable));
}
… …
Now let us return to the discussion of sketches which are used in three auxiliary forms (figures 21.8). All three forms are
designed according to the rules of user-driven applications. All elements inside these forms are movable, so you can
rearrange their views by positioning those sketches in any way you want. There is a small context menu which can be
called at any empty place; the only command of this menu allows to reinstall the default view of the form, so do not be
afraid to change anything.
Whenever you are developing the user-driven applications, you have to design from time to time movable objects with some
interesting and non-standard features because the requirements are non-standard. I think it is the most interesting in
programming when you have to think out something non-trivial.
Sketches in those three forms belong to the ButtonsSketch class. Sketch is a graphical object with majority of its
fields representing its geometry.
public class ButtonsSketch : GraphicalObject
{
bool bSelected; // indicator of the sketch selection
Rectangle rc; // the whole area of an object
Rectangle rcSelect; // the rectangle of a fictional check box
Point [] ptBtn; // positions of all the “buttons” on the sketch
int w = 16; // sizes for the "buttons"
World of Movable Objects 743 (978) Chapter 21 Medley of examples
int h = 8;
int spaceInside = 4; // spaces between "buttons"
int spaceOnBorders = 6; // spaces on the sides
int sideSelectSquare = 14;
Sketches in each file demonstrate
variants of positioning controls for
some group. All sketches in one file
are associated with the same number
of controls; let us call this number
N. Figure 21.10 shows some
variants for N = 12.
• Each control is represented
on the sketch by a small
dark rectangle; at the
moment its size is 16*8
pixels (w*h) Fig.21.10 Several sketches for the case of N = 12
• Distances between neighbouring small rectangles on the sketch are always the same and equal to 4 pixels
(spaceInside).
• Sketch has a rounded frame; the distance between this frame and inner elements is 6 pixels (spaceOnBorders).
• To select the needed positioning of the group controls, one or another sketch must be checked; this is done with the
help of a small graphical check box inside the frame. This special check box occupies a rectangle with the side of
14 pixels (sideSelectSquare).
• The sketch for N “buttons” can show them all in one row or in one column. To describe the position of the
“buttons” on a sketch, I use matrix of N*N size. The numbering of positions starts with 0 in the top left corner and
goes to the right along the upper row; then it goes downstairs row by row; in each row the numbering starts on the
left position and increases to the right. Thus, the left position in the second row has the number N; the position in
the bottom right corner of the matrix has the biggest possible number of N*N-1. For example, all sketches from
the figure 21.10 are designed for the case of N = 12. An array of positions is one of the parameters for
initialization of the sketch. Here you can see an array of integers which describes the positions for the checked
sketch in the bottom left corner of figure 21.10.
ButtonsSketch sketch_2rows = new ButtonsSketch (nCode, false,
new Point (rect .Left, rect .Bottom + 20),
new int [] { 0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17 },
brushBtns);
• Sketches may have the same number of rows and columns but differ by the order of elements. For example, two
sketches from figure 21.10 have four rows and three columns each, but in the left sample the elements have to be
positioned along the rows, while in another one – along the columns. To distinguish such cases visually, the
consecutive elements of the sketch inside row or column are connected by the lines.
public void Draw (Graphics grfx)
{
Pen penConnect = new Pen (brushBtns .Color, 2);
Auxi_Drawing .CurvedFrame (grfx, rc, penFrame);
for (int i = 0; i < ptBtn .Length; i++)
{
grfx .FillRectangle (brushBtns, ptBtn [i] .X, ptBtn [i] .Y, w, h);
grfx .DrawRectangle (Pens .DarkGray, ptBtn [i] .X, ptBtn [i] .Y, w, h);
if (i > 0)
{
if (ptBtn[i].X == ptBtn [i - 1].X || ptBtn[i].Y == ptBtn [i - 1].Y)
{
grfx .DrawLine (penConnect,
ptBtn [i] .X + w / 2, ptBtn [i] .Y + h / 2,
ptBtn [i - 1] .X + w / 2, ptBtn [i - 1] .Y + h / 2);
}
World of Movable Objects 744 (978) Chapter 21 Medley of examples
}
}
ControlPaint .DrawCheckBox (grfx, rcSelect,
bSelected ? ButtonState .Checked : ButtonState .Normal);
}
Sketch is movable by any inner point with the exception of the check box area, so the cover consists of two rectangular
nodes. As usual, the smaller node with the special purpose must precede the bigger one which is responsible for moving the
whole object.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, rcSelect, Cursors .Hand),
new CoverNode (1, rc, Cursors .SizeAll) };
cover = new Cover (nodes);
}
Moving of a sketch is done only if the bigger node (with number 1) is pressed.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 1)
{
Move (dx, dy);
bRet = true;
}
}
return (bRet);
}
All sketches are non-resizable. When a sketch is moved, values of all basic points are changed synchronously.
public override void Move (int dx, int dy)
{
rc .X += dx;
rc .Y += dy;
for (int i = 0; i < ptBtn .Length; i++)
{
ptBtn [i] .X += dx;
ptBtn [i] .Y += dy;
}
rcSelect .X += dx;
rcSelect .Y += dy;
}
These are all interesting things which can be found inside the ButtonsSketch class. One more important thing related
to the class is mentioned in the form where sketches are used, for example, in the Form_BtnsPlacement_Numbers.cs.
This is about the selection of one of the sketches. The selection is done by clicking the sketch inside the special area which
is shown as a check box. It is not a real check box but only its picture which is covered by a small node. The correct
identification of the pressed sketch is done in the standard way through the order of the pressed object in the mover queue;
the number of the pressed node is also detected by mover. Small check box is covered by the node zero.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double dist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObject, iNode;
if (mover .Release (out iObject, out iNode))
{
World of Movable Objects 745 (978) Chapter 21 Medley of examples
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is ButtonsSketch && iNode == 0)
{
ButtonsSketch sketch = grobj as ButtonsSketch;
for (int i = 0; i < sketches .Count; i++)
{
sketches [i] .Selected = false;
}
sketch .Selected = true;
viewSelected = (Calculator_DigitsGroupView) (sketch .Code);
places = sketch .Places;
Invalidate ();
}
}
… …
In the well known fairy tale, a hundred years has passed between the first and the second parts. The story of my Calculator
already revealed two parts, so there are no classical instructions for the break before the third part. Yes, there is going to be
the third part, so today (May 2016) the correct version of the title for this section would be something like “Calculator: the
old, the new, and the newest”. Because Calculator number three is designed on the elements which are going to be
demonstrated and discussed in the next chapter, then we’ll have a look at the newest Calculator somewhere 130 pages
ahead.
World of Movable Objects 746 (978) Chapter 21 Medley of examples
An exercise in drawing
All the previous chapters were also filled with drawing, but only in this example the main goal is drawing. It
turned out that the rules of such application are exactly the same as for the complex scientific programs or
other complex applications.
Throughout the years I have prepared a number of programs to demonstrate the design and use of absolutely different
movable objects. All the time I try to show that the algorithm is not aimed at the specific form of objects or at any area of
design. Yes, the algorithm was born, because I understood the huge demand for movable / resizable objects in design of the
scientific / engineering applications, but shortly after it I came to the understanding that the use of such objects is going to
change the design and use of applications in many areas. In order to check how the use of movable / resizable elements can
affect the programs in different areas, I tried to include the examples which were far away from each other. One of such
examples is simply an exercise in drawing and includes drawing of different buildings. There were two examples of such
type. In one of them the buildings looked like the apartment buildings, so that example was called Town; this example was
in the Test_MoveGraphLibrary application which I already deleted several years ago. Another example includes several
types of buildings; all of them are more suitable for the country style, so the example is called Village. This is the example,
which I am going to discuss in this chapter.
File: Form_Village.cs
Menu position: Applications – Village
Fig.21.11 This village was constructed without any plan. You can do much better.
The village from figure 21.11 is definitely not an attractive one; either there was no architect at all or a very bad one. But
this picture shows nearly all the objects which are important for discussion of the programming ideas that are used in the
Form_Village.cs. I try to include into this book the examples with some features which were not used before but which,
from my point of view, can be interesting for design of other applications.
Buildings of five types (classes) are used in this example; all of them are derived from the base abstract class Building.
When you start the Village application for the first time, you
do not see any buildings at all. The form is empty with only
Buildings group in view; group contains five buttons with
the pictures of different buildings (figure 21.12). This is a
group of the ElasticGroup class with all the standard
Fig.21.12 Buildings group
World of Movable Objects 747 (978) Chapter 21 Medley of examples
features of this class. There is also a menu that can be called inside this group
(figure 21.13). All commands of this menu change the view and behaviour only of the group
itself; they have no effect on the buildings in the form.
By clicking any button in the Buildings group, you add a building of the appropriate type into
the form. The new building initially appears in the middle of the form; from there it can be
moved to any place, resized, and used in a lot of other operations. Let us look at these five
classes of buildings one after another. Fig.21.13 Menu on the
Buildings group
The Hangar class is the simplest of five classes. Hangar shape is semicircle, so its
geometry is determined by the central point and radius. There are limits both on minimum and maximum radii. A hangar
can be resized by any point of its roof, while the size of the door is fixed.
public class Hangar : Building
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut;
PointF ptCenter;
float m_radius;
double angleBeam;
float delta = 3;
float minRadius = 20;
float maxRadius = 60;
float fDoorSize = 14;
In the Form_Circles_SimpleCoverAdh.cs (figure 12.4), I have
shown resizable circles of the Circle_SimpleCoverAdh class
with a cover consisting of two nodes and the adhered mouse technique
used throughout the resizing. The same types of cover and resizing are
used in the Hangar class, but to turn a circle into semicircle, one
additional transparent node is used (figure 21.14). As usual, this
transparent node must stay ahead of other nodes which it is going to
cut. Node numbers can be seen at the figure of the cover.
Fig.21.14 Hangar object and its cover
public override void DefineCover ()
{
float radiusPlus = m_radius + delta;
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, new PointF [] {
new PointF (ptCenter .X - radiusPlus, ptCenter .Y),
new PointF (ptCenter .X + radiusPlus, ptCenter .Y),
new PointF (ptCenter .X + radiusPlus, ptCenter .Y + radiusPlus),
new PointF (ptCenter .X - radiusPlus, ptCenter .Y + radiusPlus)},
Behaviour.Transparent),
new CoverNode (1, ptCenter, m_radius - delta, Cursors .SizeAll),
new CoverNode (2, ptCenter, radiusPlus)};
cover = new Cover (nodes);
cover .SetClearance (false);
}
Resizing of semicircle with the use of adhered mouse is really simple. It was explained in the chapter Simpler covers, but it
was long ago, so I’ll remind about this process.
When the mouse is pressed anywhere in the vicinity of the border, the Hangar.StartResizing() method is called.
This method makes two things.
• The best cursor trajectory for semicircle resizing is the moving along radial line, so the angle of this line is
calculated (angleBeam). Then two end points for segment of this line are calculated. These points – ptEndIn
and ptEndOut – are determined by minimum and maximum allowed radii of semicircle.
• Cursor is switched exactly on the border. From this moment and throughout the whole resizing the current position
of the mouse cursor determines the semicircle radius.
World of Movable Objects 748 (978) Chapter 21 Medley of examples
public void StartResizing (Point ptMouse)
{
angleBeam = Auxi_Geometry .Line_Angle (ptCenter, ptMouse);
AdjustCursorPosition (Auxi_Geometry .PointToPoint (ptCenter, angleBeam,
m_radius));
ptEndIn = Auxi_Geometry .PointToPoint (ptCenter, angleBeam, minRadius);
ptEndOut = Auxi_Geometry .PointToPoint (ptCenter, angleBeam, maxRadius);
}
The above mentioned transformation of cursor position into the semicircle radius is done by the Hangar.MoveNode()
method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 1)
{
Move (dx, dy);
}
else
{
PointF ptNearest = Auxi_Geometry .NearestPointOnSegment (ptM,
ptEndIn, ptEndOut);
m_radius = Convert .ToSingle (Auxi_Geometry .Distance (ptCenter,
ptNearest));
AdjustCursorPosition (ptNearest);
bRet = true;
}
}
return (bRet);
}
If we steadily move from the simplest class of buildings to more complex, then the next one is the House_Primitive
class. Such building has a rectangular main part and symmetrical triangular roof.
public class House_Primitive : Building
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut;
RectangleF rcHouse; // points 0 - 3
PointF ptRidge; // point 4
int roomSize = 18;
int windowSize = 10;
int doorH = 12;
int doorW = 6;
int minRoofH = 16;
Any house of this class can be from one to three storeys high and has
from two to five windows on each floor; at the ground floor one window
is substituted by the door. Sizes of windows and door are fixed, while the
house itself is resizable. The main (rectangular) part of this building can
be resized by any side or corner; roof height can be changed by moving
the ridge point. Node numbers at figure 21.15 makes the understanding
of the cover design easier. Fig.21.15 House_Primitive object and
its cover
public override void DefineCover ()
{
PointF [] corner = Auxi_Geometry .CornersOfRectangle (rcHouse);
CoverNode [] nodes = new CoverNode [4 + 1 + 4 + 1];
World of Movable Objects 749 (978) Chapter 21 Medley of examples
nodes [0] =
new CoverNode (0, corner [0], 5, Cursors .SizeNWSE);
nodes [1] =
new CoverNode (1, corner [1], 5, Cursors .SizeNESW);
nodes [2] =
new CoverNode (2, corner [2], 5, Cursors .SizeNWSE);
nodes [3] =
new CoverNode (3, corner [3], 5, Cursors .SizeNESW);
nodes [4] =
new CoverNode (4, ptRidge, 5, Cursors .SizeNS);
nodes [5] =
new CoverNode (5, corner [0], corner [3], Cursors .SizeWE);
nodes [6] =
new CoverNode (6, corner [0], corner [1], Cursors .SizeNS);
nodes [7] =
new CoverNode (7, corner [1], corner [2], Cursors .SizeWE);
nodes [8] =
new CoverNode (8, corner [3], corner [2], Cursors .SizeNS);
nodes [9] =
new CoverNode (9, new PointF [] {
corner [0], ptRidge, corner [1], corner [2], corner [3] });
cover = new Cover (nodes);
}
The last node in the cover – the polygonal (pentagonal) node – is used for moving an object. All other nodes are used for
resizing which starts with initial shift of the mouse cursor either to the corner point or to the border point. Depending on the
pressed node, two different techniques are used for resizing.
• If it is the roof ridge (node 4) or any side of rectangle (nodes 5 – 8), then the adhered mouse technique is used with
the cursor moving along horizontal or vertical segment. The end points of each segment are determined by the
allowed sizes and are calculated in the StartResizing() method.
public void StartResizing (Point ptMouse, int iNode)
{
if (iNode != 9)
{
PointF ptStart;
switch (iNode)
{
… …
case 5: // left side
ptStart = new PointF (rcHouse .Left, ptMouse .Y);
ptEndIn = new PointF (rcHouse .Right - minHouseWidth,
ptStart .Y);
ptEndOut = new PointF (rcHouse .Right - maxHouseWidth,
ptStart .Y);
break;
case 6: // ceiling
ptStart = new PointF (ptMouse .X, rcHouse .Top);
ptEndIn = new PointF (ptStart .X,
rcHouse .Bottom - minHouseHeight);
ptEndOut = new PointF (ptStart .X,
rcHouse .Bottom - maxHouseHeight);
break;
… …
• If any corner of rectangular part is pressed, then this corner can move only inside the rectangle determined by the
house sizes. Area allowed for corner movement (rcMoveAllowed) is calculated in the same
StartResizing() method; after it the clipping is declared in the Form_Village.OnMouseDown()
method by using the calculated area.
public void StartResizing (Point ptMouse, int iNode)
{
if (iNode != 9)
{
PointF ptStart;
switch (iNode)
{
case 0:
ptStart = new PointF (rcHouse .Left, rcHouse .Top);
rcMoveAllowed = RectangleF .FromLTRB (
rcHouse .Right - maxHouseWidth,
World of Movable Objects 750 (978) Chapter 21 Medley of examples
rcHouse .Bottom - maxHouseHeight,
rcHouse .Right - minHouseWidth,
rcHouse .Bottom - minHouseHeight);
break;
… …
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is House_Primitive)
{
House_Primitive housePrim = grobj as House_Primitive;
housePrim .StartResizing (e .Location, iNode);
if (0 <= iNode && iNode < 4)
{
Cursor .Clip = RectangleToScreen (Rectangle .Round (
housePrim .CornerMoveAllowed));
}
}
… …
Next is the House_Rural class which differs from the
House_Primitive class by some small details (no door,
possibility of roof window(s)) and two extra points on the roof
perimeter (figure 21.16). These two points are always
positioned symmetrically to the house central line, so when one
of these points is moved, another also moves.
public class House_Rural : Building
{
int m_version = 701;
Fig.21.16 House_Rural objects and their cover
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut;
RectangleF rcHouse; // points 0 - 3
PointF ptRidge, // point 4
ptSlopeL, // point 5
ptSlopeR; // point 6
RectangleF rcCornerMoveAllowed;
PointF [] ptsSlopeMoveAllowed = new PointF [4];
Two extra points allow to change the roof shape. These two points add two circular nodes into the cover. All other nodes
are the same as in the House_Primitive class, but some of them have different numbers as new nodes get numbers
five and six.
public override void DefineCover ()
{
PointF [] corner = Auxi_Geometry .CornersOfRectangle (rcHouse);
CoverNode [] nodes = new CoverNode [12];
nodes [0] = new CoverNode (0, corner [0], 5, Cursors .SizeNWSE);
nodes [1] = new CoverNode (1, corner [1], 5, Cursors .SizeNESW);
nodes [2] = new CoverNode (2, corner [2], 5, Cursors .SizeNWSE);
nodes [3] = new CoverNode (3, corner [3], 5, Cursors .SizeNESW);
nodes [4] = new CoverNode (4, ptRidge, 5, Cursors .SizeNS);
nodes [5] = new CoverNode (5, ptSlopeL, 5);
World of Movable Objects 751 (978) Chapter 21 Medley of examples
nodes [6] = new CoverNode (6, ptSlopeR, 5);
nodes [7] = new CoverNode (7, corner [0], corner [3], Cursors .SizeWE);
nodes [8] = new CoverNode (8, corner [0], corner [1], Cursors .SizeNS);
nodes [9] = new CoverNode (9, corner [1], corner [2], Cursors .SizeWE);
nodes [10] = new CoverNode (10, corner [3], corner [2], Cursors .SizeNS);
nodes [11] = new CoverNode (11, new PointF [] { corner [0], ptSlopeL,
ptRidge, ptSlopeR, corner [1], corner [2], corner [3] });
cover = new Cover (nodes);
}
This is again the situation when the last node (now it has number 11) is used for house moving while all other nodes are
used for resizing. In the House_Rural class, three different techniques are used for resizing.
• For the node on the roof ridge (node 4) and for the nodes on the sides of rectangular part (nodes 7 – 10), the
adhered mouse technique is used with the cursor moving along horizontal or vertical segment. The end points of
each segment are determined by the allowed sizes and are calculated in the StartResizing() method.
• Movement of four corners of rectangular part (nodes 0 – 3) is identical to the same movement in the
House_Primitive class, so the standard cursor clipping is used.
• For two new nodes (numbers 5 and 6) the adhered mouse is used, but there is some difference from the first variant
because the cursor is allowed to move inside area.
Situation with nodes five and six is identical, so it is enough to look into the details of one of them; let us look at the change
of the left slope. When node five is pressed, then cursor is moved exactly on the central point of this node (ptSlopeL)
and after it this special point is going together with the mouse. Thus, the cursor movement must be organized only inside
the allowed area. If mouse cursor is moved outside this area, then it must be immediately returned back to the previous
position inside the area. It is the adhered mouse technique with the cursor allowed to move not along straight segment or
circular curve but inside some area. This technique was already demonstrated in one of the previous examples
(Form_Rectangles_AllMovementAdh.cs, figure 11.36) and I am going to use it again in the current example.
Area for allowed movement of node five is determined by some restrictions defined in the House_Rural class. There is
a limit – minSlopeWidth – on moving this point too close horizontally to the corner (node 0) and to the roof ridge
(node 4). This point must also stay above the straight line between the house corner and the roof ridge. Thus, the slope is
never turned into straight line. The difference can be really small (minVerDistToStraightSlope = 2), but I hope
that even in case of closest position to straight line it is possible to see the location of this point. In any case, the change of
cursor over its node signals about the existence of this point. At figure 21.17, the auxiliary straight
line from the top left corner of rectangular part to the roof ridge is seen not too well, but the area of
possible movement for the special point of the slope is shown in blue line and is seen very well.
When the mouse is pressed inside the node over this point (node 5), then the cursor is shifted exactly
to this point and the area of possible movement is calculated (ptsSlopeMoveAllowed[]). This
is done by the House_Rural.StartResizing() method.
public void StartResizing (Point ptMouse, int iNode) Figure 21.17
{
if (iNode != 11)
{
float dyStraight;
PointF ptStart;
switch (iNode)
{
… …
case 5: // on left slope
ptStart = ptSlopeL;
PointF ptCornerLT = new PointF (rcHouse .Left, rcHouse .Top);
dyStraight = (rcHouse .Top - ptRidge .Y) * minSlopeWidth /
(rcHouse .Width / 2);
ptsSlopeMoveAllowed [0] .X =
ptsSlopeMoveAllowed [1] .X = ptCornerLT .X + minSlopeWidth;
ptsSlopeMoveAllowed [2] .X =
ptsSlopeMoveAllowed [3] .X = ptRidge .X - minSlopeWidth;
World of Movable Objects 752 (978) Chapter 21 Medley of examples
ptsSlopeMoveAllowed [0] .Y =
rcHouse .Top - (dyStraight + minVerDistToStraightSlope);
ptsSlopeMoveAllowed [1] .Y =
ptsSlopeMoveAllowed [2] .Y = ptRidge .Y;
ptsSlopeMoveAllowed [3] .Y =
ptRidge .Y + (dyStraight - minVerDistToStraightSlope);
break;
… …
Resizing is provided by the MoveNode() method. There are two variants for moving node five. If cursor moves inside
the allowed area, then the slope point goes with the cursor. If cursor moves outside, then it is returned back on the slope
point. For this back movement, mover must be temporarily disconnected from the caught object.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == 5) // ptSlopeL
{
if (Auxi_Geometry .PointInsideConvexPolygon (ptM,
ptsSlopeMoveAllowed))
{
ptSlopeL = ptM;
ptSlopeR .X = ptRidge .X + (ptRidge .X - ptSlopeL .X);
ptSlopeR .Y = ptSlopeL .Y;
DefineCover ();
bRet = true;
}
else
{
AdjustCursorPosition (ptSlopeL);
bRet = false;
}
}
… …
Two more classes of buildings have a main part similar to the House_Rural class and additional garage on one or
another side. The garage side is mentioned in the name of the class, so the House_RuralLeftGarage class has
garage on the left (figure 21.18).
public class House_RuralLeftGarage :
Building
{
Form form;
Mover supervisor;
PointF ptEndIn, ptEndOut;
RectangleF rcHouse, // points 0 - 1 - 2 - 3
rcGarage; // points 8 - - 3 - 7
PointF ptRidge, // point 4
ptSlopeL, // point 5
ptSlopeR, // point 6
ptGarageRoofTop; // point 9
float minGarageHeight = 12;
float minGarageRoofHeight = 4;
New parts in the building require new nodes, so the cover of this class has 18 nodes. Of these nodes, not one but two are
used for moving such building. Polygonal node must be convex, so the whole area is covered by two polygonal nodes: one
covers the main building with its roof, another covers garage with its roof.
World of Movable Objects 753 (978) Chapter 21 Medley of examples
public override void DefineCover ()
{
PointF [] corner = Auxi_Geometry .CornersOfRectangle (rcHouse);
CoverNode [] nodes = new CoverNode [18];
nodes [0] = new CoverNode (0, corner [0], 5, Cursors .SizeNWSE);
nodes [1] = new CoverNode (1, corner [1], 5, Cursors .SizeNESW);
nodes [2] = new CoverNode (2, corner [2], 5, Cursors .SizeNWSE);
nodes [3] = new CoverNode (3, corner [3], 5, Cursors .SizeNESW);
nodes [4] = new CoverNode (4, ptRidge, 5, Cursors .SizeNS);
nodes [5] = new CoverNode (5, ptSlopeL, 5);
nodes [6] = new CoverNode (6, ptSlopeR, 5);
nodes [7] = new CoverNode (7, new PointF (rcGarage.Left, rcGarage .Bottom),
5, Cursors .SizeNESW);
nodes [8] = new CoverNode (8, new PointF (rcGarage .Left, rcGarage .Top),
5, Cursors .SizeNWSE);
nodes [9] = new CoverNode (9, ptGarageRoofTop, 5, Cursors .SizeNS);
nodes [10] = new CoverNode (10, corner [0], corner [3], Cursors .SizeWE);
nodes [11] = new CoverNode (11, corner [0], corner [1], Cursors .SizeNS);
nodes [12] = new CoverNode (12, corner [1], corner [2], Cursors .SizeWE);
nodes [13] = new CoverNode (13, new PointF (rcGarage .Left,
rcGarage .Bottom), corner [2], Cursors .SizeNS);
nodes [14] = new CoverNode (14, new PointF (rcGarage .Left, rcGarage .Top),
new PointF (rcGarage.Left, rcGarage.Bottom),
Cursors .SizeWE);
nodes [15] = new CoverNode (15, new PointF (rcGarage .Left, rcGarage .Top),
new PointF (rcGarage.Right, rcGarage .Top),
Cursors .SizeNS);
nodes [16] = new CoverNode (16, new PointF [] { corner [0], ptSlopeL,
ptRidge, ptSlopeR, corner [1], corner [2], corner [3] });
nodes [17] = new CoverNode (17, new PointF [] {
new PointF (rcGarage .Left, rcGarage .Top),
ptGarageRoofTop,
new PointF (rcGarage .Right, rcGarage .Bottom),
new PointF (rcGarage .Left, rcGarage .Bottom) });
cover = new Cover (nodes);
}
Three different techniques are used to organize the resizing of
the House_RuralLeftGarage objects.
• For the node on the roof ridge (node 4), for the upper
point of the garage roof (node 9), and for the nodes
on the sides of both rectangular parts (nodes 10 –
15), the adhered mouse technique is used with the
cursor moving along horizontal or vertical segment.
The end points of each segment are determined by
the allowed sizes and are calculated in the
StartResizing() method. Main building and
garage have the same floor level, so there is only one Fig.21.18 House_RuralLeftGarage objects and
combined strip node at this level. their cover
• Standard cursor clipping is used for moving the main building corners (nodes 0 – 3) and for moving two garage
corners which are on the outer side (nodes 7 and 8). Position of the bottom right garage corner is always identical
to the position of the bottom left corner of the main building, while the top right garage corner has no node. This
node is not needed because the moving of this corner is identical to the moving of garage ceiling.
• For two nodes on the slopes of the main roof (nodes 5 and 6), the movements are identical to the case of the
House_Rural class.
16 nodes can be used for resizing of such object. Several types of resizing require the correlation between the sizes of main
building and garage, so some rules are used to regulate this correlation. For example, the upper point of the garage roof
cannot be higher than the ceiling of the main building. If it happens, then the size of the garage roof is decreased. But this
World of Movable Objects 754 (978) Chapter 21 Medley of examples
size has its limit (minGarageRoofHeight = 4) and if such decrease of the roof is not enough, then the garage height
is decreased. This is all done by the AdjustGarageHeight() method.
private void AdjustGarageHeight ()
{
if (ptGarageRoofTop .Y < rcHouse .Top)
{
ptGarageRoofTop .Y = rcHouse .Top;
if (rcGarage .Top < ptGarageRoofTop .Y + minGarageRoofHeight)
{
rcGarage .Y = ptGarageRoofTop .Y + minGarageRoofHeight;
rcGarage .Height = rcHouse .Bottom - rcGarage .Top;
}
}
}
Houses of two classes with garages positioned either
on left or on right side can be looked at as mirrored
buildings, but node numbering is not entirely
mirrored (nodes of the main part have the same
order), so I prefer to demonstrate the
House_RuralRightGarage objects together
with its cover (figure 21.19). Comparison of two
figures shows better than any words all similarities
and differences.
Because objects of two classes are partly the same
and partly mirrored, I had to be extremely careful
while copying some pieces of code from one class Fig.21.19 House_RuralRightGarage objects and their cover
into another.
Initially the Village application shows nearly empty form with only one Buildings group in view. By clicking any button in
this group, you add a building of the appropriate type into the form. The new building appears in a small size in the middle
of the form; from there it can be moved to any place, resized, and used in a lot of other operations.
There are three ways of changing the whole picture of the form:
• Individual moving / resizing of the buildings.
• Using the commands from menu which can be called on any building.
• Using a special group which can be organized around any set of buildings.
The individual moving and resizing are organized in a standard way, as all buildings can be moved by any inner point and
resized by any border point. (Owners of the real houses can only dream about such thing.)
While looking through the commands that are available in the menu on any building
(figure 21.20), you can find a lot of similarities with the menu on plots from the
Form_Functions.cs (figure 18.7). Though objects in two applications are absolutely
different, a lot of these commands are based on the general expectations in the case of a
form with a lot of elements in view.
When you have a form with many movable objects, these objects can be moved across
each other and in many situations they will overlap. It is natural to have some
commands for changing the order of these objects in view (the order of their drawing).
Each object moves on its own level, so the standard set of commands allows to move an
object one level up or down and to move it either to the highest or to the lowest level.
Nearly all classes of buildings use the same set of colors to paint different parts: main
building, roof, windows, door, and edges. (Because they are used in the majority of
cases, the corresponding fields are placed in the base Building class.) One Fig.21.20 Menu on buildings
command of menu allows to save the set of colors from the pressed building as a sample;
another command allows to repaint the building with the colors from the sample. Hangar differs from the houses of other
classes, so there are two different samples: one for hangars and another for all others. There is also a command for
individual change of colors, but I will return to this process a bit later.
World of Movable Objects 755 (978) Chapter 21 Medley of examples
It would be a tiresome process if the only way to construct a village of 30 or 40 buildings would be to add them one by one
and set their sizes and colors individually. There are two ways to simplify this process.
• Any building can be duplicated by using the menu command (figure 21.20, third command from bottom).
• Any set of buildings can be united into a group and several commands can be applied to this group.
The design of such group is pretty simple and standard. Press the left button at any empty place and move it without
releasing the button. During such movement you see the painted rectangular frame based on the initial point and the current
mouse position. If at the moment of the mouse release there is more than one building inside this rectangle, then a
temporary group is organized. This temporary group belongs to the BuildingsGroup class and it is always shown as a
painted rectangle with a frame; at figure 21.11 it is shown as a cyan rectangle with a blue frame. Not only the background
color of this group is fixed, but its transparency is set to 0.2; the need for transparency will become obvious from further
discussion of this group behaviour. Whenever the group is shown, its background is painted with a brushBack.
brushBack = new SolidBrush (Color .FromArgb (204, Color .Cyan));
// transparency = 0.2 A = 255 * (1 - coef)
Let us check the organizing of the group.
When the left button is pressed at any empty place, the value of the bTemporaryFrame field is set to true and
signals the need for drawing of a temporary frame.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
bTemporaryFrame = true;
}
}
ContextMenuStrip = null;
}
When the value of the bTemporaryFrame signals the need for drawing of a temporary frame, this rectangular frame is
based on the initial point of the mouse press and the current mouse position.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (bTemporaryFrame)
{
grfx .DrawRectangle (Pens .Blue, Auxi_Geometry .RectangleAroundPoints (
ptMouse_Down, ptMouse_Move));
}
}
When the left button is released without releasing any object by mover (the temporary frame is not a movable object, so this
can be our case), then the selected rectangle must be checked for a possibility of organizing a BuildingsGroup object.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
if (e .Button == MouseButtons .Left)
{
if (mover .Release ())
{
… …
World of Movable Objects 756 (978) Chapter 21 Medley of examples
}
else
{
if (bTemporaryFrame)
{
RectangleF rc =
new RectangleF (Math .Min (ptMouse_Down .X, e .X),
Math .Min (ptMouse_Down .Y, e .Y),
Math .Abs (e .X - ptMouse_Down .X),
Math .Abs (e .Y - ptMouse_Down .Y));
SetBuildingsGroup (rc);
bTemporaryFrame = false;
RenewMover ();
}
}
… …
The group is organized (or can be organized) by the SetBuildingsGroup() method.
• Not more than one such group can exist in the Form_Village.cs at any moment, so if there is such a group at the
moment of calling this method, its existence must be terminated.
• All the existing buildings must be checked against the area of rectangle; all buildings which are found inside are
included into the special List of buildings.
• If there are two or more buildings in this List, then the group is organized.
private void SetBuildingsGroup (RectangleF rc)
{
if (groupHouses != null)
{
groupHouses = null;
RenewMover ();
}
List<Building> elems = new List<Building> ();
for (int i = 0; i < houses .Count; i++)
{
if (rc .Contains (houses [i] .RectAround))
{
elems .Add (houses [i]);
}
}
if (elems .Count > 1)
{
groupHouses = new BuildingsGroup (elems);
}
}
The drawing of the temporary frame is not the only reason for calling the SetBuildingsGroup() method. In the
previous section Calculators old and new I have demonstrated a similar group which has a set of elements fixed at the
moment of the group construction. In the example of Calculator, even if the group was moved and released in such a place
that some other controls happened to be inside the frame, it did not change the set of the group elements. In the
Form_Village.cs, in case of the BuildingsGroup object, I purposely use different logic.
• Any building can be moved from outside and dropped inside the group thus including this building into the group.
• When a group is moved and released at some location, any building which was not in a group before but happens
to be inside the frame now is added to the group. This is the main reason to make this group slightly transparent so
it is possible to look through and see what can be added to the group at one moment or another.
Both cases of adding new buildings to the group are based on the reconstruction of the group at the moment when either a
single building or a group is released.
World of Movable Objects 757 (978) Chapter 21 Medley of examples
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (e .Button == MouseButtons .Left)
{
if (mover .Release ())
{
GraphicalObject grobj = mover .ReleasedSource;
… …
if (groupHouses != null && (grobj is Building ||
grobj is BuildingsGroup))
{
SetBuildingsGroup (groupHouses .Frame);
RenewMover ();
}
… …
Moving a building from outside into a group and moving a group to the new
location can change the set of buildings belonging to the group, but there is one
more possibility to change this set. This can happen as a result of using one of
the commands from the menu of this group (figure 21.21). The second
Fig.21.21 Menu for a group of
command of this menu allows to duplicate all the buildings of the group. To
buildings
make the result of duplication obvious for users, the copies of the existing
buildings are placed not at exactly the same location but are moved aside both horizontally and vertically from the originals.
The currently used shift is equal to 60 pixels along each direction, but you can change these values in the code.
private void Click_miDuplicateBuildings (object sender, EventArgs e)
{
Point ptCur, pt;
for (int i = groupHouses .Elements .Count - 1; i >= 0; i--)
{
ptCur = groupHouses .Elements [i] .Location ();
pt = new Point (ptCur .X + 60, ptCur .Y + 60);
Building newBuilding = groupHouses .Elements [i] .Copy (pt);
houses .Insert (0, newBuilding);
}
SetBuildingsGroup (groupHouses .Frame);
RenewMover ();
}
The duplication produces the copy of each building from the group, but there are some other results which depend on the
size of the original group, its content, and the relative positions of the inner buildings (the “density” of the group). The
distance between the inner elements and the frame depends on the used constructor of the group, but usually it is around 10
pixels. Copies of those inner buildings that happened to be close to the right or bottom parts of the frame before the
command for duplication will be placed either on the frame or even beyond the frame and will be not included into the
group on the final checking. But there can be other buildings in the group whose copies appear inside the frame; these
copies are added to the group. Obviously, not all the copies will
be added but some of them might. If you use the command for
copying twice in a row without any individual movements of
buildings or any other commands in between, then it is possible
that a set of buildings to be duplicated on these commands will
differ.
Figure 21.22 shows an organized group of three buildings plus
another four buildings placed on the frame; of those four buildings
the two are over the group and other two are under the group.
This is a good example to write about the organization of the
mover queue which is not the standard one in this
Form_Village.cs.
There is one main rule for organizing the mover queue: all Fig.21.22 A part of the form with some buildings
controls must precede the graphical objects. According to this included into the group
World of Movable Objects 758 (978) Chapter 21 Medley of examples
rule, the ordering of nearly all the elements of the form is obvious. Information (of the InfoOnRequest class) is shown
atop all the buildings. Thus we receive such an order of objects.
1. The small button to show the information.
2. The Buildings group.
3. The information if it has to be shown.
4. All the buildings. The buildings can change their relative order but are never moved ahead of anything else.
So the part of the RenemMover() method to place the above mentioned elements into the mover queue is simple.
private void RenewMover ()
{
mover .Clear ();
info .IntoMover (mover, 0);
groupBtns .IntoMover (mover, 0);
scHelp .IntoMover (mover, 0);
for (int i = 0; i < houses .Count; i++)
{
mover .Add (houses [i]);
}
… …
Only one question is left: what is the correct place in the mover queue for the group of buildings? Not for the buildings of
the group; the order of buildings does not depend on whether some of them are included into a group or not. Every newly
constructed building is placed at the head of the whole list of buildings, so that the new one is never closed from view even
partly but appears atop all others. The question is about the order of that colored in cyan and slightly transparent
rectangular area with a frame around.
The first suggestion is that the group must stay behind all its inner elements; in such a case any inner building can be easily
grabbed and moved around. Does it mean that the group must be placed at the end of the queue behind all the buildings? It
is a possible solution but not a good one when you have a densely populated area and at some moment you want to move
several buildings without any other changes. There can be a lot of variants, but there are cases (I know, because I ran into
them) when it would be very difficult to find a spot by which to move such a group if all buildings are always shown atop
the group.
After some consideration I came to another solution. I check the List of buildings starting from the end. When I find the
first of the buildings belonging to the group, I put the group into the mover queue behind this building. This guarantees that
all buildings of the group precede the group itself in the queue. At the same time the group is not always blocked by all the
buildings, but some of them may be placed behind the group. Here is the final part of the RenewMover() method.
private void RenewMover ()
{
… …
if (groupHouses != null)
{
for (int i = mover .Count - 1; mover [i] .Source is Building; i--)
{
if (groupHouses.Elements.Contains (mover [i] .Source as Building))
{
mover .Insert (i + 1, groupHouses);
break;
}
}
}
if (bAfterInit)
{
Invalidate ();
}
}
You can move the group around and release it at such a place where some previously not included building can be seen
dimly through the group. On releasing the group, you see the immediate change of the visibility of this building as it
World of Movable Objects 759 (978) Chapter 21 Medley of examples
appears above the group. This does not mean the change in the order of buildings. It happened because the set of buildings
in the group was changed, the new place for the group in the mover queue was calculated, and this new place is closer to the
end of the queue than it was before.
Several pages back, while showing the menu on buildings (figure 21.20) and discussing two commands from that menu
which allow to change the colors via samples, I mentioned the possibility of individual tuning of colors for any building.
Let us look into this process. It will take us one level dipper inside the application, but there is at least one particular thing
that is worth mentioning.
When you click the Change colors command in the menu which is called on any building, an additional small form is
opened. This form contains an image of the building that is similar to the one on which the menu was called. The view of
the building inside this auxiliary form can be slightly different from the exact pressed building as the picture inside the form
only represents the buildings of some class; the number of storeys and windows can be different, but the colors will be
definitely from the original. Each class of buildings from the Form_Village.cs has its own form to change the colors.
Originally all these auxiliary forms worked in exactly the same way; now there are two different ideas of their design. This
is a bad decision for design of any real application, but this is a Demo program in which I want to demonstrate some
possibilities.
Figures 21.23 show three forms to change colors for Hangar, House_RuralLeftGarage and
House_RuralRightGarage objects. Hangars have three colors which can be changed; other classes of original
buildings have five colors for changing. Any color is changed by using the standard ColorDialog which is opened
after clicking a small button. I hope that positions of these buttons make it obvious what color is changed by using a
particular button. The change of any color has the immediate effect not only on the sample inside an auxiliary form but also
on the original building in the Form_Village.cs for which this additional tuning form was called.
Fig.21.23a Auxiliary form to change Fig.21.23b Form to change colors for Fig.21.23c Form to change colors for
colors for Hangar objects House_RuralLeftGarage objects House_RuralRightGarage objects
All three shown auxiliary forms work in identical way, so it is enough to look into details of one of them; let us look at the
Form_Colors_RuralLeftGarage.cs (figure 21.23b). Though the form is used to set colors for objects of the
House_RuralLeftGarage class, an object inside the form belongs not to this class but to the
House_RuralLeftGarage_Sample class. The difference is small but very important: this object is movable but not
resizable. Because it is a non-resizable object, then its cover is very simple and consists of only two polygonal nodes.
• House_RuralLeftGarage class has a cover consisting of 18 nodes: the first 16 nodes are used for resizing
and two remaining nodes are used for moving an object.
• House_RuralLeftGarage_Sample class has a cover consisting of those two nodes which were used in the
original class for moving.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [2];
nodes [0] = new CoverNode (0, new Point [] {
rcHouse .Location,
new Point (ptRidge.X - sizeSlopes.Width,
ptRidge.Y + sizeSlopes.Height),
ptRidge,
World of Movable Objects 760 (978) Chapter 21 Medley of examples
new Point (ptRidge .X + sizeSlopes .Width,
ptRidge .Y + sizeSlopes .Height),
new Point (rcHouse .Right, rcHouse .Top),
new Point (rcHouse .Right, rcHouse .Bottom),
new Point (rcHouse .Left, rcHouse .Bottom) });
nodes [1] = new CoverNode (1, new Point [] {
new Point (rcGarage .Left, rcGarage .Top),
ptGarageRoof,
new Point (rcGarage .Right, rcHouse .Bottom),
new Point (rcGarage .Left, rcHouse .Bottom) });
cover = new Cover (nodes);
}
There is no resizing of this house, so the MoveNode() method simply calls the Move() method which has one
interesting feature. There are several buttons in the form and each button must be associated with particular part of the
building. Initially these buttons are positioned near or atop the associated parts of the building and after it there must be
only synchronous move of all elements. To provide such movement, the House_RuralLeftGarage_Sample object
at the moment of initialization gets the List of associated controls (buttons). When the Move() method is called to
move the building, this method also changes synchronously coordinates of all those controls.
public override void Move (int dx, int dy)
{
Size size = new Size (dx, dy);
rcHouse .X += dx;
rcHouse .Y += dy;
ptRidge += size;
… …
for (int i = 0; i < controls .Count; i++)
{
controls [i] .Location += size;
}
}
All three auxiliary forms (figures 21.23) have only to organize synchronous movement of several elements and nothing
else; even this synchronous movement is provided by the Move() method of the class of buildings used in each form. As
a result, all three methods for mouse events have their simplest possible variants with only a single call to one or another
Mover method in each of them.
private void OnMouseDown (object sender, MouseEventArgs e)
{
mover .Catch (e .Location, e .Button);
}
private void OnMouseUp (object sender, MouseEventArgs e)
{
mover .Release ();
}
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Update ();
Invalidate ();
}
}
Figures 21.24 demonstrate the second type of auxiliary forms for changing the colors. There are no buttons to call the
standard ColorDialog, so it is called by a double click on any element for which the color must be changed. This
command for color change – the double click on an element to be changed – is not obvious to users, so there is very short
reminder in the form title and there is a special information inside the form. Of two types of forms for color change
demonstrated at figures 21.23 and 21.24, I prefer the second one, but I am not going to insist that second variant is better. I
prefer to demonstrate both and let readers make their decision.
World of Movable Objects 761 (978) Chapter 21 Medley of examples
Fig.21.24a Auxiliary form to change colors for House_Primitive objects Fig.21.24b Form to change colors for
House_Rural objects
Two forms at figures 21.24 are similar in design, so it is enough to look at the details of one of them; let us look at the
Form_Colors_PrimitiveHouse.cs (figure 21.24a). House which is used in this form belongs to the
House_Primitive_Sample class. The house is movable but not resizable. In any other case of movable and non-
resizable convex polygon, it would be enough to have a cover consisting of a single node. The
House_Primitive_Sample class has a cover consisting of 12 nodes (!) and none of them is redundant.
public override void DefineCover ()
{
PointF [] corners = Auxi_Geometry .CornersOfRectangle (rcHouse);
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, Auxi_Geometry .CornersOfRectangle (rcWindows [0])),
new CoverNode (1, Auxi_Geometry .CornersOfRectangle (rcWindows [1])),
new CoverNode (2, Auxi_Geometry .CornersOfRectangle (rcWindows [2])),
new CoverNode (3, Auxi_Geometry .CornersOfRectangle (rcDoor)),
new CoverNode (4, corners [0], ptRidge),
new CoverNode (5, ptRidge, corners [1]),
new CoverNode (6, corners [1], corners [2]),
new CoverNode (7, corners [2], corners [3]),
new CoverNode (8, corners [3], corners [0]),
new CoverNode (9, corners [0], corners [1]),
new CoverNode (10, corners), // main house
new CoverNode (11, new PointF [] {corners[0], ptRidge, corners [1]}),
};
cover = new Cover (nodes);
}
Houses of the House_Primitive class have five colors which can be changed. Change of color starts with a double
click on a house, but selection of particular color to change is determined by mover which can distinguish different elements
of an object only if they are covered by different nodes. Thus, there are nodes for windows (numbers 0, 1, and 2), for a door
(number 3), for edges (numbers 4 – 9), for the main part of the building (number 10), and for the roof (number 11).
The House_Primitive_Sample class demonstrates a very rare situation when the DefineCover() method of the
class is much longer than the MoveNode() method. This happened only because I purposely made this house non-
resizable. There is an easy and interesting way to make this house resizable without any change of its cover. It can be made
resizable in exactly the same way as regular polygons from the Form_RegularPolygon_SimpleCoverAdh.cs
(figure 12.8). Central point of rectangular part of the house can be used as a central point for zooming. The whole house
perimeter is covered by the strip nodes. When any such node is pressed, then the beam from this central point through the
cursor point can be calculated. There must be some limit on minimum house size, so this limit will give one end point for
cursor movement along this beam. At the starting moment of resizing, the cursor will be shifted exactly on the border line.
Throughout the resizing, the cursor will move only along the beam and at any moment the mouse position will determine
the border position. It will be a classical case of adhered mouse technique. I decided to leave this improvement as an
exercise for readers.
World of Movable Objects 762 (978) Chapter 21 Medley of examples
The last remark on the Form_Village.cs. This form demonstrates one more case of
saving and restoring the needed data not only through the Registry but also through
the binary files. Menu which can be called at any empty place (figure 21.25) includes
three commands related to this process. Usually it is enough to have two commands –
Save and Read – for the whole process; why are there three commands in this case?
On saving, two types of information are written into a file.
• General information which includes the form size, position of the small Fig.21.25 Menu which can be
button, information about this example, the Buildings group, and two samples called at any empty place inside
of colors (one for hangars and another for all other buildings). the Form_Village.cs
Fig.21.26 Each block in the Form_BookByChapters.cs corresponds to a chapter of the book and gives links to its examples
Unfortunately, when I began to think about the proper place for this example in the book, I found such a place only closer to
the end of the book. I mentioned this example at the very beginning of the book – in the Introduction; I hope that a lot of
readers are using it in parallel with reading the book. So, the purpose of this section is now only the description of some
programming solutions used for development of this example.
World of Movable Objects 764 (978) Chapter 21 Medley of examples
The idea of the Form_BookByChapters.cs is simple enough: it gives a general view on the structure of the book and at the
same time shows the examples which are discussed in each chapter. Some of the examples, even very important for
discussion, are not shown here. The cause of it is very simple: those are the auxiliary forms for other examples and cannot
be called directly. Thus, not all the examples are available directly through the Form_BookByChapters.cs and so some of
the examples are not mentioned anywhere at figure 21.26.
The main idea of design for the Form_BookByChapters.cs was obvious from the beginning: an object for each chapter
must show its name and the list of associated examples; each example can be opened by a double click on its line. Any
programming task can be solved in different ways, so here I would like to discuss not only the final solution but also to
mention other possibilities which were considered but rejected.
• Any “chapter” can be organized as a ListView control; the name of the chapter can be painted near by;
together they will form a CommentedControl object. Development of such example would be very easy, but
there would be controls, some of them would be big, and all controls can be moved around only by their frames. I
always prefer those objects which can be moved around the screen by any inner point; for this reason I did not like
the idea of such example based on CommentedControl objects.
• Any “chapter” can be organized as an ElasticGroup with a single inner element – the same ListView
control. This would be slightly better than the previous variant, as such group can be moved around by any inner
point not covered by a control. For chapters with only one or few associated examples this would be a good
enough solution, while for cases of big controls (a lot of examples in a chapter) such solution is nearly as bad as the
previous one.
• Any “chapter” can be organized as an ArbitraryGroup with a list of examples painted inside. It is a good
solution from users’ point of view, but it would require four methods for each group – standard methods for any
ArbitraryGroup object. All four methods are very simple and they are nearly identical for all chapters, but
there are 20 different chapters... I rejected this idea.
• I could develop a special group derived from the ArbitraryGroup. This would decrease the number of
needed code but still leave some negative effects which I did not like. If I would use either ElasticGroup or
ArbitraryGroup objects, I would have to use their tuning forms which include some possibilities which are
not needed in this particular example.
At last I decided to use not familiar classes but to develop a special Chapter class. In addition, I am going to use a
special tuning form which contains only needed possibilities and nothing else.
Each group at figure 21.26 has a title, a frame, and information inside. The title is always shown, while the frame drawing
is regulated by user. Each chapter behaves like an ArbitraryGroup with one simplification: regardless of the number
of text lines inside, they are all united into a single inner element. At any particular moment the space between inner texts
and frame is fixed, so there is no relative movement of texts and frame.
Width of title and texts depends on the used fonts. If the title is wider than the texts, then the frame is calculated from the
title width and there is no relative movement of title and texts. If the
texts are wider than their title, then the frame is calculated from the texts
area and the title can be moved along the upper line of such frame.
Figure 21.27 demonstrates both cases: the title of Chapter 4 is not
movable, while for Chapter 5 it is movable. Well, it is movable by
default, but it can be declared unmovable via the tuning form.
The code for the Chapter class shows that it uses the same visibility
Fig.21.27 If texts are wider than the group
parameters as the ElasticGroup class. title, then title can be moved left
public class Chapter : GraphicalObject and right
{
Form_ChapterParams formSetParams;
Form formParent;
string [] strs;
Font fntTexts;
SolidBrush brushTexts;
SolidBrush brushBack;
bool bShowFrame = true;
Pen penFrame;
int [] m_framespaces = new int [4]; // Left, Top, Right, Bottom
World of Movable Objects 765 (978) Chapter 21 Medley of examples
bool bTitleMovable = true;
string m_title;
int m_titlesidespace;
Font fntTitle;
SolidBrush brushTitle;
At maximum, any Chapter object can be involved in two different movements – forward movement of the whole object
and movement of the title along the upper line. Thus, it would be enough to have two different nodes – one for the framed
area and another for the area of a title; the behaviour of the second node can be changed depending on the movability of
title. But the Chapter.DefineCover() method shows a cover consisting of three nodes.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [3];
if (bTitleMovable && rcGap .Width < rcFrame .Width - 2 * 8)
{
nodes [0] = new CoverNode (0, rcGap, Cursors .SizeWE);
}
else
{
nodes [0] = new CoverNode (0, rcFrame .Location, 0.0f);
}
Rectangle rc = Rectangle .FromLTRB (rcFrame .Left - 3, rcFrame .Top - 3,
rcFrame .Right + 3, rcFrame .Bottom + 3);
nodes [1] = new CoverNode (1, rc, Cursors .Hand);
nodes [2] = new CoverNode (2, rcGap, Cursors .Hand);
cover = new Cover (nodes);
if (!Movable)
{
cover .SetBehaviour (Behaviour .Frozen);
}
}
What is the purpose of using an extra node? It is possible to use only two nodes, but then the node for the area of a title
would have the same size all the time and only change the behaviour; with such design of cover I would need to check some
additional conditions in order to determine what to do if this node is caught by mover. Instead, there are two nodes of the
predetermined size and one more node (the first one in the cover) with the size depending on the movability of a title. If the
title is movable, then there are two nodes of the same size and both of them cover the title! Certainly, when you press the
title in such situation, then only the upper of two nodes is caught and this node is responsible for moving the title. Thus, the
title can be moved. If the title is unmovable, then
this node is squeezed to zero, but another one
continues to cover the title. This node is responsible
for moving the whole object, so, when you catch this
node, the whole block is moved.
In the Form_BookByChapters.cs, there is a
standard pair of information with a small button and
there are 20 Chapter objects; moving of these
objects is very simple and there are no special
situations. Visibility parameters of any object can be
changed via its tuning form.
The code for the Chapter class demonstrates that
there are the same visibility parameters as in the
ElasticGroup and ArbitraryGroup
classes; not surprisingly at all that the tuning forms
for all three classes have a lot of similar elements.
There are only two minor differences in the tuning Fig.21.28 Form_ChapterParams.cs is used for tuning of the
form for the Chapter class Chapter objects
(Form_ChapterParams.cs, figure 21.28).
• The title movability is regulated in this tuning form.
World of Movable Objects 766 (978) Chapter 21 Medley of examples
Family tree
Closer to the end of this book the examples become more and more complicated. They are not any more the artificial
examples to demonstrate one or another new feature but mostly they are the real applications which we use in our everyday
work. The main example in this section is also a real application which I use myself. Since the moment when I wrote about
this example in an article [19], it attracted attention of programmers all round the world. There are several areas (electric
circuits is one of them) in which the design is similar to design of family trees, so specialists from those areas were also
interested in the current example. Because of this high interest, I also put this set of examples as a stand alone application
among other files available at www.SourceForge.net.
Before we come to the final program in this section – the real Family Tree – there is a whole set of simpler examples which
check all the needed features and demonstrate the development step by step. While writing code for this subset of
examples, I thought again about the Hogben family and the elements they used to construct some of devices.* Here I use
only straight lines and the simplest comments which were explained long ago somewhere at the beginning of the book. At
the end of this section you will see yourself what can be developed on such a primitive basis.
In the finished version of this FamilyTree application everything can be modified by users and all those changes are saved
and restored. In the preliminary examples, not all objects of the same classes are saved and restored because I want these
auxiliary examples always to have the best view for my explanations.
Let me start with a short description of the main goal for the whole series of further examples. I want to design a program
for a family tree construction. It is easy to take a sheet of paper and start drawing your own family tree. Years ago I did it
under the supervision of my grandmother and I still keep those old sheets of paper. A lot of people in many families are
*
“Exit the Professor” by Henry Kuttner and C.L.Moore
World of Movable Objects 768 (978) Chapter 21 Medley of examples
program for design of family trees, you have to think about several commands that can simplify the addition of some
standard parts.
Fig.21.33 Couple with one child Fig.21.34 Couple with three children
I have such a version of the program in which by a single command of menu you can add a couple, one child or several
siblings and so on. As a developer, you can think about several often needed variants, but there are always chances that you
miss some situation and then it will be impossible for some users to add the type of relations they need to draw. When you
have a genealogy tree that covers a significant period of time (for example, look at the genealogy tree in some serious book
about monarchs), you have to draw some lines of very strange relations
between people and all these variants cannot be covered by any set of
predetermined commands. This is a classical situation where user-driven
application is much better than any application with the restricted system of
commands and this is one of the examples where user-driven application can
be the only one really good solution.
Let us look at the figure 21.31 once more. There are rectangles and buses.
Some buses connect the rectangles, some buses connect other buses, and there
are buses between buses and rectangles. The flexible buses allow to draw Fig.21.35 The case is not correct from
whatever you need, so let us start our work on a Family Tree application with the point of biology, but there
development of some flexible buses. is no more information
public class Bus : GraphicalObject
{
Form form;
Mover supervisor;
Pen m_pen;
bool bMarkJoints = true;
bool bMarkEnds = true;
Color clrEnds = Color .Red;
Color clrJoints = Color .White;
bool bRegisteredWithMover = true;
bool bUseWarningColor = true;
Pen penWarn = new Pen (Color .Magenta, 2);
List<PointF> pts = new List<PointF> ();
List<BusConnection> headsConnected = new List<BusConnection> ();
List<BusConnection> tailsConnected = new List<BusConnection> ();
Any bus consists of a set of straight segments connected into a chain one after another; there is at least one segment in each
bus and there is no upper limit on the number of segments. Each segment is described by two points and the end point of
the previous segment is the starting point of the next one. Thus, the minimal number of points for a bus is two and the
number of segments is always one less than the number of points. A bus can be initialized either by a List of points or
by a couple of points; in the last case the bus consists of a single segment.
public Bus (Form frm, Mover mvr, List<PointF> points, Pen pn)
{
form = frm;
supervisor = mvr;
m_pen = pn;
pts = points;
Movable = false;
}
Bus cover consists of small circular nodes on all the points from the List and of another set of strip nodes covering the
segments.
World of Movable Objects 769 (978) Chapter 21 Medley of examples
public override void DefineCover ()
{
int rad = Math .Max (4, Convert .ToInt32 (Width / 2));
int nCircles = pts .Count;
int nStrips = pts .Count - 1;
CoverNode [] nodes = new CoverNode [nCircles + nStrips];
for (int i = 0; i < nCircles; i++)
{
nodes [i] = new CoverNode (i, pts [i], rad);
}
for (int i = 0; i < nStrips; i++)
{
nodes [nCircles + i] = new CoverNode (nCircles + i, pts [i],
pts [i + 1], rad, Behaviour .Frozen);
}
cover = new Cover (nodes);
if (Movable)
{
cover .SetBehaviour (NodeShape .Circle, Behaviour .Frozen);
cover .SetBehaviour (NodeShape .Strip, Behaviour .Moveable);
}
}
By moving circular nodes, the length and angle of segments can be changed; in this way the bus configuration is changed.
Now is the time to start our design step by step from one preliminary example to another. They are all divided into three
groups. In the first group of examples only free buses are used; examples of the second group demonstrate connected buses;
in the third group the Person objects are used.
Free buses
File: Form_FreeBuses.cs
Menu position: Miscellaneous – FamilyTree design step by step – Free buses
The first preliminary example demonstrates two buses; one of them consists of a single segment while another has four
segments (figures 21.36).
private void OnLoad (object sender, EventArgs e)
{
… …
bus_2 = new Bus (this, mover, new PointF (100, 100), new PointF (300, 200),
new Pen (Color .Green, 3));
bus_5 = new Bus (this, mover,
Auxi_Geometry .RandomPointsInsideRectangle (ClientRectangle, 20, 5),
new Pen (Color .Blue, 3));
… …
Fig.21.36a Normally all free buses are shown with a special warning color Fig.21.36b Warning is switched OFF
These two buses are initialized with two different pens, but at first both buses are shown in some color which was not
declared for any of them (figure 21.36a). This is not a mistake – it is a normal situation for all free buses. A bus which is
not connected to anything and even a bus with one free end – these are abnormal situations for buses and such buses are
World of Movable Objects 770 (978) Chapter 21 Medley of examples
shown with a special warning color. This preliminary example includes a check box which allows to switch OFF this
warning and to see even free buses in those colors which were declared on their initialization. The use of warning color for
each bus is regulated via the Bus.UseWarningColor property. Several pages ahead I will demonstrate an auxiliary
tuning form for buses. This form contains a check box to regulate the use of warning color. In such preliminary example as
Form_FreeBuses.cs, the mandatory switch to normal colors can be useful and the view of buses at figure 21.36b is better
than at figure 21.36a, but in the real application such warning is very helpful and I would not recommend to switch it OFF.
Bus is a very simple object and has only three main parameters of visualization: color, width, and the line style; all three
parameters are packed into a single m_pen field. Two additional colors are used to mark the end points (clrEnds) and
the joints between the connected segments (clrJoints). Two Boolean parameters – bMarkEnds and bMarkJoints
– are used to switch the drawing of these special points ON and OFF.
public void Draw (Graphics grfx)
{
if (((bHeadConnected != bTailConnected) ||
(bHeadConnected == false && bTailConnected == false &&
headsConnected .Count == 0 && tailsConnected .Count == 0 &&
bRegisteredWithMover)) && bUseWarningColor)
{
grfx .DrawLines (penWarn, pts .ToArray ());
}
else
{
grfx .DrawLines (m_pen, pts .ToArray ());
}
if (bMarkJoints)
{
for (int i = 1; i < pts .Count - 1; i++)
{
Auxi_Drawing .FillCircle (grfx, pts [i], 3, clrJoints);
}
}
if (bMarkEnds)
{
Auxi_Drawing .FillCircle (grfx, pts [0], 3, clrEnds);
Auxi_Drawing .FillCircle (grfx, pts [pts .Count - 1], 3, clrEnds);
}
}
In the Form_FreeBuses.cs neither the visualization parameters nor the number of segments in the buses can be changed but
all these things are used in further examples.
The use of two additional colors to mark all movable points makes the change of bus configuration much easier, but there
are situations when such changes are not needed at all; in such case a bus can be shown without all additional marks. When
user is already familiar with the way of changing buses, these additional marks are also not needed. In the FamilyTree
application, there is an easy way to switch them ON or
OFF and user can do it at any moment.
File: Form_FreeBuses_AddingJoints.cs
Menu position: Miscellaneous
– FamilyTree design step by step
– Free buses; adding joints
There are two ways to change the bus configuration in
this example (figure 21.37): either to move one of
special points (end points and joints) or to press inside
the segment and move the new joint which appears at
the pressed point. Both things were already
demonstrated in the Form_PolylineUnmovable.cs
(figure 17.18).
When Form and Mover are among the parameters
of initialization for any object, then it is a clear
Fig.21.37 Marks show all special points
World of Movable Objects 771 (978) Chapter 21 Medley of examples
indication that the adhered mouse technique is going to be used. In case of a Bus class, the adhered mouse is used in both
situations when the existing circular node is pressed and when a new circular node must be added.
When an existing circular node is pressed, then the Bus.Press() method is called.
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 Bus)
{
Bus bus = grobj as Bus;
if (mover .CaughtNodeShape == NodeShape .Circle)
{
bus .Press (mover .CaughtNode);
}
… …
The only parameter of the Bus.Press() method is the number of the pressed node. I remind that the Bus cover starts
with the circular nodes, so in this case the node number is also the number of the point in the List<PointF>pts. Mouse
cursor is switched exactly to this point which is also the central point of the pressed node. Prior to this moment, the link
between mover and the bus was already established, so this link must be temporarily cut in order to move the cursor without
bus reaction.
public void Press (int iCircle)
{
AdjustCursorPosition (pts [iCircle]);
}
When a bus is pressed not on circular but on a strip node (somewhere inside segment), then situation is different.
• The nearest point on this segment is calculated.
• New joint is organized at this point.
• This new joint must be covered by the new circular node, so the bus is released by mover and the cover is
redefined.
• Bus is caught again by mover, but now cursor is inside the area of the new circular node, so the bus is caught by
this node.
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 Bus)
{
Bus bus = grobj as Bus;
if (mover .CaughtNodeShape == NodeShape .Circle)
{
… …
}
else
{
if (!bus .Movable)
{
World of Movable Objects 772 (978) Chapter 21 Medley of examples
int iSegment = mover .CaughtNode - bus .Points .Count;
PointF pt =
bus .NearestPointOnSegment (e .Location, iSegment);
mover .Release ();
bus .InsertPoint (iSegment + 1, pt);
Invalidate ();
mover .Catch (e .Location, e .Button);
}
}
… …
In both cases of already existing node and newly organized node, cursor is shifted to the node central point. After it and
until the moment of the mouse release, coordinates of the mouse cursor are used as the new location for the moved point.
This can be seen in the Bus.MoveNode() method. The use of the Bus.InformConnections() method inside
the Bus.MoveNode() method will be explained later when we start working with connected buses.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
if (iNode < pts .Count)
{
pts [iNode] = ptM;
InformConnections (PositionUpdate .Soft, iNode - 1, iNode);
bRet = true;
}
… …
As you can see from the OnMouseDown() method, such adding of new joints is allowed only for the non-movable
buses; the next example demonstrates the difference in behaviour of movable and non-movable buses.
File: Form_FreeBuses_ChangingMovability.cs
Menu position: Miscellaneous – FamilyTree design step by step – Free buses; changing movability
Figure 21.38 is a slightly changed figure 21.34 with additional marks for buses of different types. Even such a simple tree
of only several people demonstrates
buses which are used for different
types of connections; at
figure 21.38 they are marked with
different letters.
Case A Buses of this type are
used between spouses and
everyone understands that
such bus cannot be
moved freely to any other Fig.21.38 In this tree, there is only one bus with its ends not placed on other
location because it has to elements. This bus is marked with letter S (special).
show the link between
two people and its ends must be always connected to the rectangles representing these people. At the figure these
buses are straight. Each of them can be turned into a broken line with several joints, but at any moment it must
show a link between those two people.
Case B Short buses of this type connect one long bus with siblings. The rectangles for siblings can be arbitrarily placed
on the screen and the bus to any of them can be much longer and may need to be turned into a broken line, but
two ends of each bus are connected to some objects (one to a bus, another to rectangle), so such bus cannot be
moved freely around the screen.
Case C Bus of this type is used as connection between two buses, so its both ends are also placed on other elements and
this bus cannot be moved freely.
World of Movable Objects 773 (978) Chapter 21 Medley of examples
Case S This is the only bus which ends are not fixed on other objects; letter S for this bus is from the word special. To
demonstrate the main difference of this bus from all others, I moved the points at which other buses are connected
to this one (compare this view with figure 21.34). The ends of this bus are not fixed, so this bus can be moved
around without changing its view. This is the bus to which the siblings are connected; when you move this bus,
all connections move with it. At figure 21.38 this bus is placed at the same distance from rectangles belonging to
two generations (between parents and their children). If anybody would prefer to move this bus closer to the
rectangles of one generation or another, he can easily do it without destroying the family tree. Well, this would
be a good feature of design, but then there is a question of the way to do it because up till now the mouse press on
any bus allowed only to add the new joint and to move this joint.
In all the examples of this book the forward movement of a whole object or any part of an object is done by the left button
and I am not going to break this rule. In the previous example Form_FreeBuses_AddingJoints.cs the left button press at
any inner point of a segment adds a joint at the
pressed point and starts the process of moving this
new joint. Yet, we have a situation when, depending
on our wish, such left button press must start either
the adding and moving of the new joint or the move of
the whole bus without any change in its configuration.
The computer cannot screen user’s mind to determine
what type of action this user plans to do at one
moment or another, so some preliminary user’s action
must determine it. This preliminary action is the
change of the bus movability.
As you see from figure 21.38, a movable bus is some
kind of exception; usually buses are not movable, so
four pages back in the code of the bus constructor you
can find such line Fig.21.39 In this example the bus movability can be changed
through menu command
Movable = false;
Thus, any bus is not movable by default. The behaviour of circular nodes is not specified and it means that they get the
default value Behaviour.Moveable. The behaviour of strip nodes is set to Behaviour.Frozen. Together it means
that:
• You can press any circular node (any joint or end point) and move it around the screen.
• You can press any strip node because mover feels all the frozen nodes but you cannot move it. Instead, the new
joint is organized at the pressed point and, as any other joint, it is movable; this was already explained in the
previous example. This adding of the new joint is done inside the OnMouseDown() method but only if the
pressed bus is non-movable (look at the code two pages back).
The switch of bus movability does not change the number, shape, or order of nodes in the cover, but changes their
behaviour. For movable bus, there is a special addition at the end of the Bus.DefineCover() method.
public override void DefineCover ()
{
… …
if (Movable)
{
cover .SetBehaviour (NodeShape .Circle, Behaviour .Frozen);
cover .SetBehaviour (NodeShape .Strip, Behaviour .Moveable);
}
}
In the movable bus, all circular nodes are frozen while all strip nodes are movable. This means that there is no more way to
add new joints because it is allowed in the OnMouseDown() method only for non-movable buses. At the same time the
Bus.MoveNode() method works as usual.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
if (catcher == MouseButtons .Left)
{
if (Movable)
World of Movable Objects 774 (978) Chapter 21 Medley of examples
{
Move (dx, dy);
InformConnections (PositionUpdate .Soft, 0, pts .Count - 1);
}
… …
For a movable bus the MoveNode() method calls the Move() method in which all special points are moved
synchronously; as a result, the whole bus is moved without changing its configuration.
public override void Move (int dx, int dy)
{
SizeF size = new SizeF (dx, dy);
for (int i = 0; i < pts .Count; i++)
{
pts [i] += size;
}
}
In the Form_FreeBuses_ChangingMovability.cs (figure 21.39) the bus movability is changed through a single command
of the small context menu.
private void Click_miMovable (object sender, EventArgs e)
{
busPressed .Movable = !busPressed .Movable;
busPressed .MarkEnds = !busPressed .Movable;
busPressed .MarkJoints = !busPressed .Movable;
Invalidate ();
}
The reaction on the left button
press is absolutely different for
movable and non-movable buses
and it would be a real confusion
for users if the movable and non-
movable buses would look
identically. To avoid such Fig.21.40a Menu on unmovable bus Fig.21.40b Menu on movable bus
confusion, the view of the bus
slightly changes depending on its movability. For non-movable buses, the joints and end points have special marks
(figure 21.40a); this makes the catching and moving of these special points easier. There are no special marks on movable
buses (figure 21.40b); the whole bus is painted with one color; there are no visible special points and this emphasizes that
there are no special points to change configuration, so the whole bus can be moved as one piece.
In the real Family Tree application more changes in view and configuration of buses can be needed; these possible changes
are tested in the next example.
File: Form_FreeBuses_AllPossibleChanges.cs
Menu position: Miscellaneous – FamilyTree design step by step – Free buses with all possible changes
General view of the Form_FreeBuses_AllPossibleChanges.cs is the same as in two previous examples. The main
difference is in using two menus which can be called on different parts of buses. You see these menus when you press on
joints or anywhere else on segments, but at any moment you can see only one of them. There is no way to see them
simultaneously, so figure 21.41 is a bit artificial, but I think that such view makes more obvious all the possibilities which
are provided by those menus. As usual, the menu to be called is determined in the OnMouseUp() method of the form.
In the current example both menus are called on objects of the same class, so the menu selection is based on the shape of the
pressed node. At the same moment the number of the pressed joint or segment is determined by the number of the pressed
node.
private void OnMouseUp (object sender, MouseEventArgs e)
{
int iWasObject, iWasNode;
NodeShape shapeNode;
ptMouse_Up = e .Location;
if (mover .Release (out iWasObject, out iWasNode, out shapeNode))
{
World of Movable Objects 775 (978) Chapter 21 Medley of examples
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Right && grobj is Bus &&
Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up) <= 3)
{
busPressed = grobj as Bus;
int nPoints = busPressed .Points .Count;
if (shapeNode == NodeShape .Strip)
{
iPressedSegment = iWasNode - nPoints;
ContextMenuStrip = menuOnSegment;
}
else
{
if (0 < iWasNode && iWasNode < nPoints - 1)
{
iPressedPoint = iWasNode;
ContextMenuStrip = menuOnJoint;
}
}
}
}
}
Menu on joints (menuOnJoint) contains a single
command to delete the pressed joint; when executed,
it allows to straighten the bus without moving the
joints manually. In this
Form_FreeBuses_AllPossibleChanges.cs the
removal of the joints is allowed both for movable and
non-movable buses, but there are some doubts about
the correctness of applying this command to the
movable buses. When the bus is declared movable,
then it can be moved around the screen without any
changes in view and the possibility of adding new
joints is also eliminated. Two features of a bus – the
movability and the possibility of its reconfiguring – to
some extent associate with each other and there can be Fig.21.41 Menus are called on joints and segments.
different views on the rigidity of their correlation.
• If you look at the change of the bus movability only as the chance to move the whole object, then the possibility to
erase the joints of such bus is correct.
• If you think that for movable buses all possibilities of changing their configuration must be blocked, then this
command for movable buses must be disabled.
Anyway, if you prefer the second point of view, then you can add a couple
of code lines into the OnMouseUp() method and allow to call the
menuOnJoint only for the non-movable buses.
Menu which is called on segments has several commands (bigger menu at
figure 21.41).
Movable (bus) allows to switch the movability of the pressed bus
between ON and OFF. The change of movability
slightly changes the view of the bus as was explained in
the previous example.
Tuning (bus) calls an additional tuning form to modify the view of the
pressed bus. The Form_Tuning_Bus.cs (figure 21.42)
allows to change the color, width, and style
(DashStyle) of the bus, to change colors for end
points and joints, and to regulate the use of warning
color. Fig.21.42 Auxiliary form for bus tuning
World of Movable Objects 776 (978) Chapter 21 Medley of examples
Remove all joints (inside bus) erases all joints and leaves the bus consisting of a single segment between the end points.
Horizontal (segment) changes the angle of the pressed segment and turns it into horizontal line.
Vertical (segment) changes the angle of the pressed segment and turns it into vertical line.
Two last commands work in similar way and have similar limitations of their use.
• Both ends of a bus look similar and it is impossible to determine visually from which of them the order of points
starts, but all special points are placed in the List<PointF>pts in some order and these commands move the
point with the bigger number. This is correct for any segment except the last one for which the point with the
lesser number is moved.
• If the pressed segment is nearly horizontal and you order to turn it into a vertical line, then its end points get the
same X coordinate and the new segment will be very short. If you apply the same command to the horizontal
segment, it will turn into a single point and disappear from view. To avoid such situations; the command is
allowed only for segments which will turn into a new segment with the length not less than 10 pixels. Similar
limitation works for another command of the pair.
Up till now we were looking at the free buses which were not connected to anything. In the real family tree we do not have
absolutely free buses; any bus is connected to one or several objects. Figure 21.31 shows that buses are either connected to
each other or can be used as the links between the rectangles representing people. We will discuss those special rectangles a
bit later and now let us work on the problem of connected buses.
Connected buses
Buses are not connected to each other by the arbitrary points. On the contrary, there is only one way to organize any bus
connection and there are strict rules on how this connection works after it. The only way to organize a connection between
two buses is to move the end point of one bus and to fix it in some way on another bus. From this moment the end point of
the first bus can move only along the second bus using it as a rail. It does not matter whether the second bus is a straight
line or consists of several segments; the connected end of the first bus can move along the full length of the second bus
between its end points.
Connection between two buses is described by the BusConnection class.
public class BusConnection : GraphicalObject
{
Form form;
Mover supervisor;
PointF pt; // connection point
int m_radius; // node radius
Bus m_busOfEnd; // bus connected by its end
EndType end_type; // type of end point
Bus m_busRail; // bus to which the end point is connected (rail)
int iSegment; // rail segment
double coefOnSegment; // positioning coefficient on this segment
An object of the BusConnection class is organized at the end point of the bus which is going to be connected to
another bus. To organize a new connection, some information about both buses is needed. This information includes the
connection point (pt), radius of sensitive area around the connection point (m_radius), bus which is connected by its end
(m_busOfEnd), additional value which specifies the connected end (end_type), and the bus to which the first one is
connected and along which the connected end can move (m_busRail).
public BusConnection (Form frm, Mover mvr, PointF point, int rad, Bus bus_End,
EndType endType, Bus bus_Rail)
{
form = frm;
supervisor = mvr;
m_radius = Math .Max (Math .Abs (rad), 3);
m_busOfEnd = bus_End;
end_type = endType;
m_busRail = bus_Rail;
… …
World of Movable Objects 777 (978) Chapter 21 Medley of examples
pt = Auxi_Geometry .NearestPointOnPolyline (point, m_busRail .Points,
out dist, out iSegment, out coefOnSegment);
}
The suggested connection point passed as a parameter can be not very accurate; you can pass even an arbitrary point as a
parameter. The real point of connection – the point on the bus which is the closest to the suggested one (pt) – is calculated
in the constructor. The Auxi_Geometry.NearestPointOnPolyline() method returns not only the exact point
of connection, but also the segment of connection (iSegment) and the positioning coefficient for the point of connection
on this segment (coefOnSegment). As we are talking about the connection point, then its cover design is simple and
obvious: cover consists of a single small circular node.
public override void DefineCover ()
{
cover = new Cover (new CoverNode (0, pt, m_radius));
}
BusConnection class has one nearly unique feature: these objects are very useful but they are invisible. There are no
parameters of visibility and there is no Draw() method. All my work is aimed at moving screen objects, so it is all about
changing the position and sizes of the objects which you see. The mentioned feature is nearly unique because I can
remember only one other class of invisible objects used in my examples. While writing about the rectangular plots, I
explained the use of the RectCorners class included into the MoveGraphLibrary.dll, but there is one big difference
in using of these two classes.
• An object of the RectCorners class is associated with some rectangular plot and is used as one of the fields of
that complex object; in this way it appears inside the Plot and BarChart objects.
• An object of the BusConnection class is associated with the end point of some bus but it is not among the
fields of that bus. Each BusConnection object contains information about two buses: the one with the end
point of which it is associated and another one to which the first one is connected. Every form in which buses are
used has a single List for all the connections
used in this form.
We had a set of four small examples to look at the free
buses; next is a similar set of four examples to explore
all features of bus connections.
File: Form_ConnectedBuses_Two.cs
Menu position: Miscellaneous
– FamilyTree design step by step
– Two connected buses
Even the name of this example informs that there are
two connected buses in the
Form_ConnectedBuses_Two.cs (figure 21.43). First
the green bus is organized, then the blue one.
public Form_ConnectedBuses_Two ()
{ Fig.21.43 Two connected buses
… …
busGreen = new Bus (this, mover, points, new Pen (Color .Green, 3));
… …
busBlue = new Bus (this, mover, points, new Pen (Color .Blue, 3));
… …
These buses do not know anything about each other. Without any special addition, they behave like absolutely independent
objects; such example was demonstrated several pages back.
Two things are needed to organize a connection between these buses.
• While the blue bus was designed, its starting point got coordinates exactly on the green bus (pt). Now a
BusConnection object is organized at this end of the blue bus.
con = new BusConnection (this, mover, pt, busBlue .NodeRadius, busBlue,
EndType .Head, busGreen);
World of Movable Objects 778 (978) Chapter 21 Medley of examples
any bus – head and tail – are visually indistinguishable, but one parameter of the BusConnection constructor specifies
the covered end, so there is no problem to move the correct bus end to the new point of bus connection. Thus, regardless of
any movement, the BusConnection object covers this end of the blue bus.
public PointF Location
{
get { return (pt); }
set
{
pt = Auxi_Geometry .NearestPointOnPolyline (value, busRail .Points,
out dist, out iSegment, out coefOnSegment);
DefineCover ();
if (end_type == EndType .Head)
{
m_busOfEnd .HeadPoint = pt;
}
else
{
m_busOfEnd .TailPoint = pt;
}
}
}
The BusConnection.Location property moves the bus connection and the bus end to one point; after it the
BusConnection.MoveNode() method moves mouse cursor to the same point. For this cursor movement, there is
again a temporary cut of the link between mover and the caught object (look at the BusConnection.MoveNode()
method on the previous page).
In this way the point of connection is moved only along the green bus and the connected end of the blue bus moves with this
point. But what happens when the configuration of the green bus is changed? Special points of the green bus can be moved
at any moment. How the end of the blue bus keeps its connection to the green bus throughout those changes?
Two pages back I have mentioned that after the connection at the end of the blue bus was organized, the information about
this connection was passed to the bus with which it was connected (the green one).
busGreen .AddConnection (con);
Any bus keeps the track of all its “side” connections. There are even not one but two separate Lists for connections by
heads and tails.
public class Bus : GraphicalObject
{
List<BusConnection> headsConnected = new List<BusConnection> ();
List<BusConnection> tailsConnected = new List<BusConnection> ();
The Bus.AddConnection() method adds a new element to the appropriate List.
public void AddConnection (BusConnection cnct)
{
if (cnct .ConnectionType == EndType .Head)
{
headsConnected .Add (cnct);
}
else
{
tailsConnected .Add (cnct);
}
}
When the green bus of four points is organized,
those points are defined from left to right; their
numbers in the List of points are shown at
figure 21.44. Green bus consists of three segments;
the blue bus is connected at segment one between
Fig.21.44 Points of the green bus are numbered from left to right
World of Movable Objects 780 (978) Chapter 21 Medley of examples
points 1 and 2. Now let us press and move some special point of the green bus. When any node of the bus is pressed and
moved, then the Bus.MoveNode() method is called. Just now we are interested only in that part of the method which
is responsible for moving of special points. All special points are covered by circular nodes which are the first in the bus
cover, so the number of the moved circular node is equal to the number of the corresponding special point.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
… …
if (iNode < pts .Count)
{
pts [iNode] = ptM;
InformConnections (PositionUpdate .Soft, iNode - 1, iNode);
bRet = true;
}
… …
When any bus is moved or reconfigured, some or all of the connections on this bus must adjust their positions. There exist
two types of adjustment which are defined by the enumeration PositionUpdate.
• Imagine the situation (it is real, but we did not look at such case yet) when the bus is moved without any change of
its configuration and all the connections must retain their relative positions. This means that each connection is
still positioned on the same segment and with the same positioning coefficient along the segment. Thus, only the
absolute screen coordinates of connection are changed because the bus has moved; such change of position is
described as PositionUpdate.Soft.
• Another situation happens when all the joints of a bus are deleted; the bus turns into a straight line, and all the
connections have to be positioned on this new bus. Certainly, there are no questions of keeping the connection to
the same segment with the same positioning coefficient. Both values must be recalculated and this situation is
described as PositionUpdate.Hard.
When the whole bus is moved, then all its connections must be relocated. When the end point is moved, then only one
segment is affected. When any joint is moved, then only the segments on both sides of this joint are changed and only the
connections on these segments must be relocated; all other connections behind the neighbouring joints are not disturbed at
all. Thus, it would be enough to inform about the movement of any joint only the connections on the neighbouring
segments, but another logic is used in the Bus class. As seen from the code of the Bus.InformConnections()
method, all connections to the bus are informed, so it is for each connection to decide whether it has to react to this
particular change or not.
public void InformConnections (PositionUpdate updateType,
int iSeg_0, int iSeg_1)
{
foreach (BusConnection connection in headsConnected)
{
connection .RailChanged (updateType, iSeg_0, iSeg_1);
}
foreach (BusConnection connection in tailsConnected)
{
connection .RailChanged (updateType, iSeg_0, iSeg_1);
}
}
Parameters that are passed to the Bus.InformConnections() method include the type of adjustment
(updateType) and the range of the affected segments (from iSeg_0 to iSeg_1). The same set of parameters is used
while applying the BusConnection.RailChanged() method to each of the connections. This method has to
calculate the new position of connection in reaction to the change of the bus on which it is positioned; any calculation is
needed only if the particular connection is situated on a segment inside the specified range. If the connection has to be
moved, then its new position depends on the type of the adjustment.
• If it is the PositionUpdate.Soft adjustment, then the number of the segment and the positioning
coefficient on this segment retain their values; the new point is calculated using these unchanged values.
World of Movable Objects 781 (978) Chapter 21 Medley of examples
• For the case of the PositionUpdate.Hard adjustment, the new position is calculated by finding the nearest
point on the whole bus; for this calculation only the bus points are needed. As a side effect of this calculation, new
segment number and the positioning coefficient on this segment are determined.
public void RailChanged (PositionUpdate updateType, int iSeg_0, int iSeg_1)
{
if (iSeg_0 <= iSegment && iSegment <= iSeg_1)
{
PointF ptNew;
if (updateType == PositionUpdate .Soft)
{
ptNew = Auxi_Geometry .PointOnLine (m_busRail .Points [iSegment],
m_busRail .Points [iSegment + 1], coefOnSegment);
}
else
{
ptNew = Auxi_Geometry .NearestPointOnPolyline (pt,
m_busRail .Points, out dist, out iSegment, out coefOnSegment);
}
Location = ptNew;
}
}
The calculated position is passed to the BusConnection.Location property where two things happen: the
connection is relocated to this new position and then this value is sent to one or another property of the connected bus; the
exact property depends on whether that bus is connected by the first or by the last of its points. (Code of the
BusConnection.Location property can be seen three pages back.)
Suppose that we have the case of connection when the bus is connected by the head; this is our case of the blue bus
connected by the head. In this case the moved connection sends the new point value to the Bus.HeadPoint property of
the connected bus and the head point of that bus is changed.
public PointF HeadPoint
{
get { return (pts [0]); }
set { ChangePoint (0, value); }
}
Instead of the direct change of pts[0], you see the call of the Bus.ChangePoint() method because the change of
the head point of connected bus may be not enough. If there are some connections on that second bus, then the
Bus.ChangePoint() method not only relocates the first point of the bus but also disturbs all the connections;
eventually this disturbance will die somewhere, but on the way all the involved connections and bus ends will take their new
positions.
public void ChangePoint (int iPoint, PointF pt)
{
if (0 <= iPoint && iPoint < pts .Count)
{
pts .RemoveAt (iPoint);
pts .Insert (iPoint, pt);
DefineCover ();
InformConnections (PositionUpdate .Soft, iPoint - 1, iPoint);
}
}
Now we have the full picture of what happens when one or another special point of the green bus is caught and moved. In
situation from figure 21.44 there are going to be different results when the second from the left (number 1) or the right point
(number 3) of the green bus are moved. Let us look at the details of both cases.
If the second from the left point of the green bus is moved, then:
1. The mouse is moved somewhere (ptM). By the Bus.MoveNode() method, the caught joint ( i == 1 ) is
moved to the same point as the mouse. All connections of the green bus are informed that two segments of the bus
have changed.
World of Movable Objects 782 (978) Chapter 21 Medley of examples
pts [1] = ptM;
InformConnections (PositionUpdate .Soft, 0, 1);
2. The green bus has only one connection and for it the BusConnection.RailChanged() method is called
with the same parameters.
connection .RailChanged (PositionUpdate .Soft, 0, 1);
3. Our connection happens to be inside the specified range of segments ( iSegment == 1 ), so the new position
for connection is calculated as a soft adjustment and then this new position is passed to the
BusConnection.Location property.
ptNew = Auxi_Geometry .PointOnLine (busRail .Points [1],
busRail .Points [2], coefOnSegment);
Location = ptNew;
4. The BusConnection.Location property changes the real location of the connection point and sends this
new location to the blue bus to be used in the Bus.HeadPoint property.
pt = Auxi_Geometry .NearestPointOnPolyline (value, busRail .Points,
out dist, out iSegment, out coefOnSegment);
m_busOfEnd .HeadPoint = pt;
5. The Bus.HeadPoint property changes the position of the head point of the blue bus and then the disturbance
is over because there are no connections on the blue bus.
When the right point of the green bus is moved, then the chain of events is shorter:
1. The mouse is moved somewhere (ptM). By the Bus.MoveNode() method the caught joint ( i == 3 ) is
moved to the same point as the mouse. All connections of the green bus are informed that two segments of the bus
have changed.
pts [3] = ptM;
InformConnections (PositionUpdate .Soft, 2, 3);
2. The green bus has only one connection and for it the BusConnection.RailChanged() method is called
with the same parameters.
connection .RailChanged (PositionUpdate .Soft, 2, 3);
3. The connection on the green bus is outside the specified range ( iSegment == 1 ), so nothing has to be done.
In situation from figure 21.44, the movement of the right point of the green bus has no effect on the shown
connection.
We already saw that whenever an end point or any joint of a bus is moved, the Bus.InformConnections() method
must be called. Next examples will demonstrate several situations when this method must be called and there is a very
simple logic to determine the set of needed parameters for such call.
• If some special point is moved by a mouse, then the relative position of any connection on that bus is not changed;
this means the use of the PositionUpdate.Soft parameter. As a result of the mentioned movement, not
more than two consecutive segments are affected, so two consecutive numbers represent the range of changing
segments. For an end point only one segment is changed, but for the simplicity of the code the range of segments
is still represented by two consecutive numbers. One of the mentioned segments does not exist, but this is not a
problem at all as no connection can be positioned on a non-existing segment, so no action will be required (and
done) for this non-existing segment.
• If the number of joints is changed, then it is always the PositionUpdate.Hard parameter and all segments
must be included into the range of affected segments.
One of the cases that fall into second variant is the addition of new joint when any segment is pressed with the left button; in
this case the call to the Bus.InformConnections() method is included into the OnMouseDown() method.
File: Form_ConnectedBuses_Three.cs
Menu position: Miscellaneous – FamilyTree design step by step – Three connected buses
In the next example – the Form_ConnectedBuses_Three.cs – we have three buses and one of them is connected to others
on both ends (figure 21.45). This example allows to check the correctness of work for connections on both ends of a bus
while everything else is nearly the same as in the previous example.
World of Movable Objects 783 (978) Chapter 21 Medley of examples
When a bus is turned into movable, not only its view changes, but its cover is also changed. The number, order, and shape
of all nodes do not change, but their behaviour changes.
public override void DefineCover ()
{
… …
if (Movable)
{
cover .SetBehaviour (NodeShape .Circle, Behaviour .Frozen);
cover .SetBehaviour (NodeShape .Strip, Behaviour .Moveable);
}
}
World of Movable Objects 785 (978) Chapter 21 Medley of examples
Movable bus has no marks on special points. Circular and strip nodes have the same default cursor (Cursors.Hand), so
you can press nodes of one or another shape.
Circular nodes occupy a tiny percent of the bus area and you must be really lucky to press at one of them. If you press a
circular node of the movable bus, then with a high probability you tried to do it on purpose. Just to see, what will happen in
such case. You might be disappointed because nothing will happen. It is impossible to catch and move the frozen node.
If you do not aim at the end point or any joint, then with a very high probability you will press the strip node. Any strip
node of movable bus is movable (Behaviour.Moveable), so the Bus.MoveNode() method is called. Though the
node number is detected by mover and is sent to this method among the parameters, you can see from the code that in this
case the number of the node does not matter at all.
public override bool MoveNode (int iNode, int dx, int dy, Point ptM,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (Movable)
{
Move (dx, dy);
InformConnections (PositionUpdate .Soft, 0, pts .Count - 1);
}
… …
We were looking at the same part of the Bus.MoveNode() method while playing with movability of free buses, so we
already know that this MoveNode() method calls the Bus.Move() method and all special points of the bus are moved
synchronously. For the free bus it was all, but now we have connections and there is also a call for the
InformConnections() method. All connections to the moved bus are informed about the change of points
throughout the InformConnections() method and in this case the range of changed segments includes all segments.
This means that all the connections will adjust their positions and with them the end points of all the connected buses.
At the same time the type of adjustment is PositionUpdate.Soft; this type was already used in the previous examples
when one or another special point was moved. With this type of adjustment, all points of connection retain the numbers of
segments to which they are connected and positioning coefficients inside those segments.
File: Form_ConnectedBuses_AllPossibleChanges.cs
Menu position: Miscellaneous – FamilyTree design step by step – Connected buses with all possible changes
This example demonstrates all possible changes that can be applied to connected buses. The system of buses is the same as
in previous examples; allowed changes are the same as were demonstrated for free buses in the
Form_FreeBuses_AllPossibleChanges.cs (figure 21.41). To start all allowed changes, two context menus are used
(figure 21.47).
While writing about the commands to turn the
pressed segment into vertical or horizontal, I
mentioned some limitations but I forgot to
mention one thing. These two commands – to
turn a segment into vertical or horizontal – can
move, if other limitations allow, one of the
joints of a bus but they never move end points.
Because of this limitation, the logic of these
commands for the last segment of a bus slightly
differs from the logic of applying these
commands to any other segment. This
restriction on moving any end point by these
two commands also do not allow to apply these
commands to any bus consisting of a single
segment.
I think that the buses to be used in the Family
Tree are now developed and tested. It is time to Fig.21.47 Connected buses with all possible changes
look at other important elements of any family tree – at rectangles representing people.
World of Movable Objects 786 (978) Chapter 21 Medley of examples
The bus at figure 21.51 is connected to a Person object on one end while its other end is free. For the real family tree it
is not normal situation; you cannot imagine a bus that connects something with nothing; it is absurd. If this happens in the
real Family Tree application, then user must be informed about this unusual situation. The best way to attract user’s
attention to something unusual is to change the color of an element; it will be a warning that some action is needed from
user. For this reason, there is a special penWarn field in the Bus object. By default, this pen for a bus in unusual
situation has the magenta color.
Pen penWarn = new Pen (Color .Magenta, 2);
There are situations when the bus, instead of its real color, is painted with this special pen.
public void Draw (Graphics grfx)
{
if (((bHeadConnected != bTailConnected) ||
(bHeadConnected == false && bTailConnected == false &&
headsConnected .Count == 0 && tailsConnected .Count == 0 &&
bRegisteredWithMover)) && bUseWarningColor)
{
grfx .DrawLines (penWarn, pts .ToArray ());
}
else
{
grfx .DrawLines (m_pen, pts .ToArray ());
}
… …
If you do not want to see this special color in any situation, it is enough to change the warning color and make it the same as
the normal color of the bus. This was already done in one of the previous examples – in the
Form_ConnectedBuses_Two.cs. In that example the blue bus is connected only on one end and thus has to appear as
magenta but its warning color is changed artificially. As the result, the bus which is declared as blue is seen as blue and not
as magenta (figure 21.43).
public Form_ConnectedBuses_Two ()
{
… …
busBlue = new Bus (this, mover, points, new Pen (Color .Blue, 3));
busBlue .ColorWarning = busBlue .Color;
… …
There is also another way to regulate the use of warning color; it can be switched ON / OFF by the
Bus.UseWarningColor property. I will show the use of this property further on.
File: Form_Spouses.cs
Menu position: Miscellaneous – FamilyTree design step by step – Spouses
The next example takes us closer
to our goal of family tree design
and represents a very simple tree
consisting of three Person
objects and two buses
(figure 21.52). Regardless of the
tree complexity, it is enough to
have three Lists for three
classes and all work with these
Lists can be checked and
demonstrated even at such simple
family tree.
Fig.21.52 A primitive family tree containing only three people
List<Person> people = new List<Person> ();
List<Bus> buses = new List<Bus> ();
List<BusConnection> connections = new List<BusConnection> ();
World of Movable Objects 792 (978) Chapter 21 Medley of examples
private void OnLoad (object sender, EventArgs e)
{
… …
Person prsnWife_1 = new Person (this, new RectangleF (80, 60, 180, 60),
Color .Cyan, "Sura-Leya", "1865", "1894");
Person prsnHusband = new Person (this, new RectangleF (320, 60, 180, 60),
Color .Yellow, "Joseph", "1862", "1939");
Person prsnWife_2 = new Person (this, new RectangleF (600, 100, 180, 60),
Color .Cyan, "Gesya", "1881", "1957");
AddPerson (prsnWife_1);
AddPerson (prsnHusband);
AddPerson (prsnWife_2);
Bus bus = LinkPeople (this, mover, new Pen (Color .Blue, 3), prsnWife_1,
Side .E, 0.4, prsnHusband, Side .W, 0.4);
AddBus (bus);
bus = LinkPeople (this, mover, new Pen (Color .Blue, 3), prsnHusband,
Side .E, 0.5, prsnWife_2, Side .W, 0.5);
AddBus (bus);
… …
Direct connection between two Person objects is used very often in design of family trees, so I wrote special method
LinkPeople() which can be very useful in such cases. Do not forget that there is not only a short bus between two
Person objects but there are also the new BusConnection elements on both ends of the new bus.
In this LinkPeople() method, the initial position of the connection point is determined by the side of rectangle and by
the positioning coefficient along this side. This coefficient takes a value from the [0, 1] range. For horizontal sides, it
increases from left to right; for vertical sides, it increases from top to bottom.
private Bus LinkPeople (Form frm, Mover mvr, Pen pn,
Person personFrom, Side side_From, double coef_From,
Person personTo, Side side_To, double coef_To)
{
Bus busFrom = personFrom .Bus;
PointF ptA = Auxi_Geometry .PointOnRectangleBorder (personFrom .Area,
side_From, coef_From);
ptA = Auxi_Geometry .NearestPointOnPolyline (ptA, busFrom .Points);
Bus busTo = personTo .Bus;
PointF ptB = Auxi_Geometry .PointOnRectangleBorder (personTo .Area,
side_To, coef_To);
ptB = Auxi_Geometry .NearestPointOnPolyline (ptB, busTo .Points);
Bus bs = new Bus (frm, mvr, ptA, ptB, pn);
List<BusConnection> conecs = new List<BusConnection> ();
conecs .Add (new BusConnection (frm, mvr, ptA, bs .NodeRadius, bs,
EndType .Head, busFrom));
conecs .Add (new BusConnection (frm, mvr, ptB, bs .NodeRadius, bs,
EndType .Tail, busTo));
busFrom .AddConnection (conecs [0]);
busTo .AddConnection (conecs [1]);
connections .AddRange (conecs);
return (bs);
}
There is no bus tuning in this example. For Person objects only fonts and colors can be modified but not texts; the call
for tuning form includes an additional parameter which blocks the texts editing.
Form_Tuning_Person form = new Form_Tuning_Person (pt, strTitle, personSrc,
false);
Procedures to save and restore family tree do not depend on the size of a tree and they are explained with the next example.
World of Movable Objects 793 (978) Chapter 21 Medley of examples
File: Form_TwoGenerations.cs
Menu position: Miscellaneous – FamilyTree design step by step – Two generations
This is going to be the last preliminary
example before design of a full size
Family Tree application. In reality, this
Form_TwoGenerations.cs is not an
example to demonstrate one or another
feature but the real, though a very small
one family tree with only one limitation:
after this small family tree is constructed,
there are no commands to add new people
into it. Figure 21.53 shows that there are
only four people from two generations.
Though this family tree is very small, it
represents all possible types of buses.
This classification of buses is based on
whether their ends are connected to any
objects and if they are, then to what types
(classes) of objects they are connected.
For better explanation I use different
colors for buses of different types. At
figure 21.38 the same four types of buses
were marked by letters A, B, C, and S. I Fig.21.53 A small family tree for two generations
am not going to use those letters any
more, but the order of definitions in the new explanation is the same as in the old one.
• Bus between two Person objects; at figure 21.53 it is a blue one. In reality this bus is connected to the
invisible buses on borders of those Person objects, but visually it is a bus between two people.
• Bus between a bus and a Person object; at figure 21.53 there are two green buses of this type. In reality each
of them is between two buses, but one of them is hidden and is placed along the border of a Person object, so it
looks like a link between bus and person.
• Bus between two buses; at figure 21.53 it is a gray one.
• Bus without connections at the ends; at figure 21.53 it is a brown one. At the figure the ends of the brown bus and
the ends of the green buses are marked with the same small red spots; they are also positioned at the same place, so
only the code can show the order of their design and the organization of their connection. But if you press this red
spot with the left button, you will immediately see that this pressed spot (connection) can move only along the
brown bus taking the end of the green bus with the mouse. At the same time you will see that the end of the brown
bus is free and is not connected to anything.
Any family tree, even such simple one, can be constructed in different ways and the type of some buses (in the mentioned
classification) can depend on the design order.
Design of this tree starts with two Person objects which represent parents and with the bus between them; this bus is
constructed with the help of the LinkPeople() method which was used in the previous example.
private void DefaultView ()
{
Person father = new Person (this, new RectangleF (140, 60, 150, 60),
Color .Cyan, "James", "1916", "1995");
AddPerson (father);
Person mother = new Person (this, new RectangleF (380, 60, 150, 60),
Color .LightPink, "Joan", "1919", "1999");
AddPerson (mother);
Bus busParents = LinkPeople (this, mover, new Pen (Color .Blue, 3),
father, Side .E, 0.5, mother, Side .W, 0.5);
AddBus (busParents);
… …
World of Movable Objects 794 (978) Chapter 21 Medley of examples
At the next step two more Person objects are designed; they represent children. A bus above them (the brown one) is
initialized, but all links to this bus will be added a bit later.
private void DefaultView ()
{
… …
float cyChildren = father .Area .Bottom + 100;
Person son = new Person (this,
new RectangleF (father .Area .Left - 80, cyChildren, 160, 60),
Color .Cyan, "James", "Feb-1943", "");
Person daughter = new Person (this,
new RectangleF (mother .Area .Left + 80, son .Area .Top, 150, 60),
Color .LightPink, "Rosemary", "May-1947", "");
AddPerson (son);
AddPerson (daughter);
float cyAbove = (father .Area .Bottom + son .Area .Top) / 2;
float cxL = Auxi_Geometry .Middle (son .Area) .X;
float cxR = Auxi_Geometry .Middle (daughter .Area) .X;
Bus busAbove = new Bus (this, mover, new PointF (cxL, cyAbove),
new PointF (cxR, cyAbove), new Pen (Color .Brown, 3));
AddBus (busAbove);
… …
Now it is time to connect “children” with the bus above them (the brown bus).
private void DefaultView ()
{
… …
AddBus (LinkBusPerson (this, mover, new Pen (Color .Green, 3), busAbove,
busAbove .HeadPoint, son, Side .N, 0.5));
AddBus (LinkBusPerson (this, mover, new Pen (Color .Green, 3), busAbove,
busAbove .TailPoint, daughter, Side .N, 0.5));
… …
These two green buses are organized with the LinkBusPerson() method. This method organizes a
BusConnection on each end of the new bus and the exact location of the connection is calculated inside this method.
The point of connection on another bus is passed as a parameter, but this is only a preferable point; the calculation of the
exact point is done inside the LinkBusPerson() method. The connection point on the border of the Person object
is described by the side and the positioning coefficient along this side; this is done in exactly the same way as in the
LinkPeople() method.
private Bus LinkBusPerson (Form frm, Mover mvr, Pen pn, Bus busFrom,
PointF ptFrom, Person personTo, Side side_To, double coef_To)
{
ptFrom = Auxi_Geometry .NearestPointOnPolyline (ptFrom, busFrom .Points);
Bus busTo = personTo .Bus;
PointF ptTo = Auxi_Geometry .PointOnRectangleBorder (personTo .Area,
side_To, coef_To);
ptTo = Auxi_Geometry .NearestPointOnPolyline (ptTo, busTo .Points);
Bus bs = new Bus (frm, mvr, ptFrom, ptTo, pn);
List<BusConnection> conecs = new List<BusConnection> ();
conecs .Add (new BusConnection (frm, mvr, ptFrom, bs .NodeRadius,
bs, EndType .Head, busFrom));
conecs .Add (new BusConnection (frm, mvr, ptTo, bs .NodeRadius,
bs, EndType .Tail, busTo));
busFrom .AddConnection (conecs [0]);
busTo .AddConnection (conecs [1]);
connections .AddRange (conecs);
return (bs);
}
The last step in design of this small family tree is the construction of the link between two generations – the gray bus.
World of Movable Objects 795 (978) Chapter 21 Medley of examples
private void DefaultView ()
{
… …
PointF pt =
Auxi_Geometry .Middle (busParents .HeadPoint, busParents .TailPoint);
AddBus (LinkBusBus (this, mover, new Pen (Color .DarkGray, 3), busParents,
pt, busAbove, new PointF (pt .X, busAbove .TailPoint .Y)));
… …
}
The link between two buses is constructed by the LinkBusBus() method. Two points passed as parameters to this
method are again the preferable locations while the real points of connections are calculated inside the method.
private Bus LinkBusBus (Form frm, Mover mvr, Pen pn,
Bus busFrom, PointF ptFrom, Bus busTo, PointF ptTo)
{
ptFrom = Auxi_Geometry .NearestPointOnPolyline (ptFrom, busFrom .Points);
ptTo = Auxi_Geometry .NearestPointOnPolyline (ptTo, busTo .Points);
Bus bs = new Bus (frm, mvr, ptFrom, ptTo, pn);
List<BusConnection> conecs = new List<BusConnection> ();
conecs .Add (new BusConnection (frm, mvr, ptFrom, bs .NodeRadius,
bs, EndType .Head, busFrom));
conecs .Add (new BusConnection (frm, mvr, ptTo, bs .NodeRadius,
bs, EndType .Tail, busTo));
busFrom .AddConnection (conecs [0]);
busTo .AddConnection (conecs [1]);
connections .AddRange (conecs);
return (bs);
}
When the design of this small family tree is over, there are such collections of elements.
• List<Person> people contains four elements.
• List<Bus> buses contains nine elements. Five of them are shown at figure 21.53
in different colors while four other buses are invisible and go
along the borders of the Person objects.
• List<BusConnection> connections contains eight elements. All of them are shown at figure 21.53
as small red spots.
Once again about the order of elements in the mover queue which is defined by the RenewMover() method.
private void RenewMover ()
{
mover .Clear ();
foreach (Person person in people)
{
person .IntoMover (mover, 0);
}
foreach (Bus bus in buses)
{
bus .IntoMover (mover, 0);
}
foreach (BusConnection con in connections)
{
con .IntoMover (mover, 0);
}
info .IntoMover (mover, 0);
}
As seen from the above code, first go all the BusConnection elements, then all Bus elements, and at the end all the
Person elements. The BusConnection elements must be at the head of the queue; otherwise something else can
close the connection points from mover and it will be impossible to move connection points. The order of Bus and
World of Movable Objects 796 (978) Chapter 21 Medley of examples
Person objects is not so important and can be changed. The order of elements in the mover queue correlates with the
order of their drawing, so all depends on what you prefer to see in the situation when a bus goes across the rectangle of a
Person object. If you prefer to see a bus over any Person object and to have an opportunity to press the bus and
move it aside, then the coded order of elements is correct. If you prefer buses to go beneath the Person objects, then you
have to change both the RenewMover() method and the OnPaint() method.
In the next example – the real Family Tree application – you will see all the tuning possibilities; in this
Form_TwoGenerations.cs some of the tunings are not implemented. For example, here any bus can be modified and the
colors for end points and joints can be changed, but there is no command to switch OFF the painting of those small circles
at the ends of the buses and over the joints. Tuning of any Person object is allowed (use double click to start it), but this
tuning is limited to fonts and colors while the text change is disabled.
Some changes in the current example are organized via the
commands of three context menus (figure 21.54). Menu on joints
includes one command and I can’t imagine other commands
which can be added to this menu. Two other menus are simplified
versions of what you will see further on in the
Form_FamilyTree.cs.
The brown bus from figure 21.53 is the only bus in this family
tree which can be declared movable (via the command of its
context menu) because it is the only bus with both ends free. If
you declare the brown bus movable, then you can move it, for
example, closer to the Person objects above or below and
throughout this movement all the points of connection on this bus
(there are three of them) will retain their relative positions.
The brown bus is different from all other buses in one more
feature. The end points of any bus with the ends connected to Fig.21.54 Three context menus are used in the
something can be moved only as the result of moving those Form TwoGenerations.cs
objects at the ends. For example, you can move two Person
objects of a couple closer to each other or farther away from each other and in any case the end points will stay with the
connected objects and thus change the length of the bus. The length of the brown bus can be changed only by the move of
its end points; unfortunately, not in the situation shown at figure 21.53.
There are small red points at the ends of the brown bus, but there are also identical red points at the ends of the green buses
which connect the brown bus with the Person objects below. When you press the red point at the end of the brown bus
in the situation shown at figure 21.53, you press not the end point of any bus, but the BusConnection element
positioned there. Move this point of connection slightly aside from the end of the brown bus; after it press the free end of
the brown bus which is now not blocked by anything and, by moving it, change the length of the brown bus.
For the first time among all the preliminary examples two new features appear in the Form_TwoGenerations.cs. They are
not too much needed in this small tree but they will be very helpful in the real big family tree and I decided to check and
demonstrate them here.
The first feature is the saving of the family tree in a binary file and restoration of the tree from such file. There are two
commands (via the File menu) to save and restore later the view of a family tree. In this small example it will only give you
a chance to organize different views of the same tree and to save all of them for comparison. In the real Family Tree
application, the same pair of commands allows to organize different trees and then work with the needed one.
A family tree consists of the Person, Bus, and BusConnection elements and for correct restoration of any tree we
need to save all these elements. Well, it is correct for the Person and BusConnection elements, while not all the
buses need to be saved. For the correct restoration of any Person object, it is enough to save the id number of its
invisible bus on border while all other parameters of this bus can be skipped. The invisible buses of the Person objects
are not registered with the mover; so only the buses registered with the mover need to be saved.
private void SaveFamilyTree (string file)
{
… …
bw .Write (people .Count); // 3
bw .Write (buses .Count - people .Count); // 4
bw .Write (connections .Count); // 5
for (int i = 0; i < people .Count; i++)
World of Movable Objects 797 (978) Chapter 21 Medley of examples
{
people [i] .IntoFile (bw);
}
for (int i = 0; i < buses .Count; i++)
{
if (buses [i] .RegisteredWithMover)
{
buses [i] .IntoFile (bw);
}
}
for (int i = 0; i < connections .Count; i++)
{
connections [i] .IntoFile (bw);
}
… …
The second new feature in the Form_TwoGenerations.cs is the possibility to move the whole family tree around the
screen. This feature is not needed at all for the tree consisting of four people but in the real big family tree it is very useful.
First, there is no rule that regulates the order of construction of your real tree. You can start from the farthest known
ancestor and go down from generation to generation, or you can put yourself in the middle of the screen and start adding
relatives and the links to them in all the directions. In any case chances are high that even the biggest screen will be not
enough and you will need to scroll it. This is done by a simple press of the left button at any empty place and by moving it
in the direction you need. The instrument is very simple.
There is a Boolean field to inform whether the whole family tree is currently under move or not.
bool bMoveAll = false;
When you press the left button at any empty spot, the value of this field is changed and the current position of the mouse is
remembered.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
bMoveAll = true;
ptMouse_prev = e .Location;
}
}
ContextMenuStrip = null;
}
When no object is moved but this Boolean parameter signals that everything is under move, then the distance between the
current mouse position and the previous one is calculated; this value is used for synchronous move of all the elements.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
Invalidate ();
}
else
{
if (bMoveAll)
{
if (ptMouse_prev != e .Location)
{
World of Movable Objects 798 (978) Chapter 21 Medley of examples
int dx = e .X - ptMouse_prev .X;
int dy = e .Y - ptMouse_prev .Y;
foreach (Person person in people)
{
person .Move (dx, dy);
}
foreach (Bus bus in buses)
{
bus .Move (dx, dy);
}
ptMouse_prev = e .Location;
Invalidate ();
}
}
}
}
When the mouse is released, the value of this bMoveAll field is changed to false, and there is no more movement of
the whole tree.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release (out iWasObject, out iWasNode, out shapeNode))
{
… …
}
else
{
bMoveAll = false;
… …
}
}
Now is the time to put everything together, to add some commands in order to simplify the design of real family trees, and
finally to develop a Family Tree application.
Relations between people are shown by proper positioning of those ovals and a lot of lines. Ovals for spouses are usually
placed at one level with a line between them; their children are usually placed at one level but below. The eldest of siblings
takes position on the left end of row (this is correct for the countries in which writing is done from left to right). To
demonstrate the relation between
generations, children are connected with a
line between their parents, but these details
can be added in different ways. It is possible
that you draw a new line from the parents’
line somewhere down and then add an oval
with a name to the lower end of this line. If
you know that a couple had three kids, then
chances are higher that first you will draw
three ovals side by side and then connect
each of them to the line on upper level. It is
not the rule, but in such way the whole
drawing will be more accurate. It also means
that your drawing can consist of several
pieces which are united into a single family
tree by adding some lines.
Occasionally you find out that you missed
some part and there is no place for new ovals
at their right place; then you draw them
somewhere aside and use a broken line to Fig.21.55 To start the construction of some family tree, call menu at any
show the relation. The same thing might empty place.
happen when you decide to draw some side
branch of your family tree.
One sheet of paper would be not enough. You take another sheep and make special marks to show how these sheets have to
be placed. Lines of relations will go from one sheet to another and around those ovals which happen to be on the way.
I mentioned the normal way of drawing a family tree on the sheets of paper, but if you want to produce some program for
the same purpose, then this program has to provide the same level of flexibility in drawing a family tree on the screen.
There are going to be some limitations, but their effect must be minimized.
Elements of three classes are used to construct any family tree; all those elements are included into three Lists.
List<Person> people = new List<Person> ();
List<Bus> buses = new List<Bus> ();
List<BusConnection> connections = new List<BusConnection> ();
New objects can be added by different commands of several menus, but eventually it all ends with addition of new elements
into these three Lists. Each element must have a unique id number; this is the only checking on adding a bus or
connection.
private void AddBus (Bus busNew)
{
foreach (Bus bus in buses)
{
if (busNew .ID == bus .ID)
{
return;
}
}
buses .Add (busNew);
}
private void AddConnection (BusConnection conNew)
{
foreach (BusConnection con in connections)
{
if (conNew .ID == con .ID)
{
World of Movable Objects 800 (978) Chapter 21 Medley of examples
return;
}
}
connections .Add (conNew);
}
Addition of a Person object is done in similar way, but more actions are needed. Each Person object contains an
invisible bus, so two elements are checked for uniqueness if id and then two Lists are increased.
private void AddPerson (Person prsn)
{
long idPerson = prsn .ID;
long idBus = prsn .Bus .ID;
foreach (Person person in people)
{
if (idPerson == person .ID)
{
MessageBox .Show (…);
return;
}
}
foreach (Bus bus in buses)
{
if (idBus == bus .ID)
{
MessageBox .Show (…);
return;
}
}
people .Add (prsn);
buses .Add (prsn .Bus);
}
While using different commands to construct new parts of the family tree, you can easily trace any command to the
AddBus(), AddConnection(), or AddPerson() method.
There are several possibilities to start the design of a new family tree and all of them are available through the commands of
context menu which can be called at any empty place (figure 21.55). There are three groups of commands in this menu; all
commands to put on the screen something new are included into the first group; one of these commands has a submenu.
Menu is opened at the place where the right button was clicked; new element(s) born by one or another command are
anchored to the same point. The view of each element can be changed at any moment by calling special tuning forms for
buses (figures 21.42) and Person objects (figure 21.50). User can simplify his work by setting the preferable
visualizing parameters and then all new
elements will be constructed according
to these settings. I’ll write about these
settings a bit later; now let us look at the
reaction on the commands from the
shown menu.
New person A new Person
object appears on the
screen. User needs to
define at least the
name of this person
and has to decide
whether some
additional information
about the birth and
death has to appear or Fig.21.56 A new Person object appears together with a tuning form to set
not. For this reason, the needed information
an additional
Form_Tuning_Person.cs is automatically called for this new Person object (figure 21.56).
World of Movable Objects 801 (978) Chapter 21 Medley of examples
private void Click_miNewPerson (object sender, EventArgs e)
{
Person person = new Person (this, ptMouse_Up, personStandard);
AddPerson (person);
RenewMover ();
ModifyPerson (Auxi_Geometry .LocationByCoefficients (person .Area, 0.5,
0.95), person);
}
The initial view of new Person object (figure 21.56) is determined by the default view of the personStandard.
This view can be changed in the auxiliary form about which I’ll write several pages further on
(Form_FamilyTreeSettings.cs, figure 21.67), but initially it is defined in such a way.
public Form_FamilyTree ()
{
… …
personStandard = new Person (this, new RectangleF (0, 0, 140, 50),
Color .Cyan, "Name", "birth", "death");
… …
New couple A couple is represented by a pair of Person
objects positioned not far from each other and
connected by a short straight bus (figure 21.57).
User has to decide about the order of changing their Fig.21.57 A new couple on the screen
information, so there is no automatic call of the
tuning form. The needed tuning form can be called through the menu on one or another object, but this
will be user’s decision.
private void Click_miNewCouple (object sender, EventArgs e)
{
NewCouple (ptMouse_Up);
SetMarks ();
RenewMover ();
}
This Click_miNewCouple() method produces two Person objects which are linked with each other and with
nothing else. You can easily imagine another situation when the same pair of Person objects is needed and this pair has
to be connected with some existing element. One of such situations is demonstrated several pages ahead (figure 21.61); it
is a case of adding parents to some person which is already included into family tree. There are other commands which add
the same elements on the screen and link them to other already existing elements. Because such link is organized from the
short bus between two new Person elements, then the NewCouple() method returns this bus.
private Bus NewCouple (PointF ptLT)
{
Person personL = new Person (this, ptLT, personStandard);
AddPerson (personL);
Person personR = new Person (this, new PointF (ptLT .X +
personL .Area .Width + nDistSpouses, ptLT .Y), personStandard);
AddPerson (personR);
Bus link = LinkPersonPerson (this, mover, busStandard, personL, Side .E,
0.5, personR, Side .W, 0.5);
return (link);
}
To organize a short bus between two new Person elements, the NewCouple() method uses the
LinkPersonPerson() method. In reality this is a link between the invisible buses of those Person objects, so the
LinkPersonPerson() method is only a wrapper around the LinkBusBus() method.
private Bus LinkPersonPerson (Form frm, Mover mvr, Bus busSample,
Person personFrom, Side side_From, double coef_From,
Person personTo, Side side_To, double coef_To)
{
Bus busFrom = personFrom .Bus;
World of Movable Objects 802 (978) Chapter 21 Medley of examples
PointF ptFrom = Auxi_Geometry .PointOnRectangleBorder (personFrom .Area,
side_From, coef_From);
Bus busTo = personTo .Bus;
PointF ptTo = Auxi_Geometry .PointOnRectangleBorder (personTo .Area,
side_To, coef_To);
return (LinkBusBus (frm, mvr, busSample, busFrom, ptFrom, busTo, ptTo));
}
private Bus LinkBusBus (Form frm, Mover mvr, Bus busSample,
Bus busFrom, PointF ptFrom, Bus busTo, PointF ptTo)
{
ptFrom = Auxi_Geometry .NearestPointOnPolyline (ptFrom, busFrom .Points);
ptTo = Auxi_Geometry .NearestPointOnPolyline (ptTo, busTo .Points);
Bus bs = new Bus (frm, mvr, ptFrom, ptTo, busSample);
BusConnection conFrom = new BusConnection (frm, mvr, ptFrom,
bs .NodeRadius, bs, EndType .Head, busFrom);
BusConnection conTo = new BusConnection (frm, mvr, ptTo, bs .NodeRadius,
bs, EndType .Tail, busTo);
busFrom .AddConnection (conFrom);
busTo .AddConnection (conTo);
connections .Add (conFrom);
connections .Add (conTo);
AddBus (bs);
return (bs);
}
In other situations when a link between a bus and a Person object is needed, similar wrappers around the
LinkBusBus() method are used, for example, LinkBusPerson() and LinkPersonBus() methods.
New siblings There is a submenu to
put on the screen
between two and six
siblings; figure 21.58
shows the variant of Fig.21.58 Three siblings
three siblings. If there
must be more than six siblings, then the best way is to use this command to put six siblings on the screen
and add more by the command from another menu; I’ll write about it further on. The returned value of
this method is the bus above siblings.
private Bus NewSiblings (PointF ptL_busAbove, int nSiblings)
{
nSiblings = Math .Min (Math .Max (2, nSiblings), 6);
float cyLineAbove = ptL_busAbove .Y;
float cySiblings = cyLineAbove + nDistGenerations / 2;
float lenLineAbove =
(nSiblings - 1) * (personStandard .Area .Width + nDistSiblings);
Bus busAbove = new Bus (this, mover, ptL_busAbove,
new PointF (ptL_busAbove .X + lenLineAbove,
cyLineAbove), busStandard);
AddBus (busAbove);
for (int i = 1; i <= nSiblings; i++)
{
PointF ptOnBus = busAbove .NearestPointByCoefficient (
(i - 1) * 1.0 / (nSiblings - 1));
Person child = new Person (this,
new PointF (ptOnBus .X - personStandard .Area .Width / 2,
cySiblings), personStandard);
child .Name_Comment .Text = "Sibling-" + i .ToString ();
AddPerson (child);
LinkBusPerson (this, mover, busStandard, busAbove, ptOnBus, child,
Side .N, 0.5);
}
World of Movable Objects 803 (978) Chapter 21 Medley of examples
SetMarks ();
RenewMover ();
return (busAbove);
}
New bus This is a simple but interesting command which produces such a small object on the screen that no figure
is needed to illustrate it. This new object is a short bus with two free ends.
private void Click_miNewBus (object sender, EventArgs e)
{
Bus bus = new Bus (this, mover, ptMouse_Up,
AnotherEndOfNewBus (ptMouse_Up), busStandard);
bus .UseWarningColor = bUseWarningColor;
bus .WarningColor = busStandard .WarningColor;
AddBus (bus);
SetMarks ();
RenewMover ();
Invalidate ();
}
This new bus is very short and is not connected to anything but such free bus allows to organize any needed link in the
family tree; this can be a link between two buses, or two Person objects, or between bus and a person. To do such a
thing, you press the end point of the bus, move it on top of the object with which you want to connect it, and release there.
Because the last action is the release of an object, then for the result we have to look on the code of the OnMouseUp()
method. The released end of the bus can be connected either to the visible bus or to the invisible bus on the border of a
Person object; in the second case it looks like a connection to the person. The code for these two cases differs only in
some tiny details, so it is enough to look at the case of a bus released over bus.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double dist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iWasObject, iWasNode;
NodeShape shapeNode;
if (mover .Release (out iWasObject, out iWasNode, out shapeNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is Bus && shapeNode == NodeShape .Circle && !grobj .Movable)
{
Bus busReleased = grobj as Bus;
if (iWasNode == 0 || iWasNode == busReleased .Points .Count - 1)
{
EndType end_type = (iWasNode == 0) ? EndType .Head
: EndType .Tail;
List<MoverPointInfo> infoAll = mover.PointInfoAll (ptMouse_Up);
for (int j = 0; j < infoAll .Count; j ++)
{
MoverPointInfo info = infoAll [j];
GraphicalObject objFound = mover [info .ObjectNum] .Source;
// bus cannot be connected to itself
if (busReleased .ID != objFound .ID)
{
if (objFound is Bus)
{
Bus busFound = objFound as Bus;
if ((iWasNode == 0 && busReleased .TailConnected &&
busReleased .ID_OnTail == busFound .ID) ||
(iWasNode == busReleased .Points .Count - 1 &&
busReleased .HeadConnected &&
busReleased .ID_OnHead == busFound .ID))
World of Movable Objects 804 (978) Chapter 21 Medley of examples
{
// two ends cannot be connected to the same bus
}
else
{
PointF ptOnBus =
Auxi_Geometry .NearestPointOnPolyline (
ptMouse_Up, busFound .Points);
if (end_type == EndType .Head)
{
busReleased .HeadPoint = ptOnBus;
}
else
{
busReleased .TailPoint = ptOnBus;
}
BusConnection con = new BusConnection (this,
mover, ptOnBus, busReleased .NodeRadius,
busReleased, end_type, busFound);
busFound .AddConnection (con);
connections .Add (con);
RenewMover ();
}
break;
}
else if (objFound is Person)
… …
There are few and obvious limitations on organizing the new connection; some explanations to this a bit lengthy piece of
code can be helpful.
1. Number and shape of the released node are needed for further analysis, so the appropriate version of the
Mover.Release() method is used.
int iWasObject, iWasNode;
NodeShape shapeNode;
if (mover .Release (out iWasObject, out iWasNode, out shapeNode))
{
GraphicalObject grobj = mover .ReleasedSource;
2. The released element has to be a circular node of the unmovable bus.
if (grobj is Bus && shapeNode == NodeShape .Circle && !grobj .Movable)
{
Bus busReleased = grobj as Bus;
3. Bus can be connected only by head (the first circular node) or by tail (the last circular node). Circular nodes are
the first in the bus cover and those nodes are numbered from head to tail. The node at the head has number 0,
while the number of the node on the tail is determined by the number of special points in the released bus.
if (iWasNode == 0 || iWasNode == busReleased .Points .Count - 1)
{
EndType end_type = (iWasNode == 0) ? EndType .Head : EndType .Tail;
4. Connection of the released bus to some object depends on the situation with those objects at the point of release.
The full information about objects at any point can be obtained from mover through the
Mover.PointInfoAll() method. This method returns a List of objects which can be found at the
specified point; the order of objects in this List is the same as their order in the mover queue.
List<MoverPointInfo> infoAll = mover .PointInfoAll (ptMouse_Up);
for (int j = 0; j < infoAll .Count; j ++)
{
MoverPointInfo info = infoAll [j];
GraphicalObject objFound = mover [info .ObjectNum] .Source;
World of Movable Objects 805 (978) Chapter 21 Medley of examples
5. The bus (busReleased) is released over some object (objFound). Connection of the bus to itself must be
expelled. I do not think that anyone would really need to organize such connection, but there are always people
who like to check any application for unthinkable situations and there is also a possibility of accidental release of
an object in the wrong place.
if (busReleased .ID != objFound .ID) // bus cannot be connected to itself
{
6. There are two situations when the released bus must be connected to an object underneath: it can be another bus or
it can be a Person object. In the second case the connection is organized with the invisible bus under the
border of rectangular object and these two cases differ only in some small details.
if (objFound is Bus)
{
… …
}
else if (objFound is Person)
{
… …
}
7. Because two cases of connection are similar, it is enough to look into details of one of them; let us consider a case
of a bus released over another bus. We need to check and avoid the situation when we try to connect the released
bus end to the same bus to which its other end is already connected.
Bus busFound = objFound as Bus;
if ((iWasNode == 0 && busReleased .TailConnected &&
busReleased .ID_OnTail == busFound .ID) ||
(iWasNode == busReleased .Points .Count - 1 &&
busReleased .HeadConnected && busReleased .ID_OnHead == busFound .ID))
{
// two ends cannot be connected to the same bus
}
8. Looks like we successfully avoided all traps and the released end of the bus can be connected to another bus found
at the point of release. This action requires a bit of calculation and the change of several parameters in two objects.
• The exact point of connection is calculated (ptOnBus).
• The value of this point is stored in the released bus.
• New connection is organized.
• It is a new connection to the found bus. Each bus keeps information about all connections, so the
information in the found bus must be updated.
• New connection is included into the full List of connections.
• A new movable object appears on the screen, so mover queue must be renewed.
else
{
PointF ptOnBus = Auxi_Geometry .NearestPointOnPolyline (ptMouse_Up,
busFound .Points);
if (end_type == EndType .Head)
{
busReleased .HeadPoint = ptOnBus;
}
else
{
busReleased .TailPoint = ptOnBus;
}
BusConnection con = new BusConnection (this, mover, ptOnBus,
busReleased .NodeRadius, busReleased, end_type, busFound);
busFound .AddConnection (con);
connections .Add (con);
World of Movable Objects 806 (978) Chapter 21 Medley of examples
RenewMover ();
}
The Family Tree application provides different commands to design trees. These commands are named in such a way as to
make them easier to understand (New couple or New siblings), but in reality there is no information inside whether the new
Person elements were organized by one command or another. Thus, there is no information about the relations between
the people represented by those elements; there are only the Bus, Person, and BusConnection elements linked
with each other in different ways. The same tree can be constructed in many different ways and the order of elements in
three lists (people, buses, and connections) can be different. It does not matter at all whether you prefer to use the
commands that put on the screen groups of connected elements or to use other commands that add one bus or one person at
a time.
For example, the above mentioned command New couple puts on the screen two Person objects with a connection
between them (figure 21.57). Exactly the same thing can be organized by a set of several commands and actions:
• Use the New person command at one point and then the same command somewhere slightly aside; two
independent Person objects will appear on the screen.
• Use the New bus command to put a new bus on the screen; on initialization this bus is not connected to anything.
• Move one end of the new bus and drop it somewhere inside one Person object; the bus will be connected to this
object (visually to the border of this object but in reality to the invisible bus that goes along the border of this
object).
• Move another end of the bus and drop it inside the second Person object; this end of the bus will be connected
to this object (again to the border of this object).
Certainly, this sequence of commands and actions is much longer than a single click on the New couple command, but the
result is the same.
If you try to organize a forbidden link (for example, to drop both ends of the bus on the same Person object) the second
link will be not organized and that is all.
Fig.21.59a Menu on connection has only one command Fig.21.59b After disconnection, there is a gap between two
objects and the warning color is used for a bus
What to do if you need to disconnect a bus? Call a context menu on the point of connection and select the only command of
this menu – Delete connection (figure 21.59a). The bus will be disconnected and in most cases its end point will be moved
aside to organize a visual gap between two elements. The end point is not moved aside and the gap is not organized only if
the decrease of the bus length will make it too small. Regardless of whether the gap is organized or not, the bus will be
disconnected. Bus with a free end is an abnormal situation, so the warning color is used to draw the bus after disconnection
(figure 21.59b).
When you already have some family tree and want
to add new elements, then you will need some
commands to do it. Certainly, it can be done by
adding one element after another, but there are two
menus with the commands which make these tasks
easier. One menu can be called on any bus; another
– on any Person object; let us start with menu on
buses.
This menu (figure 21.60) can be called on any bus
and it does not matter at all whether this bus is
connected to anything or absolutely free. Only two
things matter: the selected command and the point at
which the menu is called because the positioning of
new elements is based on this point. The new
elements will be connected to the pressed bus at the
Fig.21.60 Menu on any segment of a bus
point of the right button click.
World of Movable Objects 807 (978) Chapter 21 Medley of examples
Fig.21.66b Command Add spouse (on right) Fig.21.66c Command Fig.21.66d Command
Add single parent Add child
One remark about the Connect new bus command. The new bus will be connected to the point on the border which is the
nearest to the point of calling menu; later the point of connection can be easily moved around the border.
The Modify command opens the standard auxiliary form for tuning the Person object (figure 21.50).
There are also several commands to change the view of the pressed object according to standard, but I did not explain yet
where this standard view is organized; let us return to this question.
In the main menu of the Form_FamilyTree.cs, there is a position Settings; if you click this menu command, the
Form_FamilyTreeSettings.cs is opened (figure 21.67). Not surprisingly at all that some parts of this form may look
familiar to you. For example, the Bus group is used to set the parameters of all new buses, so elements of this group are the
same as can be seen in the tuning form for buses (figure 21.42).
Usually buses in a family tree have to connect some elements and the existence of a bus connected only at one end is often a
mistake in design or the result of some interruption in the work on a tree. There is a possibility to organize a well visible
warning about such unusual situation by changing the color of such buses. Two controls in the Bus group allow to define a
color for such special buses and to switch ON / OFF the use of this special color. The regulation of this special case color
World of Movable Objects 809 (978) Chapter 21 Medley of examples
warning can be also done through the command of menu at any empty place in the Form_FamilyTree.cs (figure 21.55, the
last command in menu).
Elements of the Person group have to define the view of any new Person object which will appear in the family tree;
some elements are the same as in the Form_Tuning_Person.cs (figure 21.50) but there are also new elements to give more
information and possibilities.
• By changing the sizes of the sample you can define the sizes of the Person objects which will appear later; the
sizes (in pixels) are shown in the special boxes.
• By moving the name comment you can change the positioning of name in all new Person objects; two
positioning coefficients are shown in the special box.
• Appearance or absence of the comments with information about the dates of birth and death are regulated by two
check boxes inside the Birth and Death groups. If these comments are shown, then they can be moved around the
screen and their positioning coefficients in relation to the “parent” Person object are shown in special boxes.
There is also one more group with the title Distances between; controls of this group allow to define the standard distances
between several Person objects that are added to the family tree by the commands from different context menus.
There are two useful commands in the main menu of the Form_FamilyTree.cs. The File – SaveAs command allows to
save the tree in the binary file while the File – Open command allows to restore a family tree from a file.
A real family tree can be a
big one and chances are
high that you will need
more space for it than you
have on the screen. The
whole tree can be easily
scrolled in any way you
want; simply press the left
button at any empty spot
and start moving it. This
scrolling of the whole
family tree was already
explained in the previous
example.
Do not be afraid to do
anything and to make any
mistakes in design because
two main rules are
implemented in this
Family Tree application: Fig.21.67 A special Form_FamilyTreeSettings.cs to set the standard parameters of
• No user’s action is visualization which are used when the new elements are added to a family tree.
fatal for the design of a family tree. If you have placed a wrong element, it can be deleted by the command of
context menu; if you placed a wrong combination of elements, you will have to use the Delete command for each
of these elements and that is all. If the bus is connected to the wrong element, simply disconnect it (via the
command of context menu) and go on with your design.
• There are only two restrictions on design: a bus cannot be connected with itself and both bus ends cannot be
connected to the same bus. Everything else is allowed. For example, if you want to highlight the intercouple link
by two buses, you can do it. Nobody is going to catch you on such an attempt and not allow you to organize two
parallel buses between the same two Person objects.
This is a standard user-driven application. It is your application, you are the boss, and you organize the view exactly as you
wish.
World of Movable Objects 810 (978) Chapter 21 Medley of examples
(The title “Elements and groups” immediately reminded me about the “Officers and Gentlemen” by Evelyn Waugh;
memory has a strange way of producing associations.)
Elements, similar to those shown at the figure, were used in many examples from the first part of this book. In order not to
mix them, all classes of elements prepared for this example have in their name the abbreviation EAG.
The table below (figure 21.69) demonstrates these classes of elements and gives a brief description of their covers. As you
see, those elements differ a lot:
• There are solid elements and perforated.
• There are elements with straight borders and curved borders.
• There are unicolor and multicolor objects.
• Objects of two classes have inner movable partitions.
All objects are
rotatable and
resizable; these
features are regulated
and I try to indicate
the current status of
these features by
some additional
marks on
visualization. For
example, the solid
elements, if rotatable,
have an additional
small white spot in
the center of rotation.
Certainly, this mark
cannot be used for
the perforated
elements with a hole
in the center, but
rectangle, strip,
regular polygon, and
convex polygon from
figure 21.68 have
such spots. For the
majority of elements,
their resizability is
marked by the wider
border (rectangle, Fig.21.68 Nearly default view of the Form_ElementsAndGroups.cs
strip, regular
polygon, and so on); absence of such wide border means that an element is not resizable at the current moment (convex
polygon and regular polygon with identical hole in the table below).
Two last elements in the table have more flexibility in changing their features and this is marked with additional variants on
visualization. Inner and outer borders of those elements can be rotated independently and this is regulated on an individual
basis. For example, for the regular polygon with a hole of a regular polygon (borders can be not identical) I switched OFF
the rotatability of the outer border; it is still resizable, so its outer border is shown with the DashStyle.Dash style. For
the convex polygon with a hole in the form of a regular polygon I switched OFF resizability, but both borders (inner and
outer) can be rotated individually, so they are shown with the DashStyle.Dot style.
To avoid an accidental disappearance of objects, all elements have minimal allowed sizes.
While demonstrating similar objects in the first chapters of the book, I often used different covers when the resizability of
an element was changed. Later I showed that the same difference in resizing can be obtained by using the unchangeable
number and order of nodes in the cover but by changing the behaviour of those nodes according to the values of additional
flags regulating the resizability. For simpler objects with primitive covers I prefer to use the first variant of changing
covers; for more complicated elements with more variants of resizing and rotatability the second way is used.
World of Movable Objects 812 (978) Chapter 21 Medley of examples
There is one common feature for objects of the EAG classes: resizing of all elements is done with the adhered mouse. For
some elements (rectangle, circle, ring, strip) the use of adhered mouse throughout the resizing was already demonstrated in
several examples in the chapter Simpler covers, but for others it is the first applying of this technique. There was one of
examples – the Form_SetOfObjects.cs (figure 9.5) – in which objects of similar shapes were demonstrated. All classes of
elements from that old example have the abbreviation EOS in their names; otherwise the names of classes for similar
elements are identical, so it is easy to find them for comparison. Elements from two sets of classes differ mostly by the use
of adhered mouse, but there are also some additional changes about which I’ll write further on.
Rectangle_EAG
Resizable rectangles have four small circles on corners, four strip nodes on sides, and a big
rectangular node to cover the whole area. Non-resizable rectangles use only this big node.
Squeezing is limited by a minimum allowed length of any side.
Circle_EAG
When changing of sector angles is allowed, then borders between sectors are covered by strip
nodes. Resizing of the whole circle is provided by two circular nodes with a small difference
between their radii. Squeezing is limited by minimum allowed radius of a circle.
Ring_EAG
When changing of sector angles is allowed, then borders between sectors are covered by strip
nodes. Resizing is provided by two pairs of circular nodes with a small difference between the
radii inside each pair. In a standard technique for perforated figures, the hole is covered by a
transparent node. There are limits on minimum hole radius and on minimum width of a ring.
Strip_EAG
Resizing of such object is provided by two thin strip nodes along the straight border parts and
two circular nodes at the ends. There are limits on minimum length of the straight part and on
radius of those curves.
RegularPolygon_EAG
Cover of resizable regular polygon consists of two polygonal nodes which duplicate the shape of
an object but slightly differ in size. Squeezing of a polygon is limited by minimum allowed
radius for its vertices.
ConvexPolygon_EAG
If polygon is declared reconfigurable, then its shape is changed by moving any vertex; in this
case each vertex is covered by a small circular node. If polygon is resizable, then it can be done
by any border point and in such case each border segment is covered by a strip node. The limit
on squeezing is set by declaring a minimum length for border segments.
ChatoyantPolygon_EAG
Such polygon consists of a central point and a set of points connected by a polyline. The central
point and vertices of the polyline are covered by circular nodes which provide reconfiguring;
segments between the neighbouring vertices of the polyline are covered by the strip nodes which
provide resizing. Each pair of neighbouring vertices and the central point are used to construct a
polygonal (triangular) node; a set of such nodes provide forward movement and rotation of the
whole object. When reconfiguring is not needed, then there are no circular nodes. When
resizing is forbidden, then there are no strip nodes. Squeezing is limited by the size of
circumscribed rectangle.
World of Movable Objects 813 (978) Chapter 21 Medley of examples
RegPoly_IdenticalHole_EAG
Identical hole in the name of the class means that the shape of the inner border is identical to the
outer border: these two regular polygons have the same number of vertices and are rotated at the
same angle. For a resizable object, the cover consists of four polygonal nodes of the same shape
but different size; the smallest node is transparent while others provide resizing by inner border,
moving of the whole object, and resizing by the outer border. For non-resizable object only two
nodes matter; the smaller one is transparent and cuts out the hole while the bigger one provides
forward moving and rotation.
RegPoly_CircularHole_EAG
Cover of resizable object consists of two pairs of nodes. The first is the pair of circular nodes.
One of them is slightly smaller than the hole and this node is transparent; the second one is
slightly bigger and provides the resizing by the inner border. Second pair of nodes has the same
shape as the outer border and very close to it in size. Smaller of these two nodes provides
forward movement and rotation of the whole object, while another one provides the resizing by
outer border.
RegPoly_RegPolyHole_EAG
This is a regular polygon with a hole in a form of another regular polygon. Both borders can be
rotated independently of each other. There can be many different variants for switching ON /
OFF the resizability and rotatability of different parts. Cover consists of two pairs of polygonal
nodes; nodes of each pair have the same shape and slightly different size. The smaller node of
the first pair is transparent; the second node provides resizing by the inner border and rotation of
this border. Nodes of the second pair are closer to the outer border. The smaller of them
provides forward movement and rotation of the whole object while the second one provides
resizing by outer border and rotation of this border.
ConvexPoly_RegPolyHole_EAG
This is a convex polygon with a hole in a form of a regular polygon. Both borders can be rotated
independently of each other. The outer border can be resized and its shape can be changed; the
inner border can be only resized. Cover of this class has the biggest number of nodes among all
elements used in this example. There are two sets of nodes on outer border; circular nodes cover
vertices and strip nodes cover segments of the outer border. There is a pair of nodes which have
the shape of the hole but slightly differ from it in size; the smaller one is transparent; the bigger
one provides resizing by the inner border. There is also a polygonal node up to the outer border;
this node provides forward movement and rotation of the whole object.
Fig.21.69 Elements of different classes which are used in the Form_ElementsAndGroups.cs
Objects of the represented classes can be used both as independent elements and in groups. Default view of the
Form_ElementsAndGroups.cs shows three groups and several independent elements, but the number of groups and
independent elements is unlimited. Regardless of these numbers, all independents are always shown atop all the groups,
while the order of objects inside both sets can be easily changed. Independent elements, as all other objects, can be moved
around the screen and placed anywhere. It is possible to place an independent element above a group and then there will be
a problem to distinguish visually such independent element from the inner elements of the underlying group. In order to see
this difference immediately, the borders of elements belonging to the groups are shown in grey color, while the independent
elements have black borders (figure 21.68).
Though I gave a short description of how the covers of all those elements are organized, it is more obvious when you see
them on the screen. A standard small button is used in the current example to switch the cover visualization ON / OFF, but
it is used in slightly unusual way because the covers are visualized only for independent elements. I want to demonstrate
some cover variants and the element views with and without covers for the case of the
ConvexPoly_RegPolyHole_EAG class. To make this demonstration more obvious, I’ll add in each case part of menu
which is called on such element; the checked commands of menu indicate those movements which are available in each
particular case. I want to underline again that the number and the order of nodes do not depend on the set of movements in
which element can be involved in each particular moment. The number and the order of nodes are determined by the case
when all available movements are possible (figure 21.70a). If one or another movement is prohibited, then the
corresponding node (or set of nodes) is diminished to zero size and thus cannot be used to organize that movement (figures
21.70b – 21.70f). The same nodes on borders are used both for resizing and for independent border rotation, so such nodes
are diminished only for the case when both movements are prohibited.
World of Movable Objects 814 (978) Chapter 21 Medley of examples
Fig.21.70a
The case when all nodes have real
(not zero) size and all movements are
possible.
Fig.21.70b
When an object is non-resizable and
the outer border is not rotatable, the
strip nodes on outer border are not
needed.
Fig.21.70c
There are strip nodes on outer border
because it is rotatable. Only one
transparent node near inner border as
there is no resizing and no rotation
by this border.
Fig.21.70d
Strip nodes on outer border and two
nodes near inner border provide
rotations, but circular nodes
disappeared because there is no
reconfiguration.
Fig.21.70e
Forward movement and rotation are
provided by a pair of nodes.
Fig.21.70f
Rotation is also prohibited, so the
same pair of big nodes provides only
forward movement.
World of Movable Objects 815 (978) Chapter 21 Medley of examples
I already mentioned that resizable and rotatable borders are marked with wide lines and special pen styles. Left
figures 21.70 demonstrate those special marks even through cover visualization but only for inner border; this is easily
explained by the difference of nodes used on two borders. Outer border is covered by circular and strip nodes which are
wiped on visualization, so no part of an object can be seen through these visualized nodes. Resizing and rotation of the
inner border are provided by a small difference between two polygonal nodes; by default these nodes are not wiped out.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [2 * nVertices_Outer + 3];
if (bReconfigure)
{
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [i] = new CoverNode (i, ptsOut [i], 4);
}
}
else
{
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [i] = new CoverNode (i, Center, 0.0, Cursors .SizeAll);
}
}
int k0 = nVertices_Outer;
if (bResize || bRotate_OuterBorder)
{
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [k0 + i] = new CoverNode (k0 + i, ptsOut [i],
ptsOut [(i + 1) % nVertices_Outer]);
}
}
else
{
for (int i = 0; i < nVertices_Outer; i++)
{
nodes [k0 +i] = new CoverNode (k0+i, Center, 0.0, Cursors.SizeAll);
}
}
k0 += nVertices_Outer;
if (bResize || bRotate_InnerBorder)
{
nodes [k0] = new CoverNode (k0, Auxi_Geometry .RegularPolygon (
m_center, rVertices_Inner - deltaR_Inner,
nVertices_Inner, m_angle), Behaviour .Transparent);
nodes [k0 + 1] = new CoverNode (k0 + 1, Auxi_Geometry .RegularPolygon (
m_center, rVertices_Inner + deltaR_Inner,
nVertices_Inner, m_angle), Cursors .Hand);
}
else
{
nodes [k0] = new CoverNode (k0, Vertices_Inner, Behaviour.Transparent);
nodes [k0 + 1] = new CoverNode (k0 + 1, Center, 0.0, Cursors .SizeAll);
}
nodes [k0 + 2] = new CoverNode (k0 + 2, ptsOut);
cover = new Cover (nodes);
}
Objects of all mentioned EAG classes can be involved in many different actions. Implementation is much easier when
there is the same basic class under all elements, so all of them are derived from the abstract class GeoFigure.
World of Movable Objects 816 (978) Chapter 21 Medley of examples
public abstract class GeoFigure : GraphicalObject
{
protected Figure_EAG m_figure;
protected bool bResize;
protected bool bRotate;
protected PointF m_center;
protected double m_angle;
protected bool bInGroup;
new public abstract RectangleF RectAround { get; }
public abstract Color Color { get; set; }
public abstract double Angle { get; set; }
public abstract GeoFigure Copy (Form frm, Mover mvr, PointF pt);
public abstract bool ZoomAllowed (double coef);
public abstract bool Zoom (double coef);
public abstract bool SqueezePossible { get; }
public abstract void SqueezeToMinimal ();
public abstract void Draw (Graphics grfx);
public abstract void StartRotation (Point ptMouse);
public abstract void IntoRegistry (RegistryKey regkey, string strAdd);
Users can think out a lot of things that they would like to do with each particular screen element; they can also get some
results faster by applying different commands not only on individual basis but simultaneously to an arbitrary group of
elements. The groups used in the Form_ElementsAndGroups.cs belong to the GroupOfElements class. The overall
behaviour of these groups is very similar to the ElasticGroup class which is widely used in the previous examples.
Similarity in behaviour is provided by using a similar set of fields, properties, and methods; the similarity of fields causes
also the similarity of tuning forms; this will be shown a bit further.
public class GroupOfElements : GraphicalObject
{
Form formParent;
List<GeoFigure> m_elements = new List<GeoFigure> ();
int [] m_framespaces = new int [4]; // Left, Top, Right, Bottom
bool bTitleMovable = true;
Rectangle rcFrame; // only through calculation
Rectangle rcGap; // only through calculation
SolidBrush brushBack;
bool bShowFrame;
Pen penFrame;
string m_title;
double coefTitleAlignment; // [0, 1] 0 - Left, 1 - Right
int m_titlesidespace;
Font fontTitle;
Color clrTitle;
I have explained not once my point of view on the design of groups: the main and ruling part in any group must be not a
frame but a collection of inner elements. These objects can be involved in moving, resizing, and anything else whatever
users would like to do with them. The frame of a group is only an auxiliary element which is supposed to make obvious the
content of the group to which different commands can be applied. As an auxiliary element, the frame cannot rule over any
other elements but is supposed to change on any requirement from the inner elements. It is more familiar situation when
there is a single dominant element with one, several, or many subordinates; the design of the GroupOfElements class
represents a strange and very rare case with a set of equal dominant elements and a single subordinate to all of them. This is
a very rare case in all other situations, but from my point of view this is the best and preferable logic for group design.
The ability for users to do whatever they want in the Form_ElementsAndGroups.cs requires, as the main thing, the
possibility to add at any moment new elements and groups; this is provided through the commands of menus by calling an
auxiliary Form_AddElementsOrGroup.cs (figure 21.71).
The Form_AddElementsOrGroup.cs contains a collection of groups; each group is used for quick definition of elements
of one particular class. When objects of these classes are used in the Form_ElementsAndGroups.cs, then they are the
subjects of different transformations. There is no sense to duplicate all those possibilities at the stage of design, so at the
World of Movable Objects 817 (978) Chapter 21 Medley of examples
moment of organizing new elements, minimum declarations are required; for polygons this is the number of vertices; for
unicolor figures I added the possibility of color selection.
As shown at figure 21.71, the
selected elements are demonstrated
in bright colors; all representatives
of the unselected classes are shown
in dim colors; in this way the
current selection is easily
distinguishable. All selected
elements are added either as
individual objects or a new group
with these inner elements can be
organized.
Now let us return to the main form
of this example – the
Form_ElementsAndGroups.cs –
and look into the realization of the
idea “do whatever you want”. From
my point of view, the
implementation of this idea requires
the ability to do two things: to have
on the screen any combination of
elements and to change each and all
elements in any possible way. Plus
everything must be done in the way
that users would prefer. Not in the
way that I, as a developer, would
prefer, but exactly in the way that
each user would prefer. Exclude
only one possibility from this “user
would prefer”: I do not want
computers to read peoples’ mind so Fig.21.71 Form_AddElementsOrGroup.cs with several selected elements
I am not considering such case.
An attempt to design an application in such a way that each user can find his preferable way of doing one or another thing
puts a very interesting requirement on interface design. With all my years of experience, I know exactly how I would prefer
different things to be organized. Certainly, I am going to implement all these things. But the same things can be organized
differently. Even if those different ways of achieving the same results may look a bit strange to me, I know that they are
used by other people. If I am going to fulfil the declared idea for users “do as you want”, then those alternative ways of
doing the same things must be available to users. I do not care about the view of some managers on providing several ways
to do the same things in the programs; I care only about users’ view on the applications and about the easiness in fulfilling
the task for users with absolutely different views. Whether I am successful in my attempt or not – it is for users to decide.
As in all previous examples, everything or at least as much as possible is done with a mouse. We have three standard
mouse events to catch, move, and release the objects; these must provide all the moving, resizing, and reconfiguring. We
might have some context menus for additional commands, but these things are also done with a mouse. Let us start and see
if it is possible to “do whatever WE want”.
Before going into the details of “do whatever you want”, I want to write a bit about my understanding of this idea. This is
my view on the situation when I am a user. Then I fix this set of requirements, switch the sides to the developer’s position,
and try to implement everything. So, here are my requirements when I am a user.
• I have a set of possible elements.
• These elements can be used as independents or in groups.
• Any set of elements can be united into a group.
• Elements can be added to the groups and excluded from groups.
• All possible features of elements and groups can be changed.
World of Movable Objects 818 (978) Chapter 21 Medley of examples
• Features of elements can be changed in four different ways: on an individual basis, for all elements of a group, for
all independent elements, and for all elements.
• Elements and groups can be created and deleted.
• There is a high probability that some elements and groups are not needed at the moment but can be useful later;
such elements and groups can be temporarily hidden and then restored at any moment.
• It is not a rare situation that an element, group of elements, or some view must be used as a sample without any
changes or with minor changes. These samples can make my (users’ !) work much faster, easier, and more
reliable. Thus, whatever is used in the program must be easily turned into such a sample.
All the things in the Form_ElementsAndGroups.cs are implemented in the same way as they were done in the previous
examples with similar features. In some of the previous examples with many different objects I organized it so that each
class has a personal context menu. Here we have elements of many different classes, but all of them are derived from the
same GeoFigure class, so I decided to organize the same menu for all elements. Another context menu can be called on
groups, so there will be two different context menus for all the screen objects. Menu on elements includes the commands to
do something with the pressed element; menu on groups allows to do something with the pressed group. When you want to
do anything with all the independent elements, or with all elements in the form, or with the form itself, then you have to call
a context menu at any empty place outside the groups.
Now it is time for me to switch the sizes and to write some code in order to answer all those requirements. The work starts
with the default view of the form (figure 21.68) which demonstrates elements of all the involved classes divided between
three different groups. I could demonstrate the samples of elements in many different ways but I stopped on such division:
• First group demonstrates solid polygons.
• Second group contains perforated polygons.
• Third group includes elements with the curved borders.
There are also several independent elements in the form just to show from the very beginning that elements can exist
outside of any group.
All groups in the form are organized into a List of groups.
List<GroupOfElements> groups = new List<GroupOfElements> ();
When a new group is constructed, it is placed at the head of the List and visually on top of all others. Later the order of
groups can be easily changed with the commands of its menu or by mouse clicks.
Elements which are not included into any group are organized into a special List of independent elements.
List<GeoFigure> elemsIndependent = new List<GeoFigure> ();
I did not find any sense for an element to be a member of more than one group; if there will be any reason to have such a
situation, it will be not a problem to organize. It is easily organized from the point of programming, but there will be
problems on visualization. If somebody is supposed to belong simultaneously to the Earth population and to the Sky
population, there are no problems in adjusting the statistics, but how are you going to show this somebody in relation to the
clouds which are in between? Think about the problems (and especially solutions) of visualization if you decide to include
any element into more than one group.*
The order of groups can be easily changed and the order of independent elements can be easily changed, but all
independents are always shown above all groups. The order of objects inside both Lists (and on the screen) can be
changed through the commands of menus or by mouse clicks (left button). Especially in this example, and this is different
from all the previous examples, I use both ordinary and double clicks to change the order; a single click moves the pressed
object one level up, while a double click moves an object on top of all others.
Groups can be moved around the screen by any inner point while their sizes depend only on the sizes and positions of their
elements. All elements can be moved and resized. A lot of other things can be started via the context menus. Different
menus are called on elements (menuOnElement), on groups (menuOnGroup), and at any empty place outside the groups
(menuOnEmpty). There is also one special group with its special menu, but we’ll look at them later. Let us start with
menu on elements.
*
Groups can be shown or hidden. What to do with an element which belongs to two groups of which one is hidden?
World of Movable Objects 819 (978) Chapter 21 Medley of examples
moving of the wrong object is possible and users want to avoid such wrong movements. At the same time users
may need a quick resizing of an object even if it was previously turned into non-resizable; for this reason the Zoom
command is always enabled regardless of the resizability status of an object.
• The resizing by border allows to change the size in an arbitrary way, but such resizing is based on the eye
estimation; the submenu of the Zoom command allows to change the size by a precise coefficient.
• Of the 11 demonstrated classes only elements of four classes – regular, chatoyant, and convex polygons together
with circles – allow to change their sizes without changing the proportions by moving one border. For other
classes any zooming requires the moving of two borders and it would be a problem to set exactly the same ratio of
all parts after moving two different borders. Especially for these elements, the Zoom command can be very
valuable.
Squeeze to minimal command has to do exactly what it declares, but there is a small catch. This command has to squeeze
an element without any change in its proportions, so for some elements it may not do anything at all
even if an element looks big. Look at the table of elements (figure 21.69) or look at the working
Form_ElementsAndGroups.cs; the second variant is better. Elements of only two classes – circles
and regular polygons – can be squeezed to minimal sizes without any problems at all. Only these two
classes have a single limit on minimal sizes. For all other classes, there are at least two limitations,
and a program cannot break any of them. It checks the possibility of squeezing by each of the limits
and allows to squeeze the element to the first reached limit.
Suppose that you call this command on a ring; class Ring_EAG has limitations on minimal inner radius and minimal
width.
static int minInner = 10;
static int minWidth = 15;
• If you deal with a ring which has inner radius of 40 pixels and width of 120 pixels, then this ring can be squeezed
to inner radius of 10 pixels and at the same time its width will decrease to 30 pixels. The result of the command is
determined by minimal inner radius.
• If you have a ring with inner radius of 80 pixels and width of 30 pixels, then the width of this ring can be decreased
to 15 pixels; at the same time the inner radius decreases to 40 pixels; the result of the command is determined by
minimal width.
• If you have a ring with any big inner radius (it can be huge) and width of 16 pixels, then this ring can be squeezed,
but the squeezing will be so small that it would be difficult even to see. The width can be decreased only by 1
pixel and then the minimal allowed width will not allow to squeeze this ring more. To squeeze such a ring further
on, you have first to change the ratio between its inner radius and width.
Commands of the third block deal with changing the visibility parameters. Because the elements in this example are only
different geometrical figures, then the only parameter to be changed is the color or a set of colors for some of the elements.
Colors command can appear also as Color; this depends on whether the pressed element is unicolor or multicolor. For the
unicolor elements (eight classes of 11), this command calls a standard Color dialog. Each class of multicolor
elements has its personal auxiliary form to set the colors. For chatoyant polygons and circles those forms were
already demonstrated and discussed in the chapter Groups. An auxiliary form to set the ring colors is similar in
World of Movable Objects 821 (978) Chapter 21 Medley of examples
design to the form for circle colors; in order not to jump throughout the book back and forth, I decided to show at
figure 21.73 all three auxiliary forms for changing multiple colors.
Rules of user-driven applications demand that whatever is done by user must be saved and restored the next time in exactly
the same way; developer has no rights to interfere in this process and to make his own changes. However, there are
situations, rare but possible, when the minor actions from developer are needed to avoid the accidental disappearance of the
needed elements from view. For the FormColors_Circle.cs and Form_Colors_Ring.cs there is no need for any special
checks from developer, but they can be needed for the Form_Colors_ChatoyantPolygon.cs. When this form is opened, its
sizes depend on the proportions of the polygon to be tuned; these proportions can significantly vary. If the OK button is
restored in exactly the same position as it had before closing the form, then this button can be restored somewhere outside
the visible part of the form and users would not see this button. To avoid such situation, if needed, a small adjustment of the
button position is done on opening the Form_Colors_ChatoyantPolygon.cs.
Save color sample command is enabled only for unicolor elements and allows to save the color of the pressed element
as a sample for further use.
Use color sample command is enabled only for unicolor elements and allows to change the color of the pressed
element to the sample color which was previously saved.
Though all commands in menu from figure 21.72 are applied to the pressed element, the commands of the fourth block can
change not only the view of this pressed element but also the view of the related group or even the view of the form.
Duplicate command makes a copy of the pressed element and puts it slightly aside from the original one. If
the pressed element does not belong to any group, then the new element simply appears as the new
top element in the List of independents. If the pressed element belongs to a group, then its copy
is also included into the same group; because it appears slightly aside from the original position, this
may cause the enlarging of the group as the frame automatically adjusts its position to the new set of
elements.
Hide command hides the pressed element; if an element belongs to any group, then this group may
squeeze. A single visible element of a group cannot be hidden, so in this case the command is
disabled.
Delete command deletes the pressed element; if an element belongs to any group, then this group may
squeeze. A single visible element of a group cannot be deleted, so in this case the command is
disabled.
Exclude from group command appears in menu when it is called on any inner element of a group; for an independent
element this menu line is changed for Include into group. This command is enabled for independent
element if its area is entirely inside the area of some group.
• The element excluded from the group becomes the top element among independents.
• When an element is included into a group, it becomes the top element of this group. If there are several groups
under the pressed element, then the element is included into the upper group.
Commands of the last block do not change anything in view but deal only with something invisible and kept in memory.
Any number of elements can be stored as samples for later use. Two different commands allow to add samples of elements
to the store.
Add to samples command simply adds the pressed element to the list of samples.
Add sample, delete old samples command adds the pressed element to the list of samples but first it clears this list.
Into Clipboard command puts the image of the pressed element into Clipboard.
Figure 21.74 shows a menu on groups; this menu has similar design and similar order and number of blocks as menu on
elements.
The upper block consists of the same single line Change level which opens similar submenu with the same four commands.
It is absolutely identical because groups, as elements, can be also hidden from view.
The second block includes the commands that change some parameters of elements, their sizes, or order but do not change
the number of inner elements in view. The block starts with the commands to change the resizability and rotatability of all
the inner elements regardless of their classes. As was shown before, each element can be declared resizable / non-resizable
and rotatable / non-rotatable via its personal menu, while the menu on group allows to unify these features for all elements
of a group without multiple calling menus on elements one after another.
World of Movable Objects 822 (978) Chapter 21 Medley of examples
Changing of the form size is the standard procedure in nearly every application. The overwhelming majority of programs
are designed now on the pure idea of dynamic layout, so nearly all programs will do the same thing on moving the border of
such a form: positions and sizes of all the inner elements will be changed linearly with the size of the form so that the
overall view will be always the same. It will be bigger or smaller but generally the same. The use of dynamic layout
became so popular that even the appearance of the underlined question may look very strange to many readers. But are you
sure that the described reaction on zooming of this form – a strict linear transformation – is really the only solution and the
one that everybody expects to see in all the cases?
1. Suppose that you are making speech on design of some plots and have prepared this form beforehand in the way
you really like it to be (as shown at figure 21.77). When the needed view is ready, you would like to squeeze it to
a tiny size, put it somewhere at the side of the screen, then enlarge it at a proper moment, and continue the
discussion of the details which are now seen better simply because they are enlarged. For such purpose the
dynamic layout with the linear transformation works perfectly.
2. Suppose that you are making the same speech and have prepared all the plots exactly in the size you want them to
be. For better use of the screen space you want to pack the form without disturbing the plots; on a proper moment
you enlarge the form and get your perfect view. On enlarging, the sizes of objects are not changed but their
positions do. They move farther away from each other; at some moment there will be no overlapping and then you
will continue your explanation about the details which will be all in view.
3. There is a third variant when the sizes of those objects change according to the form sizes, but the positions of
objects do not change at all. Yes, the increased objects will overlap even more, but with a double click you can put
any of them on top of others so you do not consider this overlapping as a problem. At any moment you are talking
about one or another detail; if you are not interested in comparison but only in demonstration of one detail at a time
then the overlapping is not a problem.
As you see, there are three absolutely different situations which can be organized as a reaction on border movement. It is
not a problem to put into code all three variants and give users a choice, but did you ever see these three choices in any
program? With very high probability the answer will be NO, at least, I never saw such an application. All applications are
designed under the main idea that all users have to think exactly the same way as developers and have to like whatever they
are given. As a result, there is hatred around the world against the applications that do whatever they want and not what
users expect them to do. After it there are long explanations from the developing companies that “users simply do not
understand their happiness in getting such outstanding programs”. This goes on and on. These users-idiots refuse to agree
that they have to like whatever they are given but they have no choices. Dynamic layout is ruling the programming world.
Certainly, I am against the overwhelming use of
dynamic layout; in reality, you will never find it in
any of my applications. When you click the Zoom
command in menu on group (figure 21.74) then one
more special group – groupZoom – appears atop
all other elements in the
Form_ElementsAndGroups.cs. In this group
(figure 21.78) you see three special scales through
which three mentioned variants of zooming are
implemented. I hope that comments to the scales
leave no chances for misunderstanding. Rules of
Fig.21.78 Group for three variants of zooming and menu which
user-driven applications have to work in all the
can be called on this group
corners: give users the chance to do whatever each
user wants at each moment.
This special group belongs to the ZoomingGroup class which is derived from the ArbitraryGroup class.
public class ZoomingGroup : ArbitraryGroup
{
RectangleF rcClose;
int nCloseSide = 20;
SolidBrush brushClose = new SolidBrush (Color .OrangeRed);
Pen penCross = new Pen (Color .White, 3);
The ZoomingGroup class inherits from its base class the behaviour, visibility parameters, and a couple of elements like
frame and title. The new fields are used to organize a small cross in the top right corner of the group area. Cover for a
ZoomingGroup object consists of three polygonal (rectangular) nodes: the first node covers the small cross near the
World of Movable Objects 825 (978) Chapter 21 Medley of examples
corner and provides the group closing, the second node is under the group title and provides its movement along the frame,
and the third node covers the whole group area and provides its movement around the screen.
I want to remind that ArbitraryGroup class provides the correlation between the movements of inner elements and
the frame. ZoomingGroup object frames three elements of the BilinearScale class; those scales are declared in
the Form_ElementsAndGroups.cs together with the group.
ZoomingGroup groupZoom;
BilinearScale scaleElements, scaleDistances, scaleElementsDistances;
Each scale of the BilinearScale class consists of four parts: main line, ticks at the side of this line, colored spot
which can be moved along the line, and comment. Line is associated with some range of values, only not two but three
values are required to describe this association. Line is divided into two segments. Each segment is associated with a sub
range. There is a linear dependence between the spot position on the line and the value from sub range, but those linear
coefficients for two parts of the whole line are different. The point in which two parts of line are connected is associated
with coefficient 1.0. Values to the left decrease, values to the right increase, so moving of the ball to the left squeezes the
associated object; moving to the right enlarges it. Rail might have a comment in which case the rail plays the role of
dominant element over the comment.
public class BilinearScale : GraphicalObject
{
Form formParent;
RectangleF rcGuideRail, rcTicks, rcUnion;
PointF ptBall;
double fSqueezePart; // part of scale to the left must be from [0.1, 0.5]
double fLeftValue, // 0.1 <= fLeftValue <= 0.98
fRightValue; // 2.0 <= fRightValue
int nRadius = 5; // ball
Color clrBall = Color .Blue;
double coef = 1.0;
bool bCommentExist = false;
CommentToRect m_comment = null;
Such scale has a very simple cover: a circular node over the ball, two small rectangular nodes on the sides of the scale to
provide its horizontal resizing, and a rectangular node over the whole object to provide its movement.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, ptBall, nRadius, Cursors .SizeWE),
new CoverNode (1, RectangleF .FromLTRB (rcUnion .Left - 3,
rcUnion .Top - nRadius, rcUnion .Left + 3,
rcUnion .Bottom), Cursors .SizeWE),
new CoverNode (2, RectangleF .FromLTRB (rcUnion .Right - 3,
rcUnion .Top - nRadius, rcUnion .Right + 3,
rcUnion .Bottom), Cursors .SizeWE),
new CoverNode (3, rcUnion) };
cover = new Cover (nodes);
}
Parameters of initialization for BilinearScale object include coefficients associated with left and right ends of scale;
for all three scales from figure 21.78, the [0.4, 4] range is declared.
private void DefaultView ()
{
… …
scaleElements = new BilinearScale (this, new PointF (140, 40), 300,
0.4, 4.0, 0.3, Side .E, SideAlignment .Center, 6,
"Elements on \nfixed positions", Font, 0, Color .Blue);
scaleElements .BallColor = Color .Blue;
scaleDistances = new BilinearScale (this, new PointF (140, 140), 300,
0.4, 4.0, 0.3, Side .E, SideAlignment .Center, 6,
"Distances to elements", Font, 0, Color .Green);
scaleDistances .BallColor = Color .Green;
World of Movable Objects 826 (978) Chapter 21 Medley of examples
scaleElementsDistances = new BilinearScale (this, new PointF (140, 240),
300, 0.4, 4.0, 0.3, Side .E,
SideAlignment .Center, 6,
"Elements and distances", Font, 0, Color .Brown);
scaleElementsDistances .BallColor = Color .Brown;
groupZoom = new ZoomingGroup (this, new GroupVisibleParameters (this,true),
"Zoom variants",
ElementsArea_groupZoom, DrawElems_groupZoom,
SynchroMove_groupZoom, IntoMover_groupZoom);
… …
Zooming is done by moving one or another ball, so the interesting code can be found in the three standard methods of the
Form_ElementsAndGroups.cs. Zooming is done in relation to the sizes and positions of elements at the moment when the
ball is caught, so all sizes and positions must be saved at this moment. Zooming is applied to the pressed group and its
inner elements, so it is easier to make the copy of the whole group and then use one or another part of data depending on the
pressed ball (and selected type of zooming).
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)
{
… …
else if (grobj is BilinearScale)
{
scalePressed = grobj as BilinearScale;
if (mover .CaughtNodeShape == NodeShape .Circle) // ball
{
Rectangle rcClip = Rectangle .Round (
scalePressed.ClipRectangleOnBallCatch (e.Location.X));
Cursor .Clip = RectangleToScreen (rcClip);
ptInitialCenter =
Auxi_Geometry .Middle (groupPressed .FrameArea);
groupCopy = groupPressed .Copy (this,
groupPressed .FrameArea .Location);
}
}
… …
For better comparison of zooming variants, I took a group (figure 21.79a), made three copies of this group by using the
Duplicate group command (figure 21.74), and put all four identical groups in a row with a small space between neighbours.
Then three types of zooming with the same coefficient 0.4 were applied to three copies of the original group. Zooming
is applied to inner elements, but the frame adjusts its sizes to all inner changes, so the size of the group also changes.
Zooming is done when the caught ball moves along the rail, so the process of zooming must be described inside the
OnMouseMove() method. There is one interesting aspect of squeezing the elements; I want to mention it before
demonstrating the code. While writing about zooming of the pressed element via the commands of menu, I mentioned that
some of submenu lines from figure 21.72 can be disabled because of the limitations on the minimum allowed sizes of the
pressed element. The decision about making those lines enabled or disabled is made at the moment of opening that
submenu and can be easily done because the pressed element is known and all the coefficients are determined. But what to
do with the simultaneous squeezing of several elements of which each one has its own limitations? What to do if one
element cannot be squeezed any more while others can? I decided not to interrupt the whole process but at any moment to
squeeze only those elements which can be decreased; the elements that cannot be squeezed any more remain unchanged.
private void OnMouseMove (object sender, MouseEventArgs e)
{
ptMouse_Move = e .Location;
if (mover .Move (e .Location))
World of Movable Objects 827 (978) Chapter 21 Medley of examples
{
GraphicalObject grobj = mover .CaughtSource;
… …
else if (grobj is BilinearScale &&
mover .CaughtNodeShape == NodeShape .Circle &&
e .Button == MouseButtons .Left)
{
double coef = (grobj as BilinearScale) .Coefficient;
if (grobj .ID == scaleElements .ID)
{
for (int i = 0; i < groupPressed .Elements .Count; i++)
{
GeoFigure elemSubstitute = groupCopy .Elements [i] .Copy (
this, mover, groupPressed .Elements [i] .Center);
if (groupPressed .Elements [i] .InView &&
elemSubstitute .Zoom (coef, true))
{
groupPressed .Elements .RemoveAt (i);
groupPressed .InsertElement (i, elemSubstitute);
}
}
}
… …
The above shown code is for the case of zooming elements without changing their positions (figure 21.79b). For the case
of changing positions of elements without changing their sizes (figure 21.79c) the code is simpler as it is enough to
calculate the new centers for all elements.
else if (grobj .ID == scaleDistances .ID)
{
for (int i = 0; i < groupPressed .Elements .Count; i++)
{
groupPressed .Elements [i] .Center =
Auxi_Geometry .PointOnLine (ptInitialCenter,
groupCopy .Elements [i] .Center, coef);
}
}
The third case of simultaneous changes in sizes and positions (figure 21.79d) is a combination of two previous variants.
Menu opened at empty places is also divided into several blocks, but it would be difficult even to formulate the special main
ideas of each block; let us simply go through the commands of this menu one by one.
Reverse groups command changes the order of groups to the opposite one.
Hide all groups command makes all groups invisible without deleting them.
Unveil groups command allows to turn invisible groups into visible; the text of this command and its implementation
depend on the number of hidden groups.
• If there is a single hidden group, then the text of this command in menu is Unveil group; on clicking the command,
this hidden group becomes visible.
• If there are several hidden groups, then an auxiliary Form_GroupsToUnveil.cs is opened (figure 21.82). This
form shows a list with the titles of the hidden groups. Any set of lines in the list can be selected and the associated
groups are turned into visible.
Add sample command allows to add new element. This command has a variant Add samples; the text
depends on the number of previously saved samples. In any case, all the stored samples
of elements are added to independents.
Add elements or group command calls the Form_AddElementsOrGroup.cs which was already shown at
figure 21.71. Combination of needed elements is selected and can be added as
independents or a new group with selected elements can be organized. New group can be
with or without title.
Add sample group command allows to add into view the group which was previously stored as a sample.
Automatic inclusion into group command allows users to decide about the preferable way of adding independent
elements into the groups. While writing about the menu on elements, I mentioned that
when that menu was called on an independent element then one of its commands is
Include into group and this menu line is either enabled or disabled depending on the
relative positions of the element and groups. The standard procedure to include any
independent element into a group is following:
• Catch the needed element and move it into the frame of the group.
• Call the menu on this element and press the Include into group command.
This looks normal but many readers will make a remark that the second step
is redundant and if this skillful user moved an element into the boundaries of
a group then the purpose of this movement was to include an element into
group; this must be done automatically and do not bother users with two
extra clicks. There can be some objections, but I don’t see any sense in
arguing about the main requirement: if users want to organize it in such a
way, they may do it. Not everyone will agree to such change of procedure,
so I added this extra command and now each user can decide for himself if
he needs this automatic inclusion or not. I want only to add a piece of
information for your decision on using this automatic inclusion.
I do not like applications (unfortunately, there are a lot of such programs)
which decide by themselves, what exactly I want to do. Too often such
applications do something according to their internal decision and this is not
what I want to do. Suppose that I have an independent element atop a group Fig.21.82 Any combination of hidden
and I do not want to include this element into the group. At the same time I groups can be unveiled
need to change the sizes of this element or slightly turn it around. If the
automatic inclusion is switched ON, then, after any, even tiny, movement or change of this element inside the frame of the
group this element will be included into the group, though I did not want to do it and never asked to do it. You can suggest
that program has to analyse not only the position of element after the movement but also to take into consideration the
position before the movement: if it was outside before, then use the automatic inclusion. This will add a small piece of code
but will not give better solution because an element can fit into the frame even as a result of some tiny rotation.
My point is simple and is formulated in the rule 3 of user-driven applications: Users’ commands on moving / resizing of
objects … must be implemented exactly as they are; no additions or expanded interpretation by developer are allowed. So I
prefer to move an element and then give direct command to include it into the group. But if you clearly understand the
consequences of using the automatic inclusion, you can certainly use it. In any way, this will never destroy anything in
World of Movable Objects 830 (978) Chapter 21 Medley of examples
application and if you accidentally included an element into a group you can return it into independents with a single
command. But the most important thing about this command is this one: there are ways to use or not to use the automatic
inclusion and it is up to user to make a decision. Do not blame the developer that the program is doing something different
from what YOU want. In user-driven applications you, as a user, make all the decisions.*
Next two lines of menu open nearly identical submenus of commands: the first submenu works with independent elements
while the second one deals with all elements. Pay attention that the commands from All elements submenu deal with all
elements in and outside the groups but not with the groups themselves. Because two submenus are nearly identical, it
would be enough to go quickly through the commands of one of them, for example, Independent elements submenu. On the
way I will mention the difference between two submenus.
Reverse order command changes the order of elements on the opposite.
Resizable command makes elements resizable.
Non-resizable command turns elements into non-resizable.
Rotatable command makes elements rotatable.
Non-rotatable command prohibits the rotation of elements.
Zoom line opens an additional submenu to select the zooming coefficient. Though both submenus with
zooming coefficients have exactly the same set of lines, the status of these lines – enabled or disabled
– is determined in a slightly different way.
• For the independent elements, I check the possibility of squeezing for all elements and set the enabled / disabled
status of command lines according to the results of those checks. Thus each command is available only when
all the independent elements can be squeezed by a particular coefficient.
• For all elements in the form I do not do such checking. All commands for zooming are enabled, but when you
click the command line, then the squeezing is done only for those elements that allow the squeezing with
selected coefficient.
When you have similar submenus with identical commands they must work identically; this is the law of good
design. I purposely left two different situations with these zooming commands. Was I too lazy to organize the
checking for All elements as it is done for Independent elements? Or is it better to take out the checking for
independent elements and squeeze only those of them which can be decreased? I am not sure about the better
solution so I demonstrate both.
Squeeze to minimal command squeezes all the elements for which it is possible according to their limitations.
Duplicate command makes copies of the elements; copies of elements from the groups are included into the
same groups.
Hide command exists only in the submenu of independent elements; this is the only difference between
submenus. This command cannot be used for all elements because no group can exist without visible
elements.
Unveil command turns hidden elements into visible.
Delete command has stronger effect for All elements. If you answer Yes on the warning message, then not
only the independent elements disappear but all groups also disappear and you have an empty form
without anything visible or hidden. Are you planning to start the new life?
We are over with submenu commands and can return to the few left commands in the main part of menu.
Font command calls the standard dialog and changes font for those few elements in the
Form_ElementsAndGroups.cs which work with fonts; these are titles of the groups and all the texts
in the groupZoom (figure 21.78).
Default view command reinstalls the view which is shown at figure 21.68.
*
There can be situations, and this is one of them, which look like a conflict between the implementation by designer of
rule 3 of user-driven applications and the user’s requirement to minimize the number of needed clicks to do something in an
application. An additional parameter regulated by user allows him to indicate his personal preference in the situation and
make the final decision. (The conflict itself reminded me the conflict between the laws of robotics formulated years ago by
Isaac Asimov.)
World of Movable Objects 831 (978) Chapter 21 Medley of examples
Into Clipboard command takes into Clipboard the picture of the whole form.
One more thing is needed in the Form_ElementsAndGroups.cs. We have groups and independent elements. Elements
and groups can be added throughout a special auxiliary form, so adding the new group with some new set of inner elements
can be done easily. But what to do if you want to organize into a group the set of already present independent elements?
Certainly, you can organize a new group with the help of an auxiliary form and then start moving those independent
elements into the new group one by one. It is possible but it is a bit lengthy. Everyone will immediately say that the most
natural way is to frame the needed elements with a line and declare this set a new group. This technique was already
demonstrated in the Village example and the same technique is used in the Form_ElementsAndGroups.cs.
The existence or absence of the temporary frame is determined by the value of a special field bTemporaryFrame.
bool bTemporaryFrame = false;
When you need to draw a temporary frame around some set of elements, you press the left button at an empty place and the
value of this flag is changed to true.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
bTemporaryFrame = true;
}
}
ContextMenuStrip = null;
}
Keep moving the mouse without releasing the button. Temporary frame changes its position together with the mouse and
looks like a movable object but in reality it is not. It is not a movable object, so it cannot be registered with the mover and
mover cannot check the existence or absence of this frame; the existence of the frame is signaled by that Boolean flag.
Usually, when the mouse is moved without any object being caught, then there is no need for painting. In our situation the
painting is also needed if this temporary frame exists at the moment.
private void OnMouseMove (object sender, MouseEventArgs e)
{
ptMouse_Move = e .Location;
if (mover .Move (e .Location))
{
… …
}
else
{
if (bTemporaryFrame)
{
Invalidate ();
}
}
}
If the temporary frame must be shown, then its position is determined by the initial point of the mouse press and the current
position of the mouse cursor.
private void OnPaint (object sender, PaintEventArgs e)
{
Graphics grfx = e .Graphics;
… …
if (bTemporaryFrame)
World of Movable Objects 832 (978) Chapter 21 Medley of examples
{
grfx .DrawRectangle (Pens .Blue, Auxi_Geometry .RectangleAroundPoints (
ptMouse_Down, ptMouse_Move));
}
}
When the left button is released without releasing any object and at the same time the temporary frame is in view, then all
independent elements are checked for the possibility of being inside this frame. If there is at least one element inside the
frame, then all the rounded elements are organized into a new group.
private void OnMouseUp (object sender, MouseEventArgs e)
{
… …
if (mover .Release (out iReleased, out iNode, out node_shape))
{
… …
}
else
{
if (e .Button == MouseButtons .Left)
{
if (bTemporaryFrame)
{
Rectangle rc = Rectangle .FromLTRB (
Math .Min (ptMouse_Down .X, e .X),
Math .Min (ptMouse_Down .Y, e .Y),
Math .Max (ptMouse_Down .X, e .X),
Math .Max (ptMouse_Down .Y, e .Y));
List<GeoFigure> elemsNew = new List<GeoFigure> ();
for (int i = elemsIndependent .Count - 1; i >= 0; i--)
{
if (rc .Contains (Rectangle .Round (
elemsIndependent [i] .RectAround)))
{
elemsNew .Insert (0, elemsIndependent [i]);
elemsIndependent .RemoveAt (i);
}
}
if (elemsNew .Count > 0)
{
groups .Insert (0, new GroupOfElements (this,
elemsNew, "New group", 0.2));
RenewMover ();
}
bTemporaryFrame = false;
Invalidate ();
}
}
… …
I wanted to demonstrate an example of application in which users would be able to do whatever they want. I think that
there are two missing things in the Form_ElementsAndGroups.cs.
1. I decided not to implement the nested groups in this example. From the programming point of view, there is
nothing new in such a thing; it was already demonstrated in the Form_PersonalData.cs. Objects of the
GroupOfElements class in the current example are very similar to the ElasticGroup objects from the
mentioned example and both implement the code technique which is essential for organizing nested groups – the
use of Visible and VisibleAsMember properties. My decision on not using the nested
GroupOfElements objects in this example is based only on an internal feeling that this would be too much;
you cannot ship cargo on a light kayak.
World of Movable Objects 833 (978) Chapter 21 Medley of examples
2. The second missing thing is the lack of dominant – subordinates relations between the elements. I found out that
many applications need to use such relations between the screen elements, so I would like to add this thing into the
current example. Technically it is not a problem at all; this was demonstrated, for example, in the
Form_Rectangles_WithComments.cs. The problem is in thinking out the natural system of commands
(movements) for all possible and needed situations. It is easy to declare some element as dominant and to add new
subordinates for it. It is easy to turn any subordinate into independent element. But I do not see any natural way to
select several elements and to declare one of them dominant. There are different possibilities but they look clumsy
to me. Anyway, I have to think about something; this is one of the problems.
With the exception of these two things, I think that everything else that I would like to see in any application is implemented
in the Form_ElementsAndGroups.cs. Let us summarize what is demonstrated in this example because it shows the
general way to develop all the applications.
• We have a wide set of different objects.
• All objects are movable and resizable.
• Any object can be moved by inner points and resized by border; objects which need a shape transformation can be
reconfigured.
• Any set of objects can be united into a group.
• A group can be moved by any inner point. The group frame is only an auxiliary element to make the content of a
group clearer; position of the frame is determined by the sizes and positions of its inner elements.
• The order of elements in view can be changed.
• Elements can be duplicated.
• Elements can be hidden.
• Elements can be unveiled.
• Elements can be deleted.
• In addition to resizing by borders, elements can be zoomed by using menu commands.
• All parameters of elements can be changed throughout the menu commands.
• Parameters can be determined for a set of elements; this can be done for all elements of a group, or for all
independent elements (not included into any group), or for all elements in view.
• The order of groups in view can be changed.
• Groups can be duplicated.
• Groups can be hidden.
• Groups can be unveiled.
• Groups can be deleted.
• New elements can be included into groups.
• Elements of a group can be duplicated (individually or all).
• Elements of a group (individually or all) can be excluded and turned into independent.
• Zooming of a group can be done in different ways; it can be zooming of elements without changing their positions,
or changing the positions of elements without changing their sizes, or simultaneous change of sizes and positions.
• Visibility parameters of a group can be modified.
• New elements and groups can be organized via an additional form.
• New group can be organized by rounding a set of independent elements.
• All elements, groups, and parameters of an application are saved; everything is restored in the same way for the
next session.
• Default view of an application can be reinstalled.
World of Movable Objects 834 (978) Chapter 21 Medley of examples
This long list of possibilities looks like a copy of commands from several menus, and even in such a way not everything is
mentioned. I want to underline the main thing of the Form_ElementsAndGroups.cs design. There are different objects,
but there are no strict limitations on users in doing one or another thing they want. All possibilities can be applied to
everything. This can be a single element, or an arbitrary group of elements, or all elements. It is for user to decide, which
way he prefers. There are a lot of possibilities, but there is no need to learn all those possibilities when you start using the
application. You – user – try to do it in the most natural way and for this there are few simple rules.
• If you want to do anything with a particular element, then there is a menu on this element.
• If you want to do something with all elements of a group, then there is a menu on the group.
• If you want to do something with a collection of elements not united into any group or belonging to several groups,
then there is a menu at empty places.
• At any moment, if you see that any command can be easy to use with another set of elements and groups, you can
rearrange the system of elements and groups in any way you want.
What else can be done with the objects? Do you have any ideas about the missed possibilities?
Forget that the demonstrated example is based on a set of geometrical figures; this was done only to abstract from the
unimportant details and to look into the essence of design. Suppose that you are a user of a real application in which all
these possibilities work with the real objects. What else you, as a user, expect from such application? Have you any other
requirements?*
*
Do not ask an application to prepare you a sandwich.
World of Movable Objects 835 (978) Chapter 22 Getting rid of controls
In 1991 the Microsoft Visual Basic was introduced and made the design of good looking programs much simpler. With a
drag-and-drop technique used throughout the design, programmers can easily populate the application area with the needed
controls. Then the needed events associated with these controls are marked. When methods for these events get the code,
the program is ready for use. Controls are the basic elements of such construction.
Usually there are controls in any form of any application. There are programs, and they are very complicated programs, in
which controls are not used in the main form. Maybe the most famous programs of such type are text editors; there are also
applications for science and engineering in which the most valuable information is shown by the plots. Yet, those
complicated programs always have many auxiliary forms for setting a lot of parameters and all those auxiliary forms are
packed with controls.
There are around 10 most popular classes of controls which can be found in all types of programs. When user starts to work
with a new program, he might know the purpose of this program and in some cases he has to be a specialist in particular
area of knowledge, but in any case he has no problems in dealing with elements of any program because all basic elements
are familiar and behave in a standard way.
The most popular controls were perfectly designed according to their purposes; their functionality is very useful in all types
of applications. The only feature of the controls which I don’t like and which does not comply with the rules of user driven
applications is that controls are treated by the system as some special elements which are more important and of higher rank
than all graphical objects. I would prefer to see controls substituted by their graphical replicas which inherit their view,
features, and functionality (at least, all needed functionality), but which will be treated equally with all other graphical
elements.
There is one main change in the code when controls are substituted with graphical analogues. Each standard control is
associated with a stream of messages which are generated when mouse is pressed or released inside the control area, when
the cursor is simply moved across, and when any selection is made. The bulk and clearness of these messages and their
unified use for different elements make the controls so valuable and useful in design. Controls differ by their view and
World of Movable Objects 836 (978) Chapter 22 Getting rid of controls
complexity; different parts of controls generate different messages and a good programmer can use a lot of them. When
controls are substituted by their graphical analogues, the whole set of those messages simply vanishes because graphical
objects have no predefined messages. Any movable object has a cover consisting of nodes. The number of the pressed or
released node is easily detected, so the correct cover design allows to distinguish the mouse press and release in different
parts of an object. Thus, the whole variety of messages from all controls is substituted by three or four mouse events.
Two different actions can be started by the left button press: it can be a movement of an object or it can be something
special like selection, change of status, and so on. Decision about the needed reaction is made at the moment of the mouse
release and is based on the distance between the points of mouse press and release. There is a very simple rule to make such
decision.
• If the distance is insignificant (in my programs –
not greater than three pixels), then the reaction on
mouse click is initiated.
• If the distance is bigger, then it is a movement.
Several examples of this book already mentioned a big
photo archive program. I started to work on that application
even before the invention of movability algorithm, so
originally that program was designed in an old classical
way. Later the program was changed not once and the ideas
of movability were applied, but one day I found out that
there was still one form which retained its old style. This
form is used to set the date for the archived photograph
(figure 22.1).
The old version of this form used a couple of Label
controls, one Button control, several
DateTimePicker controls, one ComboBox control,
and three sets of RadioButton controls. It was easy to
make nearly all controls movable by wrapping them into Fig.22.1 An old variant of the form to set the needed date
SolitaryControl objects, but there were those radio
buttons. For a long time I did not see any good solution for graphical substitution of radio buttons; from time to time I tried
some variants but with clear understanding of their weakness. All the needed features were already used in other classes but
I did not see the way to apply them in case of radio buttons. (Such mind blindness happens from time to time.) Finally I
designed the needed variant. As often happens, the graphical variant not only makes possible everything I need but also
adds some new features which are impossible with the standard controls.
Here is the table of graphical “controls” which are demonstrated and discussed in this chapter. The names of nearly all my
“controls” have the _GR ending (from GRaphical). The only exception is the combo box, because the new element
simulates only one variant of the original control and this variant is mentioned in the name of the new class.
Button Button_GR, Button_Drawing Button_GR is an abstract class; three derived classes represent
Button_Text, Button_Image variants with different types of information on top.
Label Label_GR
NumericUpDown NumericUpDown_GR
ComboBox ComboBox_DDList Classical combo box has three variants; graphical analogue
demonstrates only the one with the DropDownList style.
CheckBox CheckBox_GR
RadioButton RadioButton_GR Radio buttons are used only in groups, so there is a class of
RadioButtonsGroup buttons and the class for their groups.
Trackbar Trackbar_GR There exists an older version of graphical track bar (Trackbar
class); this new class uses the adhered mouse.
World of Movable Objects 837 (978) Chapter 22 Getting rid of controls
One more class – ListBox_GR – which is used for list box substitution is demonstrated and discussed in the book
Elements of Total Movability but not in the current chapter.
Graphical “controls” have several general features which I want to mention before going into the details of each new class.
Movability of elements and users’ control over movability is discussed throughout the whole book. For graphical
primitives, the switch between an element being movable and unmovable is often provided with a simple change of cover.
For complex objects and for the groups of objects, there are more variants. The situation with individually unmovable parts
which move synchronously with the main (parent) object is not unusual and was already discussed. For example, elements
inside movable groups (like ElasticGroup) can be declared unmovable. Controls make the whole situation a bit more
complicated as, depending on the logic of application, any control can be enabled or disabled.
Enabled / disabled controls are distinguished by their view; movability has no such visual effect. In addition, there are two
ways to organize immovability of any object: it can be done either by blocking the move by mouse (Movable property is
used) or by making an object transparent for mover (TransparentForMover property is used). In the second case the
mouse press goes through the transparent (for mover) element and can be applied to another object which happens to be
underneath. Here are several rules which I tried to implement for all my controls.
• Controls can be used with or without additional comment; as all controls are rectangular, then all of them use
comments of the CommentToRect class. There are controls which include text as a very important part and the
text itself gives enough information about the purpose of an element. These are objects of the CheckBox_GR
and RadioButton_GR classes; elements of these classes do not have comments. Buttons of the
Button_Text class have text on top and usually it gives sufficient information about such button. If additional
information is needed, such button can be used with comment. Each control has not more than one comment; the
only exception is the Trackbar_GR class with an unlimited number of comments.
• Any control can be either enabled or disabled; the only exception is the Label_GR class. Disability means that
some status or value associated with an object cannot be changed; label has no such value, so the Label_GR class
has no Enabled property. Disability does not affect the movability of an object, so enabled and disabled
objects are moved and resized in the same way. The basic GraphicalObject class does not include the field
and property to regulate the enabled / disabled status, so this is implemented in each class of controls.
• Enabled property of the class is used in three different places:
o In the Draw() method of the class because enabled and disabled objects must look differently.
o In the OnMouseDown() method to block those clicks which start the change of status (value). Be
careful not to block the start of resizing or moving as both actions are allowed for disabled objects.
o In the OnMouseUp() method to block the change of status (value) of the disabled object.
• Movable property is implemented in the basic GraphicalObject class. If control can be used with
comment, then, from my point of view, the movability of control and its comment must be changed synchronously.
In such case the Movable property must be overridden in the class of controls; here is the commonly used
variant.
new public bool Movable
{
get { return (base .Movable); }
set
{
base .Movable = value;
if (CommentExist)
{
m_cmnt .Movable = value;
}
}
}
• Movable property of the class is always used in the MoveNode() method. In case of an unmovable object,
the cursor is usually fixed at the point where it was pressed (ptPressed) and is not allowed to move anywhere
until the mouse release. If the mouse fixation is not implemented, then it looks like the mouse is absolutely
ignored. I am not sure about the better solution, so the majority of new classes use mouse fixation but there is one
class – CheckBox_GR – which allows to select the reaction and to compare both variants.
World of Movable Objects 838 (978) Chapter 22 Getting rid of controls
If you switch the Movable property, then you get the situation similar to green rectangle. If you turn the cyan rectangle
into unmovable, you can still call menu on this rectangle and in such case you will see the same menu as at figure 22.5 but
the Movable command will be unchecked.
For the transparent objects the situation is different. You can consider the TransparentForMover property as being
much stronger than Movable property. Each property changes the value of its own associated field in the
GraphicalObject class and has no influence on other fields. When an object is declared transparent for mover then it
is definitely unmovable because mover simply does not detect this object. In such case the value of Movable property is
not changed but is simply ignored; this is demonstrated by a cross in the cell of the information table (figure 22.5).
Transparency for mover affects an attempt to press such object with any button, so it is impossible to call menu for
transparent object. The result of calling menu on transparent rectangle depends on the point of the mouse press and on
positions of other objects.
• If in the situation from figure 22.5 you call menu on that part of yellow rectangle which overlaps the cyan
rectangle, then you will see the same menu as on figure 22.5. Yellow rectangle is transparent for mover, so mover
does not detect it but detects the next object at the pressed point – cyan rectangle – and calls menu for it. Thus,
there is no difference in calling menu on the free part of cyan rectangle or on its part covered by yellow rectangle.
• If you call menu on the part of yellow rectangle which does not overlap with other elements, then you will see
menu which can be called at any empty place of the form (figure 22.6).
Usually you call menu and then use one or
another of its commands. If you do not
like the result, then you can call the same
menu and either restore the previous
variant or make other changes. With the
switch of transparency, the retracing is not
so easy. You can easily declare any
rectangle transparent via the command of
its menu, but you have no chances to
declare non-transparency through the
same menu. No menu can be called on
transparent object, so non-transparency
cannot be reinstalled individually by
calling the needed rectangle. You have to
use some command from the higher (more
global) level and usually these are the
commands for a group of objects. In the Fig.22.6 An attempt to call menu on transparent object produces a menu
current example, there are no groups but for nontransparent object underneath or for empty places, if there
you have such commands in menu which are no objects. Here you see this case.
can be called at any empty place of the
form (figure 22.6).
In the Form_Rectangles_ForControls.cs, we have only individual elements and commands of their menu allow to change
properties individually for the pressed object. In all further examples of this chapter objects are demonstrated both as
individual elements and as members of a group. As I don’t see any sense in change of movability and transparency on
individual basis, then such commands are not used in further examples. But the change of movability and transparency
synchronously for all group members can be very useful, so this is demonstrated further on.
In addition to colored rectangles, there are other elements in the Form_Rectangles_ForControls.cs. There are two small
buttons; one of them has a question on top. There is also a rectangle with information and small cross in the corner of its
area (figure 22.7). Similar pair of objects is used in nearly every previous example of this book. However, the new
example uses not the same elements. More than 150 examples (forms) of this book use a
rectangular area with information which can be shown or hidden on user’s request. In all
those numerous examples, the rectangle with information is an InfoOnRequest object
paired with a small non-resizable Button control with a question mark on it; by clicking
this button you return the hidden information into view. In the
Form_Rectangles_ForControls.cs, the button is of the Button_Text class; the
rectangular object with information belongs to the ClosableText class. Fig.22.7 ClosableText
element is paired with a
Button_Text btnHelp; small Button_Text
ClosableText info; object.
World of Movable Objects 843 (978) Chapter 22 Getting rid of controls
The ClosableInfo class was discussed in the Form_ClosableInfo.cs (figure 5.4) of the Texts chapter. After
explaining the ClosableInfo class in that old example, I made a copy of the class, named it the InfoOnRequest
class, and included into the MoveGraphLibrary.dll. The union of InfoOnRequest object with a Button control
allowed me to hide short pieces of code into that library and to automate the work of the mentioned pair of elements. Both
mentioned classes – ClosableInfo and InfoOnRequest – have tuning forms which include controls. If I
demonstrate the ways to get rid of controls, then I can’t use controls for such explanation even in auxiliary forms, so we are
going to deal with elements and even forms which are familiar in view but different in design. On the way we can use a
couple of new useful things.
The ClosableText class is derived from the Text_Horizontal class.
public class ClosableText : Text_Horizontal
{
Mover supervisor;
PointF ptFixed;
RectangleF rcClose;
Form_ClosableTextParams formParams = null;
The appearance of the Mover field leaves no doubts that the adhered mouse technique is used with this class, but for what
purpose the adhered mouse can be used with a non-resizable object?
Any ClosableText object is movable but not resizable directly; the size is determined by the text and the used font. To
make any rectangle only movable, it would be enough to have one rectangular node in its cover, but there is a small square
in the corner of our rectangle and there is a special reaction on the click inside this square. To provide such behaviour, the
cover consists of two nodes; the first one covers the small square in the corner while another one covers the whole object.
public override void DefineCover ()
{
CoverNode [] nodes =
new CoverNode [] { new CoverNode (0, rcClose, Cursors .Hand),
new CoverNode (1, RectAround) };
cover = new Cover (nodes);
}
There is one thing in behaviour of the previously used ClosableInfo and InfoOnRequest classes which I don’t
like: when the mouse is pressed inside the square with a cross, then this pressed mouse can be moved anywhere without
moving an object. The whole information is not supposed to be moved by this small area and the information is not moved
by it, but a reaction is a bit awkward. You press the mouse at the small cross to close the information but then you decide to
move the mouse. You move the pressed mouse around the screen and all this time the information stays at its place. You
cannot keep the mouse pressed forever; at some moment you have to release it. At this moment the mouse can be still near
the cross or it can be far away from the whole information area, but regardless of the cursor point, the information will be
closed when you release the mouse. I think that if the mouse pressed inside the cross would be fixed at its point until the
moment of release, then the closing of information will be in better compliance with the whole press – release action and
there will be no questions about the disappearance of information area.
In many examples the adhered mouse technique is used when some restrictions on the movement are required. Usually
these are the cases for mouse movement along some line or inside some area. When the allowed area of mouse movement
is squeezed to a point, then the mouse is adhered to this point and cannot move an object. This technique is used in the
ClosableText class.
When any ClosableText object is pressed, then the ClosableText.Press() method is called.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
if (grobj is ClosableText)
{
World of Movable Objects 844 (978) Chapter 22 Getting rid of controls
info .Press (e .Location, iNode);
}
… …
If the pressed point is inside the cross area, then the mouse location is stored.
public void Press (Point ptMouse, int iNode)
{
if (iNode == 0)
{
ptFixed = ptMouse;
}
}
If you try to move the mouse pressed inside the small node, then the ClosableText.MoveNode() method blocks this
movement and keeps the cursor at the pressed point.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
supervisor .MouseTraced = false;
Cursor.Position = ParentForm.PointToScreen (Point.Round (ptFixed));
supervisor .MouseTraced = true;
}
else
{
Move (dx, dy);
bRet = true;
}
}
return (bRet);
}
It is very easy and I like this reaction much more than in the ClosableInfo and InfoOnRequest classes.
The ClosableText class has its own tuning form; the view of the Form_ClosableTextParams.cs (figure 22.8) is nearly
identical to the view of tuning forms for the ClosableInfo and
InfoOnRequest classes. (Compare it with figure 5.5 which
shows the tuning form for the ClosableInfo class). Though the
view and functionality of the tuning forms are identical, the
Form_ClosableTextParams.cs does not use controls but only their
graphical analogues. Elements which you see at figure 22.8 belong
to the Button_Drawing, CheckBox_GR, and Trackbar_GR
classes which are discussed in the following sections of the current
chapter. I do not like to introduce new elements prior to discussion
of their design, but here I have to break this rule as the information
of the ClosableText class is used in all further examples. Be
sure that buttons, check box, and track bar in this tuning form work
exactly as controls which they substitute.
Fig.22.8 Tuning form for ClosableText objects.
World of Movable Objects 845 (978) Chapter 22 Getting rid of controls
Push button
Any Button control may have on top either text or image. My graphical button has three variants for the top: text,
image, or drawing. The most important things like cover design, moving, and resizing do not depend on the top view, so
these things are implemented in the abstract class Button_GR.
public abstract class Button_GR : GraphicalObject
{
protected Form m_form;
protected TabPage m_tabpage = null;
protected Mover m_supervisor;
protected PointF [] ptsCorner;
protected Resizing resize;
protected bool bEnabled;
protected bool bPressed;
protected CommentToRect m_cmnt = null;
protected int minSide = 10;
protected SolidBrush brushVeil = new SolidBrush (
Auxi_Colours .TransparentColor (Color .LightGray, 0.35));
public abstract void Draw (Graphics grfx);
public abstract void IntoRegistry (RegistryKey regkey, string strAdd);
Three derived classes – Button_Text, Button_Image, and
Button_Drawing – implement only the things which are required for particular
top view. Each derived class has to provide two methods: one for drawing and
another one for saving into Registry.
The cover for the Button_GR class is exactly the same as in the
Rectangle_Restricted class (figure 22.9), so moving and resizing are
organized in a similar way. To avoid accidental disappearance of any button, there
is a limit on minimal allowed side (minSide = 10), but there is no upper limit,
so you will not see any RectRange parameter in any button constructor. Fig.22.9 Covers for buttons and
Constructor of the Button_GR class uses a rectangle to set the initial area of an auxiliary rectangle are
object but does not describe the range of possible change, so, by default, any button identical.
is fully resizable without upper limits.
public Button_GR (Form frm, Mover mvr, RectangleF rc)
{
m_form = frm;
m_supervisor = mvr;
float w = Math .Max (minSide, rc .Width);
float h = Math .Max (minSide, rc .Height);
ptsCorner = new PointF [4] {rc .Location, new PointF (rc .X + w, rc .Y),
new PointF (rc .X + w, rc .Y + h),
new PointF (rc .X, rc .Y + h) };
bEnabled = true;
bPressed = false;
resize = Resizing .Any;
}
Any new button is fully resizable. If another type of resizing is needed, it is set by the Resizing property.
public Resizing Resizing
{
get { return (resize); }
set
{
resize = value;
DefineCover ();
}
}
World of Movable Objects 846 (978) Chapter 22 Getting rid of controls
Any button can be associated with some comment of the CommentToRect class. Usually buttons with the text on top do
not need any additional comment as the text itself clearly informs about the purpose of the object. Buttons with image or
drawing often need some comment. For example, a tuning form may include identical buttons to change different color
parameters (figure 22.8) and it would be impossible to use them without some comments.
Comment uses the button as its parent element; comment position is changed automatically when the button is moved or
resized.
public override void Move (int dx, int dy)
{
for (int i = 0; i < 4; i++)
{
ptsCorner [i] .X += dx;
ptsCorner [i] .Y += dy;
}
InformComment ();
}
public void InformComment ()
{
if (CommentExist)
{
m_cmnt .ParentRect = MainElementArea;
}
}
Comment can be moved individually, so button with comment is a complex object which must be registered with the mover
only by the IntoMover() method.
new public void IntoMover (Mover mv, int iPos)
{
mv .Insert (iPos, this);
if (CommentExist)
{
mv .Insert (iPos, m_cmnt);
}
}
As you see from this code, comment is registered ahead of the associated button, so it must be drawn after the button. If
they overlap, then comment appears above the button, so there is no problem of the blocked comment which was discussed
with the CommentedControl class.
The main difference between three types of buttons is in their drawing, but before going into their details I want to remind
about some features of the programs based on controls and about some details of drawing the standard Button controls
because graphical buttons have to copy their behaviour and view as much as it is needed.
Controls were introduced more than 30 years ago. Prior to the mouse introduction, keyboard was the main users’
instrument. Years ago, even with the mouse at hand, a lot of users preferred to use keyboard to control applications, so
when controls were designed, they were supposed to provide (through visualization) the needed information for keyboard
control. For this purpose the idea of focus is used. Regardless of the number of controls in any form (long ago the word
form was not even used but only window or dialogue), at any particular moment only one control has focus and this is the
control which obtains commands from the keyboard. Focus can be moved from one control to another by pressing the Tab
key. All controls of the dialogue are set in order and the focus is moved from one control to another according to this order.
Focus is also moved to the control when it is clicked by a mouse, but if somebody prefers to use keyboard, then passing the
focus from one element to another in a dialogue with a significant number of controls can require a number of the Tab key
clicks. Focus is passed only between controls while graphical objects, which are considered by the system as elements of
lesser importance, are not included into this game. There are different ways to highlight focus; it can differ by the control
class and the system version, but usually there is an additional dotted frame inside control or wider (or of different color)
frame around.
From my point of view, controls are superfluous and whenever possible must be substituted with graphical analogues. Also
it is enough to have mouse as the only instrument of control in applications, so focus is not used. At any moment a button is
either enabled or disabled. Enabled button is either idle (not pressed) or pressed. Those three situations – disabled button,
pressed, or idle – must be visually distinguishable.
World of Movable Objects 847 (978) Chapter 22 Getting rid of controls
Button drawing consists of two parts: border drawing and drawing of the inner area. If I am not mistaken, buttons in the
earlier versions of Windows had frames which imitated the third dimension but the background of those buttons was
unicoloured. Later the frame was simplified to an ordinary line while the background got the color gradient from pale grey
at the top to pale blue at the bottom. These are the colors in normal situation while for the pressed button the same color
gradient is used in the opposite direction and such switch of colors perfectly signals the moment of button press or release.
Now we are ready to move from abstract buttons to real. The mentioned color gradient is used for Button controls with
text on top, so let us start with the Button_Text class.
public class Button_Text : Button_GR
{
string m_text;
Font m_font;
Color m_color;
ButtonBackType typeBack = ButtonBackType .Gradient;
SolidBrush brushBack;
Constructor of the Button_Text class has six parameters; three of them are for the base class constructor; three others
are for the text on top.
public Button_Text (Form frm, Mover mvr, RectangleF rc,
string txt, Font fnt, Color clr)
: base (frm, mvr, rc)
{
m_text = CheckedText (txt);
m_font = fnt;
m_color = clr;
brushBack = new SolidBrush (form .BackColor);
… …
Two background types were used throughout the years in Button controls: unicoloured background and color gradient.
By default, Button_Text objects are organized with the color gradient, but this class provides both views and lets users
decide about the particular view of any button at any moment. The difference in views exists only for enabled buttons.
public override void Draw (Graphics grfx)
{
Rectangle rc = Rectangle .Round (RectAround);
RectangleF rcClip = new RectangleF (rc .Left + 2, rc .Top + 2,
rc .Width - 4, rc .Height - 4);
Color clrText;
if (bEnabled)
{
ButtonState btn_state = bPressed ? ButtonState .Pushed
: ButtonState .Normal;
if (typeBack == ButtonBackType .Unicolor)
{
ControlPaint .DrawButton (grfx, rc, btn_state);
grfx .FillRectangle (brushBack, rcClip);
}
else
{
Auxi_Drawing .Button (grfx, rc, btn_state, form .BackColor);
}
clrText = m_color;
}
else
{
grfx .FillRectangle (Brushes .White, rc);
grfx .DrawRectangle (Pens .LightGray, rc);
clrText = Color .LightGray;
}
… …
World of Movable Objects 848 (978) Chapter 22 Getting rid of controls
The text is shown exactly as it is prepared. If the text includes special symbols of the line change, then such text is shown in
several lines. Usually the needed text does not have such symbols and then the text is shown in one line. In any case the
text is centered to the middle point of the button. If the text is longer than the button width and there are spaces inside the
text, then such text can be divided into parts at those places
and can be shown in several lines but only if the combined
height of all those text lines is less than the button height.
Maybe it is easier to understand this explanation by looking
at figure 22.10. First the button width was enough to show Fig.22.10 Text is changed to several lines if there are
the text in one line. Then I started to squeeze the button by spaces inside the text and the combined height
the side; the text was still shown in one line though its of several lines is less than the button height.
length was bigger than the button width. I stopped changing
the width and started to increase the height of the button. The text consists of two words separated by a space; when the
button height reached the height of two lines, only at that moment the text appeared in two lines.
To look more closely at the Button_Text objects, let us have another example with bigger number of such buttons.
File: Form_Button.cs
Menu position: Graphical objects – Replacement of controls – Push button
The Form_Button.cs example includes elements of different
classes but all of them have rectangular shape (figure 22.11).
There are five buttons in this example and one auxiliary
rectangle of the Rectangle_Restricted class.
Button_Drawing btnCover;
Button_Text btnRed, btnGreen, btnBlue,
btnDefaultView, btnPressed;
Rectangle_Restricted rcColored;
Info_Resizable info;*
Each standard control has a big number of associated events.
To organize the needed reaction on the click of a standard
button, you write the code for some method and then associate
this method with the Click event of the button.
Graphical buttons have no events of their own; as with all other Fig.22.11 The small button belongs to the
graphical objects, everything is organized through the Button_Drawing class; four other
MouseDown and MouseUp events of the form. When buttons – to the Button_Text class.
graphical button is pressed by the mouse then the Press()
method is called for this button. If the button is enabled and is pressed not on the border, then its new status is set through
the Pressed property and the button is shown in a different way.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
… …
else if (grobj is Button_GR)
{
Button_GR btn = grobj as Button_GR;
btn .Press (e .Location, iNode);
if (iNode == Button_GR .Node_FullArea && btn .Enabled)
*
Object with information in this form belongs to the Info_Resizable class. There is no cross in the corner to close
the information but the area is resizable and can be squeezed to a small size. Because some part of information, even a
small one, is always in view, then there is no need for extra button. The information area is resized by any border point
(side or corner); it also has its own tuning form which can be opened with a double click of the left button.
World of Movable Objects 849 (978) Chapter 22 Getting rid of controls
{
btn .Pressed = true;
Invalidate ();
}
}
… …
Reaction on the button click is provided at the moment of the mouse release.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is Button_GR)
{
Button_GR btn = grobj as Button_GR;
if (btn .Enabled)
{
if (fDist <= 3 && iNode == Button_GR .Node_FullArea)
{
long id = btn .ID;
… …
Depending on the released button, there are several types of reaction in the Form_Button.cs.
• When the button Default view is clicked, then the default view of the form is restored.
if (id == btnDefaultView .ID)
{
DefaultView ();
RenewMover ();
return;
}
• The smallest button switches ON / OFF the visualization of covers. (This button is of the Button_Drawing
class, so I am slightly jumping ahead, but, as you can see from the code several lines back, the analysis uses the
base class Button_GR.)
else if (id == btnCover .ID)
{
bShowCovers = !bShowCovers;
}
• Click of any button with the color name paints an auxiliary rectangle with the appropriate color.
else
{
if (id == btnRed .ID)
{
rcColored .Color = Color .Red;
}
else if (id == btnGreen .ID)
{
rcColored .Color = Color .Green;
}
else // id == btnBlue .ID
{
World of Movable Objects 850 (978) Chapter 22 Getting rid of controls
rcColored .Color = Color .Blue;
}
… …
When any button is released, its status must be changed again by the Pressed property and the view must be refreshed.
… …
if (btn .Pressed)
{
btn .Pressed = false;
Invalidate ();
}
… …
Button parameters can be changed through the commands of its
menu (figure 22.12). Menu has commands for changing text font
and color; it also allows to select the button resizing type and to
change the button background. There are two different commands
to change the background. One of them sets the standard gradient
of colors. Another allows to select the back color and
automatically makes the background unicoloured. There is a
menu command to switch button between the enabled / disabled
modes. In any real application this is regulated only by the logic Fig.22.12 Menu on the Button_Text objects
of a program, but I purposely added this command in order to
check (and to demonstrate) all possibilities of the Button_Text.Draw() method.
Now let us turn to another type of push button – the one with an image on top.
public class Button_Image : Button_GR
{
string strImage;
Image m_image;
Image file name is used as a parameter for the Button_Image constructor. The string can include either the full name
or relative path to the file; in any case the full path is stored in the designed button.
public Button_Image (Form frm, Mover mvr, RectangleF rc, string image_name)
: base (frm, mvr, rc)
{
if (File .Exists (image_name))
{
strImage = Path .GetFullPath (image_name);
m_image = Image .FromFile (strImage);
}
… …
In the Form_Rectangles_ForControls.cs (figure 22.2), there is one element of the Button_Image class – it is a small
button which is used to switch ON / OFF the drawing of covers.
void DefaultView ()
{
… …
btnCover = new Button_Image (this, mover, new RectangleF (20, 20, 30, 30),
"../../Resources/Covers.bmp");
… …
Drawing of the enabled button consists of border drawing and showing the image inside. Border is different for pressed and
not pressed button while the image is the same. Disabled button is shown as a flat element with the image draped by a gray
veil.
public override void Draw (Graphics grfx)
{
Rectangle rc = Rectangle .Round (RectAround);
RectangleF rcClip = new RectangleF (rc .Left + 2, rc .Top + 2,
rc .Width - 4, rc .Height - 4);
World of Movable Objects 851 (978) Chapter 22 Getting rid of controls
if (bEnabled)
{
ButtonState btn_state = bPressed ? ButtonState .Pushed
: ButtonState .Normal;
ControlPaint .DrawButton (grfx, rc, btn_state);
}
else
{
grfx .FillRectangle (Brushes .White, rc);
grfx .DrawRectangle (Pens .LightGray, rc);
}
Auxi_Drawing .ShowImage (grfx, m_image, rcClip,
SideAlignment .Center, SideAlignment .Center);
if (!bEnabled)
{
grfx .FillRectangle (brushVeil, rcClip);
}
if (CommentExist)
{
m_cmnt .Draw (grfx);
}
}
Image on top of any Button_Image object is centered both horizontally and
vertically. The Auxi_Drawing.ShowImage() method analyses the
image proportions and fits this image into the proposed rectangle. There is no Fig.22.13 Variants of the btnCover
image cropping; if the rectangle has different proportions, then you see empty strips on two opposite sides. Buttons are
usually resizable, so you can change it to the needed size and then change it a bit more according to the image proportions.
Figure 22.13 demonstrates four views of the btnCover: the original one, two variants of enlarging button in one
direction only, and the bigger version in which I tried to keep the original proportions.
File: Form_EnabledMovableTransparent.cs
Menu position: Graphical objects – Replacement of controls – Enabled, movable, and transparent elements
Next example has three sets of elements; each set consists of three buttons and rectangle (figure 22.14). First two sets
demonstrate Button_Text elements as stand alone and in group; another group uses buttons of the Button_Drawing
Fig.22.14 For solitary buttons, all three properties can be changed individually. Inside groups, Enabled property can
be used individually while Movable and TransparentForMover properties are applied synchronously
to all buttons of the group.
World of Movable Objects 852 (978) Chapter 22 Getting rid of controls
class. This example allows to play with the Enabled, Movable, and TransparentForMover properties. The last
two properties were discussed only 10 pages back in the very first example of this chapter. However, those rectangles from
the first example have no Enabled property which is very important for all controls.
The Button_Drawing class has a single field of its own.
public class Button_Drawing : Button_GR
{
Delegate_DrawInRect m_draw = null;
The simplest Button_Drawing constructor has three parameters for the base class plus the needed drawing method.
public Button_Drawing (Form frm, Mover mvr, RectangleF rc,
Delegate_DrawInRect drawmethod)
: base (frm, mvr, rc)
{
m_draw = drawmethod;
}
The drawing of the Button_Drawing objects is similar to the Button_Image objects.
public override void Draw (Graphics grfx)
{
Rectangle rc = Rectangle .Round (MainElementArea);
RectangleF rcClip = new RectangleF (rc .Left + 2, rc .Top + 2,
rc .Width - 4, rc .Height - 4);
if (bEnabled)
{
ButtonState btn_state = bPressed ? ButtonState .Pushed
: ButtonState .Normal;
ControlPaint .DrawButton (grfx, rc, btn_state);
}
else
{
grfx .FillRectangle (Brushes .White, rc);
grfx .DrawRectangle (Pens .LightGray, rc);
}
if (m_draw != null)
{
grfx .SetClip (rcClip);
m_draw (grfx, rcClip);
grfx .ResetClip ();
if (!bEnabled)
{
grfx .FillRectangle (brushVeil, rcClip);
}
}
if (CommentExist)
{
m_cmnt .Draw (grfx);
}
}
The drawing method which is passed on initialization has two parameters. The second one is the button area without frame.
The result of drawing is shown in this area, but it does not mean at all that the whole drawing by your method must be done
strictly inside this rectangle. Usually it is so and for simple cases like drawing an arrow on top of a button it is done inside
the provided rectangle and everything looks fine. But there are situations when the drawing on the button has to be some
part of much bigger picture. In such case you calculate the area of the big picture in relation to the passed rectangle and
draw the full picture without thinking of any limitations; the clipping is done automatically (see the code above), so only
that part of the drawing which fits into the declared rectangle will appear on the screen. In such way not only the drawing is
easier, but the drawing can depend on the button position, on relative position of the button and other screen elements, and
on many other things. The size of the button can also depend on its position (the unneeded button can be moved to the side
World of Movable Objects 853 (978) Chapter 22 Getting rid of controls
of the screen and shrink) and the drawing can depend on the button size with more details appearing in bigger area. A lot of
interesting things can be done with the Button_Drawing objects.
Let us return to the Form_EnabledMovableTransparent.cs example and to those properties which we are going to
investigate. On initialization, every button is enabled, movable, and not transparent. The first value is provided by the
button class; two others are provided by the base class GraphicalObject. (Just for information: the same combination
of values is provided on initialization of all other “controls”.) In real applications, the Enabled value is determined by the
logic of application while Movable and TransparentForMover values can be regulated directly by users. Both
properties regulate the movability but have different side effects. I would not recommend to place at users’ disposal both of
them but only one and the choice depends on the logic of application. In the current example, all three properties are
regulated via the commands of several menus.
For solitary buttons all three properties are regulated through the personal menu (figure 22.15a). Certainly, the last
command can be used only once. No menu can be called on transparent button, so in order to change its transparency again
you have to apply to upper level which in this case means calling menu at any empty place.
Fig.22.15a Menu on solitary button Fig.22.15b Menu on button Fig.22.15c Menu on group
in group
Menu for buttons inside groups has a single
command which regulates enabled / disabled
status (figure 22.15b); two other properties are
regulated synchronously for all elements of the
group, so the needed commands are included into
the group menu (figure 22.15c). Further on you
are going to see menus with similar commands Fig.22.16 Information on top of the Button_Drawing element
for the groups with other controls; each menu has perfectly explains the consequences of making an
commands for synchronous change of Movable element transparent for mover.
and TransparentForMover properties.
Each button of this example informs about its currently set values, but the Button_Drawing elements make it much
better because they clearly explain the consequences of transparency. Three buttons on the left side of figure 22.16 belong
to the Button_Text class and are used as stand alone elements; object on the right belongs to the Button_Drawing
class and is used inside group. All four buttons are transparent for mover, so an attempt to press any of them with a mouse
has the same NULL effect for the button itself. The right button makes it absolutely clear that for the transparent element
two other important properties are simply ignored. Enabled property still determines the view and this is done regardless
of other properties, but transparent for mover element is absolutely ignored by the mouse.
Transparent element is unmovable and, as a designer, you have a choice of selection between two types of immovability and
the obligation to declare and explain this immovability to users of your program. Certainly, the explanation like
“transparent for mover” cannot appear in the real program. For users, it is only movable / unmovable choice, but you have
to decide, what kind of immovability is preferable. For elements from figure 22.16, the buttons on the left are unmovable,
but pressing them with the right button will bring on top a menu associated with all empty places. When you press the right
element with the left mouse button, then you start the group movement because this transparent element belongs to the
group. For the same reason, the right mouse click on this element will open the group menu – the one from figure 22.15c
but with the last command also checked.
There is one more aspect of movability which is general for buttons and all other controls introduced in this chapter but
which wasn’t used in all graphical elements demonstrated in the previous examples of this book. When I started to work on
the algorithm of movability, the question of its regulation by users was not important at all. All objects had to be movable,
so the main question was in design of the algorithm which would be suitable for any element. The question of movability
regulation became important when scientists began to use very complex applications with a lot of movable objects. Exactly
the same problem arose inside the groups packed with a lot of elements, so the problem had to be solved in the same way
for all areas. Transparency of elements looked like the best solution for groups, so I began to use it more and more.
World of Movable Objects 854 (978) Chapter 22 Getting rid of controls
When I began to use the adhered mouse technique, I used it mostly for restricted resizing or moving. Only later I applied
the same technique to the unmovable objects in order to keep the cursor on the pressed point. At the same time, there are
still many elements which were designed years ago and in which all the latest ideas are not implemented. If you declare
such old element unmovable and press it with the mouse, there is no visual effect; an element stays at its place, while the
pressed mouse can be moved around the screen without limitations. All controls which are demonstrated in this chapter
were designed throughout a relatively short period of time and I tried to design them under the same common rules. They
all use adhered mouse for resizing and if the unmovable control is pressed with a mouse, then the cursor is fixed at the press
point. Just remember this when you decide to use in application these controls and some graphical elements of your own
design. It is much better when all objects of some application behave in a similar way.
World of Movable Objects 855 (978) Chapter 22 Getting rid of controls
Label
File: Form_Label.cs
Menu position: Graphical objects – Replacement of controls – Label
Label is a very simple control which was designed to display a text. If the text is too long to be shown in one line, then in
the original Label control it is wrapped and shown in several lines. This is a slightly different logic from what I need in
my programs and what I am going to demonstrate.
Horizontal text with (or without) a frame around its area was demonstrated at the beginning of this book (class
Text_Horizontal_Demo, figure 5.2), but there is one reason to use something different. The Text_Horizontal
class calculates the size of the text and then puts the frame around its area. The rectangle is movable but not resizable. If
two labels from the old program (figure 22.1) are substituted by the Text_Horizontal objects, then they will have
different sizes and those sizes will be defined by their inner texts.
Users have different aesthetic preferences. Some users of the new
example might agree with different length of frames for
neighbouring labels while others would prefer to see the lined
frames of equal length. To provide this, the area must be
resizable and the size (at least the length) of the frame must be
independent of the inner text.
Four objects of the Label_GR class are shown in the
Form_Label.cs (figure 22.17). Texts inside two labels represent
two views of some time period in the same way as used in the
photo archive application (figure 22.1). These two labels together
with their comments are united into a group.
The Label_GR class is used to show a single text line and there
is no correlation between the text length and the width (length) of
such object; element of the new class is resizable by the left and
right borders regardless of the inner text.
public class Label_GR : GraphicalObject Fig.22.17 Coordinates and sizes of the caught label
{ can be shown throughout the moving /
Form m_form; resizing.
TabPage m_tabpage = null;
Mover m_supervisor;
PointF [] ptsCorner;
string m_text;
Font fntText;
Color clrText;
SideAlignment alignText;
RectFrameView typeFrame;
Pen penLine = new Pen (Color .DarkGray);
SolidBrush brushBack;
int minLength = 20;
bool bResizable;
CommentToRect m_cmnt;
Graphical analogue of the Label control is fairly easy in design. The rectangular area is movable by any inner point.
Area height is determined by the used font with an addition of several pixels for a frame. Area width (or length) can be
changed by user, so there are two strip nodes on the left (node 0) and right (node 1) borders.
public override void DefineCover ()
{
float wHalf = Resizable ? 3 : 0;
CoverNode [] nodes = new CoverNode [3] {
new CoverNode (0, ptsCorner [0], ptsCorner [3], wHalf, Cursors .SizeWE),
new CoverNode (1, ptsCorner [1], ptsCorner [2], wHalf, Cursors .SizeWE),
new CoverNode (2, MainElementArea) };
cover = new Cover (nodes);
if (TransparentForMover)
World of Movable Objects 856 (978) Chapter 22 Getting rid of controls
{
cover .SetBehaviourCursor (Behaviour .Transparent, Cursors .Hand);
}
}
The adhered mouse technique is used throughout the resizing; the Press() method is called at the moment of the mouse
press. If the strip node is pressed, then cursor is switched exactly on the border to be moved and two points – ptEndLeft
and ptEndRight – are calculated. These are two end points of the horizontal segment along which the cursor of the
pressed mouse can be moved.
public void Press (Point ptMouse, int iNode)
{
if (iNode == 2)
{
ptFixed = ptMouse;
}
else
{
PointF ptNearest;
if (iNode == 0)
{
ptNearest = Auxi_Geometry .NearestPointOnSegment (ptMouse,
ptsCorner [0], ptsCorner [3]);
ptEndRight = new PointF (ptsCorner [1].X - minLength, ptNearest.Y);
ptEndLeft = new PointF (ptEndRight .X - 4000, ptNearest .Y);
}
else // iNode == 1
{
ptNearest = Auxi_Geometry .NearestPointOnSegment (ptMouse,
ptsCorner [1], ptsCorner [2]);
ptEndLeft = new PointF (ptsCorner [0] .X + minLength, ptNearest.Y);
ptEndRight = new PointF (ptEndLeft .X + 4000, ptNearest .Y);
}
AdjustCursorPosition (ptNearest);
}
}
Four context menus are used in the Form_Label.cs. The one to be called at any empty place includes a command to
regulate the appearance of the special information area. While writing about engineering and scientific programs, I have
mentioned that in some situations scientists need to see the exact position and size of the currently moved or resized plotting
area and I have demonstrated the use of a small informative area which appears next to the cursor of the pressed mouse and
moves with it. The same simple instrument is used in the current example. This area with information (yellow rectangle at
figure 22.17) appears only on user’s request. In
the working application, this additional
information is shown next to the mouse cursor.
When the form image is taken into Clipboard,
the mouse cursor disappears but the information
stays at the same place next to vanished cursor, so
the image at figure 22.17 can be a bit confusing.
Label_GR element is relatively simple but has
enough parameters which can be changed. In the
Form_Label.cs, the change of these parameters
is organized via the commands of menu
(figure 22.18) which can be called at any label.
Usually, with such a number of tuneable
parameters, it is better to organize a tuning form,
but I decided not to do it. The design of tuning
form based entirely on graphical objects is
demonstrated and discussed closer to the end of
this chapter. Fig.22.18 Menu on label has several submenus.
World of Movable Objects 857 (978) Chapter 22 Getting rid of controls
The Label_GR class was simple in design. It was so simple that I quickly wrote the code, checked the new class,
organized an example for demonstration, and moved to more interesting graphical controls. Only several months later I
understood that there was an elementary but principal mistake with this class. The mistake was so obvious, that, as I said, it
took me several months to see it.
Label shows one line of text, so the required area height is determined by the font with an addition of several pixels for
frame. Easy? Yes. Correct? NO!!!
In user-driven application, there must be no correlation between the font selected by user and the size of an element. By
being too quick in design I broke this principal rule. In reality the label height must be changeable by user and be
independent of the used font. Well, both variants can be useful, so there must be both variants with an easy change from
one to another. There is nothing new in such design because it was already demonstrated for buttons. User easily changes
the button Resizing status; the same can be organized for labels. Maybe I do it for the next version, if I’ll not forget.
World of Movable Objects 858 (978) Chapter 22 Getting rid of controls
NumericUpDown
File: Form_NumericUpDown.cs
Menu position: Graphical objects – Replacement of controls – NumericUpDown
The standard NumericUpDown control
has two instruments to change the value:
there are two small buttons to increase and
decrease value by a predetermined step
and there is a direct editing of the value.
In the new NumericUpDown_GR class,
there is no editing, but there is a small
slider which allows to go quickly through
the whole range of values. My experience
with NumericUpDown controls is too
small to insist that such addition suits very
well for any required range. The classical
NumericUpDown controls were used in
my applications not too often and the
range of values was never wide; for such
range the NumericUpDown_GR class
works perfectly.
It happened so that all the
NumericUpDown_GR objects at
figure 22.19 have sliders and nearly all of
them, with the exception of only two,
have those sliders on the left side. These
two exceptions prove that sliders can be Fig.22.19 Value of NumericUpDown_GR element can be changed either
positioned in different ways. Also slider by clicking two buttons or by moving the slider.
can be easily taken out of view and then
the new element looks exactly like its classical analogue.
public class NumericUpDown_GR : GraphicalObject
{
Form m_form;
TabPage m_tabpage = null;
Mover m_supervisor;
PointF [] ptsCorner;
Resizing m_resize;
int minVal, maxVal, curVal, stepVal;
Font fntValue;
SideAlignment alignValue;
Side sideSlider;
bool bSliderShow = true;
NumericSliderShape shapeSlider = NumericSliderShape .Rectangle;
Cover for NumericUpDown_GR object consists of 12 nodes (figure 22.20).
• For enabled element with shown slider, node 0 covers the slider;
otherwise this node is squeezed to zero and becomes inaccessible.
The shape of this node is determined by the slider shape.
• Four circular nodes on the corners (1 – 4) and four strip nodes along
the sides (5 – 8) cover the whole border and are used for resizing.
There are four standard variants of resizing (the same which were
discussed with buttons) so, depending on the selected type, some or
all of these eight nodes can be squeezed to zero size.
Fig.22.20 Numbering of nodes
• Rectangular nodes 9 and 10 cover two buttons with arrows.
• Rectangular node 11 covers the whole area and is used for moving an object.
World of Movable Objects 859 (978) Chapter 22 Getting rid of controls
Occasionally the numbering of nodes can be changed, so it’s better to rely on the names of the crucial nodes:
Node_Slider, Node_ArrowUp, and Node_ArrowDown.
Possible slider shapes are determined by the NumericSliderShape enumeration.
public enum NumericSliderShape { Circle, Rectangle, Strip };
Slider shape defines the shape of the first node; mouse cursor for this node depends on the side where the slider is
positioned. Slider is always on the side of rectangle; the slider can be moved and the side can be moved. Cursor shape has
to give a tip about the element which can be caught and moved at any moment. The side can be moved only orthogonally to
its position while the slider can be moved only along the side, so cursor shapes over these two parts are always different.
At the beginning of this chapter I mentioned three places where the enabled / disabled status must be checked. The
NumericUpDown_GR class is an exception from that rule and for this class the enabled / disabled status affects the cover
design: for disabled elements, the first node does not cover the slider but is squeezed to zero. For enabled object and with
the slider in view, the calculation of the first node is easy but depends on the slider side and shape, so it is a bit lengthy to be
shown here. The remaining part of the DefineCover() method is simple. To make the resizing by corners easier, radius
of circular nodes is slightly increased from the default value.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [12];
if (bSliderShow && bEnabled)
{
… …
}
else
{
nodes [0] = new CoverNode (0, ptsCorner [3], 0f, Cursors .Hand);
}
float rCorner = (m_resize == Resizing .Any) ? 5 : 0;
float wHalf_WE =
(m_resize == Resizing .Any || m_resize == Resizing .WE) ? 3 : 0;
float wHalf_NS =
(m_resize == Resizing .Any || m_resize == Resizing .NS) ? 3 : 0;
nodes [1] = new CoverNode (1, ptsCorner [0], rCorner, Cursors .SizeNWSE);
nodes [2] = new CoverNode (2, ptsCorner [1], rCorner, Cursors .SizeNESW);
nodes [3] = new CoverNode (3, ptsCorner [2], rCorner, Cursors .SizeNWSE);
nodes [4] = new CoverNode (4, ptsCorner [3], rCorner, Cursors .SizeNESW);
nodes [5] = new CoverNode (5, ptsCorner [0], ptsCorner [3], wHalf_WE,
Cursors .SizeWE);
nodes [6] = new CoverNode (6, ptsCorner [1], ptsCorner [2], wHalf_WE,
Cursors .SizeWE);
nodes [7] = new CoverNode (7, ptsCorner [0], ptsCorner [1], wHalf_NS,
Cursors .SizeNS);
nodes [8] = new CoverNode (8, ptsCorner [3], ptsCorner [2], wHalf_NS,
Cursors .SizeNS);
nodes [9] = new CoverNode (9, rcBtnUp, Cursors .Hand);
nodes [10] = new CoverNode (10, rcBtnDown, Cursors .Hand);
nodes [11] = new CoverNode (11, ptsCorner);
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviourCursor (Behaviour .Transparent, Cursors .Hand);
}
}
The adhered mouse is used for resizing and for slider moving; in both cases the NumericUpDown_GR.Press() method
calculates the end points of line segment along which the cursor of the pressed mouse can be moved. When one or another
small button with arrow is pressed, the same method saves the pressed point (ptFixed) and then the cursor is kept at this
point until the mouse release; same thing happens if an unmovable element is pressed at any point.
World of Movable Objects 860 (978) Chapter 22 Getting rid of controls
public void Press (Point ptMouse, int iNode)
{
PointF ptNearest;
if (iNode == Node_Slider) // slider
{
… …
else if (iNode == Node_ArrowUp || iNode == Node_ArrowDown)
{
ptFixed = ptNearest = ptMouse;
}
else
{
ptFixed = ptNearest = ptMouse; // is used for non-movable and not transparent
}
AdjustCursorPosition (ptNearest);
}
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
… …
else if (iNode == Node_ArrowUp || iNode == Node_ArrowDown) // buttons
{
AdjustCursorPosition (ptFixed);
bRet = false;
}
else // iNode == 11
{
if (Movable)
{
Move (dx, dy);
bRet = true;
}
else
{
AdjustCursorPosition (ptFixed);
bRet = false;
}
}
}
return (bRet);
}
Of all NumericUpDown_GR elements
shown at figure 22.19 only one is used
without comment and this was done in
order to demonstrate such possibility.
Comment is the only way to inform about
the value to be changed, so usually such
element has to have a comment.
Menu on NumericUpDown_GR objects
has two submenus of which only one is
shown at figure 22.21. Another submenu
allows to select the needed type of resizing,
so it has four standard variants. The
submenu shown at figure 22.21 is available
only for objects inside group.
Fig.22.21 Menu on NumericUpDown_GR elements
World of Movable Objects 861 (978) Chapter 22 Getting rid of controls
Parameters of any NumericUpDown_GR object can be changed in the special tuning form which is called by the first
command of this menu. This form – Form_Tuning_NumericUpDown.cs (figure 22.22) – allows to change:
• Font and alignment of the value
• Side, shape, and color of the slider. Certainly,
these changes are allowed only for the case when
slider is shown, which is also regulated inside this
form.
One interesting feature of this form is the set of objects
which are used for its design. As I already mentioned,
there are no controls in this application, so whatever you
see here can be only their graphical analogues.
There are buttons of the Button_Drawing class; this
class was already discussed.
There is a CheckBox_GR element and three objects of
the ComboBox_DDList class; both classes are
discussed a bit further.
Fig.22.22 Tuning form for NumericUpDown_GR objects
World of Movable Objects 862 (978) Chapter 22 Getting rid of controls
Combo box
File: Form_ComboBox.cs
Menu position: Graphical objects – Replacement of controls – ComboBox (with DropDownList)
Combo box unites a text box with a list
box. Text box can be editable or non-
editable; list can be always in view or can
be dropped down and then hidden again
after selection. In the standard
ComboBox control, three of these four
variants are represented; the needed
variant is determined by the value selected
from the ComboBoxStyle enumeration.
My graphical combo box uses non-
editable text box, so it is the case with the
DropDownList style; this is even
mentioned in the name of the new class.
Rectangular area of the
ComboBox_DDList objects can be
resized in a standard way by its corners
and sides, so the height of the list box can
be not enough to show all the strings; in Fig.22.23 Graphical combo box for the case with DropDownList.
such case a scroll bar appears next to the
right border. The height of the list cannot be made bigger than needed to show all the strings, so you’ll never see an empty
area beyond the last string.
In the Form_ComboBox.cs, elements of the ComboBox_DDList class are used with and without comments; there are
also stand alone objects and a whole set of combo boxes united into a group (figure 22.23). Only one combo box with the
names of colors visually demonstrates the effect of changing selection in the list.
public class ComboBox_DDList : GraphicalObject
{
Form m_form = null;
TabPage m_tabpage = null;
Mover m_supervisor;
PointF [] ptsCorner;
Resizing m_resize;
bool bDroppedDown, bArrowPressed, bAutoHideOnClickSelect;
int m_iSel;
string [] m_strs;
Font m_fontHead, m_fontList;
bool bEnabled = true;
CommentToRect m_cmnt;
float wBtn = SystemInformation .VerticalScrollBarWidth;
float minWidth = SystemInformation .VerticalScrollBarWidth * 5 / 2;
float minListHeight = 30;
Colors of an object are determined by the values from the standard SystemColors class. Two different fonts can be used
for two parts; from my point of view, the increased font for the text box part can be very useful.
Geometry of the combo box is described by its four corners (PointF [] ptsCorner); numbering of points starts from
the top left corner and goes clockwise. Text box part has a special arrow square on the right. The width of this area (wBtn)
is determined by one value from the SystemInformation class. The height of this area cannot be less than the width
but is increased if the big font is used for the text box.
Cover of any ComboBox_DDList object consists of 12 nodes.
• If the scroll bar is in view, it is covered by rectangular node 0; otherwise this node is squeezed to zero.
• Rectangular node 1 covers the arrow square.
World of Movable Objects 863 (978) Chapter 22 Getting rid of controls
• Four circular nodes on the corners (2 – 5) and four strip nodes along
the sides (6 – 9) cover the whole border and are used for resizing.
There are four standard variants of resizing so, depending on the
selected type, some or all of these eight nodes can be squeezed to zero.
• With the list in view, node 10 covers the list area.
• The last node (number 11) covers the combined area of the text box
and the list. Certainly, when the list is in view, the bigger part of this
node is blocked by the previous one. Part of the node is always
blocked by the node 1. As a result, only a part of this node over the
text box can work, but it is easier to define this node by the known
points from the ptsCorner[] array than to calculate the unblocked
area. Fig.22.24 Numbering of nodes for
public override void DefineCover () ComboBox DDList cover
{
float wHalf_WE =
(m_resize == Resizing .Any || m_resize == Resizing .WE) ? 3 : 0;
CoverNode [] nodes;
if (bDroppedDown)
{
float rCorner = (m_resize == Resizing .Any) ? 5 : 0;
float wHalf_NS =
(m_resize == Resizing .Any || m_resize == Resizing .NS) ? 3 : 0;
nodes = new CoverNode [] {
new CoverNode (0, ptsCorner [1], 0f, Cursors .SizeNS),
new CoverNode (1, ArrowSquare, Cursors .Hand),
new CoverNode (2, ptsCorner [0], rCorner, Cursors .SizeNWSE),
new CoverNode (3, ptsCorner [1], rCorner, Cursors .SizeNESW),
new CoverNode (4, ptsCorner [2], rCorner, Cursors .SizeNWSE),
new CoverNode (5, ptsCorner [3], rCorner, Cursors .SizeNESW),
new CoverNode (6, ptsCorner [0], ptsCorner [3], wHalf_WE,
Cursors .SizeWE),
new CoverNode (7, ptsCorner [1], ptsCorner [2], wHalf_WE,
Cursors .SizeWE),
new CoverNode (8, ptsCorner [0], ptsCorner [1], wHalf_NS,
Cursors .SizeNS),
new CoverNode (9, ptsCorner [3], ptsCorner [2], wHalf_NS,
Cursors .SizeNS),
new CoverNode (10, new PointF [] {
new PointF (ptsCorner [0] .X, ptsCorner [0] .Y + hHead),
new PointF (ptsCorner [1] .X, ptsCorner [0] .Y + hHead),
new PointF (ptsCorner [2] .X, ptsCorner [2] .Y),
new PointF (ptsCorner [3] .X, ptsCorner [3] .Y)},
Cursors .Hand),
new CoverNode (11, ptsCorner, Cursors .SizeAll) };
if (hStrings > Height_listpart - 2)
{
nodes [0] = new CoverNode (0, rcSlider, Cursors .SizeNS);
}
}
… …
This is the cover design in case of the dropped down list (figure 3.20). When the list is hidden, the number of nodes does
not change but two thirds of them are squeezed to zero. Of the four remaining nodes, two more nodes are squeezed to zero
if horizontal resizing is prohibited.
… …
else
{
// only four nodes are used (1, 6, 7, 11)
World of Movable Objects 864 (978) Chapter 22 Getting rid of controls
nodes = new CoverNode [] {
new CoverNode (0, ptsCorner [0], 0f, Cursors .Hand),
new CoverNode (1, ArrowSquare, Cursors .Hand),
new CoverNode (2, ptsCorner [0], 0f, Cursors .Hand),
new CoverNode (3, ptsCorner [1], 0f, Cursors .Hand),
new CoverNode (4, ptsCorner [2], 0f, Cursors .Hand),
new CoverNode (5, ptsCorner [3], 0f, Cursors .Hand),
new CoverNode (6, ptsCorner [0], ptsCorner [3], wHalf_WE,
Cursors .SizeWE),
new CoverNode (7, ptsCorner [1], ptsCorner [2], wHalf_WE,
Cursors .SizeWE),
new CoverNode (8, ptsCorner [0], 0f, Cursors .Hand),
new CoverNode (9, ptsCorner [0], 0f, Cursors .Hand),
new CoverNode (10, ptsCorner [0], 0f, Cursors .Hand),
new CoverNode (11, ptsCorner, Cursors .SizeAll) };
}
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviourCursor (Behaviour .Transparent, Cursors .Hand);
}
}
Occasionally the numbering of nodes can be changed, so it’s better to rely on the names of the crucial nodes:
Node_Slider, Node_Arrow, and Node_List.
There are two main changes in drawing of the ComboBox_DDList objects in comparison with the standard ComboBox
control; both changes are caused by the lack of focus.
• When the list of the ComboBox control is hidden and this combo box has focus, then the text in the text box is
highlighted. In the ComboBox_DDList objects this text is always shown in standard colors.
• When mouse moves across the list of the ComboBox control, then the current line under cursor is highlighted; this
is not duplicated in the ComboBox_DDList objects because I prefer the currently selected line to be highlighted
all the time. If I decide to reproduce the logic of the ComboBox control, then, instead of the node number 10, I
would organize an array of nodes (one node per line starting from the top) and then easily get the number of the
node under cursor with the help of the MoverPointInfo class. The use of this class is demonstrated in a couple
of examples.
When a ComboBox_DDList object is pressed with the left button, the ComboBox_DDList.Press() method is
called. Scrolling of the list works in the same way as in the ListBox_GR class. Resizing by corners and sides works in
exactly the same way as in other rectangular objects and it was already demonstrated for Button_GR and ListBox_GR
classes. The whole combo box can be moved by any inner point except the area of the small button with arrow. This small
area is used only to change the list view (shown or hidden), so when node 1 is pressed, then the pressed point is remembered
(ptFixed).
public void Press (Point ptMouse, int iNode)
{
PointF ptNearest;
… …
else if (iNode == Node_Arrow)
{
ptFixed = ptNearest = ptMouse;
}
… …
Further action is described in the OnMouseDown() and OnMouseUp() methods of the Form_ComboBox.cs.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button , bShowAngle))
{
World of Movable Objects 865 (978) Chapter 22 Getting rid of controls
if (e .Button == MouseButtons .Left)
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
… …
else if (grobj is ComboBox_DDList)
{
ComboBox_DDList combo = grobj as ComboBox_DDList;
combo .Press (e .Location, iNode);
if (combo .Enabled && iNode == ComboBox_DDList .Node_Arrow)
{
combo .ArrowPressed = !combo .ArrowPressed;
combo .DroppedDown = !combo .DroppedDown;
Invalidate ();
}
… …
If the arrow button (node 1) of the enabled object is pressed:
• The ComboBox_DDList.ArrowPressed property gets opposite value and the background of the arrow
square gets different view. (This area looks like a button with the gradient of colors. When the square is pressed,
the same colors are used in reverse order.)
• The ComboBox_DDList.DroppedDown property gets opposite value and the list either appears or disappears.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is ComboBox_DDList)
{
ComboBox_DDList combo = grobj as ComboBox_DDList;
if (iNode == ComboBox_DDList .Node_Arrow)
{
combo .ArrowPressed = false;
Invalidate ();
}
else if (iNode == ComboBox_DDList .Node_List)
{
if (combo .Enabled && fDist <= 3)
{
combo .SelectByPoint (e .Location);
if (combo .AutoHideOnClickSelect)
{
combo .DroppedDown = false;
}
if (combo .ID == comboColors .ID)
{
rcColored .Color = Auxi_Colours .RainbowColors
[combo .SelectedIndex];
}
Invalidate ();
}
}
}
… …
World of Movable Objects 866 (978) Chapter 22 Getting rid of controls
• If arrow square (node 1) is released, then the ComboBox_DDList.ArrowPressed property gets the false
value and the background of the arrow square gets normal view.
• If the list area (node 10) of the enabled object is released and this is not an end of movement but a selection
(fDist <= 3), then the selected line is determined by the ComboBox_DDList.SelectByPoint()
method. In the standard ComboBox control, the list is automatically hidden after new selection; in the new class,
the list hiding is regulated through the ComboBox_DDList.AutoHideOnClickSelect property; its value
can be changed through the command of menu on combo box. There can be some “outer” reaction to the change
of the selected line; in the current example it happens only with the comboColors object – the auxiliary
rectangle is painted with the selected color.
Prevention of the automatic list hiding may look like a small feature but in some cases it can be very helpful. It is not a rare
situation when each selection changes something on the screen and before making the final decision you would like to see
and visually compare (or estimate) all the possibilities. This process becomes really tiresome if each time you have to start
with an arrow click in order to see the list. The usefulness of the new feature is demonstrated with the group of combo
boxes, but I’ll return to this feature a bit later. Now let us pay attention to one unusual aspect of using comment in the
ComboBox_DDList class.
While working on design of graphical controls, I preferred to use the same order of steps: first the control itself was
designed; then a comment of the CommentToRect class was added. The main elements of the Button_GR,
Label_GR, NumericUpDown_GR, and ListBox_GR objects have rectangular shape, so the use of the
CommentToRect element with them is absolutely natural. The CommentToRect class was designed years ago in order
to have comments that could be used with any rectangular object. This class was widely used throughout the years and I
never had any problems in using such comment with any rectangular element. So, when I started to add comments to the
new combo boxes, I did not expect any difficulties, but then I understood that there was a problem.
From the very beginning when the CommentToRect class was designed years ago and up till this moment it was always
used with manually resizable rectangles. Relative position of any CommentToRect object is determined by two
coefficients and while the size of rectangle is changed by moving its borders, the comment position is recalculated by using
those two coefficients. Manual resizing means smooth change of rectangle sizes, so the calculated central point of comment
also moves smoothly. Throughout all the previous years there was not a single case of abrupt change of rectangle, so there
was not a single case of comment jump from one position to another. The combo box area can change abruptly, so
something different must be thought out for comments of the combo boxes.
When a ComboBox_DDList object is moved or resized, then the associated comment is informed about the changes
through the CommentToRect.ParentRect property and accordingly changes its position.
public override void Move (int dx, int dy)
{
for (int i = 0; i < 4; i++)
{
ptsCorner [i] .X += dx;
ptsCorner [i] .Y += dy;
}
… …
InformComment ();
}
public void InformComment ()
{
if (CommentExist)
{
m_cmnt .ParentRect = MainElementArea;
}
}
There is absolutely nothing new in such change of comment position; you can find similar code lines in those examples
from the main book where a CommentToRect element is used as a part of some complex object. In case of the Move()
method, the combo box sizes do not change at all; in case of resizing or of the font change (for example, through menu
command), the sizes change smoothly. In all these cases the change of comment position by ParentRect property works
perfectly and does not cause any problems. But combo box has one more possibility of changing its geometry by showing
or hiding the list of strings. Depending on the number of strings in the list, such change can be very abrupt. If there are N
World of Movable Objects 867 (978) Chapter 22 Getting rid of controls
strings in the list, then the height change can be described by the ratio 1 : (N + 1) and the use of the same ParentRect
property throughout such change can cause problems.
Appearance / disappearance of the list happens when an arrow is just pressed with a mouse, so we have to start from the
OnMouseDown() method; the code can be seen two pages back.
The combo box view is regulated by the
ComboBox_DDList.DroppedDown property and the
associated comment is informed by this property about the
rectangular area of the main part. There are two ways to
associate positions of the main rectangle and its comment; in
each case comment gets the new rectangle and the difference is
in reaction on this new value:
• When the CommentToRect.ParentRect
property is used, then two existing coefficients are
used to change the comment position.
• When the CommentToRect.SetParentRect()
method is used, the comment position is not changed Fig.22.25 The SetParentRect() method is used
but the new coefficients are calculated. when comment is initially inside one of two
marked areas; in all other cases the
At last I came to the understanding that the selection of the ParentRect property is used.
needed case must be based on the initial comment position (on
the position of its central point). Figure 22.25 shows two areas (they are bordered by the red lines) for which the
SetParentRect() method is called; in all other cases the ParentRect property is used.
public bool DroppedDown
{
get { return (bDroppedDown); }
set
{
… …
if ((ptC.X < MainElementArea.Left || MainElementArea.Right < ptC.X) &&
ptC .Y <= MainElementArea .Top + hHead)
{
m_cmnt .SetParentRect (MainElementArea);
}
else
{
InformComment ();
}
… …
Now let us return back to our example Form_ComboBox.cs which has two solitary combo boxes and a set of combo boxes
united into a group of the Group_ArbitraryElements class (figure 22.23). Similar looking group was
demonstrated in one of the examples which used standard ComboBox controls. My main complaint against that variant
was about the inability to see simultaneously all possible variants from all those combo boxes. Standard ComboBox of the
DropDownList style hides its list after selection, so at any moment only one list inside the group can be seen. In the
current example we have ComboBox_DDList objects for which the closing of the list at the moment of selection is not
mandatory; the whole process is controlled by users, but there is also a bit more.
Many possibilities are available through the commands of context menus, so for further discussion it is helpful to see their
views. Four context menus are used in this example. Elements of the same classes inside and outside the group use the
same menus, but some commands for outside elements are disabled.
Menu on combo boxes (figure 22.26) has two submenus. The one with the
long strings (at the figure it is shown next to the menu itself) is accessible only
for combo boxes inside the group; commands of this submenu allow to spread
the view of the pressed element on all the siblings. Another submenu allows to
regulate the resizing of the pressed combo box; this submenu is not accessible Fig.22.27 Menu on comments
when the list of the combo box is hidden.
Menu on comments (figure 22.27) has three commands; the last one is enabled only for elements inside the group.
Inner elements of the group have their menus; there is also a menu on the group itself (figure 22.28); this menu also has two
submenus.
The system of menus and their commands are determined by the ideas of user-driven applications and the purpose of this
particular group. In user-driven applications, each parameter of any element must be under user’s control, so for each
combo box you can change two fonts and regulate the hiding of the list after selection (figure 22.26); for any comment you
can easily change font and color (figure 22.27). There are also commands to spread parameters from some element on its
siblings or to set simultaneously the same parameter for all siblings.
The original version of the group was used to set viewing parameters in a big photo archive program. While demonstrating
the old example with ComboBox controls, I mentioned the inability to show all the possibilities simultaneously. There is
no such problem in
the current example
because the hiding
of the list after
selection can be
switched OFF;
when you use any
combo box as a
sample for others,
this feature is also Fig.22.29 Horizontal default view for the same group
copied to the
siblings.
One more interesting feature of the group is the existence of two different default views. Figure 22.23 shows the vertical
default view of the group. If in such situation the command to show all the lists is used, then the lists overlap. You can
switch to the horizontal default view of the group and then all lists in view will give you the full picture of all possible
combinations of parameters (figure 22.29). While all the lists are shown, you can select the needed set of parameters and
then hide all the lists with a single command of the group menu (figure 22.28). Figures 22.23 and 22.29 show two default
views of the group, but do not forget that this is a Group_ArbitraryElements object with which you can do
whatever you want and change its view in any way you prefer.
World of Movable Objects 869 (978) Chapter 22 Getting rid of controls
Check box
File: Form_CheckBox.cs
Menu position: Graphical objects – Replacement of controls – CheckBox
Preliminary remark. The classical CheckBox control is mostly used in two–state variant (checked – unchecked) but also
has a three–state alternative. Though it is not a problem to design a three–state graphical replica, the currently demonstrated
CheckBox_GR class has only two–state version.
public class CheckBox_GR : GraphicalObject
{
Form m_form;
TabPage m_tabpage = null;
Mover m_supervisor;
RectangleF rcCheck, rcText;
float m_space;
Side sideText;
SideAlignment alignParts;
string m_text;
Font m_font;
Color m_color;
bool bChecked = true;
bool bEnabled = true;
bool bFixMouseOnUnmovable = true;
static int nSquare = 18;
Form_CheckBoxParams_Demo formSetParams;
Each check box consists of two absolutely different parts: a small square and text. Classical CheckBox control allows to
change the relative position of these parts, but I have never seen any other variants except the text to the right of the square.
The CheckBox_GR class allows 12 variants of relative position; this and several other parameters are regulated through
the tuning form.
One important remark about the meaning of the
sideText field. The square is small and the text
is either bigger or significantly bigger; for example,
look at the check box in the bottom left corner of
figure 22.30. Regardless of this discrepancy in
sizes, the sideText field defines the side of the
square at which the text is positioned. This is done
because, regardless of the sizes, the square is the
anchor part of the check box. For the most often
used case of the text positioned to the right of the
square, sideText = Side.E. This is also the
default value for the sideText field when the
appropriate parameter is skipped in the
CheckBox_GR constructor. Fig.22.30 Check box consists of a small square and text. The
Check box consists of two obvious parts and it looks CheckBox_GR class allows 12 different variants of
like it would be enough to have two nodes in its their relative positioning.
cover, but there are three. For better view of an
object, there is a small space between two parts and I don’t like the idea of catching another object through this gap, so the
gap is covered by an additional polygonal node. Depending on the relative sizes of a square (this size is fixed) and text (this
one depends on the used font), the extra node is either a rectangle or a trapezium). Calculation of the corner points
(PointF [] pts) for this node is simple but too lengthy to be shown here.
public override void DefineCover ()
{
PointF [] pts = new PointF [4];
switch (sideText)
{
… …
}
World of Movable Objects 870 (978) Chapter 22 Getting rid of controls
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, rcCheck, Cursors .Hand),
new CoverNode (1, rcText),
new CoverNode (2, pts) };
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviourCursor (Behaviour .Transparent, Cursors .Hand);
}
}
Putting or erasing check mark can be done by clicking at any point of the check box, so, for the purpose of changing the
check status, both parts – square and text – work identically, but there is a difference in moving. When an object is pressed
by the mouse, the CheckBox_GR.Press() method is called.
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)
{
int iNode = mover .CaughtNode;
… …
else if (grobj is CheckBox_GR)
{
(grobj as CheckBox_GR) .Press (e .Location, iNode);
}
… …
For the CheckBox_GR class, the needed initial action is the same regardless of the pressed point, so the second parameter
– the node number – is not needed. Because similar method for any other class needs such parameter, I decided to add it
here.
public void Press (Point ptMouse, int iNode)
{
ptPressed = ptMouse;
}
For movable check box, reactions on pressing inside the square and outside can be different. If mouse is pressed inside the
square, then any attempt to move the cursor is blocked and the cursor is kept at the same point until the mouse release. For
points outside square, there is no cursor fixing because this is the way to move check box around the screen.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (Movable)
{
if (iNode == 0)
{
AdjustCursorPosition (ptPressed);
}
else
{
Move (dx, dy);
bRet = true;
}
}
… …
World of Movable Objects 871 (978) Chapter 22 Getting rid of controls
The reaction on the mouse press is determined at the moment of mouse release and depends on the distance between the
points of mouse press and release. If the distance is negligible (fDist <= 3), then it is considered as the change of the
check status which is done by the CheckBox_GR.Checked property. This is the only possible reaction on a click
inside square as the cursor can be released only at the same point.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
if (grobj is CheckBox_GR && fDist <= 3)
{
CheckBox_GR checkbox = grobj as CheckBox_GR;
if (checkbox .Enabled)
{
checkbox .Checked = !checkbox .Checked;
if (checkbox .ID == checkRectangle .ID)
{
UpdateRect ();
}
… …
RenewMover ();
}
… …
The described reaction is correct for pressing the movable check box; this was purposely underlined on the previous page.
Before looking at the case of pressing an unmovable check box, let us ask more general question. What do you expect as a
reaction on pressing an unmovable element?
Movable / unmovable elements do not differ visually, so the current movability status of any element can be revealed only
as a reaction on mouse press-and-move attempt. Movable element simply moves with the mouse; for unmovable element,
there can be two possible reactions.
• Cursor can be fixed at the same point and no attempts will allow to move it anywhere until the mouse release.
• Cursor can be freely moved around without any element movement.
I prefer the first variant, so in all previously discussed “controls” the idea of mouse fixing on an unmovable element is
implemented. However, the best way to form your personal opinion is to compare both variants. To make the comparison
easier, in the CheckBox_GR class both reactions are possible. To implement both variants, the CheckBox_GR class gets
one more field (bFixMouseOnUnmovable) and the FixMouseOnUnmovable property to deal with this field. Menu
on the group gets a command for synchronous change of this field for all check boxes inside the group.
Thus, for unmovable check box, there can be two different reactions; the choice is regulated by menu command. The
difference in reaction is determined by several lines in the MoveNode() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (Movable)
{
… …
}
else
{
World of Movable Objects 872 (978) Chapter 22 Getting rid of controls
if (bFixMouseOnUnmovable)
{
AdjustCursorPosition (ptPressed);
}
}
}
return (bRet);
}
Tuning of the CheckBox_GR objects is organized via the
special form Form_Tuning_CheckBox.cs (figure 22.31).
The design of this form is based on the classes which were
discussed earlier: Info_Resizable, Button_Drawing,
and ComboBox_DDList. Familiar objects are used to
change the font and color of the text part and to set the space
between square and text.
The only unfamiliar object in this tuning form is a rectangle
(with the word Text inside) and a track around. There are 12
specially marked positions on the track – three along each
side of rectangle. There is also a small square on the track
and this square can be moved along the track. The whole
Fig.22.31 Tuning form for CheckBox_GR objects
construction is an object of the SideAndAlignment class.
public class SideAndAlignment : GraphicalObject
{
Form m_form;
Mover m_supervisor;
RectangleF rcFrame;
PointF ptMark;
PointF [] pts = new PointF [12];
List<Way_Segment> segments = new List<Way_Segment> ();
MarkOnTrack mark;
int iPosition;
An element on the track belongs to the MarkOnTrack class. Such object can
appear as a square (figure 22.31) or as a circle (figure 22.32). The small mark can
be moved only along the track and can be released at any moment, but at the
moment of release its position is adjusted and the mark is moved to the nearest
allowed position. The mark position at any of the predetermined places can be
described by the side of rectangle around which it is moved and the alignment with
rectangle. Yet, there are no fields of the Side and SideAlignment type in the Fig.22.32 Numbering of positions
SideAndAlignment class. Each position has its number (figure 22.32).
To set the conformity between the position number and the pair Side – SideAlignment, we need to know the values
of these enumerations.
public enum Side { W, N, E, S } ;
public enum SideAlignment { Left, Top = 0, Center, Right, Bottom = 2 } ;
If the position number (iPosition) is known, then
side = (Side) (iPosition / 3)
alignment = (SideAlignment) (iPosition % 3)
Opposite equation is also simple
iPosition = side * 3 + alignment
Two things are important in using SideAndAlignment object for setting relative position of the square and text in the
check box.
• In the SideAndAlignment object, a small mark is moved around much bigger rectangle because such
movement is better from the point of using the screen space. Thus, in the SideAndAlignment object, the big
World of Movable Objects 873 (978) Chapter 22 Getting rid of controls
rectangle is an anchor element, while position of the small mark can be changed. For CheckBox_GR objects, the
small square is used as an anchor element and the text is placed at one or another side of this square. In the
Form_Tuning_CheckBox.cs (figure 22.31), the square is moved around and released at the needed relative
position. The selected relative position is transformed to the check box under tuning, but there the small square is
not moved anywhere and keeps its place while the text is repositioned.
• Because of this difference in anchor elements for SideAndAlignment and CheckBox_GR classes, the side
value from the SideAndAlignment class has to be changed to the opposite one and only then used in the
CheckBox_GR class. This opposite value is provided by the Auxi_Common.OppositeSide() method.
World of Movable Objects 874 (978) Chapter 22 Getting rid of controls
Radio button
File: Form_RadioBtnsGroup_Light.cs
Menu position: Graphical objects – Replacement of controls – Radio button (light variant)
Three types of buttons are widely used in interface design: push buttons, check boxes, and radio buttons. We already have
the graphical analogues for the first two; now it is the time for radio buttons. One thing makes them different not only from
other buttons but even from all other controls: radio buttons are never used individually but only in groups. There is
absolutely no sense in individual radio button because its only purpose is to organize a single selection among N
possibilities, so several RadioButton controls are usually united into a group either by a GroupBox or by Panel.
Graphical analog of radio button is also used only in a group, but certainly no controls are used for this purpose. Before
going into the details of graphical radio buttons, let us talk a bit about some features of the RadioButton controls and the
groups of such buttons.
• RadioButton object consists of a small circle and associated text. It is possible to organize different types of
their relative positions, but I had never seen anything else except both of them placed on the same horizontal line
with the text to the right of the circle. I assume that positioning of the circle to the right of the text would be more
appropriate for countries with the rule of writing from right to left but I never worked with such applications.
• RadioButton objects are used only in groups. Inside the group, there can be an arbitrary positioning of those
buttons, but the standard practice is to position their circles strictly on a single vertical line. I have seen a group in
which radio buttons were placed in several columns, but it was a very rare deviation. Figure 22.1 demonstrates
two identical groups with the circles of radio buttons positioned along the inclined line.
• RadioButton objects are usually
positioned at the equal distances from each
other. At the same figure 22.1, you can see
one more group of radio buttons with the
uneven distances between neighbours. Those
radio buttons are associated with other
elements of the form and such positioning of
buttons makes this association more obvious.
There is nothing wrong in different distances
between buttons but it is an extremely rare
case of design.
Let us start our work on the graphical radio buttons
with some kind of light version which provides
exactly the type of buttons that we usually see in the
programs. The light version of radio button – the
RadioButton_Light class – means that circle and
text are positioned on the same horizontal line with
the text to the right of the circle. Such radio buttons
can be organized into a group of the
RadioBtnsGroup_Light class. In such group, Fig.22.33 Groups of the RadioBtnsGroup_Light class
the circles of all radio buttons are positioned along the can be used with or without title.
same vertical line and these buttons can be moved
only up or down.
public class RadioButton_Light : GraphicalObject
{
Form m_form;
Mover m_supervisor;
PointF ptCenter;
string m_text;
Font m_font;
Color m_color;
bool bSwitched;
bool bEnabled = true;
static float rBig = 8;
RectangleF rcText;
World of Movable Objects 875 (978) Chapter 22 Getting rid of controls
Cover of the RadioButton_Light object consists of three nodes. First node covers the circle; second node covers the
text. For better view, there is a small gap between circle and text; this gap is equal to the half of the circle radius; the gap is
covered by polygonal node with four corners.
public override void DefineCover ()
{
PointF [] pts = new PointF [] {
new PointF (ptCenter .X, ptCenter .Y - rBig),
new PointF (rcText .Left,
Math .Max (ptCenter .Y - rBig, rcText .Top)),
new PointF (rcText .Left,
Math .Min (ptCenter .Y + rBig, rcText .Bottom)),
new PointF (ptCenter .X, ptCenter .Y + rBig) };
CoverNode [] nodes = new CoverNode [] { new CoverNode (0, ptCenter, rBig),
new CoverNode (1, rcText, Cursors .SizeNS),
new CoverNode (2, pts, Cursors .SizeNS) };
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviour (NodeShape .Polygon, Behaviour .Transparent);
}
}
Button selection can be done by clicking at any point; the last two nodes can be also used to move the button up or down.
Regardless of the point at which the button is pressed, this point is remembered. Nothing else is done at the moment of the
mouse press so the second parameter of the RadioButton_Light.Press() method is needless; it is included only
in order to have the same standard set of parameters as used in the Press() methods of other classes.
public void Press (Point ptMouse, int iNode)
{
ptPressed = ptMouse;
}
Radio button is not moved by its circle, so when the circle is pressed, then no movement is allowed and the cursor is kept at
the same point until the mouse release. When a movable button is pressed anywhere outside the circle, then the cursor is
allowed to move only vertically and throughout such movement the button moves with the cursor. When an unmovable
button is pressed outside circle, then the cursor is kept at the same point until the mouse release and the button does not
move anywhere. Keeping the cursor at the point or on the line is done by the AdjustCursorPosition() method.
public override bool MoveNode (int iNode, int dx, int dy, Point ptMouse,
MouseButtons catcher)
{
bool bRet = false;
if (catcher == MouseButtons .Left)
{
if (iNode == 0)
{
AdjustCursorPosition (ptPressed);
}
else
{
if (Movable)
{
Move (0, dy);
bRet = true;
AdjustCursorPosition (new Point (ptPressed .X, ptMouse .Y));
}
else
{
AdjustCursorPosition (ptPressed);
}
}
World of Movable Objects 876 (978) Chapter 22 Getting rid of controls
}
return (bRet);
}
Radio buttons are used only inside groups. The Form_RadioBtnsGroup_Light.cs (figure 22.33) demonstrates three
groups of the RadioBtnsGroup_Light class. The design ideas of new group are the same as were used in the
ElasticGroup and the Group_ArbitraryElements classes.
• There is a set of inner elements; in the new case all elements belong to the RadioButton_Light class.
• Inner elements are movable. It is possible to declare those elements unmovable in order to prevent accidental
movement, but this must be only user’s decision. In general those elements can be easily moved up or down
without any restrictions, so user is free to change the distances between elements, to position them in visually
obvious subgroups, and to change their order.
• The group area can be marked by the frame, or by different background color, or by both. Group area is
automatically adjusted to any change of inner elements.
• Group can be used with or without a title. The group width is determined by the bigger of two elements: the title,
if it exists, and the longest button. If the group width allows, the title can be moved left and right; the adhered
mouse technique is used for such movement. User can regulate the title movability.
public class RadioBtnsGroup_Light : GraphicalObject
{
Form m_form;
Mover m_supervisor;
RectangleF rcFrame, rcBigNode, rcTitle;
List<RadioButton_Light> m_elements = new List<RadioButton_Light> ();
string m_title;
bool bTitleMovable = true;
Font fntTitle;
Color clrTitle;
float m_titlesidespace;
float fSpaceAbove;
float [] m_spacesToFrame = new float [4]; // Left, Top, Right, Bottom
Pen penFrame;
SolidBrush brushBack;
Design of the RadioBtnsGroup_Light class is fairly simple. Group cover consists of two nodes. The first one covers
the title rectangular area (rcTitle); the second one is also a rectangle (rcBigNode) which is several pixels bigger than
the calculated frame. For a group without title, the first node is squeezed to zero.
public override void DefineCover ()
{
rcBigNode = RectangleF .FromLTRB (rcFrame .Left - 3, rcFrame .Top - 3,
rcFrame .Right + 3, rcFrame .Bottom + 3);
CoverNode [] nodes = new CoverNode [2];
if (TitleExist)
{
nodes [0] = new CoverNode (0, rcTitle);
if (bTitleMovable)
{
nodes [0] .SetBehaviourCursor (Behaviour.Moveable, Cursors.SizeWE);
}
}
else
{
nodes [0] = new CoverNode (0, rcFrame .Location, 0f);
}
nodes [1] = new CoverNode (1, rcBigNode); //, Cursors .Hand);
cover = new Cover (nodes);
if (TransparentForMover)
{
World of Movable Objects 877 (978) Chapter 22 Getting rid of controls
cover .SetBehaviour (Behaviour .Transparent);
}
}
The RadioBtnsGroup_Light class is similar in view and behaviour to the ElasticGroup and the
Group_ArbitraryElements classes which are thoroughly discussed in the main book, but there is one feature in
which the new group differs from these predecessors. The difference is in the used cursor shape.
Older groups include elements which can be moved around in any direction; usually the possibility of such movement is
signaled by the mouse cursor in the shape of a cross (Cursors.SizeAll). In order to distinguish the places of element
movement and group movement, the big node in the older groups uses different cursor shape (Cursors.Hand). Inner
elements of the RadioBtnsGroup_Light class can be moved only up or down; this movement is signaled by the
special cursor (Cursors.SizeNS), so the group node is free to use the default cursor shape (Cursors.SizeAll). If
you want the cursor shape for the RadioBtnsGroup_Light class to be consistent with the older groups, then the cursor
shape Cursors.Hand can be specified as a parameter for the big node (just now it is commented in the code).
The visibility parameters of groups and their buttons must be under users’ control. For the light version of radio buttons, the
light version of tuning is used, so some secondary parameters in this example are not tunable. All the tuning in the
Form_RadioBtnsGroup_Light.cs is organized via the menu commands (figures 22.34). To change parameters of
particular button or to spread its parameters on all the siblings, you call menu on the button (figure 22.34a). To change
group parameters or to set the same parameters for all buttons of the group, you call menu at any point of the group not
blocked by its buttons (figure 22.34b).
*
Just now I need to write about the difference in view of the groups belonging to several classes, but I think that in the
nearest future I’ll change their design so that they all will look alike. The Group_ArbitraryElements class has the
same view as the Group_RadioButtons class. In the nearest future the ArbitraryGroup objects will be
substituted by the Group_ArbitraryElements objects (I am in the middle of this process) and after it the
ElasticGroup class will get the same design.
World of Movable Objects 880 (978) Chapter 22 Getting rid of controls
additional changes, the cursor would have the same default shape and would not give a tip about the possible movement.
To distinguish visually the possibility of moving group or inner element, cursor for one of them must be changed. I tried
both variants and decided to keep standard cursor (Cursors.SizeAll) for the group itself while the cursor for buttons is
changed to Cursors.Hand.
Visual parameters of buttons and groups can be changed
through the commands of context menus. Buttons have
only three changeable parameters – font and color for the
text and relative position of circle and text. First two
commands of menu on radio buttons (figure 22.36) call
standard dialogues to change font and color. The third
command calls a tiny dialogue Form_CirclePosition.cs
with a single SideAndAlignment object inside; this is
the way to set relative positions of the circle and its text.
Submenu also includes commands to spread the parameters
from the pressed radio button on all its siblings. Fig.22.36 Menu on buttons
Fig.22.37 Several views of the same group are organized by changing parameters of radio buttons, by changing the
relative position of circle and text in some of them, and by changing the order of buttons.
The smallest group in the Form_RadioButtonsGroup.cs (figure 22.35) contains the buttons with the names of the week
days. For a lot of readers (I think, for majority) the shown order of days looks strange, but… Depending on traditions,
there are countries in which the first week day is either Sunday or Monday; there are (or at least there were not long ago)
some countries in which the week starts on Saturday, so the upper line in the list of days might be different. Suppose that
you want to distinguish visually (with font and color) the working days and the week-end. In many countries there are five
working days while in others people work six days. These are all general cases but there can be also personal requirements.
You might work on a special schedule with only two, three, or four working days; you may have classes in the morning on
some days and in the evening on others; for all such cases you can organize a very special view of this group. Figure 22.37
shows several variants, but there can be many others. All changes are done in seconds and at any moment whenever it is
needed. In all four shown variants the buttons are positioned in one column, but this is my fault and only because I have
minimized my efforts in preparation of this figure. An
arbitrary positioning of buttons inside the group is
allowed. Even if you decide to place Friday between
Tuesday and Wednesday, it is not prohibited.
Group parameters are regulated in two different ways.
Nearly all visibility parameters can be changed in a
special tuning form while movability and some other
general things are regulated via the commands of
context menu. Figure 22.38 shows this menu for the
group with the names of days and in the case when
this group is switched into light version. For other
groups the switch into light version is blocked, so for
them not all commands of this menu are available.
Fig.22.38 Menu on groups
World of Movable Objects 881 (978) Chapter 22 Getting rid of controls
Fig.22.40a Background group with the Fig.22.40b Background group in case Fig.22.40c Similar group from the
enabled selection of color when the back color is Form_DesignOfTuningForms.cs.
and transparency. declared the same as in
the form.
Figure 22.40c shows similar group from the old example Form_DesignOfTuningForms.cs; this example can be found in
the main book. For color transparency setting, that old example uses a Trackbar element. It is a rectangular object
resizable by the left and right sides. This complex object has subordinates of two types: there are comments of the
CommentToRect class and there is a tiny pentagonal slider which is an element of the TrackbarSlider class.
Identical track bar from figure 22.40a belongs to the Trackbar_GR class. It
is also a rectangle resizable by the left and right sides; it has identical comments
of the CommentToRect class, but it has no slider of another class because
here the slider is part of the main element. Figure 22.41 shows the places and
numbers of nodes in the Trackbar_GR cover. Nodes in this cover always
have the same position and size. Only the behaviour of the node over the slider Fig.22.41 Nodes in the cover of the
depends on whether an object is enabled or disabled. Trackbar_GR object.
public override void DefineCover ()
{
CoverNode [] nodes = new CoverNode [] {
new CoverNode (0, ptsSlider, Cursors.SizeWE),
World of Movable Objects 882 (978) Chapter 22 Getting rid of controls
new CoverNode (1, rcArea .Location,
new PointF (rcArea .Left, rcArea .Bottom),
Cursors.SizeWE),
new CoverNode (2, new PointF (rcArea .Right, rcArea .Top),
new PointF (rcArea .Right, rcArea .Bottom),
Cursors.SizeWE),
new CoverNode (3, rcArea, Cursors .SizeAll)};
if (!bEnabled)
{
nodes [0] .Behaviour = Behaviour .Frozen;
}
cover = new Cover (nodes);
if (TransparentForMover)
{
cover .SetBehaviourCursor (Behaviour .Transparent, Cursors .Hand);
}
}
Regardless of whether the track bar is enabled or disabled, it can be still moved around the screen and resized, so the view
and the size of the Background group can be changed at any moment. The disability of the track bar means only the
blocking of the transparency change; for this purpose the freezing of the node on slider (node 0) works perfectly.
There is one more feature which could be easily applied to the older variants but which is demonstrated only with the new
track bar. With older tuning forms, the transparency of the group under tuning was changed only at the moment of slider
release, so it was impossible to estimate the result until the moment of final coefficient selection. The
Form_Tuning_RadioGroup.cs changes the background color of the group under tuning in parallel with the slider
movement.
private void OnMouseMove (object sender, MouseEventArgs e)
{
if (mover .Move (e .Location))
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
if (grobj is Trackbar_GR && iNode == 0)
{
Color clrNew = Auxi_Colours .TransparentColor (
groupRadios .BackColor, trackbar .Coefficient);
groupRadios .BackColor = clrNew;
if (Changed != null) Changed (this, new EventArgs ());
}
… …
Group Frame. This group (figure 22.42a) also
includes one familiar looking element but designed
in a new way. For better comparison, there are
also older versions of style selection
(figure 22.42b) from the
Form_DesignOfTuningForms.cs.
The appearance and the view of the frame line are
based on two decisions.
• First you decide either to show the line at Fig.22.42a Group Frame includes Fig.22.42b Older versions of
all or not. style selection style selection
• If you decide to show the frame, then you
can select the line style and width. (In the older groups, the width was always one pixel, but there are some
variants in the new one.)
There are five standard line styles to select from and it is possible to unite the area of both decisions inside one screen
element by adding an additional empty strip to those five; this is shown with lower example at figure 22.42b. It is a good
enough design solution, but I think that an additional check box with direct command to show the line or not is more
obvious and thus better for users, so such variant is used in the new tuning form. When there is no mark in the check box,
World of Movable Objects 883 (978) Chapter 22 Getting rid of controls
then the frame is not shown and selection of its color, style, and width has no sense, so in this case three objects of the
Frame group become disabled. No selection is shown in disabled elements of the LineStyleSelector and
ComboBox_DDList classes. In a standard way, the Enabled property of both classes is used inside the OnMouseUp()
method of the form to decide about the reaction on the click.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is LineStyleSelector && selectorPressed.Enabled
&& fDist <= 3)
{
… …
}
else if (grobj is ComboBox_DDList)
{
… …
else if (iNode == 3 && comboPressed .Enabled && fDist <= 3)
… …
Group Title. This group includes only two buttons;
figure 22.43a shows the group view when the currently
tuned group has title. In case of tuning a group without
title, both buttons are disabled. In the older groups,
there was a changeable space between title and frame
lines; now there is a small unchangeable space.
Figure 22.43b demonstrates the closest variant for the Fig.22.43a Tuning Fig.22.43b Tuning of the title for some
same tuning which is used in the of the title ElasticGroup object
Form_ElasticGroupParams.cs; this tuning form for the ElasticGroup objects is discussed in the main book.
sizes, so the distance to the opposite side is constantly checked. For the Group_RadioButtons class, the size of the
frame does not matter at all, but at each particular moment there is a fixed area of inner elements and each frame line has
limits on minimum and maximum distance from this area.
Group Buttons. This group (figure 22.45) is used for setting identical parameters for all
radio buttons of the group. Font and color for the texts can be changed in any situation
while the change of circle position is allowed only for normal version. For light version
of the tuned group, the SideAndAlignment object is disabled.
I want to remind about one problem which is similar to one mentioned throughout the
discussion of the check boxes. Each radio button consists of two parts and the Fig.3.42 Changing the view of
SideAndAlignment object is used to change their relative position. In the tuning all buttons in the group
form, it is easier to organize the change by moving a small circle around much bigger
rectangle. In the radio button itself, the circle is an anchor part, so the circle does not move but the text is placed according
to selected relative position.
All groups from the Form_Tuning_RadioGroup.cs (figure 22.39) belong to the Group_ArbitraryElements class.
This class has its own tuning form which was
used not once in the previous examples and
certainly can be used here. Instead, I decided to
use a simplified version of tuning through the
commands of two context menus. One menu can
be called inside any group; this menu has a single
command to reinstall the default view of the
pressed group (figure 22.46a). Another menu
can be called outside all the groups at any empty
place of the form. Commands of this menu Fig.22.46a Menu inside groups Fig.22.46b Menu outside groups
(figure 22.46b) provide the identical change of parameters for all the groups of the tuning form.
World of Movable Objects 885 (978) Chapter22 Getting rid of controls
is impossible, so whenever a group is organized, it has to be of the Group_ArbitraryElements class. But because
this class was designed as a copy of the ElasticGroup but for arbitrary elements, then there is no difference at all in
using one or another class. At first I thought about some improvements in the newest Calculator but later decided not to
change anything at all in order to have a pure example of transformation from controls to their graphical analogues.
In the older version, each button was associated with a couple of events. In the new version, everything is organized in a
standard way for graphical objects, so the whole work is based on the mouse events. As nearly all elements belong to the
Button_Text class, then the code of the OnMouseDown() and OnMouseUp() methods is nearly identical to the code
from the special example Form_Button.cs which was discussed 35 pages back. Dealing with new “controls” in Demo
examples and real applications is the same.
private void OnMouseDown (object sender, MouseEventArgs e)
{
ptMouse_Down = e .Location;
if (mover .Catch (e .Location, e .Button))
{
GraphicalObject grobj = mover .CaughtSource;
int iNode = mover .CaughtNode;
if (e .Button == MouseButtons .Left)
{
if (grobj is Info_Resizable)
{
info .Press (e .Location, iNode);
}
else if (grobj is Label_GR)
{
labelExpression .StartResizing (e .Location, iNode);
}
else if (grobj is Button_Text)
{
Button_Text btn = grobj as Button_Text;
btn .Press (e .Location, iNode);
if (iNode == 8 && btn .Enabled)
{
btn .Pressed = true;
Invalidate ();
}
}
… …
For the easiness of understanding and because of the big number of buttons, the reaction on their clicks is not included
directly into the OnMouseUp() method but can be found in the ButtonClick() method.
private void OnMouseUp (object sender, MouseEventArgs e)
{
ptMouse_Up = e .Location;
double fDist = Auxi_Geometry .Distance (ptMouse_Down, ptMouse_Up);
int iObj, iNode;
if (mover .Release (out iObj, out iNode))
{
GraphicalObject grobj = mover .ReleasedSource;
if (e .Button == MouseButtons .Left)
{
… …
else if (grobj is Button_Text)
{
btnPressed = grobj as Button_Text;
if (fDist <= 3 && iNode == 8 && btnPressed .Enabled)
{
ButtonClick ();
}
if (btnPressed .Pressed)
World of Movable Objects 887 (978) Chapter22 Getting rid of controls
{
btnPressed .Pressed = false;
Invalidate ();
}
}
… …
In the same way as it was done in the previous version, the majority of buttons is distributed between three groups:
numbers, operations, and functions. Each group has a special form for standard view selection. Figures 22.48 show these
auxiliary forms; compare them with the view of the corresponding forms from the older version (figures 21.8). Only the
sliders in the familiar looking elements inside groups signal that these are not standard NumericUpDown controls but their
graphical analogues NumericUpDown_GR. When the sliders are hidden (figure 22.48b), it is impossible to name the
class of elements by the view. In such case only the movement of elements by any inner point and the use of adhered
mouse when you press the buttons of these “controls” inform that these are not standard controls.
Fig.22.48b Form_BtnPlaces_Operations.cs
Summary
This summary consists of three parts: rules of cover design, rules to organize moving and resizing of objects,
and the rules of user driven applications. All rules work under the main idea that any type of moving and
resizing is done only by a mouse without any involvement of the keyboard.
Covers
• A cover consists of an arbitrary number of nodes. The minimum number of nodes is one, so a cover may consist of a
single node. The number of nodes is unlimited. Covers of a special type may include hundreds of identical small
nodes; such covers are called the N-node covers.
• Usually each node is used either for moving or resizing of an object; it is possible to use the same node for both
operations, but then they are started by different buttons. If an object must be involved in moving and resizing and both
of them are started by the same button, then the minimum number of nodes is two.
• Nodes of three different shapes are used: circles, strips with semicircles at the ends, and convex polygons. Circular
node is defined by its central point and radius. Strip node is defined by two middle points at the ends of a strip and
radius of semicircles; the width of a strip is equal to the diameter of semicircles. Convex polygon is defined by
vertices.
• Nodes of a cover may overlap, or they can be placed side by side, or stay apart. In some cases the order of nodes in the
cover is absolutely unimportant; in other cases it can be very important as nodes are checked for moving according to
this order. In the areas of overlapping, moving / resizing of an object is determined by the first selected node.
• Nodes do not duplicate the shape of an object and they are not required to be only inside the area of an object. Borders
of objects are usually covered by the nodes in such a way as to make sensitive strip on both sides of the border line;
thus the united area of all nodes is usually slightly bigger than the area of an object.
• Use of transparent nodes can make the design of covers for the nontrivial areas much simpler. With the use of
transparent nodes, the area of cover may noticeably differ from the object area.
• Nodes can be moved individually thus allowing to reconfigure an object.
• Nodes can be enlarged to cover as much of the object area as possible. Such enlarged nodes are often used for moving
the whole object.
• A cover may consist of the nodes which are not moved individually, but an attempt to move such node results in the
moving of the whole object (the MoveNode() method only calls the Move() method). This makes an object
movable but not resizable.
• Each of the nodes has its own parameters. It is easy to allow resizing along one direction but prohibit it along another;
this means organizing a limited reconfiguration.
• One of the node parameters is the shape of a cursor above it; the change of mouse cursor signals about possible actions
with an object underneath.
• It does not matter that some covers are associated with the graphical objects and others with controls or groups of
controls. All covers are treated in the same way thus allowing users to change easily the inner view of any application.
• Covers can be visualized, though the best design makes moving / resizing of objects obvious without such visualization.
Visualization of cover includes possible filling of the node area and drawing of the node perimeter.
• Three levels of moving objects across the borders of a form (panel, etc.) can be organized: moving outside is not
allowed, moving allowed only across the right and lower borders, or moving allowed across all four borders.
• Any graphical object and any control can be turned into movable / resizable, but different procedures are used for
turning into movable the elements of these two types.
• To make any graphical object movable / resizable, its class must be derived from the GraphicalObject class and
three methods must be written for the new class: DefineCover(), Move(), and MoveNode().
• DefineCover() method defines the cover of an object as a set of nodes. Each node has an individual number
which is used for identification. Numbers start from zero and go up with an increment of one. In the cover consisting
of N nodes their identification numbers use all the values from the [0, N-1] range.
• Move() method describes the forward movement of an object as a whole. In reality it means the simple change of
one or several primitives (points, rectangles) on which the drawing of an object is based.
• MoveNode() method describes the individual movement of nodes. If the moving of a node results in the moving of
a whole object, then the Move() method is called from inside the MoveNode() method. Individual node
movement may cause the relocation of some or even all other nodes of the cover; in such cases the DefineCover()
method is often called from inside the MoveNode() method. Covers of a special type (N-node covers) might have
some restrictions on calling the DefineCover() method from inside the MoveNode() method.
• To make any control movable / resizable, it is enough to include it into the mover queue. Three mentioned methods are
not needed for controls.
• To make any control resizable, the appropriate values must be determined in its MinimumSize and
MaximumSize properties.
• Mover has a queue of movable objects and supervises the whole moving / resizing process only for the objects which
are included into this queue. Simple graphical objects and controls are registered in the mover queue by the standard
Mover methods.
mover .Add (…);
mover .Insert (…);
• Any combination of elements can organize a set of synchronously moving objects.
• For the complicated objects consisting of the parts which can be moved both synchronously and independently and for
the objects for which the set of such parts can be changed, it is much better to develop an IntoMover() method
which is used instead of manual registering of all movable parts. Such IntoMover() method guarantees the
correct registering of an object and all its parts regardless of a set of constituents.
• Moving and resizing are done with a mouse; the whole process is organized via three standard mouse events:
MouseDown, MouseUp and MouseMove.
• MouseDown starts moving / resizing of an object by grabbing one of the nodes in its cover. The only mandatory line
of code in the associated method is
mover .Catch (…);
• MouseMove moves the whole object or some part of it. There is one mandatory line of code in this method, but in
order to see the movement, the Paint method must be called.
if (mover .Move (e .Location))
{
Invalidate ();
}
• MouseUp ends moving / resizing by releasing any object that could be involved in the process. The only mandatory
line of code in this method is
mover .Release ();
• Moving / resizing of an object starts when the mouse is pressed and mover catches an object. There can be several
objects at the point of the mouse press; mover decides about an object to catch by checking the objects in its queue, so
the order of objects (their covers) in the mover queue is very important.
World of Movable Objects 891 (978) Summary
• If several objects overlap at the point where the mouse is pressed, any user would expect the upper one to be caught by
mover, so the upper object on a screen must be the first in the mover queue. Controls appear on the screen atop all
graphical objects, so all controls must precede all graphical objects in the mover queue. Controls have to be registered
in the mover queue according to their Z-order; this will guarantee the catching of the upper control in case of their
overlapping. Drawing of graphical objects must be organized in the opposite order to their positions in the mover
queue; this will guarantee that the upper graphical object will be caught in case of their overlapping.
• Mover can provide a lot of information about the object which is currently moved or just released and even about an
object that is simply underneath the mouse cursor. This data can be used to change the order of objects on the screen,
to call the context menus, and so on. Mover can also provide the information about the objects at any point of the form
in which it works.
• If covers must be shown, then one of the available drawing methods can be used, for example,
mover .DrawCovers (grfx);
• If needed, several Mover objects can be used to organize the whole moving / resizing process. Each mover deals only
with the objects from its own queue.
User-driven applications
• All elements are movable.
• All parameters of visibility must be easily controlled by users.
• Users’ commands on moving / resizing of objects or on changing the visibility parameters must be implemented exactly
as they are; no additions or expanded interpretation by developer are allowed.
• All parameters must be saved and restored.
• The above mentioned rules must be implemented at all the levels beginning from the main form and up to the farthest
corners.
World of Movable Objects 892 (978) Conclusion
Conclusion
This book is about the programs of the new type; about the applications that change the whole set of relations along the
chain “developer – applications – users”. User-driven applications are not some kind of improvement of the currently used
programs but are their alternative from the point of users’ control over the applications. Visually the programs are the same
and they continue to fulfil their main purposes. But they are designed according to another philosophy; the step from the
currently used applications to the user-driven applications is bigger (for USERS!) than from DOS to Windows (or similar
systems). The switch to multi-window operating systems which happened 25 years ago greatly increased the flexibility of
our work with computers but did not change the programmer – user relations: users continue to do only whatever was
written for them in the designer’s scenario. The new programs give the main role to users. The idea of user-driven
applications is a new programming paradigm.
It is a standard practice to describe the proposed changes with the examples from different areas. A comparison from
another area is thought out by an author as a good example of similarity, though not all readers may agree with it. In any
way, a comparison is an artificial model of the discussed process but simplified in order to highlight only the most important
features. Here are three comparisons of the transformation from currently used programs to user-driven applications. I
purposely selected for these examples three different areas which are far away from each other.
Area 1
There are autocratic regimes and democracy. The level of freedom in the first case greatly depends on the personality of the
ruler. It can be extremely bad or it can be good enough, so that people would not complaint about it. But in any case, if the
people of such country want some changes, they have to write a petition and ask for the highest authorization of their
request. If the ruler agrees with the proposal, he makes the needed adjustments. Citizens have to accept whatever is
imposed.
The currently used programs are designed by somebody and are ruled by developers. If users want some changes,
they can send the proposals to the authors or speak at the users’ conference. The author of the program can agree to
make some changes, but these changes may differ from what was asked. In any way, users get the new version with
the changes made by the author and have to accept it.
Under the democracy the changes come via the voting process and election of those who put into laws the expectations of
the majority. The process can be not straightforward, but the general movement is in the direction of citizens’
requirements. Citizens may not be the best specialists on one or another process, but usually they get whatever they want at
the moment. If later they change their mind, there is another election to occur earlier than the next millennium will come.
User-driven applications are ruled by users. Changes are made according to the user’s wish. It is possible that
some changes are not the best, but then another change can be made at any moment. At different moments user may
wish different things; he has a chance to make different changes in the program.
Area 2
The bus has a predetermined route and the fixed number of stops along its route. You can widen the picture to the whole
public transportation system which is a collection of those predetermined routes. With a good public transportation system,
you have a variety of choices but only from the list that was thought out and organized by some department.
You can drive your car to any place; the route and the places to stop are decided by you. You are the driver, all
decisions are yours.
Area 3
For many years (decades, centuries...) and in many countries around the world the stores worked only during the fixed hours
and not all days a week. There were variations from place to place and throughout the duration of time, but the schedule
was fixed. Often the schedule was fixed not for a short period of time but for decades along the time scale in both
directions. There were some inconveniences but they never raised a question of the fairness of the whole process. It was
organized in the same way throughout the parent’s life and the grandparent’s life; there were no different examples, so there
was nothing to compare with to raise any questions.
Now there are stores which work 24 hours a day and 7 days a week; people do not complaint about such schedule.
It is the best for everyone, because each individual can decide about the best procedure personally for him.
Personal circumstances can change; an individual adjusts his schedule according to them.
People got so used to this freedom of personal decisions that even a short return to the old style causes the huge
inconvenience and immediately raises the questions of its foolishness.
World of Movable Objects 893 (978) Conclusion
You may agree or disagree with these examples from different areas. You can continue to insist that there is no sense in
switching from the applications ruled by developers to the user-driven applications. From my point of view, it would be
difficult to insist that there is no sense in user-driven applications if you have tried any of them. You have the right to have
your own opinion and I would like people to form there own opinion by trying the user-driven applications.
Programming is one of many forms of peoples’ activity. As in any other area, there are those who produce something
(programmers) and there are those who use the results (users). Programming is one of those areas in which the same person
plays both roles. Programmer is always a user! I wrote this book for programmers to show the way of developing the new
products – user-driven applications. While reading this book, you were estimating the usefulness of those ideas as a
PROGRAMMER. But you have also to look at the results of implementation of those ideas as a USER. Would you like to
see those rules implemented in the programs that you use every day from morning till night? I want to remind those basic
rules.
Rule 1. All elements are movable.
Rule 2. All parameters of visibility must be easily controlled by users.
Rule 3. Users’ commands on moving / resizing of objects or on changing the parameters of visibility must be
implemented exactly as they are; no additions or expanded interpretation by developer are allowed.
Rule 4. All parameters must be saved and restored.
These rules have to be used at all levels of an application from the main form to the smallest element in the rarely used part.
User-driven applications differ from the currently used programs not by some tiny or unimportant details but by the main
ideas:
• Designer has to provide an instrument to solve users’ problems. This instrument consists of a correctly working
engine (calculation part) and a set of tools to put data inside, to get the results, and to visualize them.
• Designer has to provide a very flexible system of visualization.
• Designer has no right to decide instead of a user, how this instrument has to be used.
• Users get an instrument for solving their tasks and all the possibilities of rearranging the view of this instrument in
any way they want.
Both sides – designers and users – can be involved in transformation from the designer-driven to user-driven applications
and both sides can (and will) react to such transformation.
I think that rule 3 will be the most controversial one and will be the most criticized by programmers. This rule may cause
an outcry because the opposite thing was hammered into the programmers’ heads for years by the use of dynamic layout
which makes a mess of the fool-proof programming and the design of interfaces. The fool-proof programming is one of the
axioms of our work; it is really hard to doubt one of the axioms. In reality, I do not argue with this axiom, I only want it to
be used exactly in the range for which it must be an axiom. Originally it was an axiom for behaviour of applications which
meant only calculations; for this area it is absolutely correct. The spreading of the same axiom on the interface (that is one
of the origins of dynamic layout) was definitely a mistake. So, continue to develop the fool-proof programs from the
purpose of any of them, but do not implement fool-proof ideas, as YOU understand them, into the interface design. Let the
programs work absolutely correctly but be user-driven. An attempt to design user-driven applications with some kind of
developers’ control over the users’ changes in interface will be a mistake. (Some out-of-date doctors continue to insist that
it is impossible to be pregnant partly.)
For users the transformation from the old style programs to the new goes easily and quickly. Users do not need the long
instructions of how to deal with user-driven applications. Users have to know only one main rule: EVERYTHING is
movable and tunable. From there they can work exactly as before but with all the new possibilities. Users accept the new
rules very quickly and none of them ever talk about going back or about an overwhelming burden of adjusting the
applications according to their preferences. Each user selects the level at which he controls an application; he also knows
that he can change the level of this control at any moment according to his own wish.
I am convinced that after the introduction of the user-driven applications to the significant amount of users, there would be
no way back to the fixed applications. It may look a bit strange and unusual at the beginning, but it will become normal
very quickly. Exactly like it happened with the transition from DOS to multi windows systems.
The decision about making a step from the designer-driven to the user-driven applications must be made personally by each
developer. If the majority of programmers decide to develop the user-driven applications, we will find ourselves in another
programming world which gives us – USERS – another level of possibilities.
World of Movable Objects 894 (978) Conclusion
My book is not a detailed atlas of this new world. I am sure that the world of user-driven applications is much wider than
the well known and densely populated area of our day programming. This book is about the path to that new world. Either
to take this path or not is up to you. The way is shown and described in details, so anyone with even an average skill can do
it. You can use the marked path or try your own way (invent your own algorithm); any way will eventually bring you to the
same new world. This new world is full of wonders and unknown things. Explore it and good luck. The way is shown.
May - October 2010
If it is possible to have an addition to conclusion…
What role the computer displays play now in our life and how do we use them? Computers link us with the huge amount of
data and the results of calculations or research work. We type in some input data or request for information and receive
something back. This something can be words, numbers, pictures, and so on; whatever we get, we see it on displays. The
information that we get in response to our requests is out of our control. This is the prerogative of applications to give us
the correct answers on our requests. The correct result of calculations, the requested text, picture, or data file – it is the
programmers’ responsibility to provide us – USERS – with the correct answers on our requests. From this moment we must
have the full control over the received data, but....
• Decades ago the received data could be viewed in one form only; in the form that a programmer considered the
best or adequate.
• The next step of progress increased the flexibility of viewing the results (texts with the changeable font, color, and
size; resizable pictures; changeable format of numbers, and so on), but all these variations are limited by what
programmers continue to consider as “good enough”. Also more flexibility is provided for the easiest cases (texts,
pictures), while the complex results are given out with zero flexibility.
• Extrapolate the previous changes and you will come to the full users’ control over the results; I do not see any
other possible future for the interface. Under the word results I mean everything that is shown on the screen. All
parameters of visibility, places, and sizes for all the screen elements – everything must be controlled by us –
USERS!
The possibility of full users’ control over all the screen elements will noticeably change our relations with computers. This
book is about the way to that next level.
Dr. Sergey Andreyev ( [email protected] )
June 2011
World of Movable Objects 895 (978) Bibliography
Bibliography
1. G.Orwell, Animal farm, 1945.
2. S.Hawking, The universe in a nutshell. Bantam Press, 2001.
3. C.Perrault, Cendrillon, 1697.
4. C.Petzold, Programming Microsoft Windows Forms. Microsoft Press, 2006.
5. S.Andreyev, Fixed Interfaces, Adaptive Interfaces... What is next? Total movability – a new paradigm for the
user interface. Cornell University Library, Computing Research Repository (CoRR), August 2012
6. S.Andreyev, A Door into Another World. Cornell University Library, Computing Research Repository (CoRR),
December 2011
7. S.Andreyev, Rejecting adaptive interface. Cornell University Library, Computing Research Repository (CoRR),
September 2011
8. S.Andreyev, User-driven applications. Cornell University Library, Computing Research Repository (CoRR),
April 2010
9. S.Andreyev, On the theory of moveable objects. Cornell University Library, Computing Research Repository
(CoRR), December 2009
10. S.Andreyev, Personal applications, based on moveable / resizable elements. Cornell University Library,
Computing Research Repository (CoRR), June 2009
11. S.Andreyev, Moveable objects and applications based on them. Cornell University Library, Computing Research
Repository (CoRR), April 2009
12. S.Andreyev, Moving and resizing of the screen objects. Cornell University Library, Computing Research
Repository (CoRR), September 2008
13. S.Andreyev, Design and use of moveable and resizable graphics. Part 1. Component Developer Magazine,
March / April 2008, pp. 58-69.
14. S.Andreyev, Design and use of moveable and resizable graphics. Part 2. Component Developer Magazine,
May / June 2008, pp. 56-68.
15. S.Andreyev, Design of moveable and resizable graphics. Cornell University Library, Computing Research
Repository (CoRR), September 2007
16. S.Andreyev, User-driven applications – new design paradigm. Cornell University Library, Computing Research
Repository (CoRR), June 2007
17. H.Lieberman, F.Paterno, V.Wulf, End-User Development. Springer, 2006
18. S.Andreyev, Design of movable / resizable plots and their use in applications.
http://www.codeproject.com/Articles/672349/Design-of-Movable-Resizable-Plots-and-Their-Use-in,
October 2013
19. S.Andreyev, Family Tree. http://www.codeproject.com/Articles/531953/Family-Tree, January 2013
20. Dijkstra, Edsger W. A Case against the GO TO Statement, published as Go-to statement considered harmful in
Communications of ACM 11, 1968, 3: 147-148
World of Movable Objects 896 (978) Programs and documents
All these books and programs were prepared throughout the last 10 and plus years. Some very important examples were
changed and improved (I hope) throughout this period and I think that the best versions appear in the book “Introduction to
User-driven Applications”. Some versions in the older books were replaced by the new ones; others demonstrate different
versions of the same program.
Occasionally I decide to make some changes at the basic level and then the previous versions of some forms cannot be
restored from Registry. I try my best in checking the new library versions against the older saved in Registry
information, but it might happen that something was missed throughout such checking and then you will need to delete the
old key in the Registry.
--------------------------------------
After long consideration four files were removed from the set just at the last moment, so they can be still mentioned
somewhere in the texts. The latest versions of Calculator, Family Tree, and Function Analyser programs are included into
the “Introduction to User-Driven Applications”, so I decided to delete the same examples as stand alone applications. The
book “Elements of Total Movability” is also deleted because the same topics are covered by the “Introduction to User-
World of Movable Objects 897 (978) Programs and documents
Driven Applications”. Only several examples from the “Elements of Total Movability” are not used anywhere else, so I’ll
have to think about their future.
MovableElements.zip File contains the book “Elements of Total Movability” in DOC and PDF
formats and the program to accompany this book (all codes in C#). It includes
the demonstration and explanation of results in two main areas:
moving/resizing of elements of the most popular shapes and graphical
analogues of the most often used controls. Everything is organized with the
use of adhered mouse technique. After some changes close to 90 percent of the
included examples are used in the book “Introduction to User-Driven
Applications”, but there are still several important examples which are not
used anywhere else.
FamilyTreeStepByStep.zip An application for family tree design is included as one of many examples into
the WorldOfMoveableObjects.exe but is presented here as a stand alone
program. In the same zip file there is also a short description in DOC and PDF
formats (5 pages) and much bigger description which is an excerpt from the
book “World of Movable Objects” (44 pages).
FunctionAnalyser.zip It is a Function Analyser for Y(x) functions and parametric functions
{X(p), Y(p)}. Functions are typed in as standard mathematical expressions
and then shown in the plotting areas. Any set of functions can be shown in the
same area; any number of the plotting areas can be organized; sets of plotting
areas are organized into different views. Everything is controlled by users:
functions, views, visibility parameters. Two files are needed for the work of
this Function Analyser (EXE and DLL). There is also a short file with
description inside this zip file. The same Function Analyser is included into
big Demo application as one of the examples and there is more detailed
explanation of its work in the book, but this explanation is scattered among
several sections of the chapter Applications for science and engineering.
Calculator_GR.zip Calculator which is included as one of the examples into the
WorldOfMoveableObjects.exe is presented here as a separate program. Short
description is included into the same zip file.
World of Movable Objects 898 (978) Appendix A. Examples (forms) used in Demo application
Form_About.cs RigidlyBoundRectangles
The only form in which Text_Horizontal and Text_Horizontal
Text_Rotatable objects are used Text_Rotatable
simultaneously. I do not recommend such thing
because these elements are indistinguishable from
each other and at the same time objects of only one
class can be rotated.
Form_AboutCalculator.cs InfoOnRequest
This form shows information about Calculator. The RigidlyBoundRectangles
main part of information is shown by the
SolitaryControl
Text_Hor_Dominant objects.
Text_Horizontal
Form_AboutCalculator_GR.cs RigidlyBoundRectangles
This form shows information about graphical Text_Horizontal
Calculator. The main part of information is shown
by the Text_Horizontal object.
Form_AboutFormMain.cs InfoOnRequest
The view and design of this form is similar to the SolitaryControl
previous one. Text_Horizontal
SubordinateControl
UnclosableInfo
InfoOnRequest
Plot
PlotAuxi
Scale
SolitaryControl
UnclosableInfo
Form_FuncYx.cs Applications for science 625 18.25 CommentedControl
and engineering COmmentedControlLTP
In this form the text of the new Y(x) function can be ElasticGroup
typed in and checked. The correct function can be Analyser of functions ElasticGroupElement
added to the list of new functions in the Elem
Form_Function.cs. FunctionInterpreter
InfoOnRequest
Plot
PlotAuxi
Scale
SolitaryControl
UnclosableInfo
Form_RegularPolygon_IdenticalHole.cs SolitaryControl
Regular polygons with a hole of the identical shape. Text_Horizontal
Resizing, rotation, and color are regulated via the
context menu.
Form_Tuning_Spaces.cs CommentedControlLTP
An auxiliary form to set some spaces for ElasticGroup
ArbitraryGroup; can be called from inside the SolitaryControl
Form_ArbitraryGroup.cs.
Form_AboutLibrary.cs
Can be called from the tuning forms
provided by MoveGraphLibrary.dll
Form_InfoOnRequestParams.cs
The tuning form for the InfoOnRequest
class.
Button_Text CalcElem.cs
Form_BtnPlaces_Functions.cs 889
Form_BtnPlaces_Numbers.cs 889
Form_BtnPlaces_Operations.cs 889
849 Form_Button.cs 850
Form_Calculator_GR.cs 887
Form_CheckBox.cs 870
Form_ComboBox.cs 862
Form_EnabledMovableTransparent.cs 853
Form_Label.cs 856
Form_NumericUpdown.cs 859
Form_RadioBtnsGroup_Light.cs 875
Form_RadioButtonsGroup.cs 880
Form_Rectangles_ForControls.cs 841
CircleData Circle_EOS.cs
Circle_PartitionsAndComments.cs
Circle_PlusRings.cs
Circle_SimpleCover.cs
Circle_SimpleCoverAdh.cs
Circle_SlidingPartitions.cs
Circle_WithComments.cs
World of Movable Objects 943 (978) Appendix B. Use of classes from MoveGraphLibrary.dll
138 Element_Nnodes.cs
Form_Circle_PlusRings.cs 377
Form_Circles_Multicolored.cs 150
Form_Circles_SimpleCover.cs 357
Form_Circles_SimpleCoverAdh.cs 364
Form_ReferenceBook_Primitives.cs 224
Form_ClippingLevels.cs 268
Form_ClosableInfoParams.cs 82
Form_Colors_Circle.cs 478
Form_Colors_Ring.cs 822
399 - 401 Form_CommentedControlsLTP.cs 400
Form_DataRefinement_Zoom.cs 644
Form_DominComControlLTP.cs 513
Form_ElasticVsDominant.cs 502
Form_FamilyTreeSettings.cs 811
Form_FreeBuses.cs 771
Form_FuncYx.cs 625
Form_GroupOfElementsParams.cs 825
Form_ModifyComment.cs 731
Form_OrdinaryPanels.cs 406
Form_Pie.cs 498
Form_Rectangles_WithCells.cs 352
Form_ReferenceBook_Groups.cs 516
Form_Rings_SimpleCoverAdh.cs 366
Form_SolitaryControls.cs 394
Form_Strips_SimpleCoverAdh.cs 368
Form_Text_Rotatable_Class.cs 83
Form_Texts_MoveAsSample.cs 89
Form_Triangles.cs 111
Form_Tuning_Background.cs 830
Form_Tuning_Bus.cs 777
Form_Tuning_Person.cs 790
Form_Tuning_Spaces.cs
CommentToCircle Circle_WithComments.cs
379 Form_Circle_WithComments.cs 380
Way_ArcCommented.cs
Way_LineCommented.cs
Form_Person.cs 790
Form_PersonalData.cs 430
Form_PlotAnalogue.cs 244
229 Form_Rectangles_WithComments.cs 229
(uses CommentToRect_Demo)
Form_Rectangles_WithComments_Advanced.cs 241
Form_ReferenceBook_Groups.cs 516
Form_SetOfControls.cs 732
Form_SettingsVariants.cs 554
Form_Trackbars.cs 250
HorScaleAnalogue.cs
Person.cs
PlotAnalogue.cs
RectangleCommented.cs
Submenu_BD.cs
VerScaleAnalogue.cs
CommentToRing CommentOnRingRadius.cs
Form_PlotsVariety.cs 688
382 Form_Rings_WithComments.cs 382
Ring_WithComments.cs
FilenameScrolling Form_DataViewBIN.cs
Form_DataViewTXT.cs 659
527 - 529 Form_FilenameViewersPlus.cs 525
Form_OnePlot.cs 663
FilenameViewer FilenameViewer_Demo.cs
521 - 524 Form_FilenameViewersPlus.cs (uses the analogue 525
FilenameViewer_Demo)
Plot AreaOnScreen.cs
Form_ArbitraryGroup.cs 452
Form_DataRefinement.cs 638
Form_DataRefinement_Zoom.cs 644
Form_DataViewBIN.cs
Form_DataViewTXT.cs 659
590 - 619 Form_Functions.cs 594
Form_FuncXrYr.cs 627
Form_FuncYx 625
Form_GraphManualDefinition.cs 669
Form_Main.cs 17
Form_OnePlot.cs 663
Form_PlotPartsMovability.cs 599
Form_Trackbars.cs 250
FunctionDesigned.cs
SpotsOnPlot.cs
RectRange ControlWithComments.cs
Form_BallsInRectangles.cs 288
Form_GroupsWithDynamicLayout.cs 417
41 Form_Rectangles_Standard.cs 41
Form_Rectangles_StandardToDisappear.cs 47
Form_ReferenceBook_Groups.cs 516
Form_ReferenceBook_Primitives.cs 224
Rectangle_Standard.cs
Rectangle_StandardDisappear.cs
RingData Circle_PlusRings.cs
140 Element_Nnodes.cs 137
Form_Circle_PlusRings.cs 377
Form_Rings_Coaxial.cs 372
Ring_EOS.cs
Ring_Multicolor.cs
Ring_PartitionsAndComments.cs
Ring_SimpleCover.cs
Ring_SimpleCoverAdh.cs
Ring_SlidingPartitions.cs
Ring_WithComments.cs
Rings_Coaxial.cs
Form_DataViewBIN.cs
Form_DataViewTXT.cs 659
Form_Functions.cs 594
Form_FuncXrYr.cs 627
Form_FuncYx 625
Form_GraphManualDefinition.cs 669
Form_OnePlot.cs 663
Form_PlotPartsMovability.cs 599
Form_PlotsVariety.cs 688
SetOfCommentedControls 733 - 734 Form_SetOfControls.cs.cs 732
Text_Horizontal ClosableInfo.cs
Form_About.cs
Form_AboutCalculator.cs
Form_AboutCalculator_GR.cs
Form_AboutDataViewerBIN.cs 668
Form_AboutDataViewerTXT.cs 665
Form_AboutFormMain.cs
Form_Circles_Nonresizable.cs 61
Form_ClosableInfo.cs 78
Form_DefineNewBarChart.cs 706
Form_Functions.cs 594
Form_Label.cs 856
Form_LinesSolitary.cs 36
Form_Main.cs 17
Form_Nodes.cs 30
Form_ObjectsOnTabControl.cs 462
Form_Rectangles_AllMovements.cs 69
Form_Rectangles_FixedRatio.cs 56
Form_Rectangles_OneSideTurnOut.cs 52
Form_Rectangles_SingleSideResizing.cs 50
Form_Rectangles_Standard.cs 41
Form_Rectangles_StandardToDisappear.cs 47
World of Movable Objects 951 (978) Appendix B. Use of classes from MoveGraphLibrary.dll
Form_Rectangles_SymmetricalChange.cs 54
Form_RegPoly_RegHole_RotatableBorders.cs 186
Form_RegularPolygon_IdenticalHole.cs
74 - 75 Form_Text_Horizontal_Class.cs 76
(uses Text_Horizontal_Demo class)
Text_Hor_Dominant.cs
Text_Hor_Subordinate.cs
Text_Horizontal_Demo.cs
Text_Rotatable Circle_PartitionsAndComments.cs
CommentToRect_Demo.cs
Form_About.cs
Form_DorothyHouse.cs 132
Form_Main.cs 17
Form_Polyline.cs 64
83 - 89 Form_Text_Rotatable_Class.cs 83
(uses Text_Rotatable_Demo class)
Ring_PartitionsAndComments.cs
Text_Rotatable_Demo.cs
TextScale BarChart_ForDefinition.cs
Form_DefineNewBarChart.cs 706
692 Form_PlotsVariety.cs 688
UnclosableInfo 83 Form_Main.cs 17
Form_Person.cs 790
Form_ReferenceBook_Groups.cs 516
Form_TemporaryGroup.cs 447
… …
This class is used in more than 20 examples;
mostly these are the simplest examples with only
few elements in view. The same type of
information is also often used on the tab pages.
Underlayer BarChart_ForDefinition.cs
Form_DataRefinement.cs 638
Form_DataViewBIN.cs
Form_DataViewTXT.cs 659
Form_DefineNewBarChart.cs 706
590 Form_Functions.cs 594
Form_GraphManualDefinition.cs 669
Form_OnePlot.cs 663
Form_PlotsVariety.cs 688
Form_Trackbars.cs 250
SpotsOnPlot.cs
World of Movable Objects 952 (978) Appendix B. Use of classes from MoveGraphLibrary.dll
Form_ElementsAndGroups 813 21.68 circle Circle_EAG resizing, rotation, and partitions movement (all On/Off)
polygon (chatoyant) ChatoyantPolygon_EAG resizing, rotation, and reconfiguration (all On/Off)
polygon (convex) ConvexPolygon_EAG resizing, rotation, and reconfiguration (all On/Off)
polygon (convex) ConvexPoly_RegPolyHole_EAG resizing, rotation, and reconfiguration (all On/Off)
polygon (regular) RegPoly_CircularHole_EAG resizing and rotation (all On/Off)
polygon (regular) RegPoly_IdenticalHole_EAG resizing and rotation (all On/Off)
polygon (regular) RegPoly_RegPolyHole_EAG resizing, rotation, and independent borders rotation (On/Off)
polygon (regular) RegularPolygon_EAG resizing and rotation (all On/Off)
rectangle BilinearScale resizable horizontally; with additional movable circle
rectangle GroupOfElements adjusting to all changes of inner elements
rectangle Rectangle_EAG resizing and rotation (all On/Off)
rectangle ZoomingGroup adjusting to all changes of inner elements
ring Ring_EAG resizing, rotation, and partitions movement (all On/Off)
strip Strip_EAG resizing and rotation (all On/Off)
Form_FamilyTree 801 21.55 circle BusConnection small non-resizable
polyline Bus moving (On/Off); if non-movable, then reconfigurable
rectangle Person resizable
Form_FamilyTreeSettings 811 21.67 polyline Bus movable (On/Off); if non-movable, then reconfigurable
rectangle Person resizable
Form_FileNameViewerPlus 525 16.3 rectangle FileNameViewer_Demo resizable
rectangle HorRailCircle resizable; with additional movable circle
Form_FillTheHoles 193 8.16 circle Plug (with Shape.Circle) resizable; N-node cover
polygon (regular) Plug (with Shape.Polygon) resizable
rectangle AreaWithHoles non-resizable with a lot of holes
Form_FreeBuses 771 21.36 polyline Bus movable (On/Off); if non-movable, then reconfigurable
Form_FreeBuses_AddingJoints 772 21.37 polyline Bus movable (On/Off); if non-movable, then reconfigurable
Form_FreeBuses_AllPossibleChanges 776 21.41 polyline Bus movable (On/Off); if non-movable, then reconfigurable
Form_FreeBuses_ChangingMovability
774 21.38 polyline Bus movable (On/Off); if non-movable, then reconfigurable
Form_GraphManualDefinition 669 18.46 circle(s) SpotsOnPlot array of small circles visible only inside a rectangle
rectangle SpotNest non-resizable; with additional circle movable anywhere
Form_Label 856 22.17 rectangle LabelGR_Demo horizontal resizing; no rotation; adhered mouse
Form_LabyrinthForDachshund 339 11.32 circle SpotOnCommentedWay adhered mouse; movement along the way
Form_Lines_Solitary 36 2.3 line (segment) LineSegment movable, resizable, rotatable
Form_Main 17 I.1 polygon (chatoyant) ChatoyantPolygon_EAG resizing, rotation, and reconfiguration (all On/Off)
polygon (convex) House_Simple reconfiguration
ring Ring_EAG resizing, rotation, and partitions movement (all On/Off)
World of Movable Objects 958 (978) Appendix C. Classes designed for Demo application
Form_Rings_SimpleCoverAdh 366 12.6 ring Ring_SimpleCoverAdh two pairs of circular nodes; adhered mouse
Form_Rings_SlidingPartitions 212 9.4 ring Ring_SlidingPartitions N-node cover; movable partitions
Form_Rings_WithComments 382 12.16 ring Ring_WithComments adhered mouse
Form_SegmentOnLine 321 11.25 line SegmentOnLine adhered mouse moves end points along the line
Form_SetOfObjects 213 9.5 circle Circle_EOS resizing, rotation, and moving partitions (all On/Off)
polygon (chatoyant) ChatoyantPolygon_EOS resizing (On/Off); rotation (On/Off)
polygon (convex) ConvexPolygon_EOS resizing (On/Off); rotation (On/Off)
polygon (convex) ConvexPoly_RegPolyHole_EOS resizing (On/Off); rotation (On/Off);
polygon (regular) RegPoly_CircularHole_EOS resizing (On/Off); rotation (On/Off)
polygon (regular) RegPoly_IdenticalHole_EOS resizing (On/Off); rotation (On/Off)
polygon (regular) RegPoly_RegPolyHole_EOS resizing (On/Off); rotation (On/Off);
polygon (regular) RegularPolygon_EOS resizing (On/Off); rotation (On/Off)
rectangle Rectangle_EOS resizing (On/Off); rotation (On/Off)
ring Ring_EOS resizing, rotation, and moving partitions (all On/Off)
strip Strip_EOS resizing (On/Off); rotation (On/Off)
Form_SettingsVariants 554 17.8 circle UnmovableRadioButton the only node in the cover is Frozen, so it is unmovable
line SliderInRectangle movable (Hor or Ver) inside rectangular area
Form_SlicesIndividual 492 15.51 circle sector Slice_Individual rotation, resizing; movement along radius; adhered mouse
Form_SlidersChangeableOrder 281 11.9 line SliderInRectangle movable (Hor or Ver) inside rectangular area
Form_SlidersInRectangle 280 11.8 line SliderInRectangle movable (Hor or Ver) inside rectangular area
Form_SlidersUnchangeableOrder 282 11.10 line SliderInRectangle movable (Hor or Ver) inside rectangular area
Form_SpotInArbitraryLabyrinth 250 11.19 circle Spot_Restricted movement is restricted by the walls; adhered mouse
line (segment) LineSegment movable, resizable, rotatable
rectangle GroupOfLines contains a set of lines; adjusts to their movements
Form_SpotOnCommentedWay 335 11.30 circle SpotOnCommentedWay adhered mouse
Form_SpotOnConnectedSegments 334 11.29 circle SpotOnSegments adhered mouse
Form_SpotsOnLinesAndArcs 317 11.24 circle SpotOnArc adhered mouse
circle SpotOnLine adhered mouse
Form_Spouses 793 21.52 circle BusConnection small non-resizable
polyline Bus movable (ON/OFF); if non-movable, then reconfigurable
rectangle Person resizable
Form_StripCommented 96 5.10 strip Strip_Nnodes N-node cover
Form_StripInLabyrinth 301 11.18 strip Strip_Wandering N-node cover; adhered mouse
Form_Strips_SimpleCoverAdh 368 12.9 strip Strip_SimpleCoverAdh adhered mouse
World of Movable Objects 962 (978) Appendix C. Classes designed for Demo application
Form_Text_Horizontal_Class 76 5.2 rectangle Text_Horizontal_Demo analogue of the Text_Horizontal class from DLL
Form_Text_Rotatable_Class 84 5.7 rectangle Text_Rotatable_Demo analogue of the Text_Rotatable class from DLL
Form_Texts_MoveAsSample 89 5.8 rectangle Text_Spotted non-resizable rectangle with nine small circular nodes
Form_Triangles 111 6.5 triangle Triangle reconfigurable; two types of node order
Form_Tuning_Bus 777 21.42 polyline Bus movable (On/Off); if non-movable, then reconfigurable
Form_Tuning_Line 310 11.21 line LineSegment movable, resizable, rotatable
Form_Tuning_Person 790 21.50 rectangle Person resizable
Form_TwoGenerations 795 21.53 circle BusConnection small non-resizable
polyline Bus movable (ON/OFF); if non-movable, then reconfigurable
rectangle Person resizable
Form_UpperMenu 274 11.7 rectangle UpperMenu_BD resizable (left-right) with movable rectangles inside
Form_Village 748 21.11 polygon (convex) House_Primitive reconfiguration
polygon (convex) House_Rural reconfiguration
polygon (convex) House_RuralLeftGarage reconfiguration
polygon (convex) House_RuralRightGarage reconfiguration
rectangle BuildingsGroup non-resizable
semicircle Hangar resizable
World of Movable Objects 963 (978) Appendix C. Classes designed for Demo application
Arc
Arc_Thin 151 No Yes thin arc; N-node cover Form_Arcs_Thin
Form_ReferenceBook_Primitives
Arc_Thin_ChangeableAngle 324 No Yes thin arc; adhered mouse; change angle [10, 350] Form_Arcs_ChangeableLength
Arc_Thin_FullyChangeable 328 Yes Yes thin arc; adhered mouse; change angle [10, 350] Form_Arcs_FullyChangeable
derived from Arc_Thin_ChangeableAngle
Arc_Wide 153 No Yes wide arc; N-node cover Form_Arcs_Wide
Form_ReferenceBook_Primitives
Arc_Wide_SimpleCover 183 No Yes wide arc; 3 or 4 nodes (depends on the arc angle) Form_Arcs_SimpleCover
Form_ReferenceBook_Primitives
Circle(s)
Ball 289 No No movement restrictions (rectangle) Form_BallsInRectangles
Ball_SCNO 292 No No movement restrictions (rectangle and some other balls) Form_NoSameColorOverlapping
BusConnection 778 No No small circle Form_ConnectedBuses_AllPossibleChanges
Form_ConnectedBuses_ChangingMovability
Form_ConnectedBuses_Three
Form_ConnectedBuses_Two
Form_FamilyTree
Form_PersonPlusBus
Form_Spouses
Form_TwoGenerations
CircleInsideConvexPoly 294 No No adhered mouse; moves inside regular polygon Form_AdheredMouse
Circle_EAG 814 Yes/No Yes/No resizing, rotation, and partitions moving (all On/Off) Form_AddElementsOrGroup
Form_ElementsAndGroups
Circle_EOS 214 Yes/No Yes/No resizing, rotation, and partitions moving (all On/Off) Form_SetOfObjects
Circle_Multicolor_Regulated 476 Yes/No Yes/No moving, resizing, and rotation (all On/Off) Form_AddCircle
partitions moving (On/Off) Form_ObjectsOnTabControl
allow disappearance (On/Off) Form_Order_Circles
auxiliary line (On/Off) Form_ReferenceBook_Groups
Circle_Nnodes 138 Yes Yes N-node cover Form_Circles_Multicolored
Form_NnodeCovers
Form_ReferenceBook_Primitives
World of Movable Objects 964 (978) Appendix C. Classes designed for Demo application
UnmovableRadioButton 558 No No the only node in the cover is Frozen, so it is unmovable Form_SettingsVariants
Circle sector
CircleSector_FullyResizable 180 Yes Yes all possible changes; angle < 180; N-node cover Form_CircleSector_FullyResizable
Form_ReferenceBook_Primitives
CircleSector_MovableArc 175 Yes Yes only radius is changeable; angle < 180; N-node cover Form_CircleSector_MovableArc
Form_ReferenceBook_Primitives
CircleSector_Nonresizable 171 No Yes only movable; no other changes; angle < 180 Form_CircleSector_Nonresizable
Form_ReferenceBook_Primitives
CircleSector_OneMovableSide 177 Yes Yes arc and one side are movable; angle < 180; N-node cover Form_CircleSector_OneMovableSide
Form_ReferenceBook_Primitives
CircleSector_Sibling 486 Yes Yes composes a circle with sectors-siblings Form_CircleComposedOfSectors
PieSlice 497 Yes Yes movement individual and correlated with other slices Form_Pie
Slice_Individual 493 Yes Yes rotation, resizing; movement along radius; adhered mouse Form_Pie
Form_Slices_Individual
Crescent
Crescent 168 Yes Yes cover of six circular nodes; the biggest one is transparent Form_Crescent
Form_ReferenceBook_Primitives
Line (segment)
LineSegment 36 Yes Yes movable, resizable, rotatable Form_Lines_Solitary
Form_ReferenceBook_Primitives
Form_SpotInArbitraryLabyrinth
Form_TuningLine
LineSegment_Regulated 463 Yes/No Yes/No moving, resizing, rotation (all On/Off) Form_ObjectsOnTabControl
SegmentOnLine 322 Yes Yes adhered mouse moves end points along the line Form_SegmentOnLine
SliderInRectangle 278 No No movable (Hor or Ver) inside rectangular area Form_SettingsVariants
Form_SlidersChangeableOrder
Form_SlidersInRectangle
Form_SlidersUnchangeableOrder
SliderLocal 649 No No line movable (left/right) between two limits Form_DataRefinement_Zoom
Polygon (chatoyant)
ChatoyantPolygon 116 Yes Yes resizing; rotation; reconfiguration Form_Polygons_Chatoyant
Form_ReferenceBook_Primitives
World of Movable Objects 966 (978) Appendix C. Classes designed for Demo application
ChatoyantPolygon_EAG 814 Yes/No Yes/No resizing; rotation; reconfiguration (all On/Off) Form_AddElementsOrGroup
Form_Colors_ChatoyantPolygon
Form_ElementsAndGroups
Form_Main
ChatoyantPolygon_EOS 215 Yes/No Yes/No resizing (On/Off); rotation (On/Off) Form_SetOfObjects
ChatPoly_Regulated 467 Yes/No Yes/No moving, resizing, rotation, and reconfiguration (all On/Off) Form_AddPolygon
Form_ObjectsOnTabControl
Polygon (convex)
ConvexPoly_RegHole_RotatableBorders Form_ConvexPoly_RegHole_RotatableBorders
191 Yes/No Yes/No resizing; reconfiguration; different rotations (all On/Off) Form_ReferenceBook_Primitives
ConvexPoly_RegPolyHole 162 Yes Yes Form_ConvexPoly_RegPolyHole
Form_ReferenceBook_Primitives
ConvexPoly_RegPolyHole_EAG 815 Yes/No Yes/No resizing; rotation; reconfiguration (all On/Off) Form_AddElementsOrGroup
Form_ElementsAndGroups
ConvexPoly_RegPolyHole_EOS 215 Yes/No Yes/No resizing (On/Off); rotation (On/Off) Form_SetOfObjects
ConvexPolygon 108 No No reconfiguration Form_Polygons_Convex
ConvexPolygon_EAG 814 Yes/No Yes/No resizing; rotation; reconfiguration (all On/Off) Form_AddElementsOrGroup
Form_ElementsAndGroups
ConvexPolygon_EOS 215 Yes/No Yes/No resizing (On/Off); rotation (On/Off) Form_ReferenceBook_Primitives
Form_SetOfObjects
House_Dorothy 132 No Yes reconfiguration Form_DorothyHouse
House_Primitive 750 No No reconfiguration Form_Village
House_Primitive_Sample 763 No No Form_Colors_PrimitiveHouse
House_Rural 752 No No reconfiguration Form_Colors_RuralHouse
Form_Village
House_Rural_Sample 763 No No only movable Form_Colors_RuralHouse
House_RuralLeftGarage 754 No No reconfiguration Form_Colors_RuralLeftGarage
Form_Village
House_RuralLeftGarage_Sample 761 No No only movable Form_Colors_RuralLeftGarage
House_RuralRightGarage 756 No No reconfiguration Form_Colors_RuralRightGarage
Form_Village
House_RuralRightGarage_Sample 761 No No only movable Form_Colors_RuralRightGarage
House_Simple 17 No No reconfiguration Form_Main
World of Movable Objects 967 (978) Appendix C. Classes designed for Demo application
Polyline
Bus 770 No No movable (On/Off); if Off, then reconfigurable Form_ConnectedBuses_AllPossibleChanges
Form_ConnectedBuses_ChangingMovability
Form_ConnectedBuses_Three
Form_ConnectedBuses_Two
Form_FamilyTree
Form_FamilyTreeSettings
Form_FreeBuses
Form_FreeBuses_AddingJoints
Form_FreeBuses_AllPossibleChanges
Form_FreeBuses_ChangingMovability
Form_PersonPlusBus
Form_Spouses
Form_Tuning_Bus
Form_TwoGenerations
Link_Submenu_Submenu 578 No No movable are only inner joints Form_BlockDiagram
Link_Uppermenu_Submenu 579 No No movable are inner joints and end points Form_BlockDiagram
Polyline 64 No Yes reconfigurable by moving segment ends Form_Polyline
whole line is movable and rotatable Form_ReferenceBook_Primitives
Polyline_Unmovable 568 No No movable are only inner joints Form_Polylines_Unmovable
movability of end points is regulated
Rectangle
AreaWithHoles 194 No No with a lot of holes Form_FillTheHoles
BarChart_ForDefinition 707 Yes No resizable rectangle includes a set of SingleBar objects Form_DefineNewBarChart
Form_PlotsVariety
BilinearScale 827 Yes No with additional movable circle Form_ElementsAndGroups
BoardWithBalls 292 No No non-resizable; includes 20 small balls Form_NoSameColorOverlapping
BuildingsGroup 757 No No non-resizable Form_Village
ButtonsSketch 744 No No a set of small rectangles connected by lines Form_BtnPlaces_Functions
Form_BtnPlaces_Numbers
Form_BtnPlaces_Operations
Form_BtnsPlacement_Functions
Form_BtnsPlacement_Numbers
Form_BtnsPlacement_Operations
Chapter 766 No No with additional movable rectangle Form_BookByChapters
Form_ChapterParams
World of Movable Objects 969 (978) Appendix C. Classes designed for Demo application
ClosableInfo 78 No No Form_ClosableInfo
Form_ClosableInfoParams
ComboBox_DDList 853 Yes No horizontal resizing Form_ComboBox
CommentOnCircleRadius 387 No No adhered mouse is used for moving along circle radius Form_CirclesRingsSpecialComments
Form_DefineNewPieChart
Form_PlotsVariety
CommentOnRingRadius 391 No No adhered mouse is used for moving along ring radius Form_CirclesRingsSpecialComments
Form_DefineNewRing
Form_PlotsVariety
CommentToRect_Demo 229 No Yes similar behaviour to the CommentToRect (from DLL) Form_Rectangles_WithComments
FileNameViewer_Demo 521 Yes No analogue of FileNameViewer class from DLL Form_FilenameViewersPlus
FrameSpacesSetter_Demo 876 Yes No frame movable inside limits Form_RadioGroupParams
GroupOfElements 818 No No adjusting to all changes of inner elements Form_ElementsAndGroups
GroupOfLines 310 No Yes contains a set of lines; adjusts to their movements Form_SpotInArbitraryLabyrinth
HorRailCircle 528 Yes No with additional movable (Left – Right) circle Form_FilenameViewersPlus
HorScaleAnalogue 245 Yes/No No moving and resizing (together On/Off) Form_PlotAnalogue
Label_GR 856 Yes No horizontal resizing Form_Label
LineStyleSelector_Demo 875 Yes No Form_RadioGroupParams
Person 788 Yes No resizable Form_FamilyTree
Form_FamilyTreeSettings
Form_Person
Form_PersonPlusBus
Form_Spouses
Form_Tuning_Person
Form_TwoGenerations
PlotAnalogue 246 Yes No resizable Form_PlotAnalogue
RadioBtnsGroup_Light 875 No No group adjusts to the inner elements Form_RadioBtnsGroup_Light
RadioButtonsGroup 882 No No group adjusts to the inner elements Form_RadioButtonsGroup
RadioButton_GR 880 No No movable in all directions Form_RadioButtonsGroup
RadioButton_Light 875 No No movable only up and down Form_RadioBtnsGroup_Light
Rectangle_AllMovements 68 Yes Yes moving, resizing, and rotation Form_Rectangles_AllMovements
Form_ReferenceBook_Primitives
Rectangle_AllMovementsAdh 347 Yes Yes adhered mouse; all movements Form_Rectangles_AllMovementsAdh
World of Movable Objects 970 (978) Appendix C. Classes designed for Demo application
Rectangle_CellsInView_Demo 352 Yes No adhered mouse; resizing by one corner and adjacent sides Form_Rectangles_WithCells
Rectangle_EAG 814 Yes/No Yes/No resizing (On/Off); rotation (On/Off) Form_AddElementsOrGroup
Form_ElementsAndGroups
Rectangle_EOS 214 Yes/No Yes/No resizing (On/Off); rotation (On/Off) Form_SetOfObjects
Rectangle_FixedRatio 56 Yes No resizing; no rotation Form_Rectangles_FixedRatio
Form_ReferenceBook_Primitives
Rectangle_FixedRatioAdh 341 Yes No resizing; no rotation; adhered mouse Form_Rectangles_FixedRatioAdh
Rectangle_OneSideTurnOut 52 Yes No while squeezed into line, can disappear from view Form_Rectangles_OneSideTurnOut
Form_ReferenceBook_Primitives
Rectangle_Regulated Yes/No Yes/No moving, reconfiguring, and rotation (all On/Off) Form_ObjectsOnTabControl
Rectangle_ResizeByOneSide 50 Yes No one side resizing Form_Rectangles_SingleSideResizing
Form_ReferenceBook_Primitives
Rectangle_Restricted 839 Yes/No No resizing (all variants) Form_Rectangles_ForControls
Rectangle_Sibling 482 Yes Yes additional node for synchronous movement of siblings Form_Rectangles_Siblings
Rectangle_Simple 481 No No only forward moving Form_Order_Rectangles
Rectangle_SlidingPartitions 203 Yes No resizing; sliding partitions Form_Rectangles_SlidingPartitions
Form_ReferenceBook_Primitives
Rectangle_Standard 41 Yes No variants of resizing Form_Polylines_Unmovable
Form_Rectangles_Standard
Form_ReferenceBook_Primitives
Rectangle_StandardDisappear 47 Yes No resizing; disappear when squeezed to tiny size Form_Rectangles_StandardToDisappear
Rectangle_SymmetricalChange 54 Yes No symmetrical change throughout resizing Form_Rectangles_SymmetricalChange
Form_ReferenceBook_Primitives
Rectangle_WithBalls 289 Yes No resizing inside range; balls inside Form_BallsInRectangles
Rectangle_WithComments 230 Yes No associated with arbitrary number of comments Form_Rectangles_WithComments
RectangleCommented 240 Yes No resizable; associated with a List of comments Form_Rectangles_WithComments_Advanced
SegmentedData 639 No No set of narrow rectangles movable left/right Form_DataRefinement
Form_DataRefinement_Zoom
SingleBar 707 No No rectangle with one movable side; cover has only one node Form_DefineNewBarChart
SpotNest 670 No No with additional small circle movable anywhere Form_GraphManualDefinition
Submenu_BD 575 No No non-resizable Form_BlockDiagram
World of Movable Objects 971 (978) Appendix C. Classes designed for Demo application
RuralHouse House_Rural
RuralPlusLeft House_RuralLeftGarage
RuralPlusRight House_RuralRightGarage
Sample_Hangar Hangar_Sample
Sample_PrimitiveHouse House_Primitive_Sample
Sample_RuralHouse House_Rural_Sample
Sample_RuralPlusLeft House_RuralLeftGarage_Sample
Sample_RuralPlusRight House_RuralRightGarage_Sample
SegmentOfLine SegmentOnLine
SimpleCircle Circle_Simple
SimpleHouse House_Simple
SimpleRect Rectangle_Simple
SimpleSquare Square_Simple
SimpleTrio Triangle_Simple
SlidingPartitionsCircleOnTab Circle_Multicolor_Regulated
SpotOnLineSegment SpotOnLine
SpottedText Text_Spotted
SpottedTextSample Text_SpottedSample
SquareSample Square_Sample
StripNR Strip_Nnodes
StripSV Strip_Wandering
SubordinateTextM Text_Hor_Subordinate
TextMR Text_Rotatable
TriangleOnTab Triangle_Regulated
UnicolorCircleOnTab Circle_Unicolor_Regulated
UpDownSpots Spots_UpDown
Way_ArcSegment Way_Arc
Way_ArcSegmentCommented Way_ArcCommented
Way_LineSegment Way_Line
Way_LineSegmentCommented Way_LineCommented