向Windows进程发送消息(而不是其主窗口)

16

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

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

在启动时,上一个实例的 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;
                    }
                }
            }
        }
    }
}
3个回答

12

我是这样做的:

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);
        }
    }
}

我希望我已经正确地记录了这个。我不得不省略很多其他的东西,但我认为我已经得到了必要的部分。对我而言,这个方案一直都很有效。如果你遇到问题,请让我知道,我会看看我错过了什么。


@Matt 做得好,Matt!FYI:在VS 2010 beta 2中编译Framework 4.0,构建正常。我真正喜欢的是,如果您重新激活唯一实例,可以调用'MessageBox.Show("...");'来让最终用户知道正在发生什么。我的一个问题是关于将激活放入try-catch块中以便您可以释放Mutex:如果主窗体实例继续创建其他窗体或其他内容,是否会对应用程序行为产生任何影响? - BillW
1
@BillW:我唯一能想到的负面影响是,如果在 MainForm 退出后仍有前台线程在运行,则应用程序会消失但仍保持活动状态,直到前台线程完成。如果用户尝试“重新启动”应用程序,则可能会出现问题,因为同一前台线程可能会第二次创建。在我的情况下,这不是问题,因为我只使用后台线程,因此当 MainForm 退出时,应用程序立即关闭。 - Matt Davis
1
@BillW:使用using语句替换try/catch块在技术上没有任何问题。即使抛出异常,using语句也会确保Mutex被正确处理。但这正是我使用try/catch块的原因。如果发生未处理的异常,我的catch语句将错误记录到事件查看器中,以便我至少了解发生了什么。或者,您可以在MessageBox中显示异常详细信息。using语句不允许您执行此操作。 - Matt Davis
谢谢Matt Davis,我刚才意识到我忘了说了 :) 那时我非常渴望实现/测试你的解决方案。 - chitza
@chitza:不用谢。祝你感恩节快乐。我也要准备出门了…… - Matt Davis
显示剩余5条评论

5

对于其他想要实现这一点的人,我在下面发布了我的实现,使用了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,我在VS2010b2上测试了你的解决方案,使用FrameWork 3.5和4.0:我找不到任何触发WndProc的情况。如果我启动一个应用程序实例,然后最小化,启动另一个应用程序实例会使当前实例最小化。如果窗口没有最小化,它会按照您的期望工作,将唯一的实例带到前面。我发现,如果我将对WndProc的调用移动到对'SetForeGroundWindow'的调用之后(基本上消除了else情况),那么在MainForm中拦截代码中的WndProc拦截代码中,可以测试WindowState = Minimized并执行正确的操作。 - BillW
1
我没有考虑到窗体被最小化的情况。代码只处理隐藏的窗口和非最小化的后台窗口。 然而,代码应该被重写,只在Program.cs中发布消息,所有窗口激活/恢复的代码应该在WndProc(MainForm.cs)中。我会在周末重新编写并重新发布它。 - chitza
你可以将(IntPtr)HWND_BROADCAST更改为handle,这样只会向目标应用程序发送消息,而不是全部。 - Ray Chakrit

1

可以使用命名管道来实现。在 .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

1
为了上下文的完整性,此示例使用WCF通过命名管道交换数据。 - Matt Davis
我调查了其他进程间通信的方式,确实,这是其中一种选择,但我认为使用简单的Windows消息更轻便、更简单。 - chitza
@chitza 是的,使用广播PostMessage,不是那么轻巧,对吧? - peenut

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接