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 9: Subclassing TPJConsoleApp

Applies to: ~>3.0

This example requires the TPJPipe class from the PJPipe.pas unit. The unit is included in the I/O Utitlity Classes download.

Most of the previous examples have had to handle events to provide some of the needed functionality. In the later examples they have also had to manipulate pipes. This is all very well, but it does rather clutter up the main form code. There are a couple of alternatives that move the messy code out of the form:

  1. Encapsulate all the code in another class that creates a TPJConsoleApp and supplies the required event handlers. This new class can then provide a simplified interface to the main form.
  2. Derive a new class from TPJCustomConsoleApp and override methods to provide the required functionality.

There are benefits to both approaches. In this example we’re going to look only at the latter case and derive a new class from TPJCustomConsoleApp.

Look again at Example 7. In essence what this code does is:

  1. Read the data out of Memo1.
  2. Pass that data to a pipe.
  3. Set up an output pipe and stream.
  4. Execute the console application.
  5. Write the resulting data to Memo2.

We can replace this with the following:

  1. Read data out of Memo1 into an input stream.
  2. Create an output stream.
  3. Call a method of another object that uses the console application to process the input stream and write the output stream.
  4. Write the resulting data to Memo2.

This has moved all knowledge of how to load streams into pipes and vice versa out of the form. We can create a descendant of TPJCustomConsoleApp that performs the task at item 3 above.

In this example we will recreate Example 7 by subclassing TPJCustomConsoleApp.

Start a new Delphi GUI project and add a new unit file, named UConsoleAppEx.pas. Add the following code to its interface section:

uses
  Classes, PJConsoleApp, PJPipe;

type
  TConsoleAppEx = classTPJCustomConsoleApp-
  private
    fOutPipe: TPJPipe;
    fOutStream: TStream;
  protected
    procedure DoWork; override;
  public
    function Execute(const CmdLine: string; const InStream,
      OutStream: TStream): Boolean;
    // Make protected properties public
    property ErrorCode;
    property ErrorMessage;
    property TimeSlice;
  end;

The Execute method replaces the one in the base class. It receives the command we are to execute as well as an input stream containing data to be piped to the console process’ standard input and a stream to receive data piped from standard output. We also make three required properties public. (All properties of TPJCustomConsoleApp are protected.) The inherited DoWork method is overridden to read the output pipe. This replaces the OnWork event handler in Example 7.

TConsoleAppEx is implemented as follows:

uses
  SysUtils;

procedure TConsoleAppEx.DoWork;
begin
  fOutPipe.CopyToStream(fOutStream, 0);
end;

function TConsoleAppEx.Execute(const CmdLine: string; const InStream,
  OutStream: TStream): Boolean;
begin
  fOutStream := OutStream;
  InPipe := nil;
  fOutPipe := nil;
  try
    // Set up input pipe and associated with console app stdin
    InPipe := TPJPipe.Create(InStream.Size);
    InPipe.CopyFromStream(InStream, 0);
    InPipe.CloseWriteHandle;
    // Set up output pipe and associate with console app stdout
    fOutPipe := TPJPipe.Create;
    StdIn := InPipe.ReadHandle;
    StdOut := fOutPipe.WriteHandle;
    // Run the application
    Result := inherited Execute(CmdLine);
  finally
    StdIn := 0;
    StdOut := 0;
    FreeAndNil(fOutPipe);
    FreeAndNil(InPipe);
  end;
end;

DoWork is straightforward - it copies the current pipe contents to the output stream.

The new Execute method now contains some of the set up code that was in the main form code in Example 7.

First we record a reference to the output stream for later use. Next we create the pipe associated with standard input and copy all the data from InStream to it. The output pipe is then created before associating pipe handles with the console application’s standard input and output. Finally, the Execute method of the base class is called to execute the console application.

Now switch back to the main form. Add UConsoleAppEx to the uses statement. Now drop a button and two memos on the form then create an OnClick event handler for the button, with the following code:

procedure TForm1.Button1Click(Sender: TObject);
var
  App: TConsoleAppEx;
  InStream: TStream;
  OutStream: TStream;
begin
  // Set up i/o streams
  InStream := nil;
  OutStream := nil;
  try
    InStream := TMemoryStream.Create;
    Memo1.Lines.SaveToStream(InStream);
    InStream.Position := 0;
    OutStream := TMemoryStream.Create;
    // Run console app
    App := TConsoleAppEx.Create;
    try
      App.TimeSlice := 2;
      if not App.Execute('Echoer "-->"', InStream, OutStream) then
        raise Exception.CreateFmt(
          'Error %X: %s', [App.ErrorCode, App.ErrorMessage]
        );
    finally
      App.Free;
    end;
    // Read output stream
    OutStream.Position := 0;
    Memo2.Lines.LoadFromStream(OutStream);
  finally
    InStream.Free;
    OutStream.Free;
  end;
end;

First a stream is created and the contents of Memo1 are copied into it. Next the empty output stream is created. Then we run the console application using our new class. And finally we display the data from the output stream in Memo2.

In the above code Echoer.exe from Appendix 2 has been used once again.

Run the application. It should perform exactly the same as Example 7.