Welcome to the new DelphiDabbler Code Library Documentation.

This is a new site that's currently running on alpha code. There are going to be bugs. If you discover any, please report them on the site's issues page (GitHub account required). Thanks.

Warning: Many URLs are going to change. Refer to the README file to discover which library project's documentation has been completed.

Console Application Runner Classes Example 8: Capturing console output in a GUI

Applies to: ~>3.0

You may have seen applications (Inno Setup springs to mind), that display output from a console application in real time in a GUI control. In this example we’ll see how to use TPJConsoleApp to do this.

Here is an overview of what we will do:

Start a new Delphi GUI application and drop a button and two memo controls. We will display standard output in Memo1 and standard error in Memo2.

We will use the following helper classes to avoid a considerable amount of complexity in the code:

All these classes are included in the I/O Utitlity Classes download.

First add the PJConsoleApp, PJPipe, PJFileHandle and PJPipeFilters units to the uses statement.

Now add the following declarations to the form class’ private section:

  private
    fErrFilter, fOutFilter: TPJAnsiSBCSPipeFilter;
    procedure ErrLineEndHandler(Sender: TObject; const Line: AnsiString);
    procedure OutLineEndHandler(Sender: TObject; const Line: AnsiString);
    procedure WorkHandler(Sender: TObject);
    procedure CompletionHandler(Sender: TObject);

Create an OnClick event handler for the button as follows:

procedure TForm1.Button1Click(Sender: TObject);
var
  App: TPJConsoleApp;
  InFile: TPJFileHandle;
  OutPipe, ErrPipe: TPJPipe;
const
  InFileName = 'InputFile.txt';
begin
  fOutFilter := nil;
  fErrFilter := nil;
  OutPipe := nil;
  ErrPipe := nil;
  InFile := nil;
  try
    // Open input file
    InFile := TPJFileHandle.Create(InFileName, fmOpenRead or fmShareDenyNone);

    // Create output pipes: one each for stdout and stderr
    OutPipe := TPJPipe.Create;
    ErrPipe := TPJPipe.Create;

    // Create filter objects used to format text from output pipe into lines
    fOutFilter := TPJAnsiSBCSPipeFilter.Create(OutPipe);
    fOutFilter.OnLineEnd := OutLineEndHandler;
    fErrFilter := TPJAnsiSBCSPipeFilter.Create(ErrPipe);
    fErrFilter.OnLineEnd := ErrLineEndHandler;

    App := TPJConsoleApp.Create;
    try
      // redirect stdin to file and stdout/stderr to pipes
      App.StdIn := InFile.Handle;
      App.StdOut := OutPipe.WriteHandle;
      App.StdErr := ErrPipe.WriteHandle;
      App.OnWork := WorkHandler;
      App.OnComplete := CompletionHandler;
      App.TimeSlice := 1;
      if not App.Execute('Echoer') then
        raise Exception.CreateFmt(
          'Error %X: %s', [App.ErrorCode, App.ErrorMessage]
        );
    finally
      App.Free;
    end;
  finally
    FreeAndNil(fErrFilter);
    FreeAndNil(fOutFilter);
    ErrPipe.Free;
    OutPipe.Free;
    InFile.Free;
  end;
end;

Here’s what the code does:

The OnWork and OnComplete handlers are where we process the piped output from the console application. Implement these like this:

procedure TForm1.WorkHandler(Sender: TObject);
begin
  fOutFilter.ReadPipe;
  fErrFilter.ReadPipe;
  Application.ProcessMessages;       // Let the memo controls update
end;

procedure TForm1.CompletionHandler(Sender: TObject);
begin
  fOutFilter.Flush;
  fErrFilter.Flush;
end;

WorkHandler calls the ReadPipe methods of the two TPJAnsiSBCSPipeFilter objects. This method simpy copies all available data from the associated pipe and processes it. Importantly, WorkHandler also calls Application.ProcessMessages to enable the memo controls to update.

CompletionHandler causes the TPJAnsiSBCSPipeFilter objects to flush any remaining text from their internal buffers, ensuring it gets written to the memo controls.

So far we haven’t seen how the memo controls actually get updated. This is done in the final piece of code, and that’s the implementation of the TPJAnsiSBCSPipeFilter objects’ OnLineEnd event handlers:

procedure TForm1.OutLineEndHandler(Sender: TObject; const Line: AnsiString);
begin
  Memo1.Lines.Add(string(Line));
end;

procedure TForm1.ErrLineEndHandler(Sender: TObject; const Line: AnsiString);
begin
  Memo2.Lines.Add(string(Line));
end;

These events are triggered once for each complete line of text that the TPJAnsiSBCSPipeFilter read from the pipes. The events are also triggered for any remaining text when the application ends (in the calls to the Flush method). Each event is handled simply by adding the line of text to the appropriate memo control.

Run the application and watch the output appear in the two memo controls.

A shortened version

The code above is longer than it needs to be in order to make it clear what exactly is happening in the hope that this makes the code easier to understand.

TPJAnsiSBCSPipeFilter is able to take ownership of any pipe passed to its constructor so that the pipe is freed when the filter object is freed. The object also exposes a Pipe property to provide access to the wrapped pipe. This means we don’t need the OutPipe and ErrPipe local variables.

The Button1Click event handler can be shortened to:

procedure TForm1.Button1Click(Sender: TObject);
var
  App: TPJConsoleApp;
  InFile: TPJFileHandle;
const
  InFileName = 'InputFile.txt';
begin
  fOutFilter := nil;
  fErrFilter := nil;
  InFile := nil;
  try
    InFile := TPJFileHandle.Create(InFileName, fmOpenRead or fmShareDenyNone);

    fOutFilter := TPJAnsiSBCSPipeFilter.Create(TPJPipe.Create, True);
    fOutFilter.OnLineEnd := OutLineEndHandler;
    fErrFilter := TPJAnsiSBCSPipeFilter.Create(TPJPipe.Create, True);
    fErrFilter.OnLineEnd := ErrLineEndHandler;

    App := TPJConsoleApp.Create;
    try
      App.StdIn := InFile.Handle;
      App.StdOut := fOutFilter.Pipe.WriteHandle;
      App.StdErr := fErrFilter.Pipe.WriteHandle;
      App.OnWork := WorkHandler;
      App.OnComplete := CompletionHandler;
      App.TimeSlice := 1;
      if not App.Execute('Echoer') then
        raise Exception.CreateFmt(
          'Error %X: %s', [App.ErrorCode, App.ErrorMessage]
        );
    finally
      App.Free;
    end;
  finally
    FreeAndNil(fErrFilter); // frees pipe object and closes pipe handles
    FreeAndNil(fOutFilter); // frees pipe object and closes pipe handles
    InFile.Free;
  end;
end;

The pipe objects are created on the fly in the TPJAnsiSBCSPipeFilter constructor calls. The optional second True parameter tells the filter object to free the pipe when it is destroyed. We use the Pipe property of the filter to access the pipe’s WriteHandle when setting the console application object’s StdOut and StdIn properties.

The is one further bit of slimming down we can do, and that is to remove the console application object’s OnComplete event handler. We can do this because TPJAnsiSBCSPipeFilter instance automatically calls the Flush method when the object is being destroyed. This causes the OnLineEnd event handler to be called with any remaining text. Therefore the explicit call to Flush in the OnComplete handler is redundant.