窗体上的多个 DataGridView






4.67/5 (5投票s)
这是一个在 Windows 窗体上使用 C++/CLI 编写的三个 DataGridViews 的工作示例,其中第二个和第三个网格的值在执行期间会发生变化。
目标
本文旨在提供一个 C++/CLI 中带有多个 DataGridView
s 的窗体的实际工作示例,其中第二个及后续网格的内容受前一个网格中选定值的影响。
背景
CodeProject 上的 db_developer 曾提供过为数不多的在线示例之一,但我发现难以理解。我将针对 C++/CLI 用户群尝试提供我的努力。
示例起源
该示例改编自我正在开发的票务系统中用户管理区域。
其理念是,一个用户名在主网格中关联一个人,然后在第二个网格中,该用户可以被赋予任意数量的角色,而第三个网格显示与每个角色关联的系统功能。
系统中包含所有角色、所有用户、所有功能、角色-功能映射表和用户-角色映射表的表。在此示例中,将使用硬编码的示例数据来模拟从这些表中读取数据,这样您就不必拥有特定的数据库即可遵循该示例。
用户界面
UI 使用 Visual Studio 中的新 Windows 窗体应用程序项目进行设计。窗体上的控件是三个 DataGridView
s,分别称为 gridUser
、gridUserRoles
和 gridFeatures
,它们通过 IDE 从工具箱拖放到窗体上,另外还有五个标签和一个退出按钮。gridUser
DataGridView
有四列,两列为文本字段,dgUsername
和 dgPassword
,后续两列均为 DataGridViewComboBoxColumn
类型,dgNumber
和 dgPerson
。gridUserRoles DataGridView
只有一列,即一个名为 dgRoles
的 DataGridViewComboBoxColumn
。gridFeatures DataGridView
也只有一列,即一个名为 dgFeatureName
的文本字段。五个标签中有三个是重要的——它们是 lblPersonID
、lblPersonName
和 lblUsername
。我将使用它们来显示更改用户的影响。该示例使用五个小型程序集来提供数据——这在文本中会进一步解释。所有这些都编译到同一个解决方案中,并且必须为它们添加程序集引用,如下所示。
重要声明
这是我添加到 form1.h 以完成任务的特定声明列表。这些声明大多是我实现的特定内容。唯一我认为必不可少的是将绑定到每个网格的 DataTables
。
DataTable ^dtPeople;
DataSet ^dsPeople;
BindingSource ^bsPeople;
DataTable ^dtViewUserData;
List<CUserMaster^>^ UserList;
DataTable ^dtRoles;
DataSet ^dsRoles;
BindingSource ^bsRoles;
DataTable ^dtViewUserRoles;
List<CUserRole^>^UserRoleList;
DataTable ^dtRoleFeatures;
DataTable ^dtViewRoleFeatures;
List<CRoleFeatures^>^ RoleFeatureList;
因此,三个网格中的每个网格都将绑定到一个 datatable dtView
...。
^dtPeople
是一个由DB_PersonManager
模块管理的内部人员编号及其姓名表。它以及下面的两个..People
变量存在的原因是我坚持要求用户必须从系统中已存在的人员列表中选择。在多网格解决方案的上下文中,它们是完全可选的。^dsPeople
是^dtPeople
的容器。^bsPeople
是将馈送到网格组合列并管理人员列表的绑定源。^dtViewUserData
绑定到gridUser
。它对我提出的多网格模型至关重要。^UserList
由DB_UserManager
模块填充。这决定了gridUser
中会显示什么。在此示例中,它提供三行硬编码的数据,但该模块可以轻松地从关系数据库或 XML 中读取数据。^dtRoles
是可分配给用户的角色内部表。您的第二个及后续网格不必遵循此方法。^dsRoles
是^dsRoles
的容器。^bsRoles
在这里是因为角色将显示在组合列中。^dtViewUserRoles
绑定到gridUserRoles
。这是我方法中另一个至关重要的声明。^UserRoleList
由DB_UserRoleMgr
模块填充。它在用户在gridUser
中更改的每次刷新过程中被调用,以确定gridUserRoles
中会显示什么。在此示例中,它仅提供硬编码数据,但如上所述,它可以从存储中获取数据。^dtRoleFeatures
是每个角色允许的功能的内部表。这是我开发的系统访问控制机制的一部分——请注意这次没有包含的数据集。角色的功能在此模块中无法更改,因此没有组合框。同样,这是设计选择而非限制。^dtViewRoleFeatures
绑定到gridFeatures
。这是我方法中第三个至关重要的声明。^RoleFeatureList
由DB_RoleFeatureMgr
模块填充。它在用户在gridUserRoles
中角色更改的每次刷新过程中被调用,以确定gridFeatures
中会显示什么。一如既往,仅提供硬编码用于说明目的。现实世界可以自由地从任何当天的策略中提取数据。
放置功能
本节将带您了解上述声明如何应用于我所概述的 UI,以实现一个带有多个 DataGridView
s 的窗体。
初始化
LaunchForm
函数在构建窗体但加载任何数据之前执行关键的初始化。
首先,我们有分配内存的代码,用于我们每个 People
结构。
dtPeople = gcnew DataTable("dtPeople");
dtPeople->Columns->Add("PersonID", Int32::typeid);
dtPeople->Columns->Add("PersonName", String::typeid);
bsPeople = gcnew BindingSource();
dsPeople = gcnew DataSet();
dsPeople->Tables->Add(dtPeople);
接下来,我们定义并向绑定到主 datagridview
的 datatable
添加列。
dtViewUserData = gcnew DataTable("ViewUserData");
dtViewUserData->Columns->Add("Username", String::typeid);
dtViewUserData->Columns->Add("Password", String::typeid);
dtViewUserData->Columns->Add("Number", Int32::typeid);
dtViewUserData->Columns->Add("PersonID", Int32::typeid);
现在我们已准备好处理数据,我们调用一些函数来加载人员、角色和功能。
接下来,我们定义用户和用户角色的列表。
UserList = gcnew List<CUserMaster^>();
UserRoleList = gcnew List<CUserRole^>();
最后,我们调用一个函数来加载用户,然后完成列表设置。这将加载主网格并启动一系列事件,这些事件将加载其他两个网格。加载人员、角色和功能的函数不负责实现多网格解决方案——它们所做的只是提供将在网格中显示的信息,因此我们将推迟在查看网格创建之后再进行研究。
Load_Users
Load_Users
函数通过访问外部世界并获取窗体上第一个 DataGridView
的数据来启动一切。它有四个重要部分,第一个部分确保用户列表是清晰的。
UserList->Clear();
然后重置数据表和网格。
dtViewUserData->Clear();
gridUser->DataSource = nullptr;
gridUser->Rows->Clear();
确信一切都已正确初始化后,我们调用 Fetch
例程来填充我们的用户列表。
UserList = CComs_UM::Fetch_User_Local(nullptr);
最后,将用户列表映射到绑定到第一个网格的数据表中。
DataRow ^dr;
for each(CUserMaster^ candidate in UserList)
{
dr = dtViewUserData->NewRow();
dr["Username"]= candidate->p_Username;
dr["Password"]= candidate->p_Password;
dr["Number"] = candidate->p_Person_ID;
dr["PersonID"]= candidate->p_Person_ID;
dtViewUserData->Rows->Add(dr);
}
List_SetUp
Load_Users
只是开始过程的一部分,List_SetUp
通过以下操作完成了第一个 DataGridView
的填充:
gridUser->AutoGenerateColumns = false;
gridUser->DataSource = dtViewUserData;
gridUser->Columns[dgUsername->Index]->DataPropertyName = "Username";
gridUser->Columns[dgPassword->Index]->DataPropertyName = "Password";
gridUser->Columns[dgNumber->Index]->DataPropertyName = "Number";
gridUser->Columns[dgPerson->Index]->DataPropertyName = "PersonID";
m_LoadCompleted = true;
gridUser->AutoResizeColumns();
现在我们的主数据网格中有数据了。其他两个需要触发主网格上的行进入事件。当 gridUser->DataSource
从 dtViewUserData
拉取数据时,该事件会对每一行触发,但我们只需要它在网格完全加载时触发——其他所有操作只会增加机器、数据库和网络的负载,因此 m_LoadCompleted
标志(您会在函数末尾看到它被设置为 true
)在这里起着关键作用。
在运行时填充第二个及后续网格
Process_RowEntered
Process_RowEntered
是处理 gridUser DataGridView
上 RowEnter
事件的函数。它的目的是执行特定任务——在这种情况下,一旦用户进入某一行,就读取该行的内容。我主要使用它来记录行的当前内容,以便与我完成该行后的最终状态进行比较,从而尽量减少要写入数据库的字段数量。本质上,我希望避免“Update TableA set X=1 where X=1
”。**然而,在此多网格场景中,它执行了一项关键的附加任务,即决定何时在运行时重新加载第二个网格。**在检查当前行中的用户名单元格内容后进行此检查,并且当它具有值且已完成加载为 true 时,我们使用 Load_UserRoles
和 RoleList_Setup
获取第二个网格的数据。
if (gridUser->Rows[e->RowIndex]->Cells[dgUsername->Index]->Value != nullptr)
{
m_OrigUsername = gridUser->Rows[e->RowIndex]->
Cells[dgUsername->Index]->Value->ToString();
if (m_LoadCompleted)
{
Load_UserRoles(m_OrigUsername);
RoleList_Setup();
m_Role_LoadCompleted = true;
}
}
请注意,我们正在设置一个 m_Role_LoadCompleted
标志——这将在我们获取第二个网格的数据后,作为进入加载第三个网格的信号。
此函数中还包含用于初始化标签、填充标签以及确保 Person
组合框始终显示正确值的代码,但这些对于多网格解决方案不是必需的。
if (e->RowIndex == gridUser->RowCount - 1)
{
lblPersonID->Text= nullptr;
lblPersonName->Text = nullptr;
lblUsername->Text = nullptr;
return;
}
if ((m_LoadCompleted && e->RowIndex < gridUser->RowCount)
&& (e->RowIndex != gridUser->RowCount - 1))
{
lblUsername->Text = gridUser->Rows[e->RowIndex]->
Cells[dgUsername->Name]->Value->ToString();
array<DataRow^>^ row;
try
{
if (gridUser->Rows[e->RowIndex]->Cells
[dgPerson->Index]->Value != nullptr
&&gridUser->Rows[e->RowIndex]->Cells
[dgPerson->Index]->Value->ToString() != "")
{
String^ IndPerson = gridUser->Rows[e->RowIndex]->
Cells[dgPerson->Index]->Value->ToString();
row =dtPeople->Select(String::Format
("PersonID={0}", IndPerson));
if (row->Length > 0)
{
lblPersonID->Text = row[0]->ItemArray[0]->ToString();
lblPersonName->Text =
row[0]->ItemArray[1]->ToString();
}
}
}
catch (Exception ^e)
{
String ^MessageString =
" Error reading internal people table: " + e->Message;
MessageBox::Show(MessageString);
}
}
Load_UserRoles
Load_UserRoles
在执行其数据检索任务时使用了与 Load_Users
完全相同的方法。唯一不同的是引用的数据结构/变量名称。但重要的是,它将用户名作为参数,并使用它来限制获取的数据集。
RoleList_Setup
RoleList_Setup
对于 gridUserRoles
所做的,与 List_SetUp
对于 gridUser
所做的相同,只是已完成标志在此函数之外设置,但这不影响逻辑。与 gridUser
的情况一样,此过程会触发 RowEnter
事件。
Process_Roles_RowEntered
Process_Roles_RowEntered
是处理 gridUserRoles
DataGridView
上 RowEnter
事件的函数。它的目的也是执行特定任务——例如,一旦用户进入某一行,就读取该行的内容。**在此多网格场景中,它执行了决定何时在运行时重新加载第三个网格的关键附加任务。**在检查当前行中的角色单元格内容后进行此检查,并且当它具有值且已完成加载为 true 时,我们仅使用 Load_RoleFeatures
获取第三个网格的数据。
Load_RoleFeatures
Load_RoleFeatures
将角色 ID 作为参数。它与其他两个加载函数略有不同,原因有两个:首先,我选择预加载所有功能;其次,它融合了 ..._Setup
函数的功能。这两种差异都不会影响多网格功能。这里发生的是:
第一个操作是声明一个 DataRow
数组。
array<DataRow^>^ FeatureRows;
接下来,执行标准的重置。
dtViewRoleFeatures->Clear();
gridFeatures->DataSource = nullptr;
gridFeatures->Rows->Clear();
这次我们从内存表(in-memory table)中选择所需的功能。
FeatureRows =dtRoleFeatures->Select(String::Format("Role_ID='{0}'", arg_Role_ID));
然后将选定的功能映射到绑定到第三个网格的 datatable
。
DataRow ^dr;
for each(DataRow ^row in FeatureRows)
{
dr = dtViewRoleFeatures->NewRow();
dr["Feature_Name"]= row[2];
dtViewRoleFeatures->Rows->Add(dr);
}
最后,我们执行了其他网格中设置函数包含的步骤。
dtViewRoleFeatures->AcceptChanges();
gridFeatures->AutoGenerateColumns = false;
gridFeatures->DataSource = dtViewRoleFeatures;
gridFeatures->Columns[dgFeatureName->Index]->DataPropertyName = "Feature_Name";
这就是在窗体上拥有三个或更多链接 DataGridView
s 所需的一切。由于第三个网格是只读的,因此我没有为其设置 RowEntered
事件。现在剩下的是查看为这些网格提供数据和处理数据的其他函数。
此示例中的其他有趣函数
Load_People
Load_People
获取系统中所有人员的 ID 和姓名,并将它们放入一个我们将绑定到 gridUser
上 dgNumber
和 dgPerson
列的绑定源数据结构中。
首先,定义一个列表来保存人员。
List<cpersonmaster^ />^ PersonList = gcnew List<cpersonmaster^ />();
然后,该列表由 DB_PersonManager
中的“Fetch
”例程填充,该例程在此示例中仅提供三个硬编码值,但也可以从任何源中获取。
PersonList = CComs_PM::Fetch_PersonMaster();
然后,将人员从列表映射到 datatable
。
DataRow ^row;
for each(CPersonMaster^ candidate in PersonList)
{
row = dsPeople->Tables["dtPeople"]->NewRow();
row["PersonID"] = candidate->p_PersonID;
row["PersonName"] = candidate->p_Surname + ", " + candidate->p_Forename;
dsPeople->Tables["dtPeople"]->Rows->Add(row);
}
设置一个绑定源,其中 dataset
提供 DataSource
,datatable
是 DataMember
。
bsPeople->DataSource = dsPeople;
bsPeople->DataMember = "dtPeople";
gridUser
上人员姓名列 dgPerson
的数据源从绑定源获取值,其中 DisplayMember
是人员姓名,而 ValueMember
是人员 ID。这一点很重要,因为 Person ID
将来自 User
表,我们将使用它来查询 person
表以获取姓名。向上滚动并查看 Process_RowEntered
以了解这一点的发生。
dgPerson->DataSource = bsPeople;
dgPerson->DisplayMember = "PersonName";
dgPerson->ValueMember = "PersonID";
该网格还显示 person ID
,其方式与姓名相同,只是在此情况下,显示成员和值成员都指向 person ID
。
dgNumber->DataSource = bsPeople;
dgNumber->DisplayMember = "PersonID";
dgNumber->ValueMember = "PersonID";
Load_Roles
Load_Roles
首先创建其绑定源和数据表,包括将绑定到 gridUserRoles
的 dtViewUserRoles
。
dtRoles = gcnew DataTable("dtRoles");
dtRoles->Columns->Add("Role_ID", String::typeid);
bsRoles = gcnew BindingSource();
dsRoles = gcnew DataSet();
dsRoles->Tables->Add(dtRoles);
dtViewUserRoles = gcnew DataTable("ViewUserRoles");
dtViewUserRoles->Columns->Add("Role_ID", String::typeid);
从那时起,它遵循的逻辑与 Load_People
中看到的非常相似,唯一的例外是只有角色 ID 绑定到网格列。
Load_Features
Load_Features
与其他加载函数不同,部分原因是它不必提供组合框列,也因为我们上面处理功能的方式。首先,我们定义一个用于角色功能的 datatable
。
dtRoleFeatures = gcnew DataTable("dtRoles");
dtRoleFeatures->Columns->Add("Role_ID", String::typeid);
dtRoleFeatures->Columns->Add("Feature_Code", String::typeid);
dtRoleFeatures->Columns->Add("Feature_Name", String::typeid);
然后是一个将绑定到 gridFeatures
的 datatable
。
dtViewRoleFeatures = gcnew DataTable("ViewRoleFeatures");
dtViewRoleFeatures->Columns->Add("Feature_Name", String::typeid);
然后是一个标准的列表来保存功能。
RoleFeatureList = gcnew List<crolefeatures^ />();
然后获取功能。
RoleFeatureList = CComs_RF::Fetch_Features(nullptr);
并将它们映射到角色功能数据表。
DataRow ^dr;
for each(CRoleFeatures^ candidate in RoleFeatureList)
{
dr = dtRoleFeatures->NewRow();
dr["Role_ID"]= candidate->p_Role_ID;
dr["Feature_Code"] = candidate->p_Feature_Code;
dr["Feature_Name"] = candidate->p_Feature_Name;
dtRoleFeatures->Rows->Add(dr);
}
最后,将 gridFeatures
绑定到为此目的设置的数据源。您之前已经在上面的 Load_RoleFeatures
中看到此表已填充。请记住,虽然我们之前调用了此函数(Load_Features
),但我们推迟到查看三个网格如何协同工作之后才进行研究。
gridFeatures->DataSource = dtViewRoleFeatures;
Process_CellValueChanged
Process_CellValueChanged
是处理 gridUser DataGridView
上 CellValueChanged
事件的函数。它不会触发对重新加载角色的新调用。如果您正在更改现有条目,那么它允许您在不更改关联角色的情况下更改用户详细信息。对于新用户,请填写其详细信息,然后转到第二个网格并添加要与该新用户关联的角色。
Process_Roles_CellValueChanged
Process_Roles_CellValueChanged
是处理 gridUserRoles DataGridView
上 CellValueChanged
事件的函数。这次当 dgRoles
发生更改且不为 null
时,我会重新加载功能网格。
Process_CellClick
Process_CellClick
包含处理 DataGridView
的 CellClick
事件的代码。它不是必需的,但如果没有它,组合框单元格将需要点击两次才能激活下拉列表。这是函数引擎。
gridUser->BeginEdit(true);
(safe_cast<combobox^ />(gridPassenger->EditingControl))->DroppedDown = true;
Process_Roles_CellClick
在 gridUserRoles
上执行类似的功能。
Process_CellEndEdit
Process_CellEndEdit
是处理用户 DataGridView
上 CellEndEdit
事件的函数。如果您在未填写必需单元格的情况下离开该行,它会在任何需要该图标的单元格中放置一个小红图标。它与 RowValidating
事件协同提供此功能。
Process_Roles_CellEndEdit
对角色网格执行类似的功能。
Process_RowValidating
Process_RowValidating
处理 DataGridView
上的 RowValidating
事件。它使用 If
语句来决定要检查哪些列是否存在错误。在这种情况下,我选择禁止任何列为空。它为单元格中的图标设置了内边距。
Process_Roles_RowValidating
为用户角色网格执行相同的任务。
Process_DataError
我在创建 Enum
示例时引入了 DataError
事件来处理 DataGridView
上未预期的显示故障。您可以不用它,但如果您意外遗漏了某些内容,它可能会被触发,并且当它被调用时,它会提供有关出现问题的有用信息,因为它能很好地捕获 DataGridView
的意外显示问题。此函数由所有三个网格的显示错误事件调用。
代码演练
本节将为您提供示例解决方案布局方式的非常简短但按顺序的概述。该解决方案围绕 DataGridViewMultiEx1.cpp 及其关联的 Form1.h 创建。这些被编译以形成 EXE。其他模块分别用于构成 DB_PersonManager
、DB_RoleFeatureMgr
、DB_RoleManager
、DB_UserManager
和 DB_UserRoleMgr
程序集。为了简化问题,所有这些都包含在同一个解决方案中。
Form1.h
Form1.h 以命名空间开头。以下命名空间已添加到标准集中。
using namespace System::Collections::Generic;//Needed for list containers
using namespace DB_PersonManager;
using namespace DB_RoleFeatureMgr;
using namespace DB_RoleManager;
using namespace DB_UserManager;
using namespace DB_UserRoleMgr;
之后是构造函数,它包含几行用于错误图标的设置,以及对 LaunchForm
函数的调用,该函数启动所有内容。在此之后是 IDE 添加的代码来构建窗体。然后是用户定义的函数和变量,用于文章开头引入的数据表和列表。然后是我的 Process_
... 函数,它们从相应的网格和窗体事件中调用。查看每个控件上的属性以了解其相关事件。
DataGridViewMultiEx1.cpp
包含所有函数代码,包括事件处理程序的代码,这些程序默认为 .h 文件,但通过用户定义的函数重定向到此处。例如,Process_RowEntered
是 gridUser_RowEnter
的主力。由于我们已经考察了所有代码,因此这里没有什么新鲜事,除了指出每个网格的所有函数都是分组在一起的。
DB_PersonManager.h
DB_PersonManager.h 定义了 Person
类,它的 Get
属性以及一个用于返回值的通信类。DB_RoleFeatureMgr.h, DB_RoleManager.h, DB_UserManager.h 和 DB_UserRoleMgr.h 为其表格执行类似的任务。
DB_PassengerManager.cpp
DB_PassengerManager.cpp 被简化为仅包含一个 fetch
函数,并将一些值硬编码到一个列表中返回给调用模块。DB_RoleFeatureMgr.cpp, DB_RoleManager.cpp, DB_UserManager.cpp 和 DB_UserRoleMgr.cpp 为其表格执行类似的任务。
历史
- 2011-09-08 - V1.0 - 初始提交