我很容易遗漏一些简单的东西,但是,从我的测试中,除了“创建”一些容器来保存“编辑”单元格。
根据您的评论...
”类似(但不完全相同)问题的其他解决方案是更改 rowpaint / valuechanged 等事件中的颜色,但是
这不相关,因为它们根据单元格的颜色应用颜色
VALUE(待定/等待/已付款等)而不是单元格的“已更改”
或“修改”状态。”
从某种意义上说,您在此陈述中有些正确,因为您声明您查看的所有示例都确定了单元格的背景颜色“基于单元格 VALUE”,这很可能是真的……但是,这并不意味着您必须根据单元格值对单元格进行着色。您可以将单元格的背景颜色基于您喜欢的任何内容。在这种情况下,您希望根据单元格是否已“编辑”来为单元格着色。
我希望我理解目标......您想要做的是“着色”自用户上次按下“保存”按钮以来在网格中“编辑”过的每个单元格。此外,(困难的部分)如果网格被排序和/或过滤以及添加或删除行时,您希望维护这些“彩色编辑”单元格。由于需要对行进行排序、过滤和添加/删除,这将需要一些额外的工作,并且比乍看之下更具挑战性。
我下面的解决方案确实使用网格的CellPainting 事件作为最后的手段。我尝试了多种使用网格CellEditingControlShowing、CellBegin/EndEdit 等的方法。在每次测试中,我在使用网格的非绘制事件时遇到的主要问题是网格何时排序、过滤或行是否被删除。如果把图片中的排序、过滤和删除去掉,那么问题就比较小了。
因此,如果排序、过滤和删除不适用,那么您可以使用网格CellValueChanged 事件、CurrentCellDirtyStateChanged 事件或其他事件来实现此目的,而无需连接网格的“绘画”事件之一。
一个小而重要的警告!
正如我之前所暗示的,出于多种原因,我通常会尽量避免使用网格的绘制事件。一个原因是他们经常开火。在加载网格或对网格进行排序等时,网格CellPainting 事件实际上可能会触发数百/数千次。显然,在确定事件触发多少次时,网格的大小是一个重要因素。
关键是在使用网格“绘制”事件时,明智的做法是采取额外的预防措施以避免代码影响网格性能。
例如,如果我们将一些可能需要很长时间才能执行的代码放入网格CellPainting 事件中,那么您很容易体验到“迟缓”的 UI。网格会出现冻结,或者光标移动跳跃而不顺畅,或者您在单元格中按下回车键,网格需要一两秒钟才能响应......等等......你在做事时要小心任何“PAINT”类型的事件。下面的答案是这条规则的任何例外,并且很容易成为导致“缓慢”网格 UI 的牺牲品。
鉴于警告,这个解决方案中的问题是这样的……我们将创建一个DataTable 来保存网格中完成的所有单元格更改。声明时,此表为空,因为未进行任何更改。在 gridsCellPainting 事件中,代码需要在这个表中搜索,找到与当前单元格行/列索引匹配的行/列索引。显然,随着对网格的更多更改,此表的大小会变大,并且显然需要更长的时间来搜索。
在我使用少量数据的小型测试中,即使对许多已编辑的单元格进行排序或过滤,这也没有影响,但是,如果数据很大并且@987654333 开始时,不难看到最终的性能下降@ 变大并且搜索速度很慢。幸运的是,只需保存“已编辑”单元格并清除“更改”表即可轻松解决此问题。请注意。
警告被阻止,让我们无论如何都要实施它......
让我们退后一步,将“数据”和 UI 分开。正如我们已经知道的,我们可以很容易地从DataTable. 中获取“更改的行”。在这种情况下,我们可能不知道哪些单独的“单元格”发生了更改,但是,我相信用一个值或全部更新数据库表该行的值是相当不相关的。换句话说,从数据库的角度来看,我们并不关心哪些“单元格”被“编辑”了,我们只关心行,因为我们会更新整行。
如果您想更新单个单元格,那么下面的解决方案将有所帮助,但是,只要将“数据”保存到 DB 透视图……我们就完成了。当用户单击“保存”按钮保存“已编辑”单元格时,代码只是保存数据并清除“已编辑”单元格行的表格。因此,从这里开始,主要关注点是 UI。
我们真的需要编辑单元格的集合吗?…
如果我们想要跟踪自上次单击保存按钮以来已更改/编辑的单元格,那么,如果没有一些“集合”来跟踪“编辑”单元格。
第一次尝试使用单元格Tag 属性。不幸的是,单元格Tag 属性可以被网格清除。具体来说,如果用户单击列标题对网格进行排序,Tag 将被清除。如果每个单元格都有一个属性,我们可以用它来识别这个“编辑”状态,那么问题就解决了。不幸的是,单元格属性不存在。因此,我们可以从选项列表中检查单元格 Tag 属性。
您建议添加额外的不可见列以跟踪更改可能会起作用,但是,由于这是按“行”添加的,因此您需要做一些额外的事情来跟踪各个单元格。此外,您可能会在表格中创建许多从不包含任何可用数据的额外单元格位置。即,未编辑的单元格。这可能会浪费很多空间。另外,我尽量避免任何“改变”数据库中原始源数据的事情。显然,在将表保存回数据库时,对原始表的任何更改都可能需要额外的代码,因为它已被更改。所以,我想说,这个选项是值得商榷但可行的。在这种情况下,我会简单地从原始表的“更改”中将其从列表中删除,但我相信它可以工作。
代码使用 DataTable 作为集合来保存网格中更改的单元格。
为了提供帮助,您应该能够复制/粘贴下面的代码并进行一些小的名称更改,并测试下面描述的内容。创建一个新的VB winform 项目,将两 (2) 个DataGridviews 拖放到一个表单上,并如图所示重命名它们,以及一个按钮。右侧包含ChangesGridTable 的网格显然不会显示,仅用于测试以直观地查看正在添加/删除/更新的行。最终产品可能如下所示。请注意,左侧的网格已按Col1 排序...
当一个单元格的值改变时,我们会在这个表中添加一行包含相关信息。表格中的属性/列将是与网格数据源相关的单元格的明显行和列索引,这也是 DataTable. 根据您的要求,我们还将添加额外的 ChangeDate DateTime 属性, OldValue 和 NewValue string 属性。这个ChangesGridTable 可能看起来像……
Dim dt = New DataTable()
dt.Columns.Add("DTRowIndex", GetType(Int32))
dt.Columns.Add("DTColIndex", GetType(Int32))
dt.Columns.Add("ChangeDate", GetType(DateTime))
dt.Columns.Add("OldValue", GetType(String))
dt.Columns.Add("NewValue", GetType(String))
我们可以在网格CellPainting 事件中使用此表中的一行。对于每个正在绘制的单元格,我们将获取该单元格“数据”行的索引号。然后我们将搜索ChangesGridTable 中的行,并检查该单元格的行和列索引是否至少匹配“ChangesGridTable”中的行/列(DTRowIndex, DTColIndex)索引之一。如果我们找到匹配项,那么我们就知道为该单元格着色。
下面是网格的CellPainting 事件,带有一些用于测试和额外错误检查的注释调试语句……
‘Dim ecount As Int32 = 1
Private Sub DataGridView1_CellPainting(sender As Object, e As DataGridViewCellPaintingEventArgs) Handles UserGrid.CellPainting
'Debug.WriteLine("CellPainting-> Enter Count: " + count.ToString())
If (ChangesGridTable.Rows.Count > 0) Then
If e.RowIndex >= 0 And e.ColumnIndex >= 0 Then
If Not UserGrid.Rows(e.RowIndex).IsNewRow Then
Dim drv As DataRowView = UserGrid.Rows(e.RowIndex).DataBoundItem ' <- Get the DATA row that this GRID row points to
Dim RowIndex As Int32 = UserGridTable.Rows.IndexOf(drv.Row) ' <- Get that DATA rows index in it DataTable
' Check to see if this row and column index matches any of the row/col indexes in the ChangesTable
Dim result = ChangesGridTable.Select("DTRowIndex = " & RowIndex & " AND DTColIndex = " & e.ColumnIndex)
If result.Length > 0 Then
e.CellStyle.BackColor = Color.LightGreen
'Debug.WriteLine("CellPainting-> Enter Count: " + ecount.ToString())
'ecount = ecount + 1
End If
End If
End If
End If
'Debug.WriteLine("CellPainting-> Leave")
End Sub
浏览代码……没有什么特别的事情发生,而且非常直接,并且有充分的理由,这段代码将被调用很多很多次。显然如果ChangesGridTable 为空,我们可以退出。然后粗略地检查行和列索引以及网格“新”行是否存在。
由于我们现在有一个单元格……我们需要在“数据源”中获取该单元格的行索引。为此,我们需要整行……。
Dim drv As DataRowView = UserGrid.Rows(e.RowIndex).DataBoundItem
从该行中,我们可以从UserGridTable 中获取其“数据行”索引...
Dim RowIndex As Int32 = UserGridTable.Rows.IndexOf(drv.Row)
现在我们有了那个单元格的“数据”行索引,我们可以搜索ChangesGridTable 来查看是否有匹配项。如果我们得到具有相同行和列索引的匹配项,则为该单元格着色。
Dim result = ChangesGridTable.Select("DTRowIndex = " & RowIndex & " AND DTColIndex = " & e.ColumnIndex)
指向上一个警告……“理想情况下”“搜索”ChangesGridTable 集合中的单元格索引的最后一步将尽可能快。就速度而言,使用DataTablesSelect 方法可能不是最佳选择。我猜如果您需要加快速度,那么将使用不同的数据结构。哈希表可能是更好的选择。我会把这个留给你。我相信就速度而言,有许多不同的“更好”的方法。重点是,这种“搜索”是唯一会导致 UI 开始滞后的事情。
我们可以绘制单元格,但是如何向ChangesGridTable 添加行?
所以现在我们有了网格CellPainting 事件来为ChangesGridTable. 中的单元格着色因此,接下来我们需要确定使用哪个网格事件来向ChangesGridTable. 添加行
首先……我有信心/知道有许多不同的方法可以做到这一点。如果可行,一种方法可能与另一种方法一样好。我当然愿意接受任何建议/cmets。
我为此选择了网格的CurrentCellDirtyStateChanged 事件。一个原因是为了处理用户正在“编辑”一个单元格,而网格单元格处于“编辑”模式,并且用户已经对单元格进行了一些更改,最后用户按下“Esc”的情况钥匙。在这种情况下,使用网格事件,代码仍会将单元格着色为“已编辑”,因为用户已经在单元格中输入了一些字符。
不幸的是,即使您通过订阅单元格的按键事件来查找“Esc”键,它也不起作用。原因是……当单元格处于“编辑”模式并且用户按下“Esc”键时……这是网格默认调用“取消”编辑。因此,当用户按下“Esc”键时……网格“编辑”模式随即结束。它会将单元格重置为其原始值并退出其“编辑”模式。在这种情况下,单元格的按键事件不会被触发。在许多情况下,这无关紧要……在这种情况下,它是相关的。
使用CurrentCellDirtyStateChanged 事件的另一个原因是因为我们想“保留”旧值和新值作为“ChangedGridTable”中的数据。
如果您跟踪网格CurrentCellDirtyStateChanged 事件,您可能会注意到它触发了两次。一次是当用户开始“编辑”一个单元格时,第二次是当用户离开该单元格时。我们将利用这一点来帮助解决“Esc”关键问题并保留旧值。
为了提供帮助,我们将创建一个名为 OriginalCellValue. 的全局 string 变量,最初它的值将设置为 null/Nothing. 这个想法是,当脏单元事件触发时,代码中的第一个检查是在此OriginalCellValue 变量...它的值将是 null 或一些 string 值。这将允许我们确定单元格的值是否已更改或未更改。这将在代码下方进一步解释。
Dim OriginalCellValue As String = Nothing
Private Sub DataGridView1_CurrentCellDirtyStateChanged(sender As Object, e As EventArgs) Handles UserGrid.CurrentCellDirtyStateChanged
If UserGrid.CurrentCell.RowIndex >= 0 Then
If OriginalCellValue IsNot Nothing Then
Dim curCell As DataGridViewCell = UserGrid.CurrentCell
If curCell.Value IsNot Nothing Then
If OriginalCellValue <> curCell.Value.ToString() Then
Dim drv As DataRowView = UserGrid.Rows(UserGrid.CurrentCell.RowIndex).DataBoundItem
Dim dtRowIndex As Int32 = UserGridTable.Rows.IndexOf(drv.Row)
If (dtRowIndex = -1) Then ' <- if -1, then the user changed a cell in the grids "new" row
dtRowIndex = UserGridTable.Rows.Count ' <- even though the row may not exit in the table now
End If ' we know it will get added after the event exits
ChangesGridTable.Rows.Add(dtRowIndex,
curCell.ColumnIndex,
DateTime.Now,
OriginalCellValue,
curCell.Value.ToString())
End If
OriginalCellValue = Nothing
End If
Else
OriginalCellValue = UserGrid.CurrentCell.Value.ToString()
End If
End If
End Sub
浏览代码,我们有粗略的索引边界检查等……然后……
如果OriginalCellValue 是null/nothing,那么它还没有被设置,这告诉我们这是第一次触发事件。因此,我们只需将单元格值设置为OriginalCellValue 字符串。事件退出,用户继续编辑单元格。现在,我们在 OrignalCellValue 变量中拥有了起始/原始单元格值。
稍后,用户完成“编辑”单元格并尝试离开单元格,然后CurrentCellDirtyStateChanged 事件第二次触发。同样,第一次检查是在 OriginalCellValue 字符串上。这次显然不会是null,因此会检查当前单元格的值是否“匹配”OriginalCellValue 字符串。
如果单元格字符串值“匹配”OriginalCellValue 字符串,则意味着用户没有对单元格进行任何更改,或者用户可能在进行了一些更改后按下了“Esc”键。
如果单元格字符串值与OriginalCellValue 不匹配,则意味着用户确实更改了单元格中的某些内容。因此,我们希望通过在ChangesGridTable. 中添加一行来保存这个更改的单元格
我们拥有大部分所需的东西。 OriginalCellValue. 中的单元格列索引、日期、单元格当前“编辑/新”值和单元格原始值最后,我们使用该行DataBoundItem 属性从网格数据源中获取“数据”行索引。
在删除行时维护ChangesGridTable……
这应该在排序/过滤时处理大多数用户交互,甚至在“添加”新行时。但是,删除行可能会导致已编辑的单元格不着色。
如果用户单击行标题以选择整行,然后按删除键,那么……该行将从网格中删除。此外,基础数据行也将从表中删除。这意味着ChangesGridTable 可能会在没有额外工作的情况下陷入不一致的状态。
显然,我们需要删除所有具有相同已删除行索引的行。即使可能有“多个”行具有相同的DTRowIndex 值,这也相当简单。不幸的是,还有另一个问题……
如果剩余ChangesGridTable 中的任何单元格的DTRowIndex 值“大于”已删除行的行索引,则这些索引需要“上移”一 (1) 行,因为该行上方的行已被删除。被删除行上方的行索引显然不会改变。
因此,为简单起见,当删除一行时,我们需要三 (3) 个步骤来保持 ChangesGridTable 处于一致状态……
-
从ChangesGridTable 中收集行索引列表
包含与DTRowIndex 值相同的所有行
给定值。请记住,可能不止一 (1) 个。
-
遍历该列表并从ChangesGridTable. 中删除这些行
-
遍历剩余的ChangesGridTable 行并递减
每行的DTRowIndex 值加 1,如果其值大于
删除的行索引。
由于我们很多人都需要在 UI 和可能的代码中调用此方法,因此创建一个方法来从 ChangesGridTable 中删除给定行索引的行可能会派上用场,看起来像……
Private Sub RemoveRowFromChangesTable(targetDTIndex As Int32)
Dim rowsToDel As List(Of DataRow) = New List(Of DataRow)()
' Get an int list of all the row indexs of the edited row
' that MATCH the given targetDTIndex
For Each row As DataRow In ChangesGridTable.Rows
If row("DTRowIndex") = targetDTIndex Then
rowsToDel.Add(row)
End If
Next
' loop through the int list and remove those rows
For Each row As DataRow In rowsToDel
ChangesGridTable.Rows.Remove(row)
Next
' for each row "below" the removed row,
' we need to decrement those rows DTRowIndex value by 1 as it has moved up
For Each row As DataRow In ChangesGridTable.Rows
If row("DTRowIndex") > targetDTIndex Then
row("DTRowIndex") = row("DTRowIndex") - 1
End If
Next
End Sub
这应该有望在删除一行时使ChangesGridTable 保持一致状态。我会给你留下一个“插入”。但是,它类似于删除,而不是递增而不是递减。
如果在代码中删除了该行,那么您应该可以完全访问应该删除哪一行,这样就可以简单地调用上述方法。在 UI 方面,我们需要弄清楚要订阅哪个“RowDelete”事件,以便我们可以调用上面的方法。
在这个例子中,我使用了网格的UserDeletingRow 事件。每次删除一行时都会触发此事件。如果选择了多行,则该事件将为每个已删除的行触发一次。因此,我们只需要 GRID 行对应数据源表中的“数据”行索引。然后我们可以用表格行索引调用上面的方法。像……
Private Sub UserGrid_UserDeletingRow(sender As Object, e As DataGridViewRowCancelEventArgs) Handles UserGrid.UserDeletingRow
Dim drv As DataRowView = UserGrid.Rows(e.Row.Index).DataBoundItem
Dim targetDTIndex = UserGridTable.Rows.IndexOf(drv.Row)
RemoveRowFromChangesTable(targetDTIndex)
End Sub
接近了……我们有网格CellPainting 事件根据ChangesGridTable. 中的行为单元格着色并且我们有网格CurrentCellDirtyStateChanged 事件将行添加到ChangesGridTable. 此外,我们还有其他代码要保留如果用户或代码从表中删除行,ChangesGridTable 处于一致状态。
剩下的一件事就是按下“保存”按钮时要做什么。幸运的是,这是两行代码。将数据保存到数据库后,我们需要清除以前颜色为“已编辑”的单元格的网格。要清除已编辑的单元格,只需从ChangesGridTable 中删除所有行,然后调用网格的Invalidate 方法。上面的代码将完成其余的工作。有点像……
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles btnSave.Click
' Update the rows in the DB… check for newly added rows
ChangesGridTable.Rows.Clear()
UserGrid.Invalidate()
End Sub
最后一项正在过滤……
不知道您是如何准确过滤数据的。在我的测试中,我使用了一个简单的DataView 对象,过滤了DataView,然后将DataView 设置为DataSource 到网格。幸运的是,在这种情况下,DataView 仍将包含所有单元格,即使它们未显示。这将允许着色按预期工作,而无需编写额外的代码来保持ChangesGridTable 一致状态,就像我们在删除行时所做的那样。例子……
过滤…
Dim dv As DataView = New DataView(UserGridTable)
dv.RowFilter = "Col1 > 12"
UserGrid.DataSource = dv
取消过滤…
UserGrid.DataSource = UserGridTable
最后,为了完成这个例子,下面的代码创建了一些数据来测试上面描述的内容。
Dim UserGridTable As DataTable
Dim ChangesGridTable As DataTable
Dim OriginalCellValue As String = Nothing
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
UserGridTable = GetUserTable()
UserGrid.DataSource = UserGridTable
UserGrid.Columns(2).AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
' initialize the changes table
ChangesGridTable = GetChangesTable()
ChangesGrid.DataSource = ChangesGridTable
End Sub
Private Function GetUserTable() As DataTable
Dim dt = New DataTable()
dt.Columns.Add("Col0", GetType(String))
dt.Columns.Add("Col1", GetType(Int32))
dt.Columns.Add("Col2", GetType(DateTime))
FillTable(dt)
Return dt
End Function
Private Function GetChangesTable() As DataTable
Dim dt = New DataTable()
dt.Columns.Add("DTRowIndex", GetType(Int32))
dt.Columns.Add("DTColIndex", GetType(Int32))
dt.Columns.Add("ChangeDate", GetType(DateTime))
dt.Columns.Add("OldValue", GetType(String))
dt.Columns.Add("NewValue", GetType(String))
Return dt
End Function
Private Sub FillTable(dt As DataTable)
Dim s As String
Dim i As Int32
Dim d As DateTime
Dim rand = New Random()
For index = 1 To 10
s = GetRandomString(rand, 10)
i = rand.Next(1, 100)
d = DateTime.Now.AddDays(rand.Next(-1000, 1000))
dt.Rows.Add(s, i, d)
Next
End Sub
Private Function GetRandomString(rand As Random, length As Int32) As String
Dim sb = New StringBuilder()
Dim letter As Char
Dim offset As Int32
For i = 1 To length
offset = Convert.ToInt32(Math.Floor(25 * rand.NextDouble()))
letter = Convert.ToChar(offset + 65)
sb.Append(letter)
Next
Return sb.ToString()
End Function
抱歉,帖子太长了。
我希望这是有道理的并有所帮助。祝你好运