Manipulating the positions of desktop icons


Today's little program demonstrates how you can manipulate the positions of desktop icons.

The entire program is just scaffolding to get us far enough that we can call IFolder­View::Get­Item­Position and IFolder­View::Select­And­Position­Items.

First, we adapt the code we saw some time ago that extracts the IFolder­View from a window.

Reminder: These "Little Programs" do no error checking because they are intended as demonstrations, not production-ready applications.

void FindDesktopFolderView(REFIID riid, void **ppv)
{
 CComPtr<IShellWindows> spShellWindows;
 spShellWindows.CoCreateInstance(CLSID_ShellWindows);

 CComVariant vtLoc(CSIDL_DESKTOP);
 CComVariant vtEmpty;
 long lhwnd;
 CComPtr<IDispatch> spdisp;
 spShellWindows->FindWindowSW(
     &vtLoc, &vtEmpty,
     SWC_DESKTOP, &lhwnd, SWFO_NEEDDISPATCH, &spdisp);

 CComPtr<IShellBrowser> spBrowser;
 CComQIPtr<IServiceProvider>(spdisp)->
     QueryService(SID_STopLevelBrowser,
                  IID_PPV_ARGS(&spBrowser));

 CComPtr<IShellView> spView;
 spBrowser->QueryActiveShellView(&spView);

 spView->QueryInterface(riid, ppv);
}

The Find­Desktop­Folder­View function takes the code from that earlier article and uses it to extract the shell view for the desktop. Everything here should look familiar (just in a different costume), aside from the call to Find­Window­SW, because we are looking for a specific window by location rather than just enumerating through all of them.

The first parameter to Find­Window­SW. is the folder we are looking for. In our case, we are looking for the desktop.

The second parameter is reserved and must be VT_EMPTY.

The third parameter describes the types of windows we are looking for. We use the special SWC_DESKTOP flag (available starting in Windows Vista) to say, "Hey, I know the desktop isn't the sort of thing people think of when they go looking for Explorer windows, but I know what I'm talking about, so let me have it."

The fourth parameter receives the window handle, which is of no interest to us, but the parameter is mandatory, so we have to give it something.

The fifth parameter specifies the search options. We use SWFO_NEED­DISPATCH to say, "Please return the IDispatch in the sixth parameter." And the sixth parameter is where we want the IDispatch to be returned.

Okay, we already have enough to be able to enumerate all the desktop icons and print their names and locations.

#define UNICODE
#define _UNICODE
#include <windows.h>
#include <shlobj.h>
#include <exdisp.h>
#include <shlwapi.h>
#include <atlbase.h>
#include <atlalloc.h>
#include <stdio.h>

// CCoInitialize incorporated by reference

int __cdecl wmain(int argc, wchar_t **argv)
{
 CCoInitialize init;
 CComPtr<IFolderView> spView;
 FindDesktopFolderView(IID_PPV_ARGS(&spView));
 CComPtr<IShellFolder> spFolder;
 spView->GetFolder(IID_PPV_ARGS(&spFolder));

 CComPtr<IEnumIDList> spEnum;
 spView->Items(SVGIO_ALLVIEW, IID_PPV_ARGS(&spEnum));
 for (CComHeapPtr<ITEMID_CHILD> spidl;
      spEnum->Next(1, &spidl, nullptr) == S_OK;
      spidl.Free()) {
  STRRET str;
  spFolder->GetDisplayNameOf(spidl, SHGDN_NORMAL, &str);
  CComHeapPtr<wchar_t> spszName;
  StrRetToStr(&str, spidl, &spszName);

  POINT pt;
  spView->GetItemPosition(spidl, &pt);
  
  wprintf(L"At %4d,%4d is %ls\n", pt.x, pt.y, spszName);
 }
 return 0;
}

After getting the IFolder­View, we also ask for the corresponding IShell­Folder. This isn't actually necessary for enumerating the icons, but it lets us print their names.

We ask the view for its Items enumeration, then proceed to enumerate each of the items. For each item, we ask the IShell­Folder for its name, and we ask the IFolder­View for its position. Then we print the results.

Okay, that was neat, but you can do more than just query the positions. You can also modify them.

int __cdecl wmain(int argc, wchar_t **argv)
{
 CCoInitialize init;
 CComPtr<IFolderView> spView;
 FindDesktopFolderView(IID_PPV_ARGS(&spView));

 CComPtr<IEnumIDList> spEnum;
 spView->Items(SVGIO_ALLVIEW, IID_PPV_ARGS(&spEnum));
 for (CComHeapPtr<ITEMID_CHILD> spidl;
      spEnum->Next(1, &spidl, nullptr) == S_OK;
      spidl.Free()) {
  POINT pt;
  spView->GetItemPosition(spidl, &pt);
  pt.x += (rand() % 5) - 2;
  pt.y += (rand() % 5) - 2;

 PCITEMID_CHILD apidl[1] = { spidl };
 spView->SelectAndPositionItems(
     1, apidl, &pt, SVSI_POSITIONITEM);
 }
 return 0;
}

This time, instead of printing the item's name and position, we jiggle the icon position by a few pixels randomly, then set the jiggled coordinates as the new position.

Turn off Auto arrange icons and Align icons to grid on the desktop, and then run this program. Hey, look, your icons shifted randomly by a few pixels.

For extra hijinx, drop a call to spView->Set­Current­Folder­Flags(FWF_AUTO­ARRANGE | FWF_SNAP­TO­GRID, 0) before you enter the loop (to programmatically turn off auto-arrange and snap-to-grid), then put this program in a loop, and slip it onto a friend's (or enemy's) computer.

More seriously, we can we put the two pieces together to make a program that saves and restores desktop icon positions.

Second reminder: These "Little Programs" do no error checking because they are intended as demonstrations, not production-ready applications.

void SavePositions(IFolderView *pView, PCWSTR pszFile)
{
 CComPtr<IStream> spStream;
 SHCreateStreamOnFileEx(pszFile, STGM_CREATE | STGM_WRITE,
     FILE_ATTRIBUTE_NORMAL, TRUE, nullptr, &spStream);
 CComPtr<IEnumIDList> spEnum;
 pView->Items(SVGIO_ALLVIEW, IID_PPV_ARGS(&spEnum));
 for (CComHeapPtr<ITEMID_CHILD> spidl;
      spEnum->Next(1, &spidl, nullptr) == S_OK;
      spidl.Free()) {
  IStream_WritePidl(spStream, spidl);
  POINT pt;
  pView->GetItemPosition(spidl, &pt);
  IStream_Write(spStream, &pt, sizeof(pt));
 }
}

The Save­Positions function enumerates all the icons in a view and writes their identities and positions to a file.

void RestorePositions(IFolderView *pView, PCWSTR pszFile)
{
 CComPtr<IStream> spStream;
 SHCreateStreamOnFileEx(pszFile, STGM_READ,
     FILE_ATTRIBUTE_NORMAL, FALSE, nullptr, &spStream);
 POINT pt;
 for (CComHeapPtr<ITEMID_CHILD> spidl;
      SUCCEEDED(IStream_ReadPidl(spStream, &spidl)) &&
      SUCCEEDED(IStream_Read(spStream, &pt, sizeof(pt)));
      spidl.Free()) {
  PCITEMID_CHILD apidl[1] = { spidl };
  pView->SelectAndPositionItems(1, apidl, &pt, SVSI_POSITIONITEM);
 }
}

The Restore­Positions function does the reverse. It reads the identities and positions from the file and calls IFolder­View::Select­And­Position­Items to move the item to its previously-saved position.

int __cdecl wmain(int argc, wchar_t **argv)
{
 if (argc != 3) {
  wprintf(L"Usage: %ls save filename\n"
          L"       %ls restore filename\n", argv[0], argv[0]);
  return 0;
 }
 CCoInitialize init;

 CComPtr<IFolderView> spView;
 FindDesktopFolderView(IID_PPV_ARGS(&spView));

 if (wcscmp(argv[1], L"save") == 0) {
  SavePositions(spView, argv[2]);
 } else if (wcscmp(argv[1], L"restore") == 0) {
  RestorePositions(spView, argv[2]);
 }
 return 0;
}

And all that's left is to write the main program that calls either the Save­Positions or Restore­Positions function based on the command line parameters.

Exercise: Discuss what happens if you rename an item on the desktop, and then try to restore its position. What could be done to address this?

Comments (12)
  1. Dan Bugglin says:

    Presumably you'd use something less likely to change than the name of the shortcut, such as the target's full path (minus command line parameters), or some other field (do shortcuts get an internal unique ID?  I don't know much about these things).

  2. Dan Bugglin says:

    Alternatively, depending on what use case this tool is for, you may also want to extend your program to backup the entire contents of the user's Desktop folder (and perhaps the shared Desktop folder) elsewhere along with the icon position information, and restore it later.  In which case renamed shortcuts aren't a problem.

  3. Jeffrey Bosboom says:

    I'm surprised programmatic access to desktop icon arrangement is supported, given the potential for evil applications to insist their icon appear in a particular location.  Though I guess programs would try to move files off the desktop, create their icon, then (hopefully) move the files back to the desktop — so perhaps just letting them move their icon is a harm reduction strategy.

  4. C1 says:

    For those trying to mess with their coworkers, spView->SetCurrentFolderFlags(FWF_AUTOARRANGE | FWF_SNAPTOGRID, 0); requires IFolderView2 only available on Vista or later.

    Funny that this doesn't require elevation to mess with the desktop.

    [Non-issue because SWC_DESKTOP already requires Vista or higher. And this doesn't require elevation because non-administrators are allowed to customize their desktop icon positions. -Raymond]
  5. Paul Parks says:

    @C1: I would think that the "airtight hatchway" rule applies to desktop icon positioning.

  6. RE: desktop icon position says:

    I wished for something like this in the XP days, so that icons could be properly restored after resolution changes. At one point when I attempted my multiple desktops app, I thought of this to allow different icon layout per desktop…

  7. Cheong says:

    I'll suggest to put the "reminder" thing inside the code as comment. So if the code somehow ends in some production code, the people following the code can have a chance to know what should be done with it.

  8. Neil says:

    The solution to the exercise is to create and serialise shortcuts to all of the desktop items. The shell will then track the shortcuts' targets for you. More information is available in a handy blog post from 24th February, 2011.

  9. GregM says:

    "More seriously, we can we put the two pieces together to make a program that saves and restores desktop icon positions."

    I was very happy when I got to this sentence.  This was exactly what I thought of when I read the article title.

  10. Dan Bugglin says:

    "Non-issue because SWC_DESKTOP already requires Vista or higher. And this doesn't require elevation because non-administrators are allowed to customize their desktop icon positions."

    But annoyingly, they can't remove shared desktop icons.  Which makes sense from a security standpoint (no access to remove the shortcut) but not a UX standpoint (imo Explorer should simply hide the file if it can't remove it because of that type of issue).

    [True, but then you have the problem of where to store this "files that have been hidden" state and what to do about it when the profile roams. (Since the shared desktop does not roam, but the user desktop does.) It's bad enough when icon positions are reset when your profile roams, imagine if files magically unhid too. -Raymond]
  11. This post reminds me of the time years ago when a customer requested that we build a custom solution that would ensure that the organization's logo in their mandated desktop wallpaper would never be obscured by an icon.  If a new icon were created or a user moved an existing icon such that it obscured part of the logo, desktop monitoring software would move it someplace else.  That still ranks as the single dumbest customer request I can remember ever receiving.  (And that's saying something.)

  12. Medinoc says:

    Wait, you can use CSIDLs in place of PIDLs? I always thought they only worked with SHGet[Special]FolderLocation/Path…

Comments are closed.