如何通过命令行参数在 运行 WinForm 中更改 NotifyIcon.Text?

How to change NotifyIcon.Text in a running WinForm via command-line-arguments?

这是一个只有托盘图标的 Windows 表单应用程序。我正在尝试使用参数来控制某些内容并更改表单上的文本以显示状态信息。

但是我发现当我在运行期间使用参数调用它时,我想改变的是nullNotifyIcon()MenuItem()),似乎当我使用参数时,它 运行 是一个不同的应用程序。我也试过 Invoke() 但是 NotifyIcon().

中没有这个定义

这是我写的代码:

static void Main(string[] args)
{
    if (args.Length > 0)
    {
        Arg_Call(args[0]);
    }
    if (new Mutex(true, "{XXX}").WaitOne(TimeSpan.Zero, true))
    {
        Init_Tray();
        Application.Run();
    }
}
private static NotifyIcon trayicon;

private static void Init_Tray()
{
    trayicon = new NotifyIcon() { Icon = new Icon(@"D:\projects\Icon.ico"), Text = "Waiting", Visible = true };
    trayicon.Visible = true;
    Application.Run();
}
private static void Arg_Call(string args)
{
    trayicon.Invoke((MethodInvoker)delegate {
        trayicon.Text = "OK";
    }); //from: 
}

我哪里错了?如何以及通过命令行参数更改 运行 表单中的 NotifyIcon.Text 属性 的最佳方法是什么?

很抱歉,我无法充分解释为什么您的问题与现有 "single-instance-application" 问题重复。我会在这里重申一下思路:

  1. 你写了"How to and what is the best way to change the texts in the running form via command-line-arguments?"
  2. 你的需求涉及一个currently-运行ning进程,它在任务栏中呈现NotifyIcon,并且希望使用命令行修改那个current-运行宁进程的状态。
  3. 一个简单的事实是,当您在命令行中键入任何内容时,它会启动一个全新的进程。该进程必然 不同于 已经 运行 宁的进程,后者在任务托盘中呈现 NotifyIcon

综上所述,我们得出的结论是您希望在命令行上启动的新进程与现有进程进行交互。实现该目标的最简单方法是使用 .NET 中内置的单实例应用程序支持。这是因为对单实例应用程序的支持包括将新的命令行参数自动传递给以前的 运行ning 程序。因此,重复。

正如我之前提到的,你应该尝试培养概括的技能,看看看似新的问题实际上只是变相的旧问题,而你或其他人已经知道如何解决。

就像所有问题的解决都可以概括为 "break the large problem down into smaller problems, repeat as necessary until all of the smaller problems are problems you already know how to solve" 一样,编程通常不是解决新问题的问题,而是认识到你当前的问题是如何真正成为你已经知道的问题如何解决

综上所述,我的印象是您仍然难以弄清楚如何将这些信息应用到您的特定场景中。所以,也许这是一个机会来说明我所拥护的哲学的有效性,向您展示您看似不同的问题实际上是我声称的问题。 :)

那么,让我们从您最初的场景开始吧。我没有使用您发布的代码,因为它主要是不需要的代码。从头开始对我来说似乎更简单。为此,我写了一些 TrayManager class 封装了实际的 NotifyIcon 部分功能:

class TrayManager : IDisposable
{
    private readonly NotifyIcon _notifyIcon;

    public TrayManager()
    {
        _notifyIcon = new NotifyIcon
        {
            ContextMenu = new ContextMenu(new[]
            {
                new MenuItem("Exit", ContextMenu_Exit)
            }),
            Icon = Resources.TrayIcon,
            Text = "Initial value",
            Visible = true
        };
    }

    public void Dispose()
    {
        Dispose(true);
    }

    public void SetToolTipText(string text)
    {
        _notifyIcon.Text = text;
    }

    protected virtual void Dispose(bool disposing)
    {
        _notifyIcon.Visible = false;
    }

    private void ContextMenu_Exit(object sender, EventArgs e)
    {
        Application.ExitThread();
    }

    ~TrayManager()
    {
        Dispose(false);
    }
}

上面对图标的上下文菜单进行了硬编码。当然,这是一个真实世界的程序,您可能希望将菜单与上面的 class 分离,以获得更大的灵活性。

使用上述内容的最简单方法如下所示:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        using (TrayManager trayManager = new TrayManager())
        {
            Application.Run();
        }
    }
}

那么,我们如何修改上面的内容,这样当你再次运行程序时,你可以用命令改变NotifyIconText 属性 - 你输入的行参数?这就是单实例应用程序的用武之地。如我之前标记的副本 What is the correct way to create a single-instance application? 所示,完成此操作的最简单方法之一是使用 Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase class,它具有内置了对单实例应用程序的支持,以及向现有进程提供新命令行参数的机制。

一个小缺点是这个class是为Winforms程序设计的,假设会有一个主窗体。要使用它需要创建一个 Form 实例。对于不需要实际表单的程序,这意味着创建一个从不显示的 Form 实例,并确保它从不显示确实需要一些花招。具体来说:

class TrayOnlyApplication : WindowsFormsApplicationBase
{
    public TrayOnlyApplication()
    {
        IsSingleInstance = true;
        MainForm = new Form { ShowInTaskbar = false, WindowState = FormWindowState.Minimized };

        // Default behavior for single-instance is to activate main form
        // of original instance when second instance is run, which will show
        // the window (i.e. reset Visible to true) and restore the window
        // (i.e. reset WindowState to Normal). For a tray-only program,
        // we need to force the dummy window to stay invisible.
        MainForm.VisibleChanged += (s, e) => MainForm.Visible = false;
        MainForm.Resize += (s, e) => MainForm.WindowState = FormWindowState.Minimized;
    }
}

上面唯一给我们想要的单实例应用程序行为的是IsSingleInstance = true;的设置。其他一切只是为了满足 some Form 对象作为 MainForm 存在的要求,而不实际在屏幕上显示该对象。

将上面的 class 添加到项目中后,我们现在可以 "connect the dots"。新的 Program class 看起来像这样:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        using (TrayManager trayManager = new TrayManager())
        {
            TrayOnlyApplication app = new TrayOnlyApplication();

            app.StartupNextInstance += (s, e) => trayManager
                .SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");
            app.Run(args);
        }
    }
}

您会注意到两个变化:

  1. 除了处理 NotifyIconTrayManager,我们现在还创建 TrayOnlyApplication 对象,订阅它的 StartupNextInstance 事件,以便我们可以接收给任何新实例的命令行参数,并使用它来设置 NotifyIcon 对象的 Text 属性(通过将其传递给专门为此目的创建的方法)。
  2. 而不是使用 Application.Run() 到 运行 要求消息泵循环来处理 window 消息,我们使用 Run() 方法 TrayOnlyApplication class继承自WindowsFormsApplicationBaseclass。这些方法中的任何一个在程序 运行ning 时处理消息泵,并且在 Application.ExitThread() 方法被调用时 return 控制调用者,因此消息泵的两种方法都与代码一起工作TrayManager.

现在,上面的例子只是对没有强制执行单实例操作的原始版本的轻微修改。您可能会注意到它有一个有争议的缺陷,即它总是创建托盘图标,无论它是否是 运行 的第一个实例。后续实例将 运行 创建托盘图标,然后立即关闭图标并退出。

WindowsFormsApplicationBase 提供了一种机制来避免这种情况,即 Startup 事件。 StartupNextInstance 事件在应用程序的任何实例 运行 中引发,而实例已经是 运行ning,而 Startup 事件仅在 时引发]no 其他实例已经 运行ning。 IE。在您真正想要做的事情的情况下,例如显示托盘图标。

我们可以利用该事件来推迟 NotifyIcon 的创建,直到我们知道我们是否真的需要它:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        TrayManager trayManager = null;
        TrayOnlyApplication app = new TrayOnlyApplication();

        // Startup is raised only when no other instance of the
        // program is already running.
        app.Startup += (s, e) => trayManager = new TrayManager();

        // StartNextInstance is run when the program if a
        // previously -run instance is still running.
        app.StartupNextInstance += (s, e) => trayManager
            .SetToolTipText(e.CommandLine.Count > 0 ? e.CommandLine[0] : "<no value given>");

        try
        {
            app.Run(args);
        }
        finally
        {
            trayManager?.Dispose();
        }
    }
}

注意,这里需要显式写try/finally,而不是使用using语句,因为using语句需要对变量进行初始化当它被声明时,我们希望将初始化推迟到以后,或者永远不会,这取决于哪个实例正在 运行.

(不幸的是,没有办法在 TrayOnlyApplication class 中延迟创建虚拟 window,因为只需要调用 Run() 方法,这要求已经设置了一个有效的 Form 对象,并且确定哪个实例是 运行 发生在那个调用中,而不是之前。)

仅此而已。我希望上面清楚地显示了您可以使用的单实例应用程序技术如何直接解决您寻求帮助的问题。通过为程序的 newly-运行 实例提供一种机制,将传递给它的命令行参数传递给同一程序的 already-运行ning 实例,new-运行 实例可以使已经 运行ning 实例执行它需要的任何工作(例如更改托盘图标的工具提示文本)。

自然地,任何类似的机制都会达到相同的结果。唯一重要的是让 newly-运行 实例检测到现有实例,并与之通信。碰巧 WindowsFormsApplicationBase class 提供了预制的功能。有很多其他方法可以做同样的事情,每种方法各有利弊。