« Always looking for good programmers... | Main | Using "Background Processing Mode" from C# »

October 1, 2008

Displaying a Splash Screen with C++ (Part IV)

This is Part IV of a series on creating a splash screen application in native code. For more information, see the Introduction and the Code License.

Part IV: Dismissing the Splash Screen

When the WPF application has initialised and is ready to display its main window, the splash screen needs to be dismissed. The easiest way to accomplish this is by having a named event that the native application creates and the WPF application sets.

Firstly, we'll create the named event that will dismiss the splash screen. This also has the beneficial side effect that we can prevent more than one splash screen application from running at once. (Note that the WPF application will also need to ensure that only one instance runs at a time, if that is desired. On the other hand, if you want to be able to launch multiple instances of the splash screen and WPF applications at once, each will need to have a unique event.)

// create the named close splash screen event, making sure we're the first process to create it

SetLastError(ERROR_SUCCESS);

HANDLE hCloseSplashEvent = CreateEvent(NULL, TRUE, FALSE, _T("CloseSplashScreenEvent"));

if (GetLastError() == ERROR_ALREADY_EXISTS)

    ExitProcess(0);

Once we know it's safe to continue running, we can use the code shown in Parts I-III to display the splash screen and launch the WPF application, then wait for the "close splash screen" event to be set or the WPF application to exit; once either of these events happens, it's time to dismiss the splash screen.

// call LoadSplashImage() etc.

// call SetSplashImage() etc.

 

// launch the WPF application

HANDLE hProcess = LaunchWpfApplication();

AllowSetForegroundWindow(GetProcessId(hProcess));

 

// display the splash screen for as long as it's needed

HANDLE aHandles[2] = { hProcess, hCloseSplashEvent };

PumpMsgWaitForMultipleObjects(2, &aHandles[0], INFINITE);

The PumpMsgWaitForMultipleObjects method has not yet been defined. It's similar (in API) to the Win32 WaitForMultipleObjects function, but it also dispatches window messages as they arrive. Since we have created a window on this thread, running a message pump is essential. We use MsgWaitForMultipleObjects to wait for either of two HANDLEs while also being woken up when a window message arrives. (Note that this implementation is more generic than is necessary in this example: the timeout is always INFINITE, and there's no outer message loop that would need to reprocess the WM_QUIT message.)

inline DWORD PumpMsgWaitForMultipleObjects(DWORD nCount, LPHANDLE pHandles, DWORD dwMilliseconds)

{

    // useful variables

    const DWORD dwStartTickCount = ::GetTickCount();

 

    // loop until done

    for (;;)

    {

        // calculate timeout

        const DWORD dwElapsed = GetTickCount() - dwStartTickCount;

        const DWORD dwTimeout = dwMilliseconds == INFINITE ? INFINITE :

            dwElapsed < dwMilliseconds ? dwMilliseconds - dwElapsed : 0;

 

        // wait for a handle to be signaled or a message

        const DWORD dwWaitResult = MsgWaitForMultipleObjects(nCount, pHandles, FALSE, dwTimeout, QS_ALLINPUT);

        if (dwWaitResult == WAIT_OBJECT_0 + nCount)

        {

            // pump messages

            MSG msg;

            while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) != FALSE)

            {

                // check for WM_QUIT

                if (msg.message == WM_QUIT)

                {

                    // repost quit message and return

                    PostQuitMessage((int) msg.wParam);

                    return WAIT_OBJECT_0 + nCount;

                }

 

                // dispatch thread message

                TranslateMessage(&msg);

                DispatchMessage(&msg);

            }

        }

        else

        {

            // timeout on actual wait or any other object

            return dwWaitResult;

        }

    }

}

Lastly, the WPF application needs to signal the splash screen application to close. (Finally, some C# code!)

private void CloseSplashScreen()

{

    // signal the native process (that launched us) to close the splash screen

    using (var closeSplashEvent = new EventWaitHandle(false,

        EventResetMode.ManualReset, "CloseSplashScreenEvent"))

    {

        closeSplashEvent.Set();

    }

}

As long as the same name is used, the event will be shared across the two processes and the splash screen application will exit when the event is set.

On my slow XP computer, the native application displays the splash screen in well under a second, even from a cold start. Sometimes it's necessary to drop back to native code for small feature areas with strict performance demands. Displaying UI as soon as possible when the application is launched is one of those areas; a few hundred lines of native code can result in a perceived decrease in application startup time and a better experience for the end user.

Posted by Bradley Grainger at October 1, 2008 8:31 AM

Trackback Pings

TrackBack URL for this entry:
http://blog.logos.com/mt-cgi/mt-tb.cgi/254

Comments

Did you specifically choose the named event over something like WaitForInputIdle()? If you used that, then the C# code wouldn't need to know about the splash screen at all.

Posted by: John Fisher at October 1, 2008 10:56 AM

I had never actually heard of WaitForInputIdle before. One problem is that it sounds like it is a blocking call, but this thread (that's calling it) needs to pump messages. (One could write a loop that waits for a short time, then pumps any messages, then waits again.)

I think some experimentation is required; it would be rather nice if the C# application didn't have to know about the splash screen.

Posted by: Bradley Grainger Author Profile Page at October 1, 2008 2:04 PM

I just tried using WaitForInputIdle in the following manner: After creating the WPF process, I created a background thread that called WaitForInputIdle(hProcess, INFINITE), then returned. In the main thread, I used PumpMsgWaitForMultipleObjects to wait for the thread handle. (Thus, I was pumping messages while waiting for the WPF application to be idle.)

Unfortunately, WaitForInputIdle returned (and thus the splash screen disappeared) several seconds before my WPF application's UI was visible on the screen. I tried launching several different WPF applications and in every case, the splash screen disappeared too soon; without spending a lot of time debugging the issue, it looks like WaitForInputIdle does not correctly detect if a WPF application has finished initialising and is waiting for input.

Posted by: Bradley Grainger Author Profile Page at October 1, 2008 3:22 PM

I was curious, so I tried several things to see what was going on here. With the following code, I was able to successfully wait for a WPF app to become visible before closing my test launching app.


// Run it with this code.
if (WaitForMainWindow(pi.hProcess, hi.dwProcessId))
{
PumpMessagesAndWaitForInputIdle(pi.hProcess);
}
PostQuitMessage(0);
  // SUPPORTING FUNCTIONS
  // Returns false when it's time to quit.
bool PumpMessages()
{
// pump messages
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) != FALSE)
{
// check for WM_QUIT
if (msg.message == WM_QUIT)
{
return false;
}

// dispatch thread message
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return true;
}

// Returns false when it's time to quit.
bool PumpMessagesAndWaitForInputIdle(HANDLE hProcess)
{
while (WaitForInputIdle(hProcess, 10) == WAIT_TIMEOUT)
{
if (!PumpMessages())
{
return false;
}
}
return true;
}

bool FoundWindowHandle;
BOOL CALLBACK FindMainWindow(HWND hWnd, LPARAM lParam)
{
DWORD DesiredProcessId = (DWORD)lParam;
DWORD testProcessId = NULL;
if (0 < GetWindowThreadProcessId(hWnd, (LPDWORD)(&testProcessId)))
{
if (testProcessId == DesiredProcessId)
{
if (0 != IsWindowVisible(hWnd))
{
FoundWindowHandle = true;
return FALSE; // Quit looking. Found it!
}
}
}

// Keep looking.
return TRUE;
}

// Returns false when the app should quit.
bool WaitForMainWindow(HANDLE hProcess, DWORD processId)
{
while (!FoundWindowHandle)
{
DWORD exitCode = 0;
if (0 != GetExitCodeProcess(hProcess, &exitCode))
{
if (exitCode != STILL_ACTIVE)
{
return false; // We'll never find the main window, the process no longer exists.
}
}

if (!PumpMessages())
{
return false; // Time to quit.
}

// Look for the main window for the process we want.
FoundWindowHandle = false;
EnumDesktopWindows(NULL, FindMainWindow, processId);
if (FoundWindowHandle)
{
return true;
}
}

return true;
}

Posted by: John Fisher at October 2, 2008 12:04 PM

My C++ skills are a bit rusty, if possible could you please post the complete project source code.

Posted by: Matt at October 13, 2008 10:20 AM

Post a comment




(you may use HTML tags for style)