在 Excel 中使用 UDF 更新工作表
Posted
技术标签:
【中文标题】在 Excel 中使用 UDF 更新工作表【英文标题】:Using a UDF in Excel to update the worksheet 【发布时间】:2014-06-19 10:19:57 【问题描述】:这不是一个真正的问题,而是为 cmets 发布此问题,因为我不记得以前见过这种方法。我正在回复对上一个答案的评论,并尝试了一些我以前没有尝试过的东西:结果很有趣,所以我想把它作为一个独立的问题连同我自己的答案一起发布。
在 SO(和许多其他论坛)上存在许多问题,例如“我的用户定义函数出了什么问题”,答案是“您无法从 UDF 更新工作表” - 这个此处列出的限制:
Description of limitations of custom functions in Excel
已经描述了一些方法来克服这个问题,例如请参阅此处 (https://sites.google.com/site/e90e50/excel-formula-to-change-the-value-of-another-cell),但我认为我的确切方法不在其中。
另见:changing cell comments from a UDF
【问题讨论】:
非常有趣.......当我试图从红色变为黄色时,我的 Excel 崩溃了!! 【参考方案1】:发布回复,以便我可以将自己的“问题”标记为有答案。
我见过其他解决方法,但这似乎更简单,我很惊讶它完全有效。
Sub ChangeIt(c1 As Range, c2 As Range)
c1.Value = c2.Value
c1.Interior.Color = IIf(c1.Value > 10, vbRed, vbYellow)
End Sub
'######## run as a UDF, this actually changes the sheet ##############
' changing value in c2 updates c1...
Function SetIt(src, dest)
dest.Parent.Evaluate "Changeit(" & dest.Address(False, False) & "," _
& src.Address(False, False) & ")"
SetIt = "Changed sheet!" 'or whatever return value is useful...
End Function
如果您有有趣的应用程序想要分享,请发布其他答案。
注意:未经任何类型的真正“生产”应用程序测试。
【讨论】:
有谁知道为什么这行得通?我的意思是说真的,这是巫术。 我已经在 Win 7 HB、Excel 2003 上对其进行了测试,值更改正常,但颜色格式不起作用..还有另一种解决方法 - 请参阅答案的第 2 部分@ 987654321@ Excel 2010 32 位在 Win7 64 位 - 工作!.. 一次,然后在重新计算时崩溃。 我可以设想的一个警告是创建一个无限计算循环的可能性,该方法用于在单元格中修改或创建值或公式。可以想象,这可以重新启动计算周期等等。这可能不会被系统识别(并停止)为循环引用;因此注意到崩溃。类似于在自身之上运行的事件宏(例如 Worksheet_Change)。故事的寓意:如果您试图覆盖“设计行为”,请接受随之而来的任何限制。 @Jeeped - 同意:这绝对是一种使用风险自负的事情。【参考方案2】:MSDN KB 不正确。
它说
由工作表单元格中的公式调用的用户定义函数无法更改 Microsoft Excel 的环境。这意味着这样的函数不能执行以下任何操作:
-
在电子表格上插入、删除或格式化单元格。
更改另一个单元格的值。
向工作簿移动、重命名、删除或添加工作表。
更改任何环境选项,例如计算模式或屏幕视图。
向工作簿添加名称。
设置属性或执行大多数方法。
在下面的代码中,您可以看到第 1、2、4 和 5 点可以轻松实现。
Function SetIt(RefCell)
RefCell.Parent.Evaluate "SetColor(" & RefCell.Address(False, False) & ")"
RefCell.Parent.Evaluate "SetValue(" & RefCell.Address(False, False) & ")"
RefCell.Parent.Evaluate "AddName(" & RefCell.Address(False, False) & ")"
MsgBox Application.EnableEvents
RefCell.Parent.Evaluate "ChangeEvents(" & RefCell.Address(False, False) & ")"
MsgBox Application.EnableEvents
SetIt = ""
End Function
'~~> Format cells on the spreadsheet.
Sub SetColor(RefCell As Range)
RefCell.Interior.ColorIndex = 3 '<~~ Change color to red
End Sub
'~~> Change another cell's value.
Sub SetValue(RefCell As Range)
RefCell.Offset(, 1).Value = "Sid"
End Sub
'~~> Add names to a workbook.
Sub AddName(RefCell As Range)
RefCell.Name = "Sid"
End Sub
'~~> Change events
Sub ChangeEvents(RefCell As Range)
Application.EnableEvents = False
End Sub
【讨论】:
只是为了辩论 - 是否可以说 KB 是正确的,因为 UDF 正在评估/调用 Sub,实际上是在进行更改...? 为了辩论起见 :P 知识库应该说清楚However the above/below can be achieved using Evalute/Calling a Sub
而不是发布一揽子声明 that such a function cannot do any of the following....
@MacroMan
不错的回报 ;) 显然 MS 的人并没有为此做好准备。我还为此类事情提出了vba-voodoo
的新标签
@MacroMan: Obviously the people at MS weren't prepared for this.
虽然我可以理解并接受这一点。我对这些人真正的不满是他们没有认真对待 MS Office 的反馈。我不知道在过去的几年里我在 MSDN KB 上留下了多少反馈,但没有一个被采取行动!就好像他们根本不在乎!
他们可能不会 - 就 MS 而言,Office 是他们的“摇钱树”,而且(在我看来)市场上没有任何真正的竞争对手,尤其是在企业方面,所以他们在这方面可能有点冷漠。我想他们将精力集中在创建更新版本的一切以尝试跟上市场*ahem - Apple*,而不是让他们拥有的东西变得更好......【参考方案3】:
我知道这是一个旧线程,我不确定你们是否已经发现了这个,但我发现你不仅可以从 UDF 中添加、删除或修改形状,还可以添加 @987654321 @。我正在工作中构建一个插件,它使用这个概念来返回给定一系列值的 SQL 数据,而不是数组函数的 Ctrl+Shift+Enter
方法,因为我的许多最终用户都不够精通 excel,无法理解他们的使用,
注意: 下面的代码 100% 处于测试阶段,还有很大的改进空间,但它确实说明了这个概念。这也是一段不错的代码,但我不想留下任何问题。
Option Explicit
Public Function GetPNAverages(ByRef RangeSource As Range) As Variant
Dim arrySheet As Variant
Dim lngRowCount As Long, i As Long
Dim strSQL As String
Dim rngOut As Range
Dim objQryTbl As QueryTable
Dim dictSQLData As Dictionary
Dim RcrdsetReturned As ADODB.Recordset, RcrdsetOut As ADODB.Recordset
Dim Conn As ADODB.Connection
Application.ScreenUpdating = False
If RangeSource.Columns.Count > 1 Then
MsgBox "The input Range cannot be more than" _
& " a single column.", vbCritical + vbOKOnly, "Error:" _
& " Invalid Range Dimensions"
Exit Function
End If
lngRowCount = RangeSource.Rows.Count
If RngHasData(Application.Caller.Address, lngRowCount) Then Exit Function
arrySheet = RangeSource
strSQL = ArryToDelimStr(arrySheet, lngRowCount)
If Not GetRecordSet(strSQL, "JDE.GetPNAveragesTEST", _
"@STR_PN", RcrdsetReturned, Conn) Then GoTo StopExecution
Call BuildDictionary(dictSQLData, RcrdsetReturned, lngRowCount)
Call LeftOuterJoin(dictSQLData, arrySheet, RcrdsetOut, lngRowCount)
GetPNAverages = dictSQLData.Item(RangeSource.Cells(1, 1).Value2) 'first value
If lngRowCount > 1 Then
'Place query table below first cell
Set rngOut = Range(Application.Caller.Address).Offset(1, 0)
'add query table to the range
Set objQryTbl = ActiveWorkbook.ActiveSheet.QueryTables.Add(RcrdsetOut, rngOut)
With objQryTbl
.FieldNames = False
.RefreshStyle = xlOverwriteCells
.BackgroundQuery = False
.AdjustColumnWidth = False
.PreserveColumnInfo = True
.PreserveFormatting = True
.Refresh
End With
'deletes any query table from _
ots destination range to avoid _
having external connections
rngOut.QueryTable.Delete
End If
StopExecution:
Application.ScreenUpdating = True
Application.EnableEvents = True
If Not Conn Is Nothing Then: If Conn.State > 0 Then Conn.Close
If Not RcrdsetReturned Is Nothing Then: If RcrdsetReturned.State > 0 Then RcrdsetReturned.Close
If Not RcrdsetOut Is Nothing Then: If RcrdsetOut.State > 0 Then RcrdsetOut.Close
Set Conn = Nothing
Set RcrdsetReturned = Nothing
Set RcrdsetOut = Nothing
End Function
Private Function GetRecordSet(ByRef strDelimIn As String, ByVal strStoredProcName As String, _
ByVal strStrdProcParam As String, ByRef RcrdsetIn As ADODB.Recordset, _
ByRef ConnIn As ADODB.Connection) As Boolean
Dim Cmnd As ADODB.Command
Const strConn = "Provider=VersionOfSQL;User ID=************;Password=************;" & _
"Data Source=ServerName;Initial Catalog=DataBaseName"
On Error GoTo ErrQueryingData
Set ConnIn = New ADODB.Connection
ConnIn.CursorLocation = adUseClient 'this is key for query table to work
ConnIn.Open strConn
Set Cmnd = New ADODB.Command
With Cmnd
.CommandType = adCmdStoredProc
.CommandText = strStoredProcName
.CommandTimeout = 300
.ActiveConnection = ConnIn
End With
Set RcrdsetIn = New ADODB.Recordset
Cmnd.Parameters(strStrdProcParam).Value = strDelimIn
RcrdsetIn.CursorType = adOpenKeyset
RcrdsetIn.LockType = adLockReadOnly
Set RcrdsetIn = Cmnd.Execute
If RcrdsetIn.EOF Or RcrdsetIn.BOF Then GoTo ErrQueryingData Else GetRecordSet = True
Set Cmnd = Nothing
Exit Function
ErrQueryingData:
If Not ConnIn Is Nothing Then: If ConnIn.State > 0 Then ConnIn.Close
If Not RcrdsetIn Is Nothing Then: If RcrdsetIn.State > 0 Then RcrdsetIn.Close
Set ConnIn = Nothing
Set RcrdsetIn = Nothing
Set Cmnd = Nothing
'Sometimes the error numer <> > 0 hence the else statement
If Err.Number > 0 Then
MsgBox "Error Number: " & Err.Number & "- " & Err.Description & _
" , occured while attempting to exectute the query.", _
vbCritical, "Error: " & Err.Number
Else
MsgBox "An error occured while attempting to execute the query. " & _
"Try typing the formula again. If the issue persits" & _
"please contact (Developer Name).", vbCritical, _
"Error: Could Not Query Data"
End If
End Function
Private Sub BuildDictionary(ByRef dictToReturn As Dictionary, ByRef RcrdsetIn As ADODB.Recordset, _
ByVal lngRowCountIn As Long)
'building a second recordset because I only want one field from the
'recordset returned by 'GetRecordSet', and I cannot subset it
'using any properties of the query table that I know of
Set dictToReturn = New Dictionary
dictToReturn.CompareMode = BinaryCompare
With RcrdsetIn
If lngRowCountIn > 1 Then
.MoveFirst
Do While Not RcrdsetIn.EOF
'Populate dictionary with key=LookUpValues; Item=ReturnValues
If Not dictToReturn.Exists(.Fields(0).Value) Then
dictToReturn(.Fields(0).Value) = .Fields(1).Value
End If
.MoveNext
Loop
Else 'only 1 value
dictToReturn(.Fields(0).Value) = .Fields(1).Value
End If
End With
End Sub
Private Sub LeftOuterJoin(ByRef dictIn As Dictionary, ByRef arryInPut As Variant, _
ByRef RcrdsetToReturn As ADODB.Recordset, ByVal lngRowCountIn As Long)
Dim i As Long
Dim varKey As Variant
If lngRowCountIn = 1 Then Exit Sub
Set RcrdsetToReturn = New ADODB.Recordset
With RcrdsetToReturn
.Fields.Append "Field1", adVariant, 10, adFldMayBeNull
.CursorType = adOpenKeyset
.LockType = adLockBatchOptimistic
.CursorLocation = adUseClient
.Open
If Not .BOF Then .MoveNext
'LBound(arryInPut, 1) + 1 skip first value of array
For i = LBound(arryInPut, 1) + 1 To UBound(arryInPut, 1)
.AddNew
varKey = arryInPut(i, 1)
If dictIn.Exists(varKey) Then
.Fields(0).Value = dictIn.Item(varKey)
Else
.Fields(0).Value = "DNE"
End If
varKey = Empty
.Update
.MoveNext
Next i
End With
End Sub
Private Function ArryToDelimStr(ByRef arryFromRngIn As Variant, ByVal lngRowCountIn As Long) As String
Dim arryOutPut() As Variant
Dim i As Long
Const strDelim As String = "|"
If lngRowCountIn = 1 Then
ArryToDelimStr = arryFromRngIn
Exit Function
End If
'Note: 1-based to match the worksheet array
ReDim arryOutPut(1 To lngRowCountIn)
For i = LBound(arryFromRngIn, 1) To lngRowCountIn
arryOutPut(i) = arryFromRngIn(i, 1)
Next i
ArryToDelimStr = Join(arryOutPut, strDelim)
End Function
Public Function RngHasData(ByVal strCallAddress As String, ByVal lngRowCountIn As Long) As Boolean
Dim strRangeBegin As String, strRangeOut As String, _
strCheckUserInput As String
Dim lngRangeBegin As Long, lngRangeEnd As Long
strRangeBegin = StripNumbers(strCallAddress)
lngRangeBegin = StripText(strCallAddress)
lngRangeEnd = lngRangeBegin + lngRowCountIn
strRangeOut = strCallAddress & ":" & strRangeBegin & CStr(lngRangeEnd)
If Application.CountA(ActiveSheet.Range(strRangeOut)) > 1 Then
strCheckUserInput = MsgBox("There is data in range " & strRangeOut & " are you sure" & _
"that you want to overwrite it?", vbInformation _
+ vbYesNo, "Alert: Data In This Range")
If strCheckUserInput = vbNo Then RngHasData = True
End If
End Function
Private Function StripText(ByRef strIn As String) As Long
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "[^\d]+"
StripText = CLng(.Replace(strIn, vbNullString))
End With
End Function
Private Function StripNumbers(strIn As String) As String
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "\d+"
StripNumbers = .Replace(strIn, "")
End With
End Function
将分隔字符串解析为表变量的表值函数:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION dbo.fn_Get_REGDelimStringToTable (@STR_IN NVARCHAR(MAX))
RETURNS @TableOut TABLE(ReturnedCol NVARCHAR(4000))
AS
BEGIN
DECLARE @XML xml = N'<r><![CDATA[' + REPLACE(@STR_IN, '|', ']]></r><r><![CDATA[') + ']]></r>'
INSERT INTO @TableOut(ReturnedCol)
SELECT RTRIM(LTRIM(T.c.value('.', 'NVARCHAR(4000)')))
FROM @xml.nodes('//r') T(c)
RETURN
END
GO
使用的存储过程:
CREATE PROCEDURE [JDE].[GetPNAveragesTEST] ( @STR_PN NVARCHAR(MAX)
) AS
BEGIN
SELECT TT.ReturnedCol
,IsNull(Cast(pnm.AVERAGE_COST As nvarchar(35)), 'DNE') as AVERAGE_COST
FROM dbo.fn_Get_MAXDelimStringToTable(@STR_PN) TT
Left Join PN_Interchangeable pni ON TT.ReturnedCol=pni.PN_Interchangeable
Left Join PN_MASTER pnm On pni.MPN=pnm.MPN
END;
【讨论】:
以上是关于在 Excel 中使用 UDF 更新工作表的主要内容,如果未能解决你的问题,请参考以下文章