Objective Views : Chapter 7 The Canvas : The CODController Class
The CODController Class
The controller handles the interaction between the user and the canvas. Its purpose is to receive command and mouse messages from the user, and translate them into actions or commands to be performed upon the model or viewport. The controller is a state machine that transitions when a message is received. When the user completes a task or activity, the controller typically sends a command to the model and updates the viewport.
The controller inherits message map processing from CWnd, but it does not actually have its own window handle. Instead, the controller is plugged into the window that contains the viewport so that it can process messages for the viewport.
The controller changes its state and updates the viewport as it receives events. When a user interaction task is complete, the controller typically generates a command object and passes it to the model. A set of virtual methods containing the prefix Execute() take care of command creation and execution.
State
The controller acts as a state machine: it alters its state so it can monitor a series of user actions. For example, when an end-user moves a component, the controller needs to perform multiple actions. A different state is assigned for each action, such as selecting, dragging, and dropping.
Because the controller performs multiple functions, it also supports multiple states. Each possible state is enumerated at the beginning of the OdController.h file. The controller keeps the current state in its m_state member variable.
By default, the state is ready. However, the state changes each time the end-user performs an action with the mouse, keyboard, toolbar or menu.
If you want the controller to perform a new action, you need to create a new state. Ensure that the new state does not conflict with an existing state.
Interacting with Components
The activity most frequently performed by the controller is manipulating the components on the canvas. There are many means to change components. A list of the most common means follows:
Hit Testing
If the controller is given a coordinate, it will detect whether the coordinate is within one of the components on the canvas. The system uses hit testing extensively to decide which components to act upon. Three methods support hit testing:
Rollover(). This method is called when the mouse is moved. If the user moves the mouse cursor over a component, the cursor changes shape to indicate that the user can perform some action on that component.
Hit(). This method is called when the user clicks the mouse. In addition to detecting whether the cursor is over a component when the user clicks the mouse, this method also performs an appropriate action when it detects the mouse is over a component. Typically, the end-user can select a component by clicking it. This method can distinguish between the control handle of a component and the component itself. Clicking a component or its control handle prompts a number of actions to occur such as moving, rotating, scaling, or moving the vertices of a component.
DblHit(). This method is called when the user double-clicks the mouse. If the user double-clicks a component, this method performs the appropriate action. The default action is editing of a label.
Selection
The user can select multiple components to act upon. As mentioned earlier in this section, you generally select components by clicking on them. The user can also select multiple components by clicking the canvas and then dragging a selection box around the components. The set of selected components is maintained in the member variable m_setSelection.
The selection changes when certain actions are performed. For example, if the user selected three components and then grouped them together to form one composite component, the selection would change from the three individual ones to the one group component. There are two methods that handle this logic: DoSelection() and UndoSelection(). DoSelection() alters the selection when an action is performed, while UndoSelection() undoes the selection.
Cursor Changes
When the user manipulates the components on the canvas, the cursor changes to reflect the current state and the user’s options, which are described in the “Hit Testing” section. There are two ways to change the cursor’s appearance. Select SetStandardCursor() to set the cursor to one of the Windows cursors. Select SetCustomCursor() to load a cursor from the application’s resources.
Mouse Cursor Restrictions
A user can restrict the mouse cursor to make precise movements. Several methods support this functionality.
AlignWithGrid() and FindClosestDeviceGridPoint(). These methods address aligning moving components to the grid.
EqualOffset(). This method restricts the manipulation of components to equal consideration in the horizontal and vertical directions. For example, if a user wanted to create a circle, he could select the Ellipse tool button and restrict its proportions.
Manhattan(). This method allows manipulation in only one direction: horizontal or vertical. This method is used to restrict lines to 90° degree angles.
Movement
One of the primary actions the controller performs is moving components. A move begins when the user clicks a component and drags it within the viewport. The controller handles moves in one of two ways.
OLE drag-and-drop is used when the OLE libraries are initialized, which is the default behavior. OLE drag-and-drop enables the end-user to move or copy components from the current window to another window. The controller copies the components to the clipboard. The OLE system refers to the components on the clipboard when it’s determining where to move them.
Each controller has its own drop target object (CODDropTarget) to handle all the logic for dragging and dropping components within its canvas.
If the OLE libraries have not been initialized, component movement can only occur within the component’s window. Objective Views provides three methods to support this type of movement: StartMoving(), Moving(), and EndMoving(). The state machine within the controller determines which of these methods is called in response to mouse messages.
No matter how the movement is performed within the controller, once the move is complete a CODMoveCommand is created and executed to change the model.
Rotation
An end-user can also rotate a component. The logic for rotating a component is similar to the logic for moving components. Since rotation always takes place in the same window, no OLE functionality is necessary. There are three methods that can be called depending on the state of the controller and the processing of mouse messages. These methods correspond to the methods used for movement: StartRotating(), Rotating(), and EndRotating(). A CODRotateCommand is created and executed when the user finishes rotating the component.
Scaling
Scaling components is similar to moving and rotating them, except there are a few more variables. The direction that the component is scaled depends on which of the eight control handles the user clicked to start the scaling. There are also three methods that handle the scaling action: StartScaling(), Scaling(), and EndScaling(). When the user finishes scaling a component, a CODScaleCommand is created and executed.
Vertex Editing
The user can edit the individual vertices of a primitive component, such as a polygon. An interface exists to add, delete, or move a vertex.
In the controller, moving a vertex is similar to moving a component. The methods that support this action are: StartMovingVertex(), MovingVertex(), and EndMovingVertex(). A CODMoveVertexCommand is created and executed upon completion.
Text Editing
An end-user can edit a label associated with a component on the screen. Call the methods StartTextEdit() and EndTextEdit() to enable this functionality.
Drawing New Components
The controller allows the user to draw a new primitive component on the screen. There is a set of methods for drawing each of the following primitives.
Line
Polyline
Polygon
Curve
Closed Curve
Rectangle
Ellipse
Which component is drawn depends on two factors: the state of the controller and the type of mouse message. After the user finishes drawing the component, a CODInsertCommand is created and executed.
Inserting New Components
Methods exist for inserting components directly onto the canvas. There is a set of methods for inserting image components, text components, port components, and complete symbol components.
Tracking
Tracking is the type of feedback Objective Views supplies to the end-user while he is creating or manipulating components. When the user moves the component across the canvas, tracking places an outline around the component. The purpose of this outline is to convey how the component would look if you dropped it in its current position. The controller accomplishes tracking by interpreting the end-user’s actions and then asking the viewport to draw the “ghosting” of the component.
There are several methods that address tracking objects. There is a tracking method that manages each primitive type when the end-user is drawing a new component. The application calls the BeginTracking() method when the end-user starts moving an existing component. It calls EndTracking() when the end-user stops moving an existing component.
Linking Symbols
The controller provides an interface for defining a relationship or a link between two components. Drawing a link is like drawing a line or polyline, except the endpoints of the link are connected to either the center of a symbol or a specific port on a symbol.
Other Capabilities
This section lists additional capabilities.
Zooming
The controller detects when the user wants to zoom in or out on the canvas, or focus in on a certain section of the canvas. There is a set of methods for handling this functionality.
Panning
The end-user can pan the viewport to look at different areas. The controller interprets the state and mouse movements and then instructs the viewport on a course of action.
Context Menus
You can right-click to display a shortcut (context) menu. The content of this menu varies depending on the selection. By default, different menus are assigned to the background and to symbols. In addition to customizing menus, you can also customize the rules for displaying menus. Just override the PopupMenu() method to change the behavior.
Coordinate Transformations
The mouse coordinates that are supplied through mouse messages are always in device coordinates. To convert these coordinates into the logical coordinate system, the viewport requires methods similar to DPtoLP() and LPtoDP(). The controller supplies two utility methods for this purpose: VpDPtoLP() and VpLPtoDP().
Command Creation
The end result of many of user actions is a command object. Using command objects enables you to log user actions into the model’s transaction model for undo and redo purposes. When you enable this functionality, any change that affects the model is bundled into a command object. For example, a move would become a command object while a zoom would not.
You do not have to wait for the user to do something to create a command object. There are many utility methods in the controller for creating commands whenever you wish. These methods are all in the form of ExecuteXCommand(). If you call one of these, a command will be created and executed and the model will be changed. If the user decides to undo a command, the last one you created will be undone.Instead of using commands, you can always call the corresponding methods in the model.