C++ Q&A: Parent and Child Window Captions, More...

We were unable to locate this content in de-de.

Here is the same content in en-us.

C++ Q&A
Parent and Child Window Captions, More MoveDlg
Paul DiLascia

Code download available at: CQA0307.exe (811 KB)
Browse the Code Online

Q I'm having trouble setting the multi-document interface (MDI) parent's text in C#. When the child form is in maximized state and activated, the child form's text gets appended to the parent text and is displayed in the form "Parent Text - [Child Text]". How can I override this default behavior and set custom title text?
Q I'm having trouble setting the multi-document interface (MDI) parent's text in C#. When the child form is in maximized state and activated, the child form's text gets appended to the parent text and is displayed in the form "Parent Text - [Child Text]". How can I override this default behavior and set custom title text?
Srinivasa Raghavan

A If you were using MFC, you could simply override the virtual function CFrameWnd::OnUpdateFrameTitle in the parent frame. But what about in the Microsoft® .NET Framework? To override the default MDI caption, you have to first understand that this behavior comes from MDI itself via the Windows core API, not MFC or the .NET common language runtime (CLR). When you create an MDI app, your frame and child windows get special window procedures—DefFrameProc and DefMDIChildProc—that handle various WM_MDIXXX messages as well as other messages like WM_SIZE, WM_SYSCOMMAND, and others to implement MDI behavior.
A If you were using MFC, you could simply override the virtual function CFrameWnd::OnUpdateFrameTitle in the parent frame. But what about in the Microsoft® .NET Framework? To override the default MDI caption, you have to first understand that this behavior comes from MDI itself via the Windows core API, not MFC or the .NET common language runtime (CLR). When you create an MDI app, your frame and child windows get special window procedures—DefFrameProc and DefMDIChildProc—that handle various WM_MDIXXX messages as well as other messages like WM_SIZE, WM_SYSCOMMAND, and others to implement MDI behavior.
If you're writing pure C code, you're responsible for using DefFrameProc and DefMDIChildProc when you create your windows; MFC and the .NET Framework perform these details invisibly when you create CMDIFrameWnd/CMDIChildWnd in MFC, or when you set Form.IsMdiContainer and Form.MdiParent in the .NET Framework. Either way, it's the underlying user kernel, and in particular DefFrameProc, that concatenates the parent and child captions to create the main window caption when the MDI child is maximized. With that understanding, I'll show you how to override MDI. I'll use the C# sample Scribble MDI app that's in the MSDN® Library (search for "scribble sample" or go to Scribble Sample: Visual C# MDI Drawing Application). The basic idea is to override WM_GETTEXT in Scribble's MainWindow. First, you have to add a couple of data members, NormalText and MaximizedText, to hold—what else?—the captions for normal and maximized state, as shown in the following code:
// in Scribble.cs, MainWindow class
private String NormalText = "Scribble2";
private String MaximizedText = "Window is now maximized";
If you want other classes to access these strings, you can implement them as properties instead of data members:
private String normaltext;
public String NormalText
{
  get { return normaltext; }
  set { normaltext = value; }
}
Since MainWindow is the only class in my sample to access the strings, I didn't bother with properties. With the new data members in place, all you have to do is override WM_GETTEXT to return the maximized text if the child window is maximized, and the normal text otherwise. But how do you override WM_GETTEXT? Windows.Forms provides virtual counterparts for some, but not all, WM_XXX messages. For example, there's OnResize for WM_SIZE, but there's no OnGetText for WM_GETTEXT. But never fear—if there's no virtual function, you can always override the catchall handler WndProc. You have to know the ID of the message you're looking for—in this case, WM_GETTEXT is 0x000D—which you can find by running Spy or simply by looking in the Windows® SDK file winuser.h. Once you sink to the level of WndProc, you're basically writing in C since everything, including strings, is passed as WPARAMs and LPARAMs. For WM_GETTEXT, Message.LParam is a pointer to a char* buffer and Message.WParam is its length. This means you have to perform a little interop to copy the text string into the caller's buffer. Fortunately it's not rocket science; Figure 1 shows the details (full source code is available at the link at the top of this article). Now when the MDI child is maximized, the main window caption displays the text "Window is now maximized" as in Figure 2; otherwise the main window displays the usual caption, as shown in Figure 3.
public class MainWindow : System.Windows.Forms.Form
{
   private String NormalText = "Scribble2";
   private String MaximizedText = "Window is now maximized";

   // Handle WM_GETTEXT: Return maximized or 
   // normal text, depending on
   // state of active MDI child window.
   protected override void WndProc(ref Message m)
   {
      const int WM_GETTEXT = 0x000D;
      if (m.Msg==WM_GETTEXT) {
         Form active = this.ActiveMdiChild;
         String s = active!=null &&
            active.WindowState==FormWindowState.Maximized ? MaximizedText :
            NormalText;
         char[] c = s.ToCharArray();
         IntPtr buf = m.LParam;
         int len = c.Length;
         Marshal.Copy(c, 0, buf, Math.Min((int)m.WParam, len));
         m.Result = (IntPtr)len;
         return;
      }
      base.WndProc(ref m);
   }

      ••• // rest of MainWindow unchanged from Scribble sample
   
}
}
Figure 2 Maximized Caption 
Figure 3 Normal Captions 

Q After reading your suggestion to implement window dragging by handling the WM_NCHITTEST message in the December 2002 issue of MSDN Magazine, I tried it in a program that used to do all that WM_LBUTTONDOWN, WM_MOUSEMOVE, and WM_LBUTTONUP stuff. (Of course, I wanted to do it the cool way.) But then I ran into a little problem. My window used to have a context menu, which wouldn't appear anymore. So I changed the message handler (as shown in Figure 4). This works fine for my program, but I wonder if this is the right way to do it?
Q After reading your suggestion to implement window dragging by handling the WM_NCHITTEST message in the December 2002 issue of MSDN Magazine, I tried it in a program that used to do all that WM_LBUTTONDOWN, WM_MOUSEMOVE, and WM_LBUTTONUP stuff. (Of course, I wanted to do it the cool way.) But then I ran into a little problem. My window used to have a context menu, which wouldn't appear anymore. So I changed the message handler (as shown in Figure 4). This works fine for my program, but I wonder if this is the right way to do it?
//////////////////
// Revised hit-test handler to fake out Windows into thinking user has
// clicked on the caption area of a dialog or window in order to
// implement move-by-dragging-the-client-area. 
//
UINT CMainFrame::OnNcHitTest(CPoint point)
{
    // Not returning HTCAPTION when the right button is pressed is 
    // important to keep the ability to generate WM_CONTEXTMENU messages.
    // 
    BOOL bMouseButtonsAreSwapped = GetSystemMetrics(SM_SWAPBUTTON);
    int logicalRightButton = bMouseButtonsAreSwapped ? VK_LBUTTON : 
        VK_RBUTTON;
    short rightMouseButtonState = GetKeyState(logicalRightButton);
    BOOL  bRightMouseButtonIsDown = rightMouseButtonState & 0x8000;

    CRect rc;
    GetClientRect(&rc);
    ClientToScreen(&rc);
    return (rc.PtInRect(point) && !bRightMouseButtonIsDown) ? HTCAPTION :
        CFrameWnd::OnNcHitTest(point);
}
Rocco Matano

A I love it when readers answer their own questions; it saves me so much time and energy. Just to refresh the rest of the readers, the December 2002 column showed how to implement a feature that lets users move a dialog or window by dragging its client area. The trick is to override OnNcHitTest to return HTCAPTION instead of HTCLIENT, even when the mouse is actually in the client area, to fool Windows into performing its drag/move functionality. But as Rocco discovered, this messes up your context menu. If you return HTCAPTION instead of HTCLIENT, Windows doesn't know to send a WM_CONTEXTMENU. Rocco's solution is to call GetKeyState in order to check for right-button down before doing the HTCAPTION trick. Kudos to Rocco for remembering to check for swapped left/right mouse buttons—would you have remembered that?
A I love it when readers answer their own questions; it saves me so much time and energy. Just to refresh the rest of the readers, the December 2002 column showed how to implement a feature that lets users move a dialog or window by dragging its client area. The trick is to override OnNcHitTest to return HTCAPTION instead of HTCLIENT, even when the mouse is actually in the client area, to fool Windows into performing its drag/move functionality. But as Rocco discovered, this messes up your context menu. If you return HTCAPTION instead of HTCLIENT, Windows doesn't know to send a WM_CONTEXTMENU. Rocco's solution is to call GetKeyState in order to check for right-button down before doing the HTCAPTION trick. Kudos to Rocco for remembering to check for swapped left/right mouse buttons—would you have remembered that?
I don't think there's anything wrong with Rocco's solution, unless by a fluke of timing—caused perhaps by a hardware interrupt or other unlikely event—the mouse button is already up by the time the window gets WM_NCHITTEST. After probing the depths of the MSDN Library and reading more than anyone cares to hear about the details of mouse-message processing in Windows, I was unable to definitively determine whether this is even possible. Call me paranoid, but somehow I'm reluctant to use GetKeyState to check for mouse button states. And who should have to worry whether the user is left-handed? Never fear, in Windows there's always another way. In this case, all you have to do is override OnNcRButtonUp:
// in OnNcRButtonUp
if (nHitTest==HTCAPTION && /* point in client area */) 
  SendMessage(WM_CONTEXTMENU, ...);
else
   // default thing
In other words, if the hit-test code says the mouse is in the caption, but it's really in the client area, then you should send yourself a WM_CONTEXTMENU message. This is what Windows would've done if I hadn't fooled it by returning HTCAPTION in OnNcHitTest. Figure 5 shows the details. Note that if you want the dialog to display the system menu when the user right-clicks on the caption, your context menu handler should pass control to the base handler if the cursor isn't in the client area. This is true whether you're using the hit-test trick or not; even in a vanilla dialog, you should perform this check. Figure 6 and Figure 7 show the revised MoveDlg program from my December column, with system and context menus.
////////////////////////////////////////////////////////////////
// MSDN Magazine — July 2003
// Compiles with Visual Studio .NET on Windows XP. Tab size=3.
//
#include "stdafx.h"

// As in my original December 2002 column

//////////////////
// Typical dialog.
// Override OnNcHitTest to allow dragging by background.
//
class CMyDialog : public CDialog {
    •••
protected:
   // helper
   BOOL PtInClientRect(CPoint p);

   // virtual override/message handlers
   afx_msg UINT OnNcHitTest(CPoint pt);
   afx_msg void OnNcRButtonUp(UINT nHitTest, CPoint pt);
   afx_msg void OnContextMenu(CWnd* pWnd, CPoint pos);
};

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
   ON_WM_NCHITTEST()
   ON_WM_NCRBUTTONUP()
   ON_WM_CONTEXTMENU()
END_MESSAGE_MAP()

//////////////////
// Helper function to test if screen point lies in client area
//
BOOL CMyDialog::PtInClientRect(CPoint p)
{
   CRect rc;
   GetClientRect(&rc);
   ClientToScreen(&rc);
   return rc.PtInRect(p);
}

//////////////////
// Non-client hit-test handler to move window by its client area.
// If the user clicks anywhere on the client area, pretend it's the
// caption. Windows does the rest!
//
UINT CMyDialog::OnNcHitTest(CPoint pt)
{
   return PtInClientRect(pt) ? HTCAPTION : CDialog::OnNcHitTest(pt);
}

//////////////////
// User let go of right button in "non-client" area. If mouse is really
// in the client area because I tricked Windows, send myself a
// WM_CONTEXTMENU message.
//
void CMyDialog::OnNcRButtonUp(UINT nHitTest, CPoint pt)
{
   if (nHitTest==HTCAPTION && PtInClientRect(pt)) 
      SendMessage(WM_CONTEXTMENU, (WPARAM)m_hWnd, MAKELPARAM(pt.x,pt.y));
   else
      CDialog::OnNcRButtonUp(nHitTest, pt);
}

//////////////////
// Display context menu. If mouse is in the client area, display
// resource menu—otherwise, let base class handle it to display system
// menu. This is required even in vanilla dialog (i.e., without the
// hit-test trick).
//
void CMyDialog::OnContextMenu(CWnd* pWnd, CPoint p)
{
   if (PtInClientRect(p)) {
      CMenu menu;
      VERIFY(menu.LoadMenu(IDR_CONTEXTMENU));
      CMenu* pSubMenu = menu.GetSubMenu(0);
      pSubMenu->TrackPopupMenu(0, p.x, p.y, this);
      return;
   }
   CDialog::OnContextMenu(pWnd, p);
}
Figure 6 MoveDlg with System Menu 
Figure 7 MoveDlg with Context Menu 
So which solution should you use? Rocco's has the advantage of being confined to a single function, but it seems too low level. You shouldn't have to worry about virtual key states or whether the user has swapped mouse buttons unless you're writing drivers or other grungy stuff.

Send your questions and comments for Paul to  cppqa@microsoft.com.


Paul DiLascia is a freelance writer, consultant, and Web/UI designer-at-large. He is the author of Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992). Paul can be reached at http://www.dilascia.com.

Page view tracker