2009-09-14 11 views
5

Estoy trabajando en un juego de cartas en C# para un proyecto en mi introducción al documento OOP y tengo el juego funcionando ahora, pero estoy agregando "estilo" a la GUI.Pause la ejecución de un método sin bloquear la GUI. C#

Actualmente, las cartas se reparten y aparecen en la interfaz de usuario de forma instantánea. Quiero tener que programar la pausa por un momento después de repartir una carta antes de que trate la siguiente.

Cuando un juego se inicia el siguiente código se ejecuta para rellenar los PictureBoxes que los representan (será un bucle con el tiempo):

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

tengo intentos utilizando System.Threading.Thread.Sleep (100); entre cada operación(). show() y también dentro de cada uno de esos métodos, pero todo lo que logra es bloquear mi GUI hasta que se hayan procesado todas las funciones y mostrar todas las tarjetas a la vez.

También he intentado usar una combinación de un temporizador y un ciclo While pero resultó en el mismo efecto.

¿Cuál sería la mejor manera de lograr el resultado deseado?

Respuesta

17

El problema es que cualquier código que ejecute en la interfaz de usuario bloqueará la interfaz de usuario y congelará el programa. Cuando se ejecuta su código (incluso si está ejecutando Thread.Sleep), los mensajes (como Paint o Click) enviados a la IU no se procesarán (hasta que el control regrese al bucle de mensajes cuando salga de su manejador de eventos), haciendo que se congelen.

La mejor manera de hacer esto es ejecutar en un subproceso en segundo plano, y luego Invoke al hilo de interfaz de usuario entre Camas, así:

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

Alternativamente, se puede hacer un temporizador y espectáculo cada imagen en el siguiente tic del temporizador. Si usa un temporizador, todo lo que debe hacer al principio es iniciar el temporizador; no esperes o presentarás el mismo problema.

+0

Gracias, el delegado funciona muy bien! Además, nunca había pensado colocar la llamada actual en el tic del temporizador (solo estaba haciendo un escenario de conteo ++) que probablemente también sea útil en el futuro. – Windos

+0

Como expliqué, simplemente haciendo 'count ++' en el temporizador no ayudará porque el código en el ciclo while todavía estará bloqueando el subproceso de la interfaz de usuario. – SLaks

+0

Por cierto, también es posible hacer un uso indebido de los iteradores para hacer esto; si estás interesado, te lo explicaré. – SLaks

2

Intentaré poner el código que trata el mazo (y llama a Thread.Sleep) en otro hilo.

3

La salida más barata sería realizar un bucle con llamadas a Application.DoEvents() pero una mejor alternativa sería establecer un System.Windows.Forms.Timer que se detendría después de la primera vez que transcurre. En cualquier caso, necesitará algún indicador para indicarle a los manejadores de su evento de IU que ignoren la entrada. Incluso podría usar el temporizador. Para esto, habilite la propiedad si es lo suficientemente simple.

3

Normalmente, simplemente recomendaría una función como esta para realizar una pausa y permitir que la interfaz de usuario sea interactiva.

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

Pero parece que hay alguna complicación en el uso de dicha solución cuando se llama desde un controlador de eventos como un clic de botón. Creo que el sistema quiere que el manejador de eventos haga clic en botón para volver antes de que continúe procesando otros eventos porque si trato de hacer clic de nuevo mientras el controlador de eventos aún se está ejecutando, el botón vuelve a presionar aunque intento arrastrar el formulario y no haga clic en el botón.

Así que aquí está mi alternativa. Agregue un temporizador al formulario y cree una clase de crupier para manejar las cartas interactuando con ese temporizador. Establezca la propiedad Intervalo del temporizador para que coincida con el intervalo en el que desea repartir las cartas. Aquí está mi código de muestra.

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