OWLNext Home

~ Step 11: Moving to MDI ~


Home
About OWLNext
Getting started
Prerequisites
Patching
Building
Configuring
Tutorials
Documentation
Downloads
Articles
Links
Discussions

Hosted by
Get OWL Next at SourceForge.net. Fast, secure and Free Open Source software downloads

This chapter describes how to convert the application created in Step 10 to use the Multiple Document Interface, or MDI for short. The application in Step 10 is what is known as a Single Document Interface, or SDI, application. That means the application can support and display only a single document at a time.

In the sense that it's used here, document doesn't have the same meaning you might be used to. Instead of a paper document or a word-processing document, a document refers to any set of data that your application displays and manipulates. In the case of the tutorial application, documents are the drawing files that the application creates. Converting the application to use MDI adds the ability to support multiple drawings open at the same time in multiple child windows.

Understanding the MDI model

An MDI application functions a little differently from an SDI application. In Step 10, the Drawing Pad application displayed a single drawing in a window. The window that actually displayed the drawing was a client of the frame window. The frame window managed general application tasks, such as menu handling, resizing, painting menus and control bars, and so on. The client window managed tasks specific to the application, such as handling mouse movements and button clicks in the client area, painting the lines in the drawing, responding to application-specific events, and so on.

In comparison, MDI applications divide tasks up three ways instead of two:

  • The frame window functions much as it does in the SDI application, handling basic application functionality.
  • The client window handles tasks related to creating, managing, and closing MDI child windows, along with any related functions. For example, the client window might manage the File|Open command since, in order to open an MDI child window, you usually need something to display in it.
  • MDI child windows display the data in an MDI application and give the user the ability to manipulate and control the data. These windows handle application-specific tasks, much like the client window did Step 10.

In this step, you'll take the example from Step 10 and restructure it to support MDI functionality. It's not as complicated as it may seem; most of the new classes you'll construct can be taken straight from the existing TDrawWindow class!

Adding the MDI header files

There are a number of new header files you need to include to add MDI capability to your application. This section describes the header files that need to be changed or added. It also describes the classes that are defined in each header file.

Changing the resource script file

You need to change the include statement for the STEP10.RC resource script file to include the STEP11.RC resource script file. There are only two changes you need to make to STEP11.RC:

  1. Include the resource header file owl\mdi.rh.
  2. Add a pop-up menu called Window between the Tools menu and the Help menu. This menu should have four items, described in Table 11.1.

The functions that handle these events are described later on.

MDI Window menu items and identifiers
 
Menu item text Command identifier
Cascade CM_CASCADECHILDREN
Tile CM_TILECHILDREN
Arrange Icons CM_ARRANGEICONS
Close All CM_CLOSECHILDREN

Replacing the frame window header file

In the place of owl\decframe.h, you need to include owl\decmdifr.h. This header file contains the definition of the TDecoratedMDIFrame class, which is derived from TMDIFrame and TDecoratedFrame. TMDIFrame, defined in the owl\mdi.h header file, adds the support for containing an MDI client window to the support already provided by TFrameWindow for command processing and keyboard navigation. MDI client windows are discussed below. As shown in the previous step of the tutorial, TDecoratedFrame provides the ability to support decorations such as control bars and status bars. Since the tutorial application already supports decorations from the previous step, you can use the decorated version of the MDI frame window to keep this functionality.

Adding the MDI client and child header files

You need to add the owl\mdi.h and owl\mdichild.h header files. owl\mdi.h contains the definition of the TMDIFrame and TMDIClient classes. TMDIClient provides the functionality necessary for managing MDI child windows. MDI child windows are the windows that the user of your application actually works with and that display the data contained in each document. TMDIClient provides the ability to

  • Close all of the open MDI child windows
  • Find the active MDI child window
  • Initialize a new MDI child object
  • Create a new MDI child window
  • Arrange and manage MDI child windows, including arranging icons for minimized child windows and cascading or tiling open child windows

owl\mdichild.h contains the definition of the TMDIChild class, which is derived from TWindow. TMDIChild overrides a number of TWindow's function to provide the ability to function as an MDI child.

You usually derive new classes from both TMDIClient and TMDIChild to provide the specific functionality required by your application. Creating new classes from TMDIClient and TMDIChild to support the Drawing Pad application is discussed later in this step.

Changing the frame window

The first step in moving the drawing application to MDI is to change the frame window. MDI applications use specialized MDI frame windows. As discussed earlier, ObjectWindows provides two MDI frame window classes, TMDIFrame and TDecoratedMDIFrame. Because we're using the TDecoratedMDIFrame class for the frame window, discussion of the TMDIFrame class is left for "Window objects" of the ObjectWindows Programmer's Guide.

Here's the constructor for TDecoratedMDIFrame:


TDecoratedMDIFrame(const char far* title,
                   TResId menuResId,
                   TMDIClient& clientWnd = *new TMDIClient,
                   bool trackMenuSelection = false,
                   TModule* module = 0);
	

where:

  • title is the caption for the frame window.
  • menuResId is the resource identifier for the frame window's main menu.
  • clientWnd is the MDI client window for the frame window.
  • trackMenuSelection indicates whether this frame should track menu selections. This is the same thing as menu tracking for the TDecoratedFrame you constructed in the last step.
  • module is a pointer to an program module. module is used to initialize the TWindow base object.

Besides adding the owl\decmdifr.h header file, two other changes are required to use a TDecoratedMDIFrame in the tutorial application. The first is changing the line in the TDrawApp::InitMainWindow function where the frame window is created:


TDecoratedMDIFrame *frame = new TDecoratedMDIFrame("Drawing Pad",
                 TResId("COMMANDS"), *new TDrawMDIClient, true);
     

As before, the frame window caption is Drawing Pad. The frame window is initialized with the COMMANDS menu resource. The client window is a new TDrawMDIClient, which is a TMDIClient-derived class that you'll define a little bit later in this step. The final parameter indicates that menu tracking should be on for this window. The module parameter is left to its default value of 0.

The second change is removing the AssignMenu call at the end of the InitMainWindow function of Step 10. This call is no longer necessary because the menu resource is set up by the second parameter of the TDecoratedMDIFrame constructor.

Your InitMainWindow function should now look something like this:


void
TDrawApp::InitMainWindow()
{
  // Create a decorated MDI frame
  TDecoratedMDIFrame *frame = new TDecoratedMDIFrame("Drawing Pad",
                     TResId("COMMANDS"), *new TDrawMDIClient, true);

  // Construct a status bar
  TStatusBar* sb = new TStatusBar(frame, TGadget::Recessed);

  // Construct a control bar
  TControlBar *cb = new TControlBar(frame);
  cb->EnableFlatStyle();  // Enable the new flat look of toolbar buttons
  cb->Insert(*new TButtonGadget(CM_FILENEW, CM_FILENEW,
                                      TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILEOPEN, CM_FILEOPEN,
                                      TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILESAVE, CM_FILESAVE,
                                      TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_FILESAVEAS, CM_FILESAVEAS,
                                      TButtonGadget::Command));
  cb->Insert(*new TSeparatorGadget);
  cb->Insert(*new TButtonGadget(CM_PENSIZE, CM_PENSIZE,
                                      TButtonGadget::Command));
  cb->Insert(*new TButtonGadget(CM_PENCOLOR, CM_PENCOLOR,
                                      TButtonGadget::Command));
  cb->Insert(*new TSeparatorGadget);
  cb->Insert(*new TButtonGadget(CM_ABOUT, CM_ABOUT,
                                      TButtonGadget::Command));
  // Insert the status bar and control bar into the frame
  frame->Insert(*sb, TDecoratedFrame::Bottom);
  frame->Insert(*cb, TDecoratedFrame::Top);

  // Set the main window and its menu
  SetMainWindow(frame);
}
      

These are the only changes necessary to the TDrawApp class to support MDI functionality.

Creating the MDI window classes

The functionality contained in the TDrawWindow class in the previous step needs to be divided up into two classes in the MDI model. The reason for this is that there are two windows that handle messages and user input:

  • MDI client window are created during the construction of the MDI frame class. This window is open as long as the frame window is still open (in this case, for the life of the application). This window handles the CM_FILEOPEN, CM_FILENEW, and CM_ABOUT commands. When the application is first started up, or when there are no drawings open, the only commands that make sense are opening drawing files, creating new drawings, and opening the About... dialog box. Other commands available in the tutorial application, such as saving drawings, changing the pen size or color, and so on, apply to a particular drawing, which must already be open and displayed in a child window.
  • MDI child windows are created by the MDI client window in response to CM_FILENEW or CM_FILEOPEN commands handled by the client window. In the tutorial application, MDI child windows handle the events handled by TDrawWindow in Step 10 that aren't handled by TDrawMDIClient:
    • WM_LBUTTONDOW
    • WM_RBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • CM_FILESAVE
    • CM_FILESAVEAS
    • CM_PENSIZE
    • CM_PENCOLOR

    Note that each of these commands pertains to a specific drawing or window; that is, each event only makes sense in the context of an open drawing contained in a child window. For example, in order for the user of the application to save a drawing, there must already be a drawing open. Contrast this to the events handled by the MDI client window, which either open a new child window containing a new or existing drawing or are independent of a drawing altogether.

The next sections discuss how to create the MDI client and child window classes for the tutorial application.

Creating the MDI child window class

You need to create a class declaration for the TDrawMDIChild class, along with defining the functions for the class. You can reuse most of the class declaration for TDrawWindow from Step 10, along with most of the functions with only a few changes.

Declaring the TDrawMDIChild class

The class declaration for TDrawMDIChild is very similar to the declaration of the TDrawWindow class from Step10. Here are the changes you need to make:

  1. Change all occurrences of TDrawWindow to TDrawMDIChild. This includes the name of the destructor, which otherwise doesn't change.
  2. Remove the CmFileNew, CmFileOpen, and CmAbout functions from the class declaration.
  3. The constructor for TMDIChild requires a TMDIClient reference in place of TDrawWindow's TWindow *. This parameter indicates the parent of the MDI child window. In this case, you want to add a TDrawMDIClient reference to the constructor and pass this to the TMDIChild constructor. In addition, you should add a const char* for the MDI child window's caption.
  4. In the response table, remove the entries for handling the CM_FILENEW, CM_FILEOPEN, and CM_ABOUT events.

Your class declaration should look something like this:


class TDrawMDIChild : public TMDIChild {
  public:
    TDrawMDIChild(TDrawMDIClient& parent, const char* title = 0);
   ~TDrawMDIChild() { delete DragDC; delete Line; delete Lines; delete FileData; }

  protected:
    TDC *DragDC;
    TPen *Pen;
    TLines *Lines;
    TLine *Line; // To hold a single line at a time that later gets
                 // stuck in Lines
    TOpenSaveDialog::TData
              *FileData;
    bool IsDirty, IsNewFile;

    void GetPenSize(); // GetPenSize always calls Line->SetPen().
    // Override member function of TWindow
    bool CanClose();

    // Message response functions
    void EvLButtonDown(uint, TPoint&);
    void EvRButtonDown(uint, TPoint&);
    void EvMouseMove(uint, TPoint&);
    void EvLButtonUp(uint, TPoint&);
    void Paint(TDC&, bool, TRect&);
    void CmFileSave();
    void CmFileSaveAs();
    void CmPenSize();
    void CmPenColor();
    void SaveFile();
    void OpenFile();

    DECLARE_RESPONSE_TABLE(TDrawMDIChild);
};

DEFINE_RESPONSE_TABLE1(TDrawMDIChild, TWindow)
  EV_WM_LBUTTONDOWN,
  EV_WM_RBUTTONDOWN,
  EV_WM_MOUSEMOVE,
  EV_WM_LBUTTONUP,
  EV_COMMAND(CM_FILESAVE, CmFileSave),
  EV_COMMAND(CM_FILESAVEAS, CmFileSaveAs),
  EV_COMMAND(CM_PENSIZE, CmPenSize),
  EV_COMMAND(CM_PENCOLOR, CmPenColor),
END_RESPONSE_TABLE;
 	

Creating the TDrawMDIChild functions

Just about all of the functions in TDrawMDIChild can be carried over from the TDrawWindow class. The only thing you need to do is change the class identifier in the function declarations from TDrawWindow to TDrawMDIChild. For example, the declaration for the EvLButtonDown function changes from this:


void
TDrawWindow::EvLButtonDown(uint, TPoint& point)
{
  
}
	

to this:


void
TDrawMDIChild::EvLButtonDown(uint, TPoint& point)
{
  
}
	

Change the class identifiers for the following functions:

GetPenSize, EvLButtonDown, EvMouseMove, Paint, CmFileSaveAs, CmPenColor, OpenFile CanClose, EvRButtonDown, EvLButtonUp, CmFileSave, CmPenSize, SaveFile

There is one minor change you need to make to the CmFileSaveAs function. Because the name of the drawing usually changes when the user calls the File|Save As command, you need to set the caption of the window to the file name. To do this, use the SetCaption function. This function takes a char*, which in this case should be the FileName member of the FileData object. The CmFileSaveAs function should now look like this:


void
TDrawMDIChild::CmFileSaveAs()
{
  if (IsNewFile)
    strcpy(FileData->FileName, "");
  if ((TFileSaveDialog(this, *FileData)).Execute() == IDOK)
    SaveFile();
  SetCaption(FileData->FileName);
}
	

Creating the TDrawMDIChild constructor

The main difference between TDrawMDIChild and the TDrawWindow class, other than the fact that TDrawMDIChild has three fewer functions than TDrawWindow, is in the constructor.

Initializing data members

Like TDrawWindow, TDrawMDIChild contains the device context object that displays the drawing and manages the arrays that contain the line drawing information. It also contains the IsDirty flag, setting it to false when the drawing is first created or opened and setting it to true when the drawing is modified. So the variables that contain the data for these functions-DragDC, Line, Lines, and IsDirty-need to be initialized in the TDrawMDIChild constructor. This looks just the same as their initialization in the TDrawWindow class.


  DragDC = 0;
  Lines = new TLines(5, 0, 5);
  Line = new TLine(TColor::Black, 1);
  IsDirty = false;
	

There are some notable changes from TDrawWindow's constructor here, however. First, the Init function is no longer called. TMDIChild does not provide an Init function. Instead, you should just call the base class constructor in the TDrawMDIChild initialization list, like so:


TDrawMDIChild::TDrawMDIChild(TDrawMDIClient& parent, const char* title)
  : TMDIChild(parent, title)
{
  
}
 	
Initializing file information data members

You can no longer simply initialize the IsNewFile variable to true, assuming that you are creating a new drawing whenever you create a window. In earlier steps this was a valid assumption: when the window was created, it hadn't opened a file yet, but was available to be drawn in. The IsNewFile flag was only set to false once a drawing had either been saved to a file or an existing drawing had been opened from a file into a window that had already been created.

In this case, the MDI client parent window will handle the file creation and opening operations. It then creates a child window to contain the new or existing drawing. The child window has to find out from the parent whether this is a new drawing or an existing drawing opened from a file.

For the same reason, the MDI child window does not necessarily create the TOpenSaveDialog::TData referenced by the FileData member. The TDrawMDIClient class has a function (or will have, when you get around to creating it) called GetFileData. This function takes no parameters and returns a pointer to a TOpenSaveDialog::TData object. If the MDI client window is creating the child window in response to a CM_FILEOPEN event, it creates a new TOpenSaveDialog::TData object containing the information about the file to be opened. GetFileData returns a pointer to that object. But if the client window is creating the child window in response to a CM_FILENEW event, TDrawMDIClient doesn't create a TOpenSaveDialog::TData object and GetFileData returns 0.

So the MDI child can find out whether this is a new drawing or not by testing the return value of GetFileData. If GetFileData returns a valid object, then it should assign the pointer to this object to its FileData member and set IsNewFile to false. It can then call the OpenFile function to load the drawing just as it did before. If GetFileData doesn't return a valid object (that is, it returns 0), the MDI child should set IsNewFile to true and create a new TOpenSaveDialog::TData object. The file name in the new object is set in the CmFileSaveAs function, just as it was in previous steps.

The constructor for TDrawMDIChild should look something like this:


TDrawMDIChild::TDrawMDIChild(TDrawMDIClient& parent, const char* title)
  : TMDIChild(parent, title)
{
  DragDC = 0;
  Lines = new TLines(5, 0, 5);
  Line = new TLine(TColor::Black, 1);
  IsDirty = false;

  // If the parent returns a valid FileData member, this is an open operation
  // Copy the parent's FileData member, since that'll go away
  if(FileData = parent.GetFileData()) {
    // Not a new file
    IsNewFile = false;
    OpenFile();
  }
  // But if the parent returns 0, this is a new operation
  else {
    // This is a new file
    IsNewFile = true;
    // Create a new FileData member
    FileData = new TOpenSaveDialog::TData(OFN_HIDEREADONLY | 
          OFN_FILEMUSTEXIST, "Point Files (*.PTS)|*.pts|", 0, "", "PTS");
  }
}
	

Note that, in the case of an open operation, the child assigns the pointer returned by GetFileData to its FileData member. Once this is done, the child takes over responsibility for the TOpenSaveDialog::TData object, including responsibility for cleaning it up. Since this is already done in the destructor, you don't have to do anything else.

Creating the MDI client window class

The TDrawMDIClient class manages the multiple child windows open on its client area and all the attendant functionality, such as creating new children, closing windows either singly or all at one time, tiling or cascading the windows, and arranging the icons of minimized children. TDrawMDIClient inherits a great deal of this functionality from the TMDIClient class.

TMDIClient functionality

It is important to understand the TMDIClient class, for the main reason that it is going to do a lot of work for you. TMDIClient is virtually derived from the TWindow class. TMDIClient overrides two of TWindow's virtual functions, PreProcessMsg and Create, to provide specific keyboard and menu handling functionality required by the client window. TMDIClient also handles a number of events, which are described in Table 11.2.

Events handled by TMDIClient
 
Event Response function Purpose
CM_CREATECHILD CmCreateChild Creates a new MDI child window
CM_TILECHILDREN CmTileChildren Tiles all non-minimized MDI child windows vertically
CM_TILECHILDRENHORIZ CmTileChildrenHoriz Tiles all non-minimized MDI child windows horizontally
CM_CASCADECHILDREN CmCascadeChildren Cascades all non-minimized MDI child windows
CM_ARRANGEICONS CmArrangeIcons Arranges the icons of all minimized MDI child windows
CM_CLOSECHILDREN CmCloseChildren Closes all open MDI child windows

The Drawing Pad application actually only provides menu items for four of these-CM_TILECHILDREN, CM_CASCADECHILDREN, CM_ARRANGEICONS, and CM_CLOSECHILDREN.

These response functions are simply wrappers for other TMDIClient functions that actually perform the work necessary. Each response function calls a function with the same name without the Cm prefix, so that CmCreateChild calls the CreateChild function. The only exception is CmTileChildrenHoriz, which calls the TileChildren function with the MDITILE_HORIZONTAL parameter.

Another function provided by TMDIClient is the GetActiveMDIChild function, which returns a pointer to the active MDI child window. Note that there can only be one active MDI child window at any time, but there is always one active MDI child window, even if all the MDI child windows are minimized.

There is one other function to discuss, InitChild. This is the only function in TMDIClient that you need to override in TDrawMDIClient. InitChild and overriding it to work with TDrawMDIClient are discussed on this page.

Data members in TDrawMDIClient

TDrawMDIClient requires a couple of new data members. These should both be declared private.

The first is NewChildNum. The only function of this variable is to keep track of the number of new drawing created by the CmFileNew function. This number is used for the window caption of all new drawings. It is initialized to 0 in the TDrawMDIClient constructor.

The second is FileData, a pointer to a TOpenSaveDialog::TData object, just like the FileData member of TDrawMDIChild. FileData is used to hold the file information when a user opens an existing file. It is set to 0 in the constructor. FileData is also set to 0 once the MDI child window has been opened. As shown on this page, the object returned by GetFileData is assigned to the FileData member of TDrawMDIChild. The object returned by GetFileData is actually the object (or lack thereof in the case of a new file) pointed to by TDrawMDIClient`s FileData member.

Adding response functions

In addition to the events handled by TMDIClient, TDrawMDIClient also handles the events formerly handled by TDrawWindow and not handled by TDrawMDIChild-CM_FILENEW, CM_FILEOPEN, and CM_ABOUT. The CmAbout response function is mostly unchanged from the TDrawWindow version, other than changing the class specifier. On the other hand, the CmFileNew and CmFileOpen functions must be substantially changed.

CmFileNew

The CmFileNew function is actually simplified from its TDrawWindow version. It no longer has to deal with flushing the line arrays, invalidating the window, and setting flags. Instead it sets FileData to 0 so that the MDI child object can tell that it is displaying a new drawing, increments NewChildNum, then calls CreateChild. CreateChild is the function that actually creates and displays the new MDI child window. It is discussed in more detail in the discussion of the InitChild function.

The CmFileNew function should now look something like this:


void
TDrawMDIClient::CmFileNew()
{
  FileData = 0;
  NewChildNum++;
  CreateChild();
}
      
CmFileOpen

There are a number of differences between the TDrawWindow version of CmFileOpen and the TDrawMDIClient version.

  • The TDrawMDIClient version no longer needs to call the CanClose function, because no windows need to be closed to open a new window.
  • The TDrawMDIClient needs to create a new TOpenSaveDialog::TData object to use with the TFileOpenDialog object.
  • If the call to TFileOpenDialog.Execute returns ID_OK, the TDrawMDIClient version calls CreateChild instead of OpenFile.
  • Once the CreateChild call returns, you need to set FileData to 0. Although it may seem like you should delete the FileData object before discarding the pointer to it, the object is actually taken over by the MDI child object, which deletes the object when the MDI child is destroyed.

Your CmFileOpen function should look something like this:


void
TDrawMDIClient::CmFileOpen()
{
  // Create FileData.
  FileData = new TOpenSaveDialog::TData(OFN_HIDEREADONLY | 
             OFN_FILEMUSTEXIST, "Point Files (*.PTS)|*.pts|", 0, "", "PTS");
  // As long as the file open operation goes OK...
  if ((TFileOpenDialog(this, *FileData)).Execute() == IDOK)
    // Create the child window.
    CreateChild();
  // FileData is no longer needed.
  FileData = 0;
}
       

GetFileData

The only new function required for TDrawMDIClient is GetFileData. This function is called by TDrawMDIChild in its constructor. This function should take no parameters and return a pointer to a TOpenSaveDialog::TData object. Its function is to return a pointer to the object pointed to by TDrawMDIClient's FileData member. If FileData references a valid object (that is, during a file open operation), GetFileData should return FileData. If FileData doesn't reference a valid object (that is, during a file new operation), GetFileData should return 0.

The actual function definition is very simple and can be inlined by defining the function inside the class declaration. Your GetFileData function should look something like this:


TOpenSaveDialog::TData *GetFileData() { return FileData ? FileData : 0; }
      

Overriding InitChild

The only TMDIClient function that TDrawMDIChild overrides is the InitChild function. InitChild takes no parameters and returns a pointer to a TMDIChild object. The CreateChild function calls InitChild before creating a new MDI child window. It is in InitChild that you create the TMDIChild or TMDIChild-derived object for the MDI child window. This is the only function of TMDIClient that you'll override when you create the TDrawMDIClient class.

The InitChild function for TDrawMDIClient is fairly straightforward. If FileData is 0, you should create a character array to contain a default window title. This can be initialized using the value of NewChildNum so that each new drawing has a different title.

Then you should create a TMDIChild* and create a new TDrawMDIChild object. The constructor for TDrawMDIChild takes two parameters, a reference to a TDrawMDIClient object for its parent window and a const char* containing the MDI child window's caption. In this case, the first parameter should be the dereferenced this pointer. The second parameter should be either the FileName member of the FileData object if FileData references a valid object or the character array you created earlier if not.

Once the MDI child object has been created, you need to call the SetIcon function for the object. SetIcon associates an icon resource with the function's object. This icon is displayed in the client area when the child window is minimized. You can set the icon to the icon provided for the tutorial application called IDI_TUTORIAL.

The last step of the function is to return the TMDIChild pointer. Your InitChild function should look something like this:


TMDIChild*
TDrawMDIClient::InitChild()
{
  char title[15];
  if(!FileData)
    wsprintf(title, "New drawing %d", NewChildNum);
  TMDIChild* child = new TDrawMDIChild(*this, FileData ?
                   FileData->FileName : title);
  child->SetIcon(GetApplication(), TResId("IDI_TUTORIAL"));
  return child;
}
       

Where to find more information

MDI frame, client, and child windows are described in "Window objects" in the ObjectWindows Programmer's Guide.

Prev Up Next



Last updated: NA