14

I want to show the Windows file properties dialog for a file from my C++ code (on Windows 7, using VS 2012). I found the following code in this answer (which also contains a full MCVE). I also tried calling CoInitializeEx() first, as mentioned in the documentation of ShellExecuteEx():

// Whether I initialize COM or not doesn't seem to make a difference.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

SHELLEXECUTEINFO info = {0};

info.cbSize = sizeof info;
info.lpFile = L"D:\\Test.txt";
info.nShow  = SW_SHOW;
info.fMask  = SEE_MASK_INVOKEIDLIST;
info.lpVerb = L"properties";

ShellExecuteEx(&info);

This code works, i.e. the properties dialog is shown and ShellExecuteEx() returns TRUE. However, in the Details tab, the size property is wrong and the date properties are missing:

Properties window opened via my program

The rest of the properties in the Details tab (e.g. the file attributes) are correct. Strangely, the size and date properties are shown correctly in the General tab (left-most tab).

If I open the properties window via the Windows Explorer (file → right-click → Properties), then all properties in the Details tab are shown correctly:

Properties window opened via Windows Explorer

I tried it with several files and file types (e.g. txt, rtf, pdf) on different drives and on three different PCs (1x German 64-bit Windows 7, 1x English 64-bit Windows 7, 1x English 32-bit Windows 7). I always get the same result, even if I run my program as administrator. On (64-bit) Windows 8.1 the code is working for me, though.

My original program in which I discovered the problem is an MFC application, but I see the same problem if I put the above code into a console application.

What do I have to do to show the correct values in the Details tab on Windows 7? Is it even possible?

Community
  • 1
  • 1
honk
  • 9,137
  • 11
  • 75
  • 83
  • For full details, all three Windows was german version? Upvote from me for interesting question. – user2120666 Jan 31 '17 at 22:05
  • Funny. FWIW I can reproduce this on a German Windows 7 (with English UI Language) using a simple test like [here](http://stackoverflow.com/a/32503655/21567). – Christian.K Feb 01 '17 at 08:16
  • 2
    A wild guess - and I currently don't have the resources to test it out - but maybe Explorer uses the `IShellItem` or `IShellItem2` (or related interfaces) directly, rather than `ShellExecuteEx`. Maybe they work as expected. – Christian.K Feb 01 '17 at 08:21
  • Just a side note: `ShellExecuteEx` docs say that you should call `CoInitializeEx`. (I tried so, but it did not help with your problem.) – Werner Henze Feb 01 '17 at 09:09
  • I think that problem is with formating digits using german locale. But currently I dont have English Win7 to test. – user2120666 Feb 01 '17 at 09:48
  • @WernerHenze I tried that with my test (see comment above), doesn't help either. – Christian.K Feb 01 '17 at 10:13
  • @WernerHenze: Thanks for the hint! I can confirm, that calling `CoInitializeEx()` doesn't make a difference. I updated the question accordingly. – honk Feb 01 '17 at 11:24
  • @user2120666: I was able to reproduce the problem on two English installations of Windows 7. I updated the question accordingly. – honk Feb 01 '17 at 11:28
  • Hmmm, interesting, I wish upvote this question again. – user2120666 Feb 01 '17 at 13:09
  • @Christian.K: I took a look at `IShellItem(2)`, but I fail to see how I can use it to show the file properties dialog. I understood that you are busy, but in case you could drop me a function name (or whatever) in a comment, I'd be happy. – honk Feb 06 '17 at 21:38
  • I didn't mean `IShellItem` in particular, but merely as a "placeholder" for something from [here](https://msdn.microsoft.com/en-us/library/windows/desktop/bb774328(v=vs.85).aspx) (I know it is a lot sorry). I will see if I can find out some more, until then, I can confirm that it is working on a German Windows 10 tough. – Christian.K Feb 07 '17 at 14:40
  • @Christian.K: No problem. Thanks for the list. I will try to dig through it. – honk Feb 08 '17 at 07:40
  • Is the calling program using administrator access rights? – Codor Mar 05 '17 at 18:33
  • There is a lot more to that CoInitializeEx() call than meets the eye. You are making a *promise*. Breaking the promise causes deadlock. The kind of app you created makes a big difference. If this is a console mode app then you need to document that. – Hans Passant Mar 05 '17 at 18:47
  • @HansPassant: My original program is an MFC application. But I see the same problem in my test program with is a console application. – honk Mar 05 '17 at 18:51
  • 2
    @Codor: I had already tried running my program with administrator rights (it's mentioned at the end of my question). This doesn't seem to make a difference. – honk Mar 05 '17 at 19:06
  • Did you try specifying the file path by IDList? The documentation seems to be contradictory for that, it says _Use either lpFile to identify the item by its file system path or lpIDList to identify the item by its PIDL._ **but** _Note SEE_MASK_INVOKEIDLIST overrides and implies SEE_MASK_IDLIST_ – zett42 Mar 06 '17 at 13:10
  • @zett42: I only tried the code as shown in the question, so I didn't try to use the `lpIDList` member, yet. But you are right, the documentation reads a bit strange. I think I have to give it a try... – honk Mar 06 '17 at 13:48

1 Answers1

3

As Raymond Chen suggested, replacing the path with a PIDL (SHELLEXECUTEINFO::lpIDList) makes the properties dialog correctly show the size and date fields under Windows 7 when invoked through ShellExecuteEx().

It seems that the Windows 7 implementation of ShellExecuteEx() is buggy since newer versions of the OS do not have an issue with SHELLEXCUTEINFO::lpFile.

There is another solution possible that involves creating an instance of IContextMenu and calling the IContextMenu::InvokeCommand() method. I guess this is what ShellExecuteEx() does under the hood. Scroll down to Solution 2 for example code.

Solution 1 - using a PIDL with ShellExecuteEx

#include <atlcom.h>   // CComHeapPtr
#include <shlobj.h>   // SHParseDisplayName()
#include <shellapi.h> // ShellExecuteEx()

// CComHeapPtr is a smart pointer that automatically calls CoTaskMemFree() when
// the current scope ends.
CComHeapPtr<ITEMIDLIST> pidl;
SFGAOF sfgao = 0;

// Convert the path into a PIDL.
HRESULT hr = ::SHParseDisplayName( L"D:\\Test.txt", nullptr, &pidl, 0, &sfgao );
if( SUCCEEDED( hr ) )
{
    // Show the properties dialog of the file.

    SHELLEXECUTEINFO info{ sizeof(info) };
    info.hwnd = GetSafeHwnd();
    info.nShow = SW_SHOWNORMAL;
    info.fMask = SEE_MASK_INVOKEIDLIST;
    info.lpIDList = pidl;
    info.lpVerb = L"properties";

    if( ! ::ShellExecuteEx( &info ) )
    {
        // Make sure you don't put ANY code before the call to ::GetLastError() 
        // otherwise the last error value might be invalidated!
        DWORD err = ::GetLastError();

        // TODO: Do your error handling here.
    }
}
else
{
    // TODO: Do your error handling here
}

This code works for me under both Win 7 and Win 10 (other versions not tested) when called from a button click handler of a simple dialog-based MFC application.

It also works for console applications if you set info.hwnd to NULL (simply remove the line info.hwnd = GetSafeHwnd(); from the example code as it is already initialized with 0). In the SHELLEXECUTEINFO reference it is stated that the hwnd member is optional.

Don't forget the mandatory call to CoInitialize() or CoInitializeEx() at the startup of your application and CoUninitialize() at shutdown to properly initialize and deinitialize COM.

Notes:

CComHeapPtr is a smart pointer included in ATL that automatically calls CoTaskMemFree() when the scope ends. It's an ownership-transferring pointer with semantics similar to the deprecated std::auto_ptr. That is, when you assign a CComHeapPtr object to another one, or use the constructor that has a CComHeapPtr parameter, the original object will become a NULL pointer.

CComHeapPtr<ITEMIDLIST> pidl2( pidl1 );  // pidl1 allocated somewhere before
// Now pidl1 can't be used anymore to access the ITEMIDLIST object.
// It has transferred ownership to pidl2!

I'm still using it because it is ready to use out-of-the-box and plays well together with the COM APIs.


Solution 2 - using IContextMenu

The following code requires Windows Vista or newer as I'm using the "modern" IShellItem API.

I wrapped the code into a function ShowPropertiesDialog() that takes a window handle and a filesystem path. If any error occurs, the function throws a std::system_error exception.

#include <atlcom.h>
#include <string>
#include <system_error>

/// Show the shell properties dialog for the given filesystem object.
/// \exception Throws std::system_error in case of any error.

void ShowPropertiesDialog( HWND hwnd, const std::wstring& path )
{
    using std::system_error;
    using std::system_category;

    if( path.empty() )
        throw system_error( std::make_error_code( std::errc::invalid_argument ), 
                            "Invalid empty path" );

    // SHCreateItemFromParsingName() returns only a generic error (E_FAIL) if 
    // the path is incorrect. We can do better:
    if( ::GetFileAttributesW( path.c_str() ) == INVALID_FILE_ATTRIBUTES )
    {
        // Make sure you don't put ANY code before the call to ::GetLastError() 
        // otherwise the last error value might be invalidated!
        DWORD err = ::GetLastError();
        throw system_error( static_cast<int>( err ), system_category(), "Invalid path" );
    }

    // Create an IShellItem from the path.
    // IShellItem basically is a wrapper for an IShellFolder and a child PIDL, simplifying many tasks.
    CComPtr<IShellItem> pItem;
    HRESULT hr = ::SHCreateItemFromParsingName( path.c_str(), nullptr, IID_PPV_ARGS( &pItem ) );
    if( FAILED( hr ) )
        throw system_error( hr, system_category(), "Could not get IShellItem object" );

    // Bind to the IContextMenu of the item.
    CComPtr<IContextMenu> pContextMenu;
    hr = pItem->BindToHandler( nullptr, BHID_SFUIObject, IID_PPV_ARGS( &pContextMenu ) );
    if( FAILED( hr ) )
        throw system_error( hr, system_category(), "Could not get IContextMenu object" );

    // Finally invoke the "properties" verb of the context menu.
    CMINVOKECOMMANDINFO cmd{ sizeof(cmd) };
    cmd.lpVerb = "properties";
    cmd.hwnd = hwnd;
    cmd.nShow = SW_SHOWNORMAL;

    hr = pContextMenu->InvokeCommand( &cmd );
    if( FAILED( hr ) )
        throw system_error( hr, system_category(), 
            "Could not invoke the \"properties\" verb from the context menu" );
}

In the following I show an example of how to use ShowPropertiesDialog() from a button handler of a CDialog-derived class. Actually ShowPropertiesDialog() is independent from MFC, as it just needs a window handle, but OP mentioned that he wants to use the code in an MFC app.

#include <sstream>
#include <codecvt>

// Convert a multi-byte (ANSI) string returned from std::system_error::what()
// to Unicode (UTF-16).
std::wstring MultiByteToWString( const std::string& s )
{
    std::wstring_convert< std::codecvt< wchar_t, char, std::mbstate_t >> conv;
    try { return conv.from_bytes( s ); }
    catch( std::range_error& ) { return {}; }
}

// A button click handler.
void CMyDialog::OnPropertiesButtonClicked()
{
    std::wstring path( L"c:\\temp\\test.txt" );

    // The code also works for the following paths:
    //std::wstring path( L"c:\\temp" );
    //std::wstring path( L"C:\\" );
    //std::wstring path( L"\\\\127.0.0.1\\share" );
    //std::wstring path( L"\\\\127.0.0.1\\share\\test.txt" );

    try
    {
        ShowPropertiesDialog( GetSafeHwnd(), path );
    }
    catch( std::system_error& e )
    {
        std::wostringstream msg;
        msg << L"Could not open the properties dialog for:\n" << path << L"\n\n"
            << MultiByteToWString( e.what() ) << L"\n"
            << L"Error code: " << e.code();
        AfxMessageBox( msg.str().c_str(), MB_ICONERROR );
    }
}
zett42
  • 25,437
  • 3
  • 35
  • 72
  • Sure. It worked for me on Win 7 x64 aswell as Win 10 x64. I could provide at least rudimentary error handling / cleanup, but currently I don't have much time either, so I hope the sample code is OK for you as is. – zett42 Mar 06 '17 at 21:02
  • 2
    I think you don't have to go this far. I think you can just get the pidl and set it in the SHELLEXECUTEINFO.lpIDList. Passing an explicit IDList means that the shell will use it directly instead of trying to create a simple one. – Raymond Chen Mar 06 '17 at 21:32
  • @zett42: Do you know if your code also works in a console application? I put it into a test console application, but the properties dialog doesn't show up for me. However, all functions return S_OK and all returned pointers seem to be valid handles. For the `eaten` values 0 is returned, though. For `hwnd` I tried `NULL` and the return value of `FindWindow()` on the console window title. Am I missing something here? Sorry for being stupid! I'll try it in my MFC program and the `lpIDList` idea later. – honk Mar 07 '17 at 08:12
  • @honk Maybe it needs a window actually belonging to your process. You don't own the console window, [it belongs to csrss.exe](https://blogs.msdn.microsoft.com/oldnewthing/20071231-00/?p=23983). Is your real application where you need this code a console or did you just use this for quick testing? – zett42 Mar 07 '17 at 08:46
  • @zett42: Ok, this might explain it. I just used a console application for quick testing. I don't have access to my original program (MFC application) right now. I'll try your code later in that application. – honk Mar 07 '17 at 08:52
  • Thanks @Raymond, using a PIDL makes this work. I somehow got more attracted by the complex solution using `IContextMenu` instead of following my own initial suggestion. – zett42 Mar 07 '17 at 12:35
  • @honk Great! I will add back my first solution if I find some time in the evening. – zett42 Mar 07 '17 at 14:39
  • @Raymond Is it actually correct to call `CoTaskMemFree()` to deallocate the PIDL returned by `SHParseDisplayName()` as the [reference](https://msdn.microsoft.com/en-us/library/windows/desktop/bb762236(v=vs.85).aspx) doesn't say anything about that? I just took that from this [general documentation about PIDLs](https://msdn.microsoft.com/en-us/library/windows/desktop/cc144089(v=vs.85).aspx) -> "allocating PIDLs". – zett42 Mar 07 '17 at 14:46
  • 2
    @zett42 CoTaskMemFree is the standard way of freeing memory passed between COM components. – Raymond Chen Mar 07 '17 at 15:13
  • @honk I added my first solution back, but in a "modernized" form that requires less boilerplate code and also works with root pathes like "C:\". BTW, there is much to learn from [Raymonds blog](https://blogs.msdn.microsoft.com/oldnewthing/) about the modernized shell API. Just search for `IShellItem`. Good stuff. – zett42 Mar 08 '17 at 22:29
  • Thank you for taking the effort! Excellent answer! It teaches me a lot. However, the Windows Shell is still a closed book for me. I will take a look at Raymond's blog. Just two questions: Why does the first solution use `CComHeapPtr` and the second one `CComPtr`? Does the latter work in the same way as the former? – honk Mar 09 '17 at 07:38
  • `CComHeapPtr` is for raw memory allocated by COM components (CoTaskMemAlloc/CoTaskMemFree), `CComPtr` is for class objects that are reference-counted (AddRef/Release). As a rule of thumb: everything that is returned as a pointer to a class with a big "I" prefix, should be managed via `CComPtr`. The `CComPtr` automatically calls the `Release()` method when the object goes out of scope. – zett42 Mar 09 '17 at 07:57
  • @honk _Do you know if your code also works in a console application_ ... [Normally, the ShellExecuteEx function assumes that there will be a message pump running after it returns](https://blogs.msdn.microsoft.com/oldnewthing/20041126-00/?p=37193) ... but the suggestion of adding `SEE_MASK_FLAG_DDEWAIT` doesn't work on my Win10 machine. Adding a `MessageBox()` call after `ShellExecuteEx()` to run a message loop works though. – zett42 Jun 09 '17 at 11:06
  • @zett42: Thank you very much for the follow-up. This is interesting to know. Fortunately, your first solution seem to work perfectly in my MFC application on all Windows versions. – honk Jun 09 '17 at 11:22