As you have discovered already, the popular approach1, 2 to reach right into the Shell's private parts and do whatever is not a sustainable solution3. This had never been supported, and starting with Windows 10 1809 Microsoft sent out a message that's harder to ignore than recommendations: Implementation details changed, rendering solutions based on those implementation details dysfunctional.
Luckily, the Shell has a supported programming interface, that—among others—allows manipulating the positions of items on the desktop. The interface is exposed using the Component Object Model (COM), the foundational technology that underpins the vast majority of system services offered by the operating system.
While built on simple, easy to understand principles, one of COM's major feats (strict separation between interfaces and implementations, and loose coupling) poses a specific challenge: Its decoupled nature is reflected in the documentation by a prevalent lack of structural information. Interfaces are predominatly documented in isolation, with nearly no mention of where they fit in the broader scheme of things. Using a COM interface is easy. Knowing how to get to an interface of interest is the tricky part.
We are fortunate that Raymond Chen has covered the tricky part in his article Manipulating the positions of desktop icons, allowing us to keep our sanity. The following is an approximate transliteration of his code into C4:
Prelude:
// We need the COM interfaces declared as C-compatible structs
#define CINTERFACE
// Opt in to convenience macros (not strictly required, but it
// saves us from having to repeat ourselves)
#define COBJMACROS
#include <ExDisp.h>
#include <ShObjIdl.h>
#include <ShlObj.h>
#include <objbase.h>
The first function is used to get a COM interface that represents the desktop's folder view. It starts from a magic place (CLSID_ShellWindows
), taking a detour through a number of hard to guess avenues, to finally arrive at the interface we are interested in obtaining.
FindDesktopFolderView
:
/// @brief Finds the folder view that represents the desktop
///
/// @param riid The interface ID of the requested interface. This
/// is commonly `IID_IFolderView`.
/// @param ppv The address of a pointer that receives the
/// requested interface on success. Callers need to
/// make sure that the pointer type matches `riid`.
///
/// @return Returns a success code on success, a failure code
/// otherwise.
/// If this function call succeeds, `*ppv` contains a
/// valid pointer. If this function call fails, `*ppv`
/// holds a null pointer.
///
HRESULT FindDesktopFolderView(REFIID riid, void **ppv) {
// Argument validation
if (!ppv)
return E_POINTER;
*ppv = NULL;
// Instantiate an object that provides access to the collection
// of Shell windows (Explorer, Internet Explorer, ...)
IShellWindows *pShellWindows = NULL;
HRESULT hr = CoCreateInstance(&CLSID_ShellWindows, NULL, CLSCTX_ALL,
&IID_IShellWindows, &pShellWindows);
// Find the specific Explorer window that represents the desktop
IDispatch *pDispatch = NULL;
if (SUCCEEDED(hr)) {
VARIANT vtLoc = {.vt = VT_I4, .lVal = CSIDL_DESKTOP};
VARIANT vtEmpty = {.vt = VT_EMPTY};
long lhwnd = 0;
hr = IShellWindows_FindWindowSW(pShellWindows, &vtLoc, &vtEmpty, SWC_DESKTOP,
&lhwnd, SWFO_NEEDDISPATCH, &pDispatch);
}
// Request the service provider interface...
IServiceProvider *pServiceProvider = NULL;
if (SUCCEEDED(hr)) {
hr = IDispatch_QueryInterface(pDispatch, &IID_IServiceProvider,
&pServiceProvider);
}
// ... so that we can get to the top Shell browser. You can
// think of this as the frame window that hosts one or more
// Shell views
IShellBrowser *pBrowser = NULL;
if (SUCCEEDED(hr)) {
hr = IServiceProvider_QueryService(pServiceProvider, &SID_STopLevelBrowser,
&IID_IShellBrowser, &pBrowser);
}
// Request the active Shell view. This may seem needless with
// the desktop having a single view, but File Explorer windows
// typically have more than one, and the generality of the API
// shows
IShellView *pShellView = NULL;
if (SUCCEEDED(hr)) {
hr = IShellBrowser_QueryActiveShellView(pBrowser, &pShellView);
}
// Request the interface specified by callers of this function
if (SUCCEEDED(hr)) {
hr = IShellView_QueryInterface(pShellView, riid, ppv);
}
// We're done with all the interfaces, so let's clean them up.
// The only interface we care about is stored in `*ppv`,
// assuming that all calls succeeded
if (pShellView)
IShellView_Release(pShellView);
if (pBrowser)
IShellBrowser_Release(pBrowser);
if (pServiceProvider)
IServiceProvider_Release(pServiceProvider);
if (pDispatch)
IDispatch_Release(pDispatch);
if (pShellWindows)
IShellWindows_Release(pShellWindows);
return hr;
}
When called with an riid
of IID_IFolderView
this gets us an IFolderView
interface. We can use that to obtain an iterator, find the item at a given index, and manipulate properties. That's what the following function does.
RepositionItemByIndex
:
/// @brief Moves a desktop item to a new location
///
/// @param index The zero-based index of the item to be moved.
/// @param pos The desired new position of the item. The
/// position is subject to adjustments performed by
/// the Shell (e.g. when the *"Align icons to grid"*
/// option is enabled).
///
/// @return Returns a success code on success, a failure code
/// otherwise
///
HRESULT RepositionItemByIndex(int index, POINT pos) {
HRESULT hr = S_OK;
// Lower bounds check
if (index < 0)
hr = DISP_E_BADINDEX;
IFolderView *pView = NULL;
if (SUCCEEDED(hr)) {
hr = FindDesktopFolderView(&IID_IFolderView, &pView);
}
int count = 0;
if (SUCCEEDED(hr)) {
hr = IFolderView_ItemCount(pView, SVGIO_ALLVIEW, &count);
// Upper bounds check
if (SUCCEEDED(hr) && (index >= count))
hr = DISP_E_BADINDEX;
}
// Get an enumerator for items in view order
IEnumIDList *pIDList = NULL;
if (SUCCEEDED(hr)) {
hr = IFolderView_Items(pView, SVGIO_ALLVIEW | SVGIO_FLAG_VIEWORDER,
&IID_IEnumIDList, &pIDList);
}
// Extract `index + 1` items (ideally we'd `Skip()` over the
// first `index` items, but that returns `E_NOTIMPL` for this
// interface)
ITEMIDLIST *pIDL = NULL;
int current_idx = -1;
while (SUCCEEDED(hr) && current_idx < index) {
// Clean up ID list allocated in previous iteration;
// `CoTaskMemFree` can be called on a null pointer, so the
// first iteration is fine
CoTaskMemFree(pIDL);
pIDL = NULL;
hr = IEnumIDList_Next(pIDList, 1, &pIDL, NULL);
// Exit on error, or enumerator depletion (`S_FALSE`)
if (hr != S_OK)
break;
++current_idx;
}
// With the ID list identifying the item we can finally
// request to have its position updated
if (SUCCEEDED(hr) && (index == current_idx)) {
PCITEMID_CHILD apidl[1] = {pIDL};
hr = IFolderView_SelectAndPositionItems(pView, 1, apidl, &pos,
SVSI_POSITIONITEM);
}
// Cleanup
CoTaskMemFree(pIDL);
if (pIDList)
IEnumIDList_Release(pIDList);
if (pView)
IFolderView_Release(pView);
return hr;
}
Shell items are identified by Item ID Lists, an opaque binary blob containing data private to the folder that contains the item. Functions typically accept a pointer to an item ID lists (PIDLs); this indirection allows passing item ID lists of arbitrary size by way of a fixed-size pointer value. PCITEMID_CHILD
above expands to ITEMIDLIST const*
, meaning that apidl
is an array of length 1 containing a single PIDL (ITEMIDLIST const*
).
All of this appears to be excruciatingly complex, and there is a whole book chapter's worth of text that could be said still. But, that would be crazy. We're not doing that, and instead appreciate how easy it is to use the above.
main
:
int main(int argc, char **argv) {
HRESULT hr = CoInitialize(NULL);
if (SUCCEEDED(hr)) {
POINT pos = {.x = 500, .y = 500};
hr = RepositionItemByIndex(0, pos);
}
return hr;
}
That's all there is to it.
Keep in mind that an item's position affects where it gets sorted in the item list. As a consequence, calling RepositionItemByIndex
twice with the same index
can result in moving two different items.
A better interface could be RepositionItemByDisplayName
, or RepositionItemByPIDL
, keeping the operation independent of the items' sorting predicate. If there is a demand for those more predictable implementations I can add them in a future update.
1 How to programmatically move icons' location from Windows Desktop?
2 How can I programmatically manipulate Windows desktop icon locations?
3 A reminder about the correct way of accessing and manipulating the position of icons on the desktop
4 I'm not a C expert, though I believe the implementation to be sound, with all errors handled and all acquired resources released.