C++ Q&A

Retrieving Hidden Path Names, Mouse Events in C#

Paul DiLascia

Code download available at:CQA0309.exe(164 KB)
Browse the Code Online

Q In your January 2002 column, you answered a question about customizing the Open File dialog. You showed how to get selected file names directly from the list control by calling GetItemText, but you pointed out that there's no way to get the true file name from the list control when the user has checked "Hide extensions for known file types" in the Windows® Explorer folder options dialog. At the end of the column you said, "I'm sure there's an answer to this question, but to find out what it is, you'll have to read next month's column. Same bat time... same bat channel..." Well, I tuned to the bat channel but I never saw an answer. What gives?

Q In your January 2002 column, you answered a question about customizing the Open File dialog. You showed how to get selected file names directly from the list control by calling GetItemText, but you pointed out that there's no way to get the true file name from the list control when the user has checked "Hide extensions for known file types" in the Windows® Explorer folder options dialog. At the end of the column you said, "I'm sure there's an answer to this question, but to find out what it is, you'll have to read next month's column. Same bat time... same bat channel..." Well, I tuned to the bat channel but I never saw an answer. What gives?

Several Readers

A Gosh, do you guys really pay attention to everything I write? In fact, the solution is quite simple. The reason I never published it is that it involves undocumented features I was never able to confirm with the Redmondtonians. Then, well, I kind of forgot about it. So let me now set the record straight lest you think the bats have flown the coop. (Hey, bats don't live in coops!)

A Gosh, do you guys really pay attention to everything I write? In fact, the solution is quite simple. The reason I never published it is that it involves undocumented features I was never able to confirm with the Redmondtonians. Then, well, I kind of forgot about it. So let me now set the record straight lest you think the bats have flown the coop. (Hey, bats don't live in coops!)

In the January 2002 column, I wrote a class called CFileDlgHelper that provides handy functions for dealing with file dialogs. There's CFileDlgHelper::GetListCtrl to get the list control that holds the file names and GetItemName to get the name of an item from the list control. As I pointed out back then, the problem with GetItemName (which calls CListCtrl::GetItemText) is that if the user has decided to hide extensions for known file types in Windows Explorer, the return string does not include the extension. For example, there's no way to distinguish bugs.txt from bugs.bmp since GetItemName returns bugs for both files. What to do?

The (undocumented) trick is knowing that in the Open File dialog, the list item data—that is, the DWORD returned by CListCtrl::GetItemData—is actually the item's PIDL. For those of you unfamiliar with shell programming, a PIDL uniquely identifies a shell object such as a file, folder, link, disk drive, or pseudo-object like the My Documents folder. For ordinary files, the PIDL is just the file's relative path name in wide characters.

If you know the item's data is really its PIDL, you can get its path name. All it requires is a bit of yucky shell programming. First, send the dialog a CDM_GETFOLDERIDLIST message to get the PIDL of the current folder. This is a two-step process: send CDM_GETFOLDERIDLIST once with a NULL buffer to get the length, and again after allocating a real buffer. Next you have to combine the folder's PIDL (path name) and the item's PIDL (relative path name) to get the full PIDL (full path name). You have to get the folder's IShellFolder and call IShellFolder::GetDisplayNameOf with the magic flag SHGDN_FORPARSING, which gets the full path name, including the extension.

To protect you from all those programming grungies, I added a new function, CFileDlgHelper::GetItemPathName, that does what it claims. To use it all you have to do is write the following:

// get true path name of ith list item 
CString path = m_dlgHelper.GetItemPathName(i);

Figure 1 Modified Test Program

Figure 1** Modified Test Program **

What could be easier? CFileDlgHelper::GetItemPathName is just a wrapper that calls CFileDlgHelper::GetDisplayNameOf, which comes in several overloaded flavors, one of which does all the work. Figure 1 shows the modified test program displaying the true paths of selected items with the same base name. Figure 2 shows the new functions in all their glory. As always, you can grab the full source from the link at the top of this article. You know what they say—better late than never!

Figure 2 FileDlgHelper.cpp

////////////////////////////////////////////////////////////////
// MSDN Magazine -- January 2002
// If this code works, it was written by Paul DiLascia.
// If not, I don't know who wrote it.
// Compiles with Visual C++ 6.0 for Windows XP and probably Windows 2000 
// too.
// Set tabsize = 3 in your editor.
//
#include "StdAfx.h"
#include "FileDlgHelper.h"
•••
//////////////////
// Get display name of item in file open dialog. Flags tell how.
// SHGDN_FORPARSING gets the full path name even when user has
// checked "Hide extensions for known file types" in Explorer.
//
CString CFileDlgHelper::GetDisplayNameOf(int i, DWORD flags)
{
   CListCtrl* plc = GetListCtrl();
   ASSERT(plc);
   return GetDisplayNameOf((LPCITEMIDLIST)plc->GetItemData(i), flags);
}

// Overload to get from PIDL.
CString CFileDlgHelper::GetDisplayNameOf(LPCITEMIDLIST pidl, DWORD flags)
{
   CString path;

   // First, get PIDL of current folder by sending CDM_GETFOLDERIDLIST
   // Get length first, then allocate.
   int len = m_pDlg->GetParent()->SendMessage(CDM_GETFOLDERIDLIST, 0,NULL);
   if (len>0) {
      CComQIPtr<IMalloc> malloc;
      SHGetMalloc(&malloc);
      LPCITEMIDLIST pidlFolder = (LPCITEMIDLIST)malloc->Alloc(len);
      ASSERT(pidlFolder);
      m_pDlg->GetParent()->SendMessage(CDM_GETFOLDERIDLIST, len,
         (LPARAM)(void*)pidlFolder);

      // Now get IShellFolder for pidlFolder
      CComQIPtr<IShellFolder> ishDesk;
      SHGetDesktopFolder(&ishDesk);
      CComQIPtr<IShellFolder> ishFolder;
      ishDesk->BindToObject(pidlFolder, NULL,
         IID_IShellFolder, (void**)&ishFolder);

      // finally, get the path name from pidlFolder
      path = GetDisplayNameOf(ishFolder, pidl, flags);

      malloc->Free((void*)pidlFolder);
   }
   return path;
}

// Overload to get from IShellFolder/pidl
CString CFileDlgHelper::GetDisplayNameOf(IShellFolder* ish,
   LPCITEMIDLIST pidl, DWORD flags)
{
   CString name;
   STRRET str;
   str.uType = STRRET_WSTR;
   if (SUCCEEDED(ish->GetDisplayNameOf(pidl, flags, &str))) {
      name = str.pOleStr;
   }
   return name;
}

//////////////////
// Get underlying list control
//
CListCtrl* CFileDlgHelper::GetListCtrl()
{
   ASSERT(m_pDlg);
   CWnd* pWnd = m_pDlg->GetParent()->GetDlgItem(lst2);
   ASSERT(pWnd);
   CListCtrl* plc = (CListCtrl*)pWnd->GetDlgItem(1);
   ASSERT(plc);
#ifdef _DEBUG
   char classname[32];
   ::GetClassName(plc->m_hWnd, classname, sizeof(classname));
   ASSERT(strcmp(classname,"SysListView32")==0);
#endif
   return plc;
}

Q As a C++/MFC developer, I'm used to being able to provide right mouse click functionality to my user. I have just moved over to C# and the .NET Framework. Where has the right mouse click gone on most of the controls? Can I make it available on such things as textboxes, panels, labels, and the main window background?

Q As a C++/MFC developer, I'm used to being able to provide right mouse click functionality to my user. I have just moved over to C# and the .NET Framework. Where has the right mouse click gone on most of the controls? Can I make it available on such things as textboxes, panels, labels, and the main window background?

David Rogers

A Sometimes the .NET Framework is so easy that C++ programmers can't figure it out. That's because they're used to doing everything the hard way. If all you want to do is display a context menu, there's no need to mess with events. Just set your control's ContextMenu property, like so:

ContextMenu myContextMenu = ...
 myControl.ContextMenu = myContextMenu;

That's all. Now your menu is automatically displayed when the user right-clicks.

A Sometimes the .NET Framework is so easy that C++ programmers can't figure it out. That's because they're used to doing everything the hard way. If all you want to do is display a context menu, there's no need to mess with events. Just set your control's ContextMenu property, like so:

ContextMenu myContextMenu = ... 
myControl.ContextMenu = myContextMenu;

That's all. Now your menu is automatically displayed when the user right-clicks.

If you want to do something fancier like implement right-drag, you need to use events. Coming from MFC, it's natural to think of implementing OnRButtonDown or OnRButtonUp, but that's not how Windows Forms works. While many window classes do provide virtual functions that you can override to handle common messages (for example, Form.OnActivated and Form.OnClosed), in general, when programming for the .NET Framework you must learn to think in terms of events, and look at the events a class exposes.

Figure 3 lists some of the more common events exposed by the Control class. I've highlighted the mouse events: MouseDown, MouseEnter, MouseHover, MouseLeave, MouseMove, and MouseUp, which do as their names imply. So if you want to handle the right-button down event, you can install a MouseDown event handler and check the MouseEventArgs for MouseEventArgs.Button == MouseButtons.Right. The following code snippet illustrates this:

public class MyControl : Control
{
   // ctor
   public MyControl() {
      // install button-down event handler 
      this.myctl1.MouseDown += 
         new MouseEventHandler(this.OnMouseDown);
   }

   // hander
   private void OnMouseDown(object sender, MouseEventArgs e) {
      if (e==MouseButtons.Right)
         // do something
   }
}

Figure 3 Public Events for the Control Class

Click HelpRequested MouseLeave
ContextMenuChanged Invalidated MouseMove
DoubleClick KeyDown MouseUp
DragDrop KeyPress MouseWheel
DragEnter KeyUp Move
DragLeave Layout Paint
DragOver Leave ParentChanged
EnabledChanged LostFocus Resize
Enter MouseDown SizeChanged
FontChanged MouseEnter StyleChanged
GotFocus MouseHover TextChanged

 

Figure 3 reveals three useful mouse events not directly available through MFC: MouseEnter, MouseHover, and MouseLeave. In Win32®, these are available through the special TrackMouseEvent function. The .NET Framework raises MouseEnter and MouseLeave when the mouse enters or leaves your control. These events are useful when you want to highlight your control as the mouse is moved over it, for instance highlighting a link in a browser. The .NET Framework raises MouseHover when the mouse lingers over your control for a determined amount of time (as specified by SystemParametersInfo(SPI_GETMOUSEHOVERTIME)). It's useful for tooltips and various other timed popup-doohickeys.

How do all these mouse events relate to the familiar Click event? Think of Click as a high-level, logical event, as opposed to the more low-level mouse events. The meaning of Click depends on the type of control. For example, Forms raise a Click event if the user presses either the left or right button, whereas TextBoxes only raises Click for the left button. Figure 4 shows what Click means for various kinds of controls.

Figure 4 Events Raised by Click

Control Left Mouse Click Left Mouse Double-click Right Mouse Click Right Mouse Double-click Middle Mouse Click Middle Mouse Double-click
MonthCalendar, DateTimePicker, RichTextBox, HScrollBar, VScrollBar None None None None None None
Button, CheckBox, RadioButton Click Click, Click None None None None
ListBox, CheckedListBox, ComboBox Click Click, DoubleClick None None None None
TextBox, DomainUpDown, NumericUpDown Click Click, DoubleClick None None None None
*TreeView, *ListView Click Click, DoubleClick Click Click, DoubleClick None None
ProgressBar TrackBar Click Click, Click Click Click, Click Click Click, Click
Form, DataGrid, Label, LinkLabel, Panel,GroupBox, PictureBox, Splitter, DoubleClick, StatusBar,ToolBar, TabPage, **TabControl Click Click, DoubleClick Click Click, DoubleClick Click Click, DoubleClick
* The mouse pointer must be over a child object (TreeNode or ListViewItem)
** The TabControl must have at least one TabPage in its TabPages collection

 

Finally, when desperate, you can always override Control.WndProc to handle any WM_XXX message you want—for example, WM_NCLBUTTONDOWN:

protected override void WndProc(ref Message m)
{
    // value comes from winuser.h
    const int WM_NCLBUTTONDOWN = 0x00A1;
    if (m.Msg==WM_NCLBUTTONDOWN) {
        //do something
    }
    base.WndProc(ref m); // don't forget!
}

Figure 5 MouseTrap

Figure 5** MouseTrap **

I wrote a short program called MouseTrap that shows how. Figure 5 shows it running. If you go the WndProc route, you're more or less programming in C because you have to deal with IntPtrs to get the message params—but since you're already familiar with C++, you'll feel right at home!

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 https://www.dilascia.com.