5

我正在 C# 中为我的 Intro to OOP 论文上的一个项目开发纸牌游戏,并且现在已经让游戏正常工作,但我正在向 GUI 添加“天赋”。

目前卡片已发并立即出现在 UI 上。我想在发下一张牌后暂停片刻,然后再发下一张。

当游戏开始时,会运行以下代码来填充代表它们的图片框(最终将是一个循环):

        cardImage1.Image = playDeck.deal().show();
        cardImage2.Image = playDeck.deal().show();
        cardImage3.Image = playDeck.deal().show();
        cardImage4.Image = playDeck.deal().show();
        cardImage5.Image = playDeck.deal().show();
        ...

我尝试使用 System.Threading.Thread.Sleep(100); 在每个 deal().show() 之间以及在每个方法内部,但它所实现的只是锁定我的 GUI,直到所有睡眠都处理完毕,然后一次显示所有卡片。

我也尝试过结合使用计时器和 while 循环,但效果相同。

实现预期结果的最佳方法是什么?

4

4 回答 4

17

问题是您在 UI 上运行的任何代码都会阻塞 UI 并冻结程序。当您的代码正在运行时(即使它正在运行Thread.Sleep),发送到 UI 的消息(例如 Paint 或 Click)将不会被处理(直到您退出事件处理程序时控制返回到消息循环),导致它冻结。

最好的方法是在后台线程上运行,然后Invoke在睡眠之间运行到 UI 线程,如下所示:

//From the UI thread,
ThreadPool.QueueUserWorkItem(delegate {
    //This code runs on a backround thread.
    //It will not block the UI.
    //However, you can't manipulate the UI from here.
    //Instead, call Invoke.
    Invoke(new Action(delegate { cardImage1.Image = playDeck.deal().show(); }));
    Thread.Sleep(100);

    Invoke(new Action(delegate { cardImage2.Image = playDeck.deal().show(); }));
    Thread.Sleep(100);

    Invoke(new Action(delegate { cardImage3.Image = playDeck.deal().show(); }));
    Thread.Sleep(100);

    //etc...
});
//The UI thread will continue while the delegate runs in the background.

或者,您可以制作一个计时器并在下一个计时器滴答声中显示每个图像。如果你使用定时器,一开始你应该做的就是启动定时器;不要等待它,否则你会引入同样的问题。

于 2009-09-14T01:53:06.183 回答
3

通常我会简单地推荐一个这样的函数来执行暂停,同时允许 UI 是交互式的。

  private void InteractivePause(TimeSpan length)
  {
     DateTime start = DateTime.Now;
     TimeSpan restTime = new TimeSpan(200000); // 20 milliseconds
     while(true)
     {
        System.Windows.Forms.Application.DoEvents();
        TimeSpan remainingTime = start.Add(length).Subtract(DateTime.Now);
        if (remainingTime > restTime)
        {
           System.Diagnostics.Debug.WriteLine(string.Format("1: {0}", remainingTime));
           // Wait an insignificant amount of time so that the
           // CPU usage doesn't hit the roof while we wait.
           System.Threading.Thread.Sleep(restTime);
        }
        else
        {
           System.Diagnostics.Debug.WriteLine(string.Format("2: {0}", remainingTime));
           if (remainingTime.Ticks > 0)
              System.Threading.Thread.Sleep(remainingTime);
           break;
        }
     }
  }

但是,当从事件处理程序(例如按钮单击)中调用这种解决方案时,使用这种解决方案似乎有些复杂。我认为系统希望按钮单击事件处理程序在继续处理其他事件之前返回,因为如果我在事件处理程序仍在运行时再次尝试单击,即使我试图拖动表单而不是点击按钮。

所以这是我的选择。在表单中添加一个计时器并创建一个经销商类,通过与该计时器交互来处理卡片。设置计时器的时间间隔属性以匹配您希望发牌的时间间隔。这是我的示例代码。

public partial class Form1 : Form
{

  CardDealer dealer;

  public Form1()
  {
     InitializeComponent();
     dealer = new CardDealer(timer1);
  }

  private void button1_Click(object sender, EventArgs e)
  {
     dealer.QueueCard(img1, cardImage1);
     dealer.QueueCard(img2, cardImage2);
     dealer.QueueCard(img3, cardImage1);
  }
}

class CardDealer
{
  // A queue of pairs in which the first value represents
  // the slot where the card will go, and the second is
  // a reference to the image that will appear there.
  Queue<KeyValuePair<Label, Image>> cardsToDeal;
  System.Windows.Forms.Timer dealTimer;

  public CardDealer(System.Windows.Forms.Timer dealTimer)
  {
     cardsToDeal = new Queue<KeyValuePair<Label, Image>>();
     dealTimer.Tick += new EventHandler(dealTimer_Tick);
     this.dealTimer = dealTimer;
  }

  void dealTimer_Tick(object sender, EventArgs e)
  {
     KeyValuePair<Label, Image> cardInfo = cardInfo = cardsToDeal.Dequeue();
     cardInfo.Key.Image = cardInfo.Value;
     if (cardsToDeal.Count <= 0)
        dealTimer.Enabled = false;
  }

  public void QueueCard(Label slot, Image card)
  {
     cardsToDeal.Enqueue(new KeyValuePair<Label, Image>(slot, card));
     dealTimer.Enabled = true;
  }
}
于 2009-09-14T02:45:35.867 回答
3

便宜的出路是循环调用 Application.DoEvents() 但更好的选择是设置一个 System.Windows.Forms.Timer ,您将在第一次过去后停止。无论哪种情况,您都需要一些指示器来告诉您的 UI 事件处理程序忽略输入。如果足够简单,您甚至可以为此目的使用 timer.Enabled 属性。

于 2009-09-14T01:53:52.470 回答
2

我会尝试将处理甲板(并调用 Thread.Sleep)的代码放在另一个线程中。

于 2009-09-14T01:53:48.850 回答