Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works

Random project

duckduckdelphi

View on Codeberg

Anyone may press Interesting, signed in or not. It helps others find this project.

Member rating

No ratings yet.

Sign in to rate this project.

README

Duck Duck Delphi

Duck Duck Delphi adds duck typing, fluent object-graph queries, anonymous event handlers, and a set of RTTI conveniences to Delphi. If an object has a property or method — whether or not it implements a particular interface — you can read it, set it, call it, or query for it at runtime with a tiny, readable API.

pascal
uses duck;

// Hide every control on the form that has a Visible property
Form1.Duck.all.has('Visible').setTo('Visible', False);

Requirements

  • Delphi 10 Seattle or newer (extended RTTI required).
  • VCL or FMX — the core duck.pas is framework-agnostic; duck.vcl.pas and duck.fmx.pas add framework-specific anonymous event helpers.

Installation

Option A — DPM (recommended for application projects)

DPM (Delphi Package Manager) installs Duck Duck Delphi as a source-only package. DPM downloads the sources into your local package cache and adds them to the project search path via $(DPMSearch). Nothing is registered in the IDE and no .bpl is required — duck.pas, duck.vcl.pas, and duck.fmx.pas compile into your app like any other unit.

Package id sivv.duck
Feed delphi.dev public gallery (registered by default on first DPM run)
Delphi versions 10.0 Seattle through 13.0 Florence
Installed units duck.pas, duck.vcl.pas, duck.fmx.pas (under src\ in the package cache)

1. One-time setup

  1. Install the DPM client (command-line tool and IDE plugin). Leave Add to PATH enabled.

  2. Confirm a gallery source is configured:

    text
    dpm sources List

    You should see a source pointing at https://delphi.dev/... (often named DPM or delphi.dev). If not:

    text
    dpm sources Add "-name=delphi.dev" "-source=https://delphi.dev/api/v2/index.json" -type=DPMServer
    dpm sources Enable "-name=delphi.dev"

2. Add the package to your project

From the IDE: open your .dproj, right-click the project → Manage DPM Packages, search for sivv.duck, and install.

From the command line (run from any folder):

text
dpm install sivv.duck C:\path\to\YourProject.dproj

Pin a specific release:

text
dpm install sivv.duck -version=3.0.2 C:\path\to\YourProject.dproj

Limit platforms if needed (defaults to all platforms your project targets):

text
dpm install sivv.duck -platforms=Win32,Win64 C:\path\to\YourProject.dproj

3. Use it in code

pascal
uses duck;            // required: duck typing, selectors, events, TRTTI

uses duck.vcl;        // optional: VCL mouse/key/drag event helpers
uses duck.fmx;        // optional: FMX mouse/key/tap/drag event helpers

duck.pas is framework-agnostic and works in VCL, FMX, and non-visual projects. Add duck.vcl or duck.fmx only when you need typed anonymous handlers for that framework's events (see Anonymous event handlers below). Omit duck.vcl in FMX-only apps and omit duck.fmx in VCL-only apps.

4. What DPM changes in your .dproj

After install, commit the DPM-related edits to version control:

  • PackageReference elements at the bottom of the .dproj (package id + version).
  • $(DPMSearch) added to DCC_UnitSearchPath for each installed platform.

Other developers (and CI) do not need a copy of this repo. After cloning your project:

text
dpm restore C:\path\to\YourProject.dproj

That downloads the referenced package version into %AppData%\.dpm\ and wires up the same search paths.

5. Upgrade or remove

text
dpm install sivv.duck -version=3.0.3 -upgrade C:\path\to\YourProject.dproj

Use the IDE package manager to uninstall, or edit the PackageReference entries and run dpm restore.

Local / private feed

If your team mirrors packages internally, register that folder or server as an additional DPM source and pass -source=YourSourceName on install / restore. The package spec lives at sivv.duck.dspec.yaml.

Option B — Manual (clone / submodule)

Add this repo (or a copy of duck.pas, duck.vcl.pas, and/or duck.fmx.pas) to your project's search path:

pascal
uses duck;            // core: duck typing, selectors, events, TRTTI
uses duck.vcl;        // optional: VCL event helpers
uses duck.fmx;        // optional: FMX event helpers

No package manager — units compile directly into your project. Demo projects in this repo use a relative ..\ search path so contributors can build without DPM.

Maintainers — pack and publish

Tag the release first (git tag release/3.0.2), then:

powershell
$env:DPM_API_KEY = 'your-api-key'
.\scripts\publish-dpm.ps1

The script reads the version from the release/* tag at HEAD (or the latest release/* tag), packs into dist\dpm, and pushes to the delphi.dev gallery. Pack only: .\scripts\publish-dpm.ps1 -PackOnly · Push only: -PushOnly · See scripts/publish-dpm.ps1.

Usage

Duck typing a single object

obj.Duck works on any object with RTTI. Calls fail silently if the member does not exist, or use has / can to check first.

pascal
// Set a property if it exists (no-op otherwise)
Edit1.Duck.setTo('Visible', True);

// Guard explicitly
if Edit1.Duck.has('Visible') then
  Edit1.Duck.setTo('Visible', True);

// Read a value
if Button1.Duck.has('Caption') then
  ShowMessage(Button1.Duck.get('Caption').AsString);

Querying object graphs with selectors

Duck.all returns a selector over an object and its related objects/properties. Chain filters and actions fluently.

After has('PropertyName') or has('PropertyName', value), the selector remembers that property as context, so you can batch-update without repeating the name:

pascal
// Hide every object that has a Visible property (name omitted on setTo)
Form1.Duck.all.has('Visible').setTo(False);

// Refine the set, then update the same property
Form1.Duck.all.has('Visible').isa(TButton).setTo(False);

can('MethodName') sets context for go the same way. Use use('PropertyName') to set context explicitly, or on('PropertyName') when the selection changes to related objects (context is cleared because the targets are no longer the original owners).

pascal
// Every object that exposes a Clear method — call it
Form1.Duck.all.can('Clear').go;

// Only TButton descendants, every other one, restyle their Font
Form1.Duck.all
  .isa(TButton)
  .even
  .on('Font')
    .setTo('Color', clRed)
    .setTo('Size', 18);

// Run an anonymous action against each match
Form1.Duck.all.has('Hint').each(
  procedure(obj: TObject)
  begin
    obj.Duck.setTo('ShowHint', True);
  end);

// Custom filter after has('Visible') — context is kept, so setTo(False) still works
Form1.Duck.all
  .has('Visible')
  .filter(
    function(obj: TObject): Boolean
    begin
      Result := obj.Duck.get('Tag').AsInteger = 1;
    end)
  .setTo(False);

Calling methods dynamically

pascal
// TMyAdder has overloaded Add(Integer) and Add(Single)
Sum := Adder.Duck.call('Add', [2, 5]).AsInteger;        // -> 7

// Try-call without raising when no overload matches
var v: TValue;
if TRTTI.tryCall(Adder, 'Add', [2, 5], v) then
  Sum := v.AsInteger;

Impersonating an interface

An object can be used as if it implemented an interface, even when it doesn't declare support — as long as the members line up.

pascal
if Adder.impersonates<IAdder> then
  Result := Adder.asA<IAdder>.Add(2, 7);

// Or get an interface reference backed by the object
var ia: IAdder := Adder.Duck<IAdder> as IAdder;
Result := ia.Add(12, 7);

Anonymous event handlers

Assign inline handlers without declaring methods. Release them when done to free the backing reference.

pascal
uses duck;

Button1.OnClick := Event.Notify(
  procedure(Sender: TObject)
  begin
    ShowMessage('Clicked!');
  end);

// Later:
Event.NotifyRelease(Button1.OnClick);

For events with custom signatures, use the framework helper unit:

VCL (duck.vcl):

pascal
uses duck, duck.vcl;

Edit1.OnKeyUp := Event.Key(
  procedure(Sender: TObject; var Key: Word; Shift: TShiftState)
  begin
    // ...
  end);

FMX (duck.fmx):

pascal
uses duck, duck.fmx;

Edit1.OnKeyDown := Event.Key(
  procedure(Sender: TObject; var Key: Word; var KeyChar: WideChar; Shift: TShiftState)
  begin
    // ...
  end);

Button1.OnTap := Event.Tap(
  procedure(Sender: TObject; const TapLocation: TPointF)
  begin
    // ...
  end);

Helpers exist for mouse, key, wheel, drag, tap, and focus events. Each has a matching Event.FooRelease(...) — use that instead of assigning nil when tearing down. You can also call Event.From<TEvent, TReference>(...) directly when no helper exists.

Direct RTTI helpers

When you want RTTI utilities without the duck wrapper:

pascal
if TRTTI.isProperty(obj, 'Visible') then
  TRTTI.setTo<Boolean>(obj, 'Visible', True);

var b: Boolean;
if TRTTI.tryGet<Boolean>(obj, 'Visible', b) then
  // b holds the current value

Performance

This version is dramatically faster than earlier releases for any workload that touches many objects. RTTI metadata (types, properties, methods, and overloads) is resolved once per class and cached behind a shared context, and bulk selector operations no longer rescan the result set per element. The public API is unchanged — existing code simply runs faster.

Measured with the included benchmark (controls=500, warmup=3, iterations=25, MS_PER_ITER, lower is better; improvement = (before − after) / before):

Operation Before (ms/iter) After (ms/iter) Improvement Speedup
all enumeration (Selector_AllCount) 168.3418 0.0679 99.96% 2479×
isa(TControl) filter 170.4903 0.1101 99.94% 1549×
all.has('Visible') 196.9242 0.1773 99.91% 1111×
has(['Visible','Text']) 195.6539 0.1942 99.90% 1008×
filter + even + on('Font') 381.2359 0.4632 99.88% 823×
trySet('Tag') 0.2628 0.0006 99.77% 438×
isProperty (cached) 27.1338 0.0635 99.77% 427×
tryGet('Visible') 0.0928 0.0003 99.68% 309×
can('Clear') selector 253.9453 0.9302 99.63% 273×
getValue (cached) 26.7363 0.0989 99.63% 270×
tryCall (no match) 0.0409 0.0003 99.27% 136×
isMethod overload list 0.0057 0.0001 98.25% 57×
batch setTo('Visible') 224.4920 4.1970 98.13% 54×
hide all visible (combined) 222.2564 4.2307 98.10% 53×
tryCall (hit) 0.0369 0.0011 97.02% 34×
call('Add', [Int]) 0.0426 0.0016 96.24% 27×
call('Add', [Single]) 0.0349 0.0016 95.42% 22×

The query and bulk-mutation paths that scale with object count improve by 2–4 orders of magnitude at 500 controls, and the gap widens as the count grows. See the benchmark harness to reproduce these numbers on your machine.

License

Released under the MIT License — Copyright © 2012 Sivv LLC and © 2026 Kadence Labs Inc. The full license text is in LICENSE and in the header of each source unit (duck.pas, duck.vcl.pas, duck.fmx.pas).

Benchmark

A console harness in bench/ validates and times each capability and emits pipe-delimited rows so you can compare two builds of duck.pas by CASE and MS_PER_ITER. Build it in Release and run:

text
BenchDuck.exe [iterations] [warmup] [controlCount]

Full instructions, the output format, and a per-case description are in bench/README.md.