原文:
翻译:Anders Liu
出处:
简介
本文解决下列问题:
1 创建单实例应用程序。
2 当用户试图启动新的实例时,恢复前一个实例。
3 当窗口关闭时间起最小化到任务栏的通知区域中(带动画)。
如何创建单实例应用程序?
通常,需要确保在任何时候都只有程序的一个实例在运行。如果用户试图运行另外一个实例,或者通知用户已经有一个实例了,或者激活之前运行的实例并将其带到前台。对于Windows应用程序,我们可能希望恢复现有的主窗口。因此当应用程序启动的时候,应该查看是否已经有正在运行的实例了。如果有,应该退出当前实例并激活前一个实例的主窗口并显示给用户。
将应用程序做成单实例的,可以通过mutex(Mutual Exclusion Semaphore)[互斥体(互斥信号量)]来实现。Windows应用程序通过Application.Run()方法来加载住窗体。在Main方法中,创建一个新的mutex。如果可以创建新的mutex,则允许应用程序运行。如果mutex已经被创建了,应用程序就不会启动。这样就能确保任何使用只有一个实例在运行。
// 用于检测是否创建了新的mutex bool newMutexCreated = false ; // mutex的名字以Local\作为前缀, // 确保将其创建在每会话(per-session)命名空间中, // 而不是全局命名空间中。 string mutexName = " Local\\ " + System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;Mutex mutex = null ; try { // 使用唯一的名字创建一个新的mutex 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 ();} // 如果是第一次创建mutex,则启动应用程序实例, // 因为这是第一次运行 if (newMutexCreated) { Application.Run(new AnimatedWindowForm());}
当创建mutex时,其名字的前缀可以是Globa\或Local\。Global\前缀意味着该mutex将影响全局命名空间。
以Local\为前缀意味着该mutex只会影响到用户会话命名空间。
Windows XP和Windows 2003允许通过Terminal Services Sessions(终端服务会话)快速切换用户。因此如果mutext使用Global\前缀,在整个系统范围内只能有一个实例运行。如果一个用户启动了该应用程序,其他用户就无法在他们的会话中再次创建一个实例了。如果mutext的前缀不是Local\,它也只会影响每个会话。
要了解Kernel Object命名空间,请阅读这篇MSDN文章()。
现在,还有一个任务要完成——将前一个实例移到前台。在Windows应用程序中这意味着将应用程序主窗口恢复到顶端,如果已经隐藏,则显示给用户。
恢复前一个实例
要恢复主窗口,必须要得到应用程序主窗口的句柄。通过下面这段代码可以得到进程的MainWindowHandle:
Process[] currentProcesses = Process.GetProcessesByName( " SingleInstanceApplication " );System.IntPtr mainWindowHandle = currentProcesses[ 0 ].MainWindowHandle; if (mainWindowHandle != IntPtr.Zero) { ShowWindow(mainWindowHandle,SW_RESTORE); // Restore the Window UpdateWindow(mainWindowHandle);}
但当应用程序的主窗口被隐藏时,这段代码会失败,因为句柄返回的是0。
一个可靠的机制是使MainWindowHandle变成必需的。这就轮到共享内存上场了。共享内存是IPC(Inter Process Communication,进程间通信)的一种方法,使用这种方法,两个或更多个进程可以使用共享的内存片段进行通信。在C#中创建共享内存可以使用Win32 API调用。内存映射可以将文件内容关联到你的进程地址空间或系统页文件或系统内存的特定地址中的一个特定的地址区域。
要在两个进程之间共享数据,需要在系统页文件中创建共享内存。
为了使一个进程能够将通过内存映射文件(Memory Mapped File,MMF)将数据共享给其他进程,每个进程都必须访问该文件。这通过为MMF对象起一个名字来实现,每个进程都能够使用这个名字来访问共享内存。
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}
为共享内存对象创建新的MMF,可以使用CreateFileMapping()函数。创建了新的MMF对象后,系统页文件就会为其保留一部分。
参数
- hFile——要进行内存映射的文件句柄。当在系统页文件中创建MMF时,这个值必须是0xFFFFFFFF(-1)。
- lpAttributes——指向一个SECURITY_ATTRIBUTES结构体的指针
- flProtect——为内存映射文件指定的保护类型。
- PAGE_READONLY——只读访问。
- PAGE_READWRITE——读/写访问。
- PAGE_WRITECOPY——Copy-on-write访问。
- PAGE_EXECUTE_READ——读取和执行访问。
- PAGE_EXECUTE_READWRITE——读取、写入和执行访问。
- dwMaximumSizeHigh——文件映射对象的最大大小的DWORD值的高位。
- dwMaximumSizeLow——文件映射对象的最大大小的DWORD值的低位。
- lpName——文件映射对象的名字。
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);}
下面我们启动应用程序的第一个实例,创建MMF对象。
// 当第一次创建mutex时,运行程序,因为这是第一个实例。 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());}
一旦得到了内存映射文件的句柄,就可以用它来将文件视图映射到调用进程的地址空间。只要MMF对象存活着,就能对视图进行映射和取消映射。MapViewOfFile()和UnmapViewOfFile()函数用于映射和取消映射视图。我们是否可以执行读/写操作,取决于在调用MapViewOfFile()函数时指定的访问类型。
MapViewOfFile()的参数:
- hFileMappingObject——MMF对象的句柄。CreateFileMapping和OpenFileMapping函数可以返回这个句柄。
- dwDesiredAccess——MMF对象的访问类型。这个参数可以取下列值:
- FILE_MAP_READ——只读访问。MMF对象必须具备PAGE_READWRITE或PAGE_READONLY访问。
- FILE_MAP_WRITE——读/写访问。MMF对象必须具备PAGE_READWRITE访问。
- FILE_MAP_COPY——Copy-on-write访问。MMF对象必须具备PAGE_WRITECOPY访问。
- FILE_MAP_EXECUTE——执行访问。MMF对象必须具备PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ访问。
- dwFileOffsetHigh——映射视图在文件中的起始偏移量的DWORD高位。
- dwFileOffsetLow——映射视图在文件中的起始偏移量的DWORD低位。
- dwNumberOfBytesToMap——文件映射映射到视图的字节数。
在创建了内存映射文件的视图后,可以在任何时候通过调用UnmapViewOfFile()函数来取消映射。其惟一必须的参数就是映射视图的句柄。
UnmapViewOfFile(mappedViewHandle);
为了能够写入共享内存,首先需要使用FILE_MAP_WRITE创建MMF对象的映射视图。由于我们要在主窗口中写入该句柄,可以使用Marshal.WriteIntPtr()方法写入共享内存。写入操作完成后,需要取消映射视图,最后通过调用CloseHandle()函数释放映射视图。
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);}
要读取共享内存,需要使用FILE_MAP_READ访问创建MMF对象的映射视图。使用Marshal.ReadIntPtr()方法来读取共享内存。完成读取操作后,取消映射视图并调用CloseHandle()函数释放映射视图。
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;}
当应用程序主窗口句柄创建之后,我们就将其写入共享内存。
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(); }}
当用户尝试启动应用程序的第二个实例时,从共享内存中可以得到前一个实例的窗口句柄,并使用ShowWindow()和UpdateWindow()函数恢复主窗口。
// 如果mutex已经存在,不需要启动应用程序的新实例, // 因为前一个实例已经在运行了。 try { // 获取程序主窗口的句柄, // 该句柄由前一个实例存储到共享内存中。 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");}
因此我们的应用程序的main方法看起来是下面这样的:
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(); } }}
将窗口最小化到通知区域
这包含四个任务:
第一步是防止用户单击关闭按钮时关闭窗口,重写protected virtual OnClosing方法,取消Close事件。窗体应该被隐藏,而应用程序在后台运行。但当用户关闭系统时呢?操作系统会像所有打开着的窗口发送Close消息。如果我们的应用程序拒绝关闭窗口,系统将无法关闭,它会持续等待,直到所有窗口都关闭。因此我们需要重写WndProc需方法,处理WM_QUERYENDSESSION消息。
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) { // 一旦程序收到WM_QUERYENDSESSION消息, // 将systemShutdown布尔值设置为true。 if(m.Msg == WM_QUERYENDSESSION) systemShutdown = true; base.WndProc(ref m);}
接下来,我们希望在任务栏的通知区域显示一个通知图标。向主窗体添加一个NotifyIcon控件并为其设置图标。该图标将会显示在任务栏的通知区域中。我们的下一个目的是实现窗口向通知区域靠拢的动画。在做这个动画之前,我们需要确保用户没有禁用系统中的窗口动画。用户可以通过设置HKeyCurrentUser\Control Panel\Desktop下的MinAnimate键来启用/禁用窗口动画。我们检查这个值,并根据用户的偏好来设置一个布尔值。
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 ;
如果可以使用动画,我们使用DrawAnimatedRects(IntPtr hwnd, int idAni, ref RECT lprcFrom, ref RECT lprcTo)函数来绘制窗口动画。该函数有四个参数。hwnd是要进行动画的窗口句柄。idAni是动画的类型。如果指定为IDANI_CAPTION,则窗口标题会以动画方式从lprcFrom指定的位置移动到lprcTo指定的位置。否则它会绘制一个外框矩形并对其进行动画。lprcFrom和lprcTo都是RECT类型的,指定了动画的起止矩形。我们使用GetWindowRect(IntPtr hwnd, ref RECT lpRect)函数从窗体的句柄获取其矩形。最小化时,起始位置是窗口的RECT。而终止位置是通知区域的RECT。所以下一个任务是获取通知区域的句柄。任务栏的类名字是Shell_TrayWnd。任务栏包含很多其他子窗口。我们需要“notification area”的句柄,其中包含了通知图标。我们可以通过枚举Shell_TrayWnd的子窗口来获取其句柄。现在我们就可以使用GetWindowRect(IntPtr hwnd, ref RECT lpRect)函数来获取通知区域的RECT了。
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; }
结论
诚然,获取通知区域的窗口句柄有的时候会失败,因为“TrayNotifyWnd”、“SysPager”和“Notification Area”都是undocumented(非编档)的窗口类名,可能在未来的Windows版本中有所变化。
已知问题
在应用程序的Debug版本和Release版本之间存在着一个冲突。如果首先启动了Release版本,然后用户再启动Debug版本,则两个实例都会运行。Mutex不能在开始时组织第二个实例的起动。