none
How do you get the user's sort column for a folder using shell APIs? RRS feed

  • Question

  • I have code that’s enumerating shell folders, so I have an IShellFolder(2) interface.

    The IShellFolder2::GetDefaultColumn method looks to be the function that ought to return the sort column that you can pass to IShellFolder::CompareIDs to sort items in the folder, but it always return E_NOTIMPL despite a folder viewed in Explorer using a non-default sort column.

    Is there some other way of getting the user's chosen sort column for a folder?

    Thursday, January 9, 2020 11:35 PM

Answers

  • I could make it work : I had to use 

    L"Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}"

    with

    SHGetViewStatePropertyBag

    (only updated  by Explorer when you click on another directory)

    (tested on Windows 10 only)

    Saturday, January 11, 2020 9:15 AM
  • At first I thought to QI the VT_UNKNOWN returned for various shell interfaces but then it occurred to me that the data obtained from the registry would likely be an IStream containing a data structure.  The IFolderView:2::GetSortColumns method takes an integer for column count and an array of SORTCOLUMN structs.  The size of a SORTCOLUMN struct is 24 bytes.  Coupled with a 4 byte integer value, this would be the same number of bytes as the size of the returned stream.

    The bagname used to retrieve the property bag (i.e., Shell\GUID) can vary.  There are a number of GUIDs contained in ShlGuid.h for FOLDERTYPEID that can be present.

    Following is an updated version of the sample code -

    // SBag.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    #pragma comment(lib, "propsys.lib")
    
    
    int wmain(int argc, WCHAR *argv[])
    {
    	HRESULT hr;
    	if (SUCCEEDED(CoInitialize(nullptr)))
    	{
    		CComHeapPtr<ITEMIDLIST> pidl;
    		SFGAOF sfgao{};
    
    		hr = SHParseDisplayName(argv[1], nullptr, &pidl, sfgao, &sfgao);
    		if (SUCCEEDED(hr))
    		{
    			CComPtr<IPropertyBag> pbtype;
    			WCHAR szGuid[39]{};
    
    			hr = SHGetViewStatePropertyBag(pidl, L"Shell", SHGVSPB_FOLDER, IID_PPV_ARGS(&pbtype));
    			if (SUCCEEDED(hr))
    			{
    				CComVariant vType;
    				hr = pbtype->Read(L"SniffedFolderType", &vType, nullptr);
    				if (SUCCEEDED(hr) && V_VT(&vType) == VT_BSTR)
    				{
    					CComBSTR bstrType(V_BSTR(&vType));
    
    					if (bstrType == L"Generic")
    						StringFromGUID2(FOLDERTYPEID_Generic, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Documents")
    						StringFromGUID2(FOLDERTYPEID_Documents, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Videos")
    						StringFromGUID2(FOLDERTYPEID_Videos, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Pictures")
    						StringFromGUID2(FOLDERTYPEID_Pictures, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Music")
    						StringFromGUID2(FOLDERTYPEID_Music, szGuid, ARRAYSIZE(szGuid));
    					else
    						wprintf_s(L"SniffedFolderType was %s, update FOLDERTYPEID Guids\n", (LPWSTR)bstrType);
    				}
    				else
    					wprintf(L"Property bag doesn't contain SniffedFolderType\n");
    			}
    
    			if (szGuid[0] != L'\0')
    			{
    				CString strBagname;
    				strBagname.Format(L"Shell\\%s", szGuid);
    
    				CComPtr<IPropertyBag> pb;
    				hr = SHGetViewStatePropertyBag(pidl, strBagname, SHGVSPB_FOLDER, IID_PPV_ARGS(&pb));
    				if (SUCCEEDED(hr))
    				{
    					CComVariant vProp;
    					hr = pb->Read(L"Sort", &vProp, nullptr);
    
    					if (V_VT(&vProp) == VT_UNKNOWN)
    					{
    						CComQIPtr<IStream> ps(vProp.punkVal);
    					
    						if (ps)
    						{
    							STATSTG stg{};
    							CComHeapPtr<BYTE> pBuf;
    							CComHeapPtr<WCHAR> pszPropName;
    							ULONG cbRead = 0;
    
    							hr = ps->Stat(&stg, STATFLAG_DEFAULT);
    							pBuf.Allocate(stg.cbSize.LowPart);
    							hr = ps->Read(pBuf, stg.cbSize.LowPart, &cbRead);
    
    							SORTCOLUMN *psc = (SORTCOLUMN *)&pBuf[4];
    
    							hr = PSGetNameFromPropertyKey(psc->propkey, &pszPropName);
    							CString strOrder;
    
    							if (psc->direction == SORT_ASCENDING)
    								strOrder = L"ascending";
    							else if (psc->direction == SORT_DESCENDING)
    								strOrder = L"descending";
    							else
    								strOrder = L"Unknown";
    
    							wprintf_s(L"Folder %s, Sort Column property name is %s, order is %s\n", argv[1], (LPWSTR) pszPropName, (LPCWSTR) strOrder);
    						}
    					}
    				}
    			}
    		}
    	}
    
    	CoUninitialize();
    
        return 0;
    }
    

    Is it reliable? Well, no guarantees there. :)

    That's up to you and the amount of testing you wish to perform.





    • Edited by RLWA32 Sunday, January 12, 2020 1:51 PM typos
    • Marked as answer by David LowndesMVP Monday, January 13, 2020 9:02 PM
    Sunday, January 12, 2020 1:19 PM

All replies

  • Hi,

    The reason why IShellFolder2.GetDefaultColumn always returns E_NOTIMPL is following: Almost no shell folder implements this method. This means that this folder does not want to overwrite the defaut sort order. If this method succeeds, it returns a custom sort column that differs from the default sort column.

    Refer: IShellFolder2.GetDefaultColumn aways fails with E_NOTIMPL

    You can follow this article to get the user's chosen sort column for a folder.

    Best regards,

    Strive


    MSDN Community Support
    Please remember to click "Mark as Answer" the responses that resolved your issue, and to click "Unmark as Answer" if not. This can be beneficial to other community members reading this thread. If you have any compliments or complaints to MSDN Support, feel free to contact MSDNFSF@microsoft.com.

    Friday, January 10, 2020 8:10 AM
  • Hi Strive,

    I'd seen that stackoverflow post that mentioned that "Almost no shell folder implements this method." and in my testing I found nothing that implemented it. Do you know of any folder that does implement it?

    I've had a look at that codeproject article you referenced, and while it has some code (ExComparer.cs) that uses IShellFolder and implements sorting, I couldn't see where it retrieved the user sort settings for a folder. Could you point that aspect out to me please?

    Dave

    Friday, January 10, 2020 10:02 AM
  • Thanks, I'll have a look at IFolderView2::GetSortColumns later. Not sure if I can get that interface from an IShellFolder(2) though.
    Friday, January 10, 2020 11:09 AM
  • https://stackoverflow.com/questions/31259085/programmatically-sort-folders-on-windows-8

    Response from Sheng Jiang was illuminating --"The sort order belongs to the view, not folder. You can have two Windows Explorer windows open on the same folder, each has a different sort order."

    Friday, January 10, 2020 11:48 AM
  • As I feared, I can't simply QueryInterface from IShellFolder to get an IFolderView2 and in my usage the folders won't necessarily be open in Windows Explorer.

    I'm looking for whatever method is used by Explorer itself to get a folder's saved sort column information.

    Friday, January 10, 2020 11:49 AM
  • Yes, I can see that would likely be the case. Presumably the last change is the one that's persisted. I've checked the sort column is persisted on Windows 10 after Explorer is restarted, though I suspect there's a limited number of folders whose states are stored.

    As my usage doesn't involve interactively viewing folders though, whatever the last change is, would be fine.

    Friday, January 10, 2020 11:59 AM
  • My best guess is that explorer persists the folder view information in the undocumented shellbags registry information. 

    https://support.microsoft.com/en-us/help/813711

    Friday, January 10, 2020 11:59 AM
  • Yes, registry searching had taken me there but as I couldn't find any documentation for it I thought it wasn't wise to pursue.

    Friday, January 10, 2020 2:01 PM
  • I haven't tried it, but this looks promising https://docs.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-shgetviewstatepropertybag

    I was about to answer this... but I did a quick test on Windows 10 and it did not work (when I read properties from the stream) :-(

    (maybe I missed something...)


    • Edited by Castorix31 Friday, January 10, 2020 2:33 PM
    Friday, January 10, 2020 2:33 PM
  • It looks promising, but I've been unable to guess how to use it properly even when taking clues from things in the registry.

    I can get an IPropertyBag from SHGetViewStatePropertyBag specifying "Shell" (there's better documentation on that method here), but I've not succeeded in guessing what name to pass to its Read method. My first guess was to try "Sort" since the HKCU\SOFTWARE\Microsoft\Windows\Shell\Bags have value entries with that name, but that didn't succeed.

    I think I'm onto a hiding to nothing in pursuing this.

    Friday, January 10, 2020 8:50 PM
  • Another failed experiment --

    IShellFolder::CreateViewObject to obtain IShellView.

    QI on IShellView for IFolderView2.

    IFolderView2 returns column sort info but it doesn't reflect the same sort order that is displayed when the folder is viewed in explorer.

    Saturday, January 11, 2020 12:06 AM
  • Thanks for pointing that out, I thought I'd try it too just to be sure it didn't work from my context - it didn't.

    GetSortColumns just returns the "Name" property regardless of what the actual folder sort column is when viewed with Explorer.

    Saturday, January 11, 2020 12:54 AM
  • I could make it work : I had to use 

    L"Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}"

    with

    SHGetViewStatePropertyBag

    (only updated  by Explorer when you click on another directory)

    (tested on Windows 10 only)

    Saturday, January 11, 2020 9:15 AM
  • Expanding on Castorix31's post.

    This returned the correct sort column for me on Win 8.1

    // SBag.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    
    int main()
    {
    	HRESULT hr;
    	if (SUCCEEDED(CoInitialize(nullptr)))
    	{
    		CComHeapPtr<ITEMIDLIST> pidl;
    		CComPtr<IPropertyBag> pb;
    		SFGAOF sfgao{};
    
    		hr = SHParseDisplayName(L"F:\\RLWA32\\FBrowser\\FBrowser", nullptr, &pidl, sfgao, &sfgao);
    		if (SUCCEEDED(hr))
    		{
    			hr = SHGetViewStatePropertyBag(pidl, L"Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}", SHGVSPB_FOLDER, IID_PPV_ARGS(&pb));
    			if (SUCCEEDED(hr))
    			{
    				CComVariant vProp;
    				hr = pb->Read(L"Sort", &vProp, nullptr);
    
    				if (V_VT(&vProp) == VT_UNKNOWN)
    				{
    					CComQIPtr<IStream> ps(vProp.punkVal);
    					
    					if (ps)
    					{
    						STATSTG stg{};
    						CComHeapPtr<BYTE> pBuf;
    						CComHeapPtr<WCHAR> pszPropName;
    						ULONG cbRead = 0;
    
    						hr = ps->Stat(&stg, STATFLAG_NONAME);
    						pBuf.Allocate(stg.cbSize.LowPart);
    						hr = ps->Read(pBuf, stg.cbSize.LowPart, &cbRead);
    
    						SORTCOLUMN *psc = (SORTCOLUMN *)&pBuf[4];
    
    						hr = PSGetNameFromPropertyKey(psc->propkey, &pszPropName);
    						wprintf_s(L"Sort Column property name is %s\n", pszPropName);
    					}
    				}
    
    			}
    		}
    	}
    
    	CoUninitialize();
    
        return 0;
    }
    
    

    Saturday, January 11, 2020 2:17 PM
  • Fantastic. Now, dare I rely on it :)

    How'd you determine what to do with the VT_UNKNOWN to suss there was a SORTCOLUMN lurking in there - educated guesses, or is there some documentation that implies this?

    Saturday, January 11, 2020 7:43 PM
  • At first I thought to QI the VT_UNKNOWN returned for various shell interfaces but then it occurred to me that the data obtained from the registry would likely be an IStream containing a data structure.  The IFolderView:2::GetSortColumns method takes an integer for column count and an array of SORTCOLUMN structs.  The size of a SORTCOLUMN struct is 24 bytes.  Coupled with a 4 byte integer value, this would be the same number of bytes as the size of the returned stream.

    The bagname used to retrieve the property bag (i.e., Shell\GUID) can vary.  There are a number of GUIDs contained in ShlGuid.h for FOLDERTYPEID that can be present.

    Following is an updated version of the sample code -

    // SBag.cpp : Defines the entry point for the console application.
    //
    
    #include "stdafx.h"
    #pragma comment(lib, "propsys.lib")
    
    
    int wmain(int argc, WCHAR *argv[])
    {
    	HRESULT hr;
    	if (SUCCEEDED(CoInitialize(nullptr)))
    	{
    		CComHeapPtr<ITEMIDLIST> pidl;
    		SFGAOF sfgao{};
    
    		hr = SHParseDisplayName(argv[1], nullptr, &pidl, sfgao, &sfgao);
    		if (SUCCEEDED(hr))
    		{
    			CComPtr<IPropertyBag> pbtype;
    			WCHAR szGuid[39]{};
    
    			hr = SHGetViewStatePropertyBag(pidl, L"Shell", SHGVSPB_FOLDER, IID_PPV_ARGS(&pbtype));
    			if (SUCCEEDED(hr))
    			{
    				CComVariant vType;
    				hr = pbtype->Read(L"SniffedFolderType", &vType, nullptr);
    				if (SUCCEEDED(hr) && V_VT(&vType) == VT_BSTR)
    				{
    					CComBSTR bstrType(V_BSTR(&vType));
    
    					if (bstrType == L"Generic")
    						StringFromGUID2(FOLDERTYPEID_Generic, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Documents")
    						StringFromGUID2(FOLDERTYPEID_Documents, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Videos")
    						StringFromGUID2(FOLDERTYPEID_Videos, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Pictures")
    						StringFromGUID2(FOLDERTYPEID_Pictures, szGuid, ARRAYSIZE(szGuid));
    					else if (bstrType == L"Music")
    						StringFromGUID2(FOLDERTYPEID_Music, szGuid, ARRAYSIZE(szGuid));
    					else
    						wprintf_s(L"SniffedFolderType was %s, update FOLDERTYPEID Guids\n", (LPWSTR)bstrType);
    				}
    				else
    					wprintf(L"Property bag doesn't contain SniffedFolderType\n");
    			}
    
    			if (szGuid[0] != L'\0')
    			{
    				CString strBagname;
    				strBagname.Format(L"Shell\\%s", szGuid);
    
    				CComPtr<IPropertyBag> pb;
    				hr = SHGetViewStatePropertyBag(pidl, strBagname, SHGVSPB_FOLDER, IID_PPV_ARGS(&pb));
    				if (SUCCEEDED(hr))
    				{
    					CComVariant vProp;
    					hr = pb->Read(L"Sort", &vProp, nullptr);
    
    					if (V_VT(&vProp) == VT_UNKNOWN)
    					{
    						CComQIPtr<IStream> ps(vProp.punkVal);
    					
    						if (ps)
    						{
    							STATSTG stg{};
    							CComHeapPtr<BYTE> pBuf;
    							CComHeapPtr<WCHAR> pszPropName;
    							ULONG cbRead = 0;
    
    							hr = ps->Stat(&stg, STATFLAG_DEFAULT);
    							pBuf.Allocate(stg.cbSize.LowPart);
    							hr = ps->Read(pBuf, stg.cbSize.LowPart, &cbRead);
    
    							SORTCOLUMN *psc = (SORTCOLUMN *)&pBuf[4];
    
    							hr = PSGetNameFromPropertyKey(psc->propkey, &pszPropName);
    							CString strOrder;
    
    							if (psc->direction == SORT_ASCENDING)
    								strOrder = L"ascending";
    							else if (psc->direction == SORT_DESCENDING)
    								strOrder = L"descending";
    							else
    								strOrder = L"Unknown";
    
    							wprintf_s(L"Folder %s, Sort Column property name is %s, order is %s\n", argv[1], (LPWSTR) pszPropName, (LPCWSTR) strOrder);
    						}
    					}
    				}
    			}
    		}
    	}
    
    	CoUninitialize();
    
        return 0;
    }
    

    Is it reliable? Well, no guarantees there. :)

    That's up to you and the amount of testing you wish to perform.





    • Edited by RLWA32 Sunday, January 12, 2020 1:51 PM typos
    • Marked as answer by David LowndesMVP Monday, January 13, 2020 9:02 PM
    Sunday, January 12, 2020 1:19 PM
  • I'm amazed. Well done! :)

    Many thanks to yourself and Castorix31 for the detective work you've done here.

    Other than the undocumented/unsupported nature of the code, the only issue I can see is that getting the settings this way doesn't immediately reflect the Explorer view setting. The change only take place presumably when Explorer has saved its settings, which occurs when the folder view is changed somehow (switching to another folder does it).

    Monday, January 13, 2020 9:02 PM
  • As for unsaved folder settings when explorer is open you can retrieve them with the code from Raymond Chen's blog.
    • Edited by RLWA32 Monday, January 13, 2020 9:31 PM
    Monday, January 13, 2020 9:31 PM