win_drag_source
English · 简体中文
Windows-only Flutter package for native OLE drag-and-drop. Wraps any widget into a drag source with a custom ghost image (no system ghost), supporting both CF_HDROP (file) and CF_UNICODETEXT (text / encrypted payload) modes.
⚠️ Windows-only. On macOS / Linux / Web the wrapper degrades to a plain
Listenerand never initiates a drag.
Features
- ✅ Pure Dart FFI — no native code to compile, no plugin registry headache
- ✅ Custom ghost image via
RepaintBoundary— exclude badges, frames, captions from the drag preview viaimageKey - ✅ Two payload modes: file path or arbitrary text (typically encrypted)
- ✅ Tap / double-tap semantics with Windows-native 500 ms interval
- ✅ Zero runtime dependencies beyond
ffiandwin32
Installation
dependencies:
win_drag_source: ^0.0.1
Then:
flutter pub get
Quick start
1. File drag (CF_HDROP)
The simplest case — drag a real file into Explorer, 3ds Max, SketchUp, chat apps, anything that accepts files:
import 'package:win_drag_source/win_drag_source.dart';
DragSource(
payload: FilePayload('C:\\path\\to\\file.max'),
onDropComplete: (accepted) {
debugPrint('drop ${accepted ? 'accepted' : 'rejected'}');
},
child: YourCard(),
)
2. Async payload (DB lookup, decryption, …)
When the payload needs I/O before the drag can start, use payloadProvider:
DragSource(
payloadProvider: () async {
final path = await lookupFilePath(itemId);
if (path == null) return null;
return FilePayload(path);
},
onDropComplete: (ok) => ...,
child: YourCard(),
)
If payloadProvider returns null, the drag is silently aborted.
The pointer must remain pressed while it resolves; releasing it first also
cancels the drag. The asynchronous work itself does not block the UI.
3. Encrypted payload (CF_UNICODETEXT)
For dragging to a target that knows how to decrypt the payload (e.g. your own renderer / IDE plugin):
DragSource(
payload: TextPayload(encrypt(yourJson)),
onDropComplete: (ok) => ...,
child: YourCard(),
)
The package does not ship an encryption helper — bring your own
(package:pointycastle, package:cryptography, …). The package's only job
is to deliver the bytes to the OLE drop target.
💡 Plain-text targets (Notepad, chat apps) will also receive the encrypted string and display it as opaque text. Only targets that know your format can decode it.
Custom ghost image
By default the whole child is rasterized as the drag preview. To drag
only the cover image (excluding badges, frames, captions), wrap it in a
RepaintBoundary with a GlobalKey and pass the key as imageKey:
final _coverKey = GlobalKey();
@override
Widget build(BuildContext context) {
return DragSource(
payload: FilePayload(path),
imageKey: _coverKey, // ← rasterize this subtree, not the whole card
onDropComplete: (ok) => ...,
child: Stack(
children: [
RepaintBoundary(
key: _coverKey,
child: Image.file(File(path)),
),
Positioned(right: 8, bottom: 8, child: Badge()), // excluded from ghost
],
),
);
}
API reference
DragSource
| Param | Type | Default | Description |
|---|---|---|---|
child |
Widget |
— | The widget to wrap (default ghost image source) |
payload |
DragPayload? |
— | Synchronous payload |
payloadProvider |
Future<DragPayload?> Function()? |
— | Async resolver; takes precedence over payload |
imageKey |
GlobalKey? |
— | Sub-tree to rasterize as ghost image |
enabled |
bool |
true |
Master switch |
onTap |
VoidCallback? |
— | Single-tap handler |
onDoubleTap |
VoidCallback? |
— | Double-tap handler (delays single-tap by 500 ms) |
onDropComplete |
ValueChanged<bool>? |
— | Fired after DoDragDrop returns |
DragPayload
| Class | OLE format | Use case |
|---|---|---|
FilePayload |
CF_HDROP |
Drop a real file. Receivers: Explorer, 3ds Max, chat apps, … |
TextPayload |
CF_UNICODETEXT |
Drop an arbitrary string. Receivers: any text field; or your own target that decodes the encrypted payload. |
DragDrop (low-level FFI)
Rarely needed directly — exposed for advanced cases (custom ghost-image pipelines, etc.). Public surface:
| Method | Description |
|---|---|
createDragWindow(width, height) |
Create a topmost layered window for the ghost image |
setDragWindowPixels(hwnd, w, h, pixels) |
Push BGRA pixels into the window |
moveDragWindow(hwnd, x, y) |
Move the ghost window |
showDragWindow(hwnd) |
Show the ghost window |
destroyDragWindow(hwnd) |
Tear down the ghost window |
startFileDrag(path, hwnd, offsetX, offsetY) |
Run DoDragDrop with CF_HDROP (blocking) |
startTextDrag(text, hwnd, offsetX, offsetY) |
Run DoDragDrop with CF_UNICODETEXT (blocking) |
getCursorPosition() |
Current cursor position in screen coords |
How it works
pointer down ──► move > 4 px ──► resolve payload
│
▼
rasterize RepaintBoundary
│
▼
create Win32 layered window
│
▼
DoDragDrop (blocking, runs its own modal loop)
│
▼
destroy window
│
▼
fire onDropComplete
The package does not hide your application window during drag. If you
need that behavior (e.g. to drop on a window behind your app), call
window_manager's hide() / show() yourself around the drag.
Caveats
- Windows-only. On other platforms
DragSourceis a no-op. - Main-thread blocking.
DoDragDropblocks the UI thread by design while its modal message loop runs.payloadProvidermay perform async I/O; keep it quick enough that the user can hold the pointer until it resolves. pixelRatio: 1.0. Ghost images are rasterized at device pixel ratio 1.0. On high-DPI displays this may look slightly soft; if you need a crisper image, fork and adjust.
Compatibility
The package requires Flutter 3.32 or later, Dart 3.8 or later, and Windows.
It uses OLE initialization internally, so applications do not need to set up
COM before using DragSource. A process that has explicitly initialized the
UI thread with an incompatible COM apartment model cannot start an OLE drag.
License
MIT — see LICENSE.
Libraries
- win_drag_source
- Windows-only Flutter package for native OLE drag-and-drop.