如何判断Delphi应用程序是否“拥有”其控制台?

时间:2009-09-27 02:02:16

标签: delphi console-application

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

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

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

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

6 个答案:

答案 0 :(得分:8)

你基本上要测试两件事:

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

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

  3. 对于第一个,有一个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;
    

    此代码仅供您入门,我确信有些角落案例未得到妥善处理。

答案 1 :(得分:4)

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


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.

答案 2 :(得分:2)

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

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

当您从资源管理器等通过单击或开始运行来运行它时,当子系统属性为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标头信息并将其修改为两个函数: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;

如果您想在子系统中获得更多信息,只需谷歌或访问MSDN网站。 希望它对任何人都有帮助。

格尔茨, Erwin Haantjes

答案 3 :(得分: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;

答案 4 :(得分:2)

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

所以你可以这样做:

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;

干杯队友!

Erwin Haantjes

答案 5 :(得分:1)

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

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

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