WPF 中的 Application.DoEvents() 在哪里?

Posted

技术标签:

【中文标题】WPF 中的 Application.DoEvents() 在哪里?【英文标题】:Where is the Application.DoEvents() in WPF? 【发布时间】:2011-05-28 23:19:57 【问题描述】:

我有以下示例代码,每次按下按钮时都会缩放:

XAML:

<Window x:Class="WpfApplication12.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Canvas x:Name="myCanvas">

        <Canvas.LayoutTransform>
            <ScaleTransform x:Name="myScaleTransform" />
        </Canvas.LayoutTransform> 

        <Button Content="Button" 
                Name="myButton" 
                Canvas.Left="50" 
                Canvas.Top="50" 
                Click="myButton_Click" />
    </Canvas>
</Window>

*.cs

public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();
    

    private void myButton_Click(object sender, RoutedEventArgs e)
    
        Console.WriteLine("scale 0, location: 1", 
            myScaleTransform.ScaleX,
            myCanvas.PointToScreen(GetMyByttonLocation()));

        myScaleTransform.ScaleX =
            myScaleTransform.ScaleY =
            myScaleTransform.ScaleX + 1;
        
        Console.WriteLine("scale 0, location: 1",
            myScaleTransform.ScaleX,
            myCanvas.PointToScreen(GetMyByttonLocation()));
    
    
    private Point GetMyByttonLocation()
    
        return new Point(
            Canvas.GetLeft(myButton),
            Canvas.GetTop(myButton));
    

输出是:

scale 1, location: 296;315
scale 2, location: 296;315

scale 2, location: 346;365
scale 3, location: 346;365

scale 3, location: 396;415
scale 4, location: 396;415

如您所见,有一个问题,我认为可以通过使用 Application.DoEvents(); 解决,但是......它在 .NET 4 中先验存在。

怎么办?

【问题讨论】:

线程? Application.DoEvents() 是穷人的替代品,可以替代编写正确的多线程应用程序,并且在任何情况下都非常糟糕。 我知道这很糟糕,但我更喜欢什么都没有的东西。 这是相关的:How to wait for WaitHandle while serving WPF Dispatcher events? 【参考方案1】:

试试这样的

public static void DoEvents()

    Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
                                          new Action(delegate  ));

【讨论】:

我什至为应用写了一个扩展方法:) public static void DoEvents(this Application a) @serhio: 简洁的扩展方法:) 我应该注意的是,在实际应用程序中Application.Current 有时为空......所以也许它并不完全等同。 完美。这应该是答案。反对者不应该得到赞誉。 这并不总是有效,因为它不会推送帧,如果正在执行中断指令(即调用 WCF 方法,该方法同步地继续执行此命令)您可能不会看到'刷新',因为它将被阻止。这就是为什么从 MSDN 资源提供的答案 flq 比这个更正确。【参考方案2】:

好吧,我刚刚遇到了一个案例,我开始使用在 Dispatcher 线程上运行的方法,它需要在不阻塞 UI 线程的情况下阻塞。原来,msdn 解释了如何基于 Dispatcher 本身实现 DoEvents():

public void DoEvents()

    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
        new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);


public object ExitFrame(object f)

    ((DispatcherFrame)f).Continue = false;

    return null;

(取自Dispatcher.PushFrame Method)

有些人可能更喜欢在单一方法中执行相同的逻辑:

public static void DoEvents()

    var frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
        new DispatcherOperationCallback(
            delegate (object f)
            
                ((DispatcherFrame)f).Continue = false;
                return null;
            ),frame);
    Dispatcher.PushFrame(frame);

【讨论】:

不错的发现!这看起来比 Meleak 建议的实现更安全。我找到了a blog post about it @HugoRune 该博客文章指出这种方法是不必要的,并且使用与 Meleak 相同的实现。 @Lukazoid 据我所知,简单的实现可能会导致难以追踪的锁定。 (我不确定原因,可能问题是调度程序队列中的代码再次调用 DoEvents,或者调度程序队列中的代码生成更多调度程序帧。)无论如何,exitFrame 的解决方案没有出现此类问题,所以我会推荐那个。 (或者,当然,根本不使用 doEvents) 在您的窗口上显示覆盖而不是对话框,结合 caliburn 在应用程序关闭时让虚拟机参与的方式排除了回调,并要求我们在不阻塞的情况下阻塞。如果您向我提供一个没有 DoEvents hack 的解决方案,我会很高兴。 旧博文的新链接:kent-boogaart.com/blog/dispatcher-frames【参考方案3】:

旧的 Application.DoEvents() 方法已在 WPF 中弃用,取而代之的是使用 Dispatcher 或 Background Worker Thread 来执行您所描述的处理。有关如何使用这两个对象的文章,请参阅链接。

如果您绝对必须使用 Application.DoEvents(),那么您可以简单地将 system.windows.forms.dll 导入您的应用程序并调用该方法。但是,确实不建议这样做,因为您将失去 WPF 提供的所有优势。

【讨论】:

我知道这很糟糕,但我更喜欢什么都没有的东西......在我的情况下如何使用 Dispatcher? 我了解您的情况。当我编写我的第一个 WPF 应用程序时,我就在其中,但我继续前进并花时间学习新库,从长远来看,它会更好。我强烈建议花点时间。至于您的特定情况,在我看来,您希望调度程序在每次触发单击事件时处理坐标的显示。您需要阅读有关 Dispatcher 的更多信息以了解确切的实现。 删除 Application.DoEvents() 几乎和 MS 删除 Windows 8 上的“开始”按钮一样烦人。 不,这是一件好事,这种方法弊大于利。 我不敢相信这在当今时代仍然是一个问题。我记得必须在 VB6 中调用 DoEvents 来模拟异步行为。问题是后台工作人员仅在您不进行 UI 处理时才起作用,但 UI 处理 - 例如创建数千个 ListBoxItems 可能会锁定 UI 线程。当 UI 工作真的很繁重时,我们应该怎么做?【参考方案4】:

如果你只需要更新窗口图形,最好这样使用

public static void DoEvents()

    Application.Current.Dispatcher.Invoke(DispatcherPriority.Render,
                                          new Action(delegate  ));

【讨论】:

使用 DispatcherPriority.Render 比 DispatcherPriority.Background 工作得更快。今天用 StopWatcher 测试 我只是使用了这个技巧,但使用 DispatcherPriority.Send (最高优先级)以获得最佳效果;响应速度更快的用户界面。【参考方案5】:
myCanvas.UpdateLayout();

似乎也可以。

【讨论】:

我将使用它,因为它对我来说似乎更安全,但我将保留 DoEvents 用于其他情况。 不知道为什么,但这对我不起作用。 DoEvents() 工作正常。 就我而言,我必须这样做以及 DoEvents()【参考方案6】:

这两种提议的方法的一个问题是它们需要空闲的 CPU 使用率(根据我的经验高达 12%)。这在某些情况下不是最理想的,例如当使用此技术实现模态 UI 行为时。

以下变体使用定时器引入了帧之间的最小延迟(注意这里是用 Rx 编写的,但可以使用任何常规定时器来实现):

 var minFrameDelay = Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(1).Replay();
 minFrameDelay.Connect();
 // synchronously add a low-priority no-op to the Dispatcher's queue
 Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => minFrameDelay.Wait()));

【讨论】:

【参考方案7】:

自从引入asyncawait 以来,现在可以通过使用Task.Delay 的(以前)* 同步代码块中途放弃 UI 线程,例如

private async void myButton_Click(object sender, RoutedEventArgs e)

    Console.WriteLine("scale 0, location: 1", 
        myScaleTransform.ScaleX,
        myCanvas.PointToScreen(GetMyByttonLocation()));

    myScaleTransform.ScaleX =
        myScaleTransform.ScaleY =
        myScaleTransform.ScaleX + 1;

    await Task.Delay(1); // In my experiments, 0 doesn't work. Also, I have noticed
                         // that I need to add as much as 100ms to allow the visual tree
                         // to complete its arrange cycle and for properties to get their
                         // final values (as opposed to NaN for widths etc.)

    Console.WriteLine("scale 0, location: 1",
        myScaleTransform.ScaleX,
        myCanvas.PointToScreen(GetMyByttonLocation()));

老实说,我没有使用上面的确切代码尝试过,但是当我将许多项目放入具有昂贵项目模板的 ItemsControl 中时,我会在紧密循环中使用它,有时会添加一个稍稍延迟,让 UI 上的其他内容有更多时间。

例如:

        var levelOptions = new ObservableCollection<GameLevelChoiceItem>();

        this.ViewModel[LevelOptionsViewModelKey] = levelOptions;

        var syllabus = await this.LevelRepository.GetSyllabusAsync();
        foreach (var level in syllabus.Levels)
        
            foreach (var subLevel in level.SubLevels)
            
                var abilities = new List<GamePlayingAbility>(100);

                foreach (var g in subLevel.Games)
                
                    var gwa = await this.MetricsRepository.GetGamePlayingAbilityAsync(g.Value);
                    abilities.Add(gwa);
                

                double PlayingScore = AssessmentMetricsProcessor.ComputePlayingLevelAbility(abilities);

                levelOptions.Add(new GameLevelChoiceItem()
                    
                        LevelAbilityMetric = PlayingScore,
                        AbilityCaption = PlayingScore.ToString(),
                        LevelCaption = subLevel.Name,
                        LevelDescriptor = level.Ordinal + "." + subLevel.Ordinal,
                        LevelLevels = subLevel.Games.Select(g => g.Value),
                    );

                await Task.Delay(100);
            
        

在 Windows 应用商店中,当合集有漂亮的主题过渡时,效果非常理想。

卢克

参见 cmets。当我快速写下我的答案时,我正在考虑获取同步代码块然后将线程放回其调用者的行为,其效果使代码块异步。我不想完全改写我的答案,因为这样读者就看不到我和 Servy 争吵的内容了。

【讨论】:

"现在可以通过同步块中途放弃 UI 线程" 不,不是。您刚刚使代码异步,而不是以同步方法从 UI 线程抽取消息。现在,一个正确设计的 WPF 应用程序将不会阻塞 UI 线程,首先在 UI 线程中同步执行长时间运行的操作,使用异步来允许现有的消息泵适当地泵送消息。 @Servy 在幕后,await 将导致编译器注册其余的异步方法作为等待任务的延续。这种延续将发生在 UI 线程上(相同的同步上下文)。然后控制权返回到异步方法的调用者,即 WPF 事件子系统,其中事件将一直运行,直到在延迟期到期后的某个时间运行预定的延续。 是的,我很清楚这一点。这就是使方法异步的原因(将控制权交给调用者并仅调度延续)。您的回答表明该方法实际上是使用 asynchrony 来更新 UI 时是同步的。 第一种方法(OP的代码)是同步的,Servy。第二个示例只是在循环或必须将项目倒入长列表时保持 UI 运行的提示。 而你所做的是使同步代码异步。正如您的描述或答案所要求的那样,您没有在同步方法中保持 UI 响应。【参考方案8】:

在 WPF 中创建你的 DoEvent():

Thread t = new Thread(() => 
            // do some thing in thread
            
            for (var i = 0; i < 500; i++)
            
                Thread.Sleep(10); // in thread

                // call owner thread
                this.Dispatcher.Invoke(() => 
                    MediaItem uc = new MediaItem();
                    wpnList.Children.Add(uc);
                );
            
            

        );
        t.TrySetApartmentState(ApartmentState.STA); //for using Clipboard in Threading
        t.Start();

为我工作!

【讨论】:

【参考方案9】:

回答最初的问题:DoEvents 在哪里?

我认为 DoEvents 是 VBA。而且VBA好像没有Sleep功能。但是 VBA 有办法获得与睡眠或延迟完全相同的效果。在我看来,DoEvents 相当于 Sleep(0)。

在 VB 和 C# 中,您正在处理 .NET。原来的问题是一个 C# 问题。在 C# 中,您将使用 Thread.Sleep(0),其中 0 是 0 毫秒。

你需要

using System.Threading.Task;

在文件顶部以便使用

Sleep(100);

在您的代码中。

【讨论】:

以上是关于WPF 中的 Application.DoEvents() 在哪里?的主要内容,如果未能解决你的问题,请参考以下文章

wpf窗口中的资源的混合使用---WPF

wpf 怎么获取Template中的控件

MVVM设计模式和在WPF中的实现 事件绑定

WPF中的DataGridView绘制错误

2021-11-26 WPF面试题 WPF 中的资源是什么?

WPF中的依赖属性