拖放系统 Drag & Drop
拖放(Drag & Drop)是桌面 UI 的“超能力”:把文件拖进窗口、把节点拖进蓝图、把资源拖进场景……HiEasyX 的拖放系统是全局状态驱动的,不依赖任何具体控件,因此可以跨控件、跨窗口完成一次完整的拖放操作。
核心设计
拖放生命周期只有三步:
- BeginDrag — 在某控件里按下并决定“我要拖这个东西”。
- AcceptDrop — 在目标控件里问“有没有东西拖到我头上了?”。
- EndDrag — 释放鼠标,结束拖放。
全局状态会跨帧持久,因此你可以从 Window A 拖到 Window B,中间经过任意控件。
类型与载荷
拖放按 Type(类型字符串)区分。只有 Type 匹配时,AcceptDrop 才会返回有效载荷。Data 是附带字符串,可存文件路径、节点 Id、JSON 等。
struct HXDragDropPayload {
HXString Type; // 拖放类型标识
HXString Data; // 实际载荷数据
};
API 详解
BeginDrag
void BeginDrag(const HXString &Type, const HXString &Data);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
Type | HXString | 拖放类型,如 HXStr("file")、HXStr("node")。目标必须用相同 Type 才能接收。 |
Data | HXString | 附带数据,如文件路径、序列化节点信息。 |
必须在检测到鼠标按下并满足拖拽意图时调用,通常放在 if (msg.MouseLeftPressed) 分支里。不要每帧都调,否则拖放状态会被反复重置。
IsDragging
bool IsDragging(const HXString &Type = HXString{});
参数
| 参数 | 类型 | 说明 |
|---|---|---|
Type | HXString | 若传入空字符串(默认),则判断是否有任何拖放正在进行;若传入具体 Type,则仅判断该类型是否正在拖。 |
返回值
bool — 是否正在拖拽(且类型匹配,如果指定了)。
- 源控件:在
IsDragging期间可绘制源控件的“ ghost ”效果(如原位置变半透明)。 - 目标控件:先
IsDragging判断要不要高亮接收区域,再AcceptDrop处理具体逻辑。
AcceptDrop
const HXDragDropPayload *AcceptDrop(const HXString &Type);
参数
| 参数 | 类型 | 说明 |
|---|---|---|
Type | HXString | 目标期望接收的类型。只有当前拖拽的 Type 与此一致时才返回非空。 |
返回值
const HXDragDropPayload * — 若当前有匹配的拖拽正在进行,返回载荷指针;否则返回 nullptr。
典型目标侧代码:
if (HX::IsDragging(HXStr("file"))) {
// 高亮目标区域,告诉用户“可以放这里”
}
const auto *payload = HX::AcceptDrop(HXStr("file"));
if (payload && msg.MouseLeftRelease) {
// 真正处理放置逻辑
HandleFileDrop(payload->Data);
}
EndDrag
void EndDrag();
清除全局拖放状态。通常在检测到 MouseLeftRelease 时由源控件调用,或者在 AcceptDrop 处理完毕后由目标控件调用。重复调用是安全的。
自定义预览样式
拖放时,框架会自动在鼠标旁绘制一个浮动预览卡片。你可以通过 DragDropConfig 定制它的外观,甚至完全接管绘制。
struct DragDropConfig {
std::function<void(HXBufferPainter *, const HXDragDropPayload &, HXPoint mousePos)> RenderPreviewCallback;
int FontSize = 16;
HXColor BackgroundColor;
HXColor BorderColor;
HXColor TextColor;
int Padding = 6;
HXPoint OffsetXY = {15, 15}; // 预览卡片相对鼠标的偏移
};
如果你把 RenderPreviewCallback 设为非空,框架会调用你的函数而不是默认绘制。这在拖拽复杂对象(如蓝图节点缩略图、资源缩略图)时非常有用。
HX::DragDropConfig cfg;
cfg.RenderPreviewCallback = [](HXBufferPainter *p, const HX::HXDragDropPayload &payload, HXPoint pos) {
// 自己画一个 64x64 的缩略图
p->FilledRect(pos.x, pos.y, pos.x + 64, pos.y + 64, HX::GetTheme().AccentColor);
p->Text(pos.x + 4, pos.y + 4, payload.Data.c_str());
};
完整示例:文件拖放
#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
void DemoDragDrop() {
// ========== 源面板:文件列表 ==========
HX::WindowProfile srcWp;
srcWp.Title = HXStr("Assets");
srcWp.Size = {200, 400};
HX::Window(HXStr("src_panel"), srcWp);
static std::vector<HXString> files = {
HXStr("texture/hero.png"),
HXStr("audio/bgm.ogg"),
HXStr("scene/level1.json")
};
for (size_t i = 0; i < files.size(); ++i) {
HX::ButtonProfile btn;
if (HX::Button(files[i], btn)) {
// 点击即视为开始拖拽
}
// 在鼠标按下时启动拖放
if (HX::GetHXMessage()->MouseLeftPressed) {
HX::BeginDrag(HXStr("file"), files[i]);
}
}
HX::End();
// ========== 目标面板:场景 ==========
HX::WindowProfile dstWp;
dstWp.Title = HXStr("Scene");
dstWp.Size = {400, 400};
HX::Window(HXStr("dst_panel"), dstWp);
// 如果当前有文件正在拖拽,高亮背景
if (HX::IsDragging(HXStr("file"))) {
HX::GetTheme().WindowBackground = HX::HXColor{40, 60, 40, 255};
}
// 接受放置
const auto *payload = HX::AcceptDrop(HXStr("file"));
if (payload) {
HX::Text(HXStr("Dragging over: ") + payload->Data);
if (HX::GetHXMessage()->MouseLeftRelease) {
// 放置确认!
HX::Text(HXStr("Dropped: ") + payload->Data);
HX::EndDrag(); // 清理状态
}
} else {
HX::Text(HXStr("Drag files here..."));
}
HX::End();
}
跨窗口拖放注意事项
拖放状态是在 HX::Render() 之前、控件调用期间由 BeginDrag/EndDrag 维护的。如果你有两个 Window(),从第一个窗口开始拖拽,在第二个窗口释放,AcceptDrop 仍然能正常工作,因为状态是全局的。
建议把项目中所有拖放类型定义为常量,避免拼写错误:
namespace DnDType {
const HXString File = HXStr("file");
const HXString Node = HXStr("node");
const HXString Asset = HXStr("asset");
const HXString Texture = HXStr("texture");
}
总结
| 场景 | 代码模式 |
|---|---|
| 启动拖拽 | if (pressed) BeginDrag(type, data); |
| 源控件绘制 ghost | if (IsDragging(type)) DrawGhost(); |
| 目标高亮 | if (IsDragging(type)) Highlight(); |
| 目标接收 | auto p = AcceptDrop(type); if (p && released) Handle(p->Data); |
| 结束拖拽 | EndDrag(); |
| 自定义预览 | 设置 DragDropConfig::RenderPreviewCallback |
拖放是连接不同控件的桥梁。把它用起来,你的编辑器就不再是一盘散沙,而是一个有机的整体 🌉