如何在没有窗体或控件的情况下在 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 线程的背景信息。这是因为在另一个线程中尝试访问Clipboard
或SendKeys
等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
【问题讨论】:
@CharliefaceContextMenu
和 MenuItem
是 Component
没有 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 线程?的主要内容,如果未能解决你的问题,请参考以下文章