可以将 excel 中的 VBA 函数指定为始终绑定到一个范围吗?

Can a VBA function in excel be assigned to always be tied to a Range?

我有一个简单的代码,可以在“薪资国家”单元格发生变化时清除“州”单元格。例如,如果用户在 A6 中选择“USA”,然后在 X6 中选择“Arizona”,那么可能稍后由于某种原因他们改变了主意并想为国家选择“CAN”,州单元格将被清除。

但是如果以后有人决定在 X 列之前插入一列,显然会把我的 State 列移过去。有没有办法让 VBA 更聪明(或让我更聪明),以便函数将绑定到“状态”列而不是特定的“X”列?

Private Sub Worksheet_Change(ByVal Target As Range)

    If Target.CountLarge > 1 Then Exit Sub 'CountLarge handles larger ranges...
    'check Target column and row...
    If Target.Column = 1 And Target.Row >= 6 Then
        With Target.EntireRow
        
        'State column
            .Columns("X").Value = ""
            
        
        End With
    End If
End Sub

我(总是)为列定义一个枚举,就像这样

Public enum col_TableXXX    'adjust to your needs
  col_ID = 1
  col_PayrollCountry
  col_State
end enum

枚举会自动编号 - 所以 col_PayrollCountry 等于 2,col_State 等于 3 等等

如果有新列或顺序发生变化,您只需移动枚举或添加新枚举。

(您可以避免通过 transpose-pasting excel sheet 上的列标题输入代码,然后通过公式创建代码)

然后您可以像这样使用枚举:

If target.column = col_PayrollCountry then
   target.entireRow.columns(col_State) = vbnullstring
End If

这也比 columns("X")

更“可读”

此解决方案的罪魁祸首:您必须知道列已更改。它不是基于列名的自动操作。

您可以使用命名范围,也可以使用 .Find 来确定您的州列当前所在的位置。这是一个使用 .Find

的例子
Private Sub Worksheet_Change(ByVal Target As Range)
    
    If Target.CountLarge > 1 Then Exit Sub 'CountLarge handles larger ranges...
    'check Target column and row...
    If Target.Column = 1 And Target.Row >= 6 Then
        Dim StateCol As Long
        StateCol = Me.Range("1:5").Find("State", LookIn:=xlValues, LookAt:=xlPart).Column
        
        With Target.EntireRow
        
        'State column
            .Columns(StateCol).Value = ""
            
        
        End With
    End If
End Sub

如果要改用命名范围,可以使用 StateCol = Me.Range("NamedRange").Column 定义 StateCol,这样会快一点,因为它不需要每次都搜索行用户更改了一个值。

旁注:.Find 的搜索范围是第 1 行到第 5 行,但您可能希望根据您期望的数据移动方式来限制或扩展该范围。

你的问题还有另一个解决方案(我受到了与 ACCtionMan 关于枚举的简短讨论的影响):

如果你能插入一个table(插入>table)那么你就可以使用listobject。在许多其他优点中,您可以通过名称引用该列。

我假设 table 被命名为“tblData”

Private Sub Worksheet_Change(ByVal Target As Range)
Dim lo As ListObject
Set lo = Me.ListObjects("tblData")

If Not Intersect(Target, lo.ListColumns("Payroll Country").DataBodyRange) Is Nothing Then
    'changed cell is in Payroll country column then
    'take the intersection of the targets row and the State column to change the value
    Intersect(Target.EntireRow, lo.ListColumns("State").DataBodyRange) = vbNullString
End If

End Sub

但我更喜欢以下解决方案 - 因为我喜欢在事件处理程序中包含业务逻辑。

如果您的同事(甚至 6 个月后的您)查看变更事件代码 he/she 将立即理解 这里发生了什么 - 无需阅读如何完成的。

Option Explicit

Private m_loData As ListObject

Private Sub Worksheet_Change(ByVal target As Range)

'if target cells is not within loData we don't need to check the entry
If Intersect(target, loData.DataBodyRange) Is Nothing Then Exit Sub

If ColumnHeaderOfRange(target) = "Payroll Country" Then
    resetStateColumnToEmptyValue target
End If

End Sub


Private Sub resetStateColumnToEmptyValue(c As Range)
Intersect(c.EntireRow, loData.ListColumns("State").DataBodyRange) = vbNullString
End Sub

'this could go in a general module - then add listobject as parameter
Private Function ColumnHeaderOfRange(c As Range) As String
On Error Resume Next ' in case c is outside of listobject
    ColumnHeaderOfRange = Intersect(c.Cells(1, 1).EntireColumn, loData.HeaderRowRange)
On Error GoTo 0
End Function

'this could be public then you can access the table from outside the worksheet module
Private Function loData() As ListObject
If m_loData Is Nothing Then
    Set m_loData = Me.ListObjects("tblData")
End If
Set loData = m_loData
End Function