WPF实现只打开一个窗口,并且重复打开时已经打开的窗口置顶
内容来自:https://codereview.stackexchange.com/questions/20871/single-instance-wpf-application
第一步:添加System.RunTime.Remoting引用
第二步:新建一个类class1.cs(按自己想法命名)
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Ipc; using System.Runtime.Serialization.Formatters; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; namespace SingleWPF { internal enum WM { NULL = 0x0000, CREATE = 0x0001, DESTROY = 0x0002, MOVE = 0x0003, SIZE = 0x0005, ACTIVATE = 0x0006, SETFOCUS = 0x0007, KILLFOCUS = 0x0008, ENABLE = 0x000A, SETREDRAW = 0x000B, SETTEXT = 0x000C, GETTEXT = 0x000D, GETTEXTLENGTH = 0x000E, PAINT = 0x000F, CLOSE = 0x0010, QUERYENDSESSION = 0x0011, QUIT = 0x0012, QUERYOPEN = 0x0013, ERASEBKGND = 0x0014, SYSCOLORCHANGE = 0x0015, SHOWWINDOW = 0x0018, ACTIVATEAPP = 0x001C, SETCURSOR = 0x0020, MOUSEACTIVATE = 0x0021, CHILDACTIVATE = 0x0022, QUEUESYNC = 0x0023, GETMINMAXINFO = 0x0024, WINDOWPOSCHANGING = 0x0046, WINDOWPOSCHANGED = 0x0047, CONTEXTMENU = 0x007B, STYLECHANGING = 0x007C, STYLECHANGED = 0x007D, DISPLAYCHANGE = 0x007E, GETICON = 0x007F, SETICON = 0x0080, NCCREATE = 0x0081, NCDESTROY = 0x0082, NCCALCSIZE = 0x0083, NCHITTEST = 0x0084, NCPAINT = 0x0085, NCACTIVATE = 0x0086, GETDLGCODE = 0x0087, SYNCPAINT = 0x0088, NCMOUSEMOVE = 0x00A0, NCLBUTTONDOWN = 0x00A1, NCLBUTTONUP = 0x00A2, NCLBUTTONDBLCLK = 0x00A3, NCRBUTTONDOWN = 0x00A4, NCRBUTTONUP = 0x00A5, NCRBUTTONDBLCLK = 0x00A6, NCMBUTTONDOWN = 0x00A7, NCMBUTTONUP = 0x00A8, NCMBUTTONDBLCLK = 0x00A9, SYSKEYDOWN = 0x0104, SYSKEYUP = 0x0105, SYSCHAR = 0x0106, SYSDEADCHAR = 0x0107, COMMAND = 0x0111, SYSCOMMAND = 0x0112, MOUSEMOVE = 0x0200, LBUTTONDOWN = 0x0201, LBUTTONUP = 0x0202, LBUTTONDBLCLK = 0x0203, RBUTTONDOWN = 0x0204, RBUTTONUP = 0x0205, RBUTTONDBLCLK = 0x0206, MBUTTONDOWN = 0x0207, MBUTTONUP = 0x0208, MBUTTONDBLCLK = 0x0209, MOUSEWHEEL = 0x020A, XBUTTONDOWN = 0x020B, XBUTTONUP = 0x020C, XBUTTONDBLCLK = 0x020D, MOUSEHWHEEL = 0x020E, CAPTURECHANGED = 0x0215, ENTERSIZEMOVE = 0x0231, EXITSIZEMOVE = 0x0232, IME_SETCONTEXT = 0x0281, IME_NOTIFY = 0x0282, IME_CONTROL = 0x0283, IME_COMPOSITIONFULL = 0x0284, IME_SELECT = 0x0285, IME_CHAR = 0x0286, IME_REQUEST = 0x0288, IME_KEYDOWN = 0x0290, IME_KEYUP = 0x0291, NCMOUSELEAVE = 0x02A2, DWMCOMPOSITIONCHANGED = 0x031E, DWMNCRENDERINGCHANGED = 0x031F, DWMCOLORIZATIONCOLORCHANGED = 0x0320, DWMWINDOWMAXIMIZEDCHANGE = 0x0321, #region Windows 7 DWMSENDICONICTHUMBNAIL = 0x0323, DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326, #endregion USER = 0x0400, // This is the hard-coded message value used by WinForms for Shell_NotifyIcon. // It's relatively safe to reuse. TRAYMOUSEMESSAGE = 0x800, //WM_USER + 1024 APP = 0x8000, } [SuppressUnmanagedCodeSecurity] internal static class NativeMethods { /// <summary> /// Delegate declaration that matches WndProc signatures. /// </summary> public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled); [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)] private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs); [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)] private static extern IntPtr _LocalFree(IntPtr hMem); public static string[] CommandLineToArgvW(string cmdLine) { IntPtr argv = IntPtr.Zero; try { int numArgs = 0; argv = _CommandLineToArgvW(cmdLine, out numArgs); if (argv == IntPtr.Zero) { throw new Win32Exception(); } var result = new string[numArgs]; for (int i = 0; i < numArgs; i++) { IntPtr currArg = Marshal.ReadIntPtr(argv, i * Marshal.SizeOf(typeof(IntPtr))); result[i] = Marshal.PtrToStringUni(currArg); } return result; } finally { IntPtr p = _LocalFree(argv); // Otherwise LocalFree failed. // Assert.AreEqual(IntPtr.Zero, p); } } } public interface ISingleInstanceApp { bool SignalExternalCommandLineArgs(IList<string> args); } /// <summary> /// This class checks to make sure that only one instance of /// this application is running at a time. /// </summary> /// <remarks> /// Note: this class should be used with some caution, because it does no /// security checking. For example, if one instance of an app that uses this class /// is running as Administrator, any other instance, even if it is not /// running as Administrator, can activate it with command line arguments. /// For most apps, this will not be much of an issue. /// </remarks> public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp { #region Private Fields /// <summary> /// String delimiter used in channel names. /// </summary> private const string Delimiter = ":"; /// <summary> /// Suffix to the channel name. /// </summary> private const string ChannelNameSuffix = "SingeInstanceIPCChannel"; /// <summary> /// Remote service name. /// </summary> private const string RemoteServiceName = "SingleInstanceApplicationService"; /// <summary> /// IPC protocol used (string). /// </summary> private const string IpcProtocol = "ipc://"; /// <summary> /// Application mutex. /// </summary> private static Mutex singleInstanceMutex; /// <summary> /// IPC channel for communications. /// </summary> private static IpcServerChannel channel; /// <summary> /// List of command line arguments for the application. /// </summary> private static IList<string> commandLineArgs; #endregion #region Public Properties /// <summary> /// Gets list of command line arguments for the application. /// </summary> public static IList<string> CommandLineArgs { get { return commandLineArgs; } } #endregion #region Public Methods /// <summary> /// Checks if the instance of the application attempting to start is the first instance. /// If not, activates the first instance. /// </summary> /// <returns>True if this is the first instance of the application.</returns> public static bool InitializeAsFirstInstance(string uniqueName) { commandLineArgs = GetCommandLineArgs(uniqueName); // Build unique application Id and the IPC channel name. string applicationIdentifier = uniqueName + Environment.UserName; string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); // Create mutex based on unique application Id to check if this is the first instance of the application. bool firstInstance; singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); if (firstInstance) { CreateRemoteService(channelName); } else { SignalFirstInstance(channelName, commandLineArgs); } return firstInstance; } /// <summary> /// Cleans up single-instance code, clearing shared resources, mutexes, etc. /// </summary> public static void Cleanup() { if (singleInstanceMutex != null) { singleInstanceMutex.Close(); singleInstanceMutex = null; } if (channel != null) { ChannelServices.UnregisterChannel(channel); channel = null; } } #endregion #region Private Methods /// <summary> /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved. /// </summary> /// <returns>List of command line arg strings.</returns> private static IList<string> GetCommandLineArgs(string uniqueApplicationName) { string[] args = null; if (AppDomain.CurrentDomain.ActivationContext == null) { // The application was not clickonce deployed, get args from standard API's args = Environment.GetCommandLineArgs(); } else { // The application was clickonce deployed // Clickonce deployed apps cannot recieve traditional commandline arguments // As a workaround commandline arguments can be written to a shared location before // the app is launched and the app can obtain its commandline arguments from the // shared location string appFolderPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName); string cmdLinePath = Path.Combine(appFolderPath, "cmdline.txt"); if (File.Exists(cmdLinePath)) { try { using (TextReader reader = new StreamReader(cmdLinePath, System.Text.Encoding.Unicode)) { args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd()); } File.Delete(cmdLinePath); } catch (IOException) { } } } if (args == null) { args = new string[] { }; } return new List<string>(args); } /// <summary> /// Creates a remote service for communication. /// </summary> /// <param name="channelName">Application's IPC channel name.</param> private static void CreateRemoteService(string channelName) { BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider(); serverProvider.TypeFilterLevel = TypeFilterLevel.Full; IDictionary props = new Dictionary<string, string>(); props["name"] = channelName; props["portName"] = channelName; props["exclusiveAddressUse"] = "false"; // Create the IPC Server channel with the channel properties channel = new IpcServerChannel(props, serverProvider); // Register the channel with the channel services ChannelServices.RegisterChannel(channel, true); // Expose the remote service with the REMOTE_SERVICE_NAME IPCRemoteService remoteService = new IPCRemoteService(); RemotingServices.Marshal(remoteService, RemoteServiceName); } /// <summary> /// Creates a client channel and obtains a reference to the remoting service exposed by the server - /// in this case, the remoting service exposed by the first instance. Calls a function of the remoting service /// class to pass on command line arguments from the second instance to the first and cause it to activate itself. /// </summary> /// <param name="channelName">Application's IPC channel name.</param> /// <param name="args"> /// Command line arguments for the second instance, passed to the first instance to take appropriate action. /// </param> private static void SignalFirstInstance(string channelName, IList<string> args) { IpcClientChannel secondInstanceChannel = new IpcClientChannel(); ChannelServices.RegisterChannel(secondInstanceChannel, true); string remotingServiceUrl = IpcProtocol + channelName + "/" + RemoteServiceName; // Obtain a reference to the remoting service exposed by the server i.e the first instance of the application IPCRemoteService firstInstanceRemoteServiceReference = (IPCRemoteService)RemotingServices.Connect(typeof(IPCRemoteService), remotingServiceUrl); // Check that the remote service exists, in some cases the first instance may not yet have created one, in which case // the second instance should just exit if (firstInstanceRemoteServiceReference != null) { // Invoke a method of the remote service exposed by the first instance passing on the command line // arguments and causing the first instance to activate itself firstInstanceRemoteServiceReference.InvokeFirstInstance(args); } } /// <summary> /// Callback for activating first instance of the application. /// </summary> /// <param name="arg">Callback argument.</param> /// <returns>Always null.</returns> private static object ActivateFirstInstanceCallback(object arg) { // Get command line args to be passed to first instance IList<string> args = arg as IList<string>; ActivateFirstInstance(args); return null; } /// <summary> /// Activates the first instance of the application with arguments from a second instance. /// </summary> /// <param name="args">List of arguments to supply the first instance of the application.</param> private static void ActivateFirstInstance(IList<string> args) { // Set main window state and process command line args if (Application.Current == null) { return; } ((TApplication)Application.Current).SignalExternalCommandLineArgs(args); } #endregion #region Private Classes /// <summary> /// Remoting service class which is exposed by the server i.e the first instance and called by the second instance /// to pass on the command line arguments to the first instance and cause it to activate itself. /// </summary> private class IPCRemoteService : MarshalByRefObject { /// <summary> /// Activates the first instance of the application. /// </summary> /// <param name="args">List of arguments to pass to the first instance.</param> public void InvokeFirstInstance(IList<string> args) { if (Application.Current != null) { // Do an asynchronous call to ActivateFirstInstance function Application.Current.Dispatcher.BeginInvoke( DispatcherPriority.Normal, new DispatcherOperationCallback(SingleInstance<TApplication>.ActivateFirstInstanceCallback), args); } } /// <summary> /// Remoting Object's ease expires after every 5 minutes by default. We need to override the InitializeLifetimeService class /// to ensure that lease never expires. /// </summary> /// <returns>Always null.</returns> public override object InitializeLifetimeService() { return null; } } #endregion } }
第三步:在App.xml.cs中操作:
1:实现接口:ISingleInstanceApp
2:添加以下代码
// TODO: Make this unique! private const string Unique = "Change this to something that uniquely identifies your program."; [STAThread] public static void Main() { if (SingleInstance<App>.InitializeAsFirstInstance(Unique)) { var application = new App(); application.InitializeComponent(); application.Run(); // Allow single instance code to perform cleanup operations SingleInstance<App>.Cleanup(); } } #region ISingleInstanceApp Members public bool SignalExternalCommandLineArgs(IList<string> args) { // Bring window to foreground if (this.MainWindow.WindowState == WindowState.Minimized) { this.MainWindow.WindowState = WindowState.Normal; } this.MainWindow.Activate(); return true; } #endregion
注:如果只是打开一个窗口没有其他的操作,实现
SignalExternalCommandLineArgs方法为:
public bool SignalExternalCommandLineArgs(IList<string> args) { return true; }
如果是打开一个窗口,重复打开置顶窗口,实现方法为:
public bool SignalExternalCommandLineArgs(IList<string> args) { // Bring window to foreground if (this.MainWindow.WindowState == WindowState.Minimized) { this.MainWindow.WindowState = WindowState.Normal; } this.MainWindow.Activate(); return true; }
第四步:将App.xaml的生成方式修改为Page
第五步:将项目的属性->应用程序->启动对象,选择为自己项目名称.App