Building an Editor - Part 2

From EDM2
Jump to: navigation, search

by Marc Mittelmeijer and Eric Slaats

Part 1

Part 2

Part 3

Introduction

[Editor's note - this is the second part of the series that began with "Building Custom Controls" in issue 0304 of EDM/2]

In our last article ("Building Custom Controls") we created a small PM editor by using the MLE control. The EDITOR1 program had one bug: it didn't respond properly to the ALT key. We've fixed that problem and the examples in this article use the modified code. (Thanks for the E-mail that alerted us to this problem.) The code which fixes this problem is also published in the May version of EDM/2. We've also received E-mail with proposals to improve the status-bar. Both proposals were elegant. Mark Mathews suggested to use the minimize/maximize buttons to calculate the height of the statusbar so it wouldn't change when changing the menubar's height by making the window very small.

Roland Ott takes this approach one step further. He proposes to calculate the height of the font used in the statusbar. This way you can use any font size you like in the statusbar. This last version is implemented in the [editor2.zip EDITOR2 files]. Thanks to both of them for their feedback.

The editor presented in the last article did have one fatal design flaw. It can't read or write files. So as an editor, it can't function real well. In this article we'll try to cover this area so that EDITOR1 becomes EDITOR2. EDITOR2 could be a small system editor. The article will be tough reading because the handling of files takes a lot of flags in OS/2. Unfortunately we had to describe a rather (boring) lot of them.

I know, we've promised to do the font changing. However it would make this article very large. Maybe in a next edition we'll wrap things up.

Filename Needed

When loading or saving a file one thing is absolutely necessary: a filename. Preferably, this filename should be a full path! (If no full path is supplied, the current directory is assumed). Before we plunge into the Dos API lets first take a look how to obtain a filename.

We'll handle three ways of obtaining a filename.

  1. Through OS/2's standard file dialog
  2. By dropping a file object icon on the EDITOR2 icon
  3. By dropping a file object on the MLE client area.

We also need a filename to save a file. This is a small problem, so we'll address it when it arises.

Using the Standard File Dialog Box.

OS/2 provides us with two standard dialogs which were introduced in OS/2 2.0. These dialogs handle common tasks almost every application has to perform, the changing of fonts and the handling of files. In the old days, almost every developer built his own dialogs to handle these issues. (Petzold's solution was popular enough that he even repeated it in his last book concerning OS/2 2.x.)

In this section we will discuss how to use the standard File Dialog Box.

The programmer can specify that the file dialog be either a "Save As" or a "Open" dialog; however, the dialog can be modified further in numerous different ways.

So how do we initialize this thing? There is a rather large structure called FILEDLG which contains all the parameters which can be (must be) set before invoking the dialog. This structure is shown below:

struct FILEDLG
	  {
	  ULONG   cbSize;                   // Structure size
	  ULONG   fl;                       // FDS_* flags
	  ULONG   ulUser;                   // Used by the application
	  LONG    lReturn;                  // Result code
	  LONG    lSRC;                     // System return code
	  PSZ     pszTitle;                 // Dialog title string
	  PSZ     pszOKButton;              // OK push button text
	  PFNWP   pfnDlgProc;               // Custom dialog procedure
	  PSZ     pszIType;                 // Extended-attribute type filter
	  PAPSZ   papszITypeList;           // Pointer
	  PSZ     pszIDrive;                // The initial drive
	  PAPSZ   papszIDriveList;          // list of drives (NULL=all)
	  HMODULE hMod;                     // Module for custom dialog resources
	  CHAR    szFullFile[CCHMAXPATH];   // Character array
	  PAPSZ   papszFQFilename;          // Qualified filenames (NULL=all)
	  ULONG   ulFQFCount;               // Number of file names
	  USHORT  usDlgId;                  // Custom dialog ID
	  SHORT   x;                        // X-axis dialog position
	  SHORT   y;                        // Y-axis dialog position
	  SHORT   sEAType;                  // Selected extended-attribute type
	  } FILEDLG;

Some of the fields in this structure must be filled in; otherwise the dialog will not display. Let's take a look at these fields first.

cbSize - this field contains the size of the FILEDLG structure. Usually we fill this using the sizeof() function. (It's common in OS/2 to include the size as the first field when a structure is passed as a parameter to a function!)

fl - the flags field must also be filled. The fl field can contain the following flags:

FDS_CENTER -
the dialog is centered within its owner. Normally the dialog box will show on the left bottom corner of the screen because the x and y arguments in FILEDLG will be 0.
FDS_MODELESS -
the dialog box will be modeless. (If you don't know what this means, try this flag and observe the behavior of the dialog.)
FDS_PRELOAD_VOL_INFO -
load the volume information as the file dialog log is loaded. This seems an attractive feature, but it takes a performance penalty when starting the dialog (especially if you've got a lot of logical drives). So preferably we turn this off. The label name of the current volume will show anyway.
FDS_CUSTOM -
create a customized dialog box. (This is a pain, so we won't do it)
FDS_FILTERUNION -
use EA's as well as filenames as a filter.
FDS_INCLUDE_EAS -
the EA information will be loaded in the dialog.
FDS_HELPBUTTON -
display the help button in the file dialog. (Note, the help will not be displayed by itself!)
FDS_APPLYBUTTON -
display an apply button on the dialog.
FDS_OPEN_DIALOG -
the dialog is an Open dialog. (Open text in dialog.)
FDS_SAVEAS_DIALOG -
the dialog is a Save As dialog (Save as text in dialog)
FDS_MULTIPLESEL -
more than one file can be selected from the files listbox.
FDS_ENABLEFILELB -
this flag is only valid if the dialog is a Save As dialog. It can be used to enable the filelistbox so a filename can be selected from this listbox. Note that disabled is the default.

With only those two fields filled and the rest of the FILEDLG arguments set to 0, the dialog will work just fine. Like the flags mentioned above, a number of arguments in the FILEDLG can be used to customize the dialog. Lets take a look at the ones we are likely to use.

pszTitle -
this field can be used to store a pointer to a string which contains the title of the dialog. If this filed is filled with NULL, the dialog will steal it's title from the owner window (can be the empty string!)
pszOKButton -
the string displayed on the OK button can be changed if a pointer to a string is passed in this field. If this field contains NULL, simply "OK" is used.

With the information supplied above we can set up a file dialog for our application. First we have to declare a FILEDLG variable and initiate it with all zero's. (Remember the most fields in FILEDLG can accept a NULL.) To achieve this we will use the memset function (This function is included in mem.h.) By (ab)using this function we can set a number of bytes to 0. It's simpler and faster than setting each field manually to NULL.

FILEDLG FileDlg;
memset(&FileDlg, 0, sizeof(FILEDLG));

The next step will be initializing the mandatory fields in the FILEDLG structure. So we have to fill the fl field and the cbSize field. The easiest way to fill the cbSize field is to use the sizeof() function. For the flags field we have to make a choice. Let's make it an "open" dialog. This way the only flag, then, we have to use is the FDS_OPEN_DIALOG flag.

FileDlg.cbSize = sizeof(FILEDLG);
FileDlg.fl	   = FDS_OPEN_DIALOG;

The FILEDLG structure is filled with the appropriate values, so the only thing left to do is start the dialog. OS/2 uses a special function to start the file dialog, the WinFileDlg() function. This function takes three arguments: the handle of an owner window, the handle of a parent window, and a pointer to the FILEDLG structure. It returns a handle to the dialog itself. (We won't need it.)

HWND		    hwndParent;	  // Parent-window handle
HWND		    hwndOwner;	  // Requested owner-window handle
FILEDLG	    FileDlg;	  // Pointer
WinFileDlg(hwndParent, hwndOwner &FileDlg);

We were trying to get a filename including a path. The szFullFile field of the FILEDLG contains the value of a filename including path if the user ends the dialog with the OK button. How do we know the user used the OK button? How convenient - the FILEDLG contains a lReturn field. This field contains the return code from the file dialog. The possible values are DID_OK if OK was pressed, DID_CANCEL if Cancel was pressed or 0 if an error occurred. So it's easy to check.

The following very small program starts a file dialog in "open" mode and returns the given filename in a message box if OK was pressed.


#define  INCL_WIN

#include <os2.h>
#include <mem.h>

void main(void)
   { HAB      hab;
     HMQ      hmq;
     FILEDLG  FileDlg;

     hab = WinInitialize(0);
     hmq = WinCreateMsgQueue(hab,0);
     memset(&FileDlg, 0, sizeof(FILEDLG));      // values FILEDLG set to 0
     FileDlg.cbSize = sizeof(FILEDLG);          // Set cbsize (Mandatory)
     FileDlg.fl = FDS_OPEN_DIALOG;              // Set fl (flags) (Mandatory)
     FileDlg.pszTitle  = "Test dialog use to experiment";
                                                // Dialog title
     WinFileDlg(HWND_DESKTOP, HWND_DESKTOP, &FileDlg);
                                                // Start dialog
     if (FileDlg.lReturn == DID_OK)             // User pressed OK ?
     WinMessageBox(HWND_DESKTOP, HWND_DESKTOP,
                FileDlg.szFullFile,
                "OK pressed",
                0, MB_OK);
     WinDestroyMsgQueue(hmq);
     WinTerminate(hab);
     }

One last note. If we use a file dialog as a "Save as" dialog, we usually expect the current filename to appear in the dialog. If we fill in the field szFullFile, the dialog is initiated using the path in szFullFile and displaying the filename. We will use this feature when we define the "Save as" dialog.

Note: the working code of this example can be found in FILEDLG.ZIP.

Dropping on an Icon

Drag and drop is one of the most attractive features PM has to offer. In our opinion any developer should go the extra mile to implement full support for this feature. (In this article we only discuss dragging and dropping file objects. Fonts, colors, etc. aren't discussed.)

The problem is that drag and drop in most cases isn't really easy. So we'll start with a very simple case of drag and drop. An object can be dropped on a application in two ways. It can be dropped on the opened application window or it can be dropped on the icon of the (closed) application. The latter is the simplest to implement.

What happens if we drop an object on an icon? PM will attempt to open the (executable) object attached to the icon on which we performed a drop. If we drop an object on the icon of an executable, PM starts this executable and uses the file information of the dropped object as parameters for the executable. In C++ this means the argv and argc parameters are filled. Argv and argc are standard arguments that are passed to main() for every C program (together with the env parameter). Argv contains an array of the parameters passed and argc contains the number of parameters passed.

The argv array is always filled in a standard manner.

argv[0] -
Contains the full path name of the program being run.
argv[1] -
Points to a string containing the first argument.
argv[argc-1] -
Points to the last argument being passed to main.

NOTE: At this point we stumble against a problem. Normally the various arguments used are separated by a space character. OS/2 is capable of handling filenames containing spaces. Some compilers (GCC/2) handle a filename with spaces as multiple arguments. The compiler we use (Borland C++) passes the filename as one argument. This poses an extra problem. The code supplied with this article expects the whole filename to be handles as one argument. If you use a compiler which splits filenames with spaces into more arguments you should concatenate these arguments. [This can be avoided altogether by surrounding arguments that contain spaces with double quotes. - Editor]

If we want to use these arguments in main(), the function should be modified somewhat.

main (int argc, char *argv[])

At what point in the program should these arguments be used? We want the contents of the file dropped on the icon to be used in the MLE. So we have to handle this after the MLE window is created. The best place to handle the argv and argc arguments is just before the WinDispatchMsg() loop is started. We simply call OpenFile() if argc is greater than 1. With argv[1], we have the filename and path we need to open the file object.

if (argc > 1)
	  <Fileopen action using argv[1]>

Simple, short code but very effective. A simple example which shows the effect of dropping on an icon (as well as dropping on the client area) is found in DROPON.ZIP.

Dropping a File Object on a Window

Another very attractive way to open a file is to drop the file icon on an opened window (the MLE area of the editor). The beauty of it is that it's really easy to implement this feature as an insert action for the MLE editor.

Lets take a look at the way drag and drop works. Drag and drop can be viewed from two different angles, from the source (the 'dragger') and from the target (the 'dropped on'). We're not doing an complete review of drag and drop so we'll only pick the view which will suit our goal: viewing from the 'dropped on' window. We'll narrow the view a little bit further by only discussing the messages involved in the drop which we need to open the file.

All of you out there who want more information on the whole protocol of messages involved in a drag-n-drop should read The Art of OS/2 2.1 C Programming, chapter 18. Another great source is the OS/2 Developer Connection CD where a number of drag-n-drop topics are described rather well under the Direct Manipulation Messages section.

When a drag action is being performed, PM generates a number of messages. We are interested in determining the object dragged over or dropped on. These correspond to the messages DM_DRAGOVER and DM_DROP.

Contained in the message parameter 1 of DM_DRAGOVER and DM_DROP is a pointer to a DRAGINFO structure which contains information about the drag. This information is needed to get information about the object(s) being dragged over or dropped on the window. Before looking into this structure let's find out how to obtain it.

Although the memory for DRAGINFO is allocated in shared memory, simply using the pointer passed in message parameter 1 isn't enough. We're not able to access the structure until the DrgAccessDraginfo() function is called. This function will need a pointer to a DRAGINFO structure declared in your program.

PDRAGINFO pDragInfo;
DrgAccesDragInfo(pDragInfo);

The DRAGINFO structure can be used for a number of actions. We want to check on the number of objects being dragged and to fetch information about the object being dragged. Let's take a look at the DRAGINFO structure.

typedef struct _DRAGINFO
	  {
	  ULONG	cbDraginfo;   // Structure size
	  USHORT	cbDragitem;   // DRAGITEM structures sizes
	  USHORT	usOperation;  // Modified drag operations
	  HWND	hwndSource;   // Window handle
	  SHORT	xDrop;	  // X-coordinate
	  SHORT	yDrop;	  // Y-coordinate
	  USHORT	cditem;	  // Count of DRAGITEM structures
	  USHORT	usReserved;   // Reserved
	  } DRAGINFO;

We want to check if the drag contains exactly one object (at least we want this for the MLE editor) and we want to check if the object being dragged is a "text" object. If it fits those conditions, we will also need the full filename. To check on the number of objects being dragged, just examine the cditem field. Obtaining information about the object being dragged is a little more complex though. We have to use another function - DrgQueryDragitemPtr() - to do this. This function returns a pointer to a structure containing information on the dragged object. This is the DRAGITEM structure. The DrgQueryDragitemPtr() function takes two arguments: the pointer to the DRAGINFO structure and an index number of the DRAGITEM requested. Since, in our case, we only accept one item to be dropped at a time, this index will always be 0. The following lines of code will query the object being dragged.

PDRAGINFO pDragInfo;	// Pointer to draginfo
PDRAGITEM pDragItem;	// Pointer to dragitem

DrgAccesDragInfo(pDragInfo);
DrgQueryDragitemPtr (pDragInfo, 0);

Now we know how to obtain information on a dragged item in the event of DM_DRAGOVER or DM_DROP, lets take a look at the information retrieved. The DRAGITEM structure looks like this:

typedef struct _DRAGITEM
	  {
	  HWND	hwndItem;		  // Window handle
	  ULONG	ulItemID;		  // Item ID
	  HSTR	hstrType;		  // String handle (TYPE)
	  HSTR	hstrRMF;		  // String handle(Rendering)
	  HSTR	hstrContainerName;  // String handle (name of source dir)
	  HSTR	hstrSourceName;	  // String handle (source name)
	  HSTR	hstrTargetName;	  // String handle (target name)
	  SHORT	cxOffset;		  // X-offset
	  SHORT	cyOffset;		  // Y-offset
	  USHORT	fsControl;		  // Source-object control flags
	  USHORT	fsSupportedOps;	  // Supported operations
	  } DRAGITEM;

When we retrieve this information, what fields are we interested in? We want a full filename of the dragged object and we'd like to check the type of the object being dragged. First let's take a look at the how to check the type. We could check the hstrType and the hstrRMF fields. But why do it the hard way when a simpler method is available?

The DrgVerifyType() function can be used to check the type. This function takes two arguments: the DRAGITEM structure and a string specifying the types to look for. The file type extended attribute of the dragged object specifies the type names. These type names take the form DRT_xxxxxxx. We are looking for plain text type objects which is represented by the DRT_TEXT constant. The DrgVerifyType() function will return TRUE if the type in the typestring is the same as the type in the EA of the dragged object.

DrgVerifyType(pDragItem,DRT_TEXT);

The second item we want to extract from the DRAGITEM structure is the full filename of the dragged object. Several fields in the DRAGITEM structure are of the type HSTR. This is a handle to a string. This handle is returned by the DrgAddStrHandle() function and is used by the DrgQueryStrName() function to obtain the attached strings. The strings themselves are stored by PM. We will use DrgQueryStrName() to retrieve the information needed.

Unfortunately, the full filename is stored in two different fields of the DRAGITEM structure. We need the string containing the container name (path) and the string containing the source name (filename). But, we need a character string for the full filename. Normally we would query the length of the container name and the source name before allocating memory to hold the filename. However there is an easier (dirty) way. The constant CCHMAXPATH contains the maximum length for a filename. We will use this constant in querying the DRAGITEM structure.

char dragname[CCHMAXPATH];
DrgQueryStrName(pDragItem->hstrContainerName, CCHMAXPATH, dragname);

DrgQueryStrName(pDragItem->hstrSourceName, CCHMAXPATH,
dragname+strlen(dragname));

After executing this code, dragname will contain the full filename of the dragged object.

Processing the Messages

Knowing how to extract the necessary information from a dragged object we can define the code used when processing DM_DRAGOVER and DM_DROP.

DM_DRAGOVER

This message is sent to the window under the pointer with the dragged object. PM sends this message each time the pointer moves over the target window. Why are we interested in the DM_DRAGOVER message?

This message is usually used to notify the drag operation if the object may or may not be dropped. If it may not be dropped, a little stop sign will be placed over the object. In our MLE editor case we can think of two reasons to forbid a drop.

  1. More than one object is being dragged (multiple drag)
  2. The object being dragged isn't a TEXT item.

In the previous section we described the method used to check these constraints. How should DM_DRAGOVER respond when the object will be accepted and how to respond if the object is rejected?

The return value of a DM_DRAGOVER consists of two USHORT values.

USHORT  usDrop		Drop indicator.
USHORT  usDefaultOp	Default operation.

In the case we're describing, the usDrop value is the most important. It can have the following values:

DOR_DROP
Dragged over object can be dropped. If we return this reply, the usDefaultOp must be indicate which operation must be performed if the user should drop the object.
DOR_NODROP
The object cannot be dropped. The target can eventually be accepted given different circumstances (other state of the target program, etc.).
DOR_NODROPOP
Object cannot be dropped. The type may be acceptable, but the specified drop operation isn't.
DOR_NEVERDROP
Object cannot be dropped. The object simply isn't acceptable. No more DM_DRAGOVER messages will be sent to the target until the pointer is moved out of and back into the target window.

The operations which can be indicated are simply the operations known by every PM user: Copy (DO_COPY), link/shadow (DO_LINK) and move (DO_MOVE). Besides these operations the DO_UNKNOWN is added for every other action.

We need the DOR_DROP/DOR_NEVERDROP in combination with the DO_UNKNOWN action. We use this to build the code to handle the DM_DRAGOVER message.

case DM_DRAGOVER:
	  {
	  pDragInfo = (PDRAGINFO) mpParm1;
	  DrgAccessDraginfo( pDragInfo );
	  pDragItem = DrgQueryDragitemPtr( pDragInfo, 0 );
	  //---------------------------------------------------------------
	  // Don't accept multi select
	  //---------------------------------------------------------------
	  if (pDragInfo->cditem > 1 || !DrgVerifyType(pDragItem, DRT_TEXT))
		    return ( MPFROM2SHORT (DOR_NEVERDROP, DO_UNKNOWN) );
	  else
		    return ( MPFROM2SHORT (DOR_DROP, DO_UNKNOWN) );
	  }

If the dragover message returns a DOS_NEVERDROP, the target window will never receive a DM_DROP message, so if a drop occurs, it won't be processed.

DM_DROP

It should be obvious. This message is sent after the object is really dropped on the window. This is the message we'll use to open the file object. The same DRAGINFO structure is used (contained in mpParm1). In the previous section the techniques needed to extract this information from DRAGINFO and DRAGITEM were described.

case DM_DROP:
  {
  char dragname[CCHMAXPATH];
  pDragInfo = (PDRAGINFO) mpParm1;
  DrgAccessDraginfo( pDragInfo);
  pDragItem = DrgQueryDragitemPtr(pDragInfo, 0);
  DrgQueryStrName(pDragItem->hstrContainerName, CCHMAXPATH, dragname);
  DrgQueryStrName(pDragItem->hstrSourceName,CCHMAXPATH,drgname+strlen(dragname));
  
  }

Normally processing DM_DROP is more complex. But since we don't want the source to send anything (we only need the name), rendering and other conversation with the source isn't necessary.

All the code snippets presented in dropping on an icon, dropping on a window and processing the messages is put together in a small program which demonstrates how drag and drop work. You get the filename if you drop an object on the icon (the window will open and provide the information through a message box). If an object is dropped on the open window it will also show the filename in a message box (only if it's a text item).

Note: this approach isn't complete, the file type isn't checked if the object is dropped in the icon!

The complete code and a working example can be found in the DROPON.ZIP file.

How to Open a File?

OS/2 has a few API calls to handle file I/O. The possibilities of these functions are numerous. In this article we will limit ourselves to opening a file and reading the entire content or writing a file. The files we'll handle will be strictly ASCII (as is to be expected when handling code for an editor). Emphasis will be on how the application handles a file read request from the user's point of view.

Before we're able to perform input or output operations on a file, the file has to be opened. OS/2 can open a file by using DosOpen(). This function will open the file (or create it if it doesn't exist) and return a file handle. A handle is a very common issue in OS/2 and it is simply a 32-bit number which is used to refer to an object. In this case we'll want a file handle to refer to a file object (and anything associated with it) and DosOpen() will return this handle. The DosOpen() function has the following syntax:

PSZ     pszFileName;
HFILE   hFile;
ULONG   ulAction;
ULONG   ulFileSize;
ULONG   ulFileAttribute;
ULONG   ulOpenFlag;
ULONG   ulOpenMode;
PEAOP2  pEABuf;
APIRET  rc;

rc = DosOpen(pszFileName,  // Full filename &. path
      &hFile,              // Pointer to filehandle
      &Action,             // Pointer to long, shows action taken
      ulFileSize,          // File size
      ulFileAttribute,     // File attributes (set on creation)
      ulOpenFlag,          // File open flags
      ulOpenMode,          // Mode of file open
      pEABuf);             // Buffer for extended attributes.

This looks rather complicated. Let's see if we can make some sense of the arguments this function takes.

pszFileName must contain the filename and path of the file we wish to open or create.

hFile receives the file handle OS/2 assigns to the opened file. If hFile contains 0 after DosOpen() returns, something went wrong! (Of course we can also check the return code). In our code we'll check on the value of hFile to see if we have a valid handle.

Action is a long which receives a value that indicates the action taken. Action can receive the following values:

FILE_EXISTED File already existed
FILE_CREATED File was created
FILE_TRUNCATED File existed and was replaced

The value of action can be checked after a successful DosOpen().

ulFileSize is a tricky parameter. The filesize has no meaning at all when opening a file. So when opening an existing file it is ignored and we can use 0. When creating a file or replacing an existing one, this value must contain the logical file size in bytes. We can't open a 0 sized file! When creating/changing a file this value is used as a recommended value. OS/2 will try to grab a disk area as contiguous as possible. So it pays to treat this value with some care.

Now we enter the area of the DosOpen() function where things get really messy. There are three (!) arguments which contain necessary flags and this makes for some rather interesting possibilities. We will propose a setting for these flags and discuss the other possibilities in global terms.

ulFileAttribute -
this field contains the normal file attributes as we know them from DOS. This field only has a meaning when we're creating a file. We can't use DosOpen() to modify these attributes. We will use the FILE_NORMAL setting. In this case, the file can be read or written to.
ulOpenFlag -
this flag indicates the action to be taken depending on whether the file exists or not. We will define two different settings, one for opening the file to read it, one for opening the file to write to it.
  • For opening to read the file we'll use the FILE_OPEN flag. This flag is actually a combination of the flags OPEN_ACTION_FAIL_IF_NEW and OPEN_ACTION_OPEN_IF_EXISTS. The action on these flags speaks for itself.
  • To write to a file, we can be faced with the situation that the file we want to write to doesn't exist (although a filename is supplied through filedlg).If this is the case, we want DosOpen to create the file. The flags used in this situation are: OPEN_ACTION_CREATE_IF_NEW and OPEN_ACTION_REPLACE_IF_EXISTS.
ulOpenMode -
the last flags field. Through this field we describe the way we open a file and what the rights of other processes are. We will focus on the two possible actions we want to take, reading from or writing to a file. The more sophisticated flags such as write-through cache won't be discussed.

For opening a file for reading, we don't want any other proces writing to this file. If this is the case, we will end up with corrupt contents! If DosOpen() can't open the file under these conditions it will fail! We accomplish this by defining the following flags: OPEN_FLAGS_FAIL_ON_ERROR and OPEN_SHARE_DENYWRITE. We only want to read from the file, so we could define read_only access, this is the default so we won't mention it explicitly.

When handling a write to a file, we must open it differently. We don't want any other proces to read or write during the writing. So the share mode is set to OPEN_SHARE_DENY_READWRITE.

Extended attributes are very interesting, but also very difficult to use. We won't address this problem here so the pEABuf can be skipped (use NULL).

It should be clear that DosOpen() should be handled differently if we want to read or write! Remember the flags discussed here are only a proposal. The whole description of the DosOpen() function is rather boring, but it pays to look into its resources. Feel free to experiment. It usually pays off.

Yes it's Open! And Now?

We got an open file (that is, we've got a valid file handle), and it has to be closed sometime. This action is the same for reading and writing, so lets discuss this first. Closing a file is a very simple function, the only parameter it takes is the file handle of the file we want to close.

DosClose (HFILE);

The only warning about this function is don't forget it!

Reading and writing to a file is also very simple. Both actions take a buffer as argument. This buffer is an allocated memory space which contains the data to be written to, or has space to accept data read from the file. Of course it takes the file handle of the file we're handling. The read and write functions have the following syntax.

HFILE   FileHandle;	// FileHandle
PVOID   pBuffer;		// Pointer to buffer (char in this case)
ULONG   ulBufLen;       // Buffer length
PULONG  pBytesHandled;	// Nr of bytes transfered
APIRET  rc;

rc = DosRead(FileHandle,
		 pBuffer,
		 ulBufLen,
		 pBytesHandled);

rc = DosWrite(FileHandle,
		  pBuffer,
		  ulBufLen,
		  pBytesHandled);

This isn't too difficult, is it? It should be easy to handle, but there's always something to destroy the fun of a simple solution. In this case it's the way the MLE handles the transfer of its content.

Specific Problems With the MLE Import/Export Function

The MLE control was originally designed to handle text about 4 Kb in size. We noticed it is capable of handling files several Megs in size. Most of the documentation states that if a file larger than 32Kb is loaded, the performance should be terrible. "The soup eaten isn't that hot," though (this is a Dutch saying). We found the performance acceptable even with files more than a megabyte in size.

Because the MLE was designed to handle files of about 4Kb, the functions supplied to save/read files are also limited. The limitation is set at the 64Kb boundary. So the code to handle the text in an MLE has to make some strange turns.

How can we place text in the MLE or extract it from the MLE? The MLE uses an import/export buffer to transfer data. Before we can import or export data from an MLE we have to set up an import/export buffer. This buffer can be set up by sending the MLE a MLM_SETIMPORTEXPORT message. The buffer can't be larger then 64 Kb. Here we encounter the first problem. We've found that there's a difference between the maximum buffer size depending on the compiler used. The Borland compiler can take a large buffer (EFFF), the GCC/2 compiler only takes 8FFF. (Other compilers aren't tested.) If we declare larger buffers, the import export won't function! So in all the code presented we will use 8FFF as buffer size.

So if we want to import or export more then 64Kb, we have to use an iteration in conjunction with DosRead() or DosWrite().

Let's take a look at the code we use to read a file. (It is assumed a file has already been opened and a file handle is available. This code has the same flow as the code used to write a file.)

//---------------------------------------------------------------------
// Read file in MLE
//---------------------------------------------------------------------
Buffer = new char[0x8FFF];                          // Set up memory for buffer
WinSendMsg(hwndMLE,                                 // Set up buffer for MLE
         MLM_SETIMPORTEXPORT,
         (MPARAM) Buffer,
         (MPARAM) 0x8FFF);

do   {
     DosRead(hfFile, Buffer, 0x8000, &BytesRead);   // Read buffer
     WinSendMsg(hwndMLE,                            // Buffer import in MLE
         MLM_IMPORT,
         (MPARAM) &ipt,
         MPFROMLONG(BytesRead));
     sprintf(String,"File: %s Bytes read:%d", pszFilename,ipt);
     WinSetWindowText( hwndStatus, String);         // Bytesread in statusbar
     }
while (BytesRead == 0x8000);

WinSetWindowText(hwndFrame, pszFilename);           // Filename in titlebar
DosClose(hfFile);                                   // Close file
delete Buffer;                                      // Free memory

The first thing that's done in this piece of code is setting up the import export buffer for the MLE. First the space is allocated. (The C++ statement new which is much more convenient than the OS/2 API calls in this situation.) Second we send the MLM_SETIMPORTEXPORT message to the MLE. In the do/while loop the file contents are read in chunks of 0x800 bytes. At the moment that the read chunk is smaller than 0x800, the last piece of the file is read.

As a bonus the number of bytes read are printed in the statusbar. (This has one bug. If the file is a retrieved file, it displays the last point a character was inserted.)

To wrap things up, the file is closed and the memory is freed. Note that the filename is written to the titlebar. Besides the fact that this is great as user feedback, it's also a nice place to store the filename used! In the next section we'll see that the titlebar is used as storage for the filename.

Putting it All Together

All the basics for save/load are described <sigh>. Now let's take a look at how all these pieces of information can be put together to let our editor load and save files. We won't handle everything in detail (the code supplied in [editor2.zip EDITOR2.ZIP] should be enough). However we will supply the program flow for some of the pieces.

Before discussing more code let's take a look at some constraints.

  • The titlebar has to diplay the filename of the last file loaded. (It's also the place this info is stored.) If no file was previously loaded, the titlebar should display UNTITLED.
  • The statusbar should show the number of bytes written or read after an I/O action.
  • The code supplied should have some more error checking. We will leave this to the reader's vivid imagination.
  • The menu items Open, Retrieve, Save and Save As are added. The items Save and Retrieve are also added to the buttonbar.

The following items have to be discussed:

  1. Reading a file into the MLE
  2. Writing a file from the MLE
  3. Where do we store the filename
  4. The file open dialog as supplier for a filename
  5. The save as dialog as supplier for a filename
  6. Drop on the icon as a filename supplier
  7. Drop on the MLE window as a filename supplier.
  8. Handling the new menu items
  1. Reading a file into the MLE
    Reading a file into the MLE is described in the previous section. Make sure the file is opened in the right way (OPEN_FLAGS_FAIL_ON_ERROR and OPEN_SHARE_DENYWRITE flags). The function which opens the file is called OpenFile and takes the name of the file to be opened as an argument.
  2. Writing a file from the MLE
    Writing to a file is mostly the same as reading from a file. The function to write a file is called SaveFile. It also takes the filename as argument. Note that DosOpen should create the file if no file with the filename supplied does not exist.
  3. Where do we store the filename
    The filename of the file currently being handled is saved in the titlebar of the framewindow. This is done with the function WinSetWindowText(hwndStatus, String); As the Save option is choosen from the menu or buttonbar, this is the place we receive the needed filename!
    Note: There should be a check if the file is named UNTITLED, which isn't implemented here.
  4. The file open dialog as supplier for a filename
    Using the fileopen dialog as a provider for a valid filename is easy. We just start the file dialog with the FDS_OPEN_DIALOG as described earlier. If this doesn't give us a valid filename, the FileOpen function simply refuses to open the nonexisting file! The OpenFileDlg() code can be called by the Open and Retrieve menu options. It takes one argument which specifies the parent of the dialog. Of course the FileOpen function is called from this function.
void OpenFileDlg(HWND hwndWnd)
     {
     FILEDLG     FileDlg;

     memset(&FileDlg, 0, sizeof(FILEDLG));// All values in OpenDlg set to 0
     FileDlg.cbSize = sizeof(FILEDLG);	// Set OpenDlg.cbSize (Mandatory)
     FileDlg.fl	  = FDS_OPEN_DIALOG;	// Set OpenDlg.fl (flags)
     WinFileDlg(HWND_DESKTOP, hwndWnd, &FileDlg);    // Start Openfiledialog
     if (FileDlg.lReturn == DID_OK)                  // User pressed OK ?
	    OpenFile(FileDlg.szFullFile);
     }
  1. The save as dialog as supplier for a filename
    Item 4 looked simple enough. Unfortunately handling the save as dialog isn't quit as simple. Why? because we first have to check if a filename is supplied that can be handled by DosWrite(). We can do this by querying the filename using the DosQueryPathInfo() function. The return of this function is a number which indicates the status of the filename checked.
    • File exists (ask to overwrite)
    • File does not exist, valid filename
    • Other: Error, probably invalid filename

Knowing this we can outline the program flow.

/* Initiate the dialog (use filename in titlebar) */
/* Start dialog */
/* Check supplied filename */
	  switch returncode
		    {
		    case 0: Replace Y/N
				if Y SaveFile(filename)

		    case 2:
				SaveFile(filename)

		    case 3:
				Error message
		    }

The complete translation of this piece of code to valid C++ statements is found in editor2.cpp contained in [editor2.zip Editor2.zip]

  1. Drop on the icon as a filename supplier
    Well this one is easy.
if (argc > 1)
	    OpenFile (argv[1]);

should be supplied just before the message processing loop

  1. Drop on the MLE window as a filename supplier.
    As easy as 6 was, this one's hard. We're glad most of it was described in processing the messages. But there are still a few items that remain uncovered.

The window the objects are dropped on should be the MLE control window. This window occupies the largest area in our application. This means the messages we want to act upon are sent to the MLE control. The MLE window has its own window procedure. To act upon these messages means we have to subclass the MLE window procedure (damn, a subclass again). Subclassing was handled in our previous article. We will take the same approach. The window will be subclassed and the pointer to the original MLE windowprocedure will be stored in the window word of the MLE window. To subclass the MLE window the following code has to be inserted right after the MLE window was created.

pfnwOldMLEProc = WinSubclassWindow(hwndMLE,NewMLEProc);    // Subclass
mainMLE (drag drop)

WinSetWindowULong(hwndMLE,QWL_USER,(ULONG)pfnwOldMLEProc); // Handle
winproc MainMLE in winword

In the subclass procedure we can query the window word to obtain a pointer to the original window procedure. The handling of these messages is already covered. The complete sublass code should look like this:

MRESULT EXPENTRY NewMLEProc(HWND hwndDlg, ULONG msg, MPARAM mpParm1,
MPARAM mpParm2)

  {
  PFNWP oldMLEProc;
  PDRAGINFO pDragInfo;
  PDRAGITEM pDragItem;
  oldMLEProc = (PFNWP) WinQueryWindowULong(hwndDlg, QWL_USER);
      // Get Winproc pointer from winword
  switch (msg)
    {
    case DM_DRAGOVER:
      {
      pDragInfo = (PDRAGINFO) mpParm1;
      DrgAccessDraginfo( pDragInfo );
      pDragItem = DrgQueryDragitemPtr( pDragInfo, 0 );
      //---------------------------------------------------------------
      // Don't accept multi select and non_text
      //---------------------------------------------------------------
      if (pDragInfo->cditem > 1 || !DrgVerifyType(pDragItem, DRT_TEXT))
          return ( MPFROM2SHORT (DOR_NEVERDROP, DO_UNKNOWN) );
      else
          return ( MPFROM2SHORT (DOR_DROP, DO_UNKNOWN) );
      }
    case DM_DROP:
      {
      UCHAR dragname[CCHMAXPATH];
      pDragInfo = (PDRAGINFO)mpParm1;
      DrgAccessDraginfo(pDragInfo);
      pDragItem = DrgQueryDragitemPtr(pDragInfo, 0);

      DrgQueryStrName(pDragItem->hstrContainerName,100,dragname);
      DrgQueryStrName(pDragItem->hstrSourceName,100,dragname+strlen(dragname));
      OpenFile(dragname);
      }
    break;
    }
  return oldMLEProc(hwndDlg, msg, mpParm1, mpParm2);
  }
  1. Handling the new menu items
    Of course, there are a few new menuitems. All the work is done in the described functions, so the handling of the messages should be easy.
    • the IDM_OPEN message (opening a file) This means, no matter what, a new file should be opened. Before we can do that the contents of the MLE have to be destroyed. After that the OpenFileDlg() function can be called. This is rather a crude way to handle things. If you'd like to make them more userfriendly you could ask if the file should be saved. (depending on the status of the changed flag of the MLE, which can be queried using the MLM_QUERYCHANGED message.)
IDM_OPEN:
	  WinSetWindowText(hwndMLE, "");
	  OpenFileDlg(hwnd);
break;
    • Retrieving a file is simpler, just call OpenFileDlg() and the user can use the filedialog to enter a filename whose contents will be inserted at the current cursor position.
case IDM_RETRIEVE:
	  OpenFileDlg(hwnd);
break;
    • Save and Saveas both have to query the titlebar for the filename. (A check on UNTITLED would be nice.) The further handling is straight forward.
case IDM_SAVE:
	  {
	  char szTitle[CCHMAXPATH];
	  WinQueryWindowText(hwndFrame, CCHMAXPATH, szTitle);
	  SaveFile(szTitle);
	  }
break;

case IDM_SAVEAS:
	  {
	  char szTitle[CCHMAXPATH];
	  WinQueryWindowText(hwndFrame, CCHMAXPATH, szTitle);
	  SaveasFileDlg(hwnd);
	  }
break;

Concluding Notes

Well, there it is; the second article on the MLE editor (it's rather long). To our surprise we recieved quite some mail on the first article. We hope this article will be as well received as the previous one. It took quite a long time to put together (mostly due to lack of time). We can imagine more reading is required on drag and drop because it is a fairly complex matter. This article only scratched the surface of what is possible. Besides some good books which describe this area of development, the DevCon CD does a very good job.

Feel free to use the code any way you like and send us your comments.

Also the SMALLED application (beta) on which these articles are based should be placed on Larry's FTP site [but was accidentally deleted by me. I'll try to get another copy on the FTP site as soon as possible. -Larry]; try it out! It supports

  • Drag and drop support
  • Clipboard support
  • Buttonbar
  • WYSIWYG Print facility
  • ATM support
  • Multi threading
  • EA control of fonts

In the mean time we've got our hands on Borland C++ 2.0, so the examples are built with that.