【问题标题】:How to hide all windows when using .Run in VBA, when windowStyle=0 is not sufficient在 VBA 中使用 .Run 时如何隐藏所有窗口,当 windowStyle=0 不够时
【发布时间】:2017-12-13 11:11:45
【问题描述】:

在 VBA 中使用 .Run 启动 .exe 时,典型的调用可能如下所示:

x = wsh.Run(Command:="program.exe ""argument""", WindowStyle:=0, waitonreturn:=False)

windowStyle=0 理论上应该会导致程序在用户不可见的情况下运行。但是,如果 .exe 中出现您不想让用户看到的弹出窗口怎么办?

windowStyle 输入不会抑制警告消息的出现或向用户显示声明“计算完成”之类的弹出窗口,这通常也会暂停代码,直到弹出窗口被清除。以自动方式清除窗口(即单击“确定”)是微不足道的(请参阅this answer),但对于我作为一个相对初学者来说,从一开始就阻止它出现在用户面前是很困难的。 (即当 .exe 触发弹出窗口时,它对用户是不可见的,然后由 VBA 代码自动关闭)

目前我使用这个函数检测到一个新的弹出窗口的存在(其中sCaption是弹出窗口的名称):

Private Function GetHandleFromPartialCaption(ByRef lWnd As Long, ByVal sCaption As String) As Boolean

Dim lhWndP As Long
Dim sStr As String
GetHandleFromPartialCaption = False
lhWndP = FindWindow(vbNullString, vbNullString) 'PARENT WINDOW
Do While lhWndP <> 0
    sStr = String(GetWindowTextLength(lhWndP) + 1, Chr$(0))
    GetWindowText lhWndP, sStr, Len(sStr)
    sStr = Left$(sStr, Len(sStr) - 1)
    If InStr(1, sStr, sCaption) > 0 Then
        GetHandleFromPartialCaption = True
        lWnd = lhWndP
        Exit Do
    End If
    lhWndP = GetWindow(lhWndP, GW_HWNDNEXT)
    Loop
End Function

然后自动关闭它。但它仍然会在屏幕上短暂地向用户闪现。理想情况下,我希望这个 VBA 代码在后台运行,这样用户就可以在运行时继续执行其他任务,而不会被闪烁的框分心。

有没有办法强制 program.exe 的所有窗口(包括弹出窗口)在运行时不可见?

有关更多信息,请参阅我之前关于如何关闭弹出窗口的问题,here。该线程涉及如何防止其出现给用户。

编辑 1

SendKeys 是喜怒无常的,所以当我检测到弹出窗口时,我使用这个循环代码来杀死 .exe,因此 .exe 不需要聚焦来关闭弹出窗口(关闭弹出窗口会杀死.exe 在我的情况下):

....
Main Code Body
....
    t = Now
    waittime = Now + TimeValue("0:01:30") 'limit to run a single row of calculations 
    Do While t < waittime
        If GetHandleFromPartialCaption(lhWndP, "Popup Window Text") = True Then
               Set oServ = GetObject("winmgmts:")
               Set cProc = oServ.ExecQuery("Select * from Win32_Process")
                  For Each oProc In cProc
                      If oProc.Name = "Program.exe" Then 
                         errReturnCode = oProc.Terminate() 
                         Marker2 = 1
                         Exit Do
                      End If
                  Next
        Endif
    Loop
....
Main Code Body Continues
....

其中GetHandleFromPartialCaption() 是上面的函数,根据 sCaption 参数查找弹出窗口。我的代码在 .exe 运行计算时不断循环并搜索弹出窗口,并在 .exe 出现时立即终止它。但它仍然会闪现给用户。

【问题讨论】:

  • 即使您不显示弹出窗口,它也需要有焦点才能使用SendKeys 关闭它。这意味着您的用户会在他或她正在做的事情中被打断,例如,如果他或她正在打字。
  • 我使用Terminate() 来杀死.exe 而不是SendKeys,因为它太喜怒无常并且不断打开和关闭numlock。请参阅上面我的帖子中的编辑 1
  • 我不是 100% 确定,但是如果强制您的电子表格始终位于所有其他窗口之上呢?并使用诸如 application.displayalerts = false 之类的东西来阻止提示出现(这些提示不会在您的电子表格之外,但可能会有所帮助)
  • @QHarr 请不要更改其中包含拼写错误的代码,除非您绝对确定 OP 在将代码从 IDE 转录到问题时犯了错误。代码中的拼写错误可能是问题的确切原因。
  • @TylerH 注意到虽然代码编译时不会出现该错误,这就是为什么我觉得将简单的 Thenn 更改为 Then 是安全的。

标签: vba excel hidden window-style


【解决方案1】:

要运行完全隐藏的应用程序,请在另一个 desktopCreateProcess 中启动它。

这是一个执行简单命令行并等待进程退出的示例:

Option Explicit

Private Declare PtrSafe Function OpenDesktop Lib "user32.dll" Alias "OpenDesktopW" (ByVal lpszDesktop As LongPtr, ByVal dwFlags As Long, ByVal fInherit As Byte, ByVal dwDesiredAccess As Long) As LongPtr
Private Declare PtrSafe Function CreateDesktop Lib "user32.dll" Alias "CreateDesktopW" (ByVal lpszDesktop As LongPtr, ByVal lpszDevice As LongPtr, ByVal pDevmode As LongPtr, ByVal dwFlags As Long, ByVal dwDesiredAccess As Long, ByVal lpsa As LongPtr) As LongPtr
Private Declare PtrSafe Function CloseDesktop Lib "user32.dll" (ByVal hDesktop As LongPtr) As Long
Private Declare PtrSafe Function CreateProcess Lib "kernel32.dll" Alias "CreateProcessW" (ByVal lpApplicationName As LongPtr, ByVal lpCommandLine As LongPtr, ByVal lpProcessAttributes As LongPtr, ByVal lpThreadAttributes As LongPtr, ByVal bInheritHandles As Byte, ByVal dwCreationFlags As Long, ByVal lpEnvironment As LongPtr, ByVal lpCurrentDirectory As LongPtr, ByRef lpStartupInfo As STARTUPINFO, ByRef lpProcessInformation As PROCESS_INFORMATION) As Long
Private Declare PtrSafe Function WaitForSingleObject Lib "kernel32.dll" (ByVal hHandle As LongPtr, ByVal dwMilliseconds As Long) As Long
Private Declare PtrSafe Function GetExitCodeProcess Lib "kernel32.dll" (ByVal hProcess As LongPtr, ByRef lpExitCode As Long) As Long
Private Declare PtrSafe Function CloseHandle Lib "kernel32.dll" (ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function GetWindowText Lib "user32.dll" Alias "GetWindowTextW" (ByVal hwnd As LongPtr, ByVal lpString As LongPtr, ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function EnumDesktopWindows Lib "user32.dll" (ByVal hDesktop As LongPtr, ByVal lpfn As LongPtr, ByRef lParam As Any) As Long
Private Declare PtrSafe Function SendMessageW Lib "user32.dll" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
Private Declare PtrSafe Function GetLastError Lib "kernel32.dll" () As Long

Private Type STARTUPINFO
  cb                  As Long
  lpReserved          As LongPtr
  lpDesktop           As LongPtr
  lpTitle             As LongPtr
  dwX                 As Long
  dwY                 As Long
  dwXSize             As Long
  dwYSize             As Long
  dwXCountChars       As Long
  dwYCountChars       As Long
  dwFillAttribute     As Long
  dwFlags             As Long
  wShowWindow         As Integer
  cbReserved2         As Integer
  lpReserved2         As LongPtr
  hStdInput           As LongPtr
  hStdOutput          As LongPtr
  hStdError           As LongPtr
End Type

Private Type PROCESS_INFORMATION
  hProcess            As LongPtr
  hThread             As LongPtr
  dwProcessID         As Long
  dwThreadID          As Long
End Type


Public Sub UsageExample()
  Dim exitCode As Long
  exitCode = ExecuteHidden("cmd /C echo abcd > %USERPROFILE%\Desktop\output.txt", timeoutMs:=10000)
End Sub

Public Function ExecuteHidden(command As String, timeoutMs As Long) As Long
  Dim si As STARTUPINFO, pi As PROCESS_INFORMATION, hDesktop As LongPtr, ex As Long
  Const NORMAL_PRIORITY_CLASS& = &H20&, INFINITE& = &HFFFFFFFF, GENERIC_ALL& = &H10000000

  On Error GoTo Catch

  ' get a virtual desktop '
  si.lpDesktop = StrPtr("hidden-desktop")
  hDesktop = OpenDesktop(si.lpDesktop, 0, 0, GENERIC_ALL)
  If hDesktop Then Else hDesktop = CreateDesktop(si.lpDesktop, 0, 0, 0, GENERIC_ALL, 0)
  If hDesktop Then Else Err.Raise GetLastError()

  ' run the command '
  si.cb = LenB(si)
  If CreateProcess(0, StrPtr(command), 0, 0, 1, NORMAL_PRIORITY_CLASS, 0, 0, si, pi) Then Else Err.Raise GetLastError()

  ' wait for exit '
  If WaitForSingleObject(pi.hProcess, timeoutMs) Then Err.Raise 1000, , "Timeout while waiting for the process to exit"
  If GetExitCodeProcess(pi.hProcess, ExecuteHidden) <> 0 Then Else Err.Raise GetLastError()

  ' cleanup '
Catch:
  If pi.hThread Then CloseHandle pi.hThread
  If pi.hProcess Then CloseHandle pi.hProcess
  If hDesktop Then CloseDesktop hDesktop
  If Err.Number Then Err.Raise Err.Number
End Function

如果你需要在桌面上找到一个窗口,使用EnumDesktopWindows而不是EnumWindows

Private Function FindWindow(ByVal hDesktop As LongPtr, title As String) As LongPtr
  Dim hwnds As New Collection, hwnd, buffer$
  buffer = Space$(1024)

  EnumDesktopWindows hDesktop, AddressOf EnumDesktopWindowsProc, hwnds

  For Each hwnd In hwnds
    If Left$(buffer, GetWindowText(hwnd, StrPtr(buffer), Len(buffer))) Like title Then
      FindWindow = hwnd
      Exit Function
    End If
  Next
End Function

Private Function EnumDesktopWindowsProc(ByVal hwnd As LongPtr, hwnds As Collection) As Long
  hwnds.Add hwnd
  EnumDesktopWindowsProc = True
End Function

如果您需要关闭窗口,只需将WM_CLOSE 发送到主窗口或弹出窗口:

const WM_CLOSE& = &H10&
SendMessageW hwnd, WM_CLOSE, 0, 0

【讨论】:

  • 天哪!虚拟桌面吧?哇! Florent,你绝对是 Windows API 忍者。顺便说一句,OP 使用的是 Windows Scripting Host,它具有很好的功能,例如能够读取 stdout 和 stderr。当你在这个线程上时,你能告诉我们如何使用 CreateProcess 复制它吗?
  • @S Meaden,要读取 stdout/stderr,使用CreatePipe 创建一个管道并将其分配给STARTUPINFO。然后阅读它调用ReadFile。举个例子。看看这里:stackoverflow.com/questions/7386730/…
  • 祝福你。我必须写这个/那个以供将来参考。
  • 可爱的东西,我也会尝试实现这个修复。在尝试执行此类操作时,我可能在我所在的机器上遇到特权问题,但我会试一试。谢谢你的详细回答。
  • @Petrichor, "cmd /C" 执行命令外壳。要运行您的程序,只需调用ExecuteHidden("program.exe ""argument""", 10000)。但是,您必须对其进行自定义以处理您的弹出窗口,因为此示例只是等待程序退出。
【解决方案2】:

简短的回答是隐藏需要调用ShowOwnedPopups(hwnd,0) 的弹出窗口。 VBA 声明在这里给出

Declare Function ShowOwnedPopups Lib "user32" Alias "ShowOwnedPopups" _
   (ByVal hwnd As Long, ByVal fShow As Long) As Long

有关调查此问题的一些实验性 C# 代码的更长答案,请参阅blog post。为简洁起见,我已将博文的第一部分复制到此处的答案中。

首先,一个关键的阅读资源是Windows Features,它告诉我们所有的窗口都是用CreateWindowEx创建的,但是弹出窗口是通过指定WS_POPUP来创建的,子窗口是通过指定WS_CHILD来创建的。所以弹出窗口和子窗口是不同的。

Window Visibility 部分的同一页面上,它解释说我们可以设置主窗口的可见性,并且更改将级联到所有子窗口,但没有提到这种级联会影响弹出窗口。

这是一些最终的 VBA 代码,但它依赖于一个名为 VisibilityExperiment 的简单 C# 演示程序

Option Explicit

Private Declare Function ShowOwnedPopups Lib _
    "user32" (ByVal hwnd As Long, _
    ByVal fShow As Long) As Long

Private Declare Function EnumWindows _
    Lib "user32" ( _
        ByVal lpEnumFunc As Long, _
        ByVal lParam As Long) _
        As Long

Private Declare Function GetWindowThreadProcessId _
    Lib "user32" (ByVal hwnd As Long, lpdwprocessid As Long) As Long



Private mlPid As Long
Private mlHWnd As Variant


Private Function EnumAllWindows(ByVal hwnd As Long, ByVal lParam As Long) As Long

    Dim plProcID As Long
    GetWindowThreadProcessId hwnd, plProcID
    If plProcID = mlPid Then
        If IsEmpty(mlHWnd) Then
            mlHWnd = hwnd
            Debug.Print "HWnd:&" & Hex$(mlHWnd) & "  PID:&" & Hex$(mlPid) & "(" & mlPid & ")"
        End If
    End If

    EnumAllWindows = True
End Function

Private Function GetPID(ByVal sExe As String) As Long

    Static oServ As Object
    If oServ Is Nothing Then Set oServ = GetObject("winmgmts:\\.\root\cimv2")

    Dim cProc As Object
    Set cProc = oServ.ExecQuery("Select * from Win32_Process")

    Dim oProc As Object
    For Each oProc In cProc
        If oProc.Name = sExe Then
            Dim lPid As Long
            GetPID = oProc.ProcessID
        End If
    Next

End Function


Private Sub Test()

    Dim wsh As IWshRuntimeLibrary.WshShell
    Set wsh = New IWshRuntimeLibrary.WshShell

    Dim lWinStyle As WshWindowStyle
    lWinStyle = WshNormalFocus

    Dim sExe As String
    sExe = "VisibilityExperiment.exe"

    Dim sExeFullPath As String
    sExeFullPath = Environ$("USERPROFILE") & "\source\repos\VisibilityExperiment\VisibilityExperiment\bin\Debug\" & sExe

    Dim x As Long
    x = wsh.Run(sExeFullPath, lWinStyle, False)

    mlPid = GetPID(sExe)

    mlHWnd = Empty
    Call EnumWindows(AddressOf EnumAllWindows, 0)


    Stop
    Call ShowOwnedPopups(mlHWnd, 0)  '* o to hide, 1 to show

End Sub

要重复,要隐藏弹出窗口,必须调用 ShowOwnedPopups()。可悲的是,我看不到这个限制。即使我们尝试直接使用 Windows API 来生成进程,STARTUPINFO structure (Windows) 中也没有任何内容看起来会有所帮助,也没有任何内容可以指定弹出窗口的可见性。

【讨论】:

  • 看起来很不错的信息,谢谢。我将在今天下午晚些时候尝试实施并更新我的帖子/cmets。
【解决方案3】:

怎么样:

Dim TaskID as Double
TaskID = Shell("program.exe", vbHide)

如果窗口的行为不符合预期,请尝试vbNormalNoFocusvbMinimizedNoFocus

如果由于某种原因这不合适,请分享更多关于 .exe 的内容...也许重定向输出可能是一个选项。

我假设您无法修改“program.exe”以使用不同类型的通知?

另一种方法是强制 Excel 保持“领先”

#If Win64 Then

    Public Declare PtrSafe Function SetWindowPos _
        Lib "user32" ( _
            ByVal hwnd As LongPtr, _
            ByVal hwndInsertAfter As LongPtr, _
            ByVal x As Long, ByVal y As Long, _
            ByVal cx As Long, ByVal cy As Long, _
            ByVal wFlags As Long) _
    As Long

#Else

    Public Declare Function SetWindowPos _
        Lib "user32" ( _
            ByVal hwnd As Long, _
            ByVal hwndInsertAfter As Long, _
            ByVal x As Long, ByVal y As Long, _
            ByVal cx As Long, ByVal cy As Long, _
            ByVal wFlags As Long) _
    As Long
#End If

Public Const SWP_NOSIZE = &H1
Public Const SWP_NOMOVE = &H2
Public Const HWND_TOPMOST = -1
Public Const HWND_NOTOPMOST = -2

Sub ShowXLOnTop(ByVal OnTop As Boolean)
    Dim xStype As Long
    #If Win64 Then
        Dim xHwnd As LongPtr
    #Else
        Dim xHwnd As Long
    #End If
    If OnTop Then
        xStype = HWND_TOPMOST
    Else
        xStype = HWND_NOTOPMOST
    End If
    Call SetWindowPos(Application.hwnd, xStype, 0, 0, 0, 0, SWP_NOSIZE Or SWP_NOMOVE)
End Sub

Sub SetXLOnTop()
    ShowXLOnTop True
End Sub

Sub SetXLNormal()
    ShowXLOnTop False
End Sub

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-07-13
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    • 1970-01-01
    • 2011-03-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多