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?

4

1 回答 1

3

正如 Raymond Chen 建议的那样,用PIDL ( SHELLEXECUTEINFO::lpIDList) 替换路径可以使属性对话框在 Windows 7 下通过调用时正确显示大小和日期字段ShellExecuteEx()

似乎 Windows 7 的实现ShellExecuteEx()是错误的,因为较新版本的操作系统没有SHELLEXCUTEINFO::lpFile.

还有另一种可能的解决方案,涉及创建一个实例IContextMenu并调用该IContextMenu::InvokeCommand()方法。我想这就是ShellExecuteEx()引擎盖下的作用。向下滚动到解决方案 2以获取示例代码。

解决方案 1 - 使用带有 ShellExecuteEx 的 PIDL

#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
}

当从简单的基于对话框的 MFC 应用程序的按钮单击处理程序调用时,此代码在 Win 7 和 Win 10(其他版本未测试)下都适用于我。

info.hwnd如果您设置为NULL(只需info.hwnd = GetSafeHwnd();从示例代码中删除该行,因为它已经用 0 初始化),它也适用于控制台应用程序。在SHELLEXECUTEINFO参考中声明该hwnd成员是可选的。

不要忘记强制调用CoInitialize()CoInitializeEx()在应用程序启动时和CoUninitialize()关闭时正确初始化和取消初始化 COM。

笔记:

CComHeapPtr是包含在 ATL 中的智能指针,CoTaskMemFree()在作用域结束时会自动调用。它是一个所有权转移指针,其语义类似于 deprecated std::auto_ptr。也就是说,当你将一个CComHeapPtr对象赋值给另一个对象,或者使用有CComHeapPtr参数的构造函数时,原来的对象会变成一个NULL指针。

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!

我仍在使用它,因为它可以开箱即用,并且可以与 COM API 很好地配合使用。


解决方案 2 - 使用 IContextMenu

以下代码需要 Windows Vista 或更高版本,因为我使用的是“现代” IShellItemAPI。

我将代码包装到一个函数ShowPropertiesDialog()中,该函数接受一个窗口句柄和一个文件系统路径。如果发生任何错误,该函数将引发std::system_error异常。

#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" );
}

下面我展示了一个如何使用ShowPropertiesDialog()CDialog 派生类的按钮处理程序的示例。实际上ShowPropertiesDialog()是独立于 MFC 的,因为它只需要一个窗口句柄,但 OP 提到他想在 MFC 应用程序中使用代码。

#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 );
    }
}
于 2017-03-06T15:24:26.013 回答