我手动推出了我想要自动播放的游戏的MVC风格实现。通过"自动播放"我的意思是通常用户在播放时点击的按钮我希望控制器自动启动。这样我就可以出于质量控制的原因观看游戏本身。这个特定的游戏有很多代码,所以我没有提供它作为一个例子,而是使用相同的方法创建了一个愚蠢的HelloWorld示例。
在我提供示例之前,这是我的问题:您在下面看到的所有内容都是正常的,并且"正常运行&#34 ;;除了一件事:我无法关闭自动播放,因为用户界面变得没有响应,关闭它的按钮不会响应点击事件。
首先在解决方案中创建.Net 4.6.1 winforms项目。 (.net版本可能并不重要,只要它是> = 4.5)。创建一个如下所示的表单:
在后面的代码中,复制粘贴:(根据需要更改名称以进行编译)
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();
}
}
}
}
答案 0 :(得分:3)
WinForms使用单个消息泵线程(称为UI线程)。 (如果您不熟悉该概念,则应研究Windows消息和Windows消息泵。)
Thread.Sleep
导致当前正在执行的线程暂停或暂停一段时间。这个睡眠/暂停就像死线一样 - 它什么都不知道,无法做任何事情。
由于WinForms应用程序中当前正在执行的线程通常是UI线程 - Thread.Sleep
将导致UI无响应,因为它不再能够提取消息。
另一种设计是使用基于表单的Timer
。将您的游戏代码放入计时器的Tick
事件中。
答案 1 :(得分:1)
这里发生了什么导致UI无法响应,我该如何修复它以便我得到预期的行为?
基本上有两个原因导致您的应用无响应。
Windows上的GUI应用程序通常由发布到其上的消息(鼠标点击;键盘;屏幕绘图)驱动,这些消息放在队列中。 UI线程逐个处理这些消息,将消息分派给适当的处理程序。通过这种方式,它被称为消息泵。如果在处理这些消息中的一个消息过多时间,则UI将显示为冻结。事件处理程序应该尽可能快。
在您的点击处理程序中,您正在使用Thread.Sleep(2000);
,这将阻止UI线程更新您的应用程序的UI,实质上模拟一个事务处理程序,该处理程序需要很长时间来处理事件。在UI线程上执行冗长的数据库或WCF操作也许没有什么不同,因此人们倾向于将这些调用放在单独的线程或任务上。
建议您删除Thread.Sleep
并将其替换为其他人指示的计时器。
第一次单击 button2 时,将调用button2的单击处理程序,其中启用了自动化。然后,您可以通过button1.PerformClick();
模拟 button1 。
在致电button1.PerformClick
期间,系统会调用 button1 button1_Click()
的点击处理程序。你在那里睡了2秒钟(这对于用户界面来说并不健康),但第二个问题是你立即从 button1 点击处理程序中调用button1.PerformClick();
,本质设置无限递归循环。
如果要移除Thread.Sleep(2000)
,您的应用最终会导致 StackOverflowException 。你现在的代码(即使是睡眠状态)仍会溢出,只是需要更长时间才能显现出来。
再次考虑用计时器替换它。
重要的是要注意暂时忽略堆栈错误,设计是这样的,即在这个无限循环运行时你的应用程序无法做任何其他事情。所以,如果你的游戏有其他按钮可点击;要显示的分数;发挥的音效;从 button2 处理程序的角度来看,很可能它永远不会发生,因为它太忙于专门处理 button1 。
Thread.Sleep()
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);
}
}
这方面的简单部分是当_isAutomated
为false
时,它只是通过_messages.OnNext(_message)
电话发送一条消息。
当_isAutomated
为true
时,它会使用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