wpf DispatcherFrame

本文详细探讨了WPF中Dispatcher对象和DispatcherFrame的使用,特别是在跨线程更新UI时的角色。通过示例代码展示了如何利用Dispatcher调度消息,以及DispatcherFrame在启动新消息循环中的作用。当在UI线程上执行密集循环时,如何利用DispatcherFrame避免界面卡顿并实现UI的适时更新。最后,文章提供了有效利用DispatcherFrame更新UI的解决方案。

对于 WPF 的线程模型,Dispatcher 对象相信各位大伙伴已经不陌生,尤其是跨线程更新UI的时候,都会用它来调度消息。与 Dispatcher 对象有关的,还有一个叫 DispatcherFrame 的东东,开发文档是这么说的:Represents an execution loop in the Dispatcher,这样描述肯定是让人看不明白的,老周也不明白。

那咋办呢?根据老周十几年来积累下来的一些不要脸的经验,遇到这些很是抽象的玩意儿,可以有两种途径去了解:1、看.net 源代码,看看它能干吗;2、自己写几行代码试一试,许多时候,一试便能明了。

常看老周的垃圾博客的朋友一定很了解老周,老周向来重视示例,所以,下面咱们用示例来慢慢寻找真相,千万不要急,急的人都是浮躁。

好,首先,我们在窗口中放一个矩形,一个按钮。

<Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <Rectangle Height="350" Fill="Red" Name="rect" Width="0"/>
        <Button Grid.Row="1" Content="Click Me" Click="OnClick"/>
    </Grid>

当按钮被单击后,会引发事件,然后,我在事件处理代码中,不断地让矩形的宽度增加,即增加 Width 属性。

我们先来第一种情形,把代码放到一个Task上,并用 Dispatcher 来更新矩形宽度,这种做法很常用。

Task taskrun = new Task(() =>
            {
                double i = 0d;
                for (i = 0d; i < 800d; i++)
                {
                    Task.Delay(5).Wait();
                    Action act = () => rect.Width++;
                    Dispatcher.BeginInvoke(act, DispatcherPriority.Background);
                }
            });
            taskrun.Start();

因为 Task 会在另一个线程上干活,所以修改UI要借助 Dispatcher 大叔来调度消息。DispatcherPriority.Background表示更新宽度的代码在后台进行。其实你可以更为这样:

Dispatcher.BeginInvoke(act, DispatcherPriority.Render);

这样优先级更高,优先照顾UI呈现,可以尽可能地及时更新矩形的宽度。

于是,你能看到这样的效果。

好,下面我们做第二轮试验,刚才的代码中,for 循环是在一个Task中执行的,所以它不占用UI线程,只是在修改矩形宽度时才会与UI线程交互。这次我们把所有代码都在UI线程上执行。 

Action invkAct = () =>
            {
                rect.Width++;
            };

            for (double i = 0d; i< 800d; i++)
            {
                Task.Delay(5).Wait();
                Dispatcher.BeginInvoke(invkAct, DispatcherPriority.Render);
            }

 

然后一运行,你就发现,卡住了。就算你把 DispatcherPriority 改为 Background 也一样卡住。因为for在UI线程上执行,UI更新的消息没有被及时处理(只有一条车道,塞车是肯定的),于是,更新矩形宽度的消息一直在消息队列中排队,只有等For 循环执行完了,才能更新矩形宽度,显然,这样不符合我们的预期了。

要么,你就像最前面的例子那样,把for循环写到Task上,以减轻UI线程的负担。如果你真的不想用新的线程去执行代码,而希望全在UI线程上执行,便真的要用上 DispatcherFrame 了。

DispatcherFrame 类有个很TMD重要的属性——Continue,为什么重要?先放着,待会儿回过头来再说。

要把一个 DispatcherFrame 插入到Dispatcher 中,就要调用 PushFrame 方法,这是个静态方法,Frame 会被插入到当前线程的 Dispatcher 对象中。在.net 源代码中,我发现 PushFrame 方法的实现代码,现贴出来,请各位与老周一同鉴赏。

public static void PushFrame(DispatcherFrame frame)
        {
            ……
 
            Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
            ……
            // 下面这个方法才是实现代码
            dispatcher.PushFrameImpl(frame);
        }
public static void PushFrame(DispatcherFrame frame)
        {
            ……
 
            Dispatcher dispatcher = Dispatcher.CurrentDispatcher;
            ……
            // 下面这个方法才是实现代码
            dispatcher.PushFrameImpl(frame);
        }
private void PushFrameImpl(DispatcherFrame frame)
        {
            // 用来切换线程的上下文
            SynchronizationContext oldSyncContext = null;
            SynchronizationContext newSyncContext = null;
            // Windows 消息
            MSG msg = new MSG(); 
       
            //  _frameDepth 变量用来统计,代码执行前加1,代码执行后会减1
            _frameDepth++;
            try
            {
                // 临时更改线程上下文
                oldSyncContext = SynchronizationContext.Current;
                newSyncContext = new DispatcherSynchronizationContext(this);
                SynchronizationContext.SetSynchronizationContext(newSyncContext);
 
                // 注意,下面代码是重点,开启一个消息循环
                try
                {
                    // 循环条件正是 Continue 属性
                    while(frame.Continue)
                    {
                        if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
                            break;
 
                        TranslateAndDispatchMessage(ref msg);
                    }
 
                    // If this was the last frame to exit after a quit, we
                    // can now dispose the dispatcher.
                    if(_frameDepth == 1)
                    {
                        if(_hasShutdownStarted)
                        {
                            ShutdownImpl();
                        }
                    }
                }
                finally
                {
                    // 还原线程上下文                    SynchronizationContext.SetSynchronizationContext(oldSyncContext);
                }
            }
            finally
            {
                _frameDepth--;   // 注意这里减1
                if(_frameDepth == 0)
                {
                    // We have exited all frames.
                    _exitAllFrames = false;
                }
            }
        }

代码看起来很长,看不懂不要紧,重点看那个 while 循环。

while(frame.Continue)
       {
              if (!GetMessage(ref msg, IntPtr.Zero, 0, 0))
                  break;
 
               TranslateAndDispatchMessage(ref msg);
        }

现在,你明白 DispatcherFrame.Continue 属性的作用了吧。是的,如果它为真,那么这个循环会执行,GetMessage才会获取消息,并交由 TranslateAndDispatchMessage 函数进行处理。如果为假,那循环自然不会执行了。

由于 DispatcherFrame 会开启一个消息循环来提取未处理的消息,于是你一定想到了,在 for 的每一轮循环中向 Dispatcher 对象插入一个 DispatcherFrame 实例。但是,以下两种用法都无法及时更新UI。

方案A:无效。

  Dispatcher.PushFrame(new DispatcherFrame());
            for (double n = 0d; n < 800d; n++)
            {
                Task.Delay(5).Wait();
                rect.Width++;
            }

方案B:依然无效。

for (double n = 0d; n < 800d; n++)
            {
                Task.Delay(5).Wait();
                rect.Width++;
                ProcessMsgs();
            }
 
        private void ProcessMsgs()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Dispatcher.PushFrame(frame);
            frame.Continue = false;
        }

以上两方案,一个是在循环之前开启一个新的循环,另一个则是在每一轮for循环中插入一个DispatcherFrame对象,并把Continue属性改为False,用以及时退出消息循环。

然而,想象很美好,实战很残酷。以上两个方案虽然让界面不卡了,但,矩形的宽度依旧不会实时更新。为什么会这样呢,根据老周的低见,应该是与优先级有关,毕竟 for 循环正在密集地执行,插入到消息队列的消息只能在厕所排长龙。

因为代码都在一个线程上执行,其实UI是做不到真正的实时更新的,但可以稍稍延迟更新,至少眼睛是看不出来的。

 可以把代码改成这样:

private void ProcessMsgs()
        {
            DispatcherFrame frame = new DispatcherFrame();
            Action<object> cb = obj =>
            {
                DispatcherFrame f = obj as DispatcherFrame;
                f.Continue = false;
            };
            Dispatcher.BeginInvoke(cb, DispatcherPriority.Background, frame);
            Dispatcher.PushFrame(frame);
        }

之所以要在BeginInvoke的委托中修改 Continue 属性,最重要的是可以设置优先级,除了Background,还可以使用 ApplicationIdle、SystemIdle等值,但不能用 Render、Send 这些优先级较高的值,因为这样也会被主消息循环阻塞,只能使用空闲或后台优先级。

这样改了之后,就可以达到预期效果了。

这类似于WinForm 中的 DoEvents 方法,MSDN上有这个示例。

总结一下,DispatcherFrame 的用途就是激活一新的消息循环,并以 Continue 属性作为退出新消息循环的标志。

<think>我们正在处理一个关于WPF强制刷新控件的问题。用户希望了解在WPF中强制刷新控件的方法。根据提供的引用,我们可以总结出几种不同的方法。 引用中提到了几种强制刷新的场景: 1. 引用[1]和[5]展示了使用DispatcherFrame来强制刷新UI界面。这种方法通过创建一个新的DispatcherFrame并调用PushFrame来强制UI处理所有待处理的操作,包括渲染。 2. 引用[2]提到了WebBrowser控件的刷新问题,但这不是我们当前关注的重点。 3. 引用[3]展示了通过调用Items集合的Refresh方法来刷新TabControl的Header。 4. 引用[4]展示了在后台线程中通过Dispatcher.Invoke来更新UI上的控件值。 因此,我们可以总结出以下几种方法: 方法1:使用DispatcherFrame(如引用[1]和[5]) 这种方法适用于需要在长时间循环中更新UI的场景。通过创建一个DispatcherFrame,并强制UI处理当前队列中的操作,可以达到刷新的效果。 示例代码: ```csharp public void RefreshUI() { DispatcherFrame frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(delegate (object f) { ((DispatcherFrame)f).Continue = false; return null; }), frame); Dispatcher.PushFrame(frame); } ``` 在循环中,每次更新数据后调用RefreshUI(),就可以实时刷新UI。 方法2:使用Dispatcher.Invoke(如引用[4]) 如果是在后台线程中更新UI,必须使用Dispatcher.Invoke或BeginInvoke来更新UI元素。同时,如果更新操作很频繁,可以考虑在更新后强制刷新,但通常直接更新UI属性就会刷新。如果遇到刷新不及时,可以尝试在Invoke中调用InvalidateVisual等方法。 示例代码: ```csharp this.Dispatcher.Invoke(() => { // 更新UI控件的值 textBox.Text = "新值"; // 如果需要强制重绘,可以调用以下方法 textBox.InvalidateVisual(); // 或者强制重新测量和排列 textBox.InvalidateMeasure(); textBox.InvalidateArrange(); }); ``` 方法3:调用特定控件的刷新方法(如引用[3]) 某些控件,如ItemsControl(包括TabControl、ListBox等)提供了刷新其项的方法。例如,可以调用Items.Refresh()来刷新数据绑定。 示例代码: ```csharp tabControl.Items.Refresh(); ``` 方法4:使用绑定并通知属性更改 实际上,最佳实践是使用数据绑定和INotifyPropertyChanged接口。这样,当属性值改变时,绑定会自动更新UI。强制刷新通常是最后的手段。 但是,如果绑定没有更新,可以尝试: - 确保属性设置时触发了PropertyChanged事件。 - 如果绑定是一次性的(OneTime),需要改为OneWay或TwoWay。 - 如果绑定的对象实现了INotifyPropertyChanged,但UI没有更新,可能是绑定路径错误或者没有在UI线程上更改属性。 如果确实需要强制刷新绑定,可以使用: ```csharp BindingExpression bindingExpr = textBox.GetBindingExpression(TextBox.TextProperty); bindingExpr?.UpdateTarget(); // 强制从数据源更新到控件(如果绑定是OneWay或TwoWay) // 或者 bindingExpr?.UpdateSource(); // 强制从控件更新到数据源 ``` 总结: 在WPF中强制刷新控件,可以根据具体情况选择以下方法: 1. 使用DispatcherFrame强制处理UI操作队列(适用于循环中逐步更新)。 2. 在后台线程更新UI时使用Dispatcher.Invoke,并可以结合Invalidate方法强制重绘控件。 3. 对特定控件调用Refresh方法(如ItemsControl的Items.Refresh)。 4. 使用数据绑定的UpdateTarget或UpdateSource方法更新绑定。 用户可以根据实际场景选择合适的方法。通常,优先考虑数据绑定和通知机制,只有在必要时才使用强制刷新。 相关问题: 1. 在WPF中,如何实现数据绑定并自动更新UI? 2. 什么是DispatcherFrame,它在WPF中的作用是什么? 3. 如何解决WPF中后台线程更新UI控件的问题? 4. 在WPF中,哪些控件有内置的刷新方法? 5. 如何强制WPF控件重新渲染(重绘)? </think>在 WPF 中强制刷新控件的方法主要有以下几种技术手段: ### 1. 使用 DispatcherFrame 强制刷新(推荐) ```csharp public void ForceRefreshUI() { DispatcherFrame frame = new DispatcherFrame(); Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(delegate (object f) { ((DispatcherFrame)f).Continue = false; return null; }), frame); Dispatcher.PushFrame(frame); } ``` **使用场景**:在长时间循环操作中实时更新 UI。 **原理**:创建新的调度帧强制 UI 线程立即处理待执行操作队列[^1][^5]。 ### 2. 通过 Dispatcher.Invoke 更新 ```csharp // 在后台线程中更新 UI this.Dispatcher.Invoke(() => { textBox.Text = "新值"; // 强制重绘 textBox.InvalidateVisual(); }); ``` **优势**:线程安全,适用于跨线程更新控件[^4]。 ### 3. 控件特定刷新方法 ```csharp // 刷新 ItemsControl 类型控件(如 ListBox, TabControl) myListBox.Items.Refresh(); // 刷新 TabControl 头部(引用示例) ((MainWindow)Window).TabControl.Items.Refresh(); [^3] ``` ### 4. 数据绑定强制更新 ```csharp BindingExpression binding = textBox.GetBindingExpression(TextBox.TextProperty); binding?.UpdateTarget(); // 强制从数据源更新到 UI binding?.UpdateSource(); // 强制从 UI 更新到数据源 ``` ### 最佳实践建议 1. **优先使用数据绑定**:实现 `INotifyPropertyChanged` 接口自动刷新 2. **避免频繁强制刷新**:可能影响性能,仅在必要时使用 3. **线程安全**:非 UI 线程操作必须通过 `Dispatcher.Invoke/BeginInvoke` 4. **虚拟化容器控件**:对 `ListBox` 等控件启用虚拟化(`VirtualizingStackPanel`)提升性能 > 对于特殊控件如 `WebBrowser`,需注意其 `Navigate` 方法与其他方法的刷新行为差异[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值