为什么UI在编程控制时没有响应?

时间:2016-02-03 22:23:30

标签: c# winforms

我手动推出了我想要自动播放的游戏的MVC风格实现。通过"自动播放"我的意思是通常用户在播放时点击的按钮我希望控制器自动启动。这样我就可以出于质量控制的原因观看游戏本身。这个特定的游戏有很多代码,所以我没有提供它作为一个例子,而是使用相同的方法创建了一个愚蠢的HelloWorld示例。

在我提供示例之前,这是我的问题:您在下面看到的所有内容都是正常的,并且"正常运行&#34 ;;除了一件事:我无法关闭自动播放,因为用户界面变得没有响应,关闭它的按钮不会响应点击事件。

首先在解决方案中创建.Net 4.6.1 winforms项目。 (.net版本可能并不重要,只要它是> = 4.5)。创建一个如下所示的表单:

enter image description here

在后面的代码中,复制粘贴:(根据需要更改名称以进行编译)

using System;
using System.Threading;
using System.Windows.Forms;

namespace WinformsExample
{
    public partial class HelloWorldView : Form
    {
        private readonly HelloWorldController MyHelloWorldController;

        public HelloWorldView()
        {
            InitializeComponent();

            MyHelloWorldController = new HelloWorldController();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MyHelloWorldController.HelloWorldRequested();

            if (MyHelloWorldController.IsAutomated)
            {
                Thread.Sleep(2000);
                button1.PerformClick();
            }
        }

        private void HelloWorldView_Load(object sender, EventArgs e)
        {
            MyHelloWorldController.HelloWorldRequestedEvent += OnHelloWorldRequested;
        }

        private void OnHelloWorldRequested(HelloWorldParameters parameters)
        {
            textBox1.Text += parameters.HelloWorldString + Environment.NewLine;
            textBox1.Update();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            MyHelloWorldController.IsAutomated = !MyHelloWorldController.IsAutomated;

            if (MyHelloWorldController.IsAutomated)
            {
                button2.Text = "hello world - is on";
                button2.Update();
                button1.PerformClick();
            }
            else
            {
                button2.Text = "hello world - is off";
                button2.Update();
            }
        }
    }
}

创建一个名为HelloWorldController.cs的类,并将其粘贴到其中:

namespace WinformsExample
{
    public class HelloWorldParameters
    {
        public string HelloWorldString { get; set; }
    }

    public delegate void HelloWorldEventHandler(HelloWorldParameters parameters);

    public class HelloWorldController
    {
        private readonly HelloWorldParameters _parameters;

        public event HelloWorldEventHandler HelloWorldRequestedEvent;

        public bool IsAutomated { get; set; }

        public HelloWorldController()
        {
            _parameters = new HelloWorldParameters();
        }

        public void HelloWorldRequested()
        {
            _parameters.HelloWorldString = "Hello world!!";

                        if (HelloWorldRequestedEvent != null)
            HelloWorldRequestedEvent(_parameters);
        }
    }
}

...如果需要,请继续重命名。现在构建程序。单击第一个按钮。你会看到"你好世界"。现在点击第二个按钮,你会看到" hello world"每2秒打印一次。

想法的方式这将是有效的,通过再次点击button2,它将停止自动播放。但是,UI没有响应,按钮点击事件永远不会发生。

这里发生了什么导致UI无法响应,我该如何修复它以便我得到预期的行为?

*更新 - 这是解决方案*

除了HelloWorldView.cs之外,保持一切与上面相同。删除对Thread.Sleep()的调用。将计时器从工具箱拖放到设计图面。您将在设计器表面底部看到一个标有

的图标
  

TIMER1

将以下代码复制粘贴到HelloWorldView.cs中。编译并执行。如果一切正确,你应该能够打开和关闭'#34; hello world"通过单击按钮随时显示 - UI保持响应。

using System;
using System.Windows.Forms;

namespace WinformsExample
{
    public partial class HelloWorldView : Form
    {
        private readonly HelloWorldController MyHelloWorldController;

        public HelloWorldView()
        {
            InitializeComponent();

            MyHelloWorldController = new HelloWorldController();
        }

        private void onTimerTick(object sender, EventArgs e)
        {
            button1.PerformClick();
        }

        private void OnHelloWorldRequested(HelloWorldParameters parameters)
        {
            textBox1.Text += parameters.HelloWorldString + Environment.NewLine;
            textBox1.Update();
        }

        private void HelloWorldView_Load(object sender, EventArgs e)
        {
            MyHelloWorldController.HelloWorldRequestedEvent += OnHelloWorldRequested;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            MyHelloWorldController.HelloWorldRequested();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            MyHelloWorldController.IsAutomated = !MyHelloWorldController.IsAutomated;

            if (MyHelloWorldController.IsAutomated)
            {
                button2.Text = "hello world - is on";
                button2.Update();

                timer1.Interval = 2000;
                timer1.Tick += onTimerTick;
                timer1.Start();
            }
            else
            {
                timer1.Stop();
                button2.Text = "hello world - is off";
                button2.Update();
            }
        }
    }
}

5 个答案:

答案 0 :(得分:3)

WinForms使用单个消息泵线程(称为UI线程)。 (如果您不熟悉该概念,则应研究Windows消息和Windows消息泵。)

Thread.Sleep导致当前正在执行的线程暂停或暂停一段时间。这个睡眠/暂停就像死线一样 - 它什么都不知道,无法做任何事情。

由于WinForms应用程序中当前正在执行的线程通常是UI线程 - Thread.Sleep将导致UI无响应,因为它不再能够提取消息。

另一种设计是使用基于表单的Timer。将您的游戏代码放入计时器的Tick事件中。

答案 1 :(得分:1)

  

这里发生了什么导致UI无法响应,我该如何修复它以便我得到预期的行为?

基本上有两个原因导致您的应用无响应

1。 UI线程中的Thread.Sleep()

Windows上的GUI应用程序通常由发布到其上的消息(鼠标点击;键盘;屏幕绘图)驱动,这些消息放在队列中。 UI线程逐个处理这些消息,将消息分派给适当的处理程序。通过这种方式,它被称为消息泵。如果在处理这些消息中的一个消息过多时间,则UI将显示为冻结。事件处理程序应该尽可能快。

在您的点击处理程序中,您正在使用Thread.Sleep(2000);,这将阻止UI线程更新您的应用程序的UI,实质上模拟一个事务处理程序,该处理程序需要很长时间来处理事件。在UI线程上执行冗长的数据库或WCF操作也许没有什么不同,因此人们倾向于将这些调用放在单独的线程或任务上。

建议您删除Thread.Sleep并将其替换为其他人指示的计时器。

2。 button1处理程序

上的无限递归循环

第一次单击 button2 时,将调用button2的单击处理程序,其中启用了自动化。然后,您可以通过button1.PerformClick();模拟 button1

在致电button1.PerformClick期间,系统会调用 button1 button1_Click()的点击处理程序。你在那里睡了2秒钟(这对于用户界面来说并不健康),但第二个问题是你立即从 button1 点击处理程序中调用button1.PerformClick();,本质设置无限递归循环。

如果要移除Thread.Sleep(2000),您的应用最终会导致 StackOverflowException 。你现在的代码(即使是睡眠状态)仍会溢出,只是需要更长时间才能显现出来。

enter image description here

再次考虑用计时器替换它。

3。排他性

重要的是要注意暂时忽略堆栈错误,设计是这样的,即在这个无限循环运行时你的应用程序无法做任何其他事情。所以,如果你的游戏有其他按钮可点击;要显示的分数;发挥的音效;从 button2 处理程序的角度来看,很可能它永远不会发生,因为它太忙于专门处理 button1

结论

  1. 保持用户界面响应:避免在代码中使用Thread.Sleep()
  2. 避免递归:当您在所述按钮的点击处理程序内时,不要使用PerformClick()按钮

答案 2 :(得分:0)

您的“Thread.Sleep()”调用会使UI线程进入休眠状态。请改用Timer。然后在第二次按下时终止定时器。 (您也可以使用Tasks执行此操作,如果您想使用另一个线程,您需要使2个线程以某种方式进行通信,以便UI线程是唯一实际更新UI的线程)

答案 3 :(得分:0)

桌面应用程序有一个所谓的UI线程。它基本上是一个无限循环,它会不断检查是否发生了某些事情,例如鼠标点击,并在需要时重新绘制窗口。在WinAPI中进行编码,您需要自己编写此循环,WinForms和其他UI框架将其隐藏起来。但是你的点击处理程序是从这个循环内部调用的。因此,如果您的代码花费太多时间 - 例如,因为您在内部调用Thread.Sleep - 循环将不会继续,并且将无法处理应用程序中发生的任何事情。这就是为什么长时间运行的进程需要在一个单独的线程上进行的。

答案 4 :(得分:0)

正如其他人所说,您使用Thread.Sleep和递归button1.PerformClick();调用来阻止UI线程。你必须让UI尽可能自由地运行,让它快速闲置。

所以,只是为了它的乐趣,我已经重写了你的代码来做到这一点。我还使用微软的Reactive Extensions(Rx)实现了它 - 只是NuGet" Rx-WinForms"获得位。 Rx允许你做一些非常时髦的事情,你可以轻松地处理事件。

现在就是你的表格:

public partial class HelloWorldView : Form
{
    private readonly HelloWorldController MyHelloWorldController =
        new HelloWorldController("Hello world!!", TimeSpan.FromSeconds(1.0));

    public HelloWorldView()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        MyHelloWorldController.Messages
            .ObserveOn(this)
            .Subscribe(message =>
            {
                textBox1.Text += message + Environment.NewLine;
            });

        MyHelloWorldController.IsAutomateds
            .ObserveOn(this)
            .Subscribe(isAutomated =>
            {
                button2.Text = "hello world - is " + (isAutomated ? "on" : "off");
            });
    }

    private void button1_Click(object sender, EventArgs e)
    {
        MyHelloWorldController.Trigger();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        MyHelloWorldController.IsAutomated = !MyHelloWorldController.IsAutomated;
    }
}

您会注意到我已经简化了用户界面。它确实尽可能少地更新自己并通知HelloWorldController其行为。

代码最糟糕的部分是.Subscribe中的两个Form1_Load调用。这些只是查看两个observable(Rx' s版本的事件,如果你喜欢)并确保事件在带有.ObserveOn(this)调用的UI线程上运行,然后他们订阅从HelloWorldController

UI只是从控制器更新自己并告诉控制器它正在做什么。 UI中几乎没有执行任何逻辑。这就是任何MVC风格的编码应该如何。

现在HelloWorldController就是乐趣所在。

它开始很简单:

    private string _message;
    private TimeSpan _automatedPeriod;

    public HelloWorldController(string Message, TimeSpan automatedPeriod)
    {
        _message = Message;
        _automatedPeriod = automatedPeriod;
    }

这基本上是关于要发送到UI的消息以及控制器自动执行值的频率的信息。

然后跟踪它是否自动化:

    private bool _isAutomated = false;

现在它包含Rx observables - 这些就像你正在使用的事件一样。

    private Subject<string> _messages = new Subject<string>();
    public IObservable<string> Messages { get { return _messages.AsObservable(); } }

    private Subject<bool> _isAutomateds = new Subject<bool>();
    public IObservable<bool> IsAutomateds { get { return _isAutomateds.AsObservable(); } }

    private SerialDisposable _serialSubscription = new SerialDisposable();

在Rx中,我可以订阅IObservable<T>来获取一系列值 - 就像事件一样。 Subject<T>是我可以手动将值推入的内容,但它也可以是可以订阅的IObservable<T>。这一对让我举起活动。可以认为Subject<string>等同于代码中的HelloWorldRequested方法,IObservable<string>等同于HelloWorldRequestedEvent事件。

如果我致电_messages.OnNext("Hello"),那么IObservable<string> Messages的任何订阅者都会向其发送"Hello"。就像一个事件。

IsAutomated看起来像这样:

    public bool IsAutomated
    {
        get { return _isAutomated; }
        set
        {
            _isAutomated = value;
            _isAutomateds.OnNext(value);
            if (_isAutomated)
            {
                this.Trigger();
            }
        }
    }

因此它可以更新自己的内部状态,但它也会调用_isAutomateds.OnNext(value)将更新推送给IObservable<bool> IsAutomateds的任何订阅者。如果它需要触发控制器以产生this.Trigger()调用的消息,它也可以工作。

最后,Trigger方法如下所示:

    public void Trigger()
    {
        if (_isAutomated)
        {
            _serialSubscription.Disposable =
                Observable
                    .Interval(_automatedPeriod)
                    .StartWith(0)
                    .TakeUntil(_isAutomateds.Where(x => x == false))
                    .Subscribe(n => _messages.OnNext(_message));
        }
        else
        {
            _messages.OnNext(_message);
        }
    }

这方面的简单部分是当_isAutomatedfalse时,它只是通过_messages.OnNext(_message)电话发送一条消息。

_isAutomatedtrue时,它会使用Rx的一些冷静来有效地设置计时器,以便每TimeSpan _automatedPeriod生成一个值。从您的代码中每隔2秒钟就可以获得一次,因此TimeSpan将是TimeSpan.FromSeconds(2.0)

Observable.Interval(_automatedPeriod)定义一个计时器,它在第一段时间之后开始产生值,然后在每个时间段之间产生。

因此.StartWith(0)表示它应该在订阅时立即生成一个值。

.TakeUntil(_isAutomateds.Where(x => x == false))是这里最好的部分 - 它表示它将从Observable.Interval(_automatedPeriod).StartWith(0)获取值,并在从_isAutomateds.Where(x => x == false)获取值时停止 - 换句话说,当IsAutomated设置为false。

.Subscribe(n => _messages.OnNext(_message));只是将值推送到_messages主题,以便IObservable<string> Messages的所有订阅者都能收到他们的消息。

只需将我HelloWorldController中的所有public class HelloWorldController { ... }放在using中,您就可以了。

我认为它应该是的作品,并显示了UI代码的轻量级。

我希望你觉得这值得玩。

您需要将这些using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; 添加到代码顶部以获取要编译的所有代码:

_cst2