Отсутствие данных в диалоговом окне свойств файла Windows при открытии ShellExecuteEx

Я хочу показать диалог свойств файла Windows для файла с моего кода на С++ (в Windows 7, используя VS 2012). Я нашел следующий код в этом ответе (который также содержит полный MCVE). Я также попробовал сначала называть CoInitializeEx(), как указано в документации 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);

Этот код работает, т.е. отображается диалоговое окно свойств и ShellExecuteEx() возвращает TRUE. Однако на вкладке "Сведения" свойство размера неверно и отсутствуют свойства даты:

Окно свойств, открытое через мою программу

Остальные свойства на вкладке "Сведения" (например, атрибуты файла) верны. Как ни странно, свойства размера и даты отображаются правильно на вкладке "Общие" (вкладка слева).

Если я открою окно свойств через проводник Windows (файл → щелкните правой кнопкой мыши → Свойства), то все свойства на вкладке "Сведения" отображаются правильно:

Окно свойств, открытое через проводник Windows

Я попробовал это с несколькими файлами и типами файлов (например, txt, rtf, pdf) на разных дисках и на трех разных компьютерах (1x немецкий 64-разрядный Windows 7, 1x английский 64-разрядный Windows 7, 1x английский 32-разрядный Windows 7). Я всегда получаю тот же результат, даже если я запускаю свою программу как администратор. В (64-разрядная) Windows 8.1 код работает для меня.

Моя первоначальная программа, в которой я обнаружил проблему, - это приложение MFC, но я вижу ту же проблему, если я помещаю вышеуказанный код в консольное приложение.

Что мне нужно сделать, чтобы показать правильные значения на вкладке "Сведения" в Windows 7? Возможно ли это?

Ответы

Ответ 1

Как предложил Раймонд Чен, заменив путь на PIDL (SHELLEXECUTEINFO::lpIDList), диалоговое окно свойств корректно отображает поля размера и даты под Windows 7 при вызове через ShellExecuteEx().

Кажется, что реализация Windows 7 из ShellExecuteEx() ошибочна, поскольку более новые версии ОС не имеют проблемы с SHELLEXCUTEINFO::lpFile.

Существует еще одно решение, которое включает создание экземпляра IContextMenu и вызов метода IContextMenu::InvokeCommand(). Наверное, это то, что ShellExecuteEx() делает под капотом. Прокрутите вниз до Решение 2, например, кода.

Решение 1 - использование PIDL с 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
}

Этот код работает для меня как для Win 7, так и для Win 10 (другие версии, не протестированные) при вызове с помощью обработчика кнопок простого диалогового приложения MFC.

Он также работает для консольных приложений, если вы установите info.hwnd на NULL (просто удалите строку info.hwnd = GetSafeHwnd(); из кода примера, поскольку она уже инициализирована 0). В SHELLEXECUTEINFO указано, что член hwnd является необязательным.

Не забудьте об обязательном вызове CoInitialize() или CoInitializeEx() при запуске приложения и CoUninitialize() при завершении работы, чтобы правильно инициализировать и деинициализировать COM.

Примечания:

CComHeapPtr - это умный указатель, включенный в ATL, который автоматически вызывает CoTaskMemFree(), когда область действия заканчивается. Это указатель на перенос собственности с семантикой, подобный устаревшему 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-интерфейсами.


Решение 2 - с использованием IContextMenu

Следующий код требует Windows Vista или новее, поскольку я использую "современный" IShellItem API.

Я завернул код в функцию 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 );
    }
}