跳到主要内容

弹出菜单 PopupMenu

右键点击呼出菜单、下拉列表、工具按钮旁边的快捷操作——PopupMenu 是 HiEasyX 里的"悬浮菜单"解决方案。它不占用固定布局空间,点哪弹哪,点完即走。

Modal 不同,PopupMenu 更轻量,通常用于一次性命令选择。它返回一个整数索引告诉你用户点了第几项,然后自动关闭(或者你手动关)。


函数原型

// 在屏幕指定位置打开一个弹出菜单
void OpenPopupMenu(PopupMenuProfile &Profile, HXPoint ScreenPos);

// 手动关闭弹出菜单
void ClosePopupMenu(PopupMenuProfile &Profile);

// 查询某个菜单是否正打开
bool IsPopupMenuOpen(PopupMenuProfile &Profile);

// 查询当前是否有任意弹出菜单处于打开状态
bool IsAnyPopupMenuOpen();

// 渲染并处理弹出菜单交互。返回被点击项的索引,-1 表示未点击
HXGInt PopupMenu(PopupMenuProfile &Profile);

参数详解

PopupMenuProfile

成员类型默认值说明
PositionHXPoint{0, 0}菜单左上角屏幕坐标(通常由 OpenPopupMenu 自动填充)
Openboolfalse菜单是否处于打开状态(内部维护)
Itemsstd::vector<PopupMenuItem>菜单项列表
Widthint180菜单整体宽度
ItemHeightint28普通项的高度
SeparatorHeightint8分隔线的高度
Paddingint8菜单内边距
FontSizeint16文字大小
BackgroundColorHXColor继承主题菜单背景色
BorderColorHXColor继承主题菜单边框色
TextColorHXColor继承主题文字颜色

PopupMenuItem

成员类型默认值说明
LabelHXString显示文字
Enabledbooltrue是否可点击。false 时文字变灰且不响应鼠标
Separatorboolfalse如果为 true,这一项渲染为分隔线,Label 被忽略

返回值

函数返回值说明
PopupMenuHXGIntint被点击项在 Items 数组中的索引。如果本帧没有点击任何项,返回 -1
IsPopupMenuOpenbool该 Profile 对应的菜单是否正打开
IsAnyPopupMenuOpenbool全局是否有任意菜单打开(常用于阻止背景交互)

最小示例:右键上下文菜单

#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>

void RightClickMenuDemo() {
static HX::PopupMenuProfile menu{}; // 必须是 static / 全局
menu.Width = 160;
menu.ItemHeight = 26;
menu.FontSize = 14;

// 定义菜单项(每帧可以动态改变)
menu.Items = {
HX::PopupMenuItem{HXStr("复制")},
HX::PopupMenuItem{HXStr("剪切")},
HX::PopupMenuItem{HXStr("粘贴")},
HX::PopupMenuItem{HXStr(""), true, true}, // 分隔线
HX::PopupMenuItem{HXStr("删除"), true},
HX::PopupMenuItem{HXStr("重命名"), true},
HX::PopupMenuItem{HXStr(""), true, true}, // 分隔线
HX::PopupMenuItem{HXStr("属性"), true},
};

// ---- 主界面 ----
HX::Text(HXStr("在下方区域右键点击试试"));
HX::Button(HXStr("我是背景按钮"), HX::ButtonProfile{});

// ---- 检测右键按下,打开菜单 ----
if (HX::Context.MessageQuery.MouseRightPressed) {
HX::OpenPopupMenu(menu, {
HX::Context.MessageQuery.MouseX,
HX::Context.MessageQuery.MouseY
});
}

// ---- 渲染菜单并读取点击结果 ----
HXGInt clicked = HX::PopupMenu(menu);
if (clicked >= 0) {
switch (clicked) {
case 0: /* 复制 */ break;
case 1: /* 剪切 */ break;
case 2: /* 粘贴 */ break;
case 4: /* 删除 */ break;
case 5: /* 重命名 */ break;
case 7: /* 属性 */ break;
}
// 菜单不会自动关闭所有情况,但点击后通常应该关掉
HX::ClosePopupMenu(menu);
}
}

分隔线与禁用项

menu.Items = {
HX::PopupMenuItem{HXStr("撤销"), false}, // 灰色,点不动
HX::PopupMenuItem{HXStr("重做"), false},
HX::PopupMenuItem{HXStr(""), true, true}, // 分隔线
HX::PopupMenuItem{HXStr("复制")},
HX::PopupMenuItem{HXStr(""), true, true}, // 再来一条分隔线
HX::PopupMenuItem{HXStr("删除"), true},
};
  • Separator = true 时,那一项会变成一条细横线,不占用 ItemHeight 而是占用 SeparatorHeight
  • Enabled = false 时,文字颜色会变暗,鼠标悬停不显示高亮,点击也不会返回索引。

与 Blueprint 内置右键菜单的区别

特性HX::PopupMenuBlueprint 画布右键菜单
使用方式显式调用 OpenPopupMenu + PopupMenuBlueprint 内部自动触发,设置 ContextMenuRequested 标志
数据源你手动构造 PopupMenuItem 数组Blueprint 内部根据节点注册表自动生成节点分类列表
返回值项索引 int无直接返回值,通过创建节点回调实现
定位任意屏幕坐标固定为鼠标右键位置
适用场景通用上下文菜单、工具栏下拉蓝图节点编辑器专用
一句话区分

PopupMenu通用菜单组件,哪里都能用;Blueprint 的右键菜单是蓝图专属逻辑,基于同一个底层渲染,但你不需要(也不能)直接调用 PopupMenu 去渲染它。


完整示例:带图标的文件列表 + 右键菜单

#include <include/hex.h>
#include <include/impl/EasyX/hex_impl_easyx.h>
#include <vector>

void FileBrowserWithContextMenu() {
struct FileItem {
HXString Name;
bool IsFolder;
bool Selected;
};
static std::vector<FileItem> files = {
{HXStr("src"), true, false},
{HXStr("assets"), true, false},
{HXStr("main.cpp"), false, false},
{HXStr("logo.png"), false, false},
{HXStr("README.md"), false, false},
};

// ---- 绘制文件列表 ----
HX::Text(HXStr("项目文件"));
for (size_t i = 0; i < files.size(); ++i) {
HX::BeginHorizontal();
HX::Text(files[i].IsFolder ? HXStr("📁 ") : HXStr("📄 "));
HX::SameLine();

HX::ButtonProfile bp{};
if (files[i].Selected) {
bp.BackgroundColor = HX::GetTheme().SelectionBackground;
}
if (HX::Button(files[i].Name, bp)) {
// 左键选择
for (auto &f : files) f.Selected = false;
files[i].Selected = true;
}
HX::EndHorizontal();
}

// ---- 右键菜单 Profile ----
static HX::PopupMenuProfile ctxMenu{};
ctxMenu.Width = 150;
ctxMenu.FontSize = 14;

static size_t rightClickedIndex = 0;

// 检测右键:记录当前项并打开菜单
if (HX::Context.MessageQuery.MouseRightPressed) {
// 简单演示:假设总是对第一个选中项操作
ctxMenu.Items = {
HX::PopupMenuItem{HXStr("打开")},
HX::PopupMenuItem{HXStr("在资源管理器中显示")},
HX::PopupMenuItem{HXStr(""), true, true},
HX::PopupMenuItem{HXStr("复制路径")},
HX::PopupMenuItem{HXStr("重命名")},
HX::PopupMenuItem{HXStr(""), true, true},
HX::PopupMenuItem{HXStr("删除"), true},
};
HX::OpenPopupMenu(ctxMenu, {
HX::Context.MessageQuery.MouseX,
HX::Context.MessageQuery.MouseY
});
}

// ---- 渲染并处理菜单 ----
HXGInt choice = HX::PopupMenu(ctxMenu);
if (choice >= 0) {
switch (choice) {
case 0: /* 打开 */ break;
case 1: /* 在资源管理器中显示 */ break;
case 3: /* 复制路径 */ break;
case 4: /* 重命名 */ break;
case 6: /* 删除 */ break;
}
HX::ClosePopupMenu(ctxMenu);
}
}

常见坑与注意事项

Profile 必须是 static / 全局

PopupMenuProfile 内部维护了 Open 状态、悬停索引、动画计时器等跨帧数据。栈上临时对象会导致菜单闪烁或无法保持打开。

点击后记得处理返回值

PopupMenu 返回索引后不会自动关闭菜单(某些版本行为可能不同,建议显式处理)。安全做法是在 choice >= 0 分支里执行完逻辑后调用 ClosePopupMenu

多个菜单用不同的 Profile

如果你同时需要"文件右键菜单"和"画布右键菜单",定义两个独立的 static PopupMenuProfile,分别传给各自的 PopupMenu()。不要复用同一个 Profile,否则状态会打架。

用 IsAnyPopupMenuOpen 阻止背景操作
if (!HX::IsAnyPopupMenuOpen()) {
// 只有没有菜单打开时才处理背景快捷键
if (HX::IsKeyPressed('S')) { /* Ctrl+S 保存 */ }
}

运行效果

截图占位符:请补充 $name 的运行效果截图。

截图占位符:请补充 弹出菜单 PopupMenu 运行效果 的运行效果截图,保存为 ./assets/弹出菜单 PopupMenu_view.png。`n