Integrating Python in Delphi: A Guide to Boosting Development Efficiency

Integrating Python in Delphi: A Guide to Boosting Development Efficiency

Developing a Delphi GUI for our Python application

·

13 min read

Overview

The purpose of this article is to create a Delphi FMX application that runs a Python application in a thread without locking the UI. We need to have Embarcadero’s Delphi installed (I am using version 12.2 for this build), and having Python4Delphi (you can obtain it through the GetIt Package Manager on the Delphi UI). What we will learn is to load a python script, run it, have the ability to stop the script, print to the UI, update a Label on the UI, and maintain a thread for the python code being executed.

The steps we will take is to: (1) setup our folder structure for the Project; (2) We will then do the design of the application; and (3) we will do the implementation.

Pre-requisites

Setup

The first step is to setup our new project. The best way to do this is to create a folder structure that reflects what we need in the project. This is a first organizational step, where we will create the py4delphi folder that will contain following folders: (1) Project, where we will add our Delphi project files and our builds will be located here as well; (2) Screenshots, images specific for this tutorial; (3) Scripts folder will contain all our python code *.py files; and (4) Src, this folder will contain all of our Delphi **.pas and **.fmx files. The image below shows the folder structure in the in the file system. We will create a View and a Utils folder under the Src folder.

Now that we have this simple step done along with our pre-requisites, we are going to move on to the Design step.

Design

The quick sketch below shows a quick design for the application, all it will do is load a file from a path, run the script, and push the values on to our Delphi application in a TMemo field.

With a folder structure defined, and a simple sketch of the application we can now focus on creating the project and the UI.

Project

We are going to launch our Embarcadero IDE and we will be creating a Project > Add New Project… where we will select Delphi > Multi-Device and select the Blank Application as shown in the image below. Once it is created we are going to File > Save All or CTRL + SHIFT + S and save the Form as Py4Del.View.Main.pas in the Src > View folder. Then we will save the Project as Py4Del under the Project folder.

We will be using the Align option from the “Quick Edit…” popup box when we right click on a UI component and it looks like the image below:

This will give us a project with a Unit.pas file in it that contains an empty TForm. We want to drag and drop the following UI components:

  1. Drop a TPanel, do a quick edit and align it to the top most. Then while having the TPanel selected drop the following components and update their name and captions through “Quick Edit…” menu after you right click on them:

    1. Drop a TEdit and name it EdtPath

    2. Drop three TButtons and name them: BtnRun > “Run”, BtnStop > “Stop”, and BtnBrowse > “Browse…”

  2. Drop a TMemo and name it MmConsole, align it to the center

  3. Drop another TMemo and name it MmPyCode, then align it to the bottom

  4. Drop a TSplitter, no need to name, but align it to the bottom as well.

  5. Arrange all UI components so the application looks like the image below

With this the UI should resemble our sketch, and we have completed the design phase of the visual components. Next, we are going to move into the implementation part.

Implementation

The implementation part of this tutorial is divided in two section: (1) Python, where we add the Python components from Python4Delphi and create our Python application; and (2) the Delphi code where we interact with the Python code. The flow of our application is depicted in the image below.

The application will run the Python code in a thread so that the UI is not blocked, the python code will update the Delphi application, and the UI will be updated without blocking the main thread of the application (otherwise the UI will be frozen and unable to be used).

Python

First we need to add our Python components to the application, the easiest method is to drag and drop them into your application. Since we have the Python4Delphi library installed these components will be readily available in the Palette. We want to drag and drop the following components: (1) the PythonEngine, this component allows us to execute Python code from our Delphi application and it is ALWAYS necessary when running a Python script; and (2) the PythonModule, this enables us to create a Python module, accessible from our Python script to pass data onto the Delphi application.

Next, we are going to create a file in the scripts folder and name it python4delphi.py, we will add the following code:

from PythonModule1 import printme  

def print_data(handle):

    print('starting script...')  

    for i in range(10000):
        print(i)
        val1 = 2.5 + i
        val2 = 'this is the iteration number ' + str(i)
        printme(handle, i, val1, val2)

The code does the following:

  1. imports the printme function from the PythonModule1

  2. Defines a function named print_data that takes a handle as an argument

  3. The function will iterate in a loop, create a few variables and then call the printme function passing the handle, an integer, a float value, and a string as arguments.

This will call the printme function on our Delphi code every iteration on the loop passing the variables to the function. We use a handle to identify the thread we are running on, which is passed from the Delphi application.

FMX Application

The application flow is going to go through an initialization process, then we are going to map our buttons to actions, and we are going to cleanup our thread when execution is completed as depicted on the diagram below.

First step here is mapping the PythonModule1 variable created when dragged into the application. The PythonModule will create a way for our Python code to talk to our Delphi code through a function, but to make it work we need to map it to a Delphi function. The first step is to select the PythonModule1 component in the Design view, then under the Object Inspector select the Events tab, and finally double click on the OnInitialization even t form the engine. This will create a method for the Main form where we will use the AddDelphiMethod function to map a Delphi function to a Python function.

procedure TFrmMain.OnPyModuleLoad(Sender: TObject);
begin
  with Sender as TPythonModule do
    begin
      AddDelphiMethod( 'printme',
                       ConsoleModule_Print,
                       'printme(handle,val1,val2,val3)');
    end;
end;

Next we are going to create a ConsoleModule_Print method that will parse the Python function parameters, and then call the ConsoleUpdate method on our thread. There are a few GOTCHAs when doing this: (1) I am passing a decimal value, and I am using a Single to parse the value from the Python code; (2) the handle for the process is mapped as an integer (i) if its in a 32 bit environment, and a long (L) if its in a 64 bit environment; (3) Python uses ANSI characters and Delphi uses Unicode as defaults, that is why in line 6 instead of using a string we are using a PAnsiChar variable type ; and (4) depending on the Python version (64 bits or 32 bits) it is tied to the build environment used in Delphi, for me I have a 64 bit python installed so I am only targeting 64 bit builds. Notice that on line 12 and 14 we are using the table mentioned below the code to parse the values from the Python function arguments to Delphi variable types.

function TFrmMain.ConsoleModule_Print( pself, args : PPyObject ) : PPyObject; cdecl;
var
  pprint: NativeInt;
  val1: Integer;
  val2: Single;
  val3: PAnsiChar;
begin
  with GetPythonEngine do
  begin
    if (PyErr_Occurred() = nil) and
{$IFDEF CPU64BITS}
      (PyArg_ParseTuple( args, 'Lifs',@pprint, @val1, @val2, @val3) <> 0)
{$ELSE}
      (PyArg_ParseTuple( args, 'iifs',@pprint, @val1, @val2, @val3) <> 0)
{$ENDIF}
    then
    begin
      TPyThread(pprint).ConsoleUpdate(val1, val2, val3);
      Result := ReturnNone;
    end else
      Result := nil;
  end;

end;

This table shows all the different types of values to parse based on data type:

TypeDescription
"s" (string) [char *]Convert a Python string to a C pointer to a character string
"s#" (string) [char *, int]This variant on "s" stores into two C variables, the first one a pointer to a character string, the second one its length
"z" (string or None) [char *]Like "s", but the Python object may also be None, in which case the C pointer is set to NULL
"z#" (string or None) [char *, int]This is to "s#" as "z" is to "s"
"b" (integer) [char]Convert a Python integer to a tiny int, stored in a C char
"h" (integer) [short int]Convert a Python integer to a C short int
"i" (integer) [int]Convert a Python integer to a plain C int
"l" (integer) [long int]Convert a Python integer to a C long int
"c" (string of length 1) [char]Convert a Python character, represented as a string of length 1, to a C char
"f" (float) [float]Convert a Python floating point number to a C float
"d" (float) [double]Convert a Python floating point number to a C double
"D" (complex) [Py_complex]Convert a Python complex number to a C Py_complex structure
"O" (object) [PyObject *]Store a Python object (without any conversion) in a C object pointer
"O!" (object) [typeobject, PyObject *]Store a Python object in a C object pointer
"O&" (object) [converter, anything]Convert a Python object to a C variable through a converter function
"S" (string) [PyStringObject *]Like "O" but requires that the Python object is a string object
"(items)" (tuple) [matching-items]The object must be a Python tuple whose length is the number of format units in items

Threading

We are going to add a new Unit to the project and rename it to Py4Del.Utils.PyThread.pas in the Src > Utils folder. Threading itself can be challenging as we want to avoid race conditions and locking, We will create a class named TPyThread that inherits from TPythonThread. The class will have the following: override the ExecuteWithPython method; add a Create, ConsoleUpdate, and Stop public methods; DoConsoleUpdate as a private method; and we will add several private variables to reference the data from the Python code and use it in our Delphi application.


  TPyThread = class (TPythonThread)
  private
    FModule: TPythonModule;
    FScript: TStrings;
    FMmConsole: TMemo;
    FVal1: Integer;
    FVal2: Single;
    FVal3: string;
    Fpyfuncname: string;
    FRunning : Boolean;

    procedure DoConsoleUpdate;

  protected
    procedure ExecuteWithPython; override;
  public

    constructor Create( AThreadExecMode: TThreadExecMode; script: TStrings;
                        module: TPythonModule; apyfuncname: string;
                        MmConsole: TMemo);

    procedure ConsoleUpdate(Val1: Integer; Val2: Single; Val3: string);
    procedure Stop;

  end;

We will start with the Create method, that will take the following arguments: TThreadExecMode, is how the thread will be executed; the script, which is the Python code to be exectued; the TPythonModule, which maps the function from Python to Delphi; the apyfuncname that is the name of the python function in the module in this case is ‘print_data’; and the UI component that we will be updating and in this case is a TMemo. The arguments should be tailored to your application requirements. The code below shows how we are assigning our variables from the signature of our constructor.

constructor TPyThread.Create( AThreadExecMode: TThreadExecMode; script: TStrings;
                                module: TPythonModule; apyfuncname: string;
                                MmConsole: TMemo);
begin
  Fpyfuncname := apyfuncname;
  FScript := script;
  FMmConsole := MmConsole;
  FModule := module;
  FreeOnTerminate := True;
  ThreadExecMode := AThreadExecMode;
  inherited Create(False);
end;

The next method will be the ExecuteWithPython, this execute the Python code in a thread while validating that everything has been setup accordingly.

procedure TPyThread.ExecuteWithPython;
var pyfunc: PPyObject;
begin
  FRunning := true;
  try
    with GetPythonEngine do
    begin
      if Assigned(FModule) and (ThreadExecMode <> emNewState) then
        FModule.InitializeForNewInterpreter;
      if Assigned(fScript) then
      try
        ExecStrings(fScript);
      except
      end;
      pyfunc :=  FindFunction(ExecModule, utf8encode(fpyfuncname));
      if Assigned(pyfunc) then
        try
          EvalFunction(pyfunc,[NativeInt(self)]);
        except
        end;
      Py_XDecRef(pyfunc);
    end;
  finally
    FRunning := false;
  end;
end;

Next we will use the ConsoleUpdate and DoConsoleUpdate to synchronize the main thread, assign values, and then update the component by calling the DoConsoleUpdate.

procedure TPyThread.DoConsoleUpdate;
begin
  FMmConsole.Lines.Add((IntToStr(FVal1) + ': ' + FloatToStr(FVal2) + ', ' + FVal3));
end;

procedure TPyThread.ConsoleUpdate(Val1: Integer; Val2: Single; Val3: string);
begin
  Py_BEGIN_ALLOW_THREADS;
  if Terminated then
    raise EPythonError.Create( 'Pythonthread terminated');
  FVal1 := Val1;
  FVal2 := Val2;
  FVal3 := Val3;
  Synchronize(DoConsoleUpdate);
  Py_END_ALLOW_THREADS;
end;

Finally, we will implement the Stop method. This method will force stop the execution of the thread.

procedure TPyThread.Stop;
begin
  with GetPythonEngine do
  begin
    if FRunning then
    begin
      PyEval_AcquireThread(self.ThreadState);
      PyErr_SetString(PyExc_KeyboardInterrupt^, 'Terminated');
      PyEval_ReleaseThread(self.ThreadState);
    end;
  end;
end;

Main Application

Now that our TPyThread class is completed we are going to create a reference for it in our Main application. Our Main application class definition should look as follows:

TFrmMain = class(TForm)
    MmConsole: TMemo;
    MmPyCode: TMemo;
    RctToolbar: TRectangle;
    Splitter1: TSplitter;
    EdtPath: TEdit;
    BtnBrowse: TButton;
    BtnRun: TButton;
    BtnStop: TButton;
    PythonEngine1: TPythonEngine;
    PythonModule1: TPythonModule;
    procedure BtnRunClick(Sender: TObject);
    procedure BtnStopClick(Sender: TObject);
    procedure BtnBrowseClick(Sender: TObject);
    procedure OnPyModuleLoad(Sender: TObject);
  private
    { Private declarations }
    OwnThreadState: PPyThreadState;
    ThreadsRunning: Integer;

    function ConsoleModule_Print( pself, args : PPyObject ) : PPyObject; cdecl;
    procedure InitThreads(ThreadExecMode: TThreadExecMode; script: TStrings);
    procedure ThreadDone(Sender: TObject);
  public
    { Public declarations }
    Thread1: TPyThread;
  end;

Since we have already implemented the ConsoleModule_Print and OnPyModuleLoad we will focus on the other methods. We will focus on InitThreads, this method will create our TPyThread. Notice how we will be enabling and disabling buttons to make sure our application reflects the state of our code execution.

procedure TFrmMain.InitThreads(ThreadExecMode: TThreadExecMode; script: TStrings);
begin
  ThreadsRunning := 1;
  with GetPythonEngine do
  begin
    OwnThreadState := PyEval_SaveThread;
    Thread1 := TPyThread.Create( ThreadExecMode, script, PythonModule1, 'print_data',
                           MmConsole);
    Thread1.OnTerminate := ThreadDone;
  end;
  BtnRun.Enabled := False;
end;

Next, we will work on the ThreadDone method, this will restore the thread state, enable the buttons, and set the Thread1 variable to null.

procedure TFrmMain.ThreadDone(Sender: TObject);
begin
  Dec(ThreadsRunning);
  if ThreadsRunning = 0 then
  begin
    GetPythonEngine.PyEval_RestoreThread(OwnThreadState);
    BtnRun.Enabled := True;
    BtnStop.Enabled := False;
    Thread1 := nil;
  end;
end;

Next, the BtnBrowseClick method will use a file dialog, set the filters to only display Python files, verity that the file exists, update the path TEdit, load the python code to the MmPyCode TMemo field, enable the Run TButton, ad finally free the file open dialog.

procedure TFrmMain.BtnBrowseClick(Sender: TObject);
var
  OpenDlg: TOpenDialog;
begin
  OpenDlg := TOpenDialog.Create(self);
  OpenDlg.InitialDir := GetCurrentDir;
  OpenDlg.Filter := 'Python file|*.py';
  if OpenDlg.Execute and FileExists(OpenDlg.FileName) then begin
    EdtPath.Text := OpenDlg.FileName;
    MmPyCode.Lines.LoadFromFile(OpenDlg.FileName);
    BtnRun.Enabled := True;
  end;
  OpenDlg.Free;
end;

Next, the BtnRunClick method will be implemented. All this will do is enable the Stop TButton and call the InitThreads method passing a thread execution mode and a string containing the Python code (the code we have stored in the MmPyCode TMemo).

procedure TFrmMain.BtnRunClick(Sender: TObject);
begin
  BtnStop.Enabled := True;
  InitThreads(emNewInterpreterOwnGIL, MmPyCode.Lines);
end;

Last, the BtnStopClick will validate that there is an assigned thread that has not completed can call the Stop method (this we created in the TPyThread class).

procedure TFrmMain.BtnStopClick(Sender: TObject);
begin
  if Assigned(Thread1) and not Thread1.Finished then Thread1.Stop();
end;

The full source code will be available in the Github repository link under the conclusion section.

Testing

At this point we are going to run our application, we will load our script using the browse button and navigating to the scripts folder. We will then click on the Run button and this will show the Python script updating the UI without blocking any functionality from it as show in the image below.

This final step validates our application working as expected.

Conclusion

This guide should have given you the basis to understanding the library Python4Delphi, the samples within it, and how to use it for your own purposes. Following along will have you with a fully working application, leveraging the power of python, and using it within your Delphi FMX Application. The github repo for this article can be found here.

References

Did you find this article valuable?

Support Failing 2 Build by becoming a sponsor. Any amount is appreciated!