【问题标题】:Send message to a Windows process (not its main window)向 Windows 进程(不是其主窗口)发送消息
【发布时间】:2010-12-19 03:48:35
【问题描述】:

我有一个应用程序,它在随后启动时检测是否有同名的进程已经在运行,如果是,则激活正在运行的应用程序的窗口,然后退出。

问题是主窗口可能被隐藏(只有一个通知区域图标可见),因此我没有窗口句柄。

在启动时,前一个实例的MainWindowHandle 属性为0,所以我无法发送ShowWindowPostMessage

有什么方法可以发送一条可以被正在运行的应用程序拦截的消息,从而允许它显示其主窗口?

应用程序是用 C# 编写的,下面是我用来实现此目的的代码。

[STAThread]
static void Main()
{
    bool createdNew = true;
    using (Mutex mutex = new Mutex(true, "MyMutexName", out createdNew))
    {
        if (createdNew)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
        else
        {
            Process current = Process.GetCurrentProcess();
            foreach (Process process in Process.GetProcessesByName(current.ProcessName))
            {
                if (process.Id != current.Id)
                {
                    Interop.WINDOWINFO pwi = new Interop.WINDOWINFO();
                    IntPtr handle = process.MainWindowHandle;
                    var isVisible = Interop.GetWindowInfo(handle, ref pwi);
                    if (!isVisible)
                    {
                        MessageBox.Show(Constants.APP_NAME + " is already running, check the notification area (near the clock).", 
                                        Constants.APP_NAME, MessageBoxButtons.OK, MessageBoxIcon.Information);//temporary message, until I find the solution
                        //Interop.ShowWindow(handle, Interop.WindowShowStyle.ShowNormal);
                        //Interop.PostMessage(handle, Interop.WM_CUSTOM_ACTIVATEAPP, IntPtr.Zero, IntPtr.Zero);
                    }
                    else
                        Interop.SetForegroundWindow(handle);//this works when the window is visible
                        break;
                    }
                }
            }
        }
    }
}

【问题讨论】:

    标签: c# .net windows interop


    【解决方案1】:

    我是这样做的:

    using System;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Windows.Forms;
    public partial class MainForm : Form
    {
        #region Dll Imports
        private const int HWND_BROADCAST = 0xFFFF;
    
        private static readonly int WM_MY_MSG = RegisterWindowMessage( "WM_MY_MSG" );
    
        [DllImport( "user32" )]
        private static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
    
        [DllImport( "user32" )]
        private static extern int RegisterWindowMessage(string message);
        #endregion Dll Imports
        static Mutex _single = new Mutex(true, "{4EABFF23-A35E-F0AB-3189-C81203BCAFF1}");
        [STAThread]
        static void Main()
        {
            // See if an instance is already running...
            if (_single.WaitOne(TimeSpan.Zero, true)) {
                // No...start up normally.
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                try {
                    Application.Run(new MainForm());
                } catch (Exception ex) {
                    // handle exception accordingly
                } finally {
                    _single.ReleaseMutex();
                }
            } else {
                // Yes...Bring existing instance to top and activate it.
                PostMessage(
                    (IntPtr) HWND_BROADCAST,
                    WM_MY_MSG,
                    new IntPtr(0xCDCD),
                    new IntPtr(0xEFEF));
            }
        }
    
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_MY_MSG) {
                if ((m.WParam.ToInt32() == 0xCDCD) && (m.LParam.ToInt32() == 0xEFEF)) {
                    if (WindowState == FormWindowState.Minimized) {
                        WindowState = FormWindowState.Normal;
                    }
                    // Bring window to front.
                    bool temp = TopMost;
                    TopMost = true;
                    TopMost = temp;
                    // Set focus to the window.
                    Activate();
                }
            } else {
                base.WndProc(ref m);
            }
        }
    }
    

    我希望我的转录正确。我不得不省略很多其他的东西,但我想我得到了必要的东西。我所拥有的对我有用。如果您有问题,请告诉我,我会看看我错过了什么。

    【讨论】:

    • 您将需要对“System.Runtime.InteropServices”的引用
    • @Matt 干得好,马特!仅供参考:构建,在针对 FrameWork 4.0 编译的 VS 2010 beta 2 中运行良好。我真正喜欢的是你可以调用 'MessageBox.Show("...");如果您要重新激活唯一实例以让最终用户知道发生了什么。我的一个问题是关于将激活放在 try-catch 块中以便您可以释放 Mutex 的问题:如果主表单实例继续创建其他表单或其他任何内容,这是否对应用程序行为有任何影响?
    • @Matt 我给你的答案投了赞成票,然后又不小心打到了赞成票,现在所以不会让我重新投票,发一条消息说我的投票是“太旧的”要更改,除非答案已更新!我将就此写信给 SO:如果我的赞成票一开始就“太老了”,为什么它显然会推翻赞成票?
    • @BillW:我能想到的唯一不利影响是,如果在 MainForm 退出后前台线程仍在运行。在这种情况下,应用程序将从视图中消失,但在前台线程完成之前一直保持活动状态。如果用户尝试“重新启动”应用程序,这可能是一个糟糕的 mojo,因为可能会再次创建相同的前台线程。就我而言,这不是问题,因为我只使用后台线程,所以当 MainForm 退出时,应用程序会立即死亡。
    • @BillW:用 using 语句替换 try/catch 块在技术上没有任何问题。 using 语句将确保 Mutex 被正确处理即使抛出异常。但这正是我使用 try/catch 块的原因。如果发生未处理的异常,我的 catch 语句会将错误记录到事件查看器中,这样我至少对发生的事情有一些了解。或者,您可以在 MessageBox 中显示异常详细信息。 using 语句无法让您执行此操作。
    【解决方案2】:

    命名管道可以用于此。这可能是 .net 更容易接受的方法。您可以在主应用程序中定义一个服务,该服务接受来自调用应用程序的消息。这是服务的示例,在 vb 中。它调用主应用程序并向其传递一个字符串,在本例中为一个文件名。它也返回一个字符串,但这里可以使用任何参数。

    Public Class PicLoadService : Implements IMainAppPicLoad
    
    Public Function LoadPic(ByVal fName As String) As String Implements IMainAppPicLoad.LoadPic
    ' do some stuff here.
    LoadPic = "return string"
    End Function
    
    End Class
    

    调用应用程序中的设置更复杂一些。调用和主应用程序可以是同一个应用程序。

    Imports System.Diagnostics
    Imports System.ServiceModel
    Imports System.IO
    Imports vb = Microsoft.VisualBasic
    
    Module MainAppLoader
    
    Sub Main()
    
    Dim epAddress As EndpointAddress
    Dim Client As picClient
    Dim s As String
    Dim loadFile As String
    Dim procs() As Process
    Dim processName As String = "MainApp"
    
    loadFile = "" ' filename to load
    
    procs = Process.GetProcessesByName(processName)
    
    If UBound(procs) >= 0 Then
      epAddress = New EndpointAddress("net.pipe://localhost/MainAppPicLoad")
      Client = New picClient(New NetNamedPipeBinding, epAddress)
      s = Client.LoadPic(loadFile)
    End If
    
    End Sub
    
    <System.Diagnostics.DebuggerStepThroughAttribute(), _
     System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")> _
    Partial Public Class picClient
        Inherits System.ServiceModel.ClientBase(Of IMainAppPicLoad)
        Implements IMainAppPicLoad
    
        Public Sub New(ByVal binding As System.ServiceModel.Channels.Binding, ByVal remoteAddress As System.ServiceModel.EndpointAddress)
            MyBase.New(binding, remoteAddress)
        End Sub
    
        Public Function LoadPic(ByVal fName As String) As String Implements IMainAppPicLoad.LoadPic
            Return MyBase.Channel.LoadPic(fName)
        End Function
    
    End Class
    
    ' from here down was auto generated by svcutil.
    ' svcutil.exe /language:vb /out:generatedProxy.vb /config:app.config http://localhost:8000/MainAppPicLoad
    ' Some has been simplified after auto code generation.
    <System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0"), _
     System.ServiceModel.ServiceContractAttribute(ConfigurationName:="IMainAppPicLoad")> _
    Public Interface IMainAppPicLoad
      <System.ServiceModel.OperationContractAttribute(Action:="http://tempuri.org/IMainAppPicLoad/LoadPic", ReplyAction:="http://tempuri.org/IMainAppPicLoad/LoadPicResponse")> _
      Function LoadPic(ByVal fName As String) As String
    End Interface
    
    <System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")> _
    Public Interface IMainAppPicLoadChannel
      Inherits IMainAppPicLoad, System.ServiceModel.IClientChannel
    End Interface
    
    <System.Diagnostics.DebuggerStepThroughAttribute(), _
    System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")> _
    Partial Public Class IMainAppPicLoadClient
      Inherits System.ServiceModel.ClientBase(Of IMainAppPicLoad)
      Implements IMainAppPicLoad
    
      Public Sub New(ByVal binding As System.ServiceModel.Channels.Binding, ByVal remoteAddress As System.ServiceModel.EndpointAddress)
        MyBase.New(binding, remoteAddress)
      End Sub
    
      Public Function LoadPic(ByVal fName As String) As String Implements IMainAppPicLoad.LoadPic
        Return MyBase.Channel.LoadPic(fName)
      End Function
    End Class
    
    End Module
    
    <ServiceContract()> Public Interface IMainAppPicLoad
    <OperationContract()> Function LoadPic(ByVal fName As String) As String
    End Interface
    

    【讨论】:

    • 为了上下文,本示例使用 WCF 通过命名管道交换数据
    • 我研究了其他进程间通信方式,确实,这是其中一种选择,但我相信使用简单的 Widows 消息会更轻松、更简单。
    • @chitza 是的,广播 PostMessage,不是很轻松,是吗?
    【解决方案3】:

    对于其他想要实现这一点的人,我将使用 Matt Davis 的解决方案发布在我的实现下方。

    在 Program.cs 中

    static class Program
    {
        #region Dll Imports
        public const int HWND_BROADCAST = 0xFFFF;
    
        [DllImport("user32.dll")]
        public static extern bool SetForegroundWindow(IntPtr hWnd);
    
        [DllImport("user32")]
        public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
    
        [DllImport("user32")]
        public static extern int RegisterWindowMessage(string message);
        #endregion Dll Imports
    
        public static readonly int WM_ACTIVATEAPP = RegisterWindowMessage("WM_ACTIVATEAPP");
    
        [STAThread]
        static void Main()
        {
            bool createdNew = true;
            //by creating a mutex, the next application instance will detect it
            //and the code will flow through the "else" branch 
            using (Mutex mutex = new Mutex(true, "MyMutexName", out createdNew))//make sure it's an unique identifier (a GUID would be better)
            {
                if (createdNew)
                {
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    Application.Run(new MainForm());
                }
                else
                {
                    //we tried to create a mutex, but there's already one (createdNew = false - another app created it before)
                    //so there's another instance of this application running
                    Process currentProcess = Process.GetCurrentProcess();
    
                    //get the process that has the same name as the current one but a different ID
                    foreach (Process process in Process.GetProcessesByName(currentProcess.ProcessName))
                    {
                        if (process.Id != currentProcess.Id)
                        {
                            IntPtr handle = process.MainWindowHandle;
    
                            //if the handle is non-zero then the main window is visible (but maybe somewhere in the background, that's the reason the user started a new instance)
                            //so just bring the window to front
                            if (handle != IntPtr.Zero)
                                SetForegroundWindow(handle);
                            else
                                //tough luck, can't activate the window, it's not visible and we can't get its handle
                                //so instead notify the process that it has to show it's window
                                PostMessage((IntPtr)HWND_BROADCAST, WM_ACTIVATEAPP, IntPtr.Zero, IntPtr.Zero);//this message will be sent to MainForm
    
                            break;
                        }
                    }
                }
            }
        }
    }
    

    在 MainForm.cs 中

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
                //someone (another process) said that we should show the window (WM_ACTIVATEAPP)
        if (m.Msg == Program.WM_ACTIVATEAPP)
            this.Show();
    }
    

    【讨论】:

    • @chitza,在 FrameWork 3.5、4.0 上的 VS2010b2 中测试了您的解决方案:我找不到任何触发 WndProc 的案例。如果我启动一个应用程序实例,然后最小化,启动另一个应用程序实例会使当前实例最小化。如果窗口没有被最小化,它会像你期望的那样工作,将唯一的实例放在前面。我确实发现,如果我将调用 WndProc 移动到调用 'SetForeGroundWindow 之后(基本上消除了 else 情况),这允许我在 MainForm 中的 WndProc 拦截代码中测试 WindowState = Minimized 并做正确的事事情。
    • 我没有考虑最小化表单。该代码仅处理隐藏窗口和非最小化背景窗口。但是,代码应该重写并且只在 Program.cs 中发布消息,所有窗口激活/恢复代码都应该在 WndProc (MainForm.cs) 中。我会在周末重写并重新发布。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    • 2013-04-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多