关于.net:C#WinForms表单计时器未在循环内滴答

C# WinForms forms timer not ticking inside of loop

我创建了一个C#项目,一个带有背景类的WinForms应用程序。 WinForms App类包含一个点,并在每次调用" Refresh()"方法时绘制该点。按下空格键,即可运行背景类。后台类提供了一个" run()"方法,该方法将计算方法调用特定次数(while循环)。然后,该计算方法将执行一些计算,并将结果添加到堆栈中,然后启动计时器。此计时器包含一个draw方法,该方法从堆栈中获取数据,并告诉Form为每个堆栈条目打印一个圆圈。 (下面描述代码非常令人困惑,应该很容易理解)

现在我遇到了一个我不明白的非常奇怪的问题:(请参见下面的示例代码)
在后台类中,当while循环调用计算时,将完成计算,但时间不会执行draw方法。但是,当您注释掉while循环的开始和结束并改为手动多次按空格键时,一切正常。为什么代码在没有循环的情况下可以正常工作,而手动按下多次却在while循环中却不能正常工作?以及如何解决它才能使其与循环配合使用?

重现此问题的步骤:
在Visual Studio中创建一个新的WinForms App,用下面的代码替换Form1.cs并用下面的代码创建BackgroundRunner.cs类。
然后只需运行项目并按空格键即可运行背景类。
要使代码正常工作:
只需注释掉while循环的开始和结束,然后手动多次按空格键即可。 (圆圈大喊大叫)

其他:
是的,在这个分解的示例中,创建两个单独的类和一个表单计时器看起来很奇怪,但是在整个项目中,我需要使它以这种方式工作。

预先感谢。

我已经在Google上搜索了,发现计时器和UI之类的解决方案需要在同一线程上,但是我认为它们都在同一线程上,因为它没有循环。

这是Form1.cs类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestApp
{
    public partial class Form1 : Form
    {

        private Point p;
        private BackgroundRunner runner;
        private int count = 0;
        private int max = 10;

        public Form1() {
            InitializeComponent();
            p = new Point(0, 0);
            runner = new BackgroundRunner(this);
        }

        private void Form1_Load(object sender, EventArgs e) {

        }

        //Method to refresh the form (draws the single point)
        override
        protected void OnPaint(PaintEventArgs e) {
            drawPoint(p, e.Graphics);
        }

        //Method to draw a point
        private void drawPoint(Point p, Graphics g) {
            Brush b = new SolidBrush(Color.Black);
            g.FillEllipse(b, p.X, p.Y, 10, 10);
        }

        //when you press the space bar the runner will start
        override
        protected void OnKeyDown(KeyEventArgs e) {
            if(e.KeyValue == 32) {
                runner.run();
            }
        }

        public int getCount() {
            return count;
        }

        public int getMax() {
            return max;
        }

        //sets point, refreshes the form and increases the count
        public void setPoint(Point pnew) {
            p = pnew;
            Refresh();
            count++;
        }
    }
}

这是BackgroundRunner.cs类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TestApp
{
    class BackgroundRunner
    {
        private Form1 form;
        private System.Windows.Forms.Timer timer;
        private Stack<int> test;
        private int max;
        private int count;

        public BackgroundRunner(Form1 formtemp) {
            form = formtemp;
            timer = new System.Windows.Forms.Timer();
            timer.Interval = 100;
            timer.Tick += drawMovement;
        }

        public void run() {
            count = form.getCount();
            max = form.getMax();

            //Here happens the strange stuff:
            //If you comment out the while loop start and end
            //and press manually the space bar the circle will move
            //from right to left an up to down.
            //But if you use the while loop nothing will happen
            //and the"drawMovement" Method will never be executed,
            //because the message inside will never be written...
            Console.WriteLine("Count:" + count +" Max:" + max);
            while (count < max) {
                Console.WriteLine("Calc Move");
                calculateMovement();

                count = form.getCount();
                max = form.getMax();
                Thread.Sleep(50);
            }
        }

        //Just a method that adds data to the stack and then start the timer
        private void calculateMovement() {
            test = new Stack<int>(5);
            test.Push(10);
            test.Push(20);
            test.Push(30);
            test.Push(40);
            test.Push(50);

            timer.Start();
        }

        //Method that goes through the stack and updates
        //the forms point incl refresh
        private void drawMovement(object o, EventArgs e) {
            Console.WriteLine("Draw Move");
            if (test.Count <= 0) {
                timer.Stop();
                return;
            }
            int x = test.Pop();
            int y = count;
            form.setPoint(new System.Drawing.Point(x, y));
        }


    }
}


从不阻塞发生消息循环的主线程。

计时器在消息转移中触发,这就是为什么您在通过循环和睡眠阻塞主线程时未触发计时器的原因。

通过将public void run()更改为public async void run()

await Task.Delay替换Threed.Sleep,然后循环将异步工作。

(如果您的应用程序针对.NET Framewok 2.0版或更高版本),这可能会引发异常,因为它不是线程安全的。
要解决此问题,请将所有访问UI控件的代码(可能只是抛出异常的位置,以防万一您不确定它们是什么)。

1
2
3
form.Invoke(new Action(()=>{
    //Statements to access the UI controls
}));

更正

The await will return execution on the UI thread because of the SynchronizationContext it is posted to. So there won't be exception about crossthreading.

by J.vanLangen