Transmission Zero

Building Win16 GUI Applications in C



Introduction

This article explains how to write a fully featured Win16 GUI application in C. It’s based on another article from this site, entitled “Building Win32 GUI Applications with MinGW”, and is basically a port of that application from Win32 to Win16 (if you came here by mistake looking for Win32 development in C, I’d recommend reading that article). You may be asking, “Why?”. It’s a fair question, because 16 bit Windows is long obsolete, and even if you are doing serious development for Win16 it’s most likely to be supporting a legacy application rather than developing new software. But you’re reading this article, so I can only assume that like me, you have a keen interest in 16 bit Windows, or you just want to understand how some of the crazy conventions in Win32 came from the need to be backwards compatible with Win16.

To avoid any possible confusion, when we say “Win16” we are referring to the original 16 bit Windows API, where 32 bit addresses are made up of a segment (in real mode) or selector (in protected mode) combined with an offset, and all applications are cooperatively multitasked in a single address space. This API is present from Windows 1.0 to Windows 3.11, and for backwards compatibility reasons is also present from Windows NT 3.1 to current x86 versions of Windows. Windows 95 to Windows ME also support the Win16 API, partly for reasons of backwards compatibility, but also because much of the 32 bit user mode API code on these versions of Windows do little more than call the 16 bit API equivalent to carry out their tasks!

Application Features

The following are the features which the Win16 application should demonstrate:

It will be targeted towards Windows 3.xx, and additionally runs on Windows 9x (Windows 95 to Windows ME) and NT based versions of Windows which have the WoW subsystem (Windows NT 3.1 to the x86 version of Windows 8.1 and beyond). It doesn’t support Windows 1.xx or Windows 2.xx as targeting these operating systems poses a number of difficulties, particularly the lack of freely available tools for building the applications. I have included a section in this article about targeting Windows 1, and the code download includes a Windows 1 version of the application.

The Application’s WinMain Procedure

The following is the application’s WinMain procedure. If you’re familiar with Win32 programming, you’ll notice that it is almost identical to a typical Win32 program—this is no coincidence, as Win32 was designed with maximum Win16 compatibility in mind.

/* Global instance handle */
HINSTANCE g_hInstance = NULL;

/* Our application entry point */
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  HWND hWnd;
  HACCEL hAccelerators;
  MSG msg;

  /* Assign global HINSTANCE */
  g_hInstance = hInstance;

  /* Register our main window class */
  if (! hPrevInstance)
  {
    if (! RegisterMainWindowClass())
    {
      MessageBox(NULL, "Error registering main window class.", "Error", MB_ICONHAND | MB_OK);
      return 0;
    }
  }

  /* Create our main window */
  if (! (hWnd = CreateMainWindow()))
  {
    MessageBox(NULL, "Error creating main window.", "Error", MB_ICONHAND | MB_OK);
    return 0;
  }

  /* Load accelerators */
  hAccelerators = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR));

  /* Show main window and force a paint */
  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  /* Main message loop */
  while(GetMessage(&msg, NULL, 0, 0) > 0)
  {
    if (! TranslateAccelerator(msg.hwnd, hAccelerators, &msg))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }

  return (int) msg.wParam;
}

The differences between this and a typical Win32 application are:

Although it’s not relevant in this particular code snippet, it’s important to remember that whilst in Win32 an “HMODULE” is the same as an “HINSTANCE”, this is not the case in Win16. When you are calling functions which take an instance handle as an argument, check that you’re actually passing an instance handle!

The Main Window

The main window is created by registering a window class and then creating an instance of it. This is much the same as in Win32, except that we are restricted to using a “WNDCLASS” structure and calling “RegisterClass()” rather than “WNDCLASSEX” and “RegisterClassEx()”. This is because extended window classes did not exist prior to Windows 95. Likewise “LoadImage()” did not exist in Win16, so we load icons and cursors with “LoadIcon()” and “LoadCursor()”.

/* Main window class and title */
static const char MainWndClass[] = "Win16 Example Application";

/* Register a class for our main window */
BOOL RegisterMainWindowClass()
{
  WNDCLASS wc = {0};

  wc.lpfnWndProc   = &MainWndProc;
  wc.hInstance     = g_hInstance;
  wc.hIcon         = LoadIcon(g_hInstance, MAKEINTRESOURCE(IDI_APPICON));
  wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
  wc.lpszMenuName  = MAKEINTRESOURCE(IDR_MAINMENU);
  wc.lpszClassName = MainWndClass;

  return (RegisterClass(&wc)) ? TRUE : FALSE;
}

/* Create an instance of our main window */
HWND CreateMainWindow()
{
  HWND hWnd;
  HMENU hSysMenu;

  hWnd = CreateWindowEx(0, MainWndClass, MainWndClass, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                        320, 200, NULL, NULL, g_hInstance, NULL);

  if (hWnd)
  {
    /* Add "about" to the system menu */
    hSysMenu = GetSystemMenu(hWnd, FALSE);
    InsertMenu(hSysMenu, 5, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
    InsertMenu(hSysMenu, 6, MF_BYPOSITION, (UINT) ID_HELP_ABOUT, "About");
  }

  return hWnd;
}

Unlike extended window classes, extended window styles did exist in Windows 3.xx and we have the option of using “CreateWindowEx()”.

After we create the window, we add a separator and “about” item to the system menu. We cover this in detail in the “System Menu” section.

The Version Information Resource

It’s possible to add information into the executable, containing information such as the version, author, copyright, and description. This is done with a version information resource, and means File Manager or Windows Explorer are able to display this when looking at the executable’s properties page:

[File Manager: An application properties page showing version and copyright information.]
(if you look closely at the “last change” date you’ll notice that Program Manager is not fully Y2K compliant!)

Adding a version information resource is simple a case of defining it in your resource script:

/* Executable version information */
VS_VERSION_INFO    VERSIONINFO DISCARDABLE
FILEVERSION        1,0,0,0
PRODUCTVERSION     1,0,0,0
FILEFLAGSMASK      VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
  FILEFLAGS        VS_FF_DEBUG | VS_FF_PRERELEASE
#else
  FILEFLAGS        0
#endif
FILEOS             VOS_DOS_WINDOWS16
FILETYPE           VFT_APP
FILESUBTYPE        VFT2_UNKNOWN
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "080904E4"
    BEGIN
      VALUE "CompanyName", "Transmission Zero\0"
      VALUE "FileDescription", "Win16 Example application\0"
      VALUE "FileVersion", "1.0.0.0\0"
      VALUE "InternalName", "Win16App\0"
      VALUE "LegalCopyright", "©2014 Transmission Zero\0"
      VALUE "OriginalFilename", "Win16App.exe\0"
      VALUE "ProductName", "Win16 Test application\0"
      VALUE "ProductVersion", "1.0.0.0\0"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x809, 1252
  END
END

I won’t cover the format of the version resource in detail as it’s covered very well in the MSDN, but most of it is self-explanatory (I’d suggest reading the MSDN article anyway, particularly if you need to set the language to anything other than British English). Notice that the strings are double null terminated by using an embedded null character. I don’t know why this is required in Win16, but it’s the way it has always been done in the Microsoft SDK sample applications. Without the extra null character, Windows Explorer will chop off the last character of each string when you view the details!

The Main Menu

The main menu allows exiting the application or showing the about dialog. It has been defined in a resource script rather than creating it programmatically, and is created in exactly the same way as a Win32 menu would be.

/* Our main menu */
IDR_MAINMENU MENU DISCARDABLE
BEGIN
  POPUP "&File"
  BEGIN
    MENUITEM "E&xit",                  ID_FILE_EXIT
  END
  POPUP "&Help"
  BEGIN
    MENUITEM "&About",                 ID_HELP_ABOUT
  END
END
[Window with “File → Exit” item highlighted]

As with Win32, a “WM_COMMAND” message is sent to your window procedure when a menu item is selected. There is however an important difference between Win16 and Win32 when it comes to the parameters for this message. This is because in Win16 the “wParam” is a WORD and “lParam” is a DWORD, whereas in Win32 they are both DWORDs. This means that on Win16 the “lParam” can contain a packed HANDLE plus another WORD variable, whereas on Win32 the “lParam” is only big enough for a single HANDLE. Instead the WORD variable is packed into the high WORD of “wParam”. It’s best to check the API documentation when it comes to windows messages as there are subtle differences between Win16 and Win32. If you are using message crackers defined in “windowsx.h” then you can avoid these problems and have code which compiles for Win16 and Win32 without any changes.

/* Window procedure for our main window */
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
    case WM_COMMAND:
    {
      WORD id = wParam;

      switch (id)
      {
        case ID_HELP_ABOUT:
        {
          ShowAboutDialog(hWnd);
          break;
        }

        case ID_FILE_EXIT:
        {
          DestroyWindow(hWnd);
          break;
        }

        default:
          return DefWindowProc(hWnd, msg, wParam, lParam);
      }

      break;
    }

    /* Other message handlers omitted from code snippet */

    default:
      return DefWindowProc(hWnd, msg, wParam, lParam);
  }

  return 0;
}

In this case, we use the wParam to determine which menu item was selected, and take the appropriate action.

About Dialog

The about dialog box is defined in the resource script, and done so in the same way as a Win32 dialog. The only difference between Win16 and Win32 is that Win32 adds some new properties and styles which aren’t supported in Win16, and we are using a “DIALOG” resource type rather than “DIALOGEX”.

/* Our "about" dialog */
IDD_ABOUTDIALOG DIALOG DISCARDABLE 0, 0, 147, 67
STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "About"
FONT 8, "MS Sans Serif"
BEGIN
  ICON             IDI_APPICON,IDC_STATIC,7,7,20,20
  LTEXT            "Win16 Example application.",IDC_STATIC,34,7,91,8
  LTEXT            "©2014 Transmission Zero",IDC_STATIC,34,17,86,8
  DEFPUSHBUTTON    "OK",IDOK,90,46,50,14,WS_GROUP
END
[About dialog with icon, copyright information, and an “Ok” button.]

The dialog procedure is constructed in the same way as a Win32 dialog. The return type of the procedure is “BOOL”, whereas in Win32 it would be declared as “INT_PTR” if the code is to be compatible with both Win32 and Win64.

/* Dialog procedure for our "about" dialog */
BOOL CALLBACK AboutDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  switch (uMsg)
  {
    case WM_COMMAND:
    {
      WORD id = wParam;

      switch (id)
      {
        case IDOK:
        case IDCANCEL:
        {
          EndDialog(hwndDlg, id);
          return TRUE;
        }
      }
      break;
    }

    case WM_INITDIALOG:
      return TRUE;
  }

  return FALSE;
}

The dialog box can be shown using one of the dialog functions such as “DialogBox()”:

/* Show our "about" dialog */
void ShowAboutDialog(HWND owner)
{
  /* Create dialog callback thunk */
  FARPROC aboutProc = MakeProcInstance(&AboutDialogProc, g_hInstance);

  DialogBox(g_hInstance, MAKEINTRESOURCE(IDD_ABOUTDIALOG), owner, aboutProc);

  /* Free dialog callback thunk */
  FreeProcInstance(aboutProc);
}

You might be wondering why the “DialogBox()” call has a variable of type “FARPROC” passed to it as the fourth argument. Whilst in Win32 you simply pass the address of your dialog procedure to the dialog creation function, in Win16 you must pass a thunk which has type “FARPROC”, returned by a call to the “MakeProcInstance()” function. In fact, this must be done for all callback functions except for window procedures. It doesn’t matter where in your application you call the “MakeProcInstance()” function, but you must remember that each call must be accompanied by a call to “FreeProcInstance()” to avoid leaking memory.

Keyboard Accelerators

By pressing “Alt + A”, the “about” dialog for the application will be launched. This is done using a keyboard accelerator which has been defined in the resource script:

/* Our accelerators */
IDR_ACCELERATOR ACCELERATORS DISCARDABLE
BEGIN
  "A",             ID_HELP_ABOUT,      VIRTKEY, ALT, NOINVERT
END

Again, this is completely identical to how keyboard accelerators are defined in Win32. We use the same ID for our accelerator as we use for our “help → about” menu item, which means we don’t have to write any additional message handling code. To get the accelerators working, you need only load the accelerators and translate the accelerator messages in your message loop:

/* Load accelerators */
hAccelerators = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR));

/* Some non-accelerator intermediate code ommited */

/* Main message loop */
while(GetMessage(&msg, NULL, 0, 0) > 0)
{
  if (! TranslateAccelerator(msg.hwnd, hAccelerators, &msg))
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
}

System Menu

If you click on a minimized icon in Program Manager, you’ll see the system menu. In our application we have added an item to this menu to show the “about” dialog. This isn’t the best use of the system menu, but the intention is to demonstrate how it is done.

[Windows application showing system menu with “About” option.]

The system menu additions are done entirely in code, the same way as it is done in Win32.

hWnd = CreateWindowEx(0, MainWndClass, MainWndClass, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                      320, 200, NULL, NULL, g_hInstance, NULL);

if (hWnd)
{
  /* Add "about" to the system menu */
  hSysMenu = GetSystemMenu(hWnd, FALSE);
  InsertMenu(hSysMenu, 5, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
  InsertMenu(hSysMenu, 6, MF_BYPOSITION, (UINT) ID_HELP_ABOUT, "About");
}

The following is the message handler for the “WM_SYSCOMMAND” message, which is sent by the system menu.

/* Window procedure for our main window */
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
  switch (msg)
  {
    /* Other message handlers omitted from code snippet */

    /* Item from system menu has been invoked */
    case WM_SYSCOMMAND:
    {
      WORD id = wParam;

      switch (id)
      {
        case ID_HELP_ABOUT:
        {
          ShowAboutDialog(hWnd);
          break;
        }

        default:
          return DefWindowProc(hWnd, msg, wParam, lParam);
      }

      break;
    }

    /* Other message handlers omitted from code snippet */

    default:
      return DefWindowProc(hWnd, msg, wParam, lParam);
  }

  return 0;
}

The Module Definition File

The final component of our Win16 application is the module definition file. You may have used a module definition file when creating a Win32 DLL, but for Win32 executables it’s not common to use one. It is used to pass information to the linker when building the final executable, for example stack size, heap size, and exported functions. The following is our module definition file:

NAME           Win16App
DESCRIPTION    'Win16 Test Application'
STUB           'WINSTUB.EXE'
CODE           MOVEABLE PRELOAD DISCARDABLE
DATA           MOVEABLE PRELOAD MULTIPLE
HEAPSIZE       1024
STACKSIZE      4096
EXPORTS        MainWndProc
               AboutDialogProc

You can either double quote or single quote the strings, but single quotes result in a def file which works with both older and newer linkers. Double quotes only work in newer linkers.

The “DESCRIPTION” should be self-explanatory. The “STUB” is the MS-DOS stub for the application, which will be executed when the application is run from DOS. It can do anything you want it to do as long as it’s a valid MS-DOS executable, but here we are using the default “winstub.exe”, which displays the message “This application requires Microsoft Windows”.

The “CODE” and “DATA” statements define the attributes on the code and data segments in the executable. In this case, they are both moveable and preloaded, meaning the operating system loads them when the application starts and can move them about in memory as it sees fit. The code segment is marked as “DISCARDABLE”, meaning the operating system can remove it from memory, and later load it back from the executable image on disk. This would not work for the data segment, since its contents will change during the execution of the program. We mark the data segment as “MULTIPLE”, meaning that each instance of the application gets its own copy of the data, rather than being shared as would be the case in a DLL.

The “HEAPSIZE” and “STACKSIZE” determine the size of the heap and stack at runtime—remember that in Win16, the size of the stack, heap, and all static and global variables must fit within a single 64 KB segment.

Finally, the “EXPORTS” section marks our dialog procedure and window procedure as being exported—it is essential to export every callback function which will be called by Windows. Also note that any functions exported from the executable cannot be called from within the application, as the exported functions have a specific set of machine instructions in their prolog which allow them to be called from outside the application, and this fails when you call the functions from within the application.

Putting It All Together

Now we have all of the components of our application, we can compile the code and link it all into an executable. Assuming all of the code is contained in “myfile.c”, the resource script is named “resource.rc”, and module definition file is named “myapp.def”, to create the application “myapp.exe” with Microsoft’s Visual C++ compiler you would do the following:

cl16.exe /nologo /c /D NDEBUG /D WINVER=0x0300 /G3swf /Os /W3 /Zp /FPi87 myfile.c
rc16.exe /nologo /r resource.rc
link16.exe /nologo /align:16 myfile.obj,myapp.exe,,libw.lib slibcew.lib,myapp.def
rc16.exe /nologo /30 resource.res myapp.exe

The 16 bit visual C++ compilers can be downloaded for free as part of the Windows Server 2003 SP1 DDK (you can get it using the Server 2003 DDK ISO direct download link). You will find it has executables such as “link16.exe” and “link.exe”, which are identical except for their names. In this example I have used the “16” versions to avoid any possible confusion with the more common 32 bit tools—these also have for example a “link.exe”, which is not the same as the 16 bit “link.exe”. If you have an MSDN subscription, you can download Visual C++ 1.52 and use the tools which come with it. If you prefer, you can use a different C compiler as long as it comes with the necessary headers and libraries for Win16 development. One example is the Open Watcom compiler / IDE, which is also a free download. The download link in this article contains a hand written Makefile for Visual C++, a Visual C++ Makefile project, and project files for Open Watcom.

The application should run fine on any 32 bit version of Windows, so it’s not necessary to perform initial testing in Windows 3.x. Bear in mind that a Win16 app runs in a shared address space, so if there is a bug in your application then it can potentially crash other Win16 processes. This probably isn’t an issue unless you are running legacy applications, but if this is an issue and you’re running Windows NT, you can create a shortcut to the application, check the “Run In Separate Memory Space” option, and run the application from that shortcut. By doing this, the application gets its own NTVDM and WoW process, so the worst thing it can do is crash itself. Bear in mind that by running a 16 bit application in its own address space, it cannot share memory with any other applications.

[Windows application showing system menu with “About” option.]
The application running in Windows 2000, showing the system menu.

Windows 1.xx and 2.xx

I mentioned earlier in the article that the application does not run under Windows 1 or 2 because these operating systems pose some additional difficulties. It’s certainly not impossible, but here are a few hurdles you’ll need to overcome:

Whilst these are mostly fairly minor things, it all adds up when you are writing an application. In fact I would go so far as to say that the difference between a Windows 3 application and a Win32 application are tiny compared to the difference between a Windows 1 application and a Windows 3 application! For that reason I focussed on Windows 3.xx applications in this article, but you will find a Windows 1 version of the application in the code download.

Finally, something which I find very impressive is that the Windows 1 version of the application runs unmodified in both Windows 1.01 and the preview version of Windows 10 (the latest version of Windows at the time of writing). How many other operating systems can run applications written for a 30 year old operating system?

[Windows 1 application showing the “About” dialog on Windows 1.]
The Windows 1 application running on Windows 1.
[Windows 1 application showing the “About” dialog on the Windows 10 preview.]
The Windows 1 application running on the Windows 10 preview.

Download the Win16 Example Applications

You can download the Win16 Example Application from GitHub to build the example application from this article. You can either git clone the repository or download a Win16 Example Application release. You are free to use it for whatever purpose you see fit (see the “License.txt” for the full terms of use). Using the project files, you can easily build the application from this article, and use it as a basis for your own Windows applications.

If you’re looking to create an MDI application, you can download the Win16 MDI Application from GitHub (or download a Win16 MDI Application release).

If you’re looking to create a dialog based application, you can download the Win16 Dialog Application from GitHub (or download a Win16 Dialog Application release).

Finally, if you’re looking for the Windows 1 compatible version, you can download the Windows 1 Example Application from GitHub (or download a Windows 1 Example Application release).

The Full Story on MakeProcInstance and EXPORT

I mentioned that any function called from Windows, i.e. the window procedure and dialog procedure, must be exported from the executable using the module definition file. I also mentioned that pointers to all callback functions except window procedures must not be passed to Windows API functions, and instead you pass a thunk created by a call to “MakeProcInstance()”. This is because of the slightly strange architecture of Win16, which poses some challenges loading the correct value into the data segment register inside a far function which has been called from outside your application. By exporting a function, a certain set of machine instructions are placed in the prolog of the function which allow the correct value to be loaded into the data segment register. Note that I used the word “allow”, because on its own, exporting a function does not load the correct value into the register—this is what MakeProcInstance is for. When you call MakeProcInstance, Windows creates a thunk which sits between Windows and your function. It does little more than placing the correct value of the data segment (which can only be known at runtime) into a register, and then jumping to your function. Your exported function then just moves this value from that register into the segment register, and your function will reference the correct data.

Although the above does what needs to be done, it’s a lot more complicated than it needs to be. It’s possible to avoid exports and MakeProcInstance entirely, at the same time allowing your callback functions to be called from within your own code without causing a crash. For a full and technical description on this, have a look at the FixDS readme from Michael Geary.

Problems?

This article has been put together after studying Win16 articles on the internet. Whilst I have done my best to ensure that all information is correct, due to the fact that Win16 documentation is hard to come by, it’s possible that there could be some mistakes. If you have any difficulties getting your Win16 application up and running, or you spot any mistakes in this article, please get in contact and I’ll try to help.