优雅地退出资源管理器(程序化)

36

如何在程序中 优雅地 关闭资源管理器(Explorer)?

即,在程序中如何调用以下函数:

注:图片中有错别字,应该是 "Ctrl-Shift-Right-Click" 而不是 "Shift-Click"。


1
你为什么想要这样做?你是在卸载 shell 扩展吗?如果是的话,请记住,shell 扩展可以加载到任何应用程序中... - Anders
@Anders:因为有时候资源管理器会出现故障,我需要关闭它,修改一些文件,然后重新打开它。 - user541686
出于好奇,当你选择“退出资源管理器”时会发生什么? 资源管理器会完全消失(包括开始菜单、任务栏等)吗?你可以附加windbg到explorer.exe并在TrackPopupMenu(Ex)上设置断点,并通过逆向跟踪代码来查看它实际执行了什么。 - Luke
@Mehrdad:而且你不隐藏标签... - Andreas Rejbrand
1
@David:问题的重点在于要避免这种情况... :P - user541686
显示剩余9条评论
4个回答

56
我出于好奇调试了一下。它只是向浏览器窗口之一发布消息:
BOOL ExitExplorer()
{
    HWND hWndTray = FindWindow(_T("Shell_TrayWnd"), NULL);
    return PostMessage(hWndTray, 0x5B4, 0, 0);
}

当然,这是一个未记录的 WM_USER 消息,因此其行为在未来可能会发生改变。

1
这个答案需要很多赞!非常感谢你抽出时间找到它! :) - user541686
1
@Luke:感谢你详细的分析和关于0x5B4用户消息发送到Shell_TrayWnd的提示!我发了一个相关的答案,可能会对你有兴趣。(不知道你是否已经注意到。之前我没有在所有地方都有评论的特权) - msp
2
注意1:这在Windows 7上完美运行。但是在Windows 10上,微软实施了3秒的延迟。这意味着您发送此消息并且所有资源管理器窗口立即隐藏,但Explorer.exe仍在后台隐身运行。您可以在循环中调用IsWindow(hWndTray)等待Explorer最终退出。注意2:如果您想稍后重新启动资源管理器,则不能使用ShellExecute(),否则会打开一个没有任务栏的新资源管理器。要正确重新启动资源管理器,必须改用CreateProcess()。 - Elmue

33

@Luke: 首先,感谢您对0x5B4用户消息和Shell_TrayWnd的详细分析和提示!

不幸的是,该方法有两个缺点;首先,它使用了一个未记录的用户消息,可能会在未来的Windows版本中更改;其次,在Windows XP下,它无法使用,因为退出Windows的“神奇过程”不同(打开关闭对话框,然后按SHIFT-CTRL-ALT-ESC取消),并且没有涉及到消息发送。

如果能有一种可靠且可移植的方式从另一个进程中清理终止资源管理器,无论windows版本如何都很好。因此,我继续调试终止资源管理器的代码反汇编,以找到我可以实现此目标的提示。我仍然没有完美的解决方案,但我做出了一些有趣的观察(在Windows 7和Windows XP上),我想与所有可能感兴趣的人分享:

Windows 7

0x5B4消息最终由方法CTray::_DoExitExplorer处理。如果启用了符号服务器,则可以在以下位置设置断点:

{,,explorer.exe}CTray::_DoExitExplorer(Visual Studio语法)

explorer!CTray::_DoExitExplorer(Windbg语法)

Windows XP

在WinXP中,您必须在以下位置设置断点:

{,,explorer.exe}CTray::_ExitExplorerCleanly(Visual Studio语法)

explorer!CTray::_ExitExplorer(Windbg语法)

然后您可以输入“魔术按键”(SHIFT-CTRL-ALT-ESC)。两种方法非常相似,正如您可以从反汇编中看到的那样(请参见后续帖子)。伪代码如下:

    if (bUnnamedVariable == FALSE) {
        g_fFakeShutdown = TRUE;  // (1)

        PostMessage(hWndProgMan, WM_QUIT, 0, TRUE);   // (2)

        if (PostMessage(hWndTray, WM_QUIT, 0, 0)) {    // (3)
            bUnnamedVariable = TRUE;
        }
    }

请注意,第一个PostMessage()调用将TRUE作为lParam传递,它在WM_QUIT中没有正式使用。lParam的含义似乎是bShutdown == TRUE。
当然,从另一个应用程序中设置g_fFakeShutdown是不可能的(或者不可行的)。因此,我测试了不同组合的PostMessage(hWndProgMan, WM_QUIT, 0, TRUE/FALSE),后面是否跟随PostMessage(hWndTray, WM_QUIT, 0, FALSE)。看起来,在Windows XP和Windows 7下,资源管理器显示不同的行为。
以下两种方法似乎是在Windows XP下终止资源管理器的好选择。不幸的是,在Windows 7下它们不起作用:
    BOOL ExitExplorer1() {
        HWND hWndProgMan = FindWindow(_T("Progman"), NULL);
        PostMessage(hWndProgMan, WM_QUIT, 0, TRUE);   // <=  lParam == TRUE !

        HWND hWndTray = FindWindow(_T("Shell_TrayWnd"), NULL);
        PostMessage(hWndTray, WM_QUIT, 0, 0); 

        return TRUE;
    } 


    BOOL ExitExplorer2() {
        HWND hWndProgMan = FindWindow(_T("Progman"), NULL);
        PostMessage(hWndProgMan, WM_QUIT, 0, FALSE);   // <=  lParam == FALSE !

        return TRUE;
    } 

在Windows XP中的行为

在这两种情况下,shell(explorer.exe)会终止,并在终止之前设置注册表键。

    HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\CleanShutdown = TRUE

可以使用Sysinternals Process Monitor观察,或在{,,explorer}_WriteCleanShutdown@4(resp.explorer!_ WriteCleanShutdown)处设置断点,可以发现以下内容:

在Windows 7中的行为

两种方法都无效:尽管外壳似乎已终止,但explorer.exe进程仍在运行。

备注

如果我只向hWndProgMan发布带有lParam = TRUE的WM_QUIT消息而没有向hWndTray发布消息,即

    BOOL ExitExplorer3() {
        HWND hWndProgMan = FindWindow(_T("Progman"), NULL);
        PostMessage(hWndProgMan, WM_QUIT, 0, TRUE); 

        return TRUE;
    } 

然后出现了一个有趣的行为(Win7和WinXP都有):关闭对话框出现。如果您取消,一切似乎正常,但是两到三秒后,资源管理器会终止。

结论

也许最好的解决方案是在Windows 7中使用带有未记录的WM_USER功能的ExitExplorer(),在Windows XP中使用ExitExplorer1()或ExitExplorer2()。这两种XP方法中的哪一种有优势?我不知道。

附录

CTray::_DoExitExplorer(Windows 7)和CTray::_ExitExplorerCleanly(Windows XP)的反汇编。

Windows 7

    {,,explorer.exe}CTray::_DoExitExplorer:
    explorer!CTray::_DoExitExplorer:
    00fdde24 833df027020100  cmp     dword ptr [explorer!g_fInSizeMove+0x4 (010227f0)],0 ds:0023:010227f0=00000000
    00fdde2b 53              push    ebx
    00fdde2c 8bd9            mov     ebx,ecx
    00fdde2e 7535            jne     explorer!CTray::_DoExitExplorer+0x41 (00fdde65)
    00fdde30 56              push    esi
    00fdde31 8b35ec14f700    mov     esi,dword ptr [explorer!_imp__PostMessageW (00f714ec)]
    00fdde37 57              push    edi
    00fdde38 33ff            xor     edi,edi
    00fdde3a 47              inc     edi
    00fdde3b 57              push    edi
    00fdde3c 6a00            push    0
    00fdde3e 6a12            push    12h
    00fdde40 ff35e8000201    push    dword ptr [explorer!v_hwndDesktop (010200e8)]
    00fdde46 893ddc270201    mov     dword ptr [explorer!g_fFakeShutdown (010227dc)],edi
    00fdde4c ffd6            call    esi
    00fdde4e 6a00            push    0
    00fdde50 6a00            push    0
    00fdde52 6a12            push    12h
    00fdde54 ff7304          push    dword ptr [ebx+4]
    00fdde57 ffd6            call    esi
    00fdde59 85c0            test    eax,eax
    00fdde5b 7406            je      explorer!CTray::_DoExitExplorer+0x3f (00fdde63)
    00fdde5d 893df0270201    mov     dword ptr [explorer!g_fInSizeMove+0x4 (010227f0)],edi
    00fdde63 5f              pop     edi
    00fdde64 5e              pop     esi
    00fdde65 a1f0270201      mov     eax,dword ptr [explorer!g_fInSizeMove+0x4 (010227f0)]
    00fdde6a 5b              pop     ebx
    00fdde6b c3              ret

("bUnnamedVariable"是一个模块全局变量,位于地址g_fInSizeMove+4处)

Windows XP

    {,,explorer.exe}CTray::_ExitExplorerCleanly:
    01031973 8B FF            mov         edi,edi 
    01031975 57               push        edi  
    01031976 8B F9            mov         edi,ecx 
    01031978 83 BF 40 04 00 00 00 cmp         dword ptr [edi+440h],0 
    0103197F 75 35            jne         CTray::_ExitExplorerCleanly+43h (10319B6h) 
    01031981 53               push        ebx  
    01031982 56               push        esi  
    01031983 8B 35 94 17 00 01 mov         esi,dword ptr [__imp__PostMessageW@16 (1001794h)] 
    01031989 33 DB            xor         ebx,ebx 
    0103198B 43               inc         ebx  
    0103198C 53               push        ebx  
    0103198D 6A 00            push        0    
    0103198F 6A 12            push        12h  
    01031991 FF 35 8C 60 04 01 push        dword ptr [_v_hwndDesktop (104608Ch)] 
    01031997 89 1D 48 77 04 01 mov         dword ptr [_g_fFakeShutdown (1047748h)],ebx 
    0103199D FF D6            call        esi  
    0103199F 6A 00            push        0    
    010319A1 6A 00            push        0    
    010319A3 6A 12            push        12h  
    010319A5 FF 77 04         push        dword ptr [edi+4] 
    010319A8 FF D6            call        esi  
    010319AA 85 C0            test        eax,eax 
    010319AC 74 06            je          CTray::_ExitExplorerCleanly+41h (10319B4h) 
    010319AE 89 9F 40 04 00 00 mov         dword ptr [edi+440h],ebx 
    010319B4 5E               pop         esi  
    010319B5 5B               pop         ebx  
    010319B6 8B 87 40 04 00 00 mov         eax,dword ptr [edi+440h] 
    010319BC 5F               pop         edi  
    010319BD C3               ret              

('bUnnamedVariable' 在 CTray 的相对偏移量为440h处似乎是成员)

备注 看起来在这里非常不标准地使用了 WM_QUIT,与 MSDN 上的以下摘录进行比较 WM_QUIT on MSDN

此消息没有返回值,因为它会导致消息循环在将消息发送到应用程序的窗口过程之前终止。

备注:WM_QUIT 消息与窗口无关,因此永远不会通过窗口的窗口过程接收到。它仅由 GetMessage 或 PeekMessage 函数检索。

请勿使用 PostMessage 函数发布 WM_QUIT 消息;请使用 PostQuitMessage。


2
+1 这是一篇非常好的帖子,感谢分享。我希望我能多点赞它,哈哈.. :) - user541686
这是从Windows x64安装中获取的原始Windows 7反汇编。我用来自Windows 7 32位的反汇编替换了它。现在Windows 7和Windows XP之间的相似性更加明显。 - msp
Msp,请问您知道如何使用Windows批处理文件来实现这个吗?在启动其他程序之前,我需要关闭资源管理器,而我已经厌倦了手动操作。 - Tomáš Zato
Tomas:最佳解决方案可能是编写一个小的C程序,您可以从批处理文件中调用。阅读Alex关于RestartManager API的帖子,听起来很有前途。 - msp
1
我一直在尝试让它在XP中工作,但无济于事。当您通过ctrl+alt+shift+Cancel路线进行操作时,它会神奇地工作。按照上述方式发布WM_QUIT时,它可以工作,但是当再次启动Explorer时,它会继续执行Startup/Run中的所有内容! ctrl+alt+shift+Cancel在某个地方设置了某种魔法标志,以指示会话未被终止,而WM_QUIT则使其相信会话已终止。我可以一遍又一遍地重现这个问题,但是Process Monitor没有明确指示这个标志可能在哪里。有什么想法吗? - Daniel Beardsmore
显示剩余3条评论

10

在Windows Vista和更高版本中,您可以使用RestartManager API优雅地关闭资源管理器。

这是伪代码的样子:

RmStartSession(...); 
RM_UNIQUE_PROCESS[] processes = GetProcesses("explorer.exe"); // get special handles to process you want to close    
RmRegisterResources(processes); // register those processes with restart manager session    
RmShutdown(RM_SHUTDOWN_TYPE.RmForceShutdown);     
RmRestart(...); // restart them back, optionally    
RmEndSession(...); 

1
这是唯一的方法,如果有多个explorer.exe进程,可以通过设置“在单独的进程中启动文件夹窗口”,使用shift + 右键单击文件夹并选择在新进程中打开,或使用/separate命令行选项来自动完成。0x5B4消息显然不支持非Shell_TrayWnd窗口,它只会关闭主要的explorer.exe进程,而不是其他进程。 - coderforlife
API页面:https://msdn.microsoft.com/zh-cn/library/windows/desktop/aa373656(v=vs.85).aspx | 如何使用说明:https://msdn.microsoft.com/zh-cn/library/windows/desktop/aa373665(v=vs.85).aspx - kayleeFrye_onDeck
如果你不熟悉WinAPIs,这是一些非常邪恶的伪代码 xD 第1行没问题,但第2行和第3行很让人头疼。 - kayleeFrye_onDeck

0

@cprogrammer:一定有办法……这就是菜单项的作用。 - user541686
@Mehrdad:你认为菜单项为什么要“优雅地”关闭资源管理器? - Cody Gray
@Cody:因为(1)关闭资源管理器比强制结束进程需要更长的时间,而且(2)我怀疑微软会费尽心思地隐藏一个可以强制结束资源管理器的菜单项,当用户可以在任务管理器中直接结束它。 - user541686
@cprogrammer:我应该将它发送到哪个窗口?我尝试将其发送到 GetDesktopWindow(),但它没有任何作用... - user541686
使用EnumProcesses或FindWindow函数来查找资源管理器窗口,然后使用dwThreadId=GetWindowThreadProcessId获取线程ID,最后使用::PostThreadMessage(dwThreadId, WM_QUIT/WM_CLOSE, NULL, NULL)发送消息以关闭窗口。或者可以直接使用Endtask函数关闭指定hwnd的窗口。 - cprogrammer

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