Chapter 196 · XUI 高级机制

拖拽系统

XUI 拖拽机制:DragBegin/Move/End 生命周期、DRAG 层预览、Drop 目标判定。

196

概念

XUI 拖拽系统实现控件间数据拖放:
• DragBegin:按下并移动超过阈值时启动拖拽
• DragMove:拖拽过程中更新预览位置
• DragEnd:松开鼠标,判定 Drop 目标
• DRAG 层:拖拽预览自动提升到 LAYER_DRAG 渲染
• Drop 回调:目标控件接收数据(如 InventoryGrid 的 onDrop)
典型应用:物品栏交换、列表排序、面板停靠、文件拖入。

拖拽流程

阶段说明
DragBegin源控件创建拖拽数据 + 预览 Surface
DragMove更新预览位置,高亮潜在 Drop 目标
DragEndHitTest 判定目标,调用 Drop 回调
Drop目标控件处理接收数据

编译本章示例

gcc -O2 -I. -o ch196.exe ch196_main.c src\xui_*.c build\xge.lib -lm -lws2_32 -liphlpapi -lgdi32 -luser32 -lshell32 -lopengl32 -lole32 -lwinmm -lavrt
.\ch196.exe

示例:物品栏拖拽

// InventoryGrid 拖拽回调
static void OnDrag(xui_widget pGrid,
    int iSlot, void* pUser)
{
    // 记录拖拽源槽位
    g_iDragSource = iSlot;
}

static void OnDrop(xui_widget pGrid,
    int iSrcSlot, int iDstSlot, void* pUser)
{
    // 交换两个槽位的物品
    xui_inventory_slot_t tSrc, tDst;
    xuiInventoryGridGetSlot(pGrid, iSrcSlot, &tSrc);
    xuiInventoryGridGetSlot(pGrid, iDstSlot, &tDst);
    xuiInventoryGridSetSlot(pGrid, iSrcSlot, &tDst);
    xuiInventoryGridSetSlot(pGrid, iDstSlot, &tSrc);
}

// 注册回调
xuiInventoryGridSetDragCallback(pGrid, OnDrag, NULL);
xuiInventoryGridSetDropCallback(pGrid, OnDrop, NULL);

完整代码

将以下代码保存为 ch196_main.c,编译后运行即可看到物品栏拖拽交换效果:

#include "xge.h"
#include "xui.h"

static xui_context g_pCtx;
static xui_widget g_pGrid;

static int onFrame(void* pUser) {
    (void)pUser;
    xgeClear(0x1a1a2e);
    xui_vec2_t pos;
    xgeMouseGet(&pos.x, &pos.y);
    xuiInputPointerMove(g_pCtx, pos.x, pos.y);
    if (xgeMouseDown(XGE_MOUSE_LEFT))
        xuiInputPointerDown(g_pCtx, pos.x, pos.y, 0);
    else
        xuiInputPointerUp(g_pCtx, pos.x, pos.y, 0);
    xuiUpdate(g_pCtx, xgeGetDelta());
    xuiLayout(g_pCtx);
    xuiRender(g_pCtx, NULL, NULL, 0);
    return XGE_OK;
}

static void onDrop(xui_widget pGrid,
    int iSrc, int iDst, void* pUser)
{
    (void)pUser;
    xui_inventory_slot_t tA, tB;
    xuiInventoryGridGetSlot(pGrid, iSrc, &tA);
    xuiInventoryGridGetSlot(pGrid, iDst, &tB);
    xuiInventoryGridSetSlot(pGrid, iSrc, &tB);
    xuiInventoryGridSetSlot(pGrid, iDst, &tA);
}

int main(void) {
    xge_desc_t d = {0};
    d.iWidth = 640; d.iHeight = 480;
    d.sTitle = "ch196 - Drag & Drop";
    d.iFlags = XGE_INIT_VSYNC;
    xgeInit(&d);
    xuiCreate(&g_pCtx);
    extern const xui_proxy_t tXgeProxy;
    xui_proxy_t tXgeProxy = xuiProxyXge();
xuiSetProxy(g_pCtx, &tXgeProxy);
    xuiSetViewportSize(g_pCtx, 640, 480);

    xui_widget pRoot;
xuiPanelCreate(g_pCtx, &pRoot, NULL);
    xuiWidgetSetRect(pRoot, (xui_rect_t){0, 0, 640, 480});

    xui_inventory_grid_desc_t tInv = {0};
    tInv.iColumns = 4; tInv.iRows = 2;
    xuiInventoryGridCreate(g_pCtx, &g_pGrid, &tInv);
    xuiWidgetSetRect(g_pGrid, (xui_rect_t){160, 140, 480, 320});
    xuiInventoryGridSetDropCallback(g_pGrid, onDrop, NULL);

    xgeRun(onFrame, NULL);
    xuiDestroy(g_pCtx);
    xgeUnit();
    return 0;
}

代码讲解

  • Drop 回调:拖拽释放时触发,交换源/目标槽位物品数据。
  • 拖拽流程:DragBegin → DragMove(预览跟随) → DragEnd(HitTest) → Drop。
  • DRAG 层:拖拽预览自动提升到 DRAG 层渲染,不受父控件裁剪。
  • 阈值保护:移动超过 4-6px 才触发拖拽,避免单击误触。

要点与陷阱

  • 阈值:移动超过 4-6px 才触发 DragBegin,避免误触。
  • DRAG 层:预览控件自动提升,不受父控件裁剪影响。
  • 跨控件:不同控件间拖放需统一数据格式(槽位 ID/节点路径等)。
  • 取消:按 Esc 或拖出有效区域时取消拖拽,恢复原状。

进阶练习

  1. 列表排序:ListView 项目拖拽重排。
  2. 面板停靠:拖动 Window 到边缘自动 Dock。
拖拽系统效果
图:拖拽系统渲染效果