暂停方法的执行而不锁定 GUI。 C#

发布于 2024-08-05 02:23:13 字数 658 浏览 3 评论 0原文

我正在用 C# 为我的 OOP 简介论文中的一个项目开发一款纸牌游戏,现在游戏已经可以运行了,但我正在向 GUI 添加“风格”。

目前,牌是立即发出并显示在用户界面上的。我想在发完一张牌后在发下一张牌之前必须编程暂停一会儿。

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

        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 循环的组合,但它产生了相同的效果。

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

I'm working on a card game in C# for a project on my Intro to OOP paper and have got the game working now but am adding "flair" to the GUI.

Currently cards are dealt and appear on the UI instantaneously. I want to have to program pause for a moment after dealing a card before it deals the next.

When a game is started the following code runs to populate the PictureBoxes that represent them (will be a loop eventually):

        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();
        ...

I have tries using System.Threading.Thread.Sleep(100); between each deal().show() and also inside each of those methods but all it achieves is locking up my GUI until all of the sleeps have processed then display all of the cards at once.

I have also tried using a combination of a timer and while loop but it resulted in the same effect.

What would be the best way of achieving the desired result?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

幽蝶幻影 2024-08-12 02:23:13

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

执行此操作的最佳方法是在后台线程上运行,然后 在睡眠之间调用到 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.

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

The problem is that any code that you run on the UI will block the UI and freeze the program. When your code is running (even if it's running Thread.Sleep), messages (such as Paint or Click) sent to the UI will not be processed (until control returns to the message loop when you exit your event handler), causing it to freeze.

The best way to do this is to run on a background thread, and then Invoke to the UI thread between sleeps, like this:

//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.

Alternatively, you could make a timer and show each image in the next timer tick. If you use a timer, all you should do at the beginning is start the timer; don't wait for it or you'll introduce the same problem.

与之呼应 2024-08-12 02:23:13

通常我会简单地推荐这样的函数来执行暂停,同时允许 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;
        }
     }
  }

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

这是我的替代方案。将计时器添加到表单中,并创建一个发牌者类来通过与该计时器交互来处理纸牌。设置计时器的 Interval 属性以匹配您希望发牌的时间间隔。这是我的示例代码。

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;
  }
}

Normally I'd simply recommend a function like this to perform a pause while allowing the UI to be interactive.

  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;
        }
     }
  }

But there seems to be some complication in using such a solution when it is called from within an event handler such as a button click. I think the system wants the button click event handler to return before it will continue processing other events because if I try to click again while the event handler is still running, the button depresses again even though I'm trying to drag the form and not click on the button.

So here's my alternative. Add a timer to the form and create a dealer class to handle dealing with cards by interacting with that timer. Set the Interval property of the timer to match the interval at which you want cards to be dealt. Here's my sample code.

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;
  }
}
≈。彩虹 2024-08-12 02:23:13

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

The cheap way out would be to loop with calls to Application.DoEvents() but a better alternative would be to set a System.Windows.Forms.Timer which you would stop after the first time it elapses. In either case you'll need some indicator to tell your UI event handlers to ignore input. You could even just use the timer.Enabled property for this purpose if it's simple enough.

月寒剑心 2024-08-12 02:23:13

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

I would try puting the code that deals the deck ( and calls Thread.Sleep) in another thread.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文