Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Design

Private Desktops & Windows XP


Jan03: Private Desktops & Windows XP

Steve is a contractor currently working at Microsoft on mobile applications for Pocket PC-based cell phones. He can be contacted at [email protected].


Kiosk applications, such as library card catalogs, need to present a full-screen user interface, hiding the Explorer shell and suppressing other applications' windows and dialog boxes. This is becoming more difficult to accomplish as protected operating systems such as Windows XP replace more lenient ones like Windows 98. Microsoft's DirectX API lets you take over the screen even on Windows XP, but DirectX is really intended for games that do all of their own custom drawing. It is not meant for applications that want to use ordinary GDI and Windows Manager APIs but the screen all to themselves. In this article, I present a solution for such applications—private desktops.

Private desktops have been supported by Windows NT since Version 3.51. Normally, the desktop APIs are used only by system services, such as the Winlogon service that takes over when you hit Ctrl-Alt-Del. Microsoft even buries the description of the desktop APIs inside the "Services" section of the MSDN Library, rather than the "User Interface" section. However, applications can use the full range of desktop APIs, too. While these techniques are especially useful for kiosk applications running on Windows NT Embedded or Windows XP Embedded, you can use them on any version of Windows NT, 2000, or XP.

Unfortunately, private desktops are not available under Windows 95, 98, or ME. However, as these operating systems fade into history, this limitation becomes less relevant. Furthermore, typical kiosk applications tend to be developed for a single, known environment; thus, developers of such applications usually only have to worry about targeting one operating system.

What Is a Desktop?

According to Microsoft, a desktop is a container for windows, menus, and hooks. Only one desktop can be visible on the screen at a time. Any other desktops—and all the windows on them—are completely hidden. When you log on to a typical Windows XP machine, you automatically get three desktops:

  • Default, where your Explorer shell appears, along with most of your applications.
  • Winlogon, where the Windows Security dialog appears when you press Ctrl-Alt-Del.

  • Disconnect, which is used by Terminal Services.

Do not assume that your application is running on the Default desktop. Shells can start new processes on other desktops, as the Desk Jockey application, which I present here, shows. Therefore, if you need to interact with the Default desktop explicitly, you should call OpenDesktop() with the string "Default" (desktop names are not localized).

The desktop that is currently visible is called the "input desktop." You can get a handle to the current input desktop by calling OpenInputDesktop(). You can change the input desktop by calling SwitchDesktop() to make a different desktop visible.

Applications can create new desktops by calling CreateDesktop(). You must specify a name for the new desktop; obviously, you should not use Default, Winlogon, or Disconnect. If you want to find out the name of a desktop based on its handle, call GetUserObjectInformation(). Desktop names are not local to your application. If users run another application that tries to create or open a desktop with the same name as yours, that application's windows appear on your desktop.

However, you can protect a desktop using Windows NT security descriptors. As an example, the Winlogon desktop is heavily protected because that desktop is used for entering and changing passwords. For this reason, do not assume that you can create windows on every desktop or make every desktop visible.

Each thread in an application is assigned a desktop. When a thread creates a window, it goes on that thread's desktop, which might not be the same as the current input desktop. Initially, each thread uses the desktop specified in its process's STARTUPINFO struct. You can read this information by calling GetStartupInfo(). If the STARTUPINFO does not specify a desktop explicitly, the process uses the same desktop as its parent process. Threads can get a handle to their desktop by calling GetThreadDesktop(). They can change their desktop by calling SetThreadDesktop(), but only if they don't have any windows open at the time.

Desktops go away when no processes are using them. A process may still be using its original desktop, even if none of its threads are, because that desktop still gets assigned to any new threads that the process creates. To indicate that you are finished using a desktop, close its handle using CloseDesktop(). You must use this API, not CloseHandle(). CloseDesktop() fails if the desktop is the original one assigned to the process, or if any threads in the process are still using the desktop.

As you can see, desktops are a powerful feature, supported by the operating system at a very low level.

Desktop Demo

Listing One is a simple application that creates a new, private desktop, switches to it, and puts up a message box; see Figure 1. When you run this application, notice that the private desktop is completely blank, except for the message box.

The application begins by saving two desktop handles for later: the desktop that is currently assigned to the running thread (GetThreadDesktop()), and the desktop that is currently visible (OpenInputDesktop()). These might not be the same! You must remember to make this distinction when you are writing your own applications.

Then the application creates a new desktop (CreateDesktop()). To illustrate the typical case, it requests all possible privileges for this desktop.

Next, the application changes the desktop for the current thread to the new desktop (SetThreadDesktop()). If the thread didn't do this, the message box would appear on the original desktop, even if the new desktop was visible.

Finally, the application makes the new desktop visible (SwitchDesktop()). For all intents and purposes, the application now owns the screen.

After users dismiss the message box, the application makes the original input desktop visible again. Then it switches the thread's assigned desktop back so it can close the handle to the private desktop cleanly. This lets the system destroy the private desktop.

Fasten Your Seatbelts

Here's a lesson I learned the hard way: When you are developing programs that create and activate private desktops, you may find yourself locked out of your computer. Consider what happens if a program creates a desktop, switches to it, and then crashes. You are now stuck with an empty desktop on your screen—no windows, no taskbar, no menu.

Think you can Ctrl-Alt-Del your way out? Think again. Remember that the three-fingered salute activates a special desktop of its own, the Winlogon desktop. If you cancel, you go back to your empty desktop. If you start Task Manager, Winlogon brings it up on the Default desktop, but then returns you to your empty desktop! (This is a bug, as far as I'm concerned.) The only solution is to log off, which means you lose any unsaved work.

If you're using the Windows XP Welcome screen, it gets worse. Ctrl-Alt-Del always goes straight to Task Manager, which still comes up on the Default desktop, which is still invisible. To log yourself off, you must use the Win-L key to fast-switch to an administrator account other than your own. If you aren't using Fast User Switching, or if you don't have another administrator account to log in to, then you must reboot your machine!

To avoid this gnarly situation, I wrote a desktop watchdog (Listing Two), which I make sure is running before I try out any other code that switches desktops.

The watchdog spends most of its time sleeping. Every 10 seconds, it wakes up and asks the system what the current input desktop is. If the watchdog is denied read access to the input desktop (which happens when the Winlogon desktop is active), or if the input desktop has at least one window, then the watchdog goes back to sleep. Otherwise, if the input desktop is empty, the watchdog tries to activate its own desktop. If there aren't any windows on that desktop, either, it finally tries activating the Default desktop, so you can get to Explorer or Task Manager.

Desk Jockey

The third application I present in this article, Desk Jockey (available electronically; see "Resource Center," page 5), presents a UI that lets you exercise the desktop APIs; see Figure 2. At the top of the window is the name of the desktop where the Desk Jockey window appears. Underneath is a list of all the desktops that are available, according to the EnumDesktops() API. This list is updated every five seconds. Finally, there are three buttons: New, Switch, and Run.

To create a private desktop, click the New button. Enter a name and choose which flags get passed to CreateDesktop(). You must keep DESKTOP_CREATEWINDOW selected, or the API fails. If your desktop gets created successfully, it appears in the list.

After Desk Jockey creates the private desktop, it opens a copy of its main window on that desktop. Because each thread can have windows open only on one desktop at a time, Desk Jockey creates a separate thread to deal with the private desktop. Listing Three shows what happens in the new thread. First, the thread saves a handle to its original desktop. Then, it switches itself to the private desktop and creates the main window. This means that each of Desk Jockey's threads must run a message loop, which is unusual for a Windows application, but perfectly legal. When the message loop ends, the thread switches back to its original desktop, then closes the handle to the private desktop. If no other processes are using the private desktop at that point, the system will destroy the private desktop.

When you click New, you can enter the same name as an existing desktop. This is useful for reopening the Desk Jockey window on that desktop if it was closed previously. Of course, some other application would have to be using the desktop for it to have remained alive at all.

Just creating a private desktop does not make it visible automatically. To see your private desktop, you must select it and click the Switch button. Then your screen switches to the private desktop, which should have a Desk Jockey window on it; notice the label at the top of the Desk Jockey window in Figure 3.

Desk Jockey lets you start a command prompt on any desktop by selecting the desktop from a list and clicking the Run CMD button. Listing Four shows how to specify desktops for new processes using the CreateProcess() API. Of course, if you select a desktop other than the current input desktop, you will not see the command prompt until you click the Switch button. Once you have a command prompt, you can start other applications, including Explorer. If you do this with the runas command, you can give yourself a poor-man's version of Fast User Switching, even on Windows 2000 or an XP machine that is part of a domain. This is useful for administrators who want to use a regular user account for their routine work, and have privileged programs (like MMC) running on a separate desktop.

Once you have other windows open on the private desktop, you can close the Desk Jockey window safely; the desktop will remain alive because other processes are using it. Of course, don't forget to give yourself some means of switching back to your original desktop.

Conclusion

The applications presented here illustrate three common tasks that kiosk applications must perform with private desktops: creating and activating the desktops, juggling windows on multiple desktops, and starting new processes on private desktops. With these techniques, you can write applications that get the whole user interface to themselves, preventing curious users from accessing other applications on the system.

DDJ

Listing One

// Desktop Demo.cpp
#include "stdafx.h"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                                         LPSTR lpCmdLine, int nCmdShow)
{
   HDESK hdeskOriginalThread = GetThreadDesktop(GetCurrentThreadId());
   HDESK hdeskOriginalInput = OpenInputDesktop(0,FALSE,DESKTOP_SWITCHDESKTOP);
   HDESK hdeskNewDesktop = CreateDesktop(TEXT("MyDesktop"), NULL, NULL, 0,
                                                         GENERIC_ALL, NULL);
    SetThreadDesktop(hdeskNewDesktop);
    SwitchDesktop(hdeskNewDesktop);
    MessageBox(NULL, TEXT("This message is appearing on a new desktop!"),
                                              TEXT("Desktop Demo"),MB_OK);
    SwitchDesktop(hdeskOriginalInput);
    SetThreadDesktop(hdeskOriginalThread);
    CloseDesktop(hdeskNewDesktop);
    return 0;
}

Back to Article

Listing Two

// Watchdog.cpp
#include "stdafx.h"
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
    bool * pFound = (bool *)lParam;
    *pFound = true;
    return FALSE;
}
bool IsInputDesktopEmpty()
{
    HDESK hdesk = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
    if (hdesk == NULL)
    {
        // This almost always means the Winlogon desktop is active.
        return false;
    }
    bool found = false;
    EnumDesktopWindows(hdesk, EnumWindowsProc, (LPARAM)&found);
    CloseDesktop(hdesk);
    return !found;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                                         LPSTR lpCmdLine, int nCmdShow)
{
    while (true)
    {
        if (!IsInputDesktopEmpty())
        {
            Sleep(10000);
        }
        else
        {
            // Wait 2 secs and test again, just to be sure.
            Sleep(2000);
            if (!IsInputDesktopEmpty())
                continue;
            // Try switching to the watchdog's desktop.
            HDESK hdeskMine = GetThreadDesktop(GetCurrentThreadId());
            SwitchDesktop(hdeskMine);
            // Backup plan:  switch to the default desktop.
            if (IsInputDesktopEmpty())
            {
                HDESK hdeskDefault = OpenDesktop(TEXT("Default"), 0, FALSE,
                                                      DESKTOP_SWITCHDESKTOP);
                SwitchDesktop(hdeskDefault);
                CloseDesktop(hdeskDefault);
            }
        }
    }
    return 0;
}

Back to Article

Listing Three

// From Desk Jockey.cpp. Entry point for threads other than the main thread.
DWORD WINAPI MyThreadProc(LPVOID lpv)
{
    // Save handle to original desktop.
    HDESK hdeskOriginal = GetThreadDesktop(GetCurrentThreadId());
    // Start working with private desktop.
    HDESK hdesk = static_cast<HDESK>(lpv);
    if (SetThreadDesktop(hdesk))
    {
        CreateMyMainWindow();
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    // Restore original desktop, which must still be alive
    // precisely because it is this process's original desktop.
    SetThreadDesktop(hdeskOriginal);
    // Release private desktop. Will be destroyed now, unless
    // some other process is still using it.
    CloseDesktop(hdesk);
    return 0;
}

Back to Article

Listing Four

// From Desk Jockey.cpp. Start a command prompt on any named desktop.
// Called by MainWindowProc() upon WM_COMMAND from Run button.
void CreateNewShell(LPTSTR desktop)
{
    TCHAR path[MAX_PATH + 1];
    GetSystemDirectory(path, MAX_PATH);
    PathAppend(path, TEXT("cmd.exe")); // useful func in shlwapi.dll
    PROCESS_INFORMATION pi = {0};
    STARTUPINFO si = {0};
    si.cb = sizeof(si);
    si.lpTitle = desktop;
    si.lpDesktop = desktop; // here's the important line!
    if (CreateProcess(path, TEXT("cmd"), NULL, NULL, TRUE, CREATE_NEW_CONSOLE,
                                                       NULL, NULL, &si, &pi))
    {
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        DisplayError(NULL, TEXT("CreateProcess"));
    }
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.