D3DDrawLib 绘制外部动态菜单

一个基于D3DDrawLib库开发的外部动态菜单,其允许用户在任意窗体之上绘制自定义菜单界面,这一菜单具备高度的灵活性,能够通过热键实现上下左右的导航选择功能,用户可以轻松切换不同的功能项,并能够在多个应用程序之间无缝切换和操作。

实现动态菜单绘制功能,需要分别准备两个函数,其中Menu函数可为菜单提供绘制功能,而MyFunctionCallBack函数则可用于注册及管理热键,由于绘制及热键的监控是实时的此处还需要一个Draw函数来将这两个函数串连起来,在main主函数中仅需要注册热键并调用createWindow来实现绘制即可。

引用头文件

首先读者应自行引入Microsoft DirectX SDK (June 2010)开发工具包,引入D3DDrawLib.h头文件,并通过pragma comment导入D3DDrawLib.lib静态库,在如下代码部分CurrentlySelected指明了菜单默认选中项为第一个功能,IsItDisplayed函数用于指定是否显示菜单,最后的MenuOnTheRight用于控制菜单是居左显示还是居右显示,底部的功能A-G则是不同功能项的开关按钮。

#include <Windows.h>
#include <D3DDrawLib.h>

#pragma comment(lib,"D3DDrawLib.lib")

// 窗体的宽度及高度
int WindowWidth = 0;
int WindowHeight = 0;

// 当前选中
static int CurrentlySelected = 1;

// 控制是否显示菜单
bool IsItDisplayed = true;

// 菜单居左还是居右显示
bool MenuOnTheRight = false;

// 功能项选中状态
bool 功能项A = false;
bool 功能项B = false;
bool 功能项C = false;
bool 功能项D = false;
bool 功能项E = false;
bool 功能项F = false;
bool 功能项G = false;

动态绘制菜单

如下 Menu 函数用于绘制一个动态的菜单界面,菜单的显示与否由 IsItDisplayed 变量控制。它根据窗口的宽度和高度,动态计算菜单的位置并绘制功能项。用户可以通过选择不同的功能项来开关功能,功能项的状态(开启或关闭)会在菜单中显示。

通过MenuOnTheRight开关判断菜单是居左显示还是居右显示,接着DrawBorderRectangle用于绘制选择条,紧随其后的DrawBorderRectangle用于绘制整个功能菜单的外边框矩形,其内部的每一个子功能项都由一个DrawStringAndString来绘制实现,而在字符串绘制时通过使用?:条件运算符(也称为三元运算符)实现对选择字体的动态着色,当被开启时为绿色,而被关闭时则为蓝色。

void Menu()
{
if (IsItDisplayed)
{
// 间距位置
int DistancePosition = 0;

// 菜单的宽度
int MenuWidth = 120;

// 菜单最左
int MenuLeft;

// 根据MenuOnTheRight的开关设置是左侧还是右侧
if (MenuOnTheRight)
{
// 右对齐
MenuLeft = WindowWidth - MenuWidth - WindowWidth / 30;
}
else
{
// 左对齐
MenuLeft = WindowWidth / 30;
}

// 菜单最高
int MenuTop = WindowHeight / 3;

DistancePosition += 10;

// 选择条大小
D3DDrawLib::Start()->DrawBorderRectangle(MenuLeft + 5, MenuTop + 9 + (CurrentlySelected - 1) * 25, 110, 17, 1, D3DCOLOR_XRGB(148, 0, 211));

// 外边框大小
D3DDrawLib::Start()->DrawBorderRectangle(MenuLeft, MenuTop, MenuWidth, 184, 1, D3DCOLOR_XRGB(255, 69, 0));

// 绘制功能项
D3DDrawLib::Start()->DrawStringAndString("功能项A ", 功能项A ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项A ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项B ", 功能项B ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项B ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项C ", 功能项C ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项C ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项D ", 功能项D ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项D ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项E ", 功能项E ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项E ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项F ", 功能项F ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项F ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 25;

D3DDrawLib::Start()->DrawStringAndString("功能项G ", 功能项G ? "[开启]" : "[关闭]", 12, 65, MenuLeft + 10, MenuTop + DistancePosition, D3DCOLOR_XRGB(255, 52, 179), 功能项G ? D3DCOLOR_XRGB(0, 255, 0) : D3DCOLOR_XRGB(120, 250, 250));
DistancePosition += 35;

// 绘制显示/隐藏
D3DDrawLib::Start()->DrawString("HOME 显示/隐藏", 13, MenuLeft + 12, MenuTop + DistancePosition, D3DCOLOR_RGBA(255, 0, 0, 255));
}
}

上述所示的代码虽然可以很好的实现菜单的功能绘制,但是若我们要新增一个菜单项则会很麻烦,我们需要手动计算外边框的高度及计算字体的排列位置,在优化时,我们将菜单项单独放入一个itemNames数组内,并将每一个字符串与itemStates数组中的元素相关联,通过使用For循环,循环itemCount次,并调用DrawStringAndString函数写菜单,来依次排列这些字符串到特定位置,最后通过DrawString函数在矩形菜单之外写出一段提示信息,其改进后的代码如下所示;

void Menu()
{
if (IsItDisplayed)
{
// 菜单配置
const int MenuWidth = 120; // 菜单宽度
const int ItemHeight = 25; // 每个功能项的高度
const int ExtraPadding = 10; // 顶部额外间距
const int BottomPadding = 0; // 底部额外间距(用于显示/隐藏按钮)

// 配置着色表
const D3DCOLOR BorderColor = D3DCOLOR_XRGB(255, 69, 0);
const D3DCOLOR HighlightColor = D3DCOLOR_XRGB(148, 0, 211);
const D3DCOLOR EnabledColor = D3DCOLOR_XRGB(0, 255, 0);
const D3DCOLOR DisabledColor = D3DCOLOR_XRGB(120, 250, 250);

// 菜单位置(居左或者居右)
int MenuLeft = MenuOnTheRight ? WindowWidth - MenuWidth - WindowWidth / 30 : WindowWidth / 30;

// 菜单顶部位置
int MenuTop = WindowHeight / 3;

// 菜单名称
const char* itemNames[] = {
"功能项A",
"功能项B",
"功能项C",
"功能项D",
"功能项E",
"功能项F",
"功能项G"
};

// 菜单开关状态
bool* itemStates[] = {
&功能项A,
&功能项B,
&功能项C,
&功能项D,
&功能项E,
&功能项F,
&功能项G
};

// 计算功能项数量
const int itemCount = sizeof(itemNames) / sizeof(itemNames[0]); // 动态计算功能项数量
int TotalHeight = ExtraPadding + (itemCount * ItemHeight) + BottomPadding; // 计算总高度

// 绘制外边框
D3DDrawLib::Start()->DrawBorderRectangle(
MenuLeft,
MenuTop,
MenuWidth,
TotalHeight,
1,
BorderColor
);

// 绘制选择条
D3DDrawLib::Start()->DrawBorderRectangle(
MenuLeft + 5,
MenuTop + 9 + (CurrentlySelected - 1) * ItemHeight,
MenuWidth - 10,
17,
1,
HighlightColor
);

// 绘制功能项
for (int i = 0; i < itemCount; ++i)
{
D3DDrawLib::Start()->DrawStringAndString(
itemNames[i],
(*itemStates[i]) ? "[开启]" : "[关闭]",
12,
65,
MenuLeft + 10,
MenuTop + ExtraPadding + i * ItemHeight,
D3DCOLOR_XRGB(255, 52, 179),
(*itemStates[i]) ? EnabledColor : DisabledColor
);
}

// 绘制显示/隐藏按钮,位于矩形外部底部
D3DDrawLib::Start()->DrawString("HOME 显示/隐藏", 13, MenuLeft + 12, MenuTop + TotalHeight + 5, D3DCOLOR_RGBA(255, 0, 0, 255));
}
}

我们继续完善菜单功能,在上述代码不变的前提下,为菜单增加一个表头并在表头中显示一些文本文档以标注菜单名称及版本等信息,其他部分保持不变即可。

void Menu()
{
if (IsItDisplayed)
{
// 菜单配置
const int MenuWidth = 120; // 菜单宽度
const int ItemHeight = 25; // 每个功能项的高度
const int ExtraPadding = 10; // 顶部额外间距
const int BottomPadding = 0; // 底部额外间距(用于显示/隐藏按钮)
const int HeaderHeight = 20; // 表头高度

// 配置着色表
const D3DCOLOR BorderColor = D3DCOLOR_XRGB(255, 69, 0);
const D3DCOLOR HighlightColor = D3DCOLOR_XRGB(148, 0, 211);
const D3DCOLOR EnabledColor = D3DCOLOR_XRGB(0, 255, 0);
const D3DCOLOR DisabledColor = D3DCOLOR_XRGB(120, 250, 250);
const D3DCOLOR HeaderColor = D3DCOLOR_XRGB(255, 69, 0);

// 菜单位置(居左或者居右)
int MenuLeft = MenuOnTheRight ? WindowWidth - MenuWidth - WindowWidth / 30 : WindowWidth / 30;

// 菜单顶部位置
int MenuTop = WindowHeight / 3;

// 菜单名称
const char* itemNames[] = {
"功能项A",
"功能项B",
"功能项C",
"功能项D",
"功能项E",
"功能项F",
"功能项G"
};

// 菜单开关状态
bool* itemStates[] = {
&功能项A,
&功能项B,
&功能项C,
&功能项D,
&功能项E,
&功能项F,
&功能项G
};

// 计算功能项数量
const int itemCount = sizeof(itemNames) / sizeof(itemNames[0]); // 动态计算功能项数量
int TotalHeight = ExtraPadding + (itemCount * ItemHeight) + BottomPadding + HeaderHeight; // 计算总高度

// 绘制外边框
D3DDrawLib::Start()->DrawBorderRectangle(
MenuLeft,
MenuTop,
MenuWidth,
TotalHeight,
1,
BorderColor
);

// 绘制表头矩形
D3DDrawLib::Start()->DrawBorderRectangle(
MenuLeft,
MenuTop,
MenuWidth,
HeaderHeight,
1,
HeaderColor
);

// 计算表头文字的宽度
const char* headerText = "简单自绘菜单";

// 假设获取文本宽度
int headerTextWidth = 0;

// 这里我们假设每个字符宽度为7个像素
headerTextWidth = strlen(headerText) * 7;

// 居中绘制表头文字
int headerTextX = MenuLeft + 7 + (MenuWidth - headerTextWidth) / 2; // 计算居中的X坐标
D3DDrawLib::Start()->DrawString(headerText, 12, headerTextX, MenuTop + 2, D3DCOLOR_XRGB(0, 0, 139)); // 蓝色文字

// 绘制选择条
D3DDrawLib::Start()->DrawBorderRectangle(
MenuLeft + 5,
MenuTop + 9 + HeaderHeight + (CurrentlySelected - 1) * ItemHeight,
MenuWidth - 10,
17,
1,
HighlightColor
);

// 绘制功能项
for (int i = 0; i < itemCount; ++i)
{
D3DDrawLib::Start()->DrawStringAndString(
itemNames[i],
(*itemStates[i]) ? "[开启]" : "[关闭]",
12,
65,
MenuLeft + 10,
MenuTop + ExtraPadding + HeaderHeight + i * ItemHeight,
D3DCOLOR_XRGB(255, 52, 179),
(*itemStates[i]) ? EnabledColor : DisabledColor
);
}

// 绘制显示/隐藏按钮,位于矩形外部底部
D3DDrawLib::Start()->DrawString("HOME 显示/隐藏", 13, MenuLeft + 12, MenuTop + TotalHeight + 5, D3DCOLOR_RGBA(255, 0, 0, 255));
}
}

增加热键检测

接着我们需要为菜单增加热键检测功能,此处通过上下按钮来选择菜单项,通过左右按钮来实现菜单项的打开与关闭,在主函数中通过调用RegisterHotKey我们依次定义1-5数字,并将其关联到VK_UPVK_DOWNVK_LEFTVK_RIGHTVK_HOME热键上,在MyFunctionCallBack控制器回调中,通过GetMessage来获取键盘消息,并检测WM_HOTKEY是否为热键,若是则通过判断msg.wParam中的按键码来识别光标动作。

针对光标动作,若检测到上光标,由于CurrentlySelected默认选中项是从1开始的,则等于0则说明是最顶部,此时需要将CurrentlySelected设置为7,即跳转到最后一个菜单项上。

同理,若检测到下光标则判断CurrentlySelected是否为8,若是则说明到最后一个了此时就需要将CurrentlySelected指定为第一个菜单项上。

至于左光标与右光标,仅需要针对不同的CurrentlySelected设置不同的状态即可,最后则是通过判断是否为HOME键,若是则直接执行真假切换即可。

// 控制器函数
void MyFunctionCallBack()
{
MSG msg = { 0 };

GetMessage(&msg, NULL, 0, 0);

switch (msg.message)
{
case WM_HOTKEY:
{
// 上光标
if (1 == msg.wParam)
{
// CurrentlySelected是7 因为我们有7个功能
CurrentlySelected = CurrentlySelected - 1;
if (CurrentlySelected == 0)
{
CurrentlySelected = 7;
}
}

// 下光标
else if (2 == msg.wParam)
{
CurrentlySelected = CurrentlySelected + 1;
if (CurrentlySelected == 8)
{
CurrentlySelected = 1;
}
}

// 左光标
else if (3 == msg.wParam)
{
if (CurrentlySelected == 1)
{
功能项A = false;
}
else if (CurrentlySelected == 2)
{
功能项B = false;
}
else if (CurrentlySelected == 3)
{
功能项C = false;
}
else if (CurrentlySelected == 4)
{
功能项D = false;
}
else if (CurrentlySelected == 5)
{
功能项E = false;
}
else if (CurrentlySelected == 6)
{
功能项F = false;
}
else if (CurrentlySelected == 7)
{
功能项G = false;
}
}

// 右光标
else if (4 == msg.wParam)
{
if (CurrentlySelected == 1)
{
功能项A = true;
}
else if (CurrentlySelected == 2)
{
功能项B = true;
}
else if (CurrentlySelected == 3)
{
功能项C = true;
}
else if (CurrentlySelected == 4)
{
功能项D = true;
}
else if (CurrentlySelected == 5)
{
功能项E = true;
}
else if (CurrentlySelected == 6)
{
功能项F = true;
}
else if (CurrentlySelected == 7)
{
功能项G = true;
}
}

// HOME键
else if (5 == msg.wParam)
{
if (IsItDisplayed == true)
{
IsItDisplayed = false;
}
else if (IsItDisplayed == false)
{
IsItDisplayed = true;
}
}
break;
}
default:
break;
}
}

绘制菜单演示

最后,在主调函数中通过Draw函数将MenuMyFunctionCallBack串联起来,并将Draw绘制函数一并交给Start()->createWindow窗体创建类处理,则可以完成整个窗体的绘制部分。

void Draw()
{
Menu();
MyFunctionCallBack();
}

int main(int argc, char *argv[])
{
// 注册 [上下左右] 热键
RegisterHotKey(NULL, 1, 0, VK_UP);
RegisterHotKey(NULL, 2, 0, VK_DOWN);
RegisterHotKey(NULL, 3, 0, VK_LEFT);
RegisterHotKey(NULL, 4, 0, VK_RIGHT);
RegisterHotKey(NULL, 5, 0, VK_HOME);

// 运行线程
const char* class_name = "DrawingBoard";

WindowWidth = D3DDrawLib::Start()->GetWindowWidth(class_name, class_name);
WindowHeight = D3DDrawLib::Start()->GetWindowHeight(class_name, class_name);
D3DDrawLib::Start()->createWindow(class_name, class_name, "黑体", Draw);
return 0;
}

读者可自行编译该程序代码,并运行窗体类名为DrawingBoard的进程,当本程序被运行后则会默认在DrawingBoard进程左侧出现一个可以控制的动态菜单,此时通过使用上下功能键可选择不同功能,通过使用右键开启功能,使用左键则是关闭功能,其绘制效果如下图所示;

20240801155823

警告:本篇文章中所涉及的内容仅用于技术交流与研究之用,仅允许被用于正规用途或学习目的,请读者自觉遵守相关法规,禁止滥用。