VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

The Caret (Not the Cursor)

When you type text into a program, generally a little underline, vertical bar, or box shows you where the next character you type will appear on the screen. You may know this as a "cursor," but you'll have to get out of that habit when programming for Windows. In Windows, it's called the "caret." The word "cursor" is reserved for the little bitmap image that represents the mouse position.

The Caret Functions

There are five essential caret functions:

There are also functions to get the current caret position (GetCaretPos) and to get and set the caret blink time (GetCaretBlinkTime and SetCaretBlinkTime).

In Windows, the caret is customarily a horizontal line or box that is the size of a character, or a vertical line that is the height of a character. The vertical line caret is recommended when you use a proportional font such as the Windows default system font. Because the characters in a proportional font are not of a fixed size, the horizontal line or box can't be set to the size of a character.

If you need a caret in your program, you should not simply create it during the WM_CREATE message of your window procedure and destroy it during the WM_DESTROY message. The reason this is not advised is that a message queue can support only one caret. Thus, if your program has more than one window, the windows must effectively share the same caret.

This is not as restrictive as it sounds. When you think about it, the display of a caret in a window makes sense only when the window has the input focus. Indeed, the existence of a blinking caret is one of the visual cues that allows a user to recognize that he or she may type text into a program. Since only one window has the input focus at any time, it doesn't make sense for multiple windows to have carets blinking all at the same time.

A program can determine if it has the input focus by processing the WM_SETFOCUS and WM_KILLFOCUS messages. As the names imply, a window procedure receives a WM_SETFOCUS message when it receives the input focus and a WM_KILLFOCUS message when it loses the input focus. These messages occur in pairs: A window procedure will always receive a WM_SETFOCUS message before it receives a WM_KILLFOCUS message, and it always receives an equal number of WM_SETFOCUS and WM_KILLFOCUS messages over the course of the window's lifetime.

The main rule for using the caret is simple: a window procedure calls CreateCaret during the WM_SETFOCUS message and DestroyWindow during the WM_KILLFOCUS message.

There are a few other rules: The caret is created hidden. After calling CreateCaret, the window procedure must call ShowCaret for the caret to be visible. In addition, the window procedure must hide the caret by calling HideCaret whenever it draws something on its window during a message other than WM_PAINT. After it finishes drawing on the window, the program calls ShowCaret to display the caret again. The effect of HideCaret is additive: if you call HideCaret several times without calling ShowCaret, you must call ShowCaret the same number of times before the caret becomes visible again.

The TYPER Program

The TYPER program shown in Figure 6-13 brings together much of what we've learned in this chapter. You can think of TYPER as an extremely rudimentary text editor. You can type in the window, move the cursor (I mean caret) around with the cursor movement keys (or are they caret movement keys?), and erase the contents of the window by pressing Escape. The contents of the window are also erased when you resize the window or change the keyboard input language. There's no scrolling, no search and replace, no way to save files, no spelling checker, and no anthropomorphous paper clip, but it's a start.

Figure 6-13. The TYPER program.

TYPER.C

 /*--------------------------------------
   TYPER.C -- Typing Program
              (c) Charles Petzold, 1998
  --------------------------------------*/

#include <windows.h>

#define BUFFER(x,y) *(pBuffer + y * cxBuffer + x)

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Typer") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
     
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"), 
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Typing Program"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static DWORD   dwCharSet = DEFAULT_CHARSET ;
     static int     cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
                    xCaret, yCaret ;
     static TCHAR * pBuffer = NULL ;
     HDC            hdc ;
     int            x, y, i ;
     PAINTSTRUCT    ps ;
     TEXTMETRIC     tm ;
     
     switch (message)
     {
     case WM_INPUTLANGCHANGE:
          dwCharSet = wParam ;
                                        // fall through
     case WM_CREATE:
          hdc = GetDC (hwnd) ;
          SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
                                   dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
          
          GetTextMetrics (hdc, &tm) ;
          cxChar = tm.tmAveCharWidth ;
          cyChar = tm.tmHeight ;
          
          DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
          ReleaseDC (hwnd, hdc) ;
                                        // fall through                
     case WM_SIZE:
               // obtain window size in pixels

          if (message == WM_SIZE)
          {
               cxClient = LOWORD (lParam) ;
               cyClient = HIWORD (lParam) ;
          }
               // calculate window size in characters
          
          cxBuffer = max (1, cxClient / cxChar) ;
          cyBuffer = max (1, cyClient / cyChar) ;
          
               // allocate memory for buffer and clear it
          
          if (pBuffer != NULL)
               free (pBuffer) ;

          pBuffer = (TCHAR *) malloc (cxBuffer * cyBuffer * sizeof (TCHAR)) ;
          
          for (y = 0 ; y < cyBuffer ; y++)
               for (x = 0 ; x < cxBuffer ; x++)
                    BUFFER(x,y) = ` ` ;
                    
               // set caret to upper left corner

          xCaret = 0 ;
          yCaret = 0 ;
                    
          if (hwnd == GetFocus ())
               SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;

          InvalidateRect (hwnd, NULL, TRUE) ;
          return 0 ;
                    
     case WM_SETFOCUS:
               // create and show the caret

          CreateCaret (hwnd, NULL, cxChar, cyChar) ;
          SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
          ShowCaret (hwnd) ;
          return 0 ;
          
     case WM_KILLFOCUS:
               // hide and destroy the caret

          HideCaret (hwnd) ;
          DestroyCaret () ;
          return 0 ;
          
     case WM_KEYDOWN:
          switch (wParam)
          {
          case VK_HOME:
               xCaret = 0 ;
               break ;
               
          case VK_END:
               xCaret = cxBuffer - 1 ;
               break ;
               
          case VK_PRIOR:
               yCaret = 0 ;
               break ;
               
          case VK_NEXT:
               yCaret = cyBuffer - 1 ;
               break ;
               
          case VK_LEFT:
               xCaret = max (xCaret - 1, 0) ;
               break ;
               
          case VK_RIGHT:
               xCaret = min (xCaret + 1, cxBuffer - 1) ;
               break ;
               
          case VK_UP:
               yCaret = max (yCaret - 1, 0) ;
               break ;
               
          case VK_DOWN:
               yCaret = min (yCaret + 1, cyBuffer - 1) ;
               break ;
               
          case VK_DELETE:
               for (x = xCaret ; x < cxBuffer - 1 ; x++)
                    BUFFER (x, yCaret) = BUFFER (x + 1, yCaret) ;
               
               BUFFER (cxBuffer - 1, yCaret) = ` ` ;
               
               HideCaret (hwnd) ;
               hdc = GetDC (hwnd) ;
          
               SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
                                   dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
                    
               TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
                        & BUFFER (xCaret, yCaret),
                        cxBuffer - xCaret) ;

               DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
               ReleaseDC (hwnd, hdc) ;
               ShowCaret (hwnd) ;
               break ;
          }
          SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
          return 0 ;
          
     case WM_CHAR:
          for (i = 0 ; i < (int) LOWORD (lParam) ; i++)
          {
               switch (wParam)
               {
               case `\b':                    // backspace
                    if (xCaret > 0)
                    {
                         xCaret-- ;
                         SendMessage (hwnd, WM_KEYDOWN, VK_DELETE, 1) ;
                    }
                    break ;
                    
               case `\t':                    // tab
                    do
                    {
                         SendMessage (hwnd, WM_CHAR, ` `, 1) ;
                    }
                    while (xCaret % 8 != 0) ;
                    break ;

               case `\n':                    // line feed
                    if (++yCaret == cyBuffer)
                         yCaret = 0 ;
                    break ;
                    
               case `\r':                    // carriage return
                    xCaret = 0 ;
                    
                    if (++yCaret == cyBuffer)
                         yCaret = 0 ;
                    break ;
                    
               case `\x1B':                  // escape
                    for (y = 0 ; y < cyBuffer ; y++)
                         for (x = 0 ; x < cxBuffer ; x++)
                              BUFFER (x, y) = ` ` ;
                         
                    xCaret = 0 ;
                    yCaret = 0 ;
                         
                    InvalidateRect (hwnd, NULL, FALSE) ;
                    break ;
                         
               default:                      // character codes
                    BUFFER (xCaret, yCaret) = (TCHAR) wParam ;
                    
                    HideCaret (hwnd) ;
                    hdc = GetDC (hwnd) ;
          
                    SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
                                   dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;

                    TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
                             & BUFFER (xCaret, yCaret), 1) ;

                    DeleteObject (
                         SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
                    ReleaseDC (hwnd, hdc) ;
                    ShowCaret (hwnd) ;

                    if (++xCaret == cxBuffer)
                    {
                         xCaret = 0 ;
                         
                         if (++yCaret == cyBuffer)
                              yCaret = 0 ;
                    }
                    break ;
               }
          }
          
          SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
          return 0 ;
          
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          
          SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
                                   dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
                              
          for (y = 0 ; y < cyBuffer ; y++)
               TextOut (hdc, 0, y * cyChar, & BUFFER(0,y), cxBuffer) ;

          DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;
          
     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

To keep things reasonably simple, TYPER uses a fixed-pitch font. Writing a text editor for a proportional font is, as you might imagine, much more difficult. The program obtains a device context in several places: during the WM_CREATE message, the WM_KEYDOWN message, the WM_CHAR message, and the WM_PAINT message. Each time, calls to GetStockObject and SelectObject select a fixed-pitch font with the current character set.

During the WM_SIZE message, TYPER calculates the character width and height of the window and saves these values in the variables cxBuffer and cyBuffer. It then uses malloc to allocate a buffer to hold all the characters that can be typed in the window. Notice that the size of this buffer in bytes is the product of cxBuffer, cyBuffer, and sizeof (TCHAR), which can be 1 or 2 depending on whether the program is compiled for 8-bit character processing or Unicode.

The xCaret and yCaret variables store the character position of the caret. During the WM_SETFOCUS message, TYPER calls CreateCaret to create a caret that is the width and height of a character. It then calls SetCaretPos to set the caret position and ShowCaret to make the caret visible. During the WM_KILLFOCUS message, TYPER calls HideCaret and DestroyCaret.

The WM_KEYDOWN processing mostly involves the cursor movement keys. Home and End send the caret to the beginning and end of a line, and Page Up and Page Down send the caret to the top and bottom of the window. The arrow keys work as you would expect. For the Delete key, TYPER must move everything remaining in the buffer from the next caret position to the end of the line and then display a blank space at the end of the line.

The WM_CHAR processing handles the Backspace, Tab, Linefeed (Ctrl-Enter), Enter, Escape, and character keys. Notice that I've used Repeat Count in lParam when processing the WM_CHAR message (under the assumption that every character the user types is important) but not during the WM_KEYDOWN message (to prevent inadvertent overscrolling). The Backspace and Tab processing is simplified somewhat by the use of the SendMessage function. Backspace is emulated by the Delete logic, and Tab is emulated by a series of spaces.

As I mentioned earlier, a program should hide the caret when drawing on the window during messages other than WM_PAINT. TYPER does this when processing the WM_KEYDOWN message for the Delete key and the WM_CHAR message for character keys. In both these cases, TYPER alters the contents of the buffer and then draws the new character or characters on the window.

Although TYPER uses the same logic as KEYVIEW2 to switch between character sets as the user switches keyboard layouts, it does not work quite right for Far Eastern versions of Windows. TYPER does not make any allowance for the double-width characters. This raises issues that are better covered in Chapter 17, which explores fonts and text output in more detail.