WinForms 开发中Control.Invoke是用于非UI线程中请求修改UI元素的方法, 一般配合Control.InvokeRequired使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
类似Control.Invoke的还有Control.BeginInvoke和Control.EndInvoke, 它们是异步调用.
这些方法和属性都依赖于IsHandleCreated为true
时, IsHandleCreated表示窗口句柄是否已创建, 它并不是指是否new Form1()
过, 而是指是否Show()
过, 包括Application.Run, Show, ShowDialog这些调用都会使IsHandleCreated为true
.
而在IsHandleCreated为false
时, 比如刚刚new Form1()
, Control.InvokeRequired返回false
, 调用Control.Invoke会抛出异常:
System.InvalidOperationException: 在创建窗口句柄之前,不能在控件上调用 Invoke 或 BeginInvoke。
当在非UI线程和多个窗口之间操作时, 可能会有一些麻烦的情况发生, 这种情况可能会考虑使用SynchronizationContext.
SynchronizationContext可以在当前线程第一次new Form1()
之后通过SynchronizationContext.Current取得, 之后使用Post和Send实现在UI线程执行指定的委托, 下面使用的WindowsFormsSynchronizationContext.Current在WinForms程序中等价于SynchronizationContext.Current:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
从SynchronizationContext.Current的文档可知它只会返回当前线程的同步上下文, 要在别的线程中访问需要自行保存它的引用, 即这里属性SyncContext
, 使用时确保在访问SyncContext
之前new Form1()
过一次, 且只能一次, 否则后续的会覆盖之前的, 在符合需求的情况下会很自然想到单例模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
目前看起来是没什么问题了, 现实总是会出点问题, 比如SynchronizationContext.Current总是返回当前线程的, 结合上述的单例模式, 如果第一次访问Instance
属性是在别的线程中, 测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
上面代码的两处断言都通过了, 这种情况下Form1.SyncContext.Post仍旧可以调用, 但是将不产生任何效果, 也不抛出异常, 因为new Form1()
的那个线程已经结束了, 以及那个线程并没有执行消息循环Application.Run.
如果需要在Application.Run之后, 相关的UI元素变得可用时再执行相关代码, 可以自行定义事件, 实现相关的触发和绑定, 确保new Form1
和Application.Run在同一个线程中调用, 在具体的多线程环境中解决办法会表现的完全不同.