Write Programs for the Windows API (C programming skills required)

 


1. How do I use the Default Push Button in a dialog?

The Default Push Button is the button considered to have been pressed if the user presses the ENTER key while the focus is on a control that is not a push button. It is always drawn with a thick highlighted border. Usually the default push button has the label OK or Close, and if the user enters information in a different part of the window and presses ENTER, it is as if they entered the information then clicked the default button. Windows notifies the program that the default push button has been pressed in the same way as it notifies other push buttons, by sending WM_COMMAND to the dialog with the control i/d equal to the default push button i/d (see MS KB Q67655).

The user interface of the Default Push Button works as follows. One push button in the dialog resource is defined as a DEFPUSHBUTTON control, and this button is drawn with a thick border when the dialog is shown. If the dialog contains several push buttons the default status moves between them as the user tabs through them. This behaviour is internal to Windows and is a way of showing that pressing Enter on a push button overrides the default button. When the focus moves away from the push buttons the original DEFPUSHBUTTON is shown as the default again. Thus the default button only moves when the focus moves among other push buttons. By using Spy you can see that the default status really does change as the focus moves - the original DEFPUSHBUTTON turns into an ordinary BS_PUSHBUTTON while the button with the focus turns into a BS_DEFPUSHBUTTON. When the focus moves away from the push buttons, Windows needs a way of knowing which was the original DEFPUSHBUTTON. The DM_GETDEFID message is used for this purpose.

For default push buttons to work properly, the following things need to happen:

  1. The dialog resource should contain only one DEFPUSHBUTTON. The push button control i/d does not matter and it does not have to be IDOK or IDCLOSE. There may be any number of non-default push buttons.
  2. The dialog's message loop must call IsDialogMessage, which converts the ENTER key into a default button press (WM_COMMAND + the button i/d), and handles the movement of the default status when the focus moves between push buttons. Modal dialogs created with DialogBox / EndDialog do this automatically. Modeless dialogs created with CreateDialog / DestroyWindow or custom dialogs created with CreateWindow / DestroyWindow must include a call to IsDialogMessage in their message loop.
  3. The dialog's window procedure must respond to the DM_GETDEFID message in order to return the control i/d of the default push button. Dialogs created with DialogBox / CreateDialog have a window procedure internal to Windows which does this automatically. If you have Visual C/C++ v1.5, look at the source code of the internal Windows dialog manager in \samples\defprocs\defdlg.c and note there is a case statement that responds to DM_GETDEFID. Custom dialogs created with CreateWindow have a harder job because they have to store the default push button i/d somewhere, probably in the extra window words or as a global variable. They can't find the default push button by enumerating the controls since when the focus moves among push buttons the original default button may have been changed into a non-default button. The only way of responding reliably to DM_GETDEFID in a custom dialog is to find the default button when the dialog is created and store the identifier somewhere.

If item (2) above is not attended to, the symptom is the default button does not change with the focus if you tab between several push buttons. If item (3) above is not done, then although the default button may be displayed properly when the dialog is created, if you tab around to the default push button it will lose its default status and not regain it when you tab off the button.

2. Is there an LB_SETTEXT message?

This is a technical note, of interest only to C programmers using the Windows API.

In the Windows API there is an LB_GETTEXT message which gets the text of a listbox item given a zero-based index. But there is no equivalent message LB_SETTEXT which sets the text of a listbox item at a given index. This is puzzling because in Visual Basic there is a List(i) property of the Listbox control object which is used to get and set the listbox text. Mystr = lb.List(i) corresponds to the LB_GETTEXT message but we do not know what corresponds to the code lb.List(i) = Mystr. In other words, how does Visual Basic set the listbox text? Possibly by using an undocumented Windows message, which would be known to the VB developers.

As an alternative, LB_SETTEXT can be simulated by inserting and deleting the listbox item. Here's some sample code:

  lTotal = SendMessage(hControl, LB_GETCOUNT, 0, 0L);
  if ((lIndex >= 0) && (lIndex < lTotal))
  {
    /* Find what the current selection is */
    lCurSel = SendMessage(hControl, LB_GETCURSEL, (WPARAM)0, 0L);
    /* Insert new string at List(i) */
    SendMessage(hControl, LB_INSERTSTRING, (WPARAM)(lIndex), (LPARAM)szItem);
    /* Delete string at List(i+1) */
    SendMessage(hControl, LB_DELETESTRING, (WPARAM)(lIndex+1), 0L);
    /* Restore the current selection */
    SendMessage(hControl, LB_SETCURSEL, (WPARAM)lCurSel, 0L);
  }

3. How do I create file associations?

Sometimes it's useful to associate file extensions with a particular application and an icon. This enables Windows Explorer to show the correct icon and description in the directory listings, and allows a right-click to open the file. File associations can be done at the API level without using COM or ActiveX. The file's extension is used as the basis of the association. Associations are defined by registry settings. Before defining the file type, you need to know the following:

  • File extension. This should be 3 or more characters. It can be less, although this is not usual.
  • File icon. This can be an ICO file, or it can be obtained from a DLL or possibly an EXE file. For example "%SystemRoot%\icons.dll,-3" obtains the third icon resource from the DLL.
  • File description. This appears in Windows Explorer as the description of the file type. Without a description, Explorer simply follows the extension by the word "file", for example "XYZ file".
  • Application that opens the file. This specifies a command line that opens the file. For example "c:\mydir\myapp.exe" "%1". The application needs to be coded so that it accepts files on the command line.
  • File identifier. This is a unique identifier, also known as the File type. It is used internally by the registry for storing the file association, and is the registry key under which most of the association information is held. The file identifier also makes it possible for multiple extensions to be treated in the same way. For example both ".txt" and ".text" could have the same file identifier (txtfile).

This example shows the new registry settings for an application using files with the extension ".xyz":

  HKCR\.xyz\(default) = XyzFiles
  HKCR\XyzFiles\(default) = "XYZ program file"
  HKCR\XyzFiles\DefaultIcon\(default) = "\mydir\xyz.ico"
  HKCR\XyzFiles\shell\open\command\(default) = "\mydir\xyzapp.exe" "%1"
  HKCR\XyzFiles\shell\print\command\(default) = "\mydir\xyzapp.exe" -p "%1"

"HKCR" is an abbreviation for HKEY_CLASSES_ROOT. The first setting maps the file extension onto the file identifier (XyzFiles). The second setting is the description of the file association. The third setting sets up the icon shown by Windows explorer. The last two settings provide "open" and "print" choices on the properties menu (the menu that is shown when you right-click a file). These command lines can be whatever you want, but the application must be coded to recognize them and act accordingly. The print option should be coded so it prints quietly without starting the main menu, if possible. It should also be possible to make up new actions in addition to "open" and "print".

4. How do I create a single instance program using the Windows API?

Most Windows programs are multiple instance - if you start the program twice over, you will have two separate copies of the program running. But in some cases you want programs to be single instance. This means if you start the program a second time, the second copy refuses to start and returns execution to the first copy.

To do this under the Windows API requires a named mutex semaphore using the base name of the EXE file. The named mutex can only be owned by one application at a time, so if another copy of the same application starts up, it detects the named mutex and stops running. For example:

  HANDLE hMutex;
  DWORD  dwReturn;
  BOOL   fGotMutex;
  char   szAppname[_MAX_PATH];
  char   szFname[_MAX_FNAME];

  GetModuleFileName(
    GetSystemInstance(),  // application instance
    szAppname,            // application name
    sizeof(szAppname));   // buffer size
  splitpath(szAppname, NULL, NULL, szFname, NULL);
  strupr(szFname);
  hMutex = CreateMutex(
    NULL,                 // no security attribute
    FALSE,                // don't acquire initial ownership
    szFname);             // use base name as mutex name
  dwReturn = WaitForSingleObject(
    hMutex,               // handle to wait for
    1000);                // timeout in mS
  fGotMutex = (dwReturn == WAIT_OBJECT_0) || (dwReturn == WAIT_ABANDONED);
  if (fGotMutex)
  {
    // At this point we own the mutex and we are the only
    // instance of this program to be running.
    // ... application code ...
    // Release mutex before we finish
    ReleaseMutex(hMutex);
  }
  CloseHandle(hMutex);

The call to GetModuleFilename gets the full name and path of the EXE file (eg "c:\apps\myapp.exe"). GetSystemInstance is assumed to be a helper function that returns the current application instance. The calls to splitpath and strupr get the base name of the EXE file (eg MYAPP) which is used as the name of the mutex. Mutex names are available globally to all applications, also they are case sensitive which is why we convert them to upper case. A mutex can be owned by one application only, which is the basis of this sample code which creates a named mutex for the application.

The call to CreateMutex returns a handle to the mutex szFname if it already exists, or creates a new mutex named szFname if it did not exist. The FALSE parameter means CreateMutex does not attempt to own the mutex, all it is doing is returning a handle to the mutex and creating it if necessary. Although a mutex can only have one owner, there can be any number of handles to the mutex.

The call to WaitForSingleObject tries to get ownership of the mutex. An owned mutex (known as an unsignaled mutex) is owned by a single application and cannot be owned by any other application, but an unowned mutex (signaled mutex) is available and ownership can be gained by any application. The terms "unsignaled" and "signaled" are used in the SDK documentation but are more confusing than owned and unowned. WaitForSingleObject attempts to get ownership of the mutex identified by hMutex and always returns within the time-out period. It is possible to specify an INFINITE timeout which means WaitForSingleObject blocks until the mutex becomes available. It INFINITE is not used, WaitForSingleObject returns values as follows:

  • WAIT_FAILED - if hMutex is invalid. WaitForSingleObject will not own the mutex on return.
  • WAIT_TIMEOUT - if WaitForSingleObject waited for the specified period but the mutex is owned elsewhere and did not become available during the wait. WaitForSingleObject will not own the mutex on return.
  • WAIT_ABANDONED - if the mutex was owned elsewhere, but the owning thread terminated without releasing the mutex. In this case Windows automatically releases the mutex when the owner terminates, and WaitForSingleObject owns the mutex on return.
  • WAIT_OBJECT_0 - if the mutex was unowned when WaitForSingleObject was called. WaitForSingleObject owns the mutex on return.

As a result of these return codes, the return codes WAIT_ABANDONED or WAIT_OBJECT_0 mean the mutex became available, and ownership was acquired by WaitForSingleObject on behalf of this application.

The comment marked "...application code..." represents the rest of the application, and all code called from here is single instance code. Any other applications of the same base name using WaitForSingleObject to gain ownership of the mutex would fail with the return code WAIT_TIMEOUT.

The call to ReleaseMutex releases the mutex which changes it back to the unowned (signaled) state. At this point it could be successfully acquired by another application using the same mutex name. CloseHandle is called to close the hMutex handle. It is not essential to call CloseHandle if the application is about to terminate, because Windows automatically closes the handles on application clean-up. Also Windows will automatically destroy the mutex if there are no handles to it.

Therefore it would still be correct (but sloppy) to omit calls to ReleaseMutex and CloseHandle above. If another application was blocked in a WaitForSingleObject call waiting for the named mutex to become available, and this application quit without calling ReleaseMutex and CloseHandle, the mutex would become available, and the other WaitForSingleObject call would acquire the mutex and return WAIT_ABANDONED. But it is neater to call ReleaseMutex and CloseHandle as above.

The information that follows is kept for historical interest only. 32 bit Windows has been available from 1995, and 16 bit code is obsolete.

In 16 bit Windows it is possible to make a single instance program by checking the hPrevInstance parameter of WinMain, which is non-NULL if there is an instance of the same program (same EXE file) running. This technique does not work under Win32, which always passes a NULL parameter for hPrevInstance.

5. How do I handle missing DLLs after installing software (missing MSVCIRT.DLL file)?

In some cases, if a program A is installed, then adding and later removing a program B can cause A to stop working properly afterwards. This is usually because B replaces some DLLs required by A. For example Microsoft Visual C/C++ Developer Environment had previously been installed and was working reliably, but after installing some software called ResEdit, Visual C/C++ failed to load, giving the error message:

  "A required DLL file, MSVCIRT.DLL was not found"

After reinstalling Visual C/C++ from the CD, the same error happened. Even after using Add/Remove programs to remove all traces of Visual C/C++, then reinstalling it, the error still happened. The previously installed program was called ResEdit, and it had an installation program based on Install Shield. The MSVCIRT.DLL was not found anywhere, it is not included in the standard distribution of Windows (ie not anywhere in the list of Windows files), it was not included with ResEdit, nor is on the Visual C/C++ CD. Why did Visual C/C++ want this DLL which is nowhere to be found, when it worked fine before?

It turns out Visual C/C++ uses MSVCRT40.DLL, which is on the Visual C/C++ CD, although it is not part of the Windows disk set. By grepping most files on the hard disk for "msvcirt" it turned out MSVCRT40.DLL makes calls to routines in the missing MSVCIRT.DLL, and that the installation of ResEdit replaced the original old MSVCRT40.DLL (which does not want MSVCIRT.DLL) with a newer version of MSVCRT40.DLL that does. Replacing the "new" version of MSVCRT40.DLL with the "old" version from the Visual C/C++ CD fixed the problem and allows Visual C/C++ to run properly.

After checking MSDN it appears the MSVCRT40.DLL file should not be distributed by applications, as to do so can break programs written with Visual C/C++ (and presumably Visual C/C++ itself). That explains what happened in this case. What ResEdit did wrong was to replace the MSVCRT40.DLL file and not provide the MSVCIRT.DLL file. Refer to the article: "FIX: Wrong Version of MSVCRT40.DLL(Forwarder DLL) in Windows 95", Article ID: Q154591.

6. What is the Command Line Length Limit under Windows?

The Command Line Length Limit is the longest possible command line that may be passed to programs under Windows. It varies, depending on the context in which the command line is being used. Here are the limits:

  • CreateProcess() Windows API function The maximum length is 32767 characters, based on fields in the UNICODE_STRING structure.
  • Cmd.exe When using the Windows NT / 2000 / XP command processor, the maximum length is 8192 characters.
  • ShellExecute() Windows API function The maximum length is 2048 characters as given by the INTERNET_MAX_URL_LENGTH defined constant.
  • ShellExecute() Windows 95 API function Under Windows 95 the maximum length is 255 characters as defined by MAX_PATH.
  • MS-DOS command line When using MS-DOS or the command.com MS-DOS boxes under Windows 95, Windows 98 or Windows ME, the maximum command line length is 127 characters.
  • Environment variables There is also a maximum size allowed for Environment variables under cmd.exe - this is 32767 characters, including the names of the variables. Under MS-DOS the maximum size allowed for environment variables can be set to a lesser limit using the /E command line option.

This information is based on The Old New Thing blog which is a very useful source of low-level information under Windows.