【问题标题】:Show result on screen in each FOR loop?在每个 FOR 循环的屏幕上显示结果?
【发布时间】:2011-11-16 14:47:47
【问题描述】:

我正在为飞机编写一个随机座位生成器,并在其中使用 FOR 循环。问题是,占用的座位只有在一切完成后才会显示。我想做的是在每次迭代中,显示选择的随机座位。如何做到这一点?

这是我正在使用的代码。这架飞机有 118 个座位,每个座位都有一个名为“img_Seat_X”的图片框。我知道有更好的方法,但我很快就会想到。提前致谢!

private void btn_WeightBalance_Populate_Click(object sender, EventArgs e)
{
                int passengers = Convert.ToInt32(txt_WeightBalance_Passengers.Text);
                List<int> seats = new List<int> { }; numberofSeats = 119;

                if (rdb_WeightBalance_190.Checked == true)
                    numberofSeats = 107;

                for (int x = 0; x < Passengers; x++)
                {
                    int randomNumber = RandomNumber(1, numberofSeats);

                    if (seats.Contains(randomNumber))
                        x--;

                    else
                    {

                        seats.Add(randomNumber);

                        Control[] seat = this.panWeightBalance.Controls.Find("img_Seat_" + randomNumber, true);
                        seat[0].Visible = true;
                        seat[0].Refresh();
                    }
                }
}

想通了!每次迭代中一个简单的 Refresh() 就可以完成这项工作!我还用 if 语句替换了 while 循环。

【问题讨论】:

  • 我建议以正常的 .Net 方式以小写字母开头。变量名并不表示它们包含的内容。 Passengers 是一个数字,但顾名思义,它指向代表一群人的类。这让代码有点混乱。
  • 如果您的座位分配算法花费的时间超过一毫秒,那么您做错了什么。让不感兴趣的用户知道你的算法有多聪明是一个错误。他们真的不太在乎,他们只想知道在哪里坐下。不要优化不需要的代码。

标签: c# loops for-loop iteration


【解决方案1】:

一、必读:The Windows Message Loop

@rice 指出了我明显的错误,抱歉让你走错了路,谢谢rice。

无论如何,您可以在单独的线程中执行工作,并使用BackgroundWorker 类将更新发布到 UI trhead。下面是一个简单的例子,它更新表单上的标签 100 次以响应按钮点击:

public partial class Form1 : Form
{
    BackgroundWorker _worker;
    public Form1()
    {
        InitializeComponent();
        _worker = new BackgroundWorker();
        _worker.WorkerReportsProgress = true;
        _worker.DoWork += _worker_DoWork;
        _worker.ProgressChanged += _worker_ProgressChanged;
    }

    private void _worker_ProgressChanged( object sender, ProgressChangedEventArgs e )
    {
        label1.Text = e.UserState.ToString();
    }

    private void _worker_DoWork( object sender, DoWorkEventArgs e )
    {
        for( int i = 0; i < 100; ++i )
        {
            _worker.ReportProgress( i, i );
            // allow some time between each update,
            // for demonstration purposes only.
            System.Threading.Thread.Sleep( 15 );
        }
    }   

    private void button1_Click( object sender, EventArgs e )
    {
        _worker.RunWorkerAsync();
    }
}

【讨论】:

  • 是的,问题中的代码将导致绘制操作被发布到 Windows 消息队列,但是由于 onclick 事件正忙于运行您的算法,它永远不会真正开始绘制,直到您的整个功能结束。这就是为什么即使您的代码看起来像是在循环中更新了 UI,但实际上并没有。它仅更新 UI 片段的状态,但不会逐字呈现它们。一定要阅读上面的链接,了解主 UI 线程的特殊性质是一个突破性的时刻。
  • @Ed S. 它仍在阻止消息泵,可能在 Invalidate() 期间让消息通过。这仍然是一个不正确的答案,尤其是在消息循环期间抛出 Sleep()。
  • @rice:Der,你是对的;自从我显然不得不在 Windows UI 中工作以来已经有一段时间了......你需要强制更新消息循环,即 DoEvents 才能这样做。
  • @Andrew:是的,是的,不知道我在想什么。
  • @rice:BackgroundWorker 封装了一些远离你的东西。由于事件处理程序在 UI 线程上运行,因此您永远不会因为从未在其上创建的线程访问控件而获得异常。如果您使用线程或其他类似的东西,您将需要 Invoke() 在 UI 线程上异步更新 UI 的委托。如果我没记错的话,在 .NET 1.1 天从另一个线程访问控件不会导致异常,但现在肯定会,而且总的来说这是一个坏主意
【解决方案2】:

基本上您需要生成一个后台线程来进行处理,然后每次使用BeginInvoke 来更新用户界面元素。在此解决方案中,您需要解决一些问题。用户现在可以继续单击该按钮,它将产生额外的后台线程。最常见的机制是弹出一个进度对话框(我讨厌模态对话框,所以不要这样做)或通过禁用按钮来阻止用户执行两次操作,直到工作完成。

    private void btn_WeightBalance_Populate_Click(object sender, EventArgs e)
    {
        int passengers = Convert.ToInt32(txt_WeightBalance_Passengers.Text);

        List<int> seats = new List<int> { }; numberofSeats = 119;

        if (rdb_WeightBalance_190.Checked == true)
            numberofSeats = 107;

        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += delegate
        {
            for (int x = 0; x < passengers; x++)
            {
                int randomNumber = RandomNumber(1, numberofSeats);
                while (seats.Contains(randomNumber))
                {
                    randomNumber = RandomNumber(1, numberofSeats);
                }
                seats.Add(randomNumber);

                UpdateSeat(randomNumber);
            }
        };
        worker.RunWorkerAsync();
    }

    /// <summary>
    /// Update a seat control in the correct UI thread. If this
    /// method is invoked in a thread besides the UI thread it will use
    /// BeginInvoke to put it on the UI thread queue.
    /// </summary>
    /// <param name="seatNumber"></param>
    private void UpdateSeat (int seatNumber)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke((Action)(() => UpdateSeat(seatNumber)));
        }
        else
        {
            Control[] seat = this.Controls.Find("img_Seat_" + seatNumber, true);
            seat[0].Visible = true;
        }
    }

还有其他问题需要解决。例如假设一个控件在调用FindControl 时总是存在并且不处理txt_WeightBalance_Passengers 中的格式错误的数字文本。

更新为“可爱”的答案

    private void btn_WeightBalance_Populate_Click(object sender, EventArgs e)
    {
        var passengers = Convert.ToInt32(txt_WeightBalance_Passengers.Text);
        numberofSeats = rdb_WeightBalance_190.Checked ? 107 : 119;
        var worker = new BackgroundWorker();
        worker.DoWork += delegate
        {
            var random = new Random();
            foreach (var seatNumber in Enumerable.Range(1, Int32.MaxValue).Select(r => random.Next(numberofSeats)).Distinct())
            {
                var randomSeat = seatNumber;
                BeginInvoke((Action)(() =>
                {
                    var seat = this.Controls.Find("img_Seat_" + randomSeat, true);
                    seat[0].Visible = true;
                }));                    
                if (--passengers <= 0) break;
            }
        };
        worker.RunWorkerAsync();
    }

【讨论】:

    【解决方案3】:

    我不会得到一个随机数,而是检查它是否被占用然后分配它,我会拿一个列表1 thru numberOfSeats 并随机播放它并开始按这个随机的随机顺序分配座位。这样可以避免恶性循环while (Seats.Contains(randomNumber)){...}

    【讨论】:

    • 实际上,与其改组,我只使用 1 之间的随机数,不管剩下多少座位,然后我会使用一个包含每个座位号的列表,然后删除任何随机座位被删除,直到座位 == 0
    【解决方案4】:

    您正在编写的代码仅从最多 119 个席位中选择随机席位。即使您使用的是旧硬件,它的运行速度也应该非常快,所以我不明白为什么您需要在分配时显示每个座位。在我看来,您应该拆分代码。计算座位分配,然后以您喜欢的方式显示它们。

    这是随机选择座位的代码:

    var passengers = Convert.ToInt32(txt_WeightBalance_Passengers.Text);
    var numberofSeats = rdb_WeightBalance_190.Checked ? 107 : 119;
    
    // Creates an array from 0 .. numberofSeats - 1
    var seats = Enumerable.Range(0, numberofSeats).ToArray();
    
    //Shuffle the first "passengers" elements of the array
    for (var i = 0; i < passengers; i++)
    {
        var j = RandomNumber(0, numberofSeats);
        var x = seats[i];
        seats[i] = seats[j];
        seats[j] = x;
    }
    
    //Find the first "passengers" count of seat controls
    var controls = (
            from i in seats.Take(passengers)
            let c = this.Controls.Find("img_Seat_" + i, true).FirstOrDefault()
            where c != null
            select c
        ).ToArray();
    

    现在要将座椅控件设置为可见,只需执行以下操作:

    foreach (var c in controls)
    {
        c.Visible = true;
    }
    

    如果您有特定的需要在后台运行它并更新 UI,您可以这样做:

    var t = new System.Threading.Thread(new ThreadStart(() =>
    {
        foreach (var c in controls)
        {
            this.Invoke(new Action(() => c.Visible = true));
            // Thread.Sleep(100); // Slow it down if you wish...
        }
    }));
    t.Start();
    

    这对你有用吗?

    【讨论】:

      【解决方案5】:

      问题是,占用的座位只有在一切完成后才会显示。

      查看 for 循环中的 while 循环是预期的:

      while (Seats.Contains(randomNumber))
      {
          randomNumber = RandomNumber(1, numberofSeats);
      }
      Seats.Add(randomNumber); 
      

      您可能忘记将“Add”方法放入您的 while 循环中。还要检查退出条件——我猜你想在 Contains 返回 true 时退出 while 循环。

      【讨论】:

      • 它更微妙。他想要 n 个唯一个随机数。这就是循环存在的原因。
      猜你喜欢
      • 2021-10-09
      • 2020-06-09
      • 2017-01-04
      • 2015-02-10
      • 2015-03-09
      • 2017-04-30
      • 2013-06-27
      • 2013-01-22
      • 2015-06-17
      相关资源
      最近更新 更多