Windows Programming in Poly/ML

David C.J. Matthews

David C.J. Matthews 2001. Updated 2009.

This is a brief introduction to programming with the Windows™ interface in Poly/ML.  It is arranged as a tutorial around the mlEdit example, a small example text editor.  It is not intended as a full description of writing programs for Windows and is no substitute for a more general guide.  If you are planning to develop an application using this interface it is probably worth purchasing a book or CD_ROM reference for Windows such as the Microsoft Developer Network library.

This introduction assumes some familiarity with programming in ML and perhaps some experience of other windowing systems, such as X-Windows.  It does not assume any experience of programming with Windows and, for experienced Windows programmers, it will cover familiar ground.  It does, though, point out some of the differences between the ML interface and that of other languages.  The interface functions in ML are very similar to the underlying C functions.  The main differences are in the types of the arguments and a few other changes necessary for ML.   For more information see the Windows Interface Reference.


The original version of the Windows interface was written at AHL by Panos Aguieris.  It uses the Poly/ML CInterface structure written by Nick Chapman to provide nearly all the interface.  It was extensively modified and expanded by David Matthews.


1. Windows, Messages and Window Procedures
2. Creating the child window
3. Creating the parent window
4. Menus
5. Sending Messages
6. Dialogues
7. Common Dialogues
8. Printing and Painting

1. Windows, Messages and Window Procedures

When thinking about a window we tend to think of a top-level application or a document window.  From the point of view of programming, though, this is just one variety.   In fact each label, menu and text box is a window but because they form part of a bigger application they tend to be overlooked.  These are child windows.   Nearly every window has to deal with two main areas of concern.  There has to be a way of displaying the contents of the window and there has to be a way of handling user input, whether by the mouse or from the keyboard.

Most communication with windows is by sending messages.  A message may be sent as a result of user input, such as moving the mouse or typing a character at the keyboard.  It may also be sent by the application to change the appearance of the window or to get information from it.  There are over two hundred different message types defined by Windows and it is possible to define your own for use within an application.  Many of these are only relevant in special circumstances and only a few are used regularly.  It's worth noting in passing that messages can be sent from one process to a window in another process and that hidden windows, windows which don't appear on the screen but are only ever used to receive messages, are one of the standard ways of communicating between processes in Windows.

The appearance of a window is largely governed by how it responds to different kinds of messages.  Every window has a window procedure which processes messages sent to the window.  When you create a window you give the class of the window you want to create and the new window uses the window procedure for that class.  You can either use a standard class, such as Edit, or you can create your own class using your own window procedure using RegisterClassEx.  You will nearly always need to create at least one class for your top-level window. 

To see how this works in practice let's look at an extract from the mlEdit example.  

fun wndProc(hw: HWND, msg: Message, state as SOME{edit, fileName, ...}) =
    case msg of
    |   WM_CLOSE =>
        (if checkForSave(hw, edit, fileName) then DefWindowProc(hw, msg) else LRESINT 0, state)
    |   _ => (DefWindowProc(hw, msg), state)
and fun checkForSave ....

This extract from the window procedure, wndProc, processes the WM_CLOSE message.   This message has no parameters and is generated by the system when the user clicks in the close box of the window (the box at the top left of the window with a cross in it).   Every message is sent to the window procedure.  When writing a window procedure we are often only interested in handling a few of the possibilities so Windows performs a default action if we don't want to handle the message.  Even if we do handle it we can pass it on for default processing when we have dealt with it.  The default action for WM_CLOSE is to destroy the window by a call to DestroyWindow.  In our application we want to ask the user whether to save the window if the text being edited has been modified.  This process also gives the user the opportunity for second thoughts and they may cancel the closure.  This is handled by our function checkForSave which returns true if it is safe to close the window.   Window procedures in ML return a pair as their result.  The first field of the pair is the Windows result value.  For many messages a result of LRESINT 0 is suitable.  In this example we return SOME(LRESINT 0) if we don't want the window to be destroyed. 

The full type of a window procedure in ML is
    HWND * Message * 'a -> LRESULT * 'a
The third argument is the state of the window and an updated version of the state is returned as part of the result.  This allows the window procedure to process a message and update the state as part of the process.  It is an ML extension which is not part of the underlying message system.  In our extract we return the previous state unchanged.

The mlEdit example works by constructing a parent window using a custom class and our own window procedure and then making a child window within it using the system Edit class.   The Edit window deals with all the keyboard input so saving us the need to write this ourselves.  Our window procedure has to process messages to do with menu selection and other messages sent to top-level windows. 

    WM_SETFOCUS _ =>
         SetFocus(SOME edit);
         (DefWindowProc(hw, msg), state)
|    WM_SIZE{height, width, ...} =>
         MoveWindow{hWnd=edit, x=0, y=0, height=height, width=width, repaint=true};
         (DefWindowProc(hw, msg), state)

The WM_SETFOCUS message is sent when the user clicks on a window and is intended to set the keyboard focus to that window (i.e. selects the window to receive keyboard input).  The edit child window occupies the centre of our window so the WM_FOCUS message will be sent directly to it if the user clicks within that area.  Since we are not interested in receiving characters in our top-level window we set the focus to the edit window if the user clicks on the border.   While it isn't essential to process this message at all for the mlEdit application to work correctly, by doing so we improve its usability.

The WM_SIZE message is sent if the window is resized.  Among the parameters to the window are the height and width of the client area, the area excluding the border and the menu bar.  This is the area we want to use for our edit window so we use MoveWindow to set its size.  Calling MoveWindow causes the edit window to receive a WM_SIZE message.  The window procedure for the edit window uses this to adjust the format of the text within the window to suit the new size.  We do not need to be concerned about how this happens in our application.  WM_SIZE is also sent when a window is created.  This is convenient and means we do not need to set the size of the edit window when we create it.

2. Creating the child window

So far we have not described how the edit window is created.  We need it to be a child window of the top-level window and the easiest way to do that is to create it when the WM_CREATE message is received.  This message is sent as part of the process of creation of the top-level window.

fun wndProc(hw: HWND, msg: Message, NONE) =
    case msg of
        WM_CREATE _ => (* Create an edit window and return it as the state. *)
            val edit =
             CreateWindow{class = Class.Edit, name = "",
                style = Edit.Style.flags[Edit.Style.WS_CHILD, Edit.Style.WS_VISIBLE, Edit.Style.WS_VSCROLL,
                            Edit.Style.ES_LEFT, Edit.Style.ES_MULTILINE, Edit.Style.ES_AUTOVSCROLL],
                            x  = 0, y = 0, height = 0, width = 0, relation = ChildWindow{parent=hw, id=99},
                            instance = Globals.ApplicationInstance(), init = ()}
            (* We also set the font for the edit window here.  This has been omitted. *)
            (LRESINT 0, SOME{edit=edit, devMode=NONE, devNames = NONE, fileName=""})

    | _ => (DefWindowProc(hw, msg), NONE)

This extract of the window procedure shows the creation of the edit window as part of processing WM_CREATE.  We use CreateWindow which makes a window of a specified class and returns a handle to it.  Once we have processed the message we set the state to SOME of a record containing a handle to the edit window.  (Instead of doing this we could use GetDlgItem to find it each time we needed it, but this is easier).  At this stage we just pass zeros for the size of the window since we will use the WM_SIZE message to set its size.   The id value for the child window (we use 99) is irrelevant in this example since we never use it.  The style does not include horizontal scrolling so the edit window uses word-wrapping.

3. Creating the parent window

It's now time to see how we create the main window itself.  Since we want to use our own window procedure we need to register a class even though we will only create a single window of this class.

val polyIcon = ...
val menu = ...
val className = "mlEditWindowClass"
val app = Globals.ApplicationInstance()
(* Register a class for the top-level window.  Use the Poly icon from the application. *)
val myWindowClass = RegisterClassEx{style = Class.Style.flags[], wndProc = wndProc, hInstance = app,
    hIcon = SOME polyIcon, hCursor = NONE, hbrBackGround = NONE, menuName = NONE,
    className = className, hIconSm = NONE};

val w = CreateWindow{class = myWindowClass, name = "mlEdit", style = Window.Style.WS_OVERLAPPEDWINDOW,
    relation = PopupWindow menu, instance = app, init = NONE};
ShowWindow(w, SW_SHOW);
SetForegroundWindow w;

UnregisterClass(className, app)

In the mlEdit example we use the icon from the Poly/ML application.  It isn't necessary to provide an icon: Windows will provide a default one if NONE is given for hIcon.  We use the WS_OVERLAPPEDWINDOW style for the window which is the standard for a top-level window and gives a standard border with a system menu and minimise, maximise and close boxes.  CW_USEDEFAULT is used for the size and position of the window.   The user can always move or resize it once it has been created.   We create the window with the PopupWindow value and pass the menu to be used.  Once the window has been created we call ShowWindow to make it visible and SetForegroundWindow to make it appear above other windows.

Some messages are sent to the window procedure as a result of calling the functions such as CreateWindow.  Generally, though, messages are queued and have to be explicitly dequeued and passed to the window procedure.  In C this is done by a message loop with GetMessage and DispatchMessage.  In ML we use the RunApplication function which deals with all this.  RunApplication returns when a WM_QUIT message is received.  To ensure that this happens we have to put this message into the queue.  The easiest way to do this is to call PostQuitMessage when the window is about to go away.  We have already seen how DestroyWindow is called as a result of processing the WM_CLOSE message.  DestroyWindow sends WM_DESTROY and WM_NCDESTROY messages to the window procedure to allow it to clean up properly.  WM_NCDESTROY will be the last message to be sent to this window procedure so we call PostQuitMessage while handling it.

         PostQuitMessage 0;
         (DefWindowProc(hw, msg), state)

We need to call UnregisterClass when RunApplication returns.  Classes are automatically unregistered when the application terminates but in this context the application is the whole Poly/ML session so if we want to run mlEdit again within the same session we need to unregister the class.   Otherwise the next time we call RegisterClassEx with the same name it will fail because the class is already registered.

4. Menus

We have already seen that a menu can be set up by passing its handle as part of the PopupWindow value to CreateWindow.  Let's see how the menu itself is created and how it is processed.

There are a number of ways a menu can be created but perhaps the simplest is to build it up using calls to CreateMenu and AppendMenu.  The structure we want for our menu is a bar containing File, Edit and Help menus each with several menu items.  In general a menu consists of multiple items, each of which may either be a command item or may pull up a sub-menu.

val menuOpen = 1
and menuQuit = 2
and menuSave = 3
val fileMenu =
        val fileMenu = CreateMenu();
        AppendMenu(fileMenu, [], MenuId menuOpen, MFT_STRING "&Open");
        AppendMenu(fileMenu, [], MenuId menuSave, MFT_STRING "&Save");
        AppendMenu(fileMenu, [], MenuId menuSaveAs, MFT_STRING "Save &As...");
        AppendMenu(fileMenu, [], MenuId 0, MFT_SEPARATOR);
        AppendMenu(fileMenu, [], MenuId menuPageSetup, MFT_STRING "Page Set&up...");
        AppendMenu(fileMenu, [], MenuId menuPrint, MFT_STRING "P&rint...");
        AppendMenu(fileMenu, [], MenuId 0, MFT_SEPARATOR);
        AppendMenu(fileMenu, [], MenuId menuQuit, MFT_STRING "&Quit");

We create the file menu by calling CreateMenu and then AppendMenu for each item.   Every item, apart from separators, has a different item ID.  The values are arbitrary but we need to use different values for each because these are the values which will be passed to our window procedure when a particular menu item is selected.   Separators, which appear as horizontal lines when the menu is pulled down, are used to improve the layout.  The ampersands (&s) precede the character which will be underlined in the menu.  These provide keyboard shortcuts for menu items.  Typically the first character is used but sometimes we have to use another character in order to give all the items in a menu different characters.

val menu = CreateMenu();
val _ = AppendMenu(menu, [], MenuHandle fileMenu, MFT_STRING "&File");
val _ = AppendMenu(menu, [], MenuHandle editMenu, MFT_STRING "&Edit")
val _ = AppendMenu(menu, [], MenuHandle helpMenu, MFT_STRING "&Help")

We can create the edit and help menus in exactly the same way.  When they have been created we can build the full menu.  The argument to AppendMenu is MenuHandle rather than MenuId since these are sub-menus.

As with all other input selecting a menu item causes the system to send a message to the window.  It sends a WM_COMMAND message with information about the particular menu item.  The wId value in the messagecontains the identifier which was used when the menu was created. 

|    WM_COMMAND{notifyCode = 0, wId, control} =>

        if wId = menuQuit
        if checkForSave(hw, edit, fileName) then DestroyWindow hw else();
        (LRESINT 0, state)
        else ...

The simplest item to process is when Quit is selected.  We process it in almost the same way as we process WM_CLOSE, except in this case we have to explicitly call DestroyWindow since the default action for this message is to do nothing.  Note that since we process WM_NCDESTROY by calling PostQuitMessage the message loop in RunApplication will exit when the window destruction is complete.

5. Sending Messages

So far we have seen how we process messages but not how our application can send them.   An application sends messages using either SendMessage or PostMessage.   The main difference between them is that SendMessage does not return until the window procedure has processed the message and so it is able to return a result to the caller.  It functions essentially as a, possibly remote, procedure call.

We can use SendMessage to process some of the menu items.

        else if wId = menuCut
        then (SendMessage(edit, WM_CUT); (LRESINT 0, state))
        else if wId = menuCopy
        then (SendMessage(edit, WM_COPY); (LRESINT 0, state))
        else if wId = menuPaste
        then (SendMessage(edit, WM_PASTE); (LRESINT 0, state))

The Cut, Copy and Paste items from the edit menu are handled by sending messages to the edit window.  The edit window processes these by copying data to and from the clipboard.  In this example we are not interested in the result that SendMessage returns but we use it rather than PostMessage to ensure that the command is fully processed before, for example, we accept any characters as input.

Another case where we can use SendMessage is to see whether the text has been modified and so whether we need to save it before quitting.

fun checkForSave(hw, edit, fileName) =
    case SendMessage(edit, EM_GETMODIFY) of
            LRESINT 0 => true (* Unmodified - continue. *)
        |    _ => ... (* Save it. *)

We send the edit window an EM_GETMODIFY message.  If the reply is LRESINT 0 (i.e. false) it has not been modified and we don't need to do anything.  Otherwise we need to save the document, or at least ask whether we should save it.

6. Dialogues

A dialogue box is a special kind of window which is used to present information to the user or to request information.  They contain one or more controls and a button usually labelled OK and often a Cancel button.  The simplest form of dialogue is the message box.  This presents a piece of text and has one or more buttons.  

    val res =
        MessageBox(SOME hw, "Save document?", "Confirm",
    if res = IDYES
    then .... (* Save document. *)  ...
    else if res = IDNO
    then true (* Continue anyway. *)
    else false (* Cancel - don't exit or open. *)

We use a message box in mlEdit to ask whether we should save the document if it has been modifed.  The MB_YESNOCANCEL value for the style means that the message box will have three buttons: Yes, No and Cancel.  The message box is an example of a modal dialogue.  Modal dialogues are those which need a response before the application can continue.  The application is disabled until the dialogue has been dismissed by clicking one of the buttons.  In contrast a modeless dialogue acts just like another window and the user can interact with either the application window or the dialogue box.   MessageBox returns with an identifier which indicates the button which was pressed.   If Yes, we need to save the document; if no, we don't.  If Cancel was pressed we need to cancel the operation which caused this dialogue to be presented.  For instance, if the user accidentally clicked on the Close box of the window and then pressed Cancel we do not pass the WM_CLOSE message to the default window procedure.  It is good interface design practice to provide some way for the user to cancel an action.

A more sophisticated dialogue is used when the user selects "About mlEdit..." from the Help menu.  MessageBox is not sufficient for this dialogue since we want to include an icon in the dialogue box so we use one of the general dialogue functions.   The layout of a dialogue can be given either in a resource file or as a template.   Resource files are often a convenient way of holding dialogues as well as menus and other strings.  They lend themselves particularly to producing localized versions of programs, i.e. programs with the user interaction tailored for a particular language and/or culture, since all the text that needs to be localized can be stored in the resource file.  A resource file is either an executable file (.EXE) or a dynamic library (.DLL).  To use a resource file you will need a suitable resource compiler which will normally form part of a development environment such as Microsoft Visual C or Borland C++.  You can then load the resource file and get a handle to it using LoadLibrary.  It is possible to make use of resources in the Poly/ML program using the instance handle returned by ApplicationInstance.  We actually do this in mlEdit to get a handle to the icon.

(* Borrow the Poly icon from the application program. It happens to be icon id 102. *)
val polyIcon = Icon.LoadIcon(app, Resource.MAKEINTRESOURCE 102);

For this example, though, we use a template for the dialogue.

val pictureId = 1000 (* Could use any number here. *)
open Static.Style
val template =
    {x = 0, y = 0, cx = 210, cy = 94, font = SOME (8, "MS Sans Serif"), menu = NONE,
     class = NONE, title = "About mlEdit", extendedStyle = 0,
     style = flags[WS_POPUP, WS_CAPTION],

     items =
      [{x = 73, y = 62, cx = 50, cy = 14, id = 1,
        class = DLG_BUTTON (flags[WS_CHILD, WS_VISIBLE, WS_TABSTOP]),
        title = DLG_TITLESTRING "OK", creationData = NONE, extendedStyle = 0},

       {x = 7, y = 7, cx = 32, cy = 32, id = pictureId,
        class = DLG_STATIC (flags[WS_CHILD, WS_VISIBLE, SS_ICON]),
        title = DLG_TITLESTRING "", creationData = NONE, extendedStyle = 0},

       {x = 15, y = 39, cx = 180, cy = 21, id = 65535,
        class = DLG_STATIC (flags[WS_CHILD, WS_VISIBLE, WS_GROUP]),
        title = DLG_TITLESTRING
               "mlEdit - An exmple of Windows programming in Poly/ML\
               \\nCopyright David C.J. Matthews 2001",
        creationData = NONE,  extendedStyle = 0}] }

The dialogue contains three items: a button with the title "OK", a static picture with style SS_ICON, and a piece of static text.  Along with the layout of the dialogue we also need a dialogue procedure.  A dialogue procedure is almost the same as a window procedure.  It processes messages sent to the dialogue in the same way as a window procedure does. The only difference is that a dialogue procedure does not call DefWindowProc.

fun dlgProc(dial, WM_INITDIALOG _, ()) =
        (* Send a message to the picture control to set it to this icon. *)
        SendMessage(GetDlgItem(dial, pictureId), STM_SETICON{icon=polyIcon});
        (LRESINT 1, ())

|    dlgProc(dial, WM_COMMAND{notifyCode = 0, wId=1 (* OK button *), ...}, ()) =
        (* When the OK button is pressed we end the dialogue. *)
        (EndDialog(dial, 1); (LRESINT 1, ()) )

|    dlgProc _ = (LRESINT 0, ())

We only process two messages here: WM_INITDIALOG and WM_COMMAND.  WM_INITDIALOG is sent when the dialogue is created.  We use GetDlgItem to get a handle to the static picture control and then send it a STM_SETICON message to set the picture.  We only need to do this when the dialogue is initialised.  The static control will take care of displaying the picture whenever the dialogue box is visible.  WM_COMMAND messages are sent by buttons as well as by menus and in this example we process the message by calling EndDialog to close the dialogue box.  The value of wId in this case is the identifier of the OK button.

To construct and display the dialogue box we call DialogBoxIndirect with the template and the dialogue procedure.   The parent of the dialogue is our main window (hw).  This window will automatically be disabled until the dialogue box has been closed.

DialogBoxIndirect(app, template, hw, dlgProc, ());

7. Common Dialogues

For many purposes a standard dialogue box can be used and Windows provides a number of these.  For instance, most applications include a way for the user to open a file and it would be tedious to have to program this from scratch for each new application.   It is far easier to use a standard dialogue.  More to the point, by using a standard dialogue the interface to the user is similar to that of other applications, reducing the learning load.

The mlEdit example uses a number of these.  GetOpenFileName and GetSaveFileName are used to select the file to open and for the Save As menu item.  FindText is used for the Find menu item.   PageSetupDlg and PrintDlg are used for the Page Setup and Print items.  They generally work in the same way, creating a modal dialogue requesting the information and returning when the OK or Cancel button is pressed.   The ML functions take a configuration structure and return an option type.  If the user cancels the operation they return NONE, if the user presses OK they return a new configuration structure with the requested information filled in. FindText works differently.  It creates a modeless dialogue and instead sends FINDMSGSTRING messages to the parent window.

8. Printing and Painting

One area we have not touched on is actually how we draw to the screen.  By using the Edit window to draw the text our application does not actually need to concern itself with exactly what happens when an area of the screen is uncovered and how characters in the text are converted into dots on the screen.  This is all dealt with by the Edit window class.

When it comes to printing the file these issues become apparent.  Printing to a printer and drawing to the screen are handled in exactly the same way in Windows.  In both cases we need to draw the document into a device context.  A device context is an abstraction used for printers, the screen and also for bitmaps and metafiles in memory.  Although there is an obvious difference in the printed page and an image on the screen in terms of the physical output as far as preparing the image is concerned they are both rectangular areas of dots (pixels).  They may differ in size, in the range of colours possible and in the number of pixels per inch (resolution).   Since we don't need to be concerned with drawing to the screen in this application we will focus on printing the file but we could easily arrange the code so that it could be used for both.

We get a device context for the printer by giving the PrintDlgFlags.PD_RETURNDC option to the print dialogue.  It would also be possible to get a device context using CreateDC.   Once we have the device context we can find the size of the page.

val _ = SetMapMode(hdc, MM_TEXT)
val pageWidth = GetDeviceCaps(hdc, HORZRES)
and pageHeight = GetDeviceCaps(hdc, VERTRES)

We now need to find a font to use.  We want a 10 point Courier font which we obtain using CreateFont.  This is a fixed width font which simplifies calculating the page width.

val charHeight = ~10 * GetDeviceCaps(hdc, LOGPIXELSY) div 72;
val hFont = CreateFont{height=charHeight, width=0, escapement=0, orientation=0,
       weight=FW_DONTCARE, italic=false, underline=false, strikeOut=false,
       charSet=ANSI_CHARSET, outputPrecision=OUT_DEFAULT_PRECIS,
       clipPrecision=CLIP_DEFAULT_PRECIS, quality=DEFAULT_QUALITY,
       pitch=FIXED_PITCH, family=FF_MODERN, faceName="Courier"}

The character height calculation looks odd, giving us a negative value, but is a pecularity of CreateFont.  All the other arguments are fairly obvious. 

val oldFont = SelectObject(hdc, hFont);

val textMetric = GetTextMetrics hdc;

We now select this as the font to use.  SelectObject can be used for various other sorts of object, such as pens, brushes and bitmaps.  Whenever you select in a particularly kind of object the previous object of that kind is returned as the result.   It's generally good practice to select it back before you finish with a device context.  Having selected this font we then call GetTextMetrics to find the width of the font.   From this and the page width we can compute the number of characters on a line.    The height of the page divided by the height of a character gives us the number of characters on a page.  We are now ready to print a page.

We get the text from the edit window using GetWindowText.   The print dialogue gives the user the option of printing the currently selected area of the window rather than the whole file or a range of pages.  To find the selection we send the edit window an EM_GETSEL message.

We prepare the page by setting the colours and filling the page with white.  These are likely to be the defaults anyway but there's no harm in making sure.

val white = RGB{red=255, blue=255, green=255}
val black = RGB{red=0, blue=0, green = 0}
val pageRect = {top=0, left=0, bottom=pageHeight, right=pageWidth}

SetBkColor(hdc, white);
SetTextColor(hdc, black);
ExtTextOut(hdc, {x=0, y=0}, [ETO_OPAQUE], SOME pageRect, "", []);

ExtTextOut is one of the ways of drawing text but it is also a convenient way of filling an area with a colour.  The ETO_OPAQUE option causes the rectangle to be filled with the background colour.

Actually drawing each line is done using the TabbedTextOut function.  We extract a line from the document and draw it .

TabbedTextOut(hdc, {x=0, y= lineNo * #height textMetric},  thisLine, [], 0);

Each line is drawn beneath the previous one until the page is full or we have drawn all the text.

All this is the same whether we are printing a file or drawing to the screen.   When printing, though there are a few extra function calls we need.  Before each page we call StartPage and after the page we call EndPage.  We also need to bracket the whole document with calls to StartDoc and EndDoc.  When the document is complete we also need to restore the original font, delete the Courier font we created and delete the device context.

val jobID = StartDoc(hdc, {docName=fileName, output=NONE, dType=NONE});
EndDoc hdc;
SelectObject(hdc, oldFont);
DeleteObject hFont;
DeleteDC hdc;

We could use this code to draw to the screen if we were not using the edit window.   In that case we would process the WM_PAINT message and bracket the calls with BeginPaint and EndPaint.