Приостановить выполнение метода без блокировки графического интерфейса. C # - PullRequest
5 голосов
/ 14 сентября 2009

Я работаю над карточной игрой на C # для проекта на моем вступлении к статье ООП, и сейчас игра работает, но я добавляю "талант" в 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); между каждой сделкой (). show (), а также внутри каждого из этих методов, но все, чего он добивается, это блокирует мой графический интерфейс, пока не будут обработаны все сны, а затем отобразятся все карты одновременно.

Я также пытался использовать комбинацию таймера и цикла while, но это дало тот же эффект.

Как лучше всего достичь желаемого результата?

Ответы [ 4 ]

17 голосов
/ 14 сентября 2009

Проблема в том, что любой код, который вы запускаете в пользовательском интерфейсе, блокирует пользовательский интерфейс и останавливает программу. Когда ваш код работает (даже если он работает Thread.Sleep), сообщения (такие как Paint или Click), отправленные в пользовательский интерфейс, не будут обрабатываться (пока элемент управления не вернется в цикл обработки сообщений, когда вы выйдете из обработчика событий), заставив замораживать.

Лучший способ сделать это - запустить в фоновом потоке, а затем Invoke в потоке пользовательского интерфейса между снами, например:

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

Кроме того, вы можете сделать таймер и показать каждое изображение в следующем таймере. Если вы используете таймер, все, что вам нужно сделать в начале, это запустить таймер; не ждите этого, иначе вы столкнетесь с той же проблемой.

3 голосов
/ 14 сентября 2009

Обычно я просто рекомендую функцию, подобную этой, чтобы сделать паузу, в то же время позволяя интерфейсу быть интерактивным.

  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;
  }
}
3 голосов
/ 14 сентября 2009

Дешевым выходом было бы зацикливаться на вызовах Application.DoEvents (), но лучшей альтернативой было бы установить System.Windows.Forms.Timer, который вы бы остановили после того, как он истечет в первый раз. В любом случае вам понадобится какой-нибудь индикатор, чтобы сообщить обработчикам событий пользовательского интерфейса игнорировать ввод. Для этого вы можете просто использовать свойство timer.Enabled, если оно достаточно простое.

2 голосов
/ 14 сентября 2009

Я бы попытался поместить код, который использует колоду (и вызывает Thread.Sleep), в другой поток.

...