Login | Register   
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 3

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.

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.

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



Thanks for your registration, follow us on our social networks to keep up-to-date