Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Windows Forms
Article

A Single Instance Application which Minimizes to the System Tray when Closed

Rate me:
Please Sign up or sign in to vote.
4.67/5 (50 votes)
17 Jul 2005CDDL8 min read 281.8K   3.6K   233   44
Creating a single instance application that stays in the system tray.

Introduction

This article addresses three issues:

  1. Creating a single instance application.
  2. Restoring the previous instance, if user tries to launch another instance.
  3. Minimizing (with animation) the application to the Notification Area of the task bar when the window is closed.

How to Create a Single Instance Application?

Often, it becomes necessary to ensure that only one instance of a program is running at any time. If the user tries to run another instance, either the user is to be notified that an instance is already running or the previous instance has to be activated and brought to the foreground. In the case of a Windows application we want to restore the existing app’s main window. So when an application is started, it checks to see if there is another instance already running. If there is one, the current instance quits and the previous instance’s main window is activated and displayed to the user.

Making an application single instance, can be achieved by using a mutex (Mutual Exclusion Semaphore). A Windows application loads the main form through the Application.Run( ) method. In the Main method, create a new mutex. If a new mutex is created the application is allowed to run. If the mutex has already been created, the application cannot start. This will ensure that only one instance will be running at any time.

C#
// Used to check if we can create a new mutex
bool newMutexCreated = false;
// The name of the mutex is to be prefixed with Local\ to make
// sure that its is created in the per-session namespace,
// not in the global namespace.
string mutexName = "Local\\" + 
  System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;

Mutex mutex = null;
try
{
    // Create a new mutex object with a unique name
    mutex = new Mutex(false, mutexName, out newMutexCreated);
}
catch(Exception ex)
{
    MessageBox.Show (ex.Message+"\n\n"+ex.StackTrace+ 
         "\n\n"+"Application Exiting...","Exception thrown");
    Application.Exit ();
}

// When the mutex is created for the first time
// we run the program since it is the first instance.
if(newMutexCreated)
{
    Application.Run(new AnimatedWindowForm());
}

When a new mutex is created the mutex name can be prefixed with either Global\ or Local\. Prefixing with Global\ means the mutex is effective in the global namespace.

Prefixing with Local\ means the mutex is effective in the user's session namespace only.

Windows XP and Windows 2003 allow fast user switching through Terminal Services Sessions. So if a mutex is created with a Global\ prefix, the application can have only one instance system wide. So if one user launches the application, other users cannot create a second instance in their sessions. If the mutex is not prefixed with Local\ it is effective per session only.

To know more about Kernel Object namespaces, read this MSDN article.

Now, there is one more task left, i.e. to bring up the previously running instance to the foreground. In a Windows application this means restoring the main window of the application to the top and showing it to the user if it were hidden.

Restoring the Previous Instance

In order to restore the main window, the app’s main window handle is needed. Getting the process’ MainWindowHandle was easy by using the following piece of code:

C#
Process[] currentProcesses = 
    Process.GetProcessesByName("SingleInstanceApplication");
System.IntPtr mainWindowHandle = currentProcesses[0].MainWindowHandle;
if(mainWindowHandle != IntPtr.Zero)
{
    ShowWindow(mainWindowHandle,SW_RESTORE); // Restore the Window 
    UpdateWindow(mainWindowHandle);
}

But it failed to show the window because when the app’s main window is hidden, the handle returned is zero.

A reliable mechanism to get the MainWindowHandle is needed. This is where shared memory comes into play. Shared memory is a method of IPC (Inter Process Communication) in which two or more processes communicate using a shared segment of memory. Creating a shared memory in C# can be accomplished through Win32 API calls. Memory mapping associates the contents of a file to a specific range of addresses within your process' address space or the system page file or specified address in system memory.

To share data between two processes, the shared memory is created in a system page file.

In order for one process to share data with other processes via memory mapped files (MMF), each process must have access to the file. This is achieved by giving the MMF object a name that each of the processes accessing the shared memory can use.

C#
private const int INVALID_HANDLE_VALUE = -1;
private const int FILE_MAP_WRITE = 0x2;
private const int FILE_MAP_READ = 0x0004;

[DllImport("kernel32.dll",EntryPoint="OpenFileMapping", 
              SetLastError=true, CharSet=CharSet.Auto) ]
private static extern IntPtr OpenFileMapping (int 
    wDesiredAccess, bool bInheritHandle,String lpName );

[DllImport("Kernel32.dll",EntryPoint="CreateFileMapping", 
                 SetLastError=true,CharSet=CharSet.Auto)]

private static extern IntPtr CreateFileMapping(int hFile, 
        IntPtr lpAttributes, uint flProtect, 
        uint dwMaximumSizeHigh, uint dwMaximumSizeLow, 
        string lpName);
    
[DllImport("Kernel32.dll")]
private static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, 
        uint dwDesiredAccess, uint dwFileOffsetHigh, 
        uint dwFileOffsetLow, uint dwNumberOfBytesToMap);
    
[DllImport("Kernel32.dll",EntryPoint="UnmapViewOfFile", 
        SetLastError=true,CharSet=CharSet.Auto)]
private static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);

[DllImport("kernel32.dll",EntryPoint="CloseHandle", 
        SetLastError=true,CharSet=CharSet.Auto)]
private static extern bool CloseHandle(uint hHandle);
[DllImport("kernel32.dll",EntryPoint="GetLastError", 
        SetLastError=true,CharSet=CharSet.Auto)]

private static extern uint GetLastError();
private IntPtr memoryFileHandle;

public enum FileAccess : int
{
    ReadOnly = 2,
    ReadWrite = 4
}

Create a new MMF for the shared memory object, using the CreateFileMapping() function. When a new MMF object is created, a portion of the system page file gets reserved for it.

Parameters

  • hFile - Handle of the file that is to be memory mapped. When the MMF is created in the system page file, this value should be 0xFFFFFFFF (-1).
  • lpAttributes - A pointer to a SECURITY_ATTRIBUTES structure.
  • flProtect - The type of protection given to the memory mapped file.
    • PAGE_READONLY – Read only access.
    • PAGE_READWRITE – Read/write access.
    • PAGE_WRITECOPY - Copy-on-write access.
    • PAGE_EXECUTE_READ – Read and execute access.
    • PAGE_EXECUTE_READWRITE - Read, write and execute access.
  • dwMaximumSizeHigh - A high-order DWORD for the maximum size of a file mapping object.
  • dwMaximumSizeLow - A low-order DWORD for the maximum size of a file mapping object.
  • lpName – Name of the file mapping object.
C#
public static MemoryMappedFile CreateMMF(string 
                      fileName, FileAccess access, int size)
{
    if(size < 0)
        throw new ArgumentException("The size parameter" + 
                " should be a number greater than Zero.");

    IntPtr memoryFileHandle = CreateFileMapping (0xFFFFFFFF, 
            IntPtr.Zero,(uint)access,0,(uint)size,fileName);

    if(memoryFileHandle == IntPtr.Zero)
        throw new SharedMemoryException("Creating Shared Memory failed.");

    return new MemoryMappedFile(memoryFileHandle);
}

Before we launch the first instance of the application, create the MMF object.

C#
// When the mutex is created for the first time
// we run the program since it is the first instance.
if(newMutexCreated)
{
    //Create the Shared Memory to store the window handle.
    lock(typeof(AnimatedWindowForm))
    {
        sharedMemory = MemoryMappedFile.CreateMMF("Local\\" + 
            "sharedMemoryAnimatedWindow", 
            MemoryMappedFile.FileAccess .ReadWrite, 8);
    }
    Application.Run(new AnimatedWindowForm());
}

Once the handle to the memory-mapped file is obtained, it can be used to map views of the file to the address space of a calling process. Views can be mapped and unmapped at will as long as the MMF object is alive. The MapViewOfFile() and UnmapViewOfFile() are used to map and un-map views. We can perform read/write operations to the mapped views according to the access type specified in the MapViewOfFile() function call.

Parameters of MapViewOfFile():

  • hFileMappingObject - Handle of an MMF object. The CreateFileMapping and OpenFileMapping functions return this handle.
  • dwDesiredAccess - The type of access to the MMF object. This parameter can be one of the following values:
    • FILE_MAP_READ - Read-only access. The MMF object must have PAGE_READWRITE or PAGE_READONLY access.
    • FILE_MAP_WRITE - Read/write access. The MMF object must have PAGE_READWRITE access.
    • FILE_MAP_COPY - Copy-on-write access. The MMF object must have PAGE_WRITECOPY access.
    • FILE_MAP_EXECUTE - Execute access. The MMF object must have PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_READ access.
  • dwFileOffsetHigh - A high-order DWORD of the file offset where the mapped view begins.
  • dwFileOffsetLow - A low-order DWORD of the file offset where the mapped view begins.
  • dwNumberOfBytesToMap - The number of bytes of a file mapping to map to the view.

After a view of the memory mapped file has been created, the view can be unmapped at any time by calling the UnmapViewOfFile () function. The only parameter required is the handle of the mapped view.

C#
UnmapViewOfFile(mappedViewHandle);

In-order to write to the shared memory, first create a mapped view of the MMF object with FILE_MAP_WRITE access. Because we are writing the handle to the main window, use the Marshal.WriteIntPtr() method to write to the shared memory. Once the write operation is done, un-map the view, and last of all, release the mapped view by calling the function CloseHandle().

C#
public void WriteHandle(IntPtr windowHandle)
{
    IntPtr mappedViewHandle = MapViewOfFile(memoryFileHandle, 
                                 (uint)FILE_MAP_WRITE,0,0,8);
    if(mappedViewHandle == IntPtr.Zero)
        throw new SharedMemoryException("Creating" + 
                " a view of Shared Memory failed.");

    Marshal.WriteIntPtr(mappedViewHandle,windowHandle );

    UnmapViewOfFile(mappedViewHandle);
    CloseHandle((uint)mappedViewHandle);
}

To read from the shared memory, create a mapped view of the MMF object with FILE_MAP_READ access. Use the Marshal.ReadIntPtr() method to read from the shared memory. Once the read operation is done, un-map the view and release the mapped view by calling the function CloseHandle().

C#
public static IntPtr ReadHandle(string fileName)
{
    IntPtr mappedFileHandle = 
      OpenFileMapping((int)FileAccess.ReadWrite, false, fileName);

    if(mappedFileHandle == IntPtr.Zero)
        throw new SharedMemoryException("Opening the" + 
                    " Shared Memory for Read failed.");

    IntPtr mappedViewHandle = MapViewOfFile(mappedFileHandle, 
                                        (uint)FILE_MAP_READ,0,0,8);
    if(mappedViewHandle == IntPtr.Zero)
        throw new SharedMemoryException("Creating" + 
                  " a view of Shared Memory failed.");

    IntPtr windowHandle = Marshal.ReadIntPtr(mappedViewHandle);
    if(windowHandle == IntPtr.Zero)
        throw new ArgumentException ("Reading from the specified" + 
                             " address in  Shared Memory failed.");

    UnmapViewOfFile(mappedViewHandle);
    CloseHandle((uint)mappedFileHandle);
    return windowHandle;
}

Once the application’s main window handle is created, we write it to the shared memory.

C#
protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated (e);
    IntPtr mainWindowHandle = this.Handle;
    try
    {
        lock(this)
        {
            //Write the handle to the Shared Memory 
            sharedMemory.WriteHandle (mainWindowHandle);
        }
    }
    catch(Exception ex)
    {
        MessageBox.Show (ex.Message+ "\n\n"+ex.StackTrace+ 
             "\n\n"+ "Application Exiting...","Exception thrown");
        Application.Exit();
    }
}

When the user tries to launch a second instance of the app, the window handle of the previous instance is retrieved from the shared memory and the main window is restored using ShowWindow() and UpdateWindow().

C#
// If the mutex already exists, no need to launch
// a new instance of the program because a previous instance is running.
try
{
    // Get the Program's main window handle,
    // which was previously stored in shared memory.
    IntPtr mainWindowHandle = System.IntPtr.Zero;
    lock(typeof(AnimatedWindowForm))
    {
        mainWindowHandle = MemoryMappedFile.ReadHandle("Local" + 
                                "\\sharedMemoryAnimatedWindow");
    }

    if(mainWindowHandle != IntPtr.Zero)
    {
        // Restore the Window 
        ShowWindow(mainWindowHandle,SW_RESTORE);
        UpdateWindow(mainWindowHandle);
    }
}
catch(Exception ex)
{
    MessageBox.Show (ex.Message+ "\n\n"+ex.StackTrace+ 
           "\n\n"+"Application Exiting...","Exception thrown");
}

So our application’s Main method looks like this:

C#
static void Main() 
{
    // Used to check if we can create a new mutex
    bool newMutexCreated = false;
    // The name of the mutex is to be prefixed with Local\ to make
    // sure that its is created in the per-session
    // namespace, not in the global namespace.
    string mutexName = "Local\\" + 
      System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;

    Mutex mutex = null;
    try
    {
        // Create a new mutex object with a unique name
        mutex = new Mutex(false, mutexName, out newMutexCreated);
    }
    catch(Exception ex)
    {
        MessageBox.Show (ex.Message+"\n\n"+ex.StackTrace+
           "\n\n"+"Application Exiting...","Exception thrown");
        Application.Exit ();
    }

    // When the mutex is created for the first time
    // we run the program since it is the first instance.
    if(newMutexCreated)
    {
        // Create the Shared Memory to store the window
        // handle. This memory is shared between processes
        lock(typeof(AnimatedWindowForm))
        {
            sharedMemory = MemoryMappedFile.CreateMMF("Local" + 
              "\\sharedMemoryAnimatedWindow", 
              MemoryMappedFile.FileAccess .ReadWrite ,8);
        }
        Application.Run(new AnimatedWindowForm());
    }
    else
    // If the mutex already exists, no need to launch
    // a new instance of the program because
    // a previous instance is running .
    {
        try
        {
        // Get the Program's main window handle,
        // which was previously stored in shared memory.
            IntPtr mainWindowHandle = System.IntPtr.Zero;
            lock(typeof(AnimatedWindowForm))
            {
                mainWindowHandle = 
                  MemoryMappedFile.ReadHandle("Local" + 
                  "\\sharedMemoryAnimatedWindow");
            }
            if(mainWindowHandle != IntPtr.Zero)
            {
                // Restore the Window 
                ShowWindow(mainWindowHandle,SW_RESTORE);
                UpdateWindow(mainWindowHandle);
            }
            return;
        }
        catch(Exception ex)
        {
            MessageBox.Show (ex.Message+"\n\n"+ex.StackTrace+ 
              "\n\n"+"Application Exiting...","Exception thrown");    
        }
        // Tell the garbage collector to keep the Mutex alive
        // until the code execution reaches this point,
        // ie. normally when the program is exiting.
        GC.KeepAlive(mutex);
        // Release the Mutex 
        try
        {
            mutex.ReleaseMutex();
        }
        catch(ApplicationException ex)
        {
            MessageBox.Show (ex.Message + "\n\n"+ ex.StackTrace, 
                                            "Exception thrown");    
            GC.Collect();
        }
    }
}

Minimizing the Window to the Notification Area

This involves four tasks:

Our first step is to prevent the window from closing when the user clicks the Close button, override the protected virtual OnClosing method, and cancel the Close event. The window should be hidden, while the app runs in the background. But what happens when the user tries to shutdown the system? The Operating System sends Close message to all the open windows. Our application refuses to close the window, and the system will not shutdown, it will wait until all windows are closed. Therefore we have to override the WndProc virtual method to handle the WM_QUERYENDSESSION message.

C#
protected override void OnClosing(CancelEventArgs e)
{
    if(systemShutdown == true)
        e.Cancel = false;
    else
    {
        e.Cancel = true;
        this.AnimateWindow();
        this.Visible = false;
    }
}

protected override void WndProc(ref Message m)
{
    // Once the program recieves WM_QUERYENDSESSION
    // message, set the boolean systemShutdown to true.
    
    if(m.Msg == WM_QUERYENDSESSION)
        systemShutdown = true;
    base.WndProc(ref m);
}

Next, we want to display a notify-icon in the notification area of the taskbar. Add a NotifyIcon control to the main form and set its icon. This icon will be displayed in the notification area of the taskbar. Our next aim is to animate the window towards the notification area. Before doing the animation we want to make sure that the user has not disabled window animations in the system. A user can enable/disable window animation by setting the "MinAnimate" key under HKeyCurrentUser\Control Panel\Desktop. We check this value and set a boolean according to the user’s preference.

C#
RegistryKey animationKey = 
  Registry.CurrentUser.OpenSubKey("Control Panel" + 
  "\\Desktop\\WindowMetrics",true);
object animKeyValue = animationKey.GetValue("MinAnimate");
            
if(System.Convert.ToInt32 (animKeyValue.ToString()) == 0)
    this.AnimationDisabled = true;
else
this.AnimationDisabled = false;

If animation is allowed, we use the DrawAnimatedRects(IntPtr hwnd, int idAni, ref RECT lprcFrom, ref RECT lprcTo) function to animate the window. This function takes four parameters. hwnd is the handle to the window that is to be animated. idAni specifies the type of animation. If you specify IDANI_CAPTION, the window caption is animated from the position specified by lprcFrom to the position specified by lprcTo. Otherwise it draws a wire-frame rectangle and animates it. lprcFrom and lprcTo are types of RECT and stands for the begin and end rectangles of the animation respectively. We use the GetWindowRect(IntPtr hwnd, ref RECT lpRect) function to get the window's rectangle from its handle. While minimizing, the start position is the RECT of the window. And the end position is the RECT of the notification area. So our next task is to find out the handle of the notification area. The class name of the taskbar is Shell_TrayWnd. The taskbar contains several other child windows. We need the handle of the “notification area” which contains the notification icons. We get this handle by enumerating the child windows of Shell_TrayWnd. Now we can get the RECT of the notification area by using the GetWindowRect(IntPtr hwnd, ref RECT lpRect) function.

C#
private void AnimateWindow()
{
    // if the user has not disabled animating windows...
    if(!this.AnimationDisabled)
    {
        RECT animateFrom = new RECT();
        GetWindowRect(this.Handle, ref animateFrom);

        RECT animateTo = new RECT ();
        IntPtr notifyAreaHandle = GetNotificationAreaHandle();

        if (notifyAreaHandle != IntPtr.Zero)
        {
            if ( GetWindowRect(notifyAreaHandle, ref animateTo) == true)
            {
                DrawAnimatedRects(this.Handle, 
                     IDANI_CAPTION,ref animateFrom,ref animateTo);
            }
        }
    }
}

private IntPtr GetNotificationAreaHandle()
{
    IntPtr hwnd = FindWindowEx(IntPtr.Zero,IntPtr.Zero,"Shell_TrayWnd",null);
    hwnd = FindWindowEx(hwnd , IntPtr.Zero ,"TrayNotifyWnd",null);
    hwnd = FindWindowEx(hwnd , IntPtr.Zero ,"SysPager",null);
    
    if (hwnd != IntPtr.Zero)
        hwnd = FindWindowEx(hwnd , IntPtr.Zero ,null,"Notification Area");

    return hwnd;        
}

One Last Word

Indeed, there is a chance that this method of getting the window handle of the "Notification Area" might fail because the "TrayNotifyWnd", "SysPager" and "Notification Area" are undocumented window class names and can be changed in any upcoming Windows versions.

Known Issues

However, there is a conflict between the Debug version and the Release version of the app. If the Release version starts first and then the user launches the Debug version then both instances will be running. Mutex is not able to prevent the second instance from starting.

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralA simpler, cleaner solution has been available since .NET 2.0 Pin
Greg Cadmes10-Feb-10 6:45
Greg Cadmes10-Feb-10 6:45 
GeneralUsing Mutex and PostMessage Pin
minhvc30-Sep-08 16:27
minhvc30-Sep-08 16:27 
Generalit's real helpful. Pin
grass0020-May-08 19:36
grass0020-May-08 19:36 
it's real helpful to me.
GeneralExcellent Pin
ayurhdfkl2-Oct-07 23:44
ayurhdfkl2-Oct-07 23:44 
GeneralWM_QUERYENDSESSION and other constants. Pin
cognitive_psych22-Dec-06 14:22
cognitive_psych22-Dec-06 14:22 
GeneralRe: WM_QUERYENDSESSION and other constants. Pin
cognitive_psych22-Dec-06 15:08
cognitive_psych22-Dec-06 15:08 
GeneralRe: WM_QUERYENDSESSION and other constants. Pin
Mario Fascino3-Jan-07 5:51
Mario Fascino3-Jan-07 5:51 
Questionhow to set up global access for shared memory? Pin
cathyliu62025-Oct-06 12:08
cathyliu62025-Oct-06 12:08 
Generalbest single instance Pin
Darchangel7-Oct-06 8:05
Darchangel7-Oct-06 8:05 
GeneralRe: best single instance Pin
Digitalbeach23-Oct-06 11:43
Digitalbeach23-Oct-06 11:43 
GeneralRe: best single instance Pin
camadan29-May-08 3:12
camadan29-May-08 3:12 
Questioncan u help with smth Pin
yazid_hijazeen15-Jul-06 23:36
yazid_hijazeen15-Jul-06 23:36 
GeneralExcellent Pin
VhailorZ28-Mar-06 21:00
VhailorZ28-Mar-06 21:00 
GeneralLocks Pin
gdean232314-Sep-05 13:01
gdean232314-Sep-05 13:01 
GeneralRe: Locks Pin
GoofyBB14-Sep-05 18:59
GoofyBB14-Sep-05 18:59 
GeneralRe: Locks Pin
gdean232314-Sep-05 19:48
gdean232314-Sep-05 19:48 
GeneralBrintToFront Pin
GoofyBB11-Sep-05 3:59
GoofyBB11-Sep-05 3:59 
GeneralFound issue with .NET framework v2.0 Pin
GoofyBB11-Sep-05 3:53
GoofyBB11-Sep-05 3:53 
GeneralRe: Found issue with .NET framework v2.0 Pin
gdean232314-Sep-05 13:02
gdean232314-Sep-05 13:02 
GeneralAlternate methods Pin
catatonicboy26-Jul-05 23:36
catatonicboy26-Jul-05 23:36 
GeneralRe: Alternate methods Pin
leppie27-Aug-05 7:14
leppie27-Aug-05 7:14 
GeneralRe: Alternate methods Pin
Baseman999930-Aug-05 9:13
professionalBaseman999930-Aug-05 9:13 
GeneralRe: Alternate methods Pin
shofb8-Aug-08 15:54
shofb8-Aug-08 15:54 
GeneralRe: Alternate methods [modified] Pin
Anh_Tuan12-Jul-06 7:49
Anh_Tuan12-Jul-06 7:49 
GeneralRe: Alternate methods Pin
catatonicboy12-Jul-06 10:32
catatonicboy12-Jul-06 10:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.