Catalog · How the catalog works
duckduckdelphi
Anyone may press Interesting, signed in or not. It helps others find 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.
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.pasis framework-agnostic;duck.vcl.pasandduck.fmx.pasadd 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
-
Install the DPM client (command-line tool and IDE plugin). Leave Add to PATH enabled.
-
Confirm a gallery source is configured:
dpm sources ListYou should see a source pointing at
https://delphi.dev/...(often namedDPMordelphi.dev). If not: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):
dpm install sivv.duck C:\path\to\YourProject.dprojPin a specific release:
dpm install sivv.duck -version=3.0.2 C:\path\to\YourProject.dprojLimit platforms if needed (defaults to all platforms your project targets):
dpm install sivv.duck -platforms=Win32,Win64 C:\path\to\YourProject.dproj3. Use it in code
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 helpersduck.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:
PackageReferenceelements at the bottom of the.dproj(package id + version).$(DPMSearch)added toDCC_UnitSearchPathfor each installed platform.
Other developers (and CI) do not need a copy of this repo. After cloning your project:
dpm restore C:\path\to\YourProject.dprojThat downloads the referenced package version into %AppData%\.dpm\ and wires up the
same search paths.
5. Upgrade or remove
dpm install sivv.duck -version=3.0.3 -upgrade C:\path\to\YourProject.dprojUse 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:
uses duck; // core: duck typing, selectors, events, TRTTI
uses duck.vcl; // optional: VCL event helpers
uses duck.fmx; // optional: FMX event helpersNo 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:
$env:DPM_API_KEY = 'your-api-key'
.\scripts\publish-dpm.ps1The 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.
// 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:
// 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).
// 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
// 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.
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.
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):
uses duck, duck.vcl;
Edit1.OnKeyUp := Event.Key(
procedure(Sender: TObject; var Key: Word; Shift: TShiftState)
begin
// ...
end);FMX (duck.fmx):
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:
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 valuePerformance
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:
BenchDuck.exe [iterations] [warmup] [controlCount]Full instructions, the output format, and a per-case description are in
bench/README.md.

