Building an Editor - Part 1

by Marc Mittelmeijer and Eric Slaats

Introduction
OS/2 includes two editors (if you've installed the Warp BonusPak, you even have three editors). The System Editor (E) is the smallest of the two. It has some basic functionality, and works really well for small tasks. The Enhanced Editor (EPM) claims to be something like a word processor, but is rather large and a pain to customize.

We needed an editor with a few extras. (The neural network interface we were building needed just such an editor.) Also this editor can be used as a separate application. Fortunately, OS/2 has a control which is very useful in this context - the MLE. So the only thing to be done is attach the custom controls like the buttonbar and the statusbar to a window and make them work.
 * It should have the functionality of the E editor.
 * It should be small in code.
 * It should have a statusbar, a menubar, and a buttonbar.
 * It should easily be incorporated in other applications.

In this article we'll describe how to build a small editor using the MLE control in two articles. In this first article we will describe how to build a window with a buttonbar, a statusbar, a menubar and an MLE. This will give you a full editor.

The loading and saving of files will be handled in the next article as well as changing fonts. This second article will therefore discuss the two standard dialogs OS/2 has to offer.

The Anatomy of a Window
If a standard window is created (including a menu and scrollbars) then in fact a lot of windows are created. Usually we use the WinCreateStdWindow function to create a window. With the frame control flags (FCF), PM is told which frame controls should be added. This way PM knows which child windows it must create. A complete standard window including a menubar and scrollbars should give the following "window-tree": Frame window ──┬── System menu │ 		├── Title bar │ 		├── Min/Max button │ 		├── Vertical scroll │ 		├── Horizontal scroll │ 		├── Client window │ 		└── Menubar Note: when the WinCreateStdWindow function is called with the frame controls the flags FCF_MENU and FCF_ICON, a menu and an icon should exist for this window (it should be included in the .RC file for the program). If this is not the case the compiled program will not show a window. These items can be built using for example the Borland C++ workshop or OS/2 Programmer's Toolkit.

The following program creates just such a window excluding the scrollbars. These can easily be added by adding FCF_VERTSCROLL and FCF_HORZSCROLL in the frame control flags. (The .RC file is created with the Borland workshop. The menu included is a menu which can be generated in the standard fashion.) The complete code can be found in STDWIN.ZIP

If you've ever built a PM program, the above code shouldn't be too difficult to understand. This program creates a standard window with a menubar on top. The window is sized by the WinSetWindowPos function.

The only other thing that might need some explanation is the handling of the WM_PAINT message in the window procedure. During WM_PAINT the client area is painted purple (you can choose another colour if you wish). This is done to prevent the client area from being transparent. If you don't fill the client area, the underlying windows will show. There is another way to do this, but it will show a boring standard background colour. If you return TRUE on the WM_ERASEBACKGROUND message, the client area will be filled with the standard background colour, and the WM_PAINT message won't be necessary.

Adding A Statusbar
That was easy, so now we will try a step that is a little more difficult. We want to add an extra control to the set of controls the client window already has. Let's begin with a simple one, a statusbar. If we want to add an extra control to a frame window it's necessary to understand how PM keeps track of the position, size, etc. of controls in a window.

A control in a frame window such as a menu or a scrollbar or a client area is just another window. If for example a menu-control is placed in a frame window, PM sets its size, place, etc. and all these parameters are kept in a structure known as the Set-Window-Position or SWP structure (the arguments of WinSetWindowPos also begin with SWP). This structure has the following members: typedef struct _SWP { ULONG  fl;              // flags LONG   cy;              // Window height LONG   cx;              // Window width LONG   y;               // Y-coordinate of origin LONG   x;               // X-coordinate of origin HWND   hwndInsertBehind;// Window behind which this window is placed HWND   hwnd;            // Window handle ULONG  ulReserved1;     // Reserved. This must be 0 ULONG  ulReserved2;     // Reserved. This must be 0 } SWP; Every window in the system has such a structure. Changing this structure (in the proper place) will result in a different behaviour of the window. The size or place may change, but also the Z-order may change. Normally these structures are modified by using the WinSetWindowPos function.

A frame window keeps a pointer to an array of SWP structures. This array contains a structure for every control (window) of the frame control. So adding another control is simple in theory. Just add an SWP-structure to this array of frame controls, fill it in, and voil...(!), you have another control. Unfortunately, nothing is as easy as it seems. Just meddling with this array can cause your application to crash. Because we are working with OS/2 PM, the messages have to be found which are sent by PM as all the SWP-structures for the controls are adjusted or filled.

The message we are looking for is WM_FORMATFRAME. This message is sent to a frame window to calculate the sizes and positions of all of the frame controls and the client window and it must return the number of controls connected to the frame window.

WM_FORMATFRAME
If we can intercept this message we can add controls and pass the new number of controls as return value. But nothing is ever as simple as it seems - we also need to intercept one more message. There are a number of messages which will need the number of controls attached to the frame window. Therefore it's necessary to query the number of SWP structures in the SWP structure array. This is done by the frame window message WM_QUERYFRAMECTLCOUNT. All this message does is return a SHORT that contains the number of controls attached to the frame window. The importance of this message lies in the fact that it is used to allocate the appropriate number of SWP structures which are passed in the WM_FORMATFRAME message. So if we don't intercept this message we may end up with an array of SWP structures which is too short to accommodate the SWP structures of the added custom controls.

So there are two messages that need to be intercepted. Usually we do this by subclassing the window so we can build a procedure that sees the messages before PM sees them. This way we can define a custom way to handle certain messages.

If a window is subclassed, the subclass procedure needs to know the pointer to the original window procedure so it can call this procedure for the messages it doesn't handle. The easiest way to pass this pointer to the subclass procedure is to create a global variable which contains this pointer. This is a rather crude method; there is a much more elegant way.

Every window has a space in which you can put values that are connected to that window. This space is known as window-words. If you know the handle of a certain window, you're able to access these window words. So window words are very suitable to pass the pointer to the original window procedure to the subclass function. The following code subclasses a window and puts the pointer in the window word. Now we need to build the subclass function that will handle the WM_QUERYFRAMECTLCOUNT and the WM_FORMATFRAME messages and that will pass the other messages to the original window procedure. This last point is very important and should not be forgotten. It's also the easiest to accomplish. First we have to extract the pointer from the window word. This can be done with the WinQueryWindowULong function. This function should be called with all the messages other than the two mentioned above. A framework for the complete subclass function should look like this: Before we can handle an SWP structure we need a window. For the statusbar we'll create a standard window of the WC_STATIC class. This type of window can be used to display text (which is what a statusbar is all about). The following code should be inserted in the main procedure. Now all we have to do is attach this window as a control to the frame window.

Let's handle the messages we want to intercept. The WM_QUERYFRAMECTLCOUNT is the simplest. This frame message should return the total number of frame controls. To handle this we first should know the number of original frame controls. This can be obtained by calling the original (frame) window procedure with this message. Because the variables hwnd, msg, mp1, and mp2 still contain these values a call could be made using them. This call returns the number of original frame controls. We increment this value and return it. Now PM receives from WM_QUERYFRAMECTLCOUNT the number of SWP structures it should allocate memory for. So now there's an SWP structure allocated for the statusbar, so we can fill the SWP structure for the added statusbar.

To fill in the SWP structure, we first need a pointer to this array of SWP structures. This pointer is passed in the first message parameter of the WM_FORMATFRAME message. We also need the number of original frame controls. This value is needed because we add another SWP in an array of SWP's. Because this control will be the last added, the number of original frame controls will also be the arrayindex of the new SWP. (An array starts with 0, the control count starts with 1!) WM_FORMATFRAME also returns the number of frame controls in the low order short of the return value, so we can use the same trick as in WM_QUERYFRAMECTLCOUNT, and call the original frame window procedure. The following code gets these two values. (The value for oldFrameProc is known.) How should a statusbar look? Well, let's make some choices. Let's take it one step at a time. The extra SWP structure we've added contains no valid values. To attach it to the statusbar window, the window handle of the statusbar window has to be set into the hwnd field of the SWP structure. (Remember itemCount was the index of the added SWP structure for the statusbar!) pSWP[itemCount].hwnd = hwndStatus;  // Window handle The statusbar window we created has no size or place at this moment. So first we have to make sure that we can give it a proper place and resize it. To accomplish this we'll have to set the window flags of the statusbar window. (Don't forget this, or the sizing and moving of the window will not work!) These flags can be set in the SWP structure. pSWP[itemCount].fl = SWP_SIZE | SWP_MOVE;  // SWP_SIZE style flag Now let's give the statusbar the same size as the menubar. To accomplish this we'll need the index of the menubar SWP-structure. In this structure we'll find the cx and cy values we need for the statusbar. The simplest way to get these values is to take the index number of the menubar (which depends on the number of controls.) Normally the numbering is:
 * It should have the same height as a menubar.
 * It should be located at the bottom of the frame window
 * 0) System menu icon
 * 1) Title bar
 * 2) MinMax
 * 3) Standard menu
 * 4) Client window

Of course, if you add controls like the scrollbars, this may change. Also if, for whatever reason, this sequence is changed, we've got a problem. So taking an educated guess for the index is not such a good method. So we'll have to think of something else.

Every SWP structure contains a window handle. By comparing the window handles we know with the window handles in the SWP array, we can track down the SWP for every window we want. If we know the handle of the menubar, we can track down the SWP for the menubar. The handle for the menubar can be obtained by using the WinWindowFromID function in combination with the Frame window handle and the FID_MENU constant. The following code should give us the index of the menu SWP. USHORT usMenu = 0; while (pSWP[usMenu].hwnd != WinWindowFromID(hwndFrame, FID_MENU)) ++usMenu; Now we've got the index for the menu window (There must be a menu! if there's no menu this code doesn't work!) it's easy to set the size for the statusbar. pSWP[itemCount].cy = pSWP[usMenu].cy;    // Height menu height pSWP[itemCount].cx = pSWP[usMenu].cx;    // width is menu width Now the window is sized, it has to be placed in the right spot, on the bottom of the frame window. This is in fact the (x,y) position of the client window. So if we obtain the SWP of the client window, we know the (x,y) position of the statusbar. We're not home-free yet. There are two things that need to be done before we've got a window with a statusbar. The Z-order has to be set in the SWP structure and the size and position of the client window has to be adjusted. If this step is forgotten the statusbar will exist, but it will not show because the client window will be placed over the statusbar. pSWP[itemCount].hwndInsertBehind = HWND_TOP; pSWP[usClient].cy = pSWP[usClient].cy - pSWP[itemCount].cy; pSWP[usClient].y = pSWP[itemCount].y + pSWP[itemCount].cy ; All these snippets of code may seem ill-digested, so here's the complete subclass routine. Note: The complete code can be found in STATUS.ZIP

The above code will produce a window which will look something like this:

Adding a Buttonbar
Adding a buttonbar is largely the same as adding a statusbar to the frame window. The only thing we've got to figure out is how we to make a buttonbar work without producing masses of code (remember, don't do anything the system can do for you). If we search through sample code, then we'll find masses of examples of custom controls which require a lot of code. We'll take the direct approach and try to build a buttonbar with as little code as possible.

What do we want from a buttonbar? Essentially this is the same as we want from a menubar. The only difference is that the menu items will be represented by bitmaps. When a menu is created it is possible to place bitmaps in the menu instead of text. How is this function implemented?

First we created the following (button) bitmaps:

These bitmaps can be found in the file TOOLBAR.ZIP.

To use bitmaps in a menu, the bitmaps should be known in the .RC file. To accomplish this we need to do two things. First we need to create identifiers for all the bitmaps to be used. The simplest way is to use the same identifiers for the bitmap menu entries as we used for the bitmap (toolbar) menu entries. The following lines are added to the .RC file so that the bitmaps will be recognized. The next step in creating a menu with bitmaps is building the actual menu. The following lines in the .RC file describe the toolbar menu. As text which should be displayed in the menu-item, we'll put the identifier of the bitmap with a "#". This way, as the .RC file is compiled in an .RES file, the bitmaps will be placed in the toolbar menu. To find out if this approach is working, build the main menu this way. If you recompile the application with a resource file created this way, the main menu will show icons. (It's possible to combine standard menus with bitmaps.)

Now for the tricky part - how do we attach the toolbar to the frame window so that it will react to the pressed buttons and move to the correct place?

There are two ways to connect a menu to a window. The first is used in the statusbar and the standard window example. Use the FCF_MENU flag and provide a menu resource. When a window is created, the menu is attached to this window. The other method is using the WinLoadMenu function. This function creates a menu window using a menu resource. In the toolbar problem we want two menus to be loaded. The problem with WinLoadMenu is that it only connects the last menu loaded to the given window. (Remember only one SWP structure is reserved for the standard menu!) So we load the normal menu last (don't forget to remove the FCF_MENU flag or the menu will be loaded twice with unpredictable results). The first WinLoadMenu causes the toolbar menu window with the bitmaps to be created.

To attach the toolbar menu to the frame window we use the same trick as in the statusbar example - we subclass the window procedure and capture the WM_FORMAT and WM_QUERYFRAMECTLCOUNT messages.

Life could be easy, but we're doing PM, remember. We should handle the toolbar a little bit differently from the statusbar because it's a menu window. Sizing this window isn't easy. To do this we should know the height of the button bitmaps, etc. Fortunately there's an easier way. There is a message which causes frame controls to handle their own resizing. This is the WM_ADJUSTWINDOWPOS message. This message is usually sent by the WinSetWindowPos call. If this message is sent to the toolbar window with the proper values set in the SWP structure, it will size the toolbar.

To size the toolbar a couple of values have to be set in the SWP structure. For the WM_ADJUSTWINDOWPOS message to work, the cx and cy values should be filled so that the menuitems can easily fit in this space. The client size should do just fine. WM_ADJUSTWINDOWPOS tries to fill in as many menu items in the cx range as possible. If it can't put them all in that space, a second row is formed. In the end the cy value is adjusted so that it will be exactly the height of the menu rows. (Usually we have just one row.) This way we let PM adjust the menu for us. Code to do this is built into PM so why invent it again?

By sending this message, cx and cy are filled with valid values. We can use these to calculate the place of the toolbar and to resize the client window so that it won't be drawn over the buttonbar. pSWP[usToolbar].x = pSWP[usMenu].x;                        // XPOS in FRAME pSWP[usToolbar].y = pSWP[usMenu].y - pSWP[usToolbar].cy;   // YPOS in FRAME pSWP[usClient].cy = pSWP[usClient].cy - pSWP[usToolbar].cy; // resize client If we use a statusbar and a toolbar, we should not forget to return itemCount+2 in both the messages. If we do forget them, the new controls will not be shown.



Note: The complete working code can be found in the file TOOLBAR.ZIP.

And Now Some Functionality
So far we've created a window with a menubar, a toolbar and a statusbar. But it doesn't do anything. As you may have guessed from the title of this article, the main goal is to build a simple editor. The tricks used in the previous sections can be used in a variety of situations. We will put them to work in controlling an MLE control.

What is an MLE control?
If we take a look at PM, we'll see that in a number of places it's necessary to use a control which can handle multiple lines of text. For example if we look in the third page of the File tab in an object's notebook, we'll see three fields in which text can be entered. (Here we can fill in some extended attributes.) Another example is the title field in the General tab where the title of an object can be described using more lines of text. To handle these situations, PM uses a Multi Line Entry-field or MLE control.

The MLE is a sophisticated control that enables us to handle multiple lines of text in a editor-like style. The implementation of the MLE is rather complete. It has features all editors should have like clipboard control, search, replace, undo, and font support. Originally the MLE was designed to handle text no larger then approximately 4Kb. But we found it can handle text as large as 1Mb (if programmed properly). So we've got a pretty complete editor on our hands. The only problem is that there isn't a complete user-interface (e.g. buttons, statusbar, etc.).

The conclusion is that this is the perfect control to complete our small editor. In this article we only handle the basics like controlling the clipboard. In future articles we may handle other items like file retrieval, font change, search and replace.

An MLE In The Client Area
We take the program as created in the toolbar example as a starting-point for the next step. In the client area (which was purple in our previous examples) we place an MLE control. If we set up the MLE so that it fills the entire client area, we eliminate the necessity to handle the WM_PAINT message.

We want the MLE to be a child of the client window. This way we can handle the sizing of the MLE in the WM_SIZE message. The creation of the MLE can be done in the main procedure or while handling WM_CREATE message (as long as the client window exists). Let's take a short look at how this window is created. The interesting parts are the style flags, the size and position. We do specify some MLE specific flags. In this case we specify horizontal and vertical scrollbars and a thin line around the MLE. This line is just a cosmetic effect, but it looks better. The MLS_WORDWRAP flag puts the MLE in word-wrap mode. (This can be turned on and off.)

Giving the MLE a position and a size has no meaning at all, because the client window has no size or place at this moment. The size and place of the MLE will be set during the handling of the WM_SIZE message. This way the MLE will always be the size of the client window, even if we resize the frame-window. During the WM_SIZE message we first query the SWP structure for the client window. We only need the cx and cy parameters. We use the WinSetWindowPos function with the cx and cy values to set the size of the MLE. The x and y coordinate must be 0 because these coordinates set the position of the MLE relative to the client window! The code that handles the WM_PAINT message can be discarded because the MLE is filling the client area, so there's no need to paint it ourselves.

If you recompile the code in this stage, you'll get a nice window with a menu, toolbar, buttonbar and a MLE. The MLE will function, but it won't respond to the menu actions.

Making The Menu's Work
We want the MLE to react to actions we take with the buttonbar and the menubar. Pressing a button in the toolbar or selecting a menu-item will cause the WM_COMMAND message to be send. The low-order USHORT of message parameter 1 of the WM_COMMAND will contain the ID of the pressed control/menuitem.

Now all we have to do now is send the appropriate message to the MLE control depending on the menu action taken. The messages we want to send to the MLE are really easy. For undo, cut, paste, copy and clear the messages are simple - the message parameters can be filled with zero's and the message only indicates the action.

To handle the new action, we've got to do something more. Here we use a simple trick. Instead of selection everything and deleting it, we'll write an empty string to the MLE. This example has the advantage that it's very simple. The disadvantage is that the undo won't work after a new operation. case IDM_NEW: WinSetWindowText(hwndMLE, ""); break; The exit action also needs to be handled differently. If this action is chosen, we must send a WM_CLOSE message to the main-window. This message causes the main window (and thus all it's children) to close. case IDM_EXIT: WinSendMsg(hwndWnd, WM_CLOSE, 0L, 0L); break; The last action we want to handle is the about action. Normally a "about/product" information dialogbox is displayed if this action is chosen. In our case we use the statusbar to display the about message. case IDM_ABOUT: WinSetWindowText(hwndStatus, "MLE editor by Eric & Marc"); break; The following code handles just about everything we want to handle in this examples menu actions.

We've Got A Statusbar, And Now?
Our little applet is almost complete. One thing is missing, though. We've done a lot of work to create a statusbar, and we aren't using it. (Sounds stupid.) The main thing a statusbar is used for is to display the line number and character number where the cursor is. We're going to do just that.

There are several ways to approach this problem. We can intercept the WM_CHAR message and query the current line and character. This message is send every time a user presses a key. This works just fine except for one crucial point. Users of editors like to press the up or down arrow and keep it depressed until the cursor is on the line of their liking. This generates only one WM_CHAR message. So the statusbar will only be filled once. This is not exactly what we had in mind! We can hang on to the WM_CHAR message and build a hideous contraption which will handle this dilemma, or we can take a completely different turn.

Let's take a different turn and examine the problem. All we want is the statusbar to be refreshed with accurate values a number of times every second, and only if the current values are not valid anymore. We could generate a message every tenth of a second that checks the values in the statusbar and refreshes them if necessary. Luckily (is it really luck) OS/2 has a mechanism which does just that, namely timers.

In OS/2 it's possible to set a timer which will generate a message every tenth second. If we react to this message we'll be rid of our problem. A timer can be set using the WinStartTimer function. This function starts a timer that will generate a WM_TIMER message every time the timer times out. The timer is set in thousands of seconds. The following line sets a timer which will send a WM_TIMER message every tenth second. WinStartTimer(habAnchor, hwndClient, 1, 100); The timer has to be started just before the window becomes active. This line can be inserted just before the message handling loop in the main routine.

All we have got to do now is take the appropriate action on the WM_TIMER message. Let's do the right thing and not lose any CPU cycles. We only want to update the statusbar if it doesn't contain a valid value. This means we have to remember the last value (querying the statusbar is time-consuming).

The MLE control has a number of messages that can be used for querying the cursor position. The first is the MLM_QUERYSEL message. We can abuse this message to obtain the current cursor-point in the text. This can be done by using the MLFQS_CURSORSEL constant. This way the current cursor-position is returned in one single long.

Position = LONGFROMMR(WinSendMsg(hwndMLE, MLM_QUERYSEL, (MPARAM)MLFQS_CURSORSEL, 0L));

If we remember this position we only have to refresh the statusbar if the new position differs from the old one. If this is the case, we can use the MLM_LINEFROMCHAR and the MLM_CHARFROMLINE messages to get the line and char positions. The following piece of code handles the WM_TIMER message in this way. If this last piece of code is added, we've got a fully functional small editor!

Note: The full working code can be found in the file EDITOR1.ZIP.

Concluding Notes
We've taken the concept described in this article one step further and created a small editor with the capabilities of the E editor. As a plus this small editor has WYSIWYG handling of fonts which it can print and it has drag and drop support. This editor will be made available on the Internet as soon as it is completely finished. SMALLED has the following features: In another article we'll handle the standard dialogs OS/2 has to offer in combination with the described editor. This way we can load and save files and change fonts (other than dropping them on the MLE).
 * Drag and drop support
 * Clipboard support
 * Buttonbar
 * WYSIWYG Print fascility
 * ATM support
 * Multi threading

The examples in this article were built using Borland C++ 1.5.