Metadata-Version: 2.4
Name: vcldrive
Version: 0.1.0
Summary: Drive legacy Delphi/VCL and DevExpress Windows desktop apps from Python — by relative control order, with write-and-read-back verification.
Author-email: Alple <aplestsov1@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/kamaldinsup-droid/vcldrive
Project-URL: Issues, https://github.com/kamaldinsup-droid/vcldrive/issues
Keywords: automation,rpa,pywinauto,delphi,vcl,devexpress,win32,gui-automation,legacy,owner-drawn
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Win32 (MS Windows)
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Desktop Environment
Classifier: Topic :: Software Development :: Testing :: Acceptance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: win32
Requires-Dist: pywinauto>=0.6.8; extra == "win32"
Requires-Dist: pywin32>=306; extra == "win32"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Dynamic: license-file

# vcldrive

**Drive legacy Delphi/VCL and DevExpress Windows desktop apps from Python — by
relative control order, with write-and-read-back verification.**

[![CI](https://github.com/kamaldinsup-droid/vcldrive/actions/workflows/ci.yml/badge.svg)](https://github.com/kamaldinsup-droid/vcldrive/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Python](https://img.shields.io/badge/python-3.10%2B-blue)
![Platform](https://img.shields.io/badge/platform-Windows-lightgrey)

---

## Why this exists

Plenty of business runs on old Windows desktop software built in Delphi/VCL with
DevExpress controls — ERP, manufacturing, accounting. Automating those apps is
unreasonably hard:

- **No automation ids.** Forms expose several controls of the same class
  (for example four `TcxImageComboBox` editors in a column) with nothing to tell
  them apart.
- **Pixel scripts are brittle.** Addressing controls by absolute coordinates
  breaks the moment the window moves or the screen resolution changes.
- **Owner-drawn controls are opaque.** Many DevExpress editors are not exposed to
  UI Automation, so the usual accessibility path returns nothing
  ([pywinauto #757](https://github.com/pywinauto/pywinauto/issues/757),
  [#950](https://github.com/pywinauto/pywinauto/issues/950)). The vendor's
  `dxUIAutomationEnabled` flag would help, but you cannot set it inside a
  compiled third-party `.exe`.
- **Values do not commit.** A `TcxCalcEdit` accepts a number only after real,
  per-character key events followed by a focus change. Convenience APIs that set
  the window text leave the value unposted, which surfaces later as a validation
  error.

`vcldrive` packages the techniques that actually work against these forms, so you
do not have to rediscover them. It builds on [pywinauto](https://github.com/pywinauto/pywinauto)
and adds the missing layer for owner-drawn DevExpress/VCL forms.

## What it gives you

| Capability | What it solves |
| --- | --- |
| **Map by relative order** | Identify look-alike controls top-to-bottom, left-to-right — resilient to window moves and resolution changes. |
| **Row-cluster detection** | Skip a band of look-alike fields (a material picker, a grid row) that sits between the inputs you care about. |
| **Committing numeric input** | Type with real key events and a Tab commit, the only path that posts a `TcxCalcEdit`. |
| **Dropdown by arrows** | Open with `F4` and step with arrow keys in one session — reliable for long Unicode items. |
| **Write-and-read-back** | Write, read back, normalise, retry — so a value that did not stick is caught and re-applied. |
| **Modal recognition** | Read a validation dialog's text and react (re-enter a field, retry). |
| **Screenshot as truth** | Capture the form, because on-screen state is the only trustworthy confirmation for owner-drawn controls. |

## Installation

```bash
# Pure logic only (mapping, verification, geometry, dialog matching):
pip install vcldrive

# With the Windows backend, to actually drive an app:
pip install "vcldrive[win32]"
```

The Windows extra pulls in `pywinauto` and `pywin32`. The core package has no
runtime dependencies and imports on any operating system, which keeps the
position logic testable in CI without a desktop.

## Quickstart

```python
from vcldrive import Form

# Attach to an already-open window by its class name.
form = Form.connect("TFrmItemEditor")

# Map the column of combo editors to roles by relative order, skipping a
# middle band of 3+ look-alike "material" combos.
combos = form.map_roles(
    "TcxImageComboBox",
    ["anchor", "group", "variant", "color", "side"],
    exclude_cluster_min=3,
)

# Pick a group by prefix, then a variant by stepping the open dropdown.
form.combo(combos["group"]).select_by_prefix("Widgets")
form.combo(combos["variant"]).select_by_arrows(7)

# Set the length with committing key events (width follows from the variant).
length = form.map_roles("TcxCalcEdit", ["length", "width"])["length"]
form.numeric(length).set_value(1600)

# Prove it on screen, then commit.
form.screenshot("item_form.png")
form.button("OK").click()
```

A complete, runnable walkthrough — including the "length is required" retry loop
— is in [`examples/erp_accessory_form.py`](examples/erp_accessory_form.py).

## How it works

### Map by relative order, not pixels

`map_roles` collects every control of a class, sorts them into reading order, and
binds your role names to them one by one. Because it reasons about *order* rather
than *coordinates*, the mapping holds when the window moves.

When a form interleaves a band of identical controls between the fields you want,
`exclude_cluster_min` removes that band first. `detect_row_clusters` finds any row
where that many same-class controls line up within an 8-pixel tolerance, so roles
on both sides of the band still align.

### Commit values the way the editor demands

`NumericEdit.set_value` focuses the editor, clears it, types each character as a
real key event, and presses Tab. That on-exit commit is what posts the value to a
`TcxCalcEdit`. `ComboBox.select_by_arrows` opens the list with `F4` and steps with
arrow keys in a single call, because the dropdown closes between separate key
batches.

### Trust the screen, then verify

For owner-drawn controls the committed value is not readable as window text, so
`Form.screenshot` captures ground truth. For controls that *are* readable,
`verify_write` writes, reads back, and retries until the value matches — turning a
silent failure into a handled one.

## Testing

The pure logic is fully unit-tested with in-memory fakes — no GUI, any OS:

```bash
pip install "vcldrive[dev]"
pytest
```

## Compatibility

- **Python:** 3.10+
- **OS:** Windows (for driving). Core logic imports anywhere.
- **Backend:** pywinauto `win32`. Bring your own backend by implementing the small
  [`ControlHandle`](src/vcldrive/protocols.py) protocol.

## Contributing

Issues and pull requests are welcome — especially backend adapters and wrappers
for more DevExpress/VCL control classes. Please keep new logic covered by tests.

## License

[MIT](LICENSE) © 2026 Alple
