1

I tried to show only the drives shared with Remote Desktop in a file selection dialog. Below is what I want to show:

But the list of the drives and folders in the left pane are different according to the Windows OS version. The links to the screenshots are in a reply comment.

How can we show the same list of the drives and folders in the multiple Windows versions?

This is the implementation:

    CFileDialog my_file_dialog(TRUE);

    CComPtr<IFileDialogCustomize> file_dialog = my_file_dialog.GetIFileDialogCustomize();

    CComPtr<IFileDialog2> dialog2;
    if (FAILED(file_dialog->QueryInterface(&dialog2))) {
        ::AfxMessageBox(_T("Failed to query the interface of IFileDialog2."));
        return;
    }

    CComPtr<IShellItemFilter> shell_item_filter =
        new CVisibleShellItemFilter(REMOTE_APP_VISIBLE_PATHS);
    if (FAILED(dialog2->SetFilter(shell_item_filter))) {
        ::AfxMessageBox(_T("Failed to set the shell item filter to the file dialog."));
        return;
    }

    CComPtr<IShellItem> shell_item;
    if (FAILED(::SHCreateItemInKnownFolder(FOLDERID_ComputerFolder, 0, nullptr, IID_PPV_ARGS(&shell_item)))) {
        ::AfxMessageBox(_T("Failed to create a shell item for the computer folder."));
        return;
    }

    if (FAILED(dialog2->SetDefaultFolder(shell_item))) {
        ::AfxMessageBox(_T("Failed to set the default folder to the computer folder."));
        return;
    }

    CComPtr<IShellItem> shell_item_desktop;
    if (FAILED(::SHCreateItemInKnownFolder(FOLDERID_Desktop, 0, nullptr, IID_PPV_ARGS(&shell_item_desktop)))) {
        ::AfxMessageBox(_T("Failed to create a shell item for the desktop."));
        return;
    }
    if (FAILED(dialog2->SetNavigationRoot(shell_item_desktop))) {
        ::AfxMessageBox(_T("Failed to set the navigation root to the computer folder."));
        return;
    }

    CComPtr<IShellItem> currently_selected_folder;
    if (FAILED(dialog2->GetFolder(&currently_selected_folder))) {
        ::AfxMessageBox(_T("Failed to set the navigation root to the computer folder."));
        return;
    }

    if (!IsVisible(ToPathPartsList(REMOTE_APP_VISIBLE_PATHS), currently_selected_folder)) {
        if (FAILED(dialog2->SetFolder(shell_item))) {
            ::AfxMessageBox(_T("Failed to set the folder to the computer folder."));
            return;
        }
    }

    if (my_file_dialog.DoModal() == IDOK) {
        m_message.SetWindowTextW(my_file_dialog.GetPathName());
    }
    else {
        m_message.SetWindowTextW(_T(""));
    }

What I did were:

  1. Construct an instance of CFiledialog.
  2. Retrieved an instance of IFileDialogCustomize by CFiledialog::GetIFileDialogCustomize().
  3. Retrieved an instance of IFileDialog2 from the isntance of IFileDialogCustomize.
  4. Set an instance of IShellItemFilter to the file dialog by IFileDialog::SetFilter().
  5. Set the default folder to the PC (My Computer) folder by IFileDialog::SetDefaultFolder().
  6. Set the navigation root to the PC (My Computer) folder by IFileDialog::SetNavigationRoot().
  7. Set the folder to the PC (My Computer) folder by IFileDialog::SetFolder() if the current folder is not a shared drive or its descendant folders.
  8. Show the file dialog.

I did 2. to retrieve an instance of IFileDialog2 from the instance of IFileDialogCustomize. This is because I want to support both "Open" and "Save" file dialog by the same routine.

The point is 4.. I will show the implementation of IShellItemFilter later.

I did 5. because a folder other than a shared drive or its descendant folders is shown if the default folder is not than a shared drive or its descendant folders.

I did 6. because I don't want to show the minimum contents in the file dialog, and I don't want to show the desktop folder.

I did 7. because a folder other than a shared drive or its descendant folders is shown if the current folder is not than a shared drive or its descendant folders.

The implementation of IShellItemFilter was:

    // If the desktop_absolute_parsing is "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}", it shows the
    // virtual folder that represents My Computer (PC).
    // http://www.atmarkit.co.jp/ait/articles/1004/09/news094.html
    static const std::wstring MY_COMPUTER_PATH = L"::{20D04FE0-3AEA-1069-A2D8-08002B30309D}";

    // If the desktop_absolute_parsing starts with \\tsclient\, it shows a drive shared with
    // Remote Desktop (or RemoteApp) or its child folder.
    // https://social.msdn.microsoft.com/Forums/vstudio/ja-JP/74673059-17b0-4a80-80ac-66b5dc419b56?forum=vcgeneralja
    static const std::wstring REMOTE_APP_SHARED_FOLDER_PATH_PREFIX = L"\\\\tsclient\\";

    static const std::vector<std::wstring> REMOTE_APP_VISIBLE_PATHS = {
        // Add My Compter to the paths so that users can navigate from the My Compter in the folder
        // view.
        MY_COMPUTER_PATH,
        REMOTE_APP_SHARED_FOLDER_PATH_PREFIX,
    };

    // Converts a give string to lower cases.
    // str String
    // return Lower case string.
    std::wstring ToLower(std::wstring str) {
        std::transform(str.begin(), str.end(), str.begin(),
            [](auto ch) { return std::tolower(ch, std::locale()); });
        return str;
    }

    // Split a givn path to the parts. Users need to split a path into the path parts, and pass to
    // the function below for the processing speed.
    // ex) "c:\\windows\\system32" -> {"c:", "\\", "windows", "system32"}
    // file_path Path to be splitted.
    // return Path parts.
    std::vector<std::wstring> SplitPath(const std::wstring& file_path) {
        using std::experimental::filesystem::v1::path;
        path p(file_path);
        return std::vector<std::wstring>(p.begin(), p.end());
    }

    // Checks if a path is equal to or an ancestor of another path.
    // ancestor_path_parts Given path parts.
    // descendant_path_parts Other path parts.
    // returns true if ancestor_path_parts is equal to or an ancestor of descendant_path_parts.
    //         false, otherwise.
    bool IsPathSelfOrAncestor(const std::vector<std::wstring>& ancestor_path_parts,
        const std::vector<std::wstring>& descendant_path_parts) {
        // Do not compare the path strings directly. Because "C:\Windows\System32" matches
        // "C:\Windows\System", for example.
        return std::search(descendant_path_parts.begin(), descendant_path_parts.end(),
            ancestor_path_parts.begin(), ancestor_path_parts.end()) ==
            descendant_path_parts.begin();
    }

    // Checks if two given paths are in a direct line. i.e. A path is equal to, ancestor of, or
    // decendant of another path.
    // path_parts1 Given path parts.
    // path_parts2 Other path parts.
    // return true if the given two paths are in a direct line. false, otherwise.
    bool IsInDirectLine(const std::vector<std::wstring>& path_parts1, const std::vector<std::wstring>& path_parts2) {
        return IsPathSelfOrAncestor(path_parts1, path_parts2) ||
            IsPathSelfOrAncestor(path_parts2, path_parts1);
    }

    // Gets the display name from a given shell item.
    // sigdnName SIGDN name.
    // shell_item Shell item.
    // name Display name.
    // return S_OK if this method succeeds. false, otherwise.
    HRESULT GetDisplayName(IShellItem* shell_item, SIGDN sigdnName, std::wstring& name) {
        LPWSTR raw_name = nullptr;
        HRESULT result;
        if (FAILED(result = shell_item->GetDisplayName(sigdnName, &raw_name))) {
            return result;
        }

        name = raw_name;
        ::CoTaskMemFree(raw_name);
        raw_name = nullptr;
        return S_OK;
    }

    // Checks if a given shell item is visible in a file/folder view.
    // visible_path_parts_list List of the visble paths parts.
    // shell_item Shell item to be checked.
    // return true if the given shell item is visible. false, otherwise.
    bool IsVisible(const std::vector<std::vector<std::wstring> >& visible_path_parts_list, IShellItem* shell_item) {
        std::wstring desktop_absolute_parsing;
        if (FAILED(GetDisplayName(shell_item, SIGDN_DESKTOPABSOLUTEPARSING,
            desktop_absolute_parsing))) {
            ::AfxMessageBox(_T("Failed to get the diplay name of a shell item."));
            return false;
        }

        auto path_parts = SplitPath(ToLower(desktop_absolute_parsing));

        for (const auto& visible_path_parts : visible_path_parts_list) {
            // Check if shell_item and visible_path are in the direct line. i.e. shell_item and
            // visible_path are same, shell_item is an ancestor of visible_path, or shell_item is
            // an descendant of visible_path.
            // Let users to navigate in the case that shell_item is an ancestor of visible_path.
            // Otherwise, users can not navigate to the visible_path through the folder view in
            // the file selection dialog.
            if (IsInDirectLine(path_parts, visible_path_parts)) {
                return true;
            }
        }
        return false;
    }

    // Converts the list of paths into the list of the path parts.
    std::vector<std::vector<std::wstring> > ToPathPartsList(
        const std::vector<std::wstring>& paths) {
        std::vector<std::vector<std::wstring> > path_parts_list;
        for (const auto& path : paths) {
            path_parts_list.push_back(SplitPath(ToLower(path)));
        }
        return path_parts_list;
    }

    // CVisibleShellItemFilter show only the visible shell items which are listed in the given list
    // of paths.
    class CVisibleShellItemFilter : public IShellItemFilter {
    public:
        CVisibleShellItemFilter(const std::vector<std::wstring>& visible_paths) :
            m_visible_path_parts_list(ToPathPartsList(visible_paths)) { }

        HRESULT QueryInterface(REFIID riid, void** ppvObject) override {
            if (ppvObject == nullptr) {
                return E_POINTER;
            }

            if (riid == IID_IUnknown || riid == IID_IShellItemFilter) {
                *ppvObject = static_cast<void*>(this);
                AddRef();
                return NO_ERROR;
            }
            else {
                *ppvObject = nullptr;
                return E_NOINTERFACE;
            }
        }

        ULONG AddRef() override {
            return ++m_reference_count;
        }

        ULONG Release() override {
            ULONG reference_count = --m_reference_count;
            if (reference_count == 0) {
                delete this;
            }
            return reference_count;
        }

        HRESULT IncludeItem(IShellItem *psi) override {
            return IsVisible(m_visible_path_parts_list, psi) ? S_OK : S_FALSE;
        }

        HRESULT GetEnumFlagsForItem(IShellItem *psi, SHCONTF *pgrfFlags) override {
            *pgrfFlags = static_cast<SHCONTF>(-1);
            return S_OK;
        }

    private:
        ULONG m_reference_count = 0;
        const std::vector<std::vector<std::wstring> > m_visible_path_parts_list;
    };

CVisibleShellItemFilter::IncludeItem() returns S_OK if the desktop absolute parsing of a file is "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}" or starts with "\tsclient\". "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}" means PC (My Computer) folder, and "\tsclient\" is the file path prefix of the shared drives.

Thanks,

Updated (2017/09/07 11:54 JST) "the content" -> "the list of the drives and folders"

nodchip
  • 361
  • 1
  • 3
  • 10
  • Windows 7 - The shared drives and C: are shown. https://i.stack.imgur.com/zGBJE.png Windows 8.1 - The shared drives, C: and personal folders are shown. https://i.stack.imgur.com/xCrGo.png Windows Server 2012 R2 - The shared drives, C: and personal folders are shown. https://i.stack.imgur.com/EWx0m.png – nodchip Sep 07 '17 at 01:36
  • Don't. Just don't. It is more important that your application behaves the same way as all the other applications the end user has than that your application behaves the same way on different versions of Windows. Trying to make it always look as if it were running on Windows 10, even if possible, is just going to confuse people. – Harry Johnston Sep 07 '17 at 02:10
  • Harry, thank you for responding. Do you mean that it is wrong to show only the shared drives? In general, you should be right. The background is that I want to write a RemoteApp, and I want to prevent that the users touch the host drives and folders. This is because I wanted to show only the shared drives. – nodchip Sep 07 '17 at 02:23
  • The way your question was phrased made me think you wanted the dialog to look the same in all three operating systems so that the appearance would be consistent. If you have a specific reason to desire a particular behaviour, and you only mentioned the different operating systems because on one of them you happen to get the behaviour you want without doing anything to get it, that's a different scenario and my objection doesn't apply. – Harry Johnston Sep 07 '17 at 02:29
  • ... or perhaps you just meant that your code works as desired, but only on Windows 10? – Harry Johnston Sep 07 '17 at 02:38
  • I just want to show the same drives and folders in Windows 7 / 8.1 / Server 2012 R2 / 10. The appearance does not need to be consistent. My code works as desired only on Windows 10. – nodchip Sep 07 '17 at 02:52
  • *The background is that I want to write a RemoteApp, and I want to prevent that the users touch the host drives and folders.* Security through obscurity. Just configure the RDS server correctly? – ta.speot.is Sep 07 '17 at 02:57
  • @ta.speot.is, it isn't necessarily intended as a security measure. It is often useful to guide users towards the most appropriate location to save their content, even if you can't *entirely* prevent them from putting it in an inappropriate place. – Harry Johnston Sep 07 '17 at 03:01
  • And if IsVisible returns false to everything, is the whole tree empty or are the unwanted items still there? – Anders Sep 07 '17 at 05:18
  • @Anders Windowns 7 shows PC (My Computer) and C:. Windows 8.1 shows PC, C:, personal folders and shared drives. Windows 10 shows only PC. – nodchip Sep 07 '17 at 06:54

0 Answers0