Related Links

Q183107 HOWTO: Detect When the Cursor Leaves the Window

Application Window Modes

What is an Application Mode

I have culled some definitions from The Microsoft Windows User Experience.

modal Restrictive or limited interaction due to operating in a mode. Modal often describes a secondary window that restricts a user’s interaction with other windows. A secondary window can be modal with respect to its primary window or to the entire system. Compare modeless.

mode A particular state of interaction, often exclusive in some way to other forms of interaction.

modeless Non-restrictive or non-limited interaction. Modeless often describes a secondary window that does not restrict a user’s interaction with other windows. Compare modal.

Common examples of application modes:

Some of the API functions that achieve these modes are: DoDragDrop, DialogBox and TrackPopupMenu. What is important to note here is two things:

  1. All the API functions listed return control to the appliation only when the mode has either been canceled, or completed.
  2. In addition, it is not possible to interact with the application window without canceling the mode.

A Modal function is a function that causes an application window to enter some mode, processes the users input for this mode, restores the applications input state, and only then does it return.

Implementing a Modal Function

The phrase modal window is used to describe a message or dialog box, that pops up over an applications frame window. While the modal window is present the application window cannot be used. The system provides the following functions to allow the creation of modal windows: MessageBox, DialogBox, DialogBoxParam, DialogBoxIndirect and DialogBoxParamIndirect.


Recipe #1 - A Modal Window

There are two aspects to the implementation of a modal window. First, the parent window must be disabled, so as to prevent user interaction. Second, the modal message loop - a modal window is implemented with its own message loop. A typical implementation might look something like this:


BOOL fDone;
INT  nResult;

int RunModalWindow(
  HWND hwndDialog,
  HWND hwndParent)
{
  if(hwndParent != NULL)
    EnableWindow(hwndParent,FALSE);

  MSG msg;
  
  for(fDone=FALSE;fDone;WaitMessage())
  {
    while(PeekMessage(&msg,0,0,0,PM_REMOVE))
    {
      if(msg.message == WM_QUIT)
      {
        fDone = TRUE;
        PostMessage(NULL,WM_QUIT,0,0);
        break;
      }

      if(!IsDialogMessage(hwndDlg,&msg))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
    }
  }

  if(hwndParent != NULL)
    EnableWindow(hwndParent,TRUE);

  DestroyWindow(hwndDialog);

  return nResult;
}

Note some important features of the loop:
The modal message loop cannot be terminated by calling PostQuitMessage as that function is used to terminate a UI thread. All windows owned by the thread must be destroyed. Thus, if WM_QUIT is picked up, it must be manually reposted.

The modal window is properly closed by calling a special function that sets a flag associated with the window. In the example code termination flag is fDone, and would usually be stored in a WindowLong. Usually a modal window also allows a return code - the example code uses nResult for this purpose.

Also note the order of the call to destroy the dialog and to enable the parent. The parent must be enabled before the dialog is destroyed, as disabled windows cannot recieve focus or activation. DestroyWindow will want to assign activation to a window - if the parent is disabled another top-level window will be chosen and activated - normally not the desired result.



Recipe #2 - Busy wait processing the modal way.

Quite often you find an application that must perform some lenghty task. The more professionally finished app should:

Sorting the Cursor

The biggest trick in a busy modal function is (potentially) setting the cursor. While its easy to call SetCursor(LoadCursor(NULL,IDC_WAIT)) the cursor thus set will revert to the original cursor the moment the user moves the mouse. Everytime the mouse is moves a whole slew of messages are generated: WM_NCHITTEST, WM_SETCURSOR and WM_MOUSEMOVE. WM_SETCURSOR is handled by DefWindowProc() - which calls SetCursor(GetClassLong(hwnd,GCL_HCURSOR)) thus resetting the cursor.

One could attack this problem at many levels. I prefer to solve it by getting the modal functions message handler to eat WM_SETCURSOR messages without dispatching them.



IsDialogMessage and multiple Modeless Dialogs

It is necessary in order for modeless dialogs to function properly for IsDialogMessage to be called in the applications main message loop. However - no matter how many modeless dialogs are visible at any one time - only a single call to IsDialogMessage needs to be made - for the current active dialog - more precisely the dialog that contains the focused control.

Three mechanisms come to mind for modeless dialogs to ensure that the application is calling IsDialogMessage correctly

  1. The message loop calls GetActiveWindow() and passes the result to IsDialogMessage
  2. The application defines a custom message that a modeless dialog can send when it recieves a WM_ACTIVATE to register for IsDialogMessage's attention (and also in WM_KILLACTIVE to deregister). (It doesn't have to be a message - in a C++ project method calls could be made).


DialogBox2 Pseudocode

Windows NT4 and NT5 both use a function DialogBox2 to run their internal modal dialog loops. In a fit of enthusiam I spent between 10:00 pm one night and 4:00am the next morning reverse engineering the thing to answer a Q on a newsgroup. Here is the pseudo code I came up with:


LRESULT DialogBox2(
  HWND hwnd,        // the dialog box to be processed
  HWND hwndParent,  // the owner of the dialog box
  BOOL fVisible)    // has the dialog box been made visible yet?
{
  WND* pWnd;
  if(hwnd)
    pWnd = @ValidateHwnd(hwnd);
  else
    pWnd = NULL;

  if(!pWnd)
  {
    if(IsWindow(hwndParent))
      UserCallHwndParamLock(hwndParent);
    SetFocus(hwndParent);
    return -1;
  }

  if(some_global_flag & 1)
    goto resume;
  if(!hwndParent)
    goto resumt;
  if(pWnd->pDlg->someFlags & 1)
    goto resume;

  while(1)
  {
    if(!fVisible)
    {
      fVisible = TRUE;
      ShowWindow(hwnd);
      UpdateWindow(hwnd);
      if(some_global_flag & 2)
      {
        NotifyWinEvent(EVENT_SYSTEM_DIALOGSTART,hwnd,0,0);
      }
    }
    else
    {
      if(hwndParent)
      {
        if(!IsWindow(hwndParent))
          hwndParent = NULL;
        if(hwndParent && fNotifyParent && ???)
        {
          SendMessage(hwndParent,WM_ENTERIDLE,0,0L);
          goto resume;
        }
      }

      if(!ValidateDialogHandle(hwnd) || (pWnd->some_flags & 0x0c0000))
        break;
      WaitMessage();
    }
resume:
    if(!ValidateDialogHandle(hwnd))
      break;

    if(pWnd->pDlg->someFlags & 1)
      break;

    MSG msg;

    if(!PeekMessage(&msg,0,0,0,PM_REMOVE))
      continue;

    if(msg.message == WM_QUIT)
    {
      PostQuitMessage();
      break;
    }

    if(pWnd->SomeFlags14 & 0x20)
    {
      if(msg->message == WM_CHAR)
      {
        if(wParam == VK_???)
        {
          SendMessage(hwnd,WM_COPY,0,0);
          goto resume;
        }
      }
      if(msg->message == WM_KEYDOWN && wParam == VK_INSERT)
      {
        if(GetKeyState(0x11))
          SendMessage(hwnd,WM_COPY,0,0)
      }

      if(!IsDialogMessage(hwnd,&msg))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }

      if(!(msg.message == WM_TIMER ||
           msg.message == 0x0118 ||    // WM_whatexactly ?
           msg.message == WM_SYSKEYDOWN))
        goto resume;
    }

    if(somg_global_glag & 2)
      NotifyWinEvent(EVENT_SYSTEM_DIALOGEND,hwnd,0,0);

    BOOL r = 0;

    if(ValidateDialogHandle(hwnd))
    {
      r = pWnd->pDlg->result;

      DestroyWindow(hwnd);

      if(GetThreadState(0xc) && QueryWindow(hwndParent,6))
      {
        UserCallHwndLock(hwndParent);
      }
    }

    return r;
  }
}