在开发WinForm窗体程序时,我们希望增加一个对DataGridView数据进行查找的对话框,类似于Visual Studio中的“查找和替换”对话框,但是功能没有这么复杂,需求如下:
1. 用户可以通过主窗体中的菜单打开数据查找对话框。
2. DataGridView数据未加载前不显示查找对话框。
3. 查找对话框中可以进行大小写匹配和全字匹配。
4. 查找对话框以非模式对话框的形式显示在主窗体的上面。
5. DataGridView中高亮显示被查找到的关键字所在的行。
6. 用户可以在查找对话框中DataGridView中的数据进行循环查找,即用户每进行一次查找,DataGridView都将从上一次查找到的位置开始向下进行查找直到最后一行,然后再从第一行开始继续查找。
7. 可对DataGridView进行逐行逐列查找。
对DataGridView进行逐行逐列的遍历并匹配关键字然后高亮显示当前行,这个功能实现起来应该没有什么难度,关键在于如何实现循环查找,并且能够很好地与子窗体(查找对话框)进行互动。另外就是需要实现大小写匹配和圈子匹配,这里需要使用到正则表达式。我们先看一下程序的主界面。
主窗体的实现我在这里不具体介绍了,这不是本文的重点,况且上面这个程序截图中还实现了许多其它的功能。我在这里主要介绍一下子窗体的功能以及如何实现DataGridView数据的循环查找。
先来看一下如何打造一个相对美观的查找对话框
如上图,你可以将用于设置查询参数部分的控件(Match case,Match whole word)放到一个布局控件中,如GroupBox。这样界面看起来会比较专业一些。然后你还需要对子窗体进行一些参数设置,使其看起来更像一个对话框。
FormBorderStyle: FixedDialog
Text: Find Record
Name: FindRecord
StartPosition: CenterScreen
AcceptButton: btFindNext (Find Next按钮)
CancelButton: btCancel (Cancel按钮)
MaximizeBox: False
MinimizeBox: False
ShowIcon: False
ShowInTaskbar: False
TopMost: True
给对话框增加一些功能
首先对话框应该是在全局有效的,否则我们就不能记录每一次查找后DataGridView中被命中的记录的Index。所以对话框窗体的实例应该是在主窗体中被初始化,并且只被实例化一次。每次打开对话框时只是调用实例的Show()方法,关闭对话框时只调用窗体的Hide()方法而不是Close()方法,因为Close()方法会将窗体的实例在内存中注销掉。那么我们需要定义btCancel按钮的事件和重写窗体的FormClosing事件并在其中调用窗体的Hide()方法。
查询参数中的大小写匹配和全字匹配都是复选框控件,这意味着参数会有多种组合方式,不妨将这些组合定义成一个枚举,一共是四种情况:任意匹配(None),大小写匹配(MatchCase),全字匹配(MatchWholeCase),大小写和全字匹配(MatchCaseAndWholeWord)。
以事件模型来实现数据查找功能在这里再好不过了。首先需要在查询对话框中定义一个EventHandler,然后在主窗体中订阅这个事件,事件的执行代码写到子窗体的btFindNext按钮的事件中,一共传递三个参数:查询内容,DataGridView的当前行号(用于定位下一次查找),以及查询参数枚举变量。下面是子窗体的具体实现代码:
1
using
System;
2
using
System.Collections.Generic;
3
using
System.ComponentModel;
4
using
System.Data;
5
using
System.Drawing;
6
using
System.Linq;
7
using
System.Text;
8
using
System.Windows.Forms;
9
10
namespace
ListItemEditor.UI
11
{
12
public
partial
class
FindRecord : Form
13
{
14
public
EventHandler
<
FindRecordWindowEventArgs
>
OnFindClick
=
null
;
15
public
enum
FindOptions { None, MatchCase, MatchWholeWord, MatchCaseAndWholeWord }
16
public
int
CurrentIndex
=
-
1
;
17
18
public
FindRecord()
19
{
20
InitializeComponent();
21
}
22
23
private
void
btCancel_Click(
object
sender, EventArgs e)
24
{
25
this
.Hide();
26
}
27
28
private
void
FindRecord_FormClosing(
object
sender, FormClosingEventArgs e)
29
{
30
this
.Hide();
31
e.Cancel
=
true
;
32
}
33
34
private
void
btFindNext_Click(
object
sender, EventArgs e)
35
{
36
if
(
this
.tbFindTxt.Text.Trim().Length
>
0
)
37
{
38
FindOptions options
=
FindOptions.None;
39
if
(
this
.chbMatchCase.Checked
&&
this
.chbMatchWholeWord.Checked)
40
{
41
options
=
FindOptions.MatchCaseAndWholeWord;
42
}
43
else
if
(
this
.chbMatchCase.Checked
&&
!
this
.chbMatchWholeWord.Checked)
44
{
45
options
=
FindOptions.MatchCase;
46
}
47
else
if
(
!
this
.chbMatchCase.Checked
&&
this
.chbMatchWholeWord.Checked)
48
{
49
options
=
FindOptions.MatchWholeWord;
50
}
51
else
52
{
53
options
=
FindOptions.None;
54
}
55
OnFindClick(
this
,
new
FindRecordWindowEventArgs(
this
.tbFindTxt.Text, CurrentIndex, options));
56
}
57
}
58
}
59
60
public
class
FindRecordWindowEventArgs : EventArgs
61
{
62
private
string
sFindTxt;
63
private
int
iIndex
=
0
;
64
private
FindRecord.FindOptions findOptions;
65
66
public
string
FindTxt
67
{
68
get
{
return
this
.sFindTxt; }
69
}
70
71
public
int
Index
72
{
73
get
{
return
this
.iIndex; }
74
}
75
76
public
FindRecord.FindOptions FindOptions
77
{
78
get
{
return
this
.findOptions; }
79
}
80
81
public
FindRecordWindowEventArgs(
string
_findTxt,
int
_index, FindRecord.FindOptions _options)
82
{
83
this
.sFindTxt
=
_findTxt;
84
this
.iIndex
=
_index;
85
this
.findOptions
=
_options;
86
}
87
}
88
}
主窗体做了什么
首先我们需要在主窗体中实例化子窗体并定义查询事件,因此下面这几行代码是必须的:
1
public
partial
class
Form1 : Form
2
{
3
private
FindRecord winFind
=
new
FindRecord();
4
5
public
Form1()
6
{
7
InitializeComponent();
8
9
this
.winFind.OnFindClick
+=
new
EventHandler
<
FindRecordWindowEventArgs
>
(
this
.winFind_OnFindClick);
10
}
11
}
FindRecord即子窗体所在的类。下面是具体的数据查询实现及菜单响应代码:
1
private
void
tlbFind_Click(
object
sender, EventArgs e)
2
{
3
if
(
!
this
.DataLoaded)
return
;
4
winFind.Show();
5
}
6
7
private
void
Form1_KeyDown(
object
sender, KeyEventArgs e)
8
{
9
if
(
!
this
.DataLoaded)
return
;
10
if
(e.Modifiers
==
Keys.Control
&&
e.KeyCode
==
Keys.F)
11
{
12
tlbFind.PerformClick();
13
}
14
}
15
16
private
void
winFind_OnFindClick(
object
sender, FindRecordWindowEventArgs e)
17
{
18
string
s
=
e.FindTxt;
19
int
index
=
e.Index;
20
bool
bFind
=
false
;
21
22
RegexOptions regOptions
=
RegexOptions.IgnoreCase;
23
string
pattern
=
Regex.Escape(s);
24
25
if
(e.FindOptions
==
FindRecord.FindOptions.MatchCase
||
e.FindOptions
==
FindRecord.FindOptions.MatchCaseAndWholeWord)
26
{
27
regOptions
=
RegexOptions.None;
28
}
29
30
if
(e.FindOptions
==
FindRecord.FindOptions.MatchWholeWord
||
e.FindOptions
==
FindRecord.FindOptions.MatchCaseAndWholeWord)
31
{
32
pattern
=
"
\\b
"
+
pattern
+
"\\b"
;
33
}
34
35
foreach
(DataGridViewRow row
in
theGrid.Rows)
36
{
37
this
.winFind.CurrentIndex
=
row.Index;
38
foreach
(DataGridViewCell cel
in
row.Cells)
39
{
40
//
if (cel.Value.ToString().Contains(s))
41
if
(Regex.IsMatch(cel.Value.ToString(), pattern, regOptions))
42
{
43
bFind
=
true
;
44
if
(cel.RowIndex
>
index)
45
{
46
this
.theGrid.ClearSelection();
47
this
.theGrid.Rows[cel.RowIndex].Selected
=
true
;
48
return
;
49
}
50
}
51
}
52
}
53
54
if
(
this
.winFind.CurrentIndex
==
this
.theGrid.Rows.Count
-
1
&&
bFind)
55
{
56
this
.winFind.CurrentIndex
=
-
1
;
57
MessageBox.Show(
"
Find the last record.
"
,
"
List Item Editor
"
, MessageBoxButtons.OK, MessageBoxIcon.Information);
58
return
;
59
}
60
61
if
(
!
bFind)
62
{
63
this
.winFind.CurrentIndex
=
-
1
;
64
MessageBox.Show(
string
.Format(
"
The following specified text was not found:\r\n{0}
"
, s),
"
List Item Editor
"
, MessageBoxButtons.OK, MessageBoxIcon.Information);
65
}
66
}
tlbFind_Click是菜单点击事件,在显示子窗体前我们需要通过DataLoaded变量来判断DataGridView是否已经完成数据加载了,这是一个布尔变量,在主窗体中定义的私有变量。Form1_KeyDown事件用来响应Ctrl + F快捷键,如果DataGridView已经完成数据加载并且用户使用了键盘上的Ctrl + F组合键,则执行与tblFind_Click事件相同的操作,这是通过tlbFind.PerformClick()这条语句来完成的。
winFind_OnFindClick事件实现了具体的数据查询操作,这个事件是子窗体数据查询EventHandler的具体实现。还记得前面提到过的这个吗?我们在子窗体的这个EventHandler中定义了三个参数,用来传递要查询的内容,以及DataGridView的行号和查询参数枚举值。现在在主窗体的这个事件函数中可以通过对象e来获取到这些值。代码中通过两个foreach语句来逐行逐列遍历DataGridView,字符串匹配操作使用了正则表达式,根据查询参数中的枚举值来使用不同的正则表达式匹配项:
1. 默认情况下正则表达式匹配项被设置成了大小写敏感(RegexOptions.IgnoreCase)
2. 如果用户在子窗体中选择了大小写匹配,则将正则表达式匹配项修改成None(RegexOptions.None)
3. 如果用户在子窗体中选择了全字匹配,则使用自定义的正则表达式进行匹配。在正则表达式中,'\b'用来判断单词边界,而'\B'用来判断非单词边界。有关如何使用正则表达式进行全字匹配可以参考下这里的一篇文章。
http://answers.oreilly.com/topic/217-how-to-match-whole-words-with-a-regular-expression/
正则表达式30分钟入门教程也有关于如何使用\b和\B的介绍,并且描述简单明了。
子窗体中还有一个公共整型变量CurrentIndex,主窗体在遍历DataGridView的同时会修改这个值,将DataGridView的当前行号传递回子窗体,当用户下一次进行查询时,子窗体又会将这个行号传回到主窗体中。你应该已经注意到了在内层的foreach循环语句中有一个判断,如果命中的DataGridView行的行号小于CurrentIndex值,则继续向下查找,直到找到下一个匹配的行,且这个行号要大于CurrentIndex值。如果已经找到DataGridView的最后一行则弹出一个提示信息。bFind布尔变量用于指示是否已经找到匹配的值,如果没有找到,则在程序的最后会弹出一个提示信息。
好了,程序的所有核心实现都在这里了。其实就是使用了一点小技巧,再就是子窗体通过事件模型去驱动主窗体的数据查询功能,这比直接在子窗体中定义一个public类型的方法要优雅得多,因为这样做避免了在不同的窗体间传递参数的麻烦,代码更加简洁!