一个单实例应用程序, 关闭时最小化到系统托盘
创建一个保持在系统托盘中的单实例应用程序。
引言
本文讨论了三个问题
- 创建一个单实例应用程序。
- 如果用户尝试启动另一个实例,恢复前一个实例。
- 当窗口关闭时,将应用程序最小化(带动画)到任务栏的通知区域。
如何创建单实例应用程序?
通常,需要确保程序在任何时候只运行一个实例。如果用户尝试运行另一个实例,要么通知用户已经有一个实例正在运行,要么激活并将前一个实例带到前台。对于 Windows 应用程序,我们希望恢复现有应用程序的主窗口。因此,当一个应用程序启动时,它会检查是否有另一个实例正在运行。如果存在,则当前实例退出,并激活前一个实例的主窗口并将其显示给用户。
通过使用互斥体(Mutual Exclusion Semaphore)可以实现应用程序的单实例。Windows 应用程序通过 Application.Run( )
方法加载主窗体。在 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)
{
Application.Run(new AnimatedWindowForm());
}
当创建一个新的互斥体时,互斥体名称可以以 Global\ 或 Local\ 为前缀。以 Global\ 为前缀意味着互斥体在全局命名空间中有效。
以 Local\ 为前缀意味着互斥体仅在用户会话命名空间中有效。
Windows XP 和 Windows 2003 允许通过终端服务会话进行快速用户切换。因此,如果互斥体以 Global\ 为前缀创建,则应用程序在全系统范围内只能有一个实例。所以如果一个用户启动了应用程序,其他用户无法在他们的会话中创建第二个实例。如果互斥体没有以 Local\ 为前缀,则它仅在每个会话中有效。
要了解更多关于内核对象命名空间的信息,请阅读这篇 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);
}
但是它未能显示窗口,因为当应用程序的主窗口被隐藏时,返回的句柄是零。
需要一种可靠的机制来获取 MainWindowHandle
。这就是共享内存发挥作用的地方。共享内存是一种 IPC(进程间通信)方法,其中两个或更多进程使用共享内存段进行通信。在 C# 中创建共享内存可以通过 Win32 API 调用实现。内存映射将文件的内容与进程地址空间或系统页文件或系统内存中指定地址的特定地址范围相关联。
为了在两个进程之间共享数据,在系统页文件中创建共享内存。
为了一个进程通过内存映射文件(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
}
使用 CreateFileMapping()
函数为共享内存对象创建一个新的 MMF。当创建一个新的 MMF 对象时,系统页文件的一部分将为其保留。
参数
hFile
- 要进行内存映射的文件的句柄。当在系统页文件中创建 MMF 时,该值应为 0xFFFFFFFF (-1)。lpAttributes
- 指向SECURITY_ATTRIBUTES
结构的指针。flProtect
- 赋予内存映射文件的保护类型。PAGE_READONLY
– 只读访问。PAGE_READWRITE
– 读/写访问。PAGE_WRITECOPY
- 写入时复制访问。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 对象。
// 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());
}
一旦获取到内存映射文件的句柄,就可以用它将文件的视图映射到调用进程的地址空间。只要 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
- 写入时复制访问。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()
恢复主窗口。
// 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");
}
所以我们的应用程序的 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
事件。窗口应该隐藏,而应用程序在后台运行。但是当用户尝试关闭系统时会发生什么?操作系统会向所有打开的窗口发送“关闭”消息。我们的应用程序拒绝关闭窗口,系统将不会关闭,它会一直等待直到所有窗口都关闭。因此,我们必须重写 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)
{
// Once the program recieves WM_QUERYENDSESSION
// message, set the boolean systemShutdown to 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
。任务栏包含几个其他子窗口。我们需要包含通知图标的“通知区域”的句柄。我们通过枚举 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
”是未公开的窗口类名,并且可能在任何即将发布的 Windows 版本中更改。
已知问题
但是,应用程序的调试版本和发布版本之间存在冲突。如果发布版本先启动,然后用户启动调试版本,那么两个实例都将运行。互斥体无法阻止第二个实例启动。