What if my application is really two applications bundled into a single file, and I want them collected into two groups on the taskbar in Windows 7?


A customer wanted to prevent multiple copies of their program from being grouped on the taskbar. They didn't give an explanation why, but let's assume that they are doing this for honorable purposes rather than as a way to annoy the user. For example, maybe their program is really multiple applications bundled inside a single EXE file for convenience.

The information you need to do this is in MSDN under Application User Model IDs, specifically in the Where to assign an AppUserModelID section. I'll assume you've read the guidance there, and I'm just going to dive into the implementation.

Suppose our scratch program can serve both as a floor wax and as a dessert topping. It decides on the mode based on a command line switch.

#include <shlobj.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
    MSG msg;
    HWND hwnd;

    g_hinst = hinst;

    if (!InitApp()) return 0;

    BOOL fDessert = strcmp(lpCmdLine, "-dessert") == 0;
    SetCurrentProcessExplicitAppUserModelID(fDessert ?
            L"Contoso.LitWare.DessertTopping" :
            L"Contoso.LitWare.FloorWax");

    if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */
        hwnd = CreateWindow(
            TEXT("Scratch"),                /* Class Name */
            fDessert ? TEXT("Dessert topping") : TEXT("Floor wax"),
            WS_OVERLAPPEDWINDOW,            /* Style */
            CW_USEDEFAULT, CW_USEDEFAULT,   /* Position */
            CW_USEDEFAULT, CW_USEDEFAULT,   /* Size */
            NULL,                           /* Parent */
            NULL,                           /* No menu */
            hinst,                          /* Instance */
            0);                             /* No special parameters */

    ...
}

Run this program a few times, some with the -dessert switch and some without. Observe that the dessert versions and non-dessert versions group separately.

The next level of fancy-pants behavior is to give different AppIDs to different windows within a single process. You might do this if your combination floor wax/dessert topping program actually runs both modes inside the same process. Something like this:

#include <shellapi.h>
#include <propkey.h>
#include <propvarutil.h>
#include <shlobj.h>

int g_cWindows = 0;

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
  ++g_cWindows;
  return TRUE;
}

void
OnDestroy(HWND hwnd)
{
  if (--g_cWindows == 0) PostQuitMessage(0);
}

HWND
CreateTaskWindow(BOOL fDessert, int nShowCmd)
{
  HWND hwnd = CreateWindow(
      TEXT("Scratch"),                /* Class Name */
      fDessert ? TEXT("Dessert topping") : TEXT("Floor wax"),
      WS_OVERLAPPEDWINDOW,            /* Style */
      CW_USEDEFAULT, CW_USEDEFAULT,   /* Position */
      CW_USEDEFAULT, CW_USEDEFAULT,   /* Size */
      NULL,                           /* Parent */
      NULL,                           /* No menu */
      g_hinst,                        /* Instance */
      0);                             /* No special parameters */
  if (hwnd) {
    IPropertyStore *pps;
    HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_PPV_ARGS(&pps));
    if (SUCCEEDED(hr)) {
      IPropertyStore_SetValue(pps, PKEY_AppUserModel_ID,
            fDessert ?
            L"Contoso.LitWare.DessertTopping" :
            L"Contoso.LitWare.FloorWax");
      pps->Release();
    }
    ShowWindow(hwnd, nShowCmd);
  }
  return hwnd;
}

void
OnChar(HWND hwnd, TCHAR ch, int cRepeat)
{
    switch (ch) {
    case 'd': CreateTaskWindow(TRUE, SW_SHOWNORMAL); break;
    case 'f': CreateTaskWindow(FALSE, SW_SHOWNORMAL); break;
    }
}

    HANDLE_MSG(hwnd, WM_CHAR, OnChar);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev,
                   LPSTR lpCmdLine, int nShowCmd)
{
    MSG msg;
    HWND hwnd;

    g_hinst = hinst;

    if (!InitApp()) return 0;

    BOOL fDessert = strcmp(lpCmdLine, "-dessert") == 0;
    // SetCurrentProcessExplicitAppUserModelID(...);

    if (SUCCEEDED(CoInitialize(NULL))) {/* In case we use COM */
        hwnd = CreateTaskWindow(fDessert, nShowCmd);

        // ShowWindow(hwnd, nShowCmd);
    ...
}

This time, instead of setting the application ID globally, we set it on a per-window basis. When you run this program, you can press "f" to open a new floor wax window or "d" to open a new dessert topping window. As before, observe that the two types of windows group separately.

The last detail is setting the System.AppUserModel.ID property on the shortcuts used to launch these programs. You can do this from MSI by adding an entry to your Msi­Shortcut­Property table, or if you create your shortcuts programmatically, you do this by setting the property yourself:

 CComPtr<IShellLink> spsl;
 spsl.CoCreateInstance(CLSID_ShellLink);
 spsl->SetPath(TEXT("C:\\Path\\to\\scratch.exe"));
 CComQIPtr<IPropertyStore> spps(spsl);
 IPropertyStore_SetValue(spps, PKEY_AppUserModel_ID,
                         L"Contoso.LitWare.FloorWax");
 spps->Commit();
 CComQIPtr<IPersistFile>(spsl)->Save(L"LitWare Floor Wax.lnk", TRUE);

Next time, we'll look at another reason you might want to customize how your application group on the taskbar in Windows 7.

Comments (14)
  1. voo says:

    Considering that every feature starts with -100 points, I'm extremely surprised that someone took the time to actually implement this stuff. I assume VMs use this API for unity mode (well VMWare calls it that; basically every program inside VM gets a taskbar entry in the host)?

    Other than that I don't really see where this would be used in practice

  2. And some end user control over it: 7+ Taskbar Tweaker (rammichael.com/7-taskbar-tweaker)

  3. Uh! says:

    If only there was the option "Group explorer windows but never other applications" I will be in heaven.

    (Ok I would be in heaven after the above item and office apps – excel in particular – started behaving like SDI apps).

  4. Cesar says:

    I believe LibreOffice makes use of this feature. I think I saw a commit there doing something with AppUserModelID, and I believe they do something related in the registry too.

    LibreOffice is a good example of the kind of program where this feature makes sense; it would allow the Writer windows to be grouped separate from the Calc windows, and so on.

  5. BZ says:

    I think some web browsers segregate developer windows from actual web pages in this way.

  6. Adrian says:

    I wish the grouping behavior was more directly under control of the user.

    Many applications today are web-based.  I see no reason why the browser instance I have open for email (er, webmail) should be glommed to the browser instance where I do my MSDN searches, the one I use for accessing the bug database, or the one where I read blogs during long compiles.  The only way to handle this today is to use a different browser for each task.

  7. There was a manager who decided: "Users should never be able to pin SDK/DDK documentation viewer on the taskbar." So he applied all his influence to make a special case for a shortcut with "Documentation" in it. Never mind -100 points. I guess they have things they could use to bully devs around at MS.

    [Um, no, that's not what happened. Documentation links are filtered because the same code that does Start menu filtering also does Taskbar filtering. I like how you assume that something you don't like is the result of a conspiracy. -Raymond]
  8. David Walker says:

    For anyone who doesn't like grouped icons, remember that a two-row-high taskbar gives you a lot more room for taskbar icons.  I set the option to disallow grouping completely, and I like it that way.

  9. dbacher says:

    @voo,

    If you read through the examples — Java and Silverlight, for example, each use a single executable to run multiple programs.  The program is determined by which JAR/XAP is loaded.  Going down that line, you have Python, Ruby, WSH, etc. that also work that way (one host app, but many different real applications running under it).

    Similarly, there are times when you might use multiple apps — for example one way to deal with a hybrid console/windows app is to compile the app as a console app and compile the app as a Windows app, and have the console app launch the Windows app when required.  Or maybe you want to have a patcher that runs before a game, etc.

  10. Joshua says:

    OK Dumb Question: How do I set task button by API call rather than COM interop to some library that calls in 100MB of dependencies for .NET applications.

  11. dbacher says:

    @joshua,

    Microsoft's shell folks are deathly allergic to C#.  If they even use managed c++, they will have hives for a month.  As a result, typically all shell, dwm, etc. involves interop.  Additionally, the dev div guys have painted a line down the center of the hall, and if a shell guy crosses the line, then they run screaming to Ballmer.  It's as annoying as the almost but not quite COM utilized by windows media that requires a wrapper library to work.  Some of these are available in the windows 7 API pack on codeplex (on my phone right now and can't easily pull the URL), which is a volunteer effort by people on the API teams, as well as others, to cover managed code.

  12. Joshua says:

    If Microsoft hadn't seriously messed-up their C libraries, I'd just build a native DLL to do the P/Invoke to COM inversion, but too bad really.

  13. [Um, no, that's not what happened. Documentation links are filtered because the same code that does Start menu filtering also does Taskbar filtering. I like how you assume that something you don't like is the result of a conspiracy. -Raymond]

    The KB 282066 was conveniently retired from circulation. Thanks, Microsoft, for deciding what we don't need anymore. Meanwhile, MS DOS 5.0 articles are still there.

  14. @alegr1: A quick (your favorite search engine) search to get the complete URL to the old article, followed by a Wayback archive searc (at http://www.archive.org) yielded the following historical snapshot of the page you're complaining about being missing:

    web.archive.org/…/282066

    Wayback is your friend.

Comments are closed.