如何从命令行启动的GUI应用程序写入StdOut?

时间:2013-08-10 07:37:03

标签: delphi

我正在Delphi 7中编写一个标准的Windows应用程序。

如果我正在编写控制台应用程序,我可以调用以下内容输出到cmd行或输出文件。

writeln('Some info');

如果我从我从命令行启动的标准GUI应用程序执行此操作,则会收到错误。

I/O Error 105

必须有一个简单的解决方案来解决这个问题。基本上我希望我的应用程序有两种模式,GUI模式和非GUI模式。如何正确设置以便我可以回写cmd窗口?

8 个答案:

答案 0 :(得分:10)

这个问题与我试图完成的事情非常相似(如果不完全相同)。我想检测我的应用程序是否是从cmd.exe执行并将输出发送到父控制台,否则它将显示一个gui。这里的答案帮助我解决了我的问题。以下是我作为实验提出的代码:

ParentChecker.dpr

program ParentChecker;

uses
  Vcl.Forms,
  SysUtils,
  PsAPI,
  Windows,
  TLHelp32,
  Main in 'Main.pas' {frmParentChecker};

{$R *.res}

function AttachConsole(dwProcessID: Integer): Boolean; stdcall; external 'kernel32.dll';
function FreeConsole(): Boolean; stdcall; external 'kernel32.dll';

function GetParentProcessName(): String;
const
  BufferSize = 4096;
var
  HandleSnapShot: THandle;
  EntryParentProc: TProcessEntry32;
  CurrentProcessId: THandle;
  HandleParentProc: THandle;
  ParentProcessId: THandle;
  ParentProcessFound: Boolean;
  ParentProcPath: String;
begin
  ParentProcessFound:=False;
  HandleSnapShot:=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
  if HandleSnapShot<>INVALID_HANDLE_VALUE then
  begin
    EntryParentProc.dwSize:=SizeOf(EntryParentProc);
    if Process32First(HandleSnapShot,EntryParentProc) then
    begin
      CurrentProcessId:=GetCurrentProcessId();
      repeat
        if EntryParentProc.th32ProcessID=CurrentProcessId then
        begin
          ParentProcessId:=EntryParentProc.th32ParentProcessID;
          HandleParentProc:=OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ,False,ParentProcessId);
          if HandleParentProc<>0 then
          begin
            ParentProcessFound:=True;
            SetLength(ParentProcPath,BufferSize);
            GetModuleFileNameEx(HandleParentProc,0,PChar(ParentProcPath),BufferSize);
            ParentProcPath:=PChar(ParentProcPath);
            CloseHandle(HandleParentProc);
          end;
          Break;
        end;
      until not Process32Next(HandleSnapShot,EntryParentProc);
    end;
    CloseHandle(HandleSnapShot);
  end;
  if ParentProcessFound then Result:=ParentProcPath
  else Result:='';
end;

function IsPrime(n: Integer): Boolean;
var
  i: Integer;
begin
  Result:=False;
  if n<2 then Exit;
  Result:=True;
  if n=2 then Exit;
  i:=2;
  while i<(n div i + 1) do
  begin
    if (n mod i)=0 then
    begin
      Result:=False;
      Exit;
    end;
    Inc(i);
  end;
end;

var
  i: Integer;
  ParentName: String;

begin
  ParentName:=GetParentProcessName().ToLower;
  Delete(ParentName,1,ParentName.LastIndexOf('\')+1);
  if ParentName='cmd.exe' then
  begin
    AttachConsole(-1);
    Writeln('');
    for i:=1 to 100 do if IsPrime(i) then Writeln(IntToStr(i)+' is prime');
    FreeConsole();
  end
  else
  begin
    Application.Initialize;
    Application.MainFormOnTaskbar:=True;
    Application.CreateForm(TfrmParentChecker, frmParentChecker);
    frmParentChecker.Label1.Caption:='Executed from '+ParentName;
    Application.Run;
  end;
end.

Main.pas(带标签的表格):

unit Main;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, RzLabel;

type
  TfrmParentChecker = class(TForm)
    Label1: TLabel;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmParentChecker: TfrmParentChecker;

implementation

{$R *.dfm}

end.

这允许我从命令提示符运行我的GUI应用程序并将输出显示到我的应用程序启动的同一控制台。否则,它将运行应用程序的完整GUI部分。

控制台窗口的输出示例:

I:\Delphi\Tests and Demos\ParentChecker\Win32\Debug>start /wait ParentChecker.exe

2 is prime
3 is prime
5 is prime
7 is prime
11 is prime
13 is prime
17 is prime
19 is prime
23 is prime
29 is prime
31 is prime
37 is prime
41 is prime
43 is prime
47 is prime
53 is prime
59 is prime
61 is prime
67 is prime
71 is prime
73 is prime
79 is prime
83 is prime
89 is prime
97 is prime

I:\Delphi\Tests and Demos\ParentChecker\Win32\Debug>

答案 1 :(得分:6)

致电AllocConsoleavoid错误105。

答案 2 :(得分:5)

GUI子系统应用程序没有可靠的方法可以附加到其父进程的控制台。如果您尝试这样做,最终会有两个共享同一控制台的活动进程。这导致了麻烦。

替代方案,虽然保留了一个可执行文件,如bummi所建议的,是有一个控制台应用程序,如果要求它在GUI模式下运行,则释放其控制台。这是一种更好的方法,但是当您想要在GUI模式下运行时,会导致控制台窗口闪烁,然后关闭。

我在Stack Overflow上遇到的关于这个主题的最佳讨论是Rob Kennedy的精湛答案:Can one executable be both a console and GUI application?

我相信,根据您在评论中的说法,您最好的选择是创建两个单独的可执行文件。一个用于GUI子系统,另一个用于控制台子系统。这是采取的方法:

  • Java:java.exe,javaw.exe。
  • Python:python.exe,pythonw.exe。
  • Visual Studio:devenv.com,devenv.exe。

是的,您必须发送多个可执行文件。但这样做可以为用户提供最佳体验。

答案 3 :(得分:4)

我不太确定你想要达到的目标 我理解这个问题的方法可能是

program Project1;
{$APPTYPE CONSOLE}

uses
  Forms, Classes, Windows,
  Unit1 in 'Unit1.pas' { Form1 } ;
{$R *.res}

var
  Finished: Boolean;
  Input: String;

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

procedure HandleInput;
begin
  Finished := Input = 'quit';
  if not Finished then
  begin
    Writeln('Echo: ' + Input);
  end
  else
    Writeln('Bye');
end;

begin
  if IsConsoleMode then
  begin
    Finished := false;
    Writeln('Welcome to console mode');
    while not Finished do
    begin
      readln(Input);
      HandleInput;
    end;
  end
  else
  begin
    Writeln('Entering GUI Mode');
    FreeConsole;
    Application.Initialize;
    Application.MainFormOnTaskbar := True;
    Application.CreateForm(TForm1, Form1);
    Application.Run;
  end;

end.

答案 4 :(得分:1)

FWIW,我玩弄了这个问题并发生在AttachConsole上似乎可以解决这个问题。我遇到的唯一问题就是程序在没有额外的一两个ENTER键的情况下不会给控制台。自从我试图解决这个问题并且(有点)放弃以来,这并不是真正的抛光。也许这里有人会看到它?

program writecon; uses windows, dialogs;

  function AttachConsole(dwProcessID: DWord): BOOL; stdcall; external 'kernel32.dll';

  function load_attach_console: boolean;
    begin
      Result := AttachConsole(-1);
    end;

  begin
    // the function requires XP or greater, you might want to check for that here.
    if load_attach_console = true then
      begin
        writeln;
        writeln('This is running in the console.');
        write('Press ENTER to continue.');
        readln;
        // from the linked page, you have to detach yourself from the console
        // when you're done, this is probably where the problem is.
        Flush(Output);
        Flush(Input);
        FreeConsole;
      end
    else
      MessageDlg('This is not running in the console.', mtInformation, [mbOk], 0);
  end.

答案 5 :(得分:0)

AttachConsole似乎工作正常,如上所述它等待ENTER。

然而,程序仍然是一个胜利前卫,而不是dos看到的控制台程序,因此cmd在启动后继续执行下一个命令。

test.exe & dir

首先显示目录列表,然后显示test.exe的输出

start /w test.exe & dir 

确实有效,并且不会为ENTER键暂停

BTW,上面的建议:     PostMessage的(GetCurrentProcess,$ 0101,$ 0D,0); 做了什么,但是发出了一声巨响。

答案 6 :(得分:0)

我发现这篇关于整个问题的完整文章:http://www.boku.ru/2016/02/28/posting-to-console-from-gui-app/

我创建了一个单元来执行AttachConsole,挂钩异常处理程序以将消息镜像到控制台。

要使用它,您只需要在代码中调用ATTACH。最好附上命令行选项,例如-console

if FindCmdLineSwitch('console',true) then AttachConsole(true,true);

这是一个gui应用程序,使用它时,你必须使用START / W来启动你的程序,你希望它在命令行/批处理上是阻塞的,例如start /w myprogram.exe -console

一个方便的好处是,如果需要,可以使用控制台独立启动它,并在控制台中查看所有错误消息。

unit ConsoleConnector;
// Connects the/a console to a GUI program
// Can hook exception handler to mirror messages to console.
// To use it, you only need to call ATTACH
// best to make attaching a commandline option e.g -console
//    if FindCmdLineSwitch('console',true) then AttachConsole(true,true);
// When using this, you will use START to launch your program e.g.
// start /w myprogram.exe -console
// creates Console var at end in initialise/finalise - you might want to do this explicitly in your own program instead.
// see: http://www.boku.ru/2016/02/28/posting-to-console-from-gui-app/

//sjb 18Nov16

interface
uses sysutils,forms;

type
  TConsoleConnector = class
  private
    OldExceptionEvent:TExceptionEvent;
    Hooked:boolean;
    BlockApplicationExceptionHandler:boolean; //errors ONLY to console, no error messageboxes blocking program
    procedure DetachErrorHandler;
    procedure GlobalExceptionHandler(Sender: TObject; E: Exception);
    procedure HookExceptionHandler;
  public
    IsAttached:boolean;

    function Attach(
        CreateIfNeeded:boolean=true; //Call ALLOCCONSOLE if no console to attach to
        HookExceptions:boolean=false;  //Hook Application.OnException to echo all unhandled exceptions to console
        OnlyToConsole:boolean=false  // Suppresses exception popups in gui, errors only go to console
        ):boolean;
    procedure Detach;            //detach and unhook
    procedure writeln(S:string); //only writes if console is attached
    procedure ShowMessage(S:string); //Popup ShowMessage box and mirror to console. Obeys OnlyToConsole
  end;

  var Console:TConsoleConnector;

implementation

uses Windows,dialogs;

//winapi function
function AttachConsole(dwProcessId: Int32): boolean; stdcall; external kernel32 name 'AttachConsole';

function TConsoleConnector.Attach(CreateIfNeeded:boolean=true;HookExceptions:boolean=false;OnlyToConsole:boolean=false):boolean;
begin
  IsAttached:=AttachConsole(-1);
  if not IsAttached and CreateIfNeeded
    then begin
      IsAttached:=AllocConsole;
    end;
  result:=IsAttached;
  if HookExceptions then HookExceptionHandler;
end;

procedure TConsoleConnector.Detach;
begin
  FreeConsole;
  IsAttached:=false;
  DetachErrorHandler;
end;

procedure TConsoleConnector.WriteLn(S:string);
begin
  if IsAttached then system.writeln(S);
end;
procedure TConsoleConnector.ShowMessage(S:string);
begin
  self.Writeln(S);
  if BlockApplicationExceptionHandler then exit;
  dialogs.ShowMessage(S);
end;
procedure TConsoleConnector.GlobalExceptionHandler(Sender: TObject; E: Exception);
begin
  self.Writeln(E.Message);
  if BlockApplicationExceptionHandler then exit;
  if assigned(OldExceptionEvent) //i.e there was an old event before we hooked it
    then OldExceptionEvent(Sender,E)
    else Application.ShowException(E);
end;

procedure TConsoleConnector.HookExceptionHandler;
begin
  OldExceptionEvent:=Application.OnException;
  Application.OnException:=GlobalExceptionHandler;
  Hooked:=true;
end;

procedure TConsoleConnector.DetachErrorHandler;
begin
  if Hooked //I have hooked it
    then begin
      Application.OnException:=OldExceptionEvent;
      OldExceptionEvent:=nil;
      Hooked:=false;
    end;
end;

initialization
  Console:=TconsoleConnector.create;
finalization
  Console.Detach;
  Console.Destroy;
end.

答案 7 :(得分:-1)

我也确实在一个运行脚本的报告中总结了这个主题:

http://www.softwareschule.ch/download/maxbox_starter70.pdf   作为第二个备份:

https://www.slideshare.net/maxkleiner1/nogui-maxbox-starter70

主例程具有与writeline分开的nativewriteline:

 for it:=1 to 50 do if IsPrime(it) then NativeWriteln(IntToStr(it)+' is prime');