一个秒表程序也是我的一个心病,因为一直想写这样的一个东西,但是总往GUI那边想,所以就比较怵,可能是上学的时候学MFC搞出的后遗症吧,不过当我今天想好用Win Form(话说还是第一次写win form)写这么一个东西的时候,居然so easy。
所以说,做不了不可怕,怕的是你不去做,因为你不去做,你就永远不知道你能不能做它。事实证明,大部分你犹豫能不能做的事情,实际上你都能搞定。
虽然成功实现了一个秒表的简单功能,即开始计时和停止。但是却引发了一个关于win form和C#线程的问题。
下面一个一个来,先说一下秒表的类实现
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
namespace
Utils
{
public
class
Time
{
private
int
_minute;
private
int
_second;
private
bool
_flag;
//线程标识
private
Thread _TimingThread =
null
;
public
Time()
{
this
._minute = 0;
this
._second = 0;
this
._flag =
true
;
}
/// <summary>
/// 开始计时
/// </summary>
public
void
Start()
{
if
(_TimingThread ==
null
|| !
this
._flag)
{
this
._flag =
true
;
_TimingThread =
new
Thread(
new
ThreadStart(AddSecond));
_TimingThread.Start();
}
}
/// <summary>
/// 线程执行方法
/// </summary>
private
void
AddSecond()
{
while
(_flag)
{
Thread.Sleep(1000);
if
(
this
._second == 59)
{
this
._minute++;
this
._second = 0;
}
else
{
this
._second++;
}
}
}
/// <summary>
/// 格式化显示计时结果
/// </summary>
/// <returns></returns>
public
string
FormatTimeResult()
{
string
minute =
string
.Empty;
string
second =
string
.Empty;
if
(
this
._minute < 10)
{
minute =
"0"
+
this
._minute.ToString();
}
else
{
minute =
this
._minute.ToString();
}
if
(
this
._second < 10)
{
second =
"0"
+
this
._second.ToString();
}
else
{
second =
this
._second.ToString();
}
return
minute +
":"
+ second;
}
/// <summary>
/// 停止
/// </summary>
public
void
Stop()
{
this
._flag =
false
;
}
/// <summary>
/// 归0操作
/// </summary>
public
void
Zero()
{
this
._minute = 0;
this
._second = 0;
}
}
}
|
秒表的实现还是比较简单的,感觉这样写,也方便以后做扩展。
下面说说win form方面
窗体就是这样,一个label,两个button
最开始,我写了这样一段代码
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
|
public
partial
class
Form1 : Form
{
private
Time mTime =
null
;
private
Thread mDisplayThread =
null
;
public
Form1()
{
InitializeComponent();
mTime =
new
Time();
//实例化秒表类
}
private
void
button_start_Click(
object
sender, EventArgs e)
{
mTime.Start();
mDisplayThread =
new
Thread(
new
ThreadStart(DisplayCurrentTime));
mDisplayThread.Start();
button_start.Enabled =
false
;
}
public
void
DisplayCurrentTime()
{
while
(
true
)
{
Thread.Sleep(1000);
label_Time.Text = mTime.FormatTimeResult();
Console.WriteLine(
"{0}"
, mTime.FormatTimeResult());
}
}
private
void
button_stop_Click(
object
sender, EventArgs e)
{
mTime.Stop();
button_start.Enabled =
true
;
}
}
|
这样写感觉思路上没什么问题,当点击【开始计时】按钮的同时创建一个线程,而这个线程是用来每隔一秒去更新一下label上的显示计时时间。
然而,之后却报一个这样的错误:Cross-thread operation not valid: Control 'label_Time' accessed from a thread other than the thread it was created on.
网上查了一下,这个错误貌似很常见,MSDN上也给了一个出现此错误的原因,是这样说的,当您试图从单独的线程更新一个win form时,会出现这个错误。
查了一下,就是说win form上的控件属性想要进行修改的时候,只能在创建Control的线程里调用,不能在以外的线程被调用。而上面的
1
|
label_Time.Text = mTime.FormatTimeResult();
|
这段代码呢恰恰是发生在新创建的线程之中,所以就会报错了。
解决办法是用delegate(委托)加上control.Invoke去联合实现。下面看看实现部分
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
|
public
partial
class
Form1 : Form
{
private
Time mTime =
null
;
private
Thread mDisplayThread =
null
;
public
delegate
void
UpdateLabel();
//声明一个委托
public
UpdateLabel updateLabel;
//定义一个委托
public
Form1()
{
InitializeComponent();
mTime =
new
Time();
updateLabel =
new
UpdateLabel(UpdateTime);
}
private
void
button_start_Click(
object
sender, EventArgs e)
{
mTime.Start();
mDisplayThread =
new
Thread(
new
ThreadStart(DisplayTimeFunc));
mDisplayThread.Start();
button_start.Enabled =
false
;
}
/// <summary>
/// 线程执行方法
/// </summary>
public
void
DisplayTimeFunc()
{
while
(
true
)
{
Thread.Sleep(1000);
this
.Invoke(
this
.updateLabel);
}
}
/// <summary>
/// 单独对Label进行刷新
/// </summary>
public
void
UpdateTime()
{
label_Time.Text = mTime.FormatTimeResult();
}
private
void
button_stop_Click(
object
sender, EventArgs e)
{
mTime.Stop();
button_start.Enabled =
true
;
}
}
|
这段代码里mDisplayThread线程执行了DisplayTimeFunc方法,而DisplayTimeFunc方法里实际就是在更新label,不同的是使用了Control.Invoke方法,上面不是说对控件属性的更改要在创建控件的线程里才执行吗?现在看起来好像还是老样子。那是因为我们不了解Control.Invoke是什么东东。MSDN上的解释是:在拥有此控件的基础窗口句柄的线程上执行指定的委托。OK,明白了,this.updateLabel这个委托最后还是在窗口创建的线程中执行的。
回头想想,其实思路也比较简单,就是先将更改控件属性的操作放在一个方法里,然后写个委托,再写个线程,在线程的执行方法中调用这个委托就OK啦。
不过到这还不算全完,还有一个小问题,就是当我计时之后,想要关闭这个窗体的时候,发现又开始报错了:
Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
研究了一下发现了出现此问题的原因,就是我们“上完厕所没有擦PP”,上面的代码中没有一个操作是对 mDisplayThread 这个线程做了终止的动作。
所以我们还需要添加以下动作
1
2
3
4
|
private
void
Form1_FormClosing(
object
sender, FormClosingEventArgs e)
{
mDisplayThread.Abort();
}
|
这样就完整了,在关闭Form1窗体之前,先把线程终止。
做这个小东西的时候居然连带着让我了解了一些委托和Control.Invoke以及线程的知识点。我会找个时间好好把这部分看看的,争取能总结点什么出来。