The VisualKB Application

[Previous] [Next]

Let's put together everything we've learned in this chapter by developing a sample application that accepts text input from the keyboard, displays the text in a window, and lets the user perform simple text-editing functions that include moving the caret with the arrow keys and the mouse. For educational purposes, let's add a scrolling display of the keyboard messages that the program receives and the parameters bundled with those messages, similar to the KEYLOOK program featured in Charles Petzold's Programming Windows. In addition to providing a hands-on lesson in mouse and keyboard handling, the program, which I've called VisualKB, demonstrates some techniques for handling proportionally spaced text. VisualKB also provides a handy tool for examining the stream of messages coming from the keyboard and experimenting to see what messages result from specific keystrokes and key combinations.

Figure 3-7 shows how VisualKB looks right after it's started and the letters "MFC" are typed. The typed characters appear in the text-entry rectangle (the "text box") at the top of the window, and keyboard messages are displayed in the rectangle below (the "message list"). The first and final messages were generated when the Shift key was pressed and released. In between, you see the WM_KEYDOWN, WM_CHAR, and WM_KEYUP messages generated by the M, F, and C keystrokes. To the right of each message name, VisualKB displays the message parameters. "Char" is the virtual key code or character code passed to the message handler in nChar. "Rep" is the repeat count in nRepCnt. "Scan" is the OEM scan code stored in bits 0 through 7 of the nFlags parameter, and "Ext," "Con," "Prv," and "Tran" represent the extended key flag, context code, previous key state, and transition state values. VisualKB also displays WM_SYSKEYDOWN, WM_SYSCHAR, and WM_SYSKEYUP messages, which you can verify by pressing an Alt key combination such as Alt-S.

click to view at full size.

Figure 3-7. The VisualKB window after the letters MFC are typed.

Take a moment to play with VisualKB and see what happens when you press various keys and combinations of keys. In addition to typing in text, you can use the following editing keys:

  • The left and right arrow keys move the caret one character to the left and right. Home and End move the caret to the beginning and end of the line. The caret can also be moved with mouse clicks.
  • The Backspace key deletes the character to the left of the caret and moves the caret one position to the left.
  • The Esc and Enter keys clear the text and reset the caret to the beginning of the line.

Typed characters are entered in overstrike mode, so if the caret isn't at the end of the line, the next character you type will replace the character to the right. If you type beyond the end of the line (about one character position to the left of the far right end of the text box), the text is automatically cleared. I resisted the urge to add features such as horizontal scrolling and insert mode to keep the program from becoming unnecessarily complicated. Besides, in the real world you can avoid writing a lot of the code for a program like this one by using an edit control, which provides similar text-entry capabilities and includes support for cutting and pasting, scrolling, and much more. Unless you're writing the world's next great word processor, an edit control probably has everything you need. Still, it's useful to see how text entry is done the hard way, not only because it's instructive but also because you'll get a feel for what's happening inside Windows when you start using edit controls.

There is much to be learned from VisualKB's source code, which is reproduced in Figure 3-8. The following sections point out a few of the highlights.

Figure 3-8. The VisualKB application.

VisualKB.h

 #define MAX_STRINGS 12 class CMyApp : public CWinApp { public:     virtual BOOL InitInstance (); }; class CMainWindow : public CWnd { protected:     int m_cxChar;                // Average character width     int m_cyChar;                // Character height     int m_cyLine;                // Vertical line spacing in message box     int m_nTextPos;              // Index of current character in text box     int m_nTabStops[7];          // Tab stop locations for tabbed output     int m_nTextLimit;            // Maximum width of text in text box     int m_nMsgPos;               // Current position in m_strMessages array          HCURSOR m_hCursorArrow;      // Handle of arrow cursor     HCURSOR m_hCursorIBeam;      // Handle of I-beam cursor     CPoint m_ptTextOrigin;       // Origin for drawing input text     CPoint m_ptHeaderOrigin;     // Origin for drawing header text     CPoint m_ptUpperMsgOrigin;   // Origin of first line in message box     CPoint m_ptLowerMsgOrigin;   // Origin of last line in message box     CPoint m_ptCaretPos;         // Current caret position     CRect m_rcTextBox;           // Coordinates of text box     CRect m_rcTextBoxBorder;     // Coordinates of text box border     CRect m_rcMsgBoxBorder;      // Coordinates of message box border     CRect m_rcScroll;            // Coordinates of scroll rectangle     CString m_strInputText;                // Input text     CString m_strMessages[MAX_STRINGS];    // Array of message strings public:     CMainWindow (); protected:     int GetNearestPos (CPoint point);     void PositionCaret (CDC* pDC = NULL);     void DrawInputText (CDC* pDC);     void ShowMessage (LPCTSTR pszMessage, UINT nChar, UINT nRepCnt,         UINT nFlags);     void DrawMessageHeader (CDC* pDC);     void DrawMessages (CDC* pDC); protected:     virtual void PostNcDestroy ();     afx_msg int OnCreate (LPCREATESTRUCT lpCreateStruct);     afx_msg void OnPaint ();     afx_msg void OnSetFocus (CWnd* pWnd);     afx_msg void OnKillFocus (CWnd* pWnd);     afx_msg BOOL OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message);     afx_msg void OnLButtonDown (UINT nFlags, CPoint point);     afx_msg void OnKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags);     afx_msg void OnKeyUp (UINT nChar, UINT nRepCnt, UINT nFlags);     afx_msg void OnSysKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags);     afx_msg void OnSysKeyUp (UINT nChar, UINT nRepCnt, UINT nFlags);     afx_msg void OnChar (UINT nChar, UINT nRepCnt, UINT nFlags);     afx_msg void OnSysChar (UINT nChar, UINT nRepCnt, UINT nFlags);     DECLARE_MESSAGE_MAP () }; 

 

VisualKB.cpp

 #include <afxwin.h> #include "VisualKB.h" CMyApp myApp; ///////////////////////////////////////////////////////////////////////// // CMyApp member functions BOOL CMyApp::InitInstance () {     m_pMainWnd = new CMainWindow;     m_pMainWnd->ShowWindow (m_nCmdShow);     m_pMainWnd->UpdateWindow ();     return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_MESSAGE_MAP (CMainWindow, CWnd)     ON_WM_CREATE ()     ON_WM_PAINT ()     ON_WM_SETFOCUS ()     ON_WM_KILLFOCUS ()     ON_WM_SETCURSOR ()     ON_WM_LBUTTONDOWN ()     ON_WM_KEYDOWN ()     ON_WM_KEYUP ()     ON_WM_SYSKEYDOWN ()     ON_WM_SYSKEYUP ()     ON_WM_CHAR ()     ON_WM_SYSCHAR () END_MESSAGE_MAP () CMainWindow::CMainWindow () {     m_nTextPos = 0;     m_nMsgPos = 0;     //     // Load the arrow cursor and the I-beam cursor and save their handles.     //     m_hCursorArrow = AfxGetApp ()->LoadStandardCursor (IDC_ARROW);     m_hCursorIBeam = AfxGetApp ()->LoadStandardCursor (IDC_IBEAM);     //     // Register a WNDCLASS.     //     CString strWndClass = AfxRegisterWndClass (         0,         NULL,         (HBRUSH) (COLOR_3DFACE + 1),         AfxGetApp ()->LoadStandardIcon (IDI_WINLOGO)     );     //     // Create a window.     //     CreateEx (0, strWndClass, _T ("Visual Keyboard"),         WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,         NULL, NULL); } int CMainWindow::OnCreate (LPCREATESTRUCT lpCreateStruct) {     if (CWnd::OnCreate (lpCreateStruct) == -1)         return -1;          //     // Initialize member variables whose values are dependent upon screen     // metrics.     //     CClientDC dc (this);     TEXTMETRIC tm;     dc.GetTextMetrics (&tm);     m_cxChar = tm.tmAveCharWidth;     m_cyChar = tm.tmHeight;     m_cyLine = tm.tmHeight + tm.tmExternalLeading;     m_rcTextBoxBorder.SetRect (16, 16, (m_cxChar * 64) + 16,         ((m_cyChar * 3) / 2) + 16);     m_rcTextBox = m_rcTextBoxBorder;     m_rcTextBox.InflateRect (-2, -2);          m_rcMsgBoxBorder.SetRect (16, (m_cyChar * 4) + 16,         (m_cxChar * 64) + 16, (m_cyLine * MAX_STRINGS) +         (m_cyChar * 6) + 16);     m_rcScroll.SetRect (m_cxChar + 16, (m_cyChar * 6) + 16,         (m_cxChar * 63) + 16, (m_cyLine * MAX_STRINGS) +         (m_cyChar * 5) + 16);     m_ptTextOrigin.x = m_cxChar + 16;     m_ptTextOrigin.y = (m_cyChar / 4) + 16;     m_ptCaretPos = m_ptTextOrigin;     m_nTextLimit = (m_cxChar * 63) + 16;     m_ptHeaderOrigin.x = m_cxChar + 16;     m_ptHeaderOrigin.y = (m_cyChar * 3) + 16;     m_ptUpperMsgOrigin.x = m_cxChar + 16;     m_ptUpperMsgOrigin.y = (m_cyChar * 5) + 16;     m_ptLowerMsgOrigin.x = m_cxChar + 16;     m_ptLowerMsgOrigin.y = (m_cyChar * 5) +         (m_cyLine * (MAX_STRINGS - 1)) + 16;     m_nTabStops[0] = (m_cxChar * 24) + 16;     m_nTabStops[1] = (m_cxChar * 30) + 16;     m_nTabStops[2] = (m_cxChar * 36) + 16;     m_nTabStops[3] = (m_cxChar * 42) + 16;     m_nTabStops[4] = (m_cxChar * 46) + 16;     m_nTabStops[5] = (m_cxChar * 50) + 16;     m_nTabStops[6] = (m_cxChar * 54) + 16;     //     // Size the window.     //     CRect rect (0, 0, m_rcMsgBoxBorder.right + 16,         m_rcMsgBoxBorder.bottom + 16);     CalcWindowRect (&rect);     SetWindowPos (NULL, 0, 0, rect.Width (), rect.Height (),         SWP_NOZORDER | SWP_NOMOVE | SWP_NOREDRAW);     return 0; } void CMainWindow::PostNcDestroy () {     delete this; } void CMainWindow::OnPaint () {     CPaintDC dc (this);     //     // Draw the rectangles surrounding the text box and the message list.     //     dc.DrawEdge (m_rcTextBoxBorder, EDGE_SUNKEN, BF_RECT);     dc.DrawEdge (m_rcMsgBoxBorder, EDGE_SUNKEN, BF_RECT);     //     // Draw all the text that appears in the window.     //     DrawInputText (&dc);     DrawMessageHeader (&dc);     DrawMessages (&dc); } void CMainWindow::OnSetFocus (CWnd* pWnd) {     //     // Show the caret when the VisualKB window receives the input focus.     //     CreateSolidCaret (max (2, ::GetSystemMetrics (SM_CXBORDER)),         m_cyChar);     SetCaretPos (m_ptCaretPos);     ShowCaret (); } void CMainWindow::OnKillFocus (CWnd* pWnd) {     //     // Hide the caret when the VisualKB window loses the input focus.     //     HideCaret ();     m_ptCaretPos = GetCaretPos ();     ::DestroyCaret (); } BOOL CMainWindow::OnSetCursor (CWnd* pWnd, UINT nHitTest, UINT message) {     //     // Change the cursor to an I-beam if it's currently over the text box,     // or to an arrow if it's positioned anywhere else.     //     if (nHitTest == HTCLIENT) {         DWORD dwPos = ::GetMessagePos ();         CPoint point (LOWORD (dwPos), HIWORD (dwPos));         ScreenToClient (&point);         ::SetCursor (m_rcTextBox.PtInRect (point) ?             m_hCursorIBeam : m_hCursorArrow);         return TRUE;     }     return CWnd::OnSetCursor (pWnd, nHitTest, message); } void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) {     //     / Move the caret if the text box is clicked with the left mouse button.     //     if (m_rcTextBox.PtInRect (point)) {         m_nTextPos = GetNearestPos (point);         PositionCaret ();     } } void CMainWindow::OnKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_KEYDOWN"), nChar, nRepCnt, nFlags);     //     // Move the caret when the left, right, Home, or End key is pressed.     //     switch (nChar) { case VK_LEFT:             if (m_nTextPos != 0) {                 m_nTextPos--;                 PositionCaret ();         }         break;     case VK_RIGHT:         if (m_nTextPos != m_strInputText.GetLength ()) {             m_nTextPos++;             PositionCaret ();         }         break;     case VK_HOME:         m_nTextPos = 0;         PositionCaret ();         break;     case VK_END:         m_nTextPos = m_strInputText.GetLength ();         PositionCaret ();         break;     } } void CMainWindow::OnChar (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_CHAR"), nChar, nRepCnt, nFlags);     CClientDC dc (this);     //     // Determine which character was just input from the keyboard.     //     switch (nChar) {     case VK_ESCAPE:     case VK_RETURN:         m_strInputText.Empty ();         m_nTextPos = 0;         break;     case VK_BACK:         if (m_nTextPos != 0) {             m_strInputText = m_strInputText.Left (m_nTextPos - 1) +              m_strInputText.Right (m_strInputText.GetLength () -                 m_nTextPos);             m_nTextPos--;         }         break;     default:         if ((nChar >= 0) && (nChar <= 31))             return;         if (m_nTextPos == m_strInputText.GetLength ()) {             m_strInputText += nChar;             m_nTextPos++;         }         else             m_strInputText.SetAt (m_nTextPos++, nChar);         CSize size = dc.GetTextExtent (m_strInputText,             m_strInputText.GetLength ());         if ((m_ptTextOrigin.x + size.cx) > m_nTextLimit) {             m_strInputText = nChar;             m_nTextPos = 1;         }         break;     }     //     // Update the contents of the text box.     //     HideCaret ();     DrawInputText (&dc);     PositionCaret (&dc);     ShowCaret (); } void CMainWindow::OnKeyUp (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_KEYUP"), nChar, nRepCnt, nFlags);     CWnd::OnKeyUp (nChar, nRepCnt, nFlags); } void CMainWindow::OnSysKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_SYSKEYDOWN"), nChar, nRepCnt, nFlags);     CWnd::OnSysKeyDown (nChar, nRepCnt, nFlags); } void CMainWindow::OnSysChar (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_SYSCHAR"), nChar, nRepCnt, nFlags);     CWnd::OnSysChar (nChar, nRepCnt, nFlags); } void CMainWindow::OnSysKeyUp (UINT nChar, UINT nRepCnt, UINT nFlags) {     ShowMessage (_T ("WM_SYSKEYUP"), nChar, nRepCnt, nFlags);     CWnd::OnSysKeyUp (nChar, nRepCnt, nFlags); } void CMainWindow::PositionCaret (CDC* pDC) {     BOOL bRelease = FALSE;     //     // Create a device context if pDC is NULL.     //     if (pDC == NULL) {         pDC = GetDC ();         bRelease = TRUE;     }     //     // Position the caret just right of the character whose 0-based     // index is stored in m_nTextPos.     //     CPoint point = m_ptTextOrigin;     CString string = m_strInputText.Left (m_nTextPos);     point.x += (pDC->GetTextExtent (string, string.GetLength ())).cx;     SetCaretPos (point);     //     // Release the device context if it was created inside this function.     //     if (bRelease)         ReleaseDC (pDC); } int CMainWindow::GetNearestPos (CPoint point) {     //     // Return 0 if (point.x, point.y) lies to the left of the text in     // the text box.     //     if (point.x <= m_ptTextOrigin.x)         return 0;     //     // Return the string length if (point.x, point.y) lies to the right     // of the text in the text box.     //     CClientDC dc (this);     int nLen = m_strInputText.GetLength ();     if (point.x >= (m_ptTextOrigin.x +         (dc.GetTextExtent (m_strInputText, nLen)).cx))         return nLen;     //     // Knowing that (point.x, point.y) lies somewhere within the text     // in the text box, convert the coordinates into a character index.     //     int i = 0;     int nPrevChar = m_ptTextOrigin.x;     int nNextChar = m_ptTextOrigin.x;     while (nNextChar < point.x) {         i++;         nPrevChar = nNextChar;               nNextChar = m_ptTextOrigin.x +             (dc.GetTextExtent (m_strInputText.Left (i), i)).cx;     }     return ((point.x - nPrevChar) < (nNextChar - point.x)) ? i - 1: i; } void CMainWindow::DrawInputText (CDC* pDC) {     pDC->ExtTextOut (m_ptTextOrigin.x, m_ptTextOrigin.y,         ETO_OPAQUE, m_rcTextBox, m_strInputText, NULL); } void CMainWindow::ShowMessage (LPCTSTR pszMessage, UINT nChar,     UINT nRepCnt, UINT nFlags) {     //     // Formulate a message string.     //     CString string;     string.Format (_T ("%s\t %u\t  %u\t  %u\t  %u\t  %u\t  %u\t   %u"),         pszMessage, nChar, nRepCnt, nFlags & 0xFF,         (nFlags >> 8) & 0x01, (nFlags >> 13) & 0x01,         (nFlags >> 14) & 0x01,         (nFlags >> 15) & 0x01);     //     // Scroll the other message strings up and validate the scroll     // rectangle to prevent OnPaint from being called.     //     ScrollWindow (0, -m_cyLine, &m_rcScroll);     ValidateRect (m_rcScroll);     //     // Record the new message string and display it in the window.     //     CClientDC dc (this);     dc.SetBkColor ((COLORREF) ::GetSysColor (COLOR_3DFACE));     m_strMessages[m_nMsgPos] = string;     dc.TabbedTextOut (m_ptLowerMsgOrigin.x, m_ptLowerMsgOrigin.y,         m_strMessages[m_nMsgPos], m_strMessages[m_nMsgPos].GetLength (),         sizeof (m_nTabStops), m_nTabStops, m_ptLowerMsgOrigin.x);     //     // Update the array index that specifies where the next message     // string will be stored.     //     if (++m_nMsgPos == MAX_STRINGS)         m_nMsgPos = 0;   } void CMainWindow::DrawMessageHeader (CDC* pDC) {     static CString string =         _T ("Message\tChar\tRep\tScan\tExt\tCon\tPrv\tTran");     pDC->SetBkColor ((COLORREF) ::GetSysColor (COLOR_3DFACE));     pDC->TabbedTextOut (m_ptHeaderOrigin.x, m_ptHeaderOrigin.y,         string, string.GetLength (), sizeof (m_nTabStops), m_nTabStops,         m_ptHeaderOrigin.x); } void CMainWindow::DrawMessages (CDC* pDC) {     int nPos = m_nMsgPos;     pDC->SetBkColor ((COLORREF) ::GetSysColor (COLOR_3DFACE));     for (int i=0; i<MAX_STRINGS; i++) {         pDC->TabbedTextOut (m_ptUpperMsgOrigin.x,             m_ptUpperMsgOrigin.y + (m_cyLine * i),             m_strMessages[nPos], m_strMessages[nPos].GetLength (),             sizeof (m_nTabStops), m_nTabStops, m_ptUpperMsgOrigin.x);         if (++nPos == MAX_STRINGS)             nPos = 0;         } } 

Handling the Caret

CMainWindow's OnSetFocus and OnKillFocus handlers create a caret when the VisualKB window receives the input focus and destroy the caret when the focus goes away. OnSetFocus sets the caret width to 2 or the SM_CXBORDER value returned by ::GetSystemMetrics, whichever is greater, so that the caret is visible even on very high resolution displays:

 void CMainWindow::OnSetFocus (CWnd* pWnd) {     CreateSolidCaret (max (2, ::GetSystemMetrics (SM_CXBORDER)),         m_cyChar);     SetCaretPos (m_ptCaretPos);     ShowCaret (); } 

OnKillFocus hides the caret, saves the current caret position so that it can be restored the next time OnSetFocus is called, and then destroys the caret:

 void CMainWindow::OnKillFocus (CWnd* pWnd) {     HideCaret ();     m_ptCaretPos = GetCaretPos ();     ::DestroyCaret (); } 

m_ptCaretPos is initialized with the coordinates of the leftmost character cell in CMainWindow::OnCreate. It is reinitialized with the current caret position whenever the window loses the input focus. Therefore, the call to SetCaretPos in OnSetFocus sets the caret to the beginning of the text box when the program is first activated and restores the caret to the position it previously occupied in subsequent invocations.

The OnKeyDown handler moves the caret when the left arrow, right arrow, Home key, or End key is pressed. None of these keys generates WM_CHAR messages, so VisualKB processes WM_KEYDOWN messages instead. A switch-case block executes the appropriate handling routine based on the virtual key code in nChar. The handler for the left arrow key (whose virtual key code is VK_LEFT) consists of the following statements:

 case VK_LEFT:     if (m_nTextPos != 0) {         m_nTextPos—;         PositionCaret ();     }     break; 

m_nTextPos is the position at which the next character will be inserted into the text string. The text string itself is stored in the CString object m_strInputText. PositionCaret is a protected CMainWindow member function that uses GetTextExtent to find the pixel position in the text string that corresponds to the character position stored in m_nTextPos and then moves the caret to that position with SetCaretPos. After checking m_nTextPos to make sure it hasn't run out of room to move the caret further left, the VK_LEFT handler decrements m_nTextPos and calls PositionCaret to move the caret. If m_nTextPos is 0, which indicates that the caret is already positioned at the left end of the entry field, the keystroke is ignored. The other VK_ handlers are similarly straightforward. The VK_END handler, for example, moves the caret to the end of the text string with the statements

 m_nTextPos = m_strInputText.GetLength (); PositionCaret (); 

GetLength is a CString member function that returns the number of characters in the string. The use of a CString object to hold the text entered into VisualKB makes text handling much simpler than it would be if strings were handled simply as arrays of characters. For example, all the OnChar handler has to do to add a new character to the end of the string is

 m_strInputText += nChar; 

When it comes to string handling, it doesn't get much easier than that. Browse through VisualKB.cpp and you'll see several CString member functions and operators, including CString::Left, which returns a CString object containing the string's left n characters; CString::Right, which returns the rightmost n characters; and CString::Format, which performs printf-like string formatting.

It seemed a shame not to have VisualKB do anything with the mouse when half of this chapter is devoted to mouse input, so I added an OnLButtonDown handler, which allows the caret to be moved with a click of the left mouse button in the text box. In addition to adding a nice feature to the program, the OnLButtonDown handler also lets us examine a function that takes the point at which a mouse click occurred and returns the corresponding character position within a text string. The button handler itself is exceedingly simple:

 void CMainWindow::OnLButtonDown (UINT nFlags, CPoint point) {     if (m_rcTextBox.PtInRect (point)) {         m_nTextPos = GetNearestPos (point);         PositionCaret ();     } } 

m_rcTextBox is the rectangle that bounds the text box. After calling CRect::PtInRect to determine whether the click occurred inside the rectangle (and returning without doing anything if it didn't), OnLButtonDown computes a new value for m_nTextPos with CMainWindow::GetNearestPos and calls PositionCaret to reposition the caret. GetNearestPos first checks to see if the mouse was clicked to the left of the character string and returns 0 for the new character position if it was:

 if (point.x <= m_ptTextOrigin.x)     return 0; 

m_ptTextOrigin holds the coordinates of the character string's upper left corner. GetNearestPos then returns an integer value that equals the string length if the mouse was clicked beyond the string's rightmost extent:

 CClientDC dc (this); int nLen = m_strInputText.GetLength (); if (point.x >= (m_ptTextOrigin.x +     (dc.GetTextExtent (m_strInputText, nLen)).cx))     return nLen; 

The result? If the mouse was clicked inside the text rectangle but to the right of the rightmost character, the caret is moved to the end of the string.

If GetNearestPos makes it beyond the return nLen statement, we can conclude that the cursor was clicked inside the text box somewhere between the character string's left and right extents. GetNearestPos next initializes three variables and executes a while loop that calls GetTextExtent repeatedly until nPrevChar and nNextChar hold values that bracket the x coordinate of the point at which the click occurred:

 while (nNextChar < point.x) {     i++;     nPrevChar = nNextChar;     nNextChar = m_ptTextOrigin.x +         (dc.GetTextExtent (m_strInputText.Left (i), i)).cx; } 

When the loop falls through, i holds the number of the character position to the right of where the click occurred, and i-1 identifies the character position to the left. Finding the character position is a simple matter of finding out whether point.x is closer to nNextChar or nPrevChar and returning i or i-1. This is accomplished with the following one-liner:

 return ((point.x - nPrevChar) < (nNextChar - point.x)) ? i - 1: i; 

That's it; given an arbitrary point in the window's client area, GetNearestPos returns a matching character position in the string m_strInputText. A small amount of inefficiency is built into this process because the farther to the right the point lies, the more times GetTextExtent is called. (The while loop starts with the leftmost character in the string and moves right one character at a time until it finds the character just to the right of the point at which the click occurred.) A really smart implementation of GetNearestPos could do better by using a binary-halving approach, starting in the middle of the string and iterating to the left or right by a number of characters equal to half the area that hasn't already been covered until it zeroes in on the characters to the left and right of the point at which the click occurred. A character position in a string 128 characters long could then be located with no more than 8 calls to GetTextExtent. The brute force technique employed by GetNearestPos could require as many as 127 calls.

Entering and Editing Text

The logic for entering and editing text is found in CMainWindow::OnChar. OnChar's processing strategy can be summarized in this way:

  1. Echo the message to the screen.
  2. Modify the text string using the character code in nChar.
  3. Draw the modified text string on the screen.
  4. Reposition the caret.

Step 1 is accomplished by calling CMainWindow::ShowMessage, which is discussed in the next section. How the text string is modified in step 2 depends on what the character code in nChar is. If the character is an escape or a return (VK_ESCAPE or VK_RETURN), m_strInputText is cleared by a call to CString::Empty (another handy member of the CString class) and m_nTextPos is set to 0. If the character is a backspace (VK_BACK) and m_nTextPos isn't 0, the character at m_nTextPos-1 is deleted and m_nTextPos is decremented. If the character is any other value between 0 and 31, inclusive, it is ignored. If nChar represents any other character, it is added to m_strInputText at the current character position and m_nTextPos is incremented accordingly.

With the character that was just entered now added to m_strInputText, OnChar hides the caret and proceeds to step 3. The modified string is output to the screen with CMainWindow::DrawInputText, which in turn relies on CDC::ExtTextOut to do its text output. ExtTextOut is similar to TextOut, but it offers a few options that TextOut doesn't. One of those options is an ETO_OPAQUE flag that fills a rectangle surrounding the text with the device context's current background color. Repainting the entire rectangle erases artifacts left over from the previous text-output operation if the string's new width is less than its previous width. The border around the text box (and the border around the message list) is drawn with the CDC::DrawEdge function, which calls through to the ::DrawEdge API function. DrawEdge is the easy way to draw 3D borders that conform to the specifications prescribed in the Windows interface guidelines and that automatically adapt to changes in the system colors used for highlights and shadows. You can use a related CDC function, Draw3dRect, to draw simple 3D rectangles in your choice of colors.

OnChar finishes up by calling PositionCaret to reposition the caret using the value in m_nTextPos and then ShowCaret to redisplay the caret. As an experiment, comment out OnChar's calls to HideCaret and ShowCaret, recompile the program, and type a few characters into the text-entry field. This simple exercise will make clear why it's important to hide the caret before painting text behind it.

Other Points of Interest

As you move the cursor around inside the VisualKB window, notice that it changes from an arrow when it's outside the text box to an I-beam when it's inside. CMainWindow's constructor registers a WNDCLASS with a NULL class cursor and stores the handles for the system's arrow and I-beam cursors in the member variables m_hCursorArrow and m_hCursorIBeam. Each time CMainWindow receives a WM_SETCURSOR message, its OnSetCursor handler checks the current cursor location and calls ::SetCursor to display the appropriate cursor.

VisualKB echoes keyboard messages to the screen by calling CMainWindow::ShowMessage each time a message is received. ShowMessage formulates a new output string with help from CString::Format, copies the result to the least recently used entry in the m_strMessages array, scrolls the message list up one line, and calls CDC::TabbedTextOut to display the new message string on the bottom line. TabbedTextOut is used in lieu of TextOut so that columns will be properly aligned in the output. (Without tab characters, it's virtually impossible to get characters in a proportionally spaced font to line up in columnar format.) The tab stop settings are initialized in OnCreate using values based on the default font's average character width and stored in the m_nTabStops array. Message strings are saved in the m_strMessages array so the OnPaint handler can repaint the message display when necessary. The CMainWindow data member m_nMsgPos marks the current position in the array—the index of the array element that the next string will be copied to. m_nMsgPos is incremented each time ShowMessage is called and wrapped around to 0 when it reaches the array limit so that m_strMessages can maintain a record of the last 12 keyboard messages received.

VisualKB's CMainWindow class includes OnKeyUp, OnSysKeyDown, OnSysKeyUp, and OnSysChar handlers whose only purpose is to echo keyboard messages to the screen. Each message handler is careful to call the corresponding message handler in the base class before returning, as shown here:

 void CMainWindow::OnSysKeyDown (UINT nChar, UINT nRepCnt, UINT nFlags) {             CWnd::OnSysKeyDown (nChar, nRepCnt, nFlags); } 

Nonclient-area mouse messages and system keyboard messages are frequently catalysts for other messages, so it's important to forward them to the base class to permit default processing to take place.



Programming Windows with MFC
Programming Windows with MFC, Second Edition
ISBN: 1572316950
EAN: 2147483647
Year: 1999
Pages: 101
Authors: Jeff Prosise

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net