一个Win32控制台应用程序能否检测它是否是从资源管理器中运行的?

39

我必须创建一个控制台应用程序,需要某些参数。如果缺少或错误,我会打印出错误信息。

现在的问题是:如果有人通过双击资源管理器启动程序,则控制台窗口立即消失。(但是从资源管理器启动的应用程序并不完全无用,您可以将文件拖放到它上面,它也可以工作)

我可以始终等待按键,但如果用户从命令行启动它,我不想要这个。

有没有办法区分这些情况?

6个回答

33

请参考http://support.microsoft.com/kb/99115,其中有关于“INFO: 防止控制台窗口消失”的信息。

核心思路是使用GetConsoleScreenBufferInfo函数来确定光标是否移动了初始位置0,0。

下面是基于上述知识库文章的@tomlogic提供的代码示例:

// call in main() before printing to stdout
// returns TRUE if program is in its own console (cursor at 0,0) or
// FALSE if it was launched from an existing console.
// See http://support.microsoft.com/kb/99115
#include <stdio.h>
#include <windows.h>
int separate_console( void)
{
    CONSOLE_SCREEN_BUFFER_INFO csbi;

    if (!GetConsoleScreenBufferInfo( GetStdHandle( STD_OUTPUT_HANDLE), &csbi))
    {
        printf( "GetConsoleScreenBufferInfo failed: %lu\n", GetLastError());
        return FALSE;
    }

    // if cursor position is (0,0) then we were launched in a separate console
    return ((!csbi.dwCursorPosition.X) && (!csbi.dwCursorPosition.Y));
}

1
这很有创意 :) 虽然这是一个hack,但它正好满足我的需求,比获取PPID要简单得多。 - Daniel Rikowski
只有在应用程序没有传递参数时,您才应该使用此黑客技巧,这样您仍然可以从cmd.exe执行“cls&app文件名”。 - Anders
刚刚实现了这个解决方案,对于MinGW/MSYS编译的命令行应用程序非常有效。而且,@Anders,cls && appname.exe也可以正常工作。我将编辑此答案以包含我正在使用的代码,供其他人参考。 - tomlogic

7

GetConsoleTitle()

我见过一些与之相关的代码

if (!GetConsoleTitle(NULL, 0) && GetLastError() == ERROR_SUCCESS) {
    // Console
} else {
    // GUI
}

但是...我发现AttachConsole()更有帮助。在C++中(仅凭记忆,我不是C++程序员)。
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
    // GUI
} else {
    // Console, and you have a handle to the console that already exists.
}

更有效。此外,如果你发现自己在图形用户界面(GUI)环境中,并且希望尽可能长时间留在其中,但随后发现发生了一些灾难性事件,需要将其转储到控制台窗口(你不想写一个编辑框窗口来记录它或连接到NT系统日志并弹出MessageBox()),那么你可以在进程后期使用AllocConsole(),当GUI方法失败时。


2
一些反馈:我想在使用MinGW/MSYS编译的命令行应用程序中执行此操作。然而,这两种方法都对我不起作用——我无法区分将文件拖到Windows资源管理器中的应用程序和在bash shell内的命令行中执行它之间的差别。 - tomlogic

6

我找到了一个更好的解决方案,使用GetConsoleProcessList来获取当前控制台附加的进程数量。 如果这个进程是唯一附加的进程,那么当进程退出时它将被关闭。

我在这篇文章中找到了它:https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922 但是它有一个错误(至少在Windows 10中),因为文档禁止使用null调用此函数。

我的解决方案是:

DWORD procId;
DWORD count = GetConsoleProcessList(&procId, 1);
if (count < 2) ...

3

我相信cmd.exe在启动时设置了CMDCMDLINE和CMDEXTVERSION环境变量。因此,如果这些变量被设置了,你的程序很可能是从一个shell启动的。

这并不是绝对可靠的方法,但至少值得一试。

还有一些复杂而可能不太可靠的方法可以确定你的父进程ID,你可能需要进一步研究。


2
我尝试了环境变量,但它们不存在,无论是在cmd.exe中运行还是在外部运行时都是如此。然而,键入“echo %cmdcmdline%”确实会产生一些东西,因此该变量显然仅在cmd.exe本身中有效,而不是其子进程中。 - Timwi

1

以下是@DanielBenSassoon的优秀答案,适用于C#。在Visual Studio 2019和Windows 10中进行了测试。

// Gets a list of the process IDs attached to this console
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint GetConsoleProcessList(uint[] processList, uint processCount);

public static bool IsFinalProcess()
{
    // See: https://devblogs.microsoft.com/oldnewthing/20160125-00/?p=92922
    uint[] procIDs = new uint[64];
    uint processCount = GetConsoleProcessList(procIDs, 64);
    return (processCount < 2);
}

这种方法使您能够区分四种情况:
  • 从IDE调试(F5)[进程计数=1]
  • 从IDE运行但不进行调试(Ctrl + F5)[进程计数=2]
  • 在资源管理器中双击[进程计数=1]
  • 从命令提示符窗口运行[进程计数=2]
IsFinalProcess为true时,您可以使用Console.ReadKey(false);来防止应用程序退出后控制台窗口消失。

0

编辑:我已经成功地使用这个 .bat/.cmd 包装器几年了:

@ECHO OFF

REM Determine if script was executed from an interactive shell
(echo %CMDCMDLINE% | find /i "%~0" >NUL 2>&1) && (set IS_INTERACTIVE=false) || (set IS_INTERACTIVE=true)

<call console application here>

REM Wait for keystroke
if "%IS_INTERACTIVE%" == "false" (
        echo.
        echo Hit any key to close.
        pause >NUL 2>&1
)

这里的优点是,它适用于所有控制台应用程序,无论是您自己的还是别人的(您可能没有可以修改的源代码)。缺点是,您需要一个单独的包装器。


我的2014年的原始答案是:

这个方法非常有效:

@echo off
for %%x in (%cmdcmdline%) do if /i "%%~x"=="/c" goto nonconsole

:console
<do something>
goto exit

:nonconsole
<do something>
pause

:exit

这个帖子复制过来。我也试过自己评估%cmdcmdline%,但是存在一个关于引号字符(")的问题,这会导致类似if "%cmdcmdline%" == "%ComSpec%" goto [target]这样的语句无法工作。


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