跳到主要内容

拖放系统 Drag & Drop

拖放(Drag & Drop)是桌面 UI 的“超能力”:把文件拖进窗口、把节点拖进蓝图、把资源拖进场景……HiEasyX 的拖放系统是全局状态驱动的,不依赖任何具体控件,因此可以跨控件、跨窗口完成一次完整的拖放操作。

核心设计

拖放生命周期只有三步:

  1. BeginDrag — 在某控件里按下并决定“我要拖这个东西”。
  2. AcceptDrop — 在目标控件里问“有没有东西拖到我头上了?”。
  3. 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);

参数

参数类型说明
TypeHXString拖放类型,如 HXStr("file")HXStr("node")。目标必须用相同 Type 才能接收。
DataHXString附带数据,如文件路径、序列化节点信息。
调用时机

必须在检测到鼠标按下并满足拖拽意图时调用,通常放在 if (msg.MouseLeftPressed) 分支里。不要每帧都调,否则拖放状态会被反复重置。

IsDragging

bool IsDragging(const HXString &Type = HXString{});

参数

参数类型说明
TypeHXString若传入空字符串(默认),则判断是否有任何拖放正在进行;若传入具体 Type,则仅判断该类型是否正在拖。

返回值

bool — 是否正在拖拽(且类型匹配,如果指定了)。

用途
  • 源控件:在 IsDragging 期间可绘制源控件的“ ghost ”效果(如原位置变半透明)。
  • 目标控件:先 IsDragging 判断要不要高亮接收区域,再 AcceptDrop 处理具体逻辑。

AcceptDrop

const HXDragDropPayload *AcceptDrop(const HXString &Type);

参数

参数类型说明
TypeHXString目标期望接收的类型。只有当前拖拽的 Type 与此一致时才返回非空。

返回值

const HXDragDropPayload * — 若当前有匹配的拖拽正在进行,返回载荷指针;否则返回 nullptr

与 IsDragging 的配合

典型目标侧代码:

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);
源控件绘制 ghostif (IsDragging(type)) DrawGhost();
目标高亮if (IsDragging(type)) Highlight();
目标接收auto p = AcceptDrop(type); if (p && released) Handle(p->Data);
结束拖拽EndDrag();
自定义预览设置 DragDropConfig::RenderPreviewCallback

拖放是连接不同控件的桥梁。把它用起来,你的编辑器就不再是一盘散沙,而是一个有机的整体 🌉