devxlogo

Design and Use of Moveable and Resizable Graphics, Part 1

Design and Use of Moveable and Resizable Graphics, Part 1

eople communicate with computers on two different levels. On the upper level you see a very flexible system of windows: you can move them, resize, overlap, or put side by side. However, starting an application immediately removes all the flexibility, leaving you to work only inside the scenario developed by the designer of the program. Typically, you cannot move graphics or controls nor resize them. I have designed an extremely powerful mechanism that makes graphical objects moveable and resizable. My technique not only significantly improves existing applications, but it takes them to another level. This article explains in detail the construction and use of moveable and resizable graphical objects.

When you switch on your PC, you usually sink into the world of rectangular windows. You can easily move these windows, resize them, overlap them, or put them side by side. At any moment you can reorganize the whole screen view to whatever you really need. It wasn’t this way at the beginning of the computer era; it became the law only after Windows conquered the world. Consider this axiom 1 in modern day programming design:

On the upper level, all objects are moveable and resizable.

To make these features obvious and easy to use, windows have title bars on which users can click to move them, and borders that allow users to resize windows. Being moveable and resizable are standard features of essentially all windows; developers eliminate those features only for special purposes.

Usually, the goal of switching on the computer is not to move some rectangular windows around the screen; you want to do a bit more. You start an application, step into the inner level, and then everything changes. It’s here, inside the applications, where you do 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 program designer allows you to do. The design can be excellent or horrible; it can influence the effectiveness of your work in different ways, but still it unnecessarily deprives users 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 levels is that on the upper level you have only one type of object, windows, and on the inner level there are two different types: controls, inheriting from windows, and graphical objects that have no inheritance from windows and are absolutely different. The addition of these graphical objects changes the whole inner world.

Controls and 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 for all controls, and from time to time, they do use them, for example, via anchoring and docking. The most important thing is not how you can move or resize controls, but that you can organize moving and resizing without problems.

Graphical objects have a different origin than controls and, by default, are neither moveable nor resizable. There are ways to make things look different than 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, a programmer can make it look as though a graphic is resizable as users resize a form (dialog). Simply paint on top of a panel and make the panel the subject of anchoring/docking. By default, panels have no visible borders, and if the background color of the panel is the same as its parent form, then there is no way to distinguish between painting directly on the form or on the panel. Certainly, such “resizing” of graphics is very limited, but sometimes sufficient; it all depends on the purpose of the application.

Another solution for resizing rectangular graphical objects is to use bitmap operations, but in most cases quality problems, especially when enlarging images, precludes using this solution. Both of these (painting on a panel or using bitmap operations) are tricky solutions and have one common defect?you can use them only with rectangular objects.

When a limited area is populated with two different types of tenants (controls and graphical objects) that abide by different rules, the only way to organize peaceful co-residence and avoid problems is to force them to live under one set of rules. Because graphics are neither moveable nor resizable, the easiest solution is to ignore these controls’ features, as if they didn’t exist. That is why so few applications allow users to move around any inner parts. Thus you have axiom 2:

On the inner level, objects are usually neither moveable nor resizable.

Interestingly, these two axioms create this absolutely paradoxical situation:

  • On the less important upper level, users have absolute control of all the components, and can make changes easily.
  • On the more important inner level, users have nearly no control at all. When they are given some control, it is usually limited and always organized indirectly through some additional windows or features.

No one has actually declared these axioms as axioms in a strict mathematical way; at the same time I’ve never seen, read, or heard about even a single attempt to look at this awkward situation in any other way than as an axiom, or 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 these same rules on your everyday life, it would work like this: you are free to move around the city or country, but somebody will tell you where to put each piece of furniture inside of your house. Would you question such a situation?

Certainly, anyone reading this article can easily think of an example of resizing a graphical object. For example, in Paint you’ve seen a dotted line moving with your mouse cursor. You can do this trick fairly easily using an XOR operation?but it has nothing to do with real moving or resizing of objects. This type of “moving or resizing” is only an imitation, but in some situations it works.

Making moveable/resizable graphics is not a theoretical idea of the “nice to have” type. For many years I worked to develop very complicated programs for engineering and scientific tasks in totally different areas. Though the aims of these programs had nothing in common, all of them required, to a high extent, the use of different forms of plotting. The quality of users’ analysis of the most difficult problems in both engineering and scientific tasks (and in many other disciplines) depends heavily on the quality of the graphical presentation of data and results. Because every user has his personal view about how a system must look to be the best instrument for their own work, the development of such systems happens in parallel with never-ending discussions (and even quarrels) between designers and users. I suspect anyone who designs this type of application is familiar with this situation, and is used to working under the pressure of multiple simultaneous requests, which are often in opposition to each other.

Giving users full control to move graphics in such complicated systems would help to reduce the endless discussions about which graphical layout offers the best view. Full control would significantly increase the effectiveness of engineers’ work with such applications, which is the main goal. I saw again and again that the inflexibility of the graphics, designed and fixed by the programmer of the application, became not only the main problem in further improvement of engineering and scientific software, but became the real barrier in exploration and solving of the most interesting problems.

Big engineering and scientific programs are brilliantly designed, but development of big applications takes time, so every user is restricted to whatever vision of the situation the designer had one, two, or three years ago. These are the consequences of having non-moveable graphics?everyone must work with designer-driven applications.

In The Universe in a Nutshell (Bantam Books, 2001), Steven Hawking writes, “In science, finding the right formulation of a problem is often the key to solving it?.” When I started to work on the problem of making graphics moveable and resizable, I began with the analysis of the features that I would like to implement. My goal was not to make some kind of moveable graphical object (for individual objects of a particular type anything can be done), but to find a general solution. Initially I looked at scientific and engineering plotting, which usually has a rectangular shape?but that was only one kind of experimental model. I looked for the general solution and I found it. Before describing the whole algorithm I want to emphasize that:

  • You can use my algorithm in absolutely different areas and with arbitrary forms of objects.
  • You can and must look on the design of moveable/resizable graphics separately from the consequences of using such graphics. Classes and algorithms used for the design may differ, but including any form of moveable and resizable graphics serves to move the application into an absolutely new paradigm?user-driven applications.
  • It is theoretically a “nice to have” type of idea. At www.SourceForge.net in the MoveableGraphics project you’ll find a whole package of applications and documents that help to explain moveable/resizable graphics.
Editor’s Note: This article was first published in the March/April 2008 issue of CoDe Magazine, and is reprinted here by permission.

Designing Moveable Graphics
Part 1 of this article describes the ideas of organizing moveable/resizable graphics. It covers applying these ideas to some moderately complicated cases. Part 2 discusses more about building application that use moveable graphics, for form customization for example.

Basic Requirements
As a programmer and a designer of very complicated systems, I would prefer that Visual Studio already had the ability to create moveable/resizable graphics. Unfortunately it isn’t there yet, so let me describe an imaginary scenario of what I really need and would like to have:

  • I need an easy way to declare any object in a form (dialog) as moveable and resizable.
  • Easy doesn’t mean primitive. The object configuration may be at any level of complexity; changing the configuration may be influenced by a lot of different things. For example, some objects may allow any types of changes, others may need several parameters (sizes) to remain fixed, and parts of the objects may generate restrictions on other parts’ changes. Still, the entire variety of possible reconfigurations must be easy to understand and implement.
  • These features?moveable and resizable?must be added like an extra “invisible” feature; they must not destroy any image, but it must be obvious that the features are there and available while working with the application. In some cases, I can demand a visual indication of feature availability, but typically, availability should be apparent without any extra lines or marks.
  • Not only must new classes be easily declared moveable and resizable, it must also be easy to add these features to already existing classes; by just touching them (or using the keyboard) the objects should become moveable.
  • Programmers must be able to add these new features only when they want to; adding these features shouldn’t be an “all or nothing” case. Programmers can use objects of the same classes with or without these new features; in other words, they can start using them piecemeal, so they can see how the features will affect existing complicated applications.
  • Using moveable graphics at the application level must be as simple as moving windows on the upper level: press and move, press and reconfigure, and even press and rotate, (which is not organized for windows, but can be extremely useful for many graphical objects). When it is useful, the process should make it easy to organize graphics without any limitations.

Some of these “nice to have” features look like they conflict (simple but with all the possibilities you can imagine, and not visible and obvious), but they don’t. All these features are designed and working now.

I work with C#, so all the code here is in C# and I’ll use C# terminology. But the algorithms and designed classes are not limited to C#?you can develop them easily with other languages. The Test_MoveGraphLibrary contains all the code and samples from the application.

Basic Idea: Contour Presentation
There are two ways to add moveable/resizable features to an object: use an interface or an abstract class. After trying both ways I decided upon an abstract class. Any object that you want to make moveable and resizable “must be” derived from the abstract class GraphicalObject that declares three crucial methods. (Closer to the end of this article, after describing all standard techniques and some special cases, I’ll write about the back door around the must be case.)

   public abstract class GraphicalObject   {      public abstract void DefineContour ();      public abstract void Move (int cx, int cy);      public abstract bool MoveContourPoint (int i,          int cx, int cy, Point ptMouse,          MouseButtons catcher);   }

The core idea for making graphical objects moveable and resizable is based on contour presentation. Any object involved in moving and/or resizing must have a contour. For graphical objects the contour is organized in the DefineContour() method.

A contour consists of nodes and their connections. These two types of contour elements are used for different purposes. Except on rare occasions, a contour does not duplicate an object’s shape. A contour looks more like a skeleton, which allows the flesh around it to move as a single body; however, for many objects, contour is placed outside. For these objects, the contour looks like a frame. A contour may even consist of disjointed sets of nodes and connections and it will still be a single contour. There are many possibilities, because contours were designed to cover any possible scenario of any real or imaginary object finding its way into the programming world.

Nodes (the ContourApex class) are used as sensitive areas at the ends of connections. Some nodes can be moved separately, thus providing reconfiguration or resizing of the object. The MoveContourPoint() method must act only for nodes that can be moved individually. Each node has its sensitive area, although for special cases, when a node must be excluded from the individual movement, this area will be null. When the node’s area is not null and the mouse cursor is moving across this area, you can change the shape of the cursor to indicate that the user can grab and move the node. There are some conformity rules for possible movements of the object and the shape the mouse cursor can become, but you have some flexibility in defining these. I’ll talk about them in describing the design of the real contour. Sizes and forms of sensitive nodes can vary; the simplicity of using such moveable graphics depends on the designer’s decisions when organizing nodes.

Connections (the ContourConnection class) are used for grabbing and moving an object as a whole, implemented in the Move() method. Each connection also has a sensitive area where you can change the shape of the cursor to indicate that the entire object can be grabbed and moved. The form of the sensitive area is defined by the ratio between the length of connection and the width of the area. If the length is much bigger than the width, the area will look like a strip; if the width is much bigger than the length, it will be a circle; intermediate variants have a “sausage” shape.

Very often the entire area of the graphical object is used for some mouse-generated events that initiate actions or changes with the object. Moving and resizing is based on mouse-generated events at nodes and connections, thus the addition of contour may be the cause of conflict between old commands and new requirements. To minimize this conflict, nodes are usually small and connections thin, though in special situations you can design sensitive areas that cover the maximum size.

Nodes and connections are the construction elements of a wide variety of contours; each design depends on the goal of the particular graphical object. You’ll see how all this works in the real world.

SimpleHouse: A Classical Moveable and Resizable Object
There was a time when you could take a pen and a sheet of paper and design your own house. No restrictions, just imagination. (And no bills, repairs, taxes?I want to go back!) If you liked your first house, you could put another one nearby; you could construct a street, a village, a town (if you had enough space on the paper). There was only one problem: if you drew anything wrong, you had to find a way to erase it or you had to abandon the whole project and start a new one on a new sheet of paper. Not a bad idea, but it’s a pity to abandon an entire village just because one cottage is the wrong color. So this example designs moveable and resizable buildings where all the colors can be changed.

The class SimpleHouse is part of the Test_MoveGraphLibrary project. You can find the code in the SimpleHouse.cs file. This class is used in the form Form_Houses.cs (see the Houses menu). I call these simple houses because their basic form is just a rectangle and a triangular roof (see Figure 1. Houses can be wide or narrow (at least two windows), high or low (at least one floor). The roof can be also high or low and its top can move to one side or another. The house number will be somewhere on the roof; don’t worry, the postman in the town will be Astrid Lindgren’s Carlson; if you don’t know him?he can fly:

?
Figure 1. SimpleHouse Object: Here’s the simple house image for which you’ll create a contour.
   public class SimpleHouse : GraphicalObject   {      int nNumber;      Font font;      Rectangle rcHouse;      Point ptTop;      int roomSize;      ...   }

Before writing any code, you must decide about the placement and type of the contour. This is a real game for kids, so the spots for resizing and moving (nodes and connections) must be absolutely obvious without any extra visualization. The five points at the corners of the house plus the point at the top of the roof look like good places for clicking, moving, and changing house size, so these are ideal places for nodes. Similarly, the house borders plus the edges of the roof look like good places for grabbing and moving the whole house to a new place, so these lines will be the connections. Often, there are several different variants of contour for any object, and later you’ll see some examples. Once you decide where the nodes and connections go, you can define the contour as shown below:

   public override void DefineContour ()   {      ContourApex [] ca = new ContourApex [5];      ca [0] = new ContourApex (0,         new Point (rcHouse .Left, rcHouse .Top),         new Size (0, 0), MovementFreedom .Any, Cursors .Hand);      ca [1] = new ContourApex (1,         new Point (rcHouse .Right, rcHouse .Top),         new Size (0, 0), MovementFreedom .Any, Cursors .Hand);      ca [2] = new ContourApex (2,         new Point (rcHouse .Right, rcHouse .Bottom),         new Size (0, 0), MovementFreedom .Any, Cursors .Hand);      ca [3] = new ContourApex (3,         new Point (rcHouse .Left, rcHouse .Bottom),         new Size (0, 0), MovementFreedom .Any, Cursors .Hand);      ca [4] = new ContourApex (4, ptTop,         new Size (0, 0), MovementFreedom .Any, Cursors .Hand);      ContourConnection [] cc = new ContourConnection [6] {         new ContourConnection (0, 1), new ContourConnection (1, 2),         new ContourConnection (2, 3), new ContourConnection (3, 0),         new ContourConnection (0, 4), new ContourConnection (1, 4) };      contour = new Contour (ca, cc);   }

Designing the contour consists of three steps:

  1. Initialize the array of nodes (ContourApex[]).
  2. Initialize the array of connections (ContourConnection[]).
  3. Initialize the contour, based on these two arrays.

Initializing any new ContourApex uses five parameters:

  • int nVal: Each contour node must have a unique number. The numbers must be in the range [0, nodes-1]. The sequence of numbers doesn’t matter. In this case, I began from the top-left corner, went clockwise to the other corners, and then added the rooftop. However, all the numbers must be different, as the code uses them later to initialize connections.
  • Point ptReal: This is a real point on the screen; in the example above it is either a corner of the house or the rooftop.
  • Size szRealToSense: This is a shift from the real point to the middle of the node associated with this point. For this simple house case, I don’t need to move the nodes anywhere from the real points because these nodes will be not shown at all; however, there are situations when it is very helpful to move the node slightly aside so it’s not too close the real image of the object. The decision to shift nodes away from real points is used, for example, for complicated engineering plots, where you don’t want the contour to obscure even a tiny part of the plot.
  • MovementFreedom mvt: Possible sole movements of the node.
  • Cursor cursorShape: The shape the cursor should be when it passes over the node.

Regardless of what you use as the fourth parameter for the particular ContourApex, all nodes will move when a user moves the whole object. The MovementFreedom parameter describes only the opportunities for separate movements of this node when you want it (or not) to participate in reconfiguration. The possible values are:

   public enum MovementFreedom {      None,    // is not used for reconfiguration      NS,    // can move only Up or Down      WE,    // can move only Left or Right      Any };    // can move in all directions

The last parameter in initializating the ContourApex defines the shape of the mouse cursor when the mouse is above the node. Keep users’ standard expectations in mind. For example when the cursor takes the form of Cursors.SizeWE, users expect that the object underneath can be moved only left or right?don’t create confusion by setting a very strange pair of the last two parameters. However, there are no restrictions on cursor shape based on possible movement; you can decide what to do with this fifth parameter.

The type of contour I have created for SimpleHouse is typical of contours; it has several nodes and several lengthy connections between them. Again, you can create special types of contours; I’ll describe that process in detail later.

Author’s Note: Although the whole idea of moveable and resizable graphics is based on contour presentation, the DefineContour() method is usually the only place where you will have to think about the contour! The rule is: organize the contour, and then forget about it. (There is one exception for this rule, which I’ll cover further when describing individual movements of the nodes.)

Move(cx, cy) is the method for moving the whole object by a number of pixels, passed as the parameters.

Drawing graphical objects with any level of complexity is usually based on one or a few very simple elements, such as Points and Rectangles, and some additional parameters such as size. When moving a whole object, you don’t have to change its size, only the position of these basic elements. For the SimpleHouse, there are two such elements: Rectangle rcHouse and Point ptTop:

   public override void Move (int cx, int cy)   {       rcHouse .X += cx;       rcHouse .Y += cy;       ptTop += new Size (cx, cy);   }

MoveContourPoint (i, cx, cy, ptMouse, catcher) is the method you use for moving individual nodes. The method returns a Boolean that indicates whether the required movement is allowed; for example, for forward movement, the method must return true when any proposed movement along the X or Y axes is allowed. If the movement of one node results in synchronous relocation of a lot of other nodes, it is easier to put the call to DefineContour() inside this method, and then it doesn’t matter what value is returned from MoveContourPoint(). This may happen even for forward movement, when movement of one node affects the relocation of other nodes, and it usually happens with rotation, when all nodes must be relocated. This is the exception to the previously mentioned rule that you don’t have to think about contour after defining it.

Method MoveContourPoint() has five parameters, but not all of them are used each time:

  • int i: This is the identification number of the node?the same number used in the DefineContour() method to identify this node. If the node may not be moved separately, you don’t need to mention it in the MoveContourPoint() method.
  • int cx: Controls movement (in pixels) along the horizontal scale; use a positive number for moving from left to right. Use this parameter when writing code for forward movement.
  • int cy: Controls movement (in pixels) along the vertical scale; use a positive number for moving from top to bottom. Use this parameter when writing code for forward movement.
  • Point ptMouse: This point holds the mouse cursor position; for calculations of forward movement I ignore this parameter and use the previous pair; for calculations of rotation I ignore the previous pair and use only the current cursor position, which is much more reliable for organizing rotations.
  • MouseButtons catcher: Informs you which mouse button was used to grab the object; if you want to allow movement using any mouse button, simply ignore this parameter. If only one button can initiate the move, you can use this parameter to determine which button was pressed. When nodes can be involved in two types of movement (such as forward movement and rotation), this parameter can help distinguish between them.

Usually the method for an individual node’s movement will be the longest of all three methods; however, there are interesting situations when the method you use will consist exactly of one line. Because this method must include the code for moving each node (for the majority of objects), it can be long, but relatively uncomplicated, as more often than not the code for different nodes is similar. For SimpleHouse, I will only discuss parts of the code for two different nodes: the top left corner and the rooftop. Here’s the code for moving the top left corner node:

   public override bool MoveContourPoint (int i, int cx, int cy,      Point ptM, MouseButtons catcher)   {      bool bRet = false;      if (catcher == MouseButtons .Left)  // resizing with left button      {         if (i == 0)     // Top-left corner         {            if (rcHouse .Height - cy >= minHeight)             {                          // compare with minumum height               rcHouse .Y += cy;               rcHouse .Height -= cy;               ptTop .Y += cy;          // roof height is not changing               bRet = true;            }            if (rcHouse .Width - cx >= minWidth)               // compare with minumum width            {               rcHouse .X += cx;               rcHouse .Width -= cx;               ptTop .X += cx;               ptTop .X = Math .Min (Math .Max (rcHouse .Left +                  minRoofSide, ptTop .X), rcHouse .Right - minRoofSide);               bRet = true;            }         }         ...

In the full code for this method you can see that the code for two corners on each side of the house is partly the same as the code shown above.

Note that you are doing nothing with the contour itself, but only checking the possibility of proposed movements of the real points associated with the nodes. For the top-left corner of the house the code:

  • Compares the proposed new height of the house with the minimum allowed height; if the top of the house is moving, then the rooftop must also move.
  • Compares the proposed new width of the house with the minimum allowed width; if the width of the house is changing, then the code must determine a new rooftop position. The new rooftop position should remain in relatively the same position as it was originally; but the rooftop cannot be closer to any side than the predefined minRoofSide value.

The piece of code for the rooftop (identification node 4) is similar: It compares the proposed new rooftop position with the minimum allowed roof height and with the minimum allowed distances to the sides of the house:

      else if (i == 4)     // Rooftop      {         if (ptTop .Y + cy <= rcHouse .Top - minRoofH)         {        // compare with minumum roof height            ptTop .Y += cy;            bRet = true;         }         if (rcHouse.Left +minRoofSide <= ptTop.X + cx &&            ptTop.X + cx <= rcHouse.Right - minRoofSide)         {            ptTop .X += cx;            bRet = true;         }      }

What you've just seen is a typical case for organizing a moveable and resizable graphical object. First you design the contour, consisting of several nodes and their connections, and then develop methods for moving object as a whole, and for resizing it. Now that you can move and resize SimpleHouse objects I will show you how to organize the whole process.

The Boss Class: Mover
The easiest way to move any object around the screen is to drag it. For the whole process you need only three mouse events: MouseDown, MouseMove, and MouseUp.

From a user's point of view, the whole process must be simple: press, move (or rotate), and release; any additional complication will not be acceptable. A designer planning to include moveable/resizable objects in an application must store, analyze, and process the following information for each object: positions of the nodes and connections, sensitive areas, whether they overlap, order of drawing, type of movement, etc.

The beginning of this article discussed the basic requirements for the whole moving/resizing process and wished for a class that would provide all the desired features. Such a class exists, and it's ready to do everything you need. Its only requirement is that the object involved in moving and resizing is either derived from GraphicalObject or is a control (I'll write about this case in part 2):

   public class Mover

The name declares only a portion of this class's capabilities, because it also handles resizing, and provides associated information. The Mover class is described in MoveGraphLibrary_Classes.doc.

Now I'll show you how to move from a single house to the town using the code in the file Form_Houses.cs. All the houses in the town should be moveable and resizable, so they're derived from GraphicalObject:

   public class SimpleHouse : GraphicalObject

Next, the code declares the company responsible for all house movements and reconfiguration, and start constructing the town:

   Mover Movers = new Mover ();   List Town = new List ();

A new house will pop up in the town when you click the New House button:

   private void Click_btnNewHouse(object sender,      EventArgs e)   {      SimpleHouse newhouse = new SimpleHouse(         nNew, ..., rc);  // initialize new house         // insert the new house into the List<>      Town.Insert (0, newhouse);          // register the new house as moveable      Movers .Add(newhouse);       Invalidate ();   }

The real code is slightly longer; the code here shows only the significant lines. The preceding code:

  • Initializes a new house object: The first line creates and initializes a new SimpleHouse instance.
  • Inserts the new house into the official town's list of buildings: Every new house should appear on top of all previous houses, so the application inserts new houses at position zero, while painting (see the Paint() method) occurs from the opposite end.
  • Registers the new house as moveable: This is mandatory if you want the house to become moveable/resizable! The contour gives an object the ability to be involved in the moving/resizing process; this line adds the object to the list of moveable/resizable objects.

Several important things about Mover:

  • Mover works only with the objects that it was asked to take care of. Mover has its own list of these objects. Programmers have access to the elements of this list via standard indexing or other standard List<> methods (see MoveGraphLibrary_Classes.doc for more complete descriptions of implemented methods).
  • Mover doesn't know anything about the real objects; Mover works only with object contours of objects included in its list. It is the programmer's responsibility to make parallel changes between the outer world (there can be several lists, arrays, or anything else) and the List inside Mover. For example, consider a situation in which Mover tries to move an object that a user has already deleted from the real world (from the form). You don't have to declare all objects on the form as moveable/resizable; you may decide to give this feature to all, some, or none of the objects. Therefore, it's your responsibility to ensure that Mover works on a correct List. The set of List methods makes any needed task easy (delete, add, insert).

You can see the real power of Mover in the code for the three mouse events. On the MouseDown event it tries to grab a house either for moving or for reconfiguring. In this application, I decided that you can only start this process with the left mouse button?right-click is used for other things:

   private void OnMouseDown(object sender, MouseEventArgs mea)   {      if (mea .Button == MouseButtons.Left)      { // start moving/resizing         Movers.CatchMover(mea.Location);       }      else if(mea.Button == MouseButtons.Right) {         ...         ContextMenuStrip = contextMenuOnHouse;      }      if (!Movers .MoverCaught)      {    // if not clicked for moving, bring to top         BringToTop (mea .Location);      }   }

It doesn't matter how many moveable/resizable objects are in the form; you need only one line to start the moving/resizing process:

   Movers.CatchMover(mea.Location);

This is the only piece of code in the OnMouseDown() method that is really important for starting any moving/resizing process; all the other lines simply do additional things such as opening the context menu or bringing the house that was clicked on to the top.

The MouseUp event finishes any moving/resizing process, and releases any previously grabbed object. The code is always extremely short:

   private void OnMouseUp(object sender, MouseEventArgs e)   {       Movers.ReleaseMover();   }

The third event?MouseMove?has to do the real moving/resizing, but even here, the code is not complicated:

   private void OnMouseMove(object sender, MouseEventArgs mea)   {      Cursor cursor = Cursors.Default;      foreach(SimpleHouse house in Town) {         if (house.Inside (mea.Location)) {            Cursor.Current = Cursors .Hand;            break;         }      }      Movers.MovingMover(mea.Location);       // real moving/resizing      if (Movers.MoverCaught) {         Invalidate ();      }   }

The first half of this method?the bigger part?has nothing to do with moving and only changes the cursor's shape if the mouse is over any house. The real moving occurs in one line of code:

   Movers.MovingMover(mea.Location); 

This checks the Mover's situation. If the Mover signals that it has caught some object from its list, it triggers a repaint.

Author's Note: To avoid screen flicker, don't forget to switch ON double-buffering in the form; it has nothing to do with the design of moveable/resizable graphics, but it is simply a nice feature from Visual Studio.

That's the extent of your construction?you should now have a town of moveable and resizable houses. In the sample application you'll find some additional useful things, including the ability to change all the colors, save the image to the Clipboard for printing, save the picture into a file, and restore it from the file.

SimpleHouse has a classical contour of several nodes with lengthy connections between them, but you'll soon encounter other interesting and useful situations that demand special contour design, as discussed in the following sections.

Special Contour Cases
The following sections discuss several specialized types of contours.

Case A: Same Nodes?Different Connections
Suppose you want an object to be resizable in only one direction, such as a scale that the user can only resize horizontally. For purposes of this example, the contour must be external to the scale's image (see Figure 2). If you needed an absolutely non-resizable but moveable scale, it would be sufficient to have four nodes close to the corners of the object; however, horizontal resizing requires two additional nodes on the left and right sides, that differ from the other four.

?
Figure 2. Scale Object: You can resize this scale object only horizontally, along the X axis.

Here are the variables:

   // all four borders of the scale's    // rectangular area   int cxL, cxR, cyT, cyB;      int cyM; // middle of the sides   int shift; // the distance nodes are   // shifted outside the scale's rectangle

You can organize the array of nodes like so:

   apexes = new ContourApex [6] {       new ContourApex (0, new Point (cxL, cyT),        new Size (-shift, -shift),        MovementFreedom.None, Cursors.SizeAll),       new ContourApex (1, new Point (cxR, cyT),        new Size ( shift, -shift),         MovementFreedom.None, Cursors.SizeAll),       new ContourApex (2, new Point (cxR, cyM),        new Size ( shift, 0),         MovementFreedom.WE, Cursors.SizeWE),       new ContourApex (3, new Point (cxR, cyB),        new Size ( shift, shift),         MovementFreedom.None, Cursors.SizeAll),       new ContourApex (4, new Point (cxL, cyB),        new Size (-shift, shift),         MovementFreedom.None, Cursors.SizeAll),       new ContourApex (5, new Point (cxL, cyM),        new Size (-shift, 0),         MovementFreedom.WE, Cursors.SizeWE)    };

I numbered the nodes from the top-left corner going clockwise. For the four corner nodes (those with MovementFreedom.None), the last parameter (the cursor's shape) doesn't matter. Also, while showing the contour, these nodes will not display at all?as if they do not exist (see Figure 2). Still, you need these nodes because connections can occur only between nodes. If you want to have the contour around the object, you have to use all six nodes.

The contour's initialization is based on the array of nodes:

   contour = new Contour(apexes);

You may be surprised to see that while organizing this contour I didn't organize the array of connections. Instead, I used the Contour constructor with one parameter. In this case, the array of connections is organized automatically by linking each node in the array with the next one, and then linking the last node with the first.

Figure 2 shows the contour and the scale. If you want to have the same horizontally resizable scale, but without the requirement for the contour to be outside the scale, you can construct another contour using the same two small nodes in the middles of the sides?the single connection between them can go right through the middle of the scale:

   apexes = new ContourApex [2] {       new ContourApex (0, new Point (cxR, cyM),        new Size (shift, 0),         MovementFreedom .WE, Cursors .SizeWE),      new ContourApex (1, new Point (cxL, cyM),        new Size (-shift, 0),         MovementFreedom .WE, Cursors .SizeWE)   };

For resizing, these two solutions are absolutely identical; in both, the two small nodes on the sides can be grabbed and moved left or right. The code for the Move() method will also be the same, as it is the same object. The MoveContourPoint() method will be the same (other than the difference in the identification numbers). You need to write code only for the nodes that are involved in resizing.

The difference in using identical scales with different contours becomes apparent in the process of moving the whole object. In the first case (contour around the object), the user can only move the scale by grabbing the areas close to the border of the object. In the second case (contour through the middle of the scale), the area for grabbing the object will be a thin horizontal strip somewhere in the middle. I say somewhere because, depending on the changing text and the font used, this strip's place can change. It is difficult to find nodes in the middle of the sides without special visualization; a better solution would be to place the nodes at the ends of the main line. Then at least users would have no problem in finding them.

When the scale's main line is always visible, the best solution is to put the nodes at the ends of the main line. In many cases, however, you can show the scale without the main line, using just ticks and text. For a user to find nodes at the ends of the invisible line would be a really tricky thing. The point emphasized here is that there are always different ways to organize contours. You must decide the best and most obvious solution for the users who are going to work with the objects.

Both samples in this case still belong to the most common case of contours?with several small nodes and connection(s) between them. Other special cases of contours and their design will have more impact on the MoveContourPoint() method code.

Case B: Moveable, but Non-Resizable
Using moveable but not resizable objects is common. The described system of nodes and connections can produce different contours for such objects, all of which will work perfectly?a designer must simply choose one of the possible solutions. For square shape objects, for example, you can find a description in the TwoNodesSquare.cs file and squares are used in Form_ColoredSquares.cs.

As you can assume from the name of the file (TwoNodesSquare.cs), the contour is based on two standard, small nodes. However, you can forget about the sizes of the nodes, because they won't be used at all; the code declares the nodes as not used for movement so they will be automatically set to null:

   public override void DefineContour ()   {      Point ptM = Auxi_Geometry .Middle (rc);       // middle point of Rectangle      ContourApex [] apexes = new ContourApex [2] {          new ContourApex (0, new Point (ptM .X - 1,              ptM .Y), new Size (0, 0),            MovementFreedom.None, Cursors.SizeAll),          new ContourApex (1, new Point (ptM .X + 1,              ptM .Y), new Size (0, 0),            MovementFreedom.None, Cursors.SizeAll)       };      contour = new Contour (apexes);      contour .LineSensitivity =          Math .Min (rc .Width, rc .Height) / 2 - 1;   }

Place these two nodes in the middle of the square next to each other. Neither node is moveable, so the method for moving nodes individually simply returns false, as discussed previously. The connection between the two nodes is very short (in this case, 2 pixels), but the sensitivity of this connection has been increased up to the sides of the square, so the sensitive area of the connection will be very close to a circle inscribed into a square?it will cover nearly 80 percent of the square. Most users won't notice that they cannot grab the square in the small areas close to the corners; they can drag the square from anywhere else:

   public override void Move (int cx, int cy)   {      rc .X += cx;      rc .Y += cy;   }   public override bool MoveContourPoint (   )   {      return (false);   }

Later, I added another variant of the same type of contour (null nodes, increased sensitivity of connections), but with four nodes instead of two. All four nodes are still unmovable separately. They stay far away from each other, and the sensitivity of each connection is only half as much as in the two-node case. However, these four nodes are located in such a way?each is halfway from the corner to the middle of the square?that the combined sensitive area covers 95 percent of the square. In Form_ColoredSquares.cs, you can add new objects of the TwoNodesSquare class to the view; the number of contour nodes (two or four) will be one of the parameters while initializing the new object. That points out an interesting feature: objects of the same class may have different types of contours! The program marks the number of nodes on the colored square when it's painted, so you can see the number of nodes for each square without having to remember them. You can define the same size squares with different types of contours and compare for yourself whether there's a big difference between moving two-node or four-node squares around the screen.

Case C: Another Moveable, Non-Resizable Example
There is one more type of contour design for moveable but not resizable squares. This solution may seem odd, but it covers the whole square:

   public override void DefineContour ()   {      ContourApex [] ca = new ContourApex [1];      ca [0] = new ContourApex (0, ptCenter,         new Size (0, 0),         MovementFreedom .Any, Cursors .Hand);      ca [0] .SenseAreaSize = size;      // change the node's size      contour = new Contour (ca, null);   }      public override void Move (int cx, int cy)   {      ptCenter += new Size (cx, cy);   }   public override bool MoveContourPoint (int i,      int cx, int cy, Point ptMouse,      MouseButtons catcher)   {      bool bRet = false;      if (catcher == MouseButtons .Left)      {         Move (cx, cy);         bRet = true;      }      return (bRet);   }

The preceding code defines a unique contour consisting of a single node. There are no connections, but the sensitive area (the size) of this single node has been set to cover the whole square. Because there are no connections, moving the object is based on moving this single node?you call the Move() method from inside the MoveContourPoint() method. The node's sensitive area covers 100 percent of the square, so you can start moving by clicking at any point within the square.

The last two cases are also for moveable, but non-resizable square objects. Usually non-resizable objects require a special design for the MoveContourPoint() method, and the code of this method is typically much shorter than for resizable objects.

Case D: Changing Node Shape
To make this system of nodes and connections even more flexible, you can change the shape of any node from square to circle:

   public override void DefineContour ()   {      ContourApex [] ca = new ContourApex [1];      ca [0] = new ContourApex (0, ptC,         new Size (0, 0),      MovementFreedom .Any, Cursors .Hand);      ca [0] .SenseAreaSize = nRadius;      ca [0] .SenseAreaForm = ContourApexForm .Circle;      // change the node's shape      contour = new Contour (ca, null);   }

These contours, consisting of a single circle node, are used in the RegularPolygon.cs file. Look at Form_RegularPolygons.cs to see how it works. Special case B showed an example of an object that could receive different types of contours; here you have an example of different objects that have the same contour. From a programmer's point of view all these regular polygons are objects of the same type (same class), but for a user, they are absolutely different (they look different).

Case E: Rectangular Contour
The majority of objects in most applications have a rectangular shape. There is nothing special about organizing the contour for a rectangular object; the most obvious solution is to put the contour somewhere close to the outer borders of this rectangle. The difference lies in the proximity and the possibilities for transformation that you wish to apply to a contour. Because rectangular contours are common, they have a special constructor:

   public Contour (Rectangle rc, ContourResize       resize, int shift)

The second parameter defines the possible contour transformations:

   public enum ContourResize { None, NS, WE, Any };

The last parameter defines the amount of outside shift from the rectangle. Using this type of initialization you can develop the scales contour described in case A like this:

   public override void DefineContour ()   {      contour = new Contour(Area, ContourResize.WE, 4);   }

Case F: Sizeable In Any Direction
Another widely used case is a set of nodes that can move separately in all directions, and which are placed exactly at the points to which they are related:

   public Contour (Point [] pts, int [,] connects) or   public Contour (Point [] pts)

The first array contains all the node points; the second array is for the connections. The second version of this initialization is for the set of nodes connected by the infinite loop. For example, you could use this type of contour with all freely moving nodes in the SimpleHouse class, describing the same contour in a different way, as shown below. Here again I've numbered the nodes from the top-left corner of the house going clockwise, and then added in the rooftop:

   contour = new Contour (new Point [] {      new Point (rcHouse .Left, rcHouse .Top),      new Point (rcHouse .Right, rcHouse .Top),      new Point (rcHouse .Right, rcHouse .Bottom),      new Point (rcHouse .Left, rcHouse .Bottom),      ptTop },       new int [,] { {0, 1}, {1, 2}, {2, 3}, {3, 0},         {0, 4}, {4, 1} }      );

Case G: Resizable, but Non-Moveable
All the previous samples were about moveable objects, either resizable or not; however, from time to time you need an unmovable object to be resizable. If you think about this combination of features in terms of a nodes/connections presentation, it's obvious that the object's contour must have a set of nodes, and each node will move according to specific limitations; however, the set of connections must be empty, thus eliminating any possibility of moving the whole object:

   contour = new Contour (ContourApex[] apexes, null);

This type of object is widely used in engineering applications where a user needs to quickly set the profile of some device, or use a function to start calculations where the resize defines the y(x) function as a parameter. Defining the function by several mouse clicks is so widely required that I included a special Profile class in the MoveGraphLibrary.dll. You'll see samples of using this class in both the demonstration applications, and I'll write more about this class in Part 2 of this article.

Contour Summary
You now know that you can use contours to make objects moveable/resizable. The diversity of objects calls for different types of contours. Here's a summary of contour design:

  • A contour can consist of any number of nodes and their connections.
  • The minimum number of nodes is one. A contour may consist of a single node without any connections.
  • A contour may consist of a set of disjointed parts; each part may consist of a single node or arbitrary connected nodes.
  • Connections may exist only between nodes; there must be a node on both ends of any connection.
  • You can move nodes individually, thus allowing you to reconfigure the object.
  • By grabbing any connection you can move the whole object.
  • A contour may consist of a series of connections between empty nodes. This makes an object moveable, but not resizable.
  • Each node has its own parameters. By connecting nodes of different types, for example, it is easy to allow resizing along one axis but prohibit it along another. This means organizing a limited reconfiguration.
  • It doesn't matter that some contours may represent graphical objects and others controls or groups of controls (I'll describe these in part 2). All contours are treated in the same way, allowing users to easily change the inner view of any application.

Using a combination of node shape, node size, and the sizes of sensitive areas around connections, you can organize any required contour.

You've seen a discussion about organizing contours to make objects moveable/resizable?not only the classical contour types, but also special cases that allow you to use this technique with a wide variety of objects. In Part 2 you'll see more complicated cases of moveable/resizable graphics such as engineering plotting, and more about objects involved in both moving and rotation. I'll also explain how you can apply the same technique to controls and how you can base form customization on moveable/resizable objects.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist