Build a Customer Screen in Delphi in 30 Minutes (SQL Server Step by Step)

“Delphi is fast for business screens” is easy to say. This article proves it. In about 30 minutes you’ll build a complete customer maintenance screen for SQL Server: a searchable grid, edit fields, add/edit/delete buttons, default values, validation and friendly error messages.

Most of it is done in the form designer. You’ll write about 40 lines of code, most of them begin and end.

This is part 3 of the Delphi for ERP series. You should already know how to connect Delphi to SQL Server with FireDAC and how a DBGrid shows SQL Server data.

Diagram: a Delphi customer form with search box, grid, edit fields and navigator, connected through a DataSource and FDQuery to the SQL Server Customers table

Step 1: Create the Customers Table (3 minutes)

Run this in SSMS. The rest of the series uses the same small ERP database, so keep it.

CREATE TABLE dbo.Customers (
    CustomerID   INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    CustomerCode VARCHAR(20)   NOT NULL UNIQUE,
    CustomerName NVARCHAR(100) NOT NULL,
    Phone        VARCHAR(30)   NULL,
    Email        VARCHAR(100)  NULL,
    City         NVARCHAR(50)  NULL,
    CreditLimit  DECIMAL(12,3) NOT NULL DEFAULT 0 CHECK (CreditLimit >= 0),
    IsActive     BIT           NOT NULL DEFAULT 1
);

INSERT INTO dbo.Customers (CustomerCode, CustomerName, Phone, City, CreditLimit)
VALUES ('C-001', N'Al Noor Trading',  '17000001', N'Manama',   5000),
       ('C-002', N'Gulf Office Supply', '17000002', N'Muharraq', 2500),
       ('C-003', N'Sara Electronics',  '17000003', N'Riffa',    0);

The table already protects itself: CustomerCode must be unique and CreditLimit can’t be negative. The Delphi screen will add friendly messages on top, but the database is the last line of defence. (More on this in SQL Constraints Explained Simply.)

Step 2: Add a Data Module (5 minutes)

In a real ERP, every form shares one connection. Put it in a data module:

  1. File → New → Other → Delphi → Data Module. Name it DM and save the unit as uDM.pas.
  2. Drop a TFDConnection on it, name it Conn, and set it up for your SQL Server as shown in the FireDAC connection article.
  3. Drop a TFDPhysMSSQLDriverLink and a TFDGUIxWaitCursor too.
  4. Set Conn.LoginPrompt := False and test with Connected = True in the Object Inspector. Then set it back to False; the app connects at runtime.
  5. In Project → Options → Application → Forms, move DM to the top of the auto-create list so it’s created before any form.

Edition note: FireDAC access to SQL Server needs Delphi Enterprise or higher. In Community or Professional, use a TADOConnection here and a TADOQuery in Step 3. Everything else in this article is the same.

Step 3: The Query and the DataSource (3 minutes)

Create a new form, frmCustomers (unit uCustomers.pas), and add uDM to its uses clause (File → Use Unit). Then drop:

  • A TFDQuery named qryCustomers, with Connection = DM.Conn and this SQL:
SELECT CustomerID, CustomerCode, CustomerName, Phone, Email, City, CreditLimit, IsActive
FROM dbo.Customers
ORDER BY CustomerName
  • A TDataSource named dsCustomers, with DataSet = qryCustomers.

Now the time-saving trick: double-click qryCustomers to open the Fields Editor, right-click → Add all fields. This creates a persistent field object for each column. Select a field to set properties without code:

FieldPropertyValue
CustomerIDVisibleFalse (users don’t need to see it)
CustomerCodeDisplayLabelCode
CustomerNameDisplayLabelCustomer name
CreditLimitDisplayFormat#,##0.000
IsActiveDisplayLabelActive

Step 4: Design the Form by Dragging (8 minutes)

  1. Drop a TPanel at the top (Align = alTop). On it, put a TLabel “Search:”, a TEdit named edtSearch, and a TDBNavigator with DataSource = dsCustomers.
  2. Drop a TDBGrid (Align = alClient, DataSource = dsCustomers). Set Options.dgRowSelect to True if you want the grid read-only, and edit in the fields below instead.
  3. Drop a second TPanel at the bottom (Align = alBottom, height about 150).
  4. Now drag fields from the Fields Editor onto the bottom panel. Delphi creates a label and a matching TDBEdit (or TDBCheckBox for IsActive) for each field, already connected to dsCustomers.

Run the app now (F9) and add qryCustomers.Open to the form’s OnCreate event:

procedure TfrmCustomers.FormCreate(Sender: TObject);
begin
  qryCustomers.Open;
end;

You already have a working screen: browse, add, edit, delete, save and cancel. FireDAC reads the table’s primary key and generates the INSERT, UPDATE and DELETE statements itself. It also knows CustomerID is an identity column and fills it in after an insert.

Step 5: Add Search (4 minutes)

For a customer list of a few thousand rows, filtering the rows already loaded is instant and simple. Use the dataset’s OnFilterRecord event:

procedure TfrmCustomers.qryCustomersFilterRecord(DataSet: TDataSet; var Accept: Boolean);
var
  S: string;
begin
  S := LowerCase(Trim(edtSearch.Text));
  Accept := (S = '')
    or (Pos(S, LowerCase(DataSet.FieldByName('CustomerName').AsString)) > 0)
    or (Pos(S, LowerCase(DataSet.FieldByName('CustomerCode').AsString)) > 0);
end;

procedure TfrmCustomers.edtSearchChange(Sender: TObject);
begin
  qryCustomers.Filtered := False;   // switching it off and on again re-runs the filter
  qryCustomers.Filtered := Trim(edtSearch.Text) <> '';
end;

Typing “gulf” or “C-00” now narrows the grid as you type. The search ignores upper/lower case and matches anywhere in the name.

For very large tables (hundreds of thousands of rows), don’t load everything. Search on the server instead with a WHERE ... LIKE and a parameter, as shown in Parameterised Queries in Delphi.

Step 6: Defaults and Validation (5 minutes)

When the user presses Insert, give the new row sensible defaults with OnNewRecord:

procedure TfrmCustomers.qryCustomersNewRecord(DataSet: TDataSet);
begin
  DataSet.FieldByName('IsActive').AsBoolean := True;
  DataSet.FieldByName('CreditLimit').AsCurrency := 0;
end;

Before saving, check the data with BeforePost. Raising an exception here cancels the save, and the user sees the message:

procedure TfrmCustomers.qryCustomersBeforePost(DataSet: TDataSet);
begin
  if Trim(DataSet.FieldByName('CustomerCode').AsString) = '' then
    raise Exception.Create('Please enter a customer code.');
  if Trim(DataSet.FieldByName('CustomerName').AsString) = '' then
    raise Exception.Create('Please enter the customer name.');
  if DataSet.FieldByName('CreditLimit').AsCurrency < 0 then
    raise Exception.Create('Credit limit cannot be negative.');
end;

Step 7: Friendly Error for a Duplicate Code (2 minutes)

If the user enters a code that already exists, SQL Server rejects it with a long technical message. FireDAC tells you what kind of error it was, so you can show something clearer. Add FireDAC.Stan.Error to the uses clause, then handle OnPostError:

procedure TfrmCustomers.qryCustomersPostError(DataSet: TDataSet;
  E: EDatabaseError; var Action: TDataAction);
begin
  if (E is EFDDBEngineException) and
     (EFDDBEngineException(E).Kind = ekUKViolated) then
  begin
    ShowMessage('This customer code already exists. Please use another code.');
    Action := daAbort;   // stay in edit mode so the user can fix it
  end;
end;

ekUKViolated means a unique key was violated. Other useful kinds are ekFKViolated (a foreign key) and ekRecordLocked.

The Result

In about 30 minutes you have a screen that:

  • Lists customers in a grid, sorted by name
  • Searches as you type
  • Adds, edits and deletes with the navigator (with a delete confirmation built in)
  • Fills defaults for new customers
  • Validates before saving and shows clear messages
  • Is protected by constraints in the database too

That’s the same pattern for items, suppliers, accounts, warehouses and most master data screens in an ERP. Once you’ve built one, the next takes 10 minutes.

Common Mistakes

  • A connection on every form. It opens many connections and makes transactions harder. Use one connection in a data module.
  • Leaving Connected = True at design time. The app then tries to connect to your server on the user’s PC. Connect at runtime.
  • Validation only in Delphi. Another app or a script can still insert bad data. Keep constraints in SQL Server as well.
  • Loading huge tables to filter locally. Fine for thousands of rows, slow for millions. Search on the server for big tables.
  • Building filters or SQL by joining user text. That’s how SQL injection happens. Use OnFilterRecord or parameters.

Practice Exercises

  1. Add a “Show inactive” checkbox. By default, hide customers where IsActive is False.
  2. Make the Esc key cancel an edit and F2 start editing the current row.
  3. A customer that has invoices can’t be deleted (a foreign key will stop it once the invoice tables exist in part 5). Show a friendly message instead of the SQL Server error.

Answers

1. Keep the filter always on and add a condition to OnFilterRecord:

// in FormCreate, after Open:
qryCustomers.Filtered := True;

// in qryCustomersFilterRecord, after the search check:
if Accept and not chkShowInactive.Checked then
  Accept := DataSet.FieldByName('IsActive').AsBoolean;

// chkShowInactive.OnClick and edtSearch.OnChange:
qryCustomers.Filtered := False;
qryCustomers.Filtered := True;

2. Set the form’s KeyPreview := True and handle OnKeyDown:

procedure TfrmCustomers.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if (Key = VK_ESCAPE) and (qryCustomers.State in [dsEdit, dsInsert]) then
    qryCustomers.Cancel
  else if Key = VK_F2 then
    qryCustomers.Edit;
end;

3. Handle OnDeleteError the same way as OnPostError:

procedure TfrmCustomers.qryCustomersDeleteError(DataSet: TDataSet;
  E: EDatabaseError; var Action: TDataAction);
begin
  if (E is EFDDBEngineException) and
     (EFDDBEngineException(E).Kind = ekFKViolated) then
  begin
    ShowMessage('This customer has invoices and cannot be deleted. Set it to inactive instead.');
    Action := daAbort;
  end;
end;

Summary

  • A data module holds one shared connection for the whole app.
  • TFDQuery + TDataSource + data-aware controls give a full add/edit/delete screen with almost no code.
  • Persistent fields let you set labels, formats and visibility in the designer.
  • OnFilterRecord gives quick search; OnNewRecord sets defaults; BeforePost validates.
  • OnPostError and OnDeleteError turn database errors into clear messages.

Next in the series: lookup combo boxes, so the user picks a customer or an account from a list instead of typing an ID.

Related articles

Scroll to Top