Share via


This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

MIND

CPopupText for Home-grown Tooltips, Controlling Application Instantiation

Paul DiLascia

Code for this article: CQA0900.exe (109KB)

Q I have a CListBox-derived class that implements an owner-draw listbox. I want each line of the listbox to be completely visible when the mouse points to the item. That is, when the user moves the mouse over a line I want to show a tooltip window with the entire text of the line at the current mouse position in case the text is wider than the listbox. I tried to do this with CToolTipCtrl::AddTool using the third parameter, but it didn't work. Could you please suggest a solution to this problem?
Mihai Frintu
Bucharest

A I sympathize with your efforts to use CToolTipCtrl; for some reason no one ever seems to get MFC tooltips to work right the first time. I talked about MFC tooltips in the March 1997 issue of MSJ, and Roger Jack wrote the article "Tiptoe Through the Tooltips with Our All-Encompassing ToolTip Programmer's Guide" in the April 1997 issue of MSJ. Roger's article is the most thorough tooltip guide I know of.
      You could definitely make tooltips work, but what you want is so simple there's almost no need. So just to be contrarian, I'll show you a home-grown solution that uses a class, CPopupText (see Figure 1), that I wrote for the December 1999 issue of MSJ. CPopupText implements a popup window that looks like a tooltipâ€"black text on a light yellow background. CPopupText is like any CWnd-derived object; to use it you must instantiate and create it.

  CPopupText wndText;
  
wndText.Create(...);

 

      The Create arguments are what you'd expectâ€"parent window, style, IDâ€"plus a CPoint that says where the upper-left corner is. Normally, you'll create the window as invisible (WS_VISIBLE off); then when you want to show a tip, you call SetWindowText to set the text, and ShowWindow to show it.

  wndText.SetWindowText("hello, world");
  
wndText.ShowWindow(SW_SHOWNA);

 

      CPopupText changes the size of the window to fit the text exactly. By default, CPopupText uses the same font as the status line (as defined by lfStatusFont in the NONCLIENTMETRICS struct returned by SystemParametersInfo(SPI_GETNONCLIENTMETRICS)). It's important to use SW_SHOWNA since you don't want to make the window active, you just want to show it. Alternatively, you can call a special function CPopupText::ShowDelayed, which shows the tip after a specified number of milliseconds elapse. If you give a delay of zero, CPopupText shows the tip immediately; you can use this feature instead of calling ShowWindow. To hide the tip or cancel ShowDelayed, call CPopupText::Cancel.
      OK, so much for CPopupText. Let's move on to the list control. The goal is to implement some kind of doodad you can plop in your listbox to add the show-wide-text feature. CListBoxTipHandler is the answer I came up with. Figure 2 shows the source and Figure 3 shows it in action in my test program, LCTest.

Figure 3 LCTest

Figure 3 LCTest

      Using CListBoxTipHandler is pretty easy. All you have to do is instantiate and Init it. LCTest does it in a dialog.

  class CMyDialog {
  
CListBox m_wnd_List; // normal listbox
CListBoxTipHandler
m_tipHandler; // tip handler
•••
};

BOOL CMyDialog::OnInitDialog(){
•••
m_tipHandler.Init(&m_wndList);
•••
}

 

Your listbox magically acquires the tip feature shown in Figure 3.
      CListBoxTipHandler is designed to be as easy as possible to use, but how does it work? If you look behind the curtain, you'll see CListBoxTipHandler is derived from CSubclassWnd, the class I use over and over again in my columns. CSubclassWnd lets you subclass a window in MFC without deriving a new class. That's important here; if I had derived a new CListBoxWithTips from CListBox, you wouldn't be able to use it if you had already derived your own CMyListBox. You would have to manually graft my changes onto your classâ€"yuck! CSubclassWnd lets CListBoxTipHandler subclass your listbox by instantiation, not derivation. For details, see C++ Q&A in the June 1997 issue of MSJ and the "Fun with MFC Goodies" trilogy beginning in the January 1997 issue of MSJ.
      When you call Init, CListBoxTipHandler subclasses the listbox, then intercepts all messages sent to your listbox. The only message that it cares about is WM_MOUSEMOVE. When the user moves the mouse in the listbox, the tip handler checks to see if the item under the mouse is completely visible; if not, it uses CPopupText to display the item text in a tip at the exact same position as the item itself. The details are mostly straightforward, therefore I will only highlight a couple of the tricky parts (see Figure 2 for all the details).
      If the user moves the mouse off an item for which a tip is displayed, CListBoxTipHandler calls CPopupText::Cancel to hide the tip. This works fine if the user moves from one item to another, but what happens if the user moves the mouse outside the listbox entirely? There's no way to know that a particular WM_MOUSEMOVE is the last one. It's the proverbial "get off the bus one stop before I do" paradox. To avoid it, CListBoxTipHandler captures the mouse on behalf of the listbox, so all mouse messages go to it, even when the user moves the mouse outside the listbox. When that happens, CListBoxTipHandler releases the mouse.
      That was trick number one. Trick number two has to do with activationâ€"or rather, nonactivation. To position the tip properly, CListBoxTipHandler calculates the rectangle and calls SetWindowPos. It's important to use SWP_NOACTIVATE here; otherwise the tip would be activated, deactivating the dialog and turning its title bar greyâ€"oops. I discovered this one the hard way. It's the same reason you must use SW_SHOWNA if you call CPopupText::ShowWindow.
      My test app, LCTest, uses a vanilla listbox, but CListBoxTipHandler should work with an owner-draw listbox, too; however, you may have to modify it to get the item text and determine its width. CListBoxTipHandler::OnGetItemInfo and CListBoxTipHandler::IsRectCompletelyVisible are the virtual functions to override. These are also the functions to change if you want to make CListBoxTipHandler work with other kinds of listbox-like controls, such as comboboxes and tree controls. You needn't bother with CListBoxTipHandler for tree controls, however, since they already have the tooltip feature built in.

Q I wrote a dialog-based application and I'm able to open an associated file when the user double-clicks a file in Windows® Explorer. However, each time the user double-clicks a new file, Windows starts a new instance of my app. I know how to make sure my application only runs one instance, but how do I tell the sole instance the name of the file to open?
Hafeez Jaffer

A The official way to handle this is to use Dynamic Data Exchange (DDE), which is a general-purpose interprocess communication mechanism that lets an application send a command such as open the document foo.txt to another application. You may already have noticed that some apps have registry entries like

  HKCR/foofile/shell/open/ddeexec = Open("%1")
  

 

which tell Windows to send the DDE Open command with the file name as an argument to the running instance when the user double-clicks a .foo file. I won't go into DDE details here (read the docs), except to say that MFC has functions OnDDEInitiate, OnDDEExecute, and so on to support DDE. Unfortunately, these functions belong to CFrameWnd, which doesn't help if you have a dialog-based app where your main window derives from CDialog.
      Well, never fear. You don't really need DDE to implement the only-one-instance behavior. You can do it easily enough yourself. You just need a way to find the other running instance and a way to make it open a new file. I encapsulated both capabilities into a class called COneInstance (see Figure 4). COneInstance works in any kind of app that has a main window, whether it is derived from CFrameWnd or CDialog. To demonstrate this, I wrote two test apps: MyEdit (a frame-based text editor) and Dlg1Inst (a dialog-based app). You can download both apps from the link at the top of this article.
      To use COneInstance, you must do three things: instantiate, initialize, and then call the object from your app's InitInstance function. Since you only need one COneInstance object for the entire app, and since COneInstance works through your app's main window, it's best to instantiate it is as a global (static) member of your main window class. For example:

  // in mainfrm.h
  
class CMainFrame : public CFrameWnd {
public:
static COneInstance OnlyInstance;
};

 

      The reason for making OnlyInstance static is that you need to call it before CMainFrame has been created, as you'll soon see. When you instantiate COneInstance, you must supply two arguments: the name of your main window class and an integer message ID different from any other WM_ message ID your main window uses. I call this the ident message. For MyEdit, it looks like the following:

  // in mainfrm.cpp
  
static LPCTSTR MYCLASSNAME = _T("MyEditApp10");
const WM_MAINFRM_ISME = RegisterWindowMessage
("WM_MyEditApp10_IsMe");
COneInstance CMainFrame::OnlyInstance
(MYCLASSNAME, WM_MAINFRM_ISME);

 

      The window class name is MyEditApp10 and the ident message ID is WM_MAINFRM_ISME. Note that you must modify your main window's PreCreateWindow function to register and use MYCLASSNAME. (Oops, I guess there are four things you have to do.) COneInstance uses the class name and ident message to find other instances of your app.

  CWnd* COneInstance::FindOtherInstance() const
  
{
CWnd* pWnd = CWnd::FindWindow(m_sClassName, NULL);
return (pWnd && pWnd->SendMessage(m_iIdentMsg)) ? pWnd : NULL;
}

 

      FindWindow finds the other window with the same class name as yours and, to ensure it really is yours, sends the special ident message looking for a TRUE response. COneInstance handles the message itself, so you don't have to do anything to make this part work, except supply the message ID.

  LRESULT COneInstance::WindowProc(...)
  
{
if (msg==m_iIdentMsg)
return 1;
return CSubclassWnd::WindowProc(...);
}

 

      If some other window happened to have the same class name as yours, it still wouldn't handle m_iIdentMsg (in this case WM_ MAINFRM_ISME), so its WindowProc would return zero. Of course, you must remember to hook up the COneInstance object after your main window is createdâ€"that's step number two.

  int CMainFrame::OnCreate(...)
  
{

•••
OnlyInstance.Init(this);
return 0;
}

 

      For a dialog, the place to call Init is OnInitDialog. Whatever! Once you've instantiated your COneIn-stance object and called Init, you're ready to invoke it. All you need is few lines in your app's InitInstance function.

  // standard MFC
  
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);

// do only-one-instance behavior
if (CMainFrame::OnlyInstance.
OpenOtherInstance(cmdInfo.m_strFileName))
return FALSE; // exit this instance

 

      OpenOtherInstance does everything you want: if no other instance is running, it returns NULL and control flows through the rest of InitInstance. If there is another instance, OpenOtherInstance tells it to open the new file and activates the window (there's an optional argument to prevent activation). My sample code assumes the file name is the first argument on the command line, which MFC parses for you into cmdInfo.m_strFileName.
      I've already shown you how COneInstance uses FindWindow and the ident message to find the other instance of your app, but how does it make this instance open a new file? The obvious thing would be to have another WM_ message and pass the file name as LPARAMâ€"except for just one little problem: you can't pass a pointer from one process to another; it has no meaning in the other address space.
      Fortunately, Windows has just the message for passing a string from one app to another: WM_COPYDATA. You fill out a little struct calledâ€"what else?â€"a COPYDATASTRUCT and pass it as LPARAM. Windows copies your data into the other process's address space and, presto, it can read the string. Once again, COneInstance handles WM_COPYDATA so you don't have to.

  LRESULT COneInstance::WindowProc(...)
  
{
if (msg==WM_COPYDATA) {
CString sFileName =
// get from COPYDATASTRUCT
AfxGetApp()->OpenDocumentFile(sFileName);
return TRUE; // handled
}
return CSubclassWnd::WindowProc(...);
}

 

      This is fine and dandy, but what happens if you're already using WM_COPYDATA for something else? WM_COPYDATA lets you specify a code in the COPYDATASTRUCT. You can use different codes to disambiguate different types of WM_COPYDATA messages. COneInstance has an optional third constructor argument I didn't tell you about, the WM_COPYDATA code to use.
      So far I've been assuming you have a frame-based app. For your dialog-based app there are two other tricks you need to know. First, you need the window class name. Quickâ€"what's the window class name for a dialog? (This would be a good question for the Windows edition of Trivial Pursuit.) Answer: It's #32770. All dialogs have this class name. That's why COneInstance needs the ident messageâ€"to distinguish your dialog from others that could be running.
      Dialog trick number two: you have to override CWinApp:: OpenDocumentFile to open the new file. Normally, this function works only in doc/view apps; if you call it in a dialog-based app, you'll have a major boo-boo. In my sample Dlg1Inst app, I overrode OpenDocumentFile like so:

  CDocument* CMyApp::OpenDocumentFile(LPCTSTR lpszFileName)
  
{
CString s;
s.Format("My Dialog: %s",lpszFileName);
m_pMainWnd->SetWindowText(s);
return NULL;
}

 

My implementation just sets the caption to show the name of the currently open file. In a real application, you would actually do something useful.
      Now, there is just one little bug in my implementation of COneInstance as I've presented it. I have no doubt that astute readers have already anticipated the problem. Can you guess? Hint: what if there's some other dialog running that's not an instance of your app? For the answer, you'll have to read the sourceâ€"or download it from the link at the top of this article. Ciao!

Paul DiLascia is the author of* Windows++: Writing Reusable Windows Code in C++ *(Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or https://www.dilascia.com.

From the September 2000 issue of MSDN Magazine.