如何在没有窗体或控件的情况下在 Winform 应用程序中调用 UI 线程?

Posted

技术标签:

【中文标题】如何在没有窗体或控件的情况下在 Winform 应用程序中调用 UI 线程?【英文标题】:How to invoke UI thread in Winform application without a form or control? 【发布时间】:2021-05-28 11:00:49 【问题描述】:

我创建了一个托盘应用程序来控制一些硬件组件。如何在没有主窗体或控件的情况下调用 UI 线程?

托盘应用以Application.Run(new MyTrayApp())启动:

class MyTrayApp : ApplicationContext

    private NotifyIcon trayIcon;

    public MyTrayApp()
                
        trayIcon = new NotifyIcon()
        
            Icon = Resources.app_icon,
            ContextMenu = new ContextMenu(new MenuItem[]                 
            new MenuItem("Exit", Exit)
        ),
            Visible = true
        ;
        // context is still null here
        var context = SynchronizationContext.Current;
        // but I want to invoke UI thread in hardware events
        MyHardWareController controller= new MyHardWareController(context);
    

    void Exit(object sender, EventArgs e)
    
        // context is accessible here because this is a UI event
        // too late tho
        var context = SynchronizationContext.Current;
        trayIcon.Visible = false;
        Application.Exit();
    

    Control.Invoke() 不可用,因为没有控件 Searching 建议保存SynchronizationContext.Current 以供以后调用,但没有ApplicationContext.Load() 事件...? 我注意到MainForm 在整个循环中是null。我想知道SynchronizationContext在这种情况下是如何初始化的?

编辑:

只是添加一些关于我为什么要调用 UI 线程的背景信息。这是因为在另一个线程中尝试访问ClipboardSendKeys等Windows资源时会抛出System.Threading.ThreadStateException

  HResult=0x80131520
  Message=Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. Ensure that your Main function has STAThreadAttribute marked on it.
  Source=System.Windows.Forms
  StackTrace:
...

这是另一罐蠕虫,但仅供参考:

    [STAThreadAttribute] 已为 Main 函数设置(无效) 创建新的 STA 线程会导致反病毒软件在编译时删除我的应用程序

因此Form.Invoke() 或等效调用主线程应该是最简单的。

编辑 2:

添加一个用于重现错误的要点: https://gist.github.com/jki21/eb950df7b88c06cc5c6d46f105335bbf

【问题讨论】:

@Charlieface ContextMenuMenuItemComponent 没有 Invoke() 方法。它不允许我在没有Invoke() 的情况下在硬事件线程中做各种事情,例如SendKey。此外,托盘菜单仅用于退出应用程序,因此在点击事件中捕获 SynchronizationContext 已经为时已晚。 对不起,你是对的,我的错。所以你想SendKeys,你需要在UI线程上吗? 如果您需要一个 STA 线程,您需要创建一个,或者确保应用程序正在运行一个。为什么需要消息泵或 STA 线程? 您需要更新什么以及这些更新是如何显示的? ApplicationContext 的 Form 不是在这里创建的,不清楚什么用什么更新什么。 为什么不在Application.Idle event 上实例化new MyHardwareController(context); 【参考方案1】:

如 Loathing 所述,使用 Application.Idle 解决了这个问题!谢谢大家的建议!

托盘应用程序:

class MyTrayApp: ApplicationContext 
  private MyHardwareController controller = null;

  public MyTrayApp() 
    Application.Idle += new EventHandler(this.OnApplicationIdle);
    // ...
  

  private void OnApplicationIdle(object sender, EventArgs e) 
    // prevent duplicate initialization on each Idle event
    if (controller == null) 
      var context = TaskScheduler.FromCurrentSynchronizationContext();

      controller = new MyHardwareController((f) => 
        Task.Factory.StartNew(
          () => 
            f();
          ,
          CancellationToken.None,
          TaskCreationOptions.None,
          context);
      );
    
  
  // ...

我的硬件控制器:

class MyHardwareController 
  private Action < Action > UIInvoke;

  public MyHardwareController(Action < Action > UIInvokeRef) 
    UIInvoke = UIInvokeRef;
  

  void hardware_Event(object sender, EventArgs e) 
    // Invoke UI thread
    UIInvoke(() => Clipboard.SetText("I am in UI thread!"));
  



【讨论】:

您知道Application.Idle 事件是否可以多次引发? 是的,Application.Idle 似乎会被触发多次。我没有调查,但我猜每次 UI 线程调用完成时应用程序都会空闲。【参考方案2】:

另一种解决方案是创建一个虚拟表单(它永远不会显示,但应该存储在某个地方。您只需访问表单的 Handle 属性即可从现在开始调用它。

public static DummyForm Form  get; private set; 

static void Main(string[] args)

    Form = new DummyForm();
    _ = Form.Handle;
    Application.Run();

现在可以调用 UI 线程:

Form.Invoke((Action)(() => ...);

【讨论】:

以上是关于如何在没有窗体或控件的情况下在 Winform 应用程序中调用 UI 线程?的主要内容,如果未能解决你的问题,请参考以下文章

winform中如何判断窗体上的控件是按钮

在C#winform中如何遍历子窗体中有容器中的所有的控件

在C#winform中如何遍历子窗体中所有的控件

winform窗体在其他电脑上启动控件位置出错

winform中如何判断多久没有对窗体进行操作了?

winform窗体如何美化界面