如何确保Excel计算在VBA程序中完成

在Excel电子表格中,用户定义的函数用来计算基本结果作为电子表格matrix(复合元素的横截面值)。

Public Function XSValues(inputRange as Range) as variant [...] ' returns an array and used as matrix-formula (Ctrl-Shift-Enter) End Function 

这些结果一方面在电子表格中使用。 另一方面,基于这些电子表格结果中的一些值,使用VBA程序执行相对复杂和耗时的计算(结构模型的静态分析)。 这个过程由一个button触发。

 Public Sub Recalculate() [...] myValue = range("SomeXLResult").Value ' calculation ' notification updates ' drawing ' places different results into cells End Sub 

现在,我的问题是,当Sub Recalculate触发时,电子表格计算已过时。 我发现在Excel 2016中,电子表格计算被分成多个线程。 经历过用户交互有时比电子表格计算更快。

因此,我在VBA过程中得到了折旧值以供进一步处理。 我的问题是: 如何保证从电子表格范围更新的值?

如果答案中解释的解决scheme为你工作,那么很好。 我只是想知道你是否知道应用程序的AfterCalculate事件( https://msdn.microsoft.com/en-us/vba/excel-vba/articles/application-aftercalculate-event-excel ):

只要计算完成且没有未完成的查询,就会发生此事件。 事件发生前,必须满足两个条件。 即使在工作簿中没有工作表数据的情况下,也可以提高该事件,例如每当完成整个工作簿的计算并且没有查询正在运行时。

加载项开发人员使用AfterCalculate事件来知道工作簿中的所有数据何时已完全由任何可能正在进行的查询和/或计算更新。

此事件发生在所有工作表之后。 计算,图表。 计算,AfterRefresh和SheetChange事件。 这是所有刷新处理完成后所有计算处理完成后发生的最后一个事件,并且在应用程序之后发生。 CalculationState被设置为xlDone。

这对你来说可能更容易实现。 访问应用程序对象事件的技巧是在类模块中声明WithEvents 。 例如,我已经调用了类clsAppEvents

 Option Explicit Private WithEvents mApp As Application Private Sub Class_Initialize() Set mApp = Application End Sub Private Sub mApp_AfterCalculate() Debug.Print "Calc ended at: " & Now ConsumeAfterCalculate End Sub 

在你的模块中,你只需要调用和事件处理代码:

 Option Explicit Private mAppEvents As clsAppEvents Private mEnableConsume As Boolean Public Sub RunMe() Set mAppEvents = New clsAppEvents End Sub Public Sub ConsumeAfterCalculate() If mEnableConsume Then Debug.Print "Sub called at: " & Now mEnableConsume = False End If End Sub Public Sub ConsumeButtonClick() Debug.Print "Button clicked at: " & Now mEnableConsume = True 'For demo purposes I'm just forcing a calculation on existing data. Sheet1.EnableCalculation = False Sheet1.EnableCalculation = True End Sub 

仅供参考,debugging结果如下:

button点击:25/10/2017 4:49:20 pm

Calc结束于:25/10/2017 4:49:22 pm

Sub在:25/10/2017 4:49:22 pm

最后,以下解决scheme满足我的需求:

当按下重新计算button时,vba会检查当前的Excel计算状态。 如果计算完成,直接启动VBA- Recalculate程序。 如果计算模式处于挂起或计算状态,则只有本地工作表variablesp_RecalcButtonClicked设置为true。 当Excel计算完成后,每个工作Worksheet_Calculate 计算完毕后触发Worksheet_Calculate事件。 所以我们可以指示Excel Recalculate

作为一项安全措施,我使用函数waitForRecalculation在子Recalculate开始之前保留了以上注释中相关的两个问题中描述的解决scheme。 为了避免不活动,我介绍了一个计时器,告诉用户,如果计算不能在一定的时间内完成。

这是主工作表的代码:

 ' ##### Worksheet-Code ''' ' Private Worksheet-Variable to determine, ' if the button was pressed prior to worksheet calculated-event ' Private p_RecalcButtonClicked As Boolean ''' ' Procedure to handle Button Clicked ' (either using a shape with a macro assigned or ' an Active-X-Button with this procedure as event handler: best is to use {Button}_MouseUp as {Button}_clicked is fired occasionally by excel itself) ' Public Sub ButtonClicked() ' ' depending on the calculation state ... ' Select Case Application.CalculationState Case xlDone ' ' ... all done, fine ... ' ... directly call the calculation procedure sub Recalculate ' p_RecalcButtonClicked = False Recalculate Case xlPending ' ' ... pending ... ' ... set local worksheet variable true in order to call sub Recalculate ' later, when the calculated-event was raised ' p_RecalcButtonClicked = True ' ' instruct excel to recalculate ' Application.CalculateFullRebuild ' ' now let excel perform until worksheet calculated event is raised ' Case xlCalculating ' ' ... calculating ... ' ... set local worksheet variable true in order to call sub Recalculate ' later, when the calculated-event was raised ' p_RecalcButtonClicked = True ' ' let excel continue until worksheet calculated event is raised ' Case Else End Select End Sub ''' ' worksheet calculation finished ' this event is raised AFTER calculation was finished ' (shold actually be named Worksheet_Calculated) ' Private Sub Worksheet_Calculate() ' check if the RecalcButton was clicked If p_RecalcButtonClicked Then p_RecalcButtonClicked = False Recalculate End If End Sub ''' ' Recalculation ' Public Sub wm_Recalculate() ' ' wait for calculation to be done ' just in case... ' If Not waitForRecalculation Then MsgBox "Press Ctrl+Alt+F9 for full recalculation", vbCritical + vbOKOnly, "Excel-calculation not done" Exit Sub End If ' [...] Your calculation here... End Sub ''' ' Helper function to wait and do events until Excel-calculations are done ' returns true if calculation is done within the given time ' Public Function waitForRecalculation() As Boolean Const MAXTIME_S = 10 Dim t As Double t = Timer() ' in case of sql-async queries this might be required ' ' Application.CalculateUntilAsyncQueriesDone ' ' As a safety net, ' the second solution is to ' do System events until calculation is done ' If Application.CalculationState <> xlDone Then Do DoEvents If Timer() - t > MAXTIME_S Then Exit Do Loop Until Application.CalculationState = xlDone End If ' ' return true if calculations are done ' waitForRecalculation = (Application.CalculationState = xlDone) End Function