RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Design and Use of Moveable and Resizable Graphics, Part 1 : Page 5

In typical modern operating systems and applications, windows are moveable and resizable; graphics and controls inside applications are not. But it doesn't have to be that way.

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.

Sergey Andreyev worked in the Computer Center of the Russian Academy of Sciences for many years, initially, on systems for applied optimization. Later, he became fascinated by sonogram images. He received a PhD for the thesis "Design of new algorithms and programming systems for speech analysis." He likes to implement new ideas in new areas. He has designed complicated systems for telecommunications, thermodynamics, and analysis of big electricity networks. He has also worked on applications for foreign language studies, and for organizing photo archives. Photography is one of his hobbies. Sergey prefers to work with people who are more talented than he is, and surrounded by thousands of volumes that concentrate the wisdom of the past. Throughout the years he found places to work on his ideas in the USA, Finland, and New Zealand. He likes to travel where the only sign of human existence on Earth is the rare movement of tiny lights across the dark sky.
Email AuthorEmail Author
Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date