Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Задани на лабораторные работы. ПРК / Professional Microsoft Robotics Developer Studio

.pdf
Скачиваний:
126
Добавлен:
20.04.2015
Размер:
16.82 Mб
Скачать

www.it-ebooks.info

Chapter 4: Advanced Service Concepts

8. To send messages from the Form to the service, post them to the port that was supplied to the constructor.

9. To send information from the service to the Form, either copy data into public variables inside the Form or post a FormInvoke message to the WinFormsServicePort so that a delegate can execute public methods inside the Form using the Form handle.

This might seem like a daunting task, but once you get the hang of it you will find that it is not that complicated. These steps are explained in more detail in the following sections.

Creating a WinForm

Creating a Windows Form and then setting it up to interact with your main service is a fairly involved process. However, once you have done it a couple of times you should not have any trouble.

As you saw in the last section, you need to add a reference to System.Windows.Forms to your project, and a using statement at the top of your code.

You create a new Windows Form for your service in exactly the same way as you do for any Visual Studio project, i.e., click Project Add Windows Form. Figure 4-5 shows the Add New Item dialog for adding the DriveControl Form to the TeleOperation project.

Figure 4-5

The TeleOperation service has two forms, each of which is associated with a source file where you place the event handlers:

DriveControl: This enables you to control the robot. The DriveControl Form is discussed in this section. It is the window on the left in Figure 4-4.

WebCamForm: This displays the live video feed. This is covered in the section on using web cameras. It is the window on the right in Figure 4-4.

173

www.it-ebooks.info

Part I: Robotics Developer Studio Fundamentals

For the DriveControl Form, then, at the top of the main service, you declare a variable to hold a handle (a reference to the class instance) for the Form, _driveControl, as well as a port for messages from the Form, _eventsPort:

//Handle to the main WinForm UI DriveControl _driveControl;

//Port for the UI to send messages back to here (main service) DriveControlEvents _eventsPort = new DriveControlEvents();

The DriveControlEvents class is discussed in the next section and is defined in DriveControl.cs. For now, you only need to know that it is a PortSet that enables the DriveControl Form to send information back to the main service.

There is nothing magical about these classes or variables. You can name them whatever you like, and you can change the messages that are accepted by the DriveControlEvents PortSet. This is just one example of how to implement communication between a service and a WinForm. Other samples in MRDS use different approaches. However, the authors believe this is a consistent and manageable approach.

In the Start method, you have to create a new instance of the DriveControl Form. (The WebCamForm is created only after a Webcam service has been found). This is done as follows:

// Create the WinForm UI WinFormsServicePort.Post(new RunForm(CreateForm));

The RunForm message specifies a delegate to execute to create the new Form. This delegate should return a handle to the Form as follows:

System.Windows.Forms.Form CreateForm()

{

// NOTE: Modify the constructor in the Form to pass these parameters return new DriveControl(_eventsPort, _state);

}

This routine is trivial and could be specified as an anonymous delegate in the RunForm message instead of as a separate method. If you are an experienced Windows Forms programmer, you might have noticed that the code does not save the handle to the new Form instance. It can be done here, but for illustrative purposes it is done later.

Note that when the DriveControl Form is created it is passed two parameters. Normally the constructor for a WinForm does not take any parameters. You must modify the automatically generated code for the Form to accept these parameters (in DriveControl.cs):

public partial class DriveControl : Form

{

//This port is passed across to the constructor

//It allows messages to be sent back to the main service DriveControlEvents _eventsPort;

//This is part of the main service State

GUIOptions options;

174

www.it-ebooks.info

Chapter 4: Advanced Service Concepts

public DriveControl(DriveControlEvents EventsPort, TeleOperationState state)

{

InitializeComponent();

//Remember the port to use to send back messages _eventsPort = EventsPort;

//Copy the option settings

options = state.Options;

All the constructor needs to do is copy the parameters to variables inside the Form. Other code within the Form can then use these variables as necessary. Clearly, the EventsPort is required so that the Form can post back messages to the main service. The reason for passing across the service state is not so obvious — it enables the Form to access the option settings that are stored in the config file.

You have not entirely finished with Form creation because you still need the Form handle. This last step is covered in the next section.

Passing Information Between a WinForm and a Service

Information needs to be passed in both directions between the main service and the WinForm. This section discusses how to pass information in each of these directions and the different approaches required:

Sending to the Form: A WinForm is a separate module, not a service in its own right. Because it operates as a Single-Threaded Apartment model, it cannot wait on CCR ports to receive messages. However, the main service needs to update information on the Form in response to notification messages such as game controller updates. Sending information from the main service to the Form is done using FormInvoke.

Receiving from the Form: The Form needs to pass back commands to the main service. When you interact with the Form, events fire inside the Form code. These WinForm events are not related to the CCR in any way, but the event handlers in the Form can send CCR messages back to the main service by posting to the _eventsPort, which was created especially for this purpose in the code in the previous section.

What FormInvoke does is execute code from the main service in the context of a WinForm. It does this by posting a message to the WinFormsServicePort, which means that the delegate executes asynchronously with respect to the caller. In other words, any code that follows a FormInvoke in the same routine cannot assume that the FormInvoke has completed execution.

A good example is the handler for axes updates from the game controller. This handler sends the new axes information to the DriveControl Form so that the onscreen trackball and the associated X, Y, and Z values can be updated:

IEnumerator<ITask> GameUpdateAxesHandler(game.UpdateAxes update)

{

if (_driveControl != null)

{

WinFormsServicePort.FormInvoke(

(continued)

175

www.it-ebooks.info

Part I: Robotics Developer Studio Fundamentals

(continued)

delegate()

{

_driveControl.UpdateGameControllerAxes(update.Body);

}

);

}

yield break;

}

Note the following regarding the preceding code:

The code checks to make sure that the DriveControl Form is active and then calls FormInvoke on the WinFormsServicePort. The delegate takes advantage of its access to local variables in the handler to pass update.Body to the UpdateGameControllerAxes method in

DriveControl.cs, i.e., inside the Form. Notice the use of _driveControl, which is a pointer to the Form instance, to execute a public method inside the Form.

UpdateGameControllerAxes is a public method in the Form code (DriveControl.cs). It is too involved to explain the details of how it works here. In short, it updates the current position of the trackball and then processes the new position in exactly the same way as when you use the mouse to move the trackball. This usually results in a new power setting for the drive motors, and UpdateGameControllerAxes posts an OnMove message back to the _eventsPort. The OnMove message is discussed later, but basically it causes the main service to send a SetDrivePower message to the differential drive.

If you only want to read data, you can also extract information from the Form by grabbing the values of public properties in the Form. (Technically, you can also write to public variables, but then you might have concurrency issues). Visual Studio creates all controls on a Form as private properties, so you cannot access the controls directly. However, you can declare your own public properties, and many properties at the Form level are public.

The following code snippet is from the handler that saves the state to a config file:

// Grab the current window location if (_driveControl != null)

{

_state.Options.WindowStartX = _driveControl.Location.X; _state.Options.WindowStartY = _driveControl.Location.Y;

}

The Location.X and Location.Y properties are the screen coordinates of the top-left corner of the window. This enables the window to be started at the same position the next time the service is run. Note that gathering data this way is an on-demand approach, rather than an event-driven approach.

The basic process for communication from the Form back to the main service is via the Form’s event handlers, which generate messages that are posted to the _eventsPort. Some events in the Form might not result in a message being sent, but instead set internal variables inside the Form, so there is no one-to-one correspondence between events and messages sent to the _eventsPort.

Strictly speaking, the _eventsPort is not necessary. You could define the requests that the DriveControl Form requires as additional operations on the main operations port for the service.

176

www.it-ebooks.info

Chapter 4: Advanced Service Concepts

However, it makes sense to separate WinForm interactions from the operations that the service provides because you don’t want to expose these operations via the service Proxy. It also helps to make the WinForm handling code reusable.

Windows Forms often have dozens of controls. If you defined a message type for each possible event that could occur on the Form, it would quickly get out of hand. A better approach is to define a general class to handle WinForms interaction and then subclass this as necessary for message types that are specific to particular Forms. It is also advisable to try to group messages together to share message types.

Look in TeleOperationTypes.cs in the General Form Operations region at the bottom of the file:

#region General Form Operations

// This is the base class for all Form event messages public class FormEvent

{

private Form _theForm;

public Form Form

{

get { return _theForm; } set { _theForm = value; }

}

public FormEvent(Form form)

{

_theForm = form;

}

}

public class OnLoad : FormEvent

{

public OnLoad(Form form) : base(form)

{

}

}

public class OnClosed : FormEvent

{

public OnClosed(Form form) : base(form)

{

}

}

#endregion

The class called FormEvent is the base class for any event information that is passed back from a WinForm to the main service. This base class contains a property that identifies the WinForm by its handle. In the TeleOperation example, there are two WinForms.

Two other subclasses are also defined: OnLoad and OnClosed. These events are common to all WinForms so it is sensible to define them here. In the code for a WinForm, you add event handlers similar to the

177

www.it-ebooks.info

Part I: Robotics Developer Studio Fundamentals

following. Notice that these handlers simply post a message of the appropriate type to the _eventsPort for the WinForm:

private void DriveControl_Load(object sender, EventArgs e)

{

_eventsPort.Post(new OnLoad(this));

}

private void DriveControl_FormClosed(object sender, FormClosedEventArgs e)

{

_eventsPort.Post(new OnClosed(this));

}

Do not insert these routines by typing in the code because they won’t work unless you edit the Designer-generated code and manually hook them to the events. You should allow Visual Studio to create the empty methods for you because then they will be hooked up to the events properly. Once you have the empty routines, you can insert code inside them.

If you are not familiar with adding event handlers, follow these steps:

1.

2.

3.

Go to the Design View for the Form.

Click the background of the Form to select the entire Form.

In the Properties panel, shown in Figure 4-6, click the lightning bolt icon. This shows all the possible events that can be associated with the Form.

Figure 4-6

178

www.it-ebooks.info

Chapter 4: Advanced Service Concepts

4. If you double-click one of the event handler names in the Properties panel, an empty method is added to your WinForm code.

Figure 4-6 shows event handlers defined for FormClosed, KeyDown, KeyUp and Load. Note that KeyPress is not used. The key events are discussed in the next section.

For buttons and other controls on the Form, you don’t need to go to this much trouble because you can simply double-click the control in the Design View and the default event handler is added automatically. However, the keypress events cannot be added this way because there are no controls to click for keypress.

The OnLoad handler in the main service for OnLoad messages from the DriveControl Form is as follows (there is a different OnLoad handler for the WebCamForm):

///<summary>

///On Load Handler for completion of Form Load

///</summary>

///<param name=”onLoad”></param>

///<returns></returns>

IEnumerator<ITask> OnDriveControlLoadHandler(OnLoad onLoad)

{

// Save a handle to the form _driveControl = (DriveControl)onLoad.Form;

LogInfo(“Drive Control Form Loaded”);

// Subscribe to the joystick

yield return Arbiter.ExecuteToCompletion(Environment.TaskQueue, Arbiter.FromIteratorHandler(SubscribeToGameController));

}

This handler saves the Form handle at last. It is not necessary to do this here — it could have been saved when the Form was initially created, but it is done here as an example.

Then the handler tries to subscribe to a game controller. Other initialization can also be included at this point, such as updating labels or textbox controls on the Form with initial information. The main point to note is that this handler is not executed until the OnLoad message has been received, which is an indication that the Form has been created and is running.

The OnClosed event fires if the user clicks the Close button in the Form’s title bar. There is no need to provide an Exit button on the Form, but one has been included as a convenience. The Exit button handler simply closes the Form, which fires the OnClosed event anyway, and this causes an OnClosed message to be sent to the main service.

The OnClosed handler in the main service attempts to shut down by posting a Drop message to itself. (The Drop handler is not discussed here).

179

www.it-ebooks.info

Part I: Robotics Developer Studio Fundamentals

///<summary>

///Form Closed Handler for when Drive Control Form has closed

///</summary>

///<param name=”onClosed”></param>

void OnDriveControlClosedHandler(OnClosed onClosed)

{

if (onClosed.Form == _driveControl)

{

LogInfo(“Main Form Closed”);

// Send a Drop message to ourselves

_mainPort.Post(new DsspDefaultDrop(DropRequestType.Instance));

}

}

Notice here that the code checks the handle in the message against the handle in the Form to ensure that the message has been received from the correct Form. This isn’t actually necessary, but it illustrates the point that one handler can potentially handle multiple Forms by using the Form handle to identify

the Form.

Keyboard Handling

If you refer back to Figure 4-6 you can see that there are event handlers for the KeyDown and KeyUp events. These handlers are used to enable you to drive the robot using the arrow keys on the keyboard. However, there are a couple of tricks involved in setting this up.

The keyboard handlers are in a region called Keyboard Handlers toward the bottom of the Form code, DriveControl.cs. Note that the event handlers were first created in the Properties panel by doubleclicking the events, and then the code was added to each of the routines.

Start by looking at the KeyDown event handler:

#region Keyboard Handlers

//NOTE: The arrow keys will not normally appear in a KeyDown event.

//This is because they will be pre-processed. However, the operation

//under CF seems to be different and it does no harm to leave them

//in here anyway.

private void DriveControl_KeyDown(object sender, KeyEventArgs e)

{

switch ((Keys)e.KeyValue)

{

case Keys.Up: Forward(); e.Handled = true; break;

case Keys.Down: Backward(); e.Handled = true; break;

180

www.it-ebooks.info

Chapter 4: Advanced Service Concepts

case Keys.Left: TurnLeft(); e.Handled = true; break;

case Keys.Right: TurnRight(); e.Handled = true; break;

default:

break;

}

}

It turns out that this code is not responsible for the movements of the robot, even though it looks like it should be. It is called for keypresses in general, but not for the arrow keys. You could add other case statements to the switch to use alternative keys. For example, the A, S, D, and W keys are commonly used in first-person shooter games to move around.

Are you being misled? No. When you get to Chapter 16, you will find that Windows Mobile operates a little differently. In that case, the preceding code is called. The “arrow keys” on a PDA correspond to the “rocker switch” that is used to move the cursor around.

With desktop operating systems, you have to take a different approach to process the arrow keys. You first need to set the KeyPreview property on the Form to True. This indicates that you want to handle the “special” keys, rather than use the default handling. The arrow keys are normally used to move from one control to another, or to move side to side in a TextBox. By setting the KeyPreview flag, your code can take over.

Next, you must create a handler to override the default ProcessDialogKey method. Your handler can do whatever it likes with incoming keystrokes before they are passed on to the Form. If you wanted to be really nasty, you could throw away all “2” keystrokes and the poor users would think they had a broken key. Let’s not do that.

ProcessDialogKey has to process the keys that it is interested in and return true to indicate that the keystrokes have been used up. Any keys that it is not interested in can just be passed to the original ProcessDialogKey handler, where they are processed as usual. In other words, you are intercepting keystrokes before they arrive at the Form:

// Overriding ProcessDialogKey allows us to trap the arrow keys protected override bool ProcessDialogKey(Keys keyData)

{

switch (keyData)

{

case Keys.Up: Forward(); return true;

case Keys.Down:

(continued)

181

www.it-ebooks.info

Part I: Robotics Developer Studio Fundamentals

(continued)

Backward(); return true;

case Keys.Left: TurnLeft(); return true;

case Keys.Right: TurnRight(); return true;

default:

break;

}

return base.ProcessDialogKey(keyData);

}

Notice that the last step in the code is to pass the keyData back to the normal ProcessDialogKey handler and return whatever result it gives you. It is quite likely that the original ProcessDialogKey handler won’t do anything with the keystroke (because it is not a special character), in which case it returns false and the keyData is sent to the Form for processing.

The functions that are called in response to each of the arrow keys are trivial, so only the Forward function is shown here:

void Forward()

{

_eventsPort.Post(new OnMove(this, (int)options.MotionSpeed, (int)options.MotionSpeed));

}

It is worth noting here that MotionSpeed is an option setting. This is part of the service state, so it is saved in the config file. The Form doesn’t have direct access to the service state, but the state is passed to the Form when it is created.

OnMove messages are processed in the main service by the following handler. As you would expect, it issues a SetDrivePower request to the differential drive:

///<summary>

///Handle basic moves (motor power settings)

///</summary>

///<param name=”onMove”></param>

///<returns></returns>

IEnumerator<ITask> OnMoveHandler(OnMove onMove)

{

if (_drivePort != null)

{

//Create a drive request

//There is a more concise syntax, but we need to access some

//additional properties

182