Win16 for Fun and (Probably No) Profit

Introduction

Ever wondered how GUI applications were done back in the early 1990s? I’ve had a general idea of how it worked, but nothing specific until I went and did it. It’s honestly not as hard as I was expecting, but there was some unexpected difficulty in working with old tools. In the end, I have a (very) basic application to show for it and an appreciation of more modern GUI development environments.

Motivation and Application

The web, of course.

Well, less specifically being disappointed with the state of the web, (who isn’t?) but also a curiosity about the Windows API, especially in a primordial form like this. I’ve heard that of its contemporaries, (such as X11, the Macintosh Toolbox, OS/2 Presentation Manager, etc.) the Windows API was easiest to program for. I have a passing knowledge of how Win32 works, (due to using Windows Forms) so I was curious how much it would take to get not only an application working, but one that did useful things. In addition, it might be fun to see how trivial a port would be to other Windows-shaped platforms, like Win32, Windows 1.0 or 2.0, or the various Unix and OS/2 porting layers.

Another point is the portability of C. This is 16-bit x86 code for a completely non-POSIX environment with a funny memory model, yet it wasn’t too difficult to apply my C knowledge. C is often criticized for the tortured language the standard is written in, full of undefined behavior and little assumptions about its host. Systems like 16-bit Windows and many others are the reason why. For better or for worse, the dominance of Windows means that until recently, Microsoft gave no concessions to programs written for POSIX, even if it was compliant with the C and C++ standards. Don’t assume your program will run on your average 64-bit x86 Unix system – your code could end up on something that implements C11, but has EBCDIC string constants and very wide pointers, for example.

For this article, I’ll make a display calculator. My friend made this in JavaScript a few years ago for comparing high-density displays, something this application probably won’t handle well. I brought it up as a good candidate for remaking in older tools like Visual Basic, for targeting as many platforms as possible.1 Somewhere along the way, this became C for me.

The program I made with these old tools.

Environment and Creation

I used Visual C++ 1.x for this, but you could do anything like this with anything that’d target old Windows with C, though Visual Basic would probably be quite a different experience than writing out resource files. Visual C++ is easier than the older tools, but it compared to modern Visual Studio, it’s very primitive. I set up a virtual machine, and we have the IDE running. Now what?

I have uploaded the application’s source on GitHub for your perusal.

First, we’ll create a project somewhere. Do that from the Project menu. Next, we’ll have to add a C file for our program’s source code. Once you add that to the project, you can click the first button of the toolbar, and it’ll show files managed directly by the project makefile, or incidental dependencies like header files. You might also notice a “.DEF” file created; this defines the linker settings. We won’t need to touch them, so leave it at default.

We’ll write the bare minimum of what it takes to get a message on the screen, using a message box. I’ll cover the event loop and its subtleties soon. For now, let’s see if it even works. Your C file should import <windows.h> and contain a function called WinMain. This is the entry point of the program, and it differs from your typical POSIX style entry point. It should return an integer, and the parameters are mostly the same as modern Win32:

We won’t be using any of these for a simple “hello world” program. Another thing to note is the “PASCAL” sprinkled liberally in a 16-bit Windows program. This defines the calling convention; 16-bit Windows uses Pascal instead of C calling conventions, so any functions you pass to the system should be marked as such.

The MessageBox API is pretty simple. It takes four parameters as well:

We’ll be clever and return the dialog result from the API as our function’s return value. Our program should look something like this, if you don’t mind K&R style functions:

#include <windows.h> /* Windows toolchains don’t mind including with quotes. */
int WinMain (hInst, hPrevInst, szCmdLine, nCmdShow)
    HANDLE hInst, hPrevInst;
    LPSTR szCmdLine; /* Leave nCmdShow as an integer default. */
{
    /* everyone talks to a dialog box */
    return MessageBox (NULL, “Hello world”, “My First Win16 App”, 0);
}

If you compile and run that, you should see a message show up. It does get trickier from here, but not too hard, if you can avoid the issues I ran into. It’s just mostly fatiguing writing everything by hand.

Hurdles and Tasks

So now we’ll cover the parts I ran into trying to make a useful application. To make anything beyond a test program, you’ll need documentation and good tools. If you’re used to modern Windows or POSIX tools though, you might be disappointed.

Visual C++

The Visual C++ documentation is not too bad, despite a small mistake in the API that caused me to scratch my head for a while. The help files are hypertext and work like you expect them to. Searching across multiple kinds of documentation is more difficult than it should be though.

The tooling on the other hand, isn’t as good. The Visual C++ IDE is your typical MDI arrangement of the time, that comes from Windows programs trying to ape how the Macintosh window manager worked. Your mileage may vary on how good that is; for me, it meant constantly fiddling with where the source files were on screen relative to the output and debug windows.

Visual C++ 1.

The source editor is clumsy to work with and doesn’t really support anything beyond the basic Windows edit control function (which it reimplements, so newer features like scroll wheels won’t work on it) other than showing breakpoints. It also allows you to insert your cursor anywhere, even if the line ends elsewhere; this probably feels at home to users of DOS based screen editors.

The debugger has a simplistic UI but is otherwise passable; feature wise it won’t compare to Turbo Debugger or CodeView, but it’s integrated with the IDE. It won’t save you if your program divides by zero though, which is something you probably want to debug.

APIs and Resources

If you want to create an application that does more than display a message box, you’re probably going to want to create a window and controls (which use window handles internally) to display. Unfortunately, this is a bit involved to do by hand, so I’ll make it easy and use a dialog, as my application will have a lot of controls. Unlike a normal window, dialogs exist as resources.

In Windows, a resource script is compiled into the application, which provides stuff like dialogs, icons, and string templates inside of the program image, which are loaded as needed. This is a somewhat shallow clone of the classic Mac OS’ resource manager, but it works. You could write a resource script by hand, but I’m still going to do it the easy way and use a resource editor. Visual C++ of this vintage comes with App Studio, which can do some C++ class related wizardly, but for now, we’ll just stick with simple resource scripts and C.

App Studio, the VC++ 1 resource editor.

You can lay out the dialog and menus as you expect. The “static IDs” it provides will be generated into a header file (called resource.h) that you include, so you can reference the controls in context as needed. I recommend adding the resource script to the makefile so it’ll do this for you.

Once you include the header file, the static IDs become macros, which will expand into a numeric ID. You can pass these to APIs as needed. One mistake the API seems to make is that CreateDialogBox takes a string for template name, whereas I had to give it a static ID, despite the compiler warnings.

Once you create a window, you need an event loop to process messages for the window and its children. Your event loop is given a chance to handle messages to define your application’s custom behaviour, then you can pass it onto the “default” window procedure as needed, which will do the expected Windows behaviours. Dialogs have the same requirements, but their event loop is slightly different. Since the Windows event loop hasn’t really changed in Win32, I’ll only cover the quirks of the dialog event loop, since that’s what gave me trouble earlier on.

Before you can pass functions to the Windows API, you need to call a special function that makes a trampoline, because of x86 memory management quirks. Once you create the dialog box and start the event loop, you’ll notice a simple event loop like this has difficulty with some events:

while (GetMessage (&msg, NULL, 0, 0)) { /* event loop for dialog or window */
    TranslateMessage (&msg);
    DispatchMessage (&msg);
}

Tabbing around will just cause the computer to beep at you instead of focusing on another control. This is because of the way tab stops and accelerators work in dialogs over normal windows. Another function must be called to filter messages in the loop, which will turn them into the expected events. Unfortunately, this seems to have the side effect of filtering out some messages you may want to use; you’ll have to do your own pre-processing in the event loop as a result. The event loop for processing dialog messages looks like this as a result:

while (GetMessage (&msg, NULL, 0, 0)) { /* definitely a dialog box */
    if (!IsDialogMessage (hwnd, &msg)) {
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    }
}

I had difficulty with this, due to accidentally forgetting to set my handle variables. Unfortunately, it’s hard to see legitimate compiler warnings through deficiencies in the Windows header file. The compiler crowding out legitimate warnings also didn’t help was I using C library math functions and forgot to import the header for them, so the compiler “helpfully” assumed integer for everything.

Ideas

I have some ideas for this, that I might do or write about in the future:

  1. Fix up the styles and version resources, so it doesn’t look so bad on Windows 95.

  2. Port it to Win32; it’s a somewhat trivial application, but how difficult could it be? Also see how many supported NT/CE architectures could be targeted.

  3. Port it to Winelib or some other Windows API compatible layer.

  4. Port it back to Windows 2.0. The API subset is very small here, and should be trivial to do, bar tooling difficulty.

Conclusion

Learning Win16 programming has given me a better perspective on and appreciation for modern development tools. You don’t realize how much is done for you until you’re left doing it yourself. One can see why environments like Visual Basic became popular.


  1. He thought Excel would probably be a better environment, in retrospect.