如何等待Power Query刷新完成?

8

设置:

  • Windows 7 (在工作中)
  • Windows 10 (在家中)
  • Excel 2016 (版本4627,在工作中)
  • Excel 2016 (版本8730,在家中)
  • Power Query已设置为导入、追加和转换一个Excel文件夹。该步骤有效。

在使用任何等待Power Query完成刷新的技术后,根据下面展示的“我尝试过的事情”部分所述,可以显示消息框并执行任何其他代码,然而Power Query表格更新仍未完成,这可通过“查询和连接”窗格中的刷新指示器(旋转图标?)进行查看。

上述声明的例外是应用程序类的OnTime方法,如下面的“代码”部分所示,它似乎不会中断对Power Query刷新的轮询。问题在于,它使用硬编码的时间来暂停VBA代码,这将不总是有效,因为查询的数据大小、数量和持续时间会随时间变化而改变。

我尝试过:

  • 我已经阅读了所有关于使用DoEventsBackgrgoundQuery = FalseCalculateUntilAsyncQueriesDone方法和属性的StackOverflow(和其他网站资源)。
  • 我尝试创建一个类来创建查询更新前/后事件,如这个链接所建议的(下面的代码示例中没有显示)。
  • 我尝试使用Do Until/While Loops以及QueryTable方法的.Refreshing = True/False属性等待刷新完成。
  • 我尝试根据"subro"在此处建议的方式(菜单栏 -> 数据 -> 连接 -> 属性)将Excel菜单中的BackgroundQuery属性设置为False,这里是菜单的图片:等待ActiveWorkbook.RefreshAll完成-VBA

Excel menu for setting the BackgroundQuery property.

代码:

Private Sub sht_sub_Refresh_AllConnections_dev()
    'Name: sht_sub_Refresh_AllConnections_dev
    'Purpose: An attempt at using VBA to wait for Queries to finish updating before displaying a message.
    'Description: Waits for a hard coded period of time before dislpaying the message box.
    'State: WIP.
    'Dev: Needs a way to look at the connection stream to somehow detect when its finished.
    
    'DECLARATIONS:
    '------------'
    Dim procName As String              'Stores this procedure's name.
    Dim qTblLst As QueryTables          'A query table collection object.
    Dim qTblObj As QueryTable           'A query table object.
    Dim conLst As Connections           'A connection collection object.
    Dim conObj As WorkbookConnection    'A connection object.
    Dim idx As Long                     'A loop counter.

    'INITIALIZATIONS:
    '---------------'
    procName = "sht_sub_Refresh_AllConnections_dev"    'Store this procedure's name.
    Linit.ini_Setup_Project                            'Setup the project if needed.
    Set conLst = ThisWorkbook.Connections              'Set the connections list object.
    Set conObj = conLst.Item(conLst.Count)             'Set an initial connection object.
    idx = 0                                            'As an exit if the do loop continues without end.
    
    'MAIN CODE BODY:
    '--------------'
    'Turn off backgroundquery for each connection type.
    For Each conObj In conLst                           'For each connection object,
        With conObj
            Select Case .Type                               'Check the connection type,
            Case 1                                        'If its an OLEDB connection then,
                .OLEDBConnection.BackgroundQuery = False    'Set it's backgroundquery property to false.
            Case 2                                        'If its an ODBC connection the,
                .ODBCConnection.BackgroundQuery = False     'Set it's backgroundquery property to false.
            End Select
        End With
    Next conObj
    
    ThisWorkbook.RefreshAll                             'Refresh all connections.
    
    'DEV: Using loops, DoEvents and a query name starting with the letters "zzzz" as suggsted here:
    'https://social.technet.microsoft.com/Forums/en-US/bc3f7748-8a52-498d-951c-4566b8adf45a/in-excel-2016-power-queries-dont-refresh-in-the-background-anymore?forum=powerquery
    'and here:
    'https://www.myonlinetraininghub.com/excel-forum/vba-macros/pause-macro-until-power-queries-finished-refreshing
    'Attempt to wait until the last connection has finished refreshing.
    Do Until Linit.gvTbl_ZZZZZ.QueryTable.Refreshing = True   'Wait until the last table starts refreshing,
        idx = idx + 1                                           'Icrement a loop count,
        If idx > 3000 Then Exit Do                              'If the loop goes longer then 3000 iterations exit,
    Loop                                                      'otherwise continue waiting.
    VBA.DoEvents                                              'Do events before continueing (doens't work).
    Do Until Linit.gvTbl_ZZZZZ.QueryTable.Refreshing = False  'Wait until the last table finishes refreshing,
        idx = idx + 1                                           'Icrement a loop count,
        If idx > 3000 Then Exit Do                              'If the loop goes longer then 3000 iterations exit,
    Loop                                                      'otherwise continue waiting.
    VBA.DoEvents                                              'Do events before continueing (doens't work).
    'DEV: The following is an attempt to get connections to
    '     finish refreshing before code continues as suggested here:
    'https://dev59.com/gGEh5IYBdhLWcg3wvVh3
    Application.CalculateUntilAsyncQueriesDone         'This is placed here as well as after the refresh.
    VBA.DoEvents                                              'Do events before continueing (doens't work).
    Application.EnableEvents = False                          'Maybe turning off events helps? (nope...),
    Application.ScreenUpdating = False 'This is reset in the procedure called as an argument to the next line:
    Application.OnTime DateAdd("s", 3, Now), _
                       "Lwksh.sht_sub_Msg_RefreshDone"        'The called procedure just displays a message box.
    Application.EnableEvents = True                           'Restore events,
    Application.ScreenUpdating = True                         'Restore screen updating.
    
    'MEMORY CLEANUP:
    '--------------'
EXIT_CLEAN:
    procName = Empty                                     
    Set qTblLst = Nothing
    Set qTblObj = Nothing
    Set conLst = Nothing
    Set conObj = Nothing
    idx = 0
End Sub

代码注释:

  • 代码中以“Linit.”开头的任何内容都是由代码中“INITIALIZATIONS:”部分中的“Linit.ini_Setup_Project”过程调用在过程外全局设置的对象或变量。
  • 例如,“Linit.gvTbl_ZZZZZ”是一个对象变量,指向一个空的一行表格,该表格的名称以“zzzz”开头,由Power Query生成并加载到Excel工作表中。代码显示了使用这样的空表格建议的网站链接。

问题:

  1. 由于Power Query没有内置回调来告诉Excel它已完成更新任何刷新过程,因此这是否是一个无望的事情?
  2. 如果这不是一个无望的事情,那么是否有其他方法(未在此处描述)可以用来触发错误,以某种方式使连接尚未完成刷新,或在连接完成时触发错误?(这里的想法是,如果错误不会停止查询完成,则可以将此错误作为可能检测到刷新是否已完成的一种方式进行捕获。)
  3. 是否有办法直接使用VBA探测连接流,以查找连接关闭或完成状态?
  4. 是否有办法通过调用外部程序(如C#或Python)直接访问刷新过程?
  5. 您能想到还有什么其他的方法可以尝试或测试以使其工作吗?我将继续自己寻找答案,但经过一整年的搜索,我感到有点运气不佳。

@ Mike: “TLDR”已得到理解。所有信息都包含在内,以完整描述问题,这为那些愿意浏览以获取问题所需内容的人提供了清晰度。感谢您的回答。在确保您发布的内容是否有效或仍无效后,我将在您的帖子下进一步发表评论。 - neurojelly
1
虽然有点长,但我认为它清楚地列出了问题、你尝试过的事情、研究过的事情以及你编写的代码,考虑到你是新手,做得很好! - JGlass
4个回答

3
我理解你的痛苦@neurojelly。我曾经也有过同样的经历。但事实证明,解决方案非常简单,不需要使用VBA。在查询属性窗口中,您需要取消选中“启用后台刷新”,然后使用DoEvents。我确信这个方法可行,因为我已经使用了一年多了。请查看一个包含代码的示例文件的链接。https://drive.google.com/uc?export=download&id=1ZLxSMEXPLda3QhaQoTyGGv3_sC-tpN-X

Disable background refresh

关于你的第二个问题,可以使用Iferror/OnEror方法来检测查询是否返回错误,但它不一定检查查询中的错误。它仅识别查询本身是否返回一个错误弹出窗口,而在运行VBA代码时默认跳过该弹出窗口。这种方法大多数情况下可以工作,但并非总是有效。

2

Here is a workaround

Sub MyProcedure()

    '
    ' Some procedures
    '
    Call ActiveWorkbook.RefreshAll
    Call NotifyWhenRefreshComplete
End Sub



Private Sub NotifyWhenRefreshComplete()
    Const PulseTimer As Currency = TimeValue("00:00:01")
    Dim b1 As Boolean, b2 As Boolean

    b1 = Sheet1.Range("ListObject1").ListObject.QueryTable.Refreshing
    b2 = Sheet1.Range("ListObject2").ListObject.QueryTable.Refreshing

    If b1 Or b2 Then
        Call Application.OnTime(Now + PulseTimer, "NotifyWhenRefreshComplete")
    Else
        Call MsgBox("Refresh Complete.", vbOKOnly)
    End If
End Sub

ListObject1和ListObject2是已发布的表格。只有已发布的表格需要检查是否刷新完成。您不必检查未发布的表格。
但是,如果您有很多已发布的表格,则可以遍历所有ActiveWorkbook.connections并检查每个wbConn.OLEDBConnection.Refreshing状态是否返回到false状态,替换b1b2布尔值。
注意:由于某些原因,我拒绝使用DoEvents,我希望我的用户能够在连接仍在运行时继续使用Excel,并在刷新完成时提示他们消息。但是,如果您想要使用Do... Loop迭代而不是上面显示的OnTime调用程序实现它,请继续。
最后,Public WithEvents qt As QueryTable下有一个回调函数,请查找qt.refreshing。您也可以使用此方法。
希望这可以帮助您。

-1

*********** 已解决 *************

请检查您是否已激活屏幕更新。它起作用了。

Sub

...

Application.ScreenUpdating = True

tabla.QueryTable.Refresh BackgroundQuery:=False

Do While Application.CalculationState <> xlDone

DoEvents

Loop

...

End Sub

-1

我正在ThisWorkbook.Connections.Ranges(1).ListObject.QueryTable上调用.Refresh方法,BackgroundQuery:=False。它似乎非常可靠-到目前为止,我的宏可能已经调用了该方法超过10,000次,而错误/冻结率非常低。

据我所知,当PQ在Excel 2016的对象模型中得到适当暴露时,所有这些DoEvents解决方法等都变得过时了。

以下是一个粗略的代码示例:

Dim cn As WorkbookConnection
For Each cn In ThisWorkbook.Connections
    cn.Ranges(1).ListObject.QueryTable.Refresh BackgroundQuery:=False
Next cn

在 For 循环内,您可以检查 cn.Name 以控制单个查询的执行。Name 属性遵循工作簿连接名称,例如 "Query - " & PQ 查询名称。


由于以下原因,很难测试这是否有效。我的代码中查询生成的主表仅依赖于连接查询,而您提供的代码在For Each循环的中心行上挂起运行时错误'-2147418113(8000ffff)"对象'Ranges'的方法'_Default'失败。其中错误仅发生在我的测试中的连接查询上。我的连接查询是一个函数,它有助于使后续查询的源相对于工作簿位置。因此,它无法输出到表格中。有什么建议吗? - neurojelly
好的,我已经测试过更静态的方法,只需设置一个代表表格的连接对象,并使用与您的for循环示例中心相同的字符串。 在我的情况下,如下所示:conObj.Ranges(1).ListObject.QueryTable.Refresh BackgroundQuery:=False但是,当刷新仍在进行时,代码执行仍然会继续。 换句话说,在显示和关闭消息框时,查询和连接窗格中显示的旋转器仍在旋转。 在刷新仍在进行时,工作簿中的其他单元格也可以被选中。 - neurojelly
我无法确定你的解决方案是否有效。似乎没有可验证的方法来测试查询是否已完成刷新。如果 VBA 可以在查询完成之前执行,那么使用 VBA 编辑生成的查询表是否是一个合适的测试方式?我不确定这不会导致误报。例如,如果查询只更新上次刷新后需要更改的单元格,则可能出现误报。你认为呢?你会如何测试呢?将整个表更改一遍,这样做足以作为一个好的测试吗? - neurojelly
我很感激并珍视您的意见,但是我有些难以理解您的反馈。从我的角度来看,您发表的每一条评论似乎都有些模糊不清。例如,在我们最后一次评论中,您说“这些都可以可靠地工作 - 进一步证明它确实有效...”,但是这究竟是怎样证明它有效的呢?如果代码在刷新完成之前可以继续运行,那么您的SaveAs示例只是说明在刷新尚未完成时可以执行SaveAs操作,对吗? - neurojelly
我已经看到了超过10,000次执行的证据,证明它按照我的描述运作。那么你有什么证据证明它不行呢? - Mike Honey
显示剩余5条评论

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