带参数的Excel自定义函数

3

我正在尝试在Excel模块中创建自定义函数,如下所示:

Function STATUS(valuex As String)

    If ActiveCell.Offset(0, 1).Value = valuex Then

    ActiveCell.Value = ActiveCell.Offset(0, -1).Value

    'Remove value from left column
     Activecell.offset(0,1).clearcontents

   End If

End Function

它基本上会执行以下操作:
Number  Result  Status
          11    System
22              Type
          33    System
          44    System
55              Hardware
66              Type
          77    System
          88    System
99              Software
110             Type
         121    System
         132    System
143             Hardware
154             Type
165             Type
         176    System
187             Hardware
198             Type
209             Software

如果右单元格的值等于valux(例如字符串“System”),则将左单元格的值放入公式/函数单元格中,并删除左列的值。
但无论我编写什么程序,它都只返回零(0)或Name#错误。
请帮忙。

你的函数STATUS应该在代码中返回一些内容,但是这并没有被设置。一个函数只能对输入的值进行操作,所以它不会按照你想要的方式工作,你需要在一个范围上运行子程序。继续使用你已经有的代码,但是你需要在传入的每个单元格上运行该函数。例如:for each c in range("a1:a10").cells:status(c.value):next c,但是你需要将范围传递给c,因为它将在偏移量等方面被使用。你可以在函数中获取.value - Nathan_Sav
听起来你可能真正想要的是工作表选择更改事件。 - QHarr
1
在我看来,您需要指定何时希望所需的更改发生。当您更改工作表上的选择(如@QHarr所提到的)时?当您更改单元格的内容时?仅在按下按钮时? - Inarion
2个回答

10

你正在编写一个用户定义函数(UDF),即在公共标准模块中编写的Public Function,可以从工作表单元格调用 - 而这种特定类型的函数具有一组限制条件。例如,不允许有副作用。UDF接受一些输入,处理它们,然后返回结果

因此,UDF的签名应该如下所示:

Public Function {name}({args}) As {type}

编写函数时需要首先考虑的是,需要返回什么,换句话说,在计算完成后,单元格中应该包含什么内容,例如,如果您编写了一个将两个Double值相加的Add函数,则需要返回一个Double

Public Function Add(ByVal value1 As Double, ByVal value2 As Double) As Double

函数的主体通过给定的参数计算结果:

    Dim result As Double
    result = value1 + value2

在函数返回/退出之前,您需要赋值给函数的标识符来指定函数的返回值:

    Add = result

Excel的计算引擎会接受到这个结果,这就是一个带有公式的单元格(例如=Add(2, 2))得出值为4的方式。


你的STATUS函数依赖于ActiveCell,也就是当前选定的ActiveSheet上的任何单元格: 它不是调用该函数的单元格。如果重新选择一些随机的单元格来重算工作簿,则可能产生错误的结果。

作为UDF,它不允许对单元格进行.ClearContents操作(或以任何其他方式影响其他单元格)。这就是为什么该函数对进入条件块的执行路径返回#NAME?错误,并且由于从未分配任何返回值,因此其他执行路径生成0,这是Empty变量的数值表示形式,这是您当前函数返回的内容。

如果UDF需要了解另一个单元格的值,则最好的方法是将该单元格的值作为参数传入: 这样函数就可以正常工作,而无需关心工作表的布局。如果VLOOKUP没有使用lookup_value参数,而是从.Offset(0, 1)单元格获取该值,那么它会有多有用? 人们会暴动!


当您的需求是执行某些操作而不是计算/推算某些内容时,您需要的不是UDF,而是

宏是公共标准模块(或Worksheet模块)中的无参数Public Sub过程,可以从“宏”窗口调用,或在用户单击Shape、ActiveX CommandButton或将其分配到某个自定义菜单项的OnAction属性时执行 - 随你喜欢。

Sub过程执行操作。 它们可以访问和更改全局状态,修改任何单元格、工作表或工作簿; 他们甚至可以生成一个PowerPoint实例,并将图表粘贴为图片到一个新的Slide上——你想得到的任何东西,天空就是极限!

由于您在这里需要做一些事情,因此您需要编写的代码更像宏。不要称之为STATUS;使用一个动词并描述它正在进行的操作:根据给定的条件,您正在将值从一列移动到另一列。当您编写一个Sub过程时,首先考虑如何调用它。

我认为像这样的代码会很整洁:

MoveValues Sheet1.Range("$B$2:$B$22"), "System"

所以签名看起来应该是这样的:

Private Sub MoveValues(ByVal Target As Range, ByVal Criteria As String)

现在,body可以遍历指定的Target范围,评估右侧单元格是否与Criteria匹配,并相应地将值移动到左侧。或者更好的是 - 我们根本不假设工作表布局,像这样调用它:

With Sheet1
    MoveValues .Range("A2:A22"), .Range("B2:B22"), .Range("C2:C22"), "System"
End With
现在,如果我们需要在A和B之间或B和C之间插入一列,我们只需要更改传递给我们的过程的参数,而不是过程本身!
Private Sub MoveValues(ByVal Source As Range, ByVal Target As Range, ByVal Status As Range, ByVal Criteria As String)

首先,我们需要验证我们的假设,并在我们的预期未达到时决定要做什么——我们需要具有相同行数的单列范围!

在许多情况下,引发运行时错误是最好的选择。错误#5“无效的过程调用或参数”似乎是一个相当好的选择:

    If Source.Columns.Count <> 1 Or Target.Columns.Count <> 1 Or  Status.Columns.Count <> 1 Or _
       Source.Rows.Count <> Target.Columns.Count Or _
       Status.Rows.Count <> Target.Columns.Count _
    Then
        Err.Raise 5
    End If

我们甚至可以自定义错误消息,以帮助我们在调用代码后进行调试,当我们在未来几个月更改参数并忘记该MoveValues过程的假设时。

    If Source.Columns.Count <> 1 Or Target.Columns.Count <> 1 Or  Status.Columns.Count <> 1 Or _
       Source.Rows.Count <> Target.Columns.Count Or _
       Status.Rows.Count <> Target.Columns.Count _
    Then
        Err.Raise 5, "MoveValues", _
            "Source, Target, and Status ranges must be 1 column and the same number of rows."
    End If

我们还需要验证我们的 Criteria 不是空的,也不只是空白字符!

    If Trim$(Criteria) = vbNullString Then
        Err.Raise 5, "MoveValues", "Criteria string cannot be empty or whitespace."
    End If

现在我们已经验证了输入,因此剩下的过程可以安全地假设目标状态范围都是单列范围,它们的大小都相同,并且我们有一个有效的条件可供使用。因此,我们可以继续迭代单元格并执行我们的操作:

    Dim current As Long
    For current = 1 To Target.Rows.Count
        If Status.Cells(current).Value = Criteria Then
            Target.Cells(current).Value = Source.Cells(current).Value
            Source.Cells(current).ClearContents 
        End If
    Next

现在需要做的就是编写一个调用它的宏:

Public Sub MoveSystemValues()
    With Sheet1
        MoveValues .Range("A2:A22"), .Range("B2:B22"), .Range("C2:C22"), "System"
    End With
End Sub

现在,我们可以从Excel的窗口运行MoveSystemValues宏,或将其分配给某些Shape或按钮...然后意识到对于少量行它运作得非常好,但对于更大的范围而言则相当慢- 但是现在我们已经有足够的内容来处理了。


1

你不能使用UDF(用户定义函数)实现你想要的功能。

你正在使用VBA,因此需要了解SUB和FUNCTIONS之间的主要区别。

SUB执行你的Excel文件中的操作(例如选择、清除内容、更改工作表、打开另一个工作簿、进行计算等等)。

FUNCTIONS返回值,但不执行任何操作。把它们看作是你自己的Excel公式。Excel中的公式不执行操作,它们只根据某些参数返回一个值。

更多信息请参见:http://excelhints.com/2009/02/12/difference-between-sub-and-function/

因此,你需要将代码重写为子过程,然后执行它 :)

更新:我之前说的并不总是百分之百正确。我发帖是因为我认为对于刚开始使用VBA开发代码的新手来说,这是一个不错的起点。在你还是个新手的时候,我认为只使用sub来执行操作,只使用函数来进行定制计算会更容易学习。然后,随着经验的积累,可以进入下一阶段,开始组合它们并用它们做更复杂的事情。在执行代码时,Subs和Functions可以结合使用。

当您从Sub中调用UDF时,它可以执行某些操作(如删除工作表)。我说“某些操作”是因为老实说,我不知道是否所有操作都可用。

但是,如果您将UDF从单元格中调用,像普通的Excel公式一样输入它,那么它就不会执行任何操作。

例如:

Public Function DeleteWorkSheet() As Boolean
Sheets(3).Delete
End Function

这个用户定义函数将会删除工作簿中的第三个工作表。

在子程序中调用它应该是:

Sub Macro1()
DeleteWorkSheet
End Sub

是的,它会删除第三个工作表。

但是如果我像普通的Excel公式一样从单元格调用此UDF,则什么也不会发生。

希望这个澄清能够帮助。


5
你对VBA函数的定义基本上是错误的!函数可以像子程序一样“执行操作”,但除此之外,它们还返回一个值。它们不类似于Excel公式。 - 41686d6564 stands w. Palestine
非常感谢大家的回答,我虽然是一名VBA程序员,但这个函数的东西对我来说还是比较新的。再次感谢。 - user3412266
1
@FoxfireAndBurnsAndBurns 请在答案中添加这个澄清,以便未来的读者不会误解。此外,这样做可以撤销下降的投票。 - 41686d6564 stands w. Palestine
我赞同。我没有点踩,但如果稍微改一下措辞,我肯定会点赞=) - Mathieu Guindon
@AhmedAbdelhameed。当然没问题!感谢您的建议!我会做的。 - Foxfire And Burns And Burns
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接