【问题标题】:How to tell if a Delphi app "owns" its console?如何判断 Delphi 应用程序是否“拥有”其控制台?
【发布时间】:2010-12-01 17:48:08
【问题描述】:

Delphi 控制台应用程序可以从现有控制台窗口的命令行运行,并且可以通过双击其图标来运行。在后一种情况下,它将创建自己的控制台窗口,并在应用程序终止后关闭它。

如何判断我的控制台应用程序是否创建了自己的窗口?

我想检测到这一点,以便我可以显示一条消息,如“按 Enter 关闭窗口”,让用户在窗口关闭之前阅读显示的内容。显然,如果应用程序是从命令行运行的,那么这样做是不合适的。

我正在使用 Delphi 2010,以防万一。

【问题讨论】:

  • 应用程序可以在其自己的控制台中使用重定向输出运行,在这种情况下您也不应该显示该消息。

标签: delphi console-application


【解决方案1】:

哇,尼克,这真是令人印象深刻!我已经测试了您的解决方案并且效果很好。

所以你可以这样做:

function isOutputRedirected() : boolean;
var
  StdOutHandle     : THandle;
  bIsNotRedirected : boolean;
  FileInfo         : TByHandleFileInformation;

begin
  StdOutHandle:= GetStdHandle(STD_OUTPUT_HANDLE);
  bIsNotRedirected:=( NOT GetFileInformationByHandle(StdOutHandle, FileInfo)
    and (GetLastError = ERROR_INVALID_HANDLE));
  Result:=( NOT bIsNotRedirected );
end;

function isStartedFromConsole() : boolean;
var
  SI: TStartupInfo;
begin
  SI.cb := SizeOf(TStartupInfo);
  GetStartupInfo(SI);
  Result := ((SI.dwFlags and STARTF_USESHOWWINDOW) = 0);
end;

function GetConsoleSize() : _COORD;
var
  BufferInfo: TConsoleScreenBufferInfo;
begin
  GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), BufferInfo);
  Result.x:=BufferInfo.srWindow.Right - BufferInfo.srWindow.Left + 1;
  Result.y:=BufferInfo.srWindow.Bottom - BufferInfo.srWindow.Top + 1;
end;

最后:

var
 cKey : Char;
 fCursorPos  : _COORD;

    if( NOT isOutputRedirected() ) and( NOT isStartedFromConsole() ) then
           begin
             // Windows app starts console.
             // Show message in yellow (highlight) and at the bottom of the window
            writeln;
            fCursorPos:=getConsoleSize();
            Dec( fCursorPos.y );
            Dec( fCursorPos.x, 40 );
            SetConsoleTextAttribute( GetStdHandle(STD_OUTPUT_HANDLE), 14 );
            SetConsoleCursorPosition( GetStdHandle(STD_OUTPUT_HANDLE), fCursorPos );
            write( '<< Press ENTER to close this window >>' );
            read(cKey);
           end;

干杯!

欧文·汉杰斯

【讨论】:

    【解决方案2】:

    我用过(不记得在哪里找到的):

    function WasRanFromConsole() : Boolean;
    var
      SI: TStartupInfo;
    begin
      SI.cb := SizeOf(TStartupInfo);
      GetStartupInfo(SI);
    
      Result := ((SI.dwFlags and STARTF_USESHOWWINDOW) = 0);
    end;
    

    然后这样使用它:

      if (not WasRanFromConsole()) then
      begin
        Writeln('');
        Writeln('Press ENTER to continue');
        Readln;
      end;
    

    【讨论】:

      【解决方案3】:

      我知道,这是一个旧线程,但我有一个很好的解决方案。

      您不必处理批处理文件。诀窍在于 exe 的类型,它的子系统属性。将 exe 编译为 GUI 应用程序后(没有 {$APPTYPE CONSOLE} 指令,您必须将其子系统属性 IMAGE_SUBSYSTEM_WINDOWS_GUI 更改为 IMAGE_SUBSYSTEM_WINDOWS_CUI。好消息是当您从控制台执行控制台应用程序时,它不会显示额外的控制台窗口,并且在那时你不需要像“按 Enter 关闭窗口”这样的消息。编辑:如果你在控制台应用程序中启动另一个控制台应用程序,就像我在我的项目中所做的那样) p>

      当您从资源管理器等通过单击它或通过开始|运行运行它时,当子系统属性为 IMAGE_SUBSYSTEM_WINDOWS_CUI 时,Windows 会自动打开一个控制台窗口。你不需要指定 {$APPTYPE CONSOLE} 指令,它都是关于子系统属性的。

      RRUZ 的解决方案是我也使用的解决方案,但有一个重要区别。我检查父进程的子系统以显示“按 Enter 关闭此窗口”。 RUZZ 它的解决方案只在两种情况下有效,当它是 cmd 或 explorer 时。只需检查它的父进程是否具有 NOT IMAGE_SUBSYSTEM_WINDOWS_CUI 属性,就可以显示该消息。

      但是如何检查exe子系统呢?我在 torry 提示 (http://www.swissdelphicenter.ch/torry/showcode.php?id=1302) 上找到了一个解决方案,用于获取 PE Header 信息并将其修改为两个函数:setExeSubSys() 和 getExeSubSys()。使用 setExeSubSys() 我制作了一个小控制台应用程序,这样我就可以在编译后更改 exe 的子系统属性(它只有 50 kb!)。

      获得父/潜在进程文件名后,您可以简单地执行以下操作:

          //In the very beginning in the app determine the parent process (as fast as is possible).
      // later on you can do:
      if( getExeSubSys( parentFilename ) <> IMAGE_SUBSYSTEM_WINDOWS_CUI ) then
       begin
        writeln( 'Press Enter to close the window' );
        readln;
       end;
      

      这是我创建的两个函数,但它不适用于流(如 torry 示例),我使用我自己的简单单元来处理文件,而没有愚蠢的例外。但基本上我认为你已经明白了。

      设置(以及当你没有指定指向 longint (nil) 的指针时获取):

      type
       PLongInt = ^LongInt;
      
      function setExeSubSys( fileName : string; pSubSystemId : PLongInt = nil ) : LongInt;
      var
        signature: DWORD;
        dos_header: IMAGE_DOS_HEADER;
        pe_header: IMAGE_FILE_HEADER;
        opt_header: IMAGE_OPTIONAL_HEADER;
        f : TFile;
      
      begin
       Result:=-1;
       FillChar( f, sizeOf( f ), 0 );
       if( fOpenEx( f, fileName, fomReadWrite )) and ( fRead( f, dos_header, SizeOf(dos_header)))
        and ( dos_header.e_magic = IMAGE_DOS_SIGNATURE ) then
        begin
         if( fSeek( f, dos_header._lfanew )) and ( fRead( f, signature, SizeOf(signature))) and ( signature = IMAGE_NT_SIGNATURE ) then
          begin
           if( fRead( f, pe_header, SizeOf(pe_header))) and ( pe_header.SizeOfOptionalHeader > 0 ) then
            begin
             if( fRead( f, opt_header, SizeOf(opt_header))) then
              begin
               if( Assigned( pSubSystemId )) then
               begin
                opt_header.Subsystem:=pSubSystemId^;
                if( fSeek( f, fPos( f )-SizeOf(opt_header) )) then
                 begin
                  if( fWrite( f, opt_header, SizeOf(opt_header)) ) then
                   Result:=opt_header.Subsystem;
                 end;
               end
              else Result:=opt_header.Subsystem;
              end;
            end;
          end;
        end;
      
       fClose( f );
      end;
      

      获得:

      function GetExeSubSystem( fileName : string ) : LongInt;
      var
        f         : TFile;
        signature : DWORD;
        dos_header: IMAGE_DOS_HEADER;
        pe_header : IMAGE_FILE_HEADER;
        opt_header: IMAGE_OPTIONAL_HEADER;
      
      begin
       Result:=IMAGE_SUBSYSTEM_WINDOWS_CUI; // Result default is console app
      
       FillChar( f, sizeOf( f ), 0 );
      
       if( fOpenEx( f, fileName, fomRead )) and ( fRead( f, dos_header, SizeOf(dos_header)))
        and ( dos_header.e_magic = IMAGE_DOS_SIGNATURE ) then
        begin
         if( fSeek( f, dos_header._lfanew )) and ( fRead( f, signature, SizeOf(signature))) and ( signature = IMAGE_NT_SIGNATURE ) then
          begin
           if( fRead( f, pe_header, SizeOf(pe_header))) and ( pe_header.SizeOfOptionalHeader > 0 ) then
            begin
             if( fRead( f, opt_header, SizeOf(opt_header))) then
              Result:=opt_header.Subsystem;
            end;
          end;
        end;
      
       fClose( f );
      end;
      

      如果您想了解有关子系统的更多信息,只需 google 或访问 MSDN 网站。 希望它对任何人都有帮助。

      问候, 欧文·汉杰斯

      【讨论】:

      • 您好,感谢您的新答案!我以前没有玩过子系统属性,但听起来我应该检查一下。
      【解决方案4】:

      对于程序 foo.exe,制作一个名为 foo_runner.bat 的批处理文件。不要记录该命令,因为它不打算由任何人按名称使用,而是将其用作安装程序制作的任何快捷方式图标的目标。它的内容很简单:

      @echo off
      %~dp0\foo.exe %*
      pause
      

      %~dp0 部分给出了批处理文件所在的目录,因此您可以确保在批处理文件的目录中运行 foo.exe,而不是从其他地方抓取一个搜索路径。

      【讨论】:

      • 就我而言,我想避免使用批处理文件,但仍然是一个有效的解决方案。 +1
      • @Incredulous Monk:这不是对“如何判断 Delphi 应用程序是否“拥有”其控制台?”问题的答案。或者,但是如果您对仅针对安装程序创建的图标的解决方案感到满意并希望避免使用批处理文件,那么阻止您打印行并仅在存在某个命令行开关时才等待,并将该开关添加到您的程序快捷方式图标?
      【解决方案5】:

      我过去使用过类似下面的东西:

      
      program ConsoleTest;
      {$APPTYPE CONSOLE}
      uses Windows;
      function GetConsoleWindow: HWND; stdcall; external kernel32 name 'GetConsoleWindow';
      function IsOwnConsoleWindow: Boolean;
      //ONLY POSSIBLE FOR CONSOLE APPS!!!
      //If False, we're being called from the console;
      //If True, we have our own console (we weren't called from console)
      var pPID: DWORD;
      begin
        GetWindowThreadProcessId (GetConsoleWindow,pPID);
        Result:= (pPID = GetCurrentProcessId);
      end;
      
      

      begin writeln ('Hello '); if IsOwnConsoleWindow then begin writeln ('Press enter to close console'); readln; end; end.

      【讨论】:

      • 又短又甜……我喜欢! +1
      【解决方案6】:

      你基本上有两件事要测试:

      1. 应用程序控制台是否在进程之间共享?如果您使用cmd.exe 运行控制台应用程序,它将默认共享控制台,因此您不需要显示“按 Enter 关闭窗口”消息。

      2. 输出是否重定向到文件?如果是这样,也不需要显示消息。

      对于第一个,有一个简单的解决方案,形式为GetConsoleProcessList() Windows API 函数。不幸的是,它仅适用于 Windows XP 和更高版本,但也许这对您来说已经足够了。它不在 Delphi 2009 Windows 单元中,因此您必须自己导入:

      function GetConsoleProcessList(lpdwProcessList: PDWORD;
        dwProcessCount: DWORD): DWORD; stdcall; external 'kernel32.dll';
      

      当然,如果您的软件能够在早期的 Windows 版本上运行,您应该改用 LoadLibrary()GetProcAddress()

      由于您只对进程句柄的数量是否大于 1 感兴趣,因此您可以使用非常小的句柄缓冲区来调用它,例如:

      var
        HandleCount: DWORD;
        ProcessHandle: DWORD;
      begin
        HandleCount := GetConsoleProcessList(@ProcessHandle, 1);
        // ...
      end;
      

      如果您的句柄计数大于 1,您有其他进程保持控制台打开,因此您可以跳过显示消息。

      您可以使用GetFileInformationByHandle() Windows API 函数来检查您的控制台输出句柄是否引用了真实文件:

      var
        StdOutHandle: THandle;
        IsNotRedirected: boolean;
        FileInfo: TByHandleFileInformation;
      begin
        StdOutHandle := GetStdHandle(STD_OUTPUT_HANDLE);
        IsNotRedirected := not GetFileInformationByHandle(StdOutHandle, FileInfo)
          and (GetLastError = ERROR_INVALID_HANDLE);
        // ...
      end;
      

      此代码仅用于帮助您入门,我确信有些极端情况没有正确处理。

      【讨论】:

      • 解决了我的问题,也解决了我没想到的问题(重定向输出)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-11-14
      • 2018-02-23
      • 2013-08-13
      • 2023-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多