diff --git a/20260320_new_keyboard.sln b/20260320_new_keyboard.sln
new file mode 100644
index 0000000..d11eed0
--- /dev/null
+++ b/20260320_new_keyboard.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31912.275
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "20260320_new_keyboard", "20260320_new_keyboard.vcxproj", "{33F77093-3FF4-4E32-B971-301ABF0133C5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|x64 = Debug|x64
+ Release|x64 = Release|x64
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {33F77093-3FF4-4E32-B971-301ABF0133C5}.Debug|x64.ActiveCfg = Debug|x64
+ {33F77093-3FF4-4E32-B971-301ABF0133C5}.Debug|x64.Build.0 = Debug|x64
+ {33F77093-3FF4-4E32-B971-301ABF0133C5}.Release|x64.ActiveCfg = Release|x64
+ {33F77093-3FF4-4E32-B971-301ABF0133C5}.Release|x64.Build.0 = Release|x64
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {DBF8B214-5A58-4E19-9235-54B803A6B2EA}
+ EndGlobalSection
+EndGlobal
diff --git a/20260320_new_keyboard.slnx b/20260320_new_keyboard.slnx
new file mode 100644
index 0000000..38837f3
--- /dev/null
+++ b/20260320_new_keyboard.slnx
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/20260320_new_keyboard.slnx.bak b/20260320_new_keyboard.slnx.bak
new file mode 100644
index 0000000..38837f3
--- /dev/null
+++ b/20260320_new_keyboard.slnx.bak
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/20260320_new_keyboard.vcxproj b/20260320_new_keyboard.vcxproj
new file mode 100644
index 0000000..8b22d51
--- /dev/null
+++ b/20260320_new_keyboard.vcxproj
@@ -0,0 +1,143 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ {24E649F5-C374-ADB3-DE49-835B2FCDE3DE}
+ QtVS_v304
+ 10.0
+ 10.0
+ $(LocalAppData)\QtMsBuild
+ $(MSBuildProjectDirectory)\QtMsBuild
+
+
+
+ Application
+ v143
+ true
+ Unicode
+
+
+ Application
+ v143
+ false
+ true
+ Unicode
+
+
+
+
+
+
+ D:\Qt\5.13.1\msvc2015_64
+ core;gui;widgets
+ debug
+
+
+ D:\Qt\5.13.1\msvc2015_64
+ core;gui;widgets
+ release
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(ProjectDir);%(AdditionalIncludeDirectories)
+ /utf-8 %(AdditionalOptions)
+ true
+ Level3
+ true
+ true
+
+
+ Windows
+ true
+
+
+
+
+ $(ProjectDir);%(AdditionalIncludeDirectories)
+ /utf-8 %(AdditionalOptions)
+ true
+ Level3
+ true
+ true
+ true
+ true
+
+
+ Windows
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/20260320_new_keyboard.vcxproj.filters b/20260320_new_keyboard.vcxproj.filters
new file mode 100644
index 0000000..d685d7e
--- /dev/null
+++ b/20260320_new_keyboard.vcxproj.filters
@@ -0,0 +1,149 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {99349809-55BA-4b9d-BF79-8FDBB0286EB3}
+ ui
+
+
+ {A64D7E2E-38B0-4FC7-A5E5-86BE065B5E14}
+
+
+ {A8E15F2D-A7FA-4303-B845-0E6A75B5906B}
+
+
+ {80F53A7E-0DA9-44B4-85ED-69D404BDE88A}
+
+
+ {0D0A047D-7113-47DE-9A05-D7BA3B12F9E2}
+
+
+ {3D7D0A5D-F0CB-49A6-A6D0-FA66F58B3111}
+
+
+ {2B601A06-899A-4D88-932D-9F6ED625B1E8}
+
+
+ {72C4499C-5806-4BBC-869D-0E4B33D867B6}
+
+
+ {E72E5301-BE63-408E-BD35-F817FD4A0D0E}
+
+
+ {7CB18420-5F64-4E2A-81CA-6A5E2BA4CFC8}
+
+
+ {7814B0B6-EA6A-49D3-9B6B-BC144B1AF55F}
+
+
+
+
+ Source Files
+
+
+ Source Files\APP
+
+
+ Source Files\APP
+
+
+ Source Files\APP
+
+
+ Source Files\APP
+
+
+ Source Files\APP
+
+
+ Source Files\DRI
+
+
+ Source Files\DRI
+
+
+ Source Files\DRI
+
+
+ Source Files\DEBUG
+
+
+ Source Files\LOGIC
+
+
+ Source Files\LOGIC
+
+
+ Source Files\LOGIC
+
+
+ Source Files\LOGIC
+
+
+ Source Files\LOGIC
+
+
+ Source Files\MID
+
+
+ Header Files\APP
+
+
+ Header Files\APP
+
+
+ Header Files\APP
+
+
+ Header Files\APP
+
+
+ Header Files\APP
+
+
+ Header Files\DRI
+
+
+ Header Files\DRI
+
+
+ Header Files\DRI
+
+
+ Header Files\DEBUG
+
+
+ Header Files\DEBUG
+
+
+ Header Files\LOGIC
+
+
+ Header Files\LOGIC
+
+
+ Header Files\LOGIC
+
+
+ Header Files\LOGIC
+
+
+ Header Files\LOGIC
+
+
+ Header Files\MID
+
+
+
diff --git a/APP/APP_GlassCard.cpp b/APP/APP_GlassCard.cpp
new file mode 100644
index 0000000..9bb7615
--- /dev/null
+++ b/APP/APP_GlassCard.cpp
@@ -0,0 +1,40 @@
+#include "APP/APP_GlassCard.h"
+
+#include
+
+namespace APP {
+
+APP_GlassCard::APP_GlassCard(QWidget* parent)
+ : QFrame(parent)
+{
+ // 交给我们自己统一绘制卡片外观,不使用 QFrame 默认边框。
+ setFrameShape(QFrame::NoFrame);
+ // 背景由 paintEvent 自绘,这里不走样式表背景。
+ setAttribute(Qt::WA_StyledBackground, false);
+}
+
+void APP_GlassCard::paintEvent(QPaintEvent* event)
+{
+ Q_UNUSED(event);
+
+ /*
+ * 卡片外观刻意保持简单,方便教学时理解自绘流程:
+ * 1. 先画一个带圆角的深色底板
+ * 2. 再画一圈细边框
+ */
+ const QRectF BodyRect = rect().adjusted(1.0, 1.0, -1.0, -1.0);
+ const qreal Radius = 22.0;
+ const QColor FillColor(30, 35, 43);
+ const QColor BorderColor(82, 92, 104);
+
+ QPainter Painter(this);
+ Painter.setRenderHint(QPainter::Antialiasing, true);
+
+ // 先画卡片主体。
+ Painter.setPen(QPen(BorderColor, 1.0));
+ Painter.setBrush(FillColor);
+ Painter.drawRoundedRect(BodyRect, Radius, Radius);
+
+}
+
+} // namespace APP
diff --git a/APP/APP_GlassCard.h b/APP/APP_GlassCard.h
new file mode 100644
index 0000000..0aa1540
--- /dev/null
+++ b/APP/APP_GlassCard.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include
+
+namespace APP {
+
+/*
+ * 这是项目里所有“卡片容器”的基础控件。
+ *
+ * 它只负责统一外观,不负责任何业务逻辑:
+ * 1. 统一圆角卡片风格
+ * 2. 统一边框和暗色底板
+ *
+ * 上层像主页卡片、调试卡片都直接继承它。
+ */
+class APP_GlassCard : public QFrame
+{
+public:
+ // 构造一个带统一外观的卡片容器。
+ explicit APP_GlassCard(QWidget* parent = nullptr);
+
+protected:
+ // 卡片背景和圆角边框都在这里自绘。
+ void paintEvent(QPaintEvent* event) override;
+};
+
+} // namespace APP
diff --git a/APP/APP_KeyButton.cpp b/APP/APP_KeyButton.cpp
new file mode 100644
index 0000000..0d68be2
--- /dev/null
+++ b/APP/APP_KeyButton.cpp
@@ -0,0 +1,177 @@
+#include "APP/APP_KeyButton.h"
+
+#include "APP/APP_Theme.h"
+#include
+#include
+
+namespace {
+
+/*
+ * 这个小工具函数用来把两种颜色按比例混合。
+ * 当前项目里主要用它来根据按键状态生成不同深浅的背景色和边框色。
+ */
+QColor App_Func_MixColor(const QColor& Left, const QColor& Right, qreal Value)
+{
+ const qreal Rate = qBound(0.0, Value, 1.0);
+
+ return QColor(
+ qRound(Left.red() + (Right.red() - Left.red()) * Rate),
+ qRound(Left.green() + (Right.green() - Left.green()) * Rate),
+ qRound(Left.blue() + (Right.blue() - Left.blue()) * Rate),
+ qRound(Left.alpha() + (Right.alpha() - Left.alpha()) * Rate));
+}
+
+} // namespace
+
+namespace APP {
+
+APP_KeyButton::APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent)
+ : QPushButton(parent),
+ appKeyInfo(KeyInfo)
+{
+ // 这里把按钮本身的交互属性定下来,后面就主要交给 paintEvent 自绘。
+ setCursor(Qt::PointingHandCursor);
+ setFlat(true);
+ setMinimumSize(78, 78);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+}
+
+void APP_KeyButton::App_Func_SetLatched(bool IsLatched)
+{
+ // 状态没变时不重复刷新,避免无意义重绘。
+ if (appIsLatched == IsLatched)
+ {
+ return;
+ }
+
+ appIsLatched = IsLatched;
+ update();
+}
+
+void APP_KeyButton::App_Func_SetPressed(bool IsPressed)
+{
+ // 状态没变时同样直接返回。
+ if (appIsPressed == IsPressed)
+ {
+ return;
+ }
+
+ appIsPressed = IsPressed;
+ update();
+}
+
+void APP_KeyButton::paintEvent(QPaintEvent* event)
+{
+ Q_UNUSED(event);
+
+ // 留一点内边距,让卡片式按键看起来不那么“顶满”。
+ const QRectF ButtonRect = rect().adjusted(6.0, 6.0, -6.0, -6.0);
+ const qreal Radius = 14.0;
+
+ QColor FillColor = App_Func_GetBackgroundColor();
+ QColor OutlineColor = App_Func_GetBorderColor();
+
+ // 鼠标按下时再做一层轻微压暗,形成“压下去”的感觉。
+ if (isDown())
+ {
+ FillColor = FillColor.darker(108);
+ OutlineColor = OutlineColor.darker(112);
+ }
+
+ QPainter Painter(this);
+ Painter.setRenderHint(QPainter::Antialiasing, true);
+ Painter.setRenderHint(QPainter::TextAntialiasing, true);
+
+ // 先画背景和边框。
+ Painter.setPen(QPen(OutlineColor, 1.2));
+ Painter.setBrush(FillColor);
+ Painter.drawRoundedRect(ButtonRect, Radius, Radius);
+
+ // hint 一般显示在左上角,比如 Num、Fn 这类短提示。
+ if (!appKeyInfo.hint.isEmpty())
+ {
+ Painter.setFont(APP_Theme::App_Func_GetKeyHintFont());
+ Painter.setPen(App_Func_GetTextColor().lighter(115));
+ Painter.drawText(
+ ButtonRect.adjusted(10.0, 8.0, -10.0, -8.0),
+ Qt::AlignLeft | Qt::AlignTop,
+ appKeyInfo.hint.toUpper());
+ }
+
+ // 主文字放中间。文字长度不同就稍微缩一下字号,避免挤出按钮。
+ QFont LabelFont = APP_Theme::App_Func_GetKeyLabelFont();
+ if (appKeyInfo.label.size() > 2)
+ {
+ LabelFont.setPointSize(LabelFont.pointSize() - 4);
+ }
+ else if (appKeyInfo.label.size() == 2)
+ {
+ LabelFont.setPointSize(LabelFont.pointSize() - 2);
+ }
+
+ Painter.setFont(LabelFont);
+ Painter.setPen(App_Func_GetTextColor());
+ Painter.drawText(ButtonRect, Qt::AlignCenter, appKeyInfo.label);
+}
+
+QColor APP_KeyButton::App_Func_GetAccentColor() const
+{
+ // 每个 tone 对应一组强调色,用来让不同类别的键略有区分。
+ switch (appKeyInfo.tone)
+ {
+ case APP_KeyTone::Aqua:
+ return QColor(72, 184, 162);
+ case APP_KeyTone::Amber:
+ return QColor(224, 172, 76);
+ case APP_KeyTone::Blue:
+ return QColor(103, 146, 224);
+ case APP_KeyTone::Normal:
+ default:
+ return QColor(150, 168, 196);
+ }
+}
+
+QColor APP_KeyButton::App_Func_GetBackgroundColor() const
+{
+ // 先以统一暗色为底,再按状态叠加强调色。
+ const QColor BaseColor(55, 61, 70);
+
+ // 按下态最强。
+ if (appIsPressed)
+ {
+ return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), 0.56);
+ }
+
+ // 锁定态次之。
+ if (appKeyInfo.tone != APP_KeyTone::Normal || appIsLatched)
+ {
+ return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), appIsLatched ? 0.35 : 0.18);
+ }
+
+ return BaseColor;
+}
+
+QColor APP_KeyButton::App_Func_GetBorderColor() const
+{
+ const QColor BaseColor(104, 114, 126);
+
+ if (appIsPressed)
+ {
+ return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), 0.72);
+ }
+
+ if (appKeyInfo.tone != APP_KeyTone::Normal || appIsLatched)
+ {
+ return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), appIsLatched ? 0.45 : 0.25);
+ }
+
+ return BaseColor;
+}
+
+QColor APP_KeyButton::App_Func_GetTextColor() const
+{
+ // 当前项目固定用高亮浅色字,保证暗底上阅读清晰。
+ return QColor(238, 242, 247);
+}
+
+} // namespace APP
diff --git a/APP/APP_KeyButton.h b/APP/APP_KeyButton.h
new file mode 100644
index 0000000..3ecfd17
--- /dev/null
+++ b/APP/APP_KeyButton.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "APP/APP_KeypadModel.h"
+#include
+#include
+
+namespace APP {
+
+/*
+ * 这是小键盘界面里的单个按键控件。
+ *
+ * 它的职责很单纯:
+ * 1. 保存这颗键自己的显示信息
+ * 2. 根据“锁定态 / 按下态”切换颜色
+ * 3. 自己完成绘制
+ *
+ * 它不负责协议解析,也不直接参与 DRI / LGC 逻辑。
+ */
+class APP_KeyButton : public QPushButton
+{
+public:
+ explicit APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent = nullptr);
+
+ // 锁定态通常表示这颗键处于某种功能模式。
+ void App_Func_SetLatched(bool IsLatched);
+
+ // 按下态表示实体键或逻辑键当前真的处于按下中。
+ void App_Func_SetPressed(bool IsPressed);
+
+protected:
+ // 每颗键都自己画背景、边框、提示文字和主文字。
+ void paintEvent(QPaintEvent* event) override;
+
+private:
+ // 下面这些函数只负责给 paintEvent 提供颜色。
+ QColor App_Func_GetAccentColor() const;
+ QColor App_Func_GetBackgroundColor() const;
+ QColor App_Func_GetBorderColor() const;
+ QColor App_Func_GetTextColor() const;
+
+ // 这颗键对应的显示信息。
+ APP_KeyInfo appKeyInfo;
+
+ // 是否处于锁定态。
+ bool appIsLatched = false;
+
+ // 是否处于按下态。
+ bool appIsPressed = false;
+};
+
+} // namespace APP
diff --git a/APP/APP_KeypadModel.cpp b/APP/APP_KeypadModel.cpp
new file mode 100644
index 0000000..03ee4bf
--- /dev/null
+++ b/APP/APP_KeypadModel.cpp
@@ -0,0 +1,135 @@
+#include "APP/APP_KeypadModel.h"
+
+namespace APP {
+
+APP_KeypadModel::APP_KeypadModel()
+{
+ appKeyList = {
+ {QStringLiteral("num"), QStringLiteral("Num"), QStringLiteral(""), 0x0053, 0, 0, 1, 1, APP_KeyTone::Aqua},
+ {QStringLiteral("divide"), QStringLiteral("/"), QStringLiteral(""), 0x0054, 0, 1, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("multiply"), QStringLiteral("*"), QStringLiteral(""), 0x0055, 0, 2, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("minus"), QStringLiteral("-"), QStringLiteral(""), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber},
+ {QStringLiteral("7"), QStringLiteral("7"), QStringLiteral("Home"), 0x005F, 1, 0, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("↑"), 0x0060, 1, 1, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("9"), QStringLiteral("9"), QStringLiteral("PgUp"), 0x0061, 1, 2, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("plus"), QStringLiteral("+"), QStringLiteral(""), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua},
+ {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("←"), 0x005C, 2, 0, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("5"), QStringLiteral("5"), QStringLiteral(""), 0x005D, 2, 1, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("→"), 0x005E, 2, 2, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("End"), 0x0059, 3, 0, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("↓"), 0x005A, 3, 1, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("3"), QStringLiteral("3"), QStringLiteral("PgDn"), 0x005B, 3, 2, 1, 1, APP_KeyTone::Normal},
+ {QStringLiteral("enter"), QStringLiteral("Enter"), QStringLiteral(""), 0x0058, 3, 3, 2, 1, APP_KeyTone::Blue},
+ {QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("Ins"), 0x0062, 4, 0, 1, 2, APP_KeyTone::Normal},
+ {QStringLiteral("dot"), QStringLiteral("."), QStringLiteral("Del"), 0x0063, 4, 2, 1, 1, APP_KeyTone::Normal}
+ };
+
+ appFunctionKeyList = {
+ {QStringLiteral("7"), QStringLiteral("7"), QStringLiteral("功能"), 0x005F, 0, 0, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("功能"), 0x0060, 0, 1, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("9"), QStringLiteral("9"), QStringLiteral("功能"), 0x0061, 0, 2, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("minus"), QStringLiteral("-"), QStringLiteral("功能"), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber},
+ {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("功能"), 0x005C, 1, 0, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("5"), QStringLiteral("5"), QStringLiteral("功能"), 0x005D, 1, 1, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("功能"), 0x005E, 1, 2, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("plus"), QStringLiteral("+"), QStringLiteral("功能"), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua},
+ {QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("功能"), 0x0059, 2, 0, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("功能"), 0x005A, 2, 1, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("3"), QStringLiteral("3"), QStringLiteral("功能"), 0x005B, 2, 2, 1, 1, APP_KeyTone::Blue},
+ {QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("功能"), 0x0062, 3, 0, 1, 3, APP_KeyTone::Blue}
+ };
+}
+
+const QVector& APP_KeypadModel::App_Func_GetKeyList() const
+{
+ return appKeyList;
+}
+
+const QVector& APP_KeypadModel::App_Func_GetFunctionKeyList() const
+{
+ return appFunctionKeyList;
+}
+
+bool APP_KeypadModel::App_Func_IsLatched(const QString& KeyId) const
+{
+ return (KeyId == QStringLiteral("num")) && appNumLockOn;
+}
+
+bool APP_KeypadModel::App_Func_IsPressed(const QString& KeyId) const
+{
+ return appPressedKeyIdList.contains(KeyId);
+}
+
+quint16 APP_KeypadModel::App_Func_GetUsageFromKeyId(const QString& KeyId) const
+{
+ for (const APP_KeyInfo& KeyInfo : appKeyList)
+ {
+ if (KeyInfo.id == KeyId)
+ {
+ return KeyInfo.usage;
+ }
+ }
+
+ return 0;
+}
+
+void APP_KeypadModel::App_Func_SetNumLockOn(bool IsOn)
+{
+ appNumLockOn = IsOn;
+}
+
+void APP_KeypadModel::App_Func_ClearPressedKeys()
+{
+ appPressedKeyIdList.clear();
+}
+
+void APP_KeypadModel::App_Func_SetPressedKeysFromUsageList(const QVector& UsageList)
+{
+ appPressedKeyIdList.clear();
+
+ for (quint16 Usage : UsageList)
+ {
+ const QString KeyId = App_Func_GetKeyIdFromUsage(App_Func_MapUsageForUi(Usage));
+ if (!KeyId.isEmpty() && !appPressedKeyIdList.contains(KeyId))
+ {
+ appPressedKeyIdList.append(KeyId);
+ }
+ }
+}
+
+void APP_KeypadModel::App_Func_SetSwapUsagePair(quint16 UsageLeft, quint16 UsageRight, bool IsEnabled)
+{
+ appIsSwapOn = IsEnabled;
+ appSwapUsageLeft = UsageLeft;
+ appSwapUsageRight = UsageRight;
+}
+
+quint16 APP_KeypadModel::App_Func_MapUsageForUi(quint16 Usage) const
+{
+ if (!appIsSwapOn || (appSwapUsageLeft == 0) || (appSwapUsageRight == 0) || (appSwapUsageLeft == appSwapUsageRight))
+ {
+ return Usage;
+ }
+
+ if (Usage == appSwapUsageLeft)
+ {
+ return appSwapUsageRight;
+ }
+
+ return (Usage == appSwapUsageRight) ? appSwapUsageLeft : Usage;
+}
+
+QString APP_KeypadModel::App_Func_GetKeyIdFromUsage(quint16 Usage) const
+{
+ for (const APP_KeyInfo& KeyInfo : appKeyList)
+ {
+ if (KeyInfo.usage == Usage)
+ {
+ return KeyInfo.id;
+ }
+ }
+
+ return QString();
+}
+
+} // namespace APP
diff --git a/APP/APP_KeypadModel.h b/APP/APP_KeypadModel.h
new file mode 100644
index 0000000..17a9584
--- /dev/null
+++ b/APP/APP_KeypadModel.h
@@ -0,0 +1,58 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace APP {
+
+enum class APP_KeyTone
+{
+ Normal,
+ Aqua,
+ Amber,
+ Blue
+};
+
+struct APP_KeyInfo
+{
+ QString id;
+ QString label;
+ QString hint;
+ quint16 usage = 0;
+ int row = 0;
+ int column = 0;
+ int rowSpan = 1;
+ int columnSpan = 1;
+ APP_KeyTone tone = APP_KeyTone::Normal;
+};
+
+class APP_KeypadModel
+{
+public:
+ APP_KeypadModel();
+
+ const QVector& App_Func_GetKeyList() const;
+ const QVector& App_Func_GetFunctionKeyList() const;
+ bool App_Func_IsLatched(const QString& KeyId) const;
+ bool App_Func_IsPressed(const QString& KeyId) const;
+ quint16 App_Func_GetUsageFromKeyId(const QString& KeyId) const;
+ void App_Func_SetNumLockOn(bool IsOn);
+ void App_Func_ClearPressedKeys();
+ void App_Func_SetPressedKeysFromUsageList(const QVector& UsageList);
+ void App_Func_SetSwapUsagePair(quint16 UsageLeft, quint16 UsageRight, bool IsEnabled);
+
+private:
+ quint16 App_Func_MapUsageForUi(quint16 Usage) const;
+ QString App_Func_GetKeyIdFromUsage(quint16 Usage) const;
+
+ QVector appKeyList;
+ QVector appFunctionKeyList;
+ QStringList appPressedKeyIdList;
+ bool appNumLockOn = false;
+ bool appIsSwapOn = false;
+ quint16 appSwapUsageLeft = 0;
+ quint16 appSwapUsageRight = 0;
+};
+
+} // namespace APP
diff --git a/APP/APP_Theme.cpp b/APP/APP_Theme.cpp
new file mode 100644
index 0000000..7aec925
--- /dev/null
+++ b/APP/APP_Theme.cpp
@@ -0,0 +1,131 @@
+#include "APP/APP_Theme.h"
+
+#include
+#include
+
+namespace APP {
+
+QPalette APP_Theme::App_Func_GetPalette()
+{
+ /*
+ * 不用样式表时,Qt 最稳妥的统一美化方式就是调色板:
+ * 1. 先选 Fusion 风格
+ * 2. 再给标准控件一组统一颜色
+ */
+ QPalette Palette;
+
+ const QColor WindowColor(20, 25, 33);
+ const QColor BaseColor(28, 34, 42);
+ const QColor AltBaseColor(34, 40, 49);
+ const QColor ButtonColor(38, 44, 54);
+ const QColor BorderHintColor(86, 96, 108);
+ const QColor HighlightColor(72, 184, 162);
+ const QColor TextColor(238, 242, 247);
+ const QColor DimTextColor(162, 170, 182);
+
+ Palette.setColor(QPalette::Window, WindowColor);
+ Palette.setColor(QPalette::WindowText, TextColor);
+ Palette.setColor(QPalette::Base, BaseColor);
+ Palette.setColor(QPalette::AlternateBase, AltBaseColor);
+ Palette.setColor(QPalette::Text, TextColor);
+ Palette.setColor(QPalette::Button, ButtonColor);
+ Palette.setColor(QPalette::ButtonText, TextColor);
+ Palette.setColor(QPalette::BrightText, QColor(255, 255, 255));
+ Palette.setColor(QPalette::Light, BorderHintColor.lighter(120));
+ Palette.setColor(QPalette::Midlight, BorderHintColor);
+ Palette.setColor(QPalette::Mid, BorderHintColor.darker(120));
+ Palette.setColor(QPalette::Dark, WindowColor.darker(140));
+ Palette.setColor(QPalette::Shadow, QColor(0, 0, 0, 140));
+ Palette.setColor(QPalette::Highlight, HighlightColor);
+ Palette.setColor(QPalette::HighlightedText, TextColor);
+ Palette.setColor(QPalette::ToolTipBase, BaseColor);
+ Palette.setColor(QPalette::ToolTipText, TextColor);
+ Palette.setColor(QPalette::Link, QColor(103, 146, 224));
+ Palette.setColor(QPalette::PlaceholderText, QColor(170, 178, 188, 170));
+ Palette.setColor(QPalette::Disabled, QPalette::WindowText, DimTextColor);
+ Palette.setColor(QPalette::Disabled, QPalette::Text, DimTextColor);
+ Palette.setColor(QPalette::Disabled, QPalette::ButtonText, DimTextColor);
+
+ return Palette;
+}
+
+QFont APP_Theme::App_Func_GetBodyFont()
+{
+ // 正文用相对稳妥、系统常见的字体候选。
+ QFont Font(App_Func_PickFontFamily(QStringList()
+ << QStringLiteral("Segoe UI Variable Text")
+ << QStringLiteral("Microsoft YaHei UI")
+ << QStringLiteral("Segoe UI")
+ << QStringLiteral("Bahnschrift")));
+ Font.setPointSize(10);
+ Font.setWeight(QFont::Medium);
+ return Font;
+}
+
+QFont APP_Theme::App_Func_GetTitleFont()
+{
+ // 页面标题字号更大、字重更高。
+ QFont Font(App_Func_PickFontFamily(QStringList()
+ << QStringLiteral("Segoe UI Variable Display Semibold")
+ << QStringLiteral("Microsoft YaHei UI")
+ << QStringLiteral("Bahnschrift SemiBold")));
+ Font.setPointSize(21);
+ Font.setWeight(QFont::DemiBold);
+ return Font;
+}
+
+QFont APP_Theme::App_Func_GetMetricFont()
+{
+ // 指标类标题用比正文更有力度的字重。
+ QFont Font(App_Func_PickFontFamily(QStringList()
+ << QStringLiteral("Bahnschrift SemiBold")
+ << QStringLiteral("Segoe UI Semibold")
+ << QStringLiteral("Microsoft YaHei UI")));
+ Font.setPointSize(12);
+ Font.setWeight(QFont::DemiBold);
+ return Font;
+}
+
+QFont APP_Theme::App_Func_GetKeyLabelFont()
+{
+ // 按键主文字字号较大,保证小键盘一眼能看清。
+ QFont Font(App_Func_PickFontFamily(QStringList()
+ << QStringLiteral("Bahnschrift SemiBold")
+ << QStringLiteral("Segoe UI Semibold")
+ << QStringLiteral("Microsoft YaHei UI")));
+ Font.setPointSize(22);
+ return Font;
+}
+
+QFont APP_Theme::App_Func_GetKeyHintFont()
+{
+ // 按键 hint 放左上角,所以字号更小。
+ QFont Font(App_Func_PickFontFamily(QStringList()
+ << QStringLiteral("Segoe UI Semibold")
+ << QStringLiteral("Bahnschrift SemiBold")
+ << QStringLiteral("Microsoft YaHei UI")));
+ Font.setPointSize(8);
+ Font.setLetterSpacing(QFont::AbsoluteSpacing, 1.0);
+ return Font;
+}
+
+QString APP_Theme::App_Func_PickFontFamily(const QStringList& FamilyList)
+{
+ // 从候选字体里依次挑选系统真实存在的字体。
+ const QFontDatabase Database;
+ const QStringList AvailableFamilyList = Database.families();
+
+ for (int Index = 0; Index < FamilyList.size(); ++Index)
+ {
+ const QString& Family = FamilyList.at(Index);
+ if (AvailableFamilyList.contains(Family))
+ {
+ return Family;
+ }
+ }
+
+ // 如果都不存在,就退回 Qt 当前默认字体。
+ return QApplication::font().family();
+}
+
+} // namespace APP
diff --git a/APP/APP_Theme.h b/APP/APP_Theme.h
new file mode 100644
index 0000000..61924d7
--- /dev/null
+++ b/APP/APP_Theme.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace APP {
+
+/*
+ * 主题模块现在只保留一套固定暗色风格。
+ *
+ * - 不参与 DRI 枚举
+ * - 不参与协议解析
+ * - 不参与业务判断
+ */
+class APP_Theme
+{
+public:
+ // 返回标准控件使用的统一调色板。
+ static QPalette App_Func_GetPalette();
+
+ // 正文说明文字的默认字体。
+ static QFont App_Func_GetBodyFont();
+ // 页面标题字体。
+ static QFont App_Func_GetTitleFont();
+ // 指标、卡片主标题使用的强调字体。
+ static QFont App_Func_GetMetricFont();
+ // 键帽中央主文字字体。
+ static QFont App_Func_GetKeyLabelFont();
+ // 键帽角落提示文字字体。
+ static QFont App_Func_GetKeyHintFont();
+
+private:
+ // 从候选字体列表中挑出当前系统真实存在的一项。
+ static QString App_Func_PickFontFamily(const QStringList& FamilyList);
+};
+
+} // namespace APP
diff --git a/APP/APP_UIWindow.cpp b/APP/APP_UIWindow.cpp
new file mode 100644
index 0000000..71c257c
--- /dev/null
+++ b/APP/APP_UIWindow.cpp
@@ -0,0 +1,622 @@
+#include "APP/APP_UIWindow.h"
+
+#include "APP/APP_GlassCard.h"
+#include "APP/APP_KeyButton.h"
+#include "APP/APP_Theme.h"
+#include "LOGIC/Lgc_Func_Button.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace APP {
+
+namespace {
+
+class App_TabBar : public QTabBar
+{
+public:
+ explicit App_TabBar(QWidget* parent = nullptr)
+ : QTabBar(parent)
+ {
+ setDrawBase(false);
+ setExpanding(true);
+ setUsesScrollButtons(false);
+ setElideMode(Qt::ElideNone);
+ setMouseTracking(true);
+ setCursor(Qt::PointingHandCursor);
+ }
+
+protected:
+ QSize tabSizeHint(int index) const override
+ {
+ QSize Size = QTabBar::tabSizeHint(index);
+ Size.rheight() = qMax(Size.height(), 38);
+ Size.rwidth() += 18;
+ return Size;
+ }
+
+ void mouseMoveEvent(QMouseEvent* event) override
+ {
+ const int HoverIndex = tabAt(event->pos());
+ if (appHoverIndex != HoverIndex)
+ {
+ appHoverIndex = HoverIndex;
+ update();
+ }
+
+ QTabBar::mouseMoveEvent(event);
+ }
+
+ void leaveEvent(QEvent* event) override
+ {
+ appHoverIndex = -1;
+ update();
+ QTabBar::leaveEvent(event);
+ }
+
+ void paintEvent(QPaintEvent* event) override
+ {
+ Q_UNUSED(event);
+
+ QPainter Painter(this);
+ Painter.setRenderHint(QPainter::Antialiasing, true);
+ Painter.setFont(APP_Theme::App_Func_GetBodyFont());
+
+ for (int Index = 0; Index < count(); ++Index)
+ {
+ QRect TabRect = tabRect(Index).adjusted(3, 5, -3, 0);
+ const bool IsSelected = (Index == currentIndex());
+ const bool IsHovered = (Index == appHoverIndex);
+
+ QColor FillColor(36, 40, 48);
+ QColor BorderColor(92, 102, 114);
+ QColor TextColor(214, 222, 232);
+
+ if (IsHovered)
+ {
+ FillColor = QColor(48, 54, 64);
+ }
+
+ if (IsSelected)
+ {
+ FillColor = QColor(58, 64, 74);
+ BorderColor = QColor(120, 132, 146);
+ TextColor = QColor(238, 242, 247);
+ }
+
+ Painter.setPen(QPen(BorderColor, 1.0));
+ Painter.setBrush(FillColor);
+ Painter.drawRoundedRect(TabRect, 8.0, 8.0);
+
+ Painter.setPen(TextColor);
+ Painter.drawText(TabRect, Qt::AlignCenter, tabText(Index));
+ }
+ }
+
+private:
+ int appHoverIndex = -1;
+};
+
+/*
+ * Qt 把 setTabBar() 设成 protected,
+ * 所以这里保留一个最小子类,只负责注入我们自绘的页签栏。
+ * 这层不是业务封装,而是 Qt API 访问限制带来的必要适配。
+ */
+class App_PageTabWidget : public QTabWidget
+{
+public:
+ explicit App_PageTabWidget(QWidget* parent = nullptr)
+ : QTabWidget(parent)
+ {
+ setTabBar(new App_TabBar(this));
+ setDocumentMode(true);
+ tabBar()->setExpanding(true);
+ }
+};
+
+QLabel* App_Func_CreateLabel(QWidget* parent, const QString& text, const QFont& font, bool wordWrap = false)
+{
+ QLabel* label = new QLabel(text, parent);
+ label->setFont(font);
+ label->setAttribute(Qt::WA_TranslucentBackground, true);
+ label->setWordWrap(wordWrap);
+ return label;
+}
+
+APP_GlassCard* App_Func_CreateCard(QWidget* parent, QVBoxLayout** pp_Layout)
+{
+ APP_GlassCard* card = new APP_GlassCard(parent);
+
+ QVBoxLayout* layout = new QVBoxLayout(card);
+ layout->setContentsMargins(20, 20, 20, 20);
+ layout->setSpacing(14);
+ *pp_Layout = layout;
+ return card;
+}
+
+void App_Func_SetGridStretch(QGridLayout* grid, int columnCount, int rowCount)
+{
+ for (int column = 0; column < columnCount; ++column)
+ {
+ grid->setColumnStretch(column, 1);
+ }
+ for (int row = 0; row < rowCount; ++row)
+ {
+ grid->setRowStretch(row, 1);
+ }
+}
+
+} // namespace
+
+App_UIWindow::App_UIWindow(QWidget* parent)
+ : QWidget(parent)
+{
+ App_Func_InitWindow();
+ App_Func_InitUi();
+ App_Func_InitConnect();
+ App_Func_InitLogic();
+ App_Func_RefreshUi();
+}
+
+App_UIWindow::~App_UIWindow()
+{
+ Lgc_Core_Func_Close(&appLgcState);
+}
+
+void App_UIWindow::paintEvent(QPaintEvent* event)
+{
+ Q_UNUSED(event);
+
+ QPainter Painter(this);
+ Painter.setRenderHint(QPainter::Antialiasing, true);
+ Painter.fillRect(rect(), palette().color(QPalette::Window));
+}
+
+bool App_UIWindow::nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result)
+{
+ /*
+ * Windows 原生消息不在 APP 层解析。
+ * APP 只做一件事:把消息往下转给 LGC / DRI。
+ */
+ Lgc_Core_Func_HandleNativeMessage(&appLgcState, p_Message);
+ return QWidget::nativeEvent(EventType, p_Message, p_Result);
+}
+
+void App_UIWindow::App_Func_InitWindow()
+{
+ setWindowTitle(QStringLiteral("数字键盘上位机"));
+
+#if APP_ENABLE_DEBUG_WINDOW
+ setMinimumSize(640, 960);
+ resize(700, 1020);
+#else
+ setMinimumSize(620, 760);
+ resize(680, 820);
+#endif
+
+ setAttribute(Qt::WA_StyledBackground, true);
+}
+
+void App_UIWindow::App_Func_InitUi()
+{
+ QVBoxLayout* p_RootLayout = new QVBoxLayout(this);
+ p_RootLayout->setContentsMargins(26, 22, 26, 24);
+ p_RootLayout->setSpacing(14);
+
+ /*
+ * 页面切换直接用 QTabWidget。
+ * 这比自己维护一套切页按钮更短,也更适合教学。
+ */
+ App_PageTabWidget* p_PageTab = new App_PageTabWidget(this);
+
+ p_PageTab->addTab(App_Func_CreatePadCard(), QStringLiteral("小键盘"));
+ p_PageTab->addTab(App_Func_CreateFunctionRegisterCard(), QStringLiteral("功能注册"));
+ p_PageTab->addTab(App_Func_CreateFunctionConfigCard(), QStringLiteral("功能配置"));
+
+#if APP_ENABLE_DEBUG_WINDOW
+ p_PageTab->addTab(App_Func_CreateDebugCard(), QStringLiteral("调试"));
+#endif
+
+ p_RootLayout->addWidget(p_PageTab, 1);
+}
+
+void App_UIWindow::App_Func_InitConnect()
+{
+ /*
+ * 这份教学版刻意把所有 connect 放在同一个函数里,
+ * 让学生顺着“谁发信号 -> 谁收信号”一眼就能看完。
+ */
+ connect(&appTimerPoll, &QTimer::timeout, this, &App_UIWindow::App_Func_OnPollTimer);
+
+ /*
+ * 这些控件都在 InitUi 阶段固定创建完成,
+ * 所以这里直接 connect,不再加“防御性空指针判断”干扰阅读。
+ */
+ const auto ConnectConfigSignal = [this](auto Sender, auto Signal)
+ {
+ connect(Sender, Signal, this, [this]()
+ {
+ App_Func_UpdateFunctionConfigFromUi();
+ });
+ };
+
+ ConnectConfigSignal(appFunctionEditMacroText, &QLineEdit::textChanged);
+ ConnectConfigSignal(appFunctionEditWebsite, &QLineEdit::textChanged);
+ ConnectConfigSignal(appFunctionComboSwapLeft, qOverload(&QComboBox::currentIndexChanged));
+ ConnectConfigSignal(appFunctionComboSwapRight, qOverload(&QComboBox::currentIndexChanged));
+
+ /*
+ * 功能键按钮统一交给 QButtonGroup 管,
+ * 这样既能减少每颗按钮各写一段 lambda,
+ * 也正好把 Qt 里“按钮分组 + id 分发”的知识点展示出来。
+ */
+ connect(appFunctionButtonGroup, qOverload(&QButtonGroup::buttonClicked), this, [this](int ButtonId)
+ {
+ App_Func_OnFunctionKeyClicked(static_cast(ButtonId));
+ });
+
+#if APP_ENABLE_DEBUG_WINDOW
+ connect(appDebugPanel->Debug_Func_GetRefreshButton(), &QPushButton::clicked,
+ this, &App_UIWindow::App_Func_OnRefreshDeviceClicked);
+
+ connect(appDebugPanel->Debug_Func_GetClearButton(), &QPushButton::clicked,
+ this, &App_UIWindow::App_Func_OnClearLogClicked);
+
+ connect(appDebugPanel->Debug_Func_GetApplyConfigButton(), &QPushButton::clicked,
+ this, &App_UIWindow::App_Func_OnApplyDeviceConfigClicked);
+#endif
+}
+
+void App_UIWindow::App_Func_InitLogic()
+{
+ Lgc_Core_Func_Init(&appLgcState);
+ Lgc_Core_Func_SetWindowHandle(&appLgcState, reinterpret_cast(winId()));
+
+ App_Func_UpdateFunctionConfigFromUi();
+
+#if APP_ENABLE_DEBUG_WINDOW
+ App_Func_RefreshDeviceConfigFromState();
+#endif
+
+ Lgc_Core_Func_Start(&appLgcState);
+
+ /*
+ * 轮询不仅服务调试窗口,
+ * 小键盘状态页和功能键页本身也要靠它持续拿最新状态。
+ * 所以这里始终启动,不跟调试开关绑定。
+ */
+ appTimerPoll.setInterval(30);
+ appTimerPoll.start();
+
+ App_Func_RefreshAfterLogicChange();
+}
+
+void App_UIWindow::App_Func_RefreshUi()
+{
+ App_Func_RefreshKeypadButtons();
+ App_Func_RefreshFunctionButtons();
+ App_Func_RefreshFunctionStatus();
+ update();
+}
+
+void App_UIWindow::App_Func_RefreshKeypadState()
+{
+ appKeypadModel.App_Func_SetNumLockOn(appLgcState.IsSystemNumLockOn);
+ appKeypadModel.App_Func_SetSwapUsagePair(
+ appLgcState.SwapUsageLeft,
+ appLgcState.SwapUsageRight,
+ appLgcState.IsSwapModeOn);
+
+ const QVector* p_UsageList = nullptr;
+ if (appLgcState.IsPhysicalKeyStateValid)
+ {
+ p_UsageList = &appLgcState.PhysicalUsageList;
+ }
+ else if (appLgcState.IsVisibleKeyStateValid)
+ {
+ p_UsageList = &appLgcState.VisibleUsageList;
+ }
+
+ if (p_UsageList != nullptr)
+ {
+ appKeypadModel.App_Func_SetPressedKeysFromUsageList(*p_UsageList);
+ }
+ else
+ {
+ appKeypadModel.App_Func_ClearPressedKeys();
+ }
+}
+
+void App_UIWindow::App_Func_RefreshKeypadButtons()
+{
+ for (auto It = appKeypadButtonMap.begin(); It != appKeypadButtonMap.end(); ++It)
+ {
+ APP_KeyButton* p_Button = It.value();
+ p_Button->App_Func_SetLatched(appKeypadModel.App_Func_IsLatched(It.key()));
+ p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(It.key()));
+ }
+}
+
+void App_UIWindow::App_Func_RefreshFunctionButtons()
+{
+ for (auto It = appFunctionButtonMap.begin(); It != appFunctionButtonMap.end(); ++It)
+ {
+ const QString KeyId = It.key();
+ APP_KeyButton* p_Button = It.value();
+ const quint16 Usage = appKeypadModel.App_Func_GetUsageFromKeyId(KeyId);
+ const bool IsFunctionMode = Lgc_Core_Func_IsUsageFunctionMode(&appLgcState, Usage);
+
+ p_Button->App_Func_SetLatched(IsFunctionMode);
+ p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(KeyId));
+ }
+}
+
+void App_UIWindow::App_Func_RefreshFunctionStatus()
+{
+ appFunctionLabelStatus->setText(appLgcState.TextFunctionStatus.isEmpty()
+ ? QStringLiteral("等待功能键动作。")
+ : appLgcState.TextFunctionStatus);
+}
+
+void App_UIWindow::App_Func_RefreshDebugView()
+{
+#if APP_ENABLE_DEBUG_WINDOW
+ appDebugPanel->Debug_Func_SetConnectionText(appLgcState.TextConnection, appLgcState.IsConnected);
+ appDebugPanel->Debug_Func_SetLogText(appLgcState.TextLog);
+#endif
+}
+
+void App_UIWindow::App_Func_RefreshAfterLogicChange()
+{
+ /*
+ * 逻辑层状态一旦变化,界面层真正需要做的事情其实只有这三步:
+ * 1. 先把 LGC 状态同步到 APP 自己的显示模型
+ * 2. 再把调试文本同步到调试页
+ * 3. 最后统一刷新界面控件
+ */
+ App_Func_RefreshKeypadState();
+ App_Func_RefreshDebugView();
+ App_Func_RefreshUi();
+}
+
+void App_UIWindow::App_Func_UpdateFunctionConfigFromUi()
+{
+ const quint16 OldSwapUsageLeft = appLgcState.FunctionButtonConfig.SwapUsageLeft;
+ const quint16 OldSwapUsageRight = appLgcState.FunctionButtonConfig.SwapUsageRight;
+
+ appLgcState.FunctionButtonConfig.MacroText = appFunctionEditMacroText->text();
+ appLgcState.FunctionButtonConfig.WebsiteUrl = appFunctionEditWebsite->text();
+ appLgcState.FunctionButtonConfig.SwapUsageLeft =
+ static_cast(appFunctionComboSwapLeft->currentData().toUInt());
+ appLgcState.FunctionButtonConfig.SwapUsageRight =
+ static_cast(appFunctionComboSwapRight->currentData().toUInt());
+
+ if (appLgcState.IsSwapModeOn &&
+ ((OldSwapUsageLeft != appLgcState.FunctionButtonConfig.SwapUsageLeft) ||
+ (OldSwapUsageRight != appLgcState.FunctionButtonConfig.SwapUsageRight)))
+ {
+ Lgc_Core_Func_SetSwapMode(
+ &appLgcState,
+ appLgcState.FunctionButtonConfig.SwapUsageLeft,
+ appLgcState.FunctionButtonConfig.SwapUsageRight,
+ true);
+ App_Func_RefreshAfterLogicChange();
+ }
+}
+
+void App_UIWindow::App_Func_OnFunctionKeyClicked(quint16 Usage)
+{
+ const bool IsFunctionMode = Lgc_Core_Func_IsUsageFunctionMode(&appLgcState, Usage);
+ Lgc_Core_Func_SetUsageFunctionMode(&appLgcState, Usage, !IsFunctionMode);
+ App_Func_RefreshAfterLogicChange();
+}
+
+void App_UIWindow::App_Func_OnPollTimer()
+{
+ if (Lgc_Core_Func_Poll(&appLgcState))
+ {
+ App_Func_RefreshAfterLogicChange();
+ }
+}
+
+#if APP_ENABLE_DEBUG_WINDOW
+void App_UIWindow::App_Func_RefreshDeviceConfigFromState()
+{
+ appDebugPanel->Debug_Func_SetDeviceConfigText(
+ appLgcState.DeviceConfig.VendorId,
+ appLgcState.DeviceConfig.ProductId);
+
+ appDebugPanel->Debug_Func_SetConfigStatusText(
+ QStringLiteral("当前目标 VID:PID = 0x%1:0x%2")
+ .arg(appLgcState.DeviceConfig.VendorId, 4, 16, QLatin1Char('0'))
+ .arg(appLgcState.DeviceConfig.ProductId, 4, 16, QLatin1Char('0'))
+ .toUpper(),
+ true);
+}
+
+void App_UIWindow::App_Func_OnApplyDeviceConfigClicked()
+{
+ quint16 VendorId = 0;
+ quint16 ProductId = 0;
+
+ if (!appDebugPanel->Debug_Func_TryGetDeviceConfig(&VendorId, &ProductId))
+ {
+ appDebugPanel->Debug_Func_SetConfigStatusText(
+ QStringLiteral("VID / PID 输入无效,请输入十六进制,例如 1209 和 0001。"),
+ false);
+ return;
+ }
+
+ appLgcState.DeviceConfig.VendorId = VendorId;
+ appLgcState.DeviceConfig.ProductId = ProductId;
+ App_Func_OnRefreshDeviceClicked();
+}
+
+void App_UIWindow::App_Func_OnRefreshDeviceClicked()
+{
+ Lgc_Core_Func_RefreshDevice(&appLgcState);
+ App_Func_RefreshDeviceConfigFromState();
+ App_Func_RefreshAfterLogicChange();
+}
+
+void App_UIWindow::App_Func_OnClearLogClicked()
+{
+ Lgc_Core_Func_ClearLog(&appLgcState);
+ App_Func_RefreshDebugView();
+}
+#endif
+
+QWidget* App_UIWindow::App_Func_CreatePadCard()
+{
+ QVBoxLayout* p_Layout = nullptr;
+ APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout);
+
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("小键盘状态页"),
+ APP_Theme::App_Func_GetMetricFont()));
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页直接显示实体小键盘当前的按下状态,NumLock 也会同步点亮。"),
+ APP_Theme::App_Func_GetBodyFont(), true));
+
+ QGridLayout* p_Grid = new QGridLayout();
+ p_Grid->setSpacing(14);
+
+ const QVector& KeyList = appKeypadModel.App_Func_GetKeyList();
+
+ for (const APP_KeyInfo& Key : KeyList)
+ {
+ APP_KeyButton* p_Button = new APP_KeyButton(Key, p_Card);
+ appKeypadButtonMap.insert(Key.id, p_Button);
+ p_Grid->addWidget(p_Button, Key.row, Key.column, Key.rowSpan, Key.columnSpan);
+ }
+
+ App_Func_SetGridStretch(p_Grid, 4, 5);
+
+ p_Layout->addLayout(p_Grid, 1);
+ return p_Card;
+}
+
+QWidget* App_UIWindow::App_Func_CreateFunctionRegisterCard()
+{
+ QVBoxLayout* p_Layout = nullptr;
+ APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout);
+
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("功能注册"),
+ APP_Theme::App_Func_GetMetricFont()));
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页只负责决定哪些小键盘键切到功能键模式。按钮点亮表示该键已经注册为功能键;再次点击即可取消。"),
+ APP_Theme::App_Func_GetBodyFont(), true));
+
+ QGridLayout* p_Grid = new QGridLayout();
+ p_Grid->setSpacing(14);
+
+ const QVector& KeyList = appKeypadModel.App_Func_GetFunctionKeyList();
+ appFunctionButtonGroup = new QButtonGroup(this);
+
+ for (const APP_KeyInfo& Key : KeyList)
+ {
+ APP_KeyButton* p_Button = new APP_KeyButton(Key, p_Card);
+ appFunctionButtonMap.insert(Key.id, p_Button);
+ appFunctionButtonGroup->addButton(p_Button, static_cast(Key.usage));
+ p_Grid->addWidget(p_Button, Key.row, Key.column, Key.rowSpan, Key.columnSpan);
+ }
+
+ App_Func_SetGridStretch(p_Grid, 4, 4);
+ p_Layout->addLayout(p_Grid, 1);
+ return p_Card;
+}
+
+QWidget* App_UIWindow::App_Func_CreateFunctionConfigCard()
+{
+ QVBoxLayout* p_Layout = nullptr;
+ APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout);
+
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("功能配置"),
+ APP_Theme::App_Func_GetMetricFont()));
+ p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页只放功能案例参数。当前教学版保留 0=文本输入、1=按键交换、2=打开网址,学生能更清楚看到“注册”和“配置”是两件事。"),
+ APP_Theme::App_Func_GetBodyFont(), true));
+
+ QFormLayout* p_ConfigLayout = new QFormLayout();
+ p_ConfigLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+ p_ConfigLayout->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+ p_ConfigLayout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop);
+ p_ConfigLayout->setHorizontalSpacing(10);
+ p_ConfigLayout->setVerticalSpacing(10);
+ const auto AddConfigRow = [p_Card, p_ConfigLayout](const QString& text, QWidget* field)
+ {
+ p_ConfigLayout->addRow(
+ App_Func_CreateLabel(p_Card, text, APP_Theme::App_Func_GetBodyFont()),
+ field);
+ };
+
+ appFunctionEditMacroText = new QLineEdit(p_Card);
+ appFunctionEditMacroText->setText(QStringLiteral("HELLO WORLD!"));
+ appFunctionEditMacroText->setPlaceholderText(QStringLiteral("例如:HELLO WORLD!"));
+ AddConfigRow(QStringLiteral("功能键 0 文本"), appFunctionEditMacroText);
+
+ appFunctionComboSwapLeft = new QComboBox(p_Card);
+ appFunctionComboSwapRight = new QComboBox(p_Card);
+
+ const QVector& FunctionKeyList = appKeypadModel.App_Func_GetFunctionKeyList();
+ for (const APP_KeyInfo& Key : FunctionKeyList)
+ {
+ appFunctionComboSwapLeft->addItem(Key.label, static_cast(Key.usage));
+ appFunctionComboSwapRight->addItem(Key.label, static_cast(Key.usage));
+ }
+
+ const auto SetComboIndex = [](QComboBox* Combo, quint16 Usage)
+ {
+ const int Index = Combo->findData(static_cast(Usage));
+ if (Index >= 0)
+ {
+ Combo->setCurrentIndex(Index);
+ }
+ };
+
+ SetComboIndex(appFunctionComboSwapLeft, 0x005C);
+ SetComboIndex(appFunctionComboSwapRight, 0x005D);
+
+ QWidget* p_SwapEditor = new QWidget(p_Card);
+ QHBoxLayout* p_SwapLayout = new QHBoxLayout(p_SwapEditor);
+ p_SwapLayout->setContentsMargins(0, 0, 0, 0);
+ p_SwapLayout->setSpacing(8);
+ p_SwapLayout->addWidget(appFunctionComboSwapLeft);
+ p_SwapLayout->addWidget(App_Func_CreateLabel(p_SwapEditor, QStringLiteral("<->"),
+ APP_Theme::App_Func_GetBodyFont()));
+ p_SwapLayout->addWidget(appFunctionComboSwapRight);
+ p_SwapLayout->addStretch(1);
+ AddConfigRow(QStringLiteral("功能键 1 交换"), p_SwapEditor);
+
+ appFunctionEditWebsite = new QLineEdit(p_Card);
+ appFunctionEditWebsite->setText(QStringLiteral("https://www.deepseek.com/"));
+ appFunctionEditWebsite->setPlaceholderText(QStringLiteral("例如:https://www.deepseek.com/"));
+ AddConfigRow(QStringLiteral("功能键 2 网址"), appFunctionEditWebsite);
+
+ appFunctionLabelStatus = App_Func_CreateLabel(
+ p_Card, QStringLiteral("等待功能键动作。"),
+ APP_Theme::App_Func_GetBodyFont(), true);
+ AddConfigRow(QStringLiteral("最近一次动作"), appFunctionLabelStatus);
+
+ p_Layout->addLayout(p_ConfigLayout);
+ p_Layout->addStretch(1);
+ return p_Card;
+}
+
+#if APP_ENABLE_DEBUG_WINDOW
+QWidget* App_UIWindow::App_Func_CreateDebugCard()
+{
+ appDebugPanel = new DEBUG::Debug_Panel(this);
+ appDebugPanel->Debug_Func_SetConnectionText(QStringLiteral("未连接,等待枚举设备。"), false);
+ appDebugPanel->Debug_Func_SetLogText(QStringLiteral("等待收到输入包。"));
+ return appDebugPanel;
+}
+#endif
+
+} // namespace APP
diff --git a/APP/APP_UIWindow.h b/APP/APP_UIWindow.h
new file mode 100644
index 0000000..39ffe56
--- /dev/null
+++ b/APP/APP_UIWindow.h
@@ -0,0 +1,131 @@
+#pragma once
+
+#include "APP/APP_KeypadModel.h"
+#include "DEBUG/Debug_Config.h"
+#include "LOGIC/Lgc_Core.h"
+#include
+#include
+#include
+
+#if APP_ENABLE_DEBUG_WINDOW
+#include "DEBUG/Debug_Panel.h"
+#endif
+
+class QLabel;
+class QButtonGroup;
+class QComboBox;
+class QLineEdit;
+
+namespace APP {
+
+class APP_KeyButton;
+
+/*
+ * APP 主窗口负责把界面搭起来,并把 UI 事件转交给逻辑层。
+ * 它只做三件事:
+ * 1. 创建界面
+ * 2. 周期性刷新 UI
+ * 3. 把 Windows 原生消息转给 LGC / DRI
+ */
+class App_UIWindow : public QWidget
+{
+public:
+ // 构造主窗口并完成 UI、信号和逻辑初始化。
+ explicit App_UIWindow(QWidget* parent = nullptr);
+ // 析构时负责收尾逻辑层资源。
+ ~App_UIWindow() override;
+
+protected:
+ // 自绘主窗口背景和简单装饰。
+ void paintEvent(QPaintEvent* event) override;
+ // 接收 Windows 原生消息,再转给 LGC / DRI 处理。
+ bool nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result) override;
+
+private:
+ // 设置窗口标题、大小和基础属性。
+ void App_Func_InitWindow();
+ // 创建主界面控件和布局。
+ void App_Func_InitUi();
+ // 连接按钮、定时器等信号。
+ void App_Func_InitConnect();
+ // 初始化逻辑层状态和设备配置。
+ void App_Func_InitLogic();
+ // 执行一轮完整 UI 刷新。
+ void App_Func_RefreshUi();
+ // 刷新主键盘区状态摘要。
+ void App_Func_RefreshKeypadState();
+ // 刷新主键盘按钮显示。
+ void App_Func_RefreshKeypadButtons();
+ // 刷新功能键按钮显示。
+ void App_Func_RefreshFunctionButtons();
+ // 刷新功能配置区状态文字。
+ void App_Func_RefreshFunctionStatus();
+ // 刷新调试页显示内容。
+ void App_Func_RefreshDebugView();
+ // 逻辑状态改变后集中刷新必要区域。
+ void App_Func_RefreshAfterLogicChange();
+ // 把界面里的功能配置回写到逻辑层。
+ void App_Func_UpdateFunctionConfigFromUi();
+ // 处理功能键区按钮点击。
+ void App_Func_OnFunctionKeyClicked(quint16 Usage);
+ // 定时轮询设备输入与状态。
+ void App_Func_OnPollTimer();
+
+#if APP_ENABLE_DEBUG_WINDOW
+ /*
+ * 如果后续要整体删除调试功能,
+ * 可以优先从这些入口和 APP_ENABLE_DEBUG_WINDOW 宏开始删。
+ */
+ // 处理“应用 VID/PID”按钮点击。
+ void App_Func_OnApplyDeviceConfigClicked();
+ // 处理“刷新设备”按钮点击。
+ void App_Func_OnRefreshDeviceClicked();
+ // 处理“清空日志”按钮点击。
+ void App_Func_OnClearLogClicked();
+ // 用逻辑层当前状态刷新调试页输入框。
+ void App_Func_RefreshDeviceConfigFromState();
+#endif
+
+ // 创建左侧主键盘卡片。
+ QWidget* App_Func_CreatePadCard();
+ // 创建右侧功能键配置卡片。
+ QWidget* App_Func_CreateFunctionRegisterCard();
+ QWidget* App_Func_CreateFunctionConfigCard();
+
+#if APP_ENABLE_DEBUG_WINDOW
+ // 创建调试卡片。
+ QWidget* App_Func_CreateDebugCard();
+#endif
+
+ // 键盘布局模型,负责提供按键元数据。
+ APP_KeypadModel appKeypadModel;
+ // 主键盘按钮映射表:键名 -> 按钮对象。
+ QHash appKeypadButtonMap;
+ // 功能键按钮映射表:键名 -> 按钮对象。
+ QHash appFunctionButtonMap;
+ // 功能键按钮组,用来统一处理点击。
+ QButtonGroup* appFunctionButtonGroup = nullptr;
+
+ // 功能区状态说明标签。
+ QLabel* appFunctionLabelStatus = nullptr;
+ // 功能键 0 的文本宏输入框。
+ QLineEdit* appFunctionEditMacroText = nullptr;
+ // 功能键 1 左侧交换键选择框。
+ QComboBox* appFunctionComboSwapLeft = nullptr;
+ // 功能键 1 右侧交换键选择框。
+ QComboBox* appFunctionComboSwapRight = nullptr;
+ // 功能键 2 的网址输入框。
+ QLineEdit* appFunctionEditWebsite = nullptr;
+
+#if APP_ENABLE_DEBUG_WINDOW
+ // 调试面板实例。
+ DEBUG::Debug_Panel* appDebugPanel = nullptr;
+#endif
+
+ // 周期轮询逻辑层的定时器。
+ QTimer appTimerPoll;
+ // APP 层持有的逻辑总状态。
+ Lgc_Core_Struct_State appLgcState;
+};
+
+} // namespace APP
diff --git a/CODE_READING_ORDER.md b/CODE_READING_ORDER.md
new file mode 100644
index 0000000..b8e9358
--- /dev/null
+++ b/CODE_READING_ORDER.md
@@ -0,0 +1,139 @@
+# 代码阅读顺序
+
+这份顺序不是按“文件名排序”,而是按“学生最容易建立整体理解”的顺序排的。
+
+## 第一轮:先看最上层界面怎么把链路串起来
+
+1. [main.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/main.cpp)
+ 先看程序从哪里启动,主题在哪里设置,主窗口是哪个类。
+
+2. [APP/APP_UIWindow.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_UIWindow.h)
+ 先看主窗口成员和函数声明,建立“APP 层到底做什么”的总印象。
+
+3. [APP/APP_UIWindow.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_UIWindow.cpp)
+ 重点看这几个函数:
+ - `App_Func_InitWindow()`
+ - `App_Func_InitUi()`
+ - `App_Func_InitConnect()`
+ - `App_Func_InitLogic()`
+ - `App_Func_OnPollTimer()`
+ - `App_Func_RefreshDebugView()`
+
+这一轮的目标是先搞清楚:
+- 主窗口什么时候创建
+- 调试窗口什么时候插入
+- 定时轮询什么时候启动
+- UI 是怎么调用 LGC 的
+
+## 第二轮:看调试开关和调试界面
+
+4. [DEBUG/Debug_Config.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DEBUG/Debug_Config.h)
+ 看总开关宏 `APP_ENABLE_DEBUG_WINDOW`。
+
+5. [DEBUG/Debug_Panel.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DEBUG/Debug_Panel.h)
+6. [DEBUG/Debug_Panel.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DEBUG/Debug_Panel.cpp)
+ 看调试窗口只负责“显示”,不负责“解析”。
+
+这一轮的目标是搞清楚:
+- 为什么调试功能可以整体开关
+- 为什么 UI 层只显示文本,不做协议解析
+
+## 第三轮:看 APP 层自己的纯界面数据
+
+7. [APP/APP_KeypadModel.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_KeypadModel.h)
+8. [APP/APP_KeypadModel.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_KeypadModel.cpp)
+ 这是界面按键布局数据,不是 HID 协议。
+
+9. [APP/APP_Theme.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_Theme.h)
+10. [APP/APP_Theme.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/APP/APP_Theme.cpp)
+ 这是界面主题,不是协议,也不是设备层。
+
+这一轮的目标是明确:
+- APP 层有哪些东西只是“画界面”
+- 哪些东西绝对不能混进 DRI / LGC
+
+## 第四轮:看 MID 层,建立协议常量和公共结构认知
+
+11. [MID/Mid_Def.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/MID/Mid_Def.h)
+12. [MID/Mid_Def.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/MID/Mid_Def.cpp)
+
+这一轮一定要记住:
+- 当前有 3 个主链 report:`0x01 / 0x03 / 0x04`
+- `0x04` 是 `FF00 / 0002`
+- `0x04` 固定结构是 `[report_id | modifier | usage_bitmap(29)]`
+- MID 只放“大家共用的固定定义”,不放设备打开逻辑,也不放协议语义判断
+
+## 第五轮:看 LGC 总控,理解“谁调用谁”
+
+13. [LOGIC/Lgc_Core.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Core.h)
+14. [LOGIC/Lgc_Core.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Core.cpp)
+
+重点看:
+- `Lgc_Core_Func_Start()`
+- `Lgc_Core_Func_RefreshDevice()`
+- `Lgc_Core_Func_Poll()`
+
+这一轮要看懂:
+- LGC 怎么分别驱动 `Dri_NkroRaw / Dri_Consumer / Dri_Vendor`
+- 收到原始包以后,怎么分流给不同解析函数
+- 最后为什么 UI 只拿到整理好的文本
+
+## 第六轮:看三个 report 的协议解析
+
+15. [LOGIC/Lgc_Vendor.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Vendor.h)
+16. [LOGIC/Lgc_Vendor.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Vendor.cpp)
+ 这一组最重要,先精读。
+
+17. [LOGIC/Lgc_Nkro.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Nkro.h)
+18. [LOGIC/Lgc_Nkro.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Nkro.cpp)
+
+19. [LOGIC/Lgc_Consumer.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Consumer.h)
+20. [LOGIC/Lgc_Consumer.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/LOGIC/Lgc_Consumer.cpp)
+
+这一轮的目标:
+- 彻底看懂 `0x04` 的完整解析流程
+- 再回头看 `0x01` 和 `0x03`
+
+## 第七轮:最后看 DRI,理解“原始包从哪来”
+
+21. [DRI/Dri_Vendor.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_Vendor.h)
+22. [DRI/Dri_Vendor.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_Vendor.cpp)
+ 先看 vendor,因为当前调试最关键。
+
+23. [DRI/Dri_NkroRaw.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_NkroRaw.h)
+24. [DRI/Dri_NkroRaw.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_NkroRaw.cpp)
+
+25. [DRI/Dri_Consumer.h](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_Consumer.h)
+26. [DRI/Dri_Consumer.cpp](C:/Users/shuto/Desktop/C++/test_1/20260320_new_keyboard/DRI/Dri_Consumer.cpp)
+
+这一轮要明确:
+- DRI 只负责枚举、打开、读写
+- DRI 不判断业务语义
+- DRI 不解释 `report id 0x04` 代表什么
+
+## 一个最推荐的实际阅读路径
+
+如果你这次只想先吃透“调试抓包 + 0x04 解析”这一条线,最推荐按下面顺序读:
+
+1. `main.cpp`
+2. `APP/APP_UIWindow.h`
+3. `APP/APP_UIWindow.cpp`
+4. `DEBUG/Debug_Config.h`
+5. `DEBUG/Debug_Panel.h`
+6. `DEBUG/Debug_Panel.cpp`
+7. `LOGIC/Lgc_Core.h`
+8. `LOGIC/Lgc_Core.cpp`
+9. `LOGIC/Lgc_Vendor.h`
+10. `LOGIC/Lgc_Vendor.cpp`
+11. `MID/Mid_Def.h`
+12. `MID/Mid_Def.cpp`
+13. `DRI/Dri_Vendor.h`
+14. `DRI/Dri_Vendor.cpp`
+
+这个顺序最符合教学。
+原因很简单:
+- 先知道“程序怎么串起来”
+- 再知道“调试窗口怎么显示”
+- 再知道“逻辑层怎么分流”
+- 再知道“0x04 怎么解析”
+- 最后才看“Windows HID 怎么打开”
diff --git a/CppProperties.json b/CppProperties.json
new file mode 100644
index 0000000..f598ba5
--- /dev/null
+++ b/CppProperties.json
@@ -0,0 +1,21 @@
+{
+ "configurations": [
+ {
+ "inheritEnvironments": [
+ "msvc_x64"
+ ],
+ "name": "x64-Debug",
+ "includePath": [
+ "${env.INCLUDE}",
+ "${workspaceRoot}\\**"
+ ],
+ "defines": [
+ "WIN32",
+ "_DEBUG",
+ "UNICODE",
+ "_UNICODE"
+ ],
+ "intelliSenseMode": "windows-msvc-x64"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/DRI/Dri_Consumer.cpp b/DRI/Dri_Consumer.cpp
new file mode 100644
index 0000000..56bb229
--- /dev/null
+++ b/DRI/Dri_Consumer.cpp
@@ -0,0 +1,168 @@
+#include "DRI/Dri_Consumer.h"
+
+namespace
+{
+
+bool Dri_Consumer_Func_BeginRead(Dri_Consumer_Struct_Port* p_Port, QString* p_TextStatus)
+{
+ if (!p_Port->IsOpened || (p_Port->InputLength == 0))
+ {
+ return false;
+ }
+
+ if (p_Port->IsReadPending)
+ {
+ return true;
+ }
+
+ ResetEvent(p_Port->HandleEvent);
+ p_Port->ReadBuffer.fill(0, p_Port->InputLength);
+ p_Port->OverlappedRead = {};
+ p_Port->OverlappedRead.hEvent = p_Port->HandleEvent;
+
+ DWORD BytesRead = 0;
+ if (ReadFile(
+ p_Port->HandleRead,
+ p_Port->ReadBuffer.data(),
+ p_Port->InputLength,
+ &BytesRead,
+ &p_Port->OverlappedRead) ||
+ (GetLastError() == ERROR_IO_PENDING))
+ {
+ p_Port->IsReadPending = true;
+ return true;
+ }
+
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("Consumer 接口启动异步读取失败:%1").arg(GetLastError());
+ }
+ return false;
+}
+
+} // namespace
+
+void Dri_Consumer_Func_Close(Dri_Consumer_Struct_Port* p_Port)
+{
+ if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending)
+ {
+ CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead);
+ }
+ if (p_Port->HandleRead != INVALID_HANDLE_VALUE)
+ {
+ CloseHandle(p_Port->HandleRead);
+ }
+ if (p_Port->HandleEvent != nullptr)
+ {
+ CloseHandle(p_Port->HandleEvent);
+ }
+
+ *p_Port = Dri_Consumer_Struct_Port();
+}
+
+bool Dri_Consumer_Func_Open(
+ Dri_Consumer_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ QString* p_TextStatus)
+{
+ Dri_Consumer_Func_Close(p_Port);
+
+ QString DevicePath;
+ quint16 InputLength = 0;
+ if (!Mid_Func_FindHidInterface(
+ Mid_Func_GetConsumerMatch(DeviceConfig),
+ &DevicePath,
+ &InputLength,
+ nullptr))
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("未找到 Consumer 接口:000C / 0001。");
+ }
+ return false;
+ }
+
+ p_Port->HandleRead = CreateFileW(
+ reinterpret_cast(DevicePath.utf16()),
+ GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_FLAG_OVERLAPPED,
+ nullptr);
+ if (p_Port->HandleRead == INVALID_HANDLE_VALUE)
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("Consumer 接口打开失败:%1").arg(GetLastError());
+ }
+ return false;
+ }
+
+ p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (p_Port->HandleEvent == nullptr)
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("Consumer 接口创建事件失败:%1").arg(GetLastError());
+ }
+ Dri_Consumer_Func_Close(p_Port);
+ return false;
+ }
+
+ p_Port->InputLength = InputLength;
+ p_Port->ReadBuffer = QByteArray(InputLength, 0);
+ p_Port->OverlappedRead.hEvent = p_Port->HandleEvent;
+ p_Port->IsOpened = true;
+
+ Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus);
+ return true;
+}
+
+bool Dri_Consumer_Func_Read(
+ Dri_Consumer_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString* p_TextStatus)
+{
+ p_Packet->IsValid = false;
+ p_Packet->ByteArray.clear();
+ p_Packet->PortName = QStringLiteral("Consumer");
+
+ if (!p_Port->IsOpened)
+ {
+ return false;
+ }
+
+ if (!p_Port->IsReadPending)
+ {
+ Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus);
+ return false;
+ }
+
+ DWORD BytesRead = 0;
+ if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE))
+ {
+ const DWORD ErrorCode = GetLastError();
+ if (ErrorCode == ERROR_IO_INCOMPLETE)
+ {
+ return false;
+ }
+
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("Consumer 接口读包失败:%1").arg(ErrorCode);
+ }
+ Dri_Consumer_Func_Close(p_Port);
+ return false;
+ }
+
+ p_Port->IsReadPending = false;
+ if (BytesRead > 0)
+ {
+ p_Packet->IsValid = true;
+ p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead));
+ }
+
+ Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus);
+ return p_Packet->IsValid;
+}
diff --git a/DRI/Dri_Consumer.h b/DRI/Dri_Consumer.h
new file mode 100644
index 0000000..1c45cbc
--- /dev/null
+++ b/DRI/Dri_Consumer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+#include
+#include
+#include
+
+/*
+ * DRI Consumer 层:抽象 HID Consumer 端点,封装 Win32 句柄、缓存与状态。
+ *
+ */
+struct Dri_Consumer_Struct_Port
+{
+ /* 设备读写所需的句柄与异步上下文 */
+ HANDLE HandleRead = INVALID_HANDLE_VALUE;
+ HANDLE HandleEvent = nullptr;
+ OVERLAPPED OverlappedRead = {};
+ /* 快速判断是否已经建立连接以及当前是否正挂起读 */
+ bool IsOpened = false;
+ bool IsReadPending = false;
+ /* USB 输入报文长度 & 环形缓存 */
+ quint16 InputLength = 0;
+ QByteArray ReadBuffer;
+};
+
+/* 主动释放 Consumer 端口,确保句柄/Overlapped 都被 reset。 */
+void Dri_Consumer_Func_Close(Dri_Consumer_Struct_Port* p_Port);
+/* 打开设备:按照 VID/PID 查询接口并创建 Overlapped 句柄。 */
+bool Dri_Consumer_Func_Open(Dri_Consumer_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ QString* p_TextStatus);
+/* 读取最新的 Consumer 原始包,失败时返回状态描述。 */
+bool Dri_Consumer_Func_Read(Dri_Consumer_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString* p_TextStatus);
diff --git a/DRI/Dri_NkroRaw.cpp b/DRI/Dri_NkroRaw.cpp
new file mode 100644
index 0000000..f77363b
--- /dev/null
+++ b/DRI/Dri_NkroRaw.cpp
@@ -0,0 +1,282 @@
+#include "DRI/Dri_NkroRaw.h"
+
+#include
+#include
+
+namespace
+{
+
+/* ---------- 设备过滤与 Usage 映射 ---------- */
+
+QString Dri_NkroRaw_Func_GetDevicePath(HANDLE DeviceHandle)
+{
+ if (DeviceHandle == nullptr)
+ {
+ return QString();
+ }
+
+ UINT NeedChars = 0;
+ GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, nullptr, &NeedChars);
+ if (NeedChars == 0)
+ {
+ return QString();
+ }
+
+ QVector Buffer(static_cast(NeedChars) + 1, 0);
+ if (GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, Buffer.data(), &NeedChars) == static_cast(-1))
+ {
+ return QString();
+ }
+
+ return QString::fromWCharArray(Buffer.constData()).trimmed();
+}
+
+bool Dri_NkroRaw_Func_IsTargetDevice(const QString& DevicePath, const Mid_Struct_DeviceConfig& DeviceConfig)
+{
+ if (DevicePath.isEmpty())
+ {
+ return false;
+ }
+
+ const QString UpperPath = DevicePath.toUpper();
+ return UpperPath.contains(QStringLiteral("VID_%1").arg(DeviceConfig.VendorId, 4, 16, QLatin1Char('0')).toUpper()) &&
+ UpperPath.contains(QStringLiteral("PID_%1").arg(DeviceConfig.ProductId, 4, 16, QLatin1Char('0')).toUpper());
+}
+
+quint16 Dri_NkroRaw_Func_GetUsage(const RAWKEYBOARD& Keyboard)
+{
+ const bool IsE0 = (Keyboard.Flags & RI_KEY_E0) != 0;
+ const bool IsE1 = (Keyboard.Flags & RI_KEY_E1) != 0;
+ const USHORT ScanCode = Keyboard.MakeCode;
+
+ if (IsE1)
+ {
+ return 0;
+ }
+
+ if (IsE0)
+ {
+ switch (ScanCode)
+ {
+ case 0x35: return 0x0054;
+ case 0x1C: return 0x0058;
+ case 0x1D: return 0x00E4;
+ case 0x38: return 0x00E6;
+ case 0x5B: return 0x00E3;
+ case 0x5C: return 0x00E7;
+ default:
+ return 0;
+ }
+ }
+
+ switch (ScanCode)
+ {
+ case 0x45: return 0x0053;
+ case 0x37: return 0x0055;
+ case 0x4A: return 0x0056;
+ case 0x4E: return 0x0057;
+ case 0x47: return 0x005F;
+ case 0x48: return 0x0060;
+ case 0x49: return 0x0061;
+ case 0x4B: return 0x005C;
+ case 0x4C: return 0x005D;
+ case 0x4D: return 0x005E;
+ case 0x4F: return 0x0059;
+ case 0x50: return 0x005A;
+ case 0x51: return 0x005B;
+ case 0x52: return 0x0062;
+ case 0x53: return 0x0063;
+ case 0x1D: return 0x00E0;
+ case 0x2A: return 0x00E1;
+ case 0x36: return 0x00E5;
+ case 0x38: return 0x00E2;
+ default:
+ return 0;
+ }
+}
+
+} // namespace
+
+/* ---------- 生命周期 ---------- */
+
+void Dri_NkroRaw_Func_Close(Dri_NkroRaw_Struct_Port* p_Port)
+{
+ *p_Port = Dri_NkroRaw_Struct_Port();
+}
+
+bool Dri_NkroRaw_Func_Open(
+ Dri_NkroRaw_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ void* WindowHandle,
+ QString* p_TextStatus)
+{
+ Dri_NkroRaw_Func_Close(p_Port);
+
+ if (WindowHandle == nullptr)
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("NKRO 原生输入链路打开失败:窗口句柄为空。");
+ }
+ return false;
+ }
+
+ RAWINPUTDEVICE Device = {};
+ Device.usUsagePage = MID_CONST_USAGE_PAGE_NKRO;
+ Device.usUsage = MID_CONST_USAGE_NKRO;
+ Device.dwFlags = RIDEV_INPUTSINK;
+ Device.hwndTarget = reinterpret_cast(WindowHandle);
+
+ if (!RegisterRawInputDevices(&Device, 1, sizeof(Device)))
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("NKRO 原生输入链路注册失败:%1").arg(GetLastError());
+ }
+ return false;
+ }
+
+ p_Port->IsOpened = true;
+ p_Port->WindowHandle = WindowHandle;
+ p_Port->DeviceConfig = DeviceConfig;
+
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("已启用 NKRO 原生输入链路。");
+ }
+ return true;
+}
+
+/* ---------- RawInput 主流程 ---------- */
+
+bool Dri_NkroRaw_Func_HandleNativeMessage(
+ Dri_NkroRaw_Struct_Port* p_Port,
+ void* p_Message,
+ QString* p_TextStatus)
+{
+ if (!p_Port->IsOpened || (p_Message == nullptr))
+ {
+ return false;
+ }
+
+ MSG* p_Msg = reinterpret_cast(p_Message);
+ if (p_Msg->message != WM_INPUT)
+ {
+ return false;
+ }
+
+ UINT NeedSize = 0;
+ GetRawInputData(reinterpret_cast(p_Msg->lParam), RID_INPUT, nullptr, &NeedSize, sizeof(RAWINPUTHEADER));
+ if (NeedSize == 0)
+ {
+ return false;
+ }
+
+ QByteArray Buffer(static_cast(NeedSize), 0);
+ if (GetRawInputData(
+ reinterpret_cast(p_Msg->lParam),
+ RID_INPUT,
+ Buffer.data(),
+ &NeedSize,
+ sizeof(RAWINPUTHEADER)) == static_cast(-1))
+ {
+ return false;
+ }
+
+ RAWINPUT* p_Input = reinterpret_cast(Buffer.data());
+ if (p_Input->header.dwType != RIM_TYPEKEYBOARD)
+ {
+ return false;
+ }
+
+ const QString DevicePath = Dri_NkroRaw_Func_GetDevicePath(p_Input->header.hDevice);
+ if (!Dri_NkroRaw_Func_IsTargetDevice(DevicePath, p_Port->DeviceConfig))
+ {
+ return false;
+ }
+
+ const quint16 Usage = Dri_NkroRaw_Func_GetUsage(p_Input->data.keyboard);
+ if (Usage == 0)
+ {
+ return false;
+ }
+
+ const bool IsPressed = (p_Input->data.keyboard.Flags & RI_KEY_BREAK) == 0;
+ bool IsChanged = false;
+
+ if ((Usage >= 0x00E0) && (Usage <= 0x00E7))
+ {
+ const quint8 BitMask = static_cast(1U << (Usage - 0x00E0));
+ const quint8 OldModifier = p_Port->Modifier;
+ if (IsPressed)
+ {
+ p_Port->Modifier = static_cast(p_Port->Modifier | BitMask);
+ }
+ else
+ {
+ p_Port->Modifier = static_cast(p_Port->Modifier & static_cast(~BitMask));
+ }
+ IsChanged = (OldModifier != p_Port->Modifier);
+ }
+ else
+ {
+ const int ByteIndex = Usage / 8;
+ const quint8 BitMask = static_cast(1U << (Usage % 8));
+ quint8 Value = static_cast(p_Port->UsageBitmap.at(ByteIndex));
+ const bool OldPressed = (Value & BitMask) != 0;
+ if (OldPressed == IsPressed)
+ {
+ return false;
+ }
+ Value = IsPressed ? static_cast(Value | BitMask) : static_cast(Value & static_cast(~BitMask));
+ p_Port->UsageBitmap[ByteIndex] = static_cast(Value);
+ IsChanged = true;
+ }
+
+ if (!IsChanged)
+ {
+ return false;
+ }
+
+ if (p_Port->DevicePath != DevicePath)
+ {
+ p_Port->DevicePath = DevicePath;
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("原生输入已命中目标设备:%1").arg(DevicePath);
+ }
+ }
+
+ Mid_Struct_RawPacket Packet;
+ Packet.IsValid = true;
+ Packet.PortName = QStringLiteral("NKRO(原生输入)");
+ Packet.ByteArray = QByteArray(MID_CONST_PACKET_SIZE_NKRO, 0);
+ Packet.ByteArray[0] = static_cast(Mid_Enum_ReportId_Nkro);
+ Packet.ByteArray[1] = static_cast(p_Port->Modifier);
+
+ for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index)
+ {
+ Packet.ByteArray[2 + Index] = p_Port->UsageBitmap.at(Index);
+ }
+
+ p_Port->PacketQueue.append(Packet);
+ return true;
+}
+
+bool Dri_NkroRaw_Func_Read(
+ Dri_NkroRaw_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString*)
+{
+ p_Packet->IsValid = false;
+ p_Packet->ByteArray.clear();
+ p_Packet->PortName = QStringLiteral("NKRO(原生输入)");
+
+ if (!p_Port->IsOpened || p_Port->PacketQueue.isEmpty())
+ {
+ return false;
+ }
+
+ *p_Packet = p_Port->PacketQueue.takeFirst();
+ return p_Packet->IsValid;
+}
diff --git a/DRI/Dri_NkroRaw.h b/DRI/Dri_NkroRaw.h
new file mode 100644
index 0000000..092b1fe
--- /dev/null
+++ b/DRI/Dri_NkroRaw.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+#include
+#include
+#include
+
+/*
+ * DRI NKRO RAW 层:负责注册 Windows RAWINPUT,接收键盘 104+ 通道。
+ * 这里集中保存窗口句柄、设备配置、按键队列与 Modifier/Bitmap 缓冲。
+ */
+struct Dri_NkroRaw_Struct_Port
+{
+ /* 运行状态:是否已注册 RAWINPUT 以及宿主窗口 HWND */
+ bool IsOpened = false;
+ void* WindowHandle = nullptr;
+ Mid_Struct_DeviceConfig DeviceConfig;
+ /* 最新的 Modifier 字节和 NKRO Usage 位图 */
+ quint8 Modifier = 0;
+ QByteArray UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, 0);
+ /* RAWINPUT 读取结果放入 PacketQueue,供上层逻辑顺序消费 */
+ QList PacketQueue;
+ QString DevicePath;
+};
+
+/* 注销 RAWINPUT 并清空所有缓存。 */
+void Dri_NkroRaw_Func_Close(Dri_NkroRaw_Struct_Port* p_Port);
+/* 注册 RAWINPUT:绑定目标窗口并监听指定 VID/PID。 */
+bool Dri_NkroRaw_Func_Open(Dri_NkroRaw_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ void* WindowHandle,
+ QString* p_TextStatus);
+/* 把 Windows 消息转为 NKRO 数据:核心教学入口。 */
+bool Dri_NkroRaw_Func_HandleNativeMessage(Dri_NkroRaw_Struct_Port* p_Port,
+ void* p_Message,
+ QString* p_TextStatus);
+/* 上层每次取一个封装好的 RawPacket。 */
+bool Dri_NkroRaw_Func_Read(Dri_NkroRaw_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString* p_TextStatus);
diff --git a/DRI/Dri_Vendor.cpp b/DRI/Dri_Vendor.cpp
new file mode 100644
index 0000000..804140d
--- /dev/null
+++ b/DRI/Dri_Vendor.cpp
@@ -0,0 +1,338 @@
+#include "DRI/Dri_Vendor.h"
+
+#include
+
+#pragma comment(lib, "hid.lib")
+
+namespace
+{
+
+/* ---------- 句柄与状态小工具 ---------- */
+
+void Dri_Vendor_Func_SetStatus(QString* p_TextStatus, const QString& Text)
+{
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = Text;
+ }
+}
+
+HANDLE Dri_Vendor_Func_OpenHandle(const QString& DevicePath, DWORD DesiredAccess, DWORD Flags)
+{
+ return CreateFileW(
+ reinterpret_cast(DevicePath.utf16()),
+ DesiredAccess,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr,
+ OPEN_EXISTING,
+ Flags,
+ nullptr);
+}
+
+bool Dri_Vendor_Func_BeginRead(Dri_Vendor_Struct_Port* p_Port, QString* p_TextStatus)
+{
+ if (!p_Port->IsOpened || (p_Port->InputLength == 0))
+ {
+ return false;
+ }
+
+ if (p_Port->IsReadPending)
+ {
+ return true;
+ }
+
+ ResetEvent(p_Port->HandleEvent);
+ p_Port->ReadBuffer.fill(0, p_Port->InputLength);
+ p_Port->OverlappedRead = {};
+ p_Port->OverlappedRead.hEvent = p_Port->HandleEvent;
+
+ DWORD BytesRead = 0;
+ if (ReadFile(
+ p_Port->HandleRead,
+ p_Port->ReadBuffer.data(),
+ p_Port->InputLength,
+ &BytesRead,
+ &p_Port->OverlappedRead) ||
+ (GetLastError() == ERROR_IO_PENDING))
+ {
+ p_Port->IsReadPending = true;
+ return true;
+ }
+
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 接口启动异步读取失败:%1").arg(GetLastError()));
+ return false;
+}
+
+HANDLE Dri_Vendor_Func_OpenWriteHandle(const QString& DevicePath, QString* p_TextError)
+{
+ HANDLE HandleWrite = Dri_Vendor_Func_OpenHandle(DevicePath, GENERIC_WRITE, 0);
+ if (HandleWrite != INVALID_HANDLE_VALUE)
+ {
+ return HandleWrite;
+ }
+
+ const DWORD WriteError = GetLastError();
+ HandleWrite = Dri_Vendor_Func_OpenHandle(DevicePath, 0, 0);
+
+ if ((HandleWrite == INVALID_HANDLE_VALUE) && (p_TextError != nullptr))
+ {
+ *p_TextError = QStringLiteral("GENERIC_WRITE=%1,ZeroAccess=%2")
+ .arg(WriteError)
+ .arg(GetLastError());
+ }
+ return HandleWrite;
+}
+
+bool Dri_Vendor_Func_TryWritePacket(
+ HANDLE HandleWrite,
+ quint16 OutputLength,
+ const QByteArray& Packet,
+ QString* p_TextError)
+{
+ if ((HandleWrite == INVALID_HANDLE_VALUE) || Packet.isEmpty())
+ {
+ return false;
+ }
+
+ QByteArray Buffer = Packet;
+ if ((OutputLength > 0) && (Buffer.size() < OutputLength))
+ {
+ Buffer.append(OutputLength - Buffer.size(), 0);
+ }
+
+ if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size())))
+ {
+ return true;
+ }
+
+ const DWORD SetReportError = GetLastError();
+ DWORD BytesWritten = 0;
+ if (WriteFile(
+ HandleWrite,
+ Buffer.constData(),
+ static_cast(Buffer.size()),
+ &BytesWritten,
+ nullptr) &&
+ (BytesWritten == static_cast(Buffer.size())))
+ {
+ return true;
+ }
+
+ if (p_TextError != nullptr)
+ {
+ *p_TextError = QStringLiteral("SetOutputReport=%1,WriteFile=%2")
+ .arg(SetReportError)
+ .arg(GetLastError());
+ }
+ return false;
+}
+
+} // namespace
+
+/* ---------- 生命周期 ---------- */
+
+void Dri_Vendor_Func_Close(Dri_Vendor_Struct_Port* p_Port)
+{
+ if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending)
+ {
+ CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead);
+ }
+
+ for (HANDLE* p_Handle : { &p_Port->HandleRead, &p_Port->HandleWriteVendor, &p_Port->HandleWriteNkro, &p_Port->HandleEvent })
+ {
+ if ((*p_Handle != nullptr) && (*p_Handle != INVALID_HANDLE_VALUE))
+ {
+ CloseHandle(*p_Handle);
+ }
+ }
+
+ *p_Port = Dri_Vendor_Struct_Port();
+}
+
+bool Dri_Vendor_Func_Open(
+ Dri_Vendor_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ QString* p_TextStatus)
+{
+ Dri_Vendor_Func_Close(p_Port);
+
+ QString VendorPath;
+ quint16 InputLength = 0;
+ quint16 OutputLength = 0;
+ if (!Mid_Func_FindHidInterface(
+ Mid_Func_GetVendorMatch(DeviceConfig),
+ &VendorPath,
+ &InputLength,
+ &OutputLength))
+ {
+ Dri_Vendor_Func_SetStatus(p_TextStatus, QStringLiteral("未找到 Vendor 接口:FF00 / 0002。"));
+ return false;
+ }
+
+ p_Port->HandleRead = Dri_Vendor_Func_OpenHandle(VendorPath, GENERIC_READ, FILE_FLAG_OVERLAPPED);
+ if (p_Port->HandleRead == INVALID_HANDLE_VALUE)
+ {
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 接口打开读句柄失败:%1").arg(GetLastError()));
+ return false;
+ }
+
+ p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (p_Port->HandleEvent == nullptr)
+ {
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 接口创建事件失败:%1").arg(GetLastError()));
+ Dri_Vendor_Func_Close(p_Port);
+ return false;
+ }
+
+ p_Port->HandleWriteVendor = Dri_Vendor_Func_OpenWriteHandle(VendorPath, nullptr);
+
+ QString NkroPath;
+ quint16 NkroOutputLength = 0;
+ if (Mid_Func_FindHidInterface(
+ Mid_Func_GetNkroMatch(DeviceConfig),
+ &NkroPath,
+ nullptr,
+ &NkroOutputLength))
+ {
+ QString TextError;
+ p_Port->HandleWriteNkro = Dri_Vendor_Func_OpenWriteHandle(NkroPath, &TextError);
+ p_Port->NkroWriteOutputLength = NkroOutputLength;
+
+ if (p_Port->HandleWriteNkro == INVALID_HANDLE_VALUE)
+ {
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 读链路已打开,但 NKRO 写句柄打开失败:%1").arg(TextError));
+ }
+ }
+ else
+ {
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 读链路已打开,但没有找到 NKRO 写接口。"));
+ }
+
+ p_Port->InputLength = InputLength;
+ p_Port->OutputLength = OutputLength;
+ p_Port->ReadBuffer = QByteArray(InputLength, 0);
+ p_Port->OverlappedRead.hEvent = p_Port->HandleEvent;
+ p_Port->IsOpened = true;
+
+ if (!Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus))
+ {
+ Dri_Vendor_Func_Close(p_Port);
+ return false;
+ }
+ return true;
+}
+
+/* ---------- 读写流程 ---------- */
+
+bool Dri_Vendor_Func_Read(
+ Dri_Vendor_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString* p_TextStatus)
+{
+ *p_Packet = Mid_Struct_RawPacket();
+ p_Packet->PortName = QStringLiteral("Vendor");
+
+ if (!p_Port->IsOpened)
+ {
+ return false;
+ }
+
+ if (!p_Port->IsReadPending)
+ {
+ Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus);
+ return false;
+ }
+
+ DWORD BytesRead = 0;
+ if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE))
+ {
+ const DWORD ErrorCode = GetLastError();
+ if (ErrorCode == ERROR_IO_INCOMPLETE)
+ {
+ return false;
+ }
+
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 接口读包失败:%1").arg(ErrorCode));
+ Dri_Vendor_Func_Close(p_Port);
+ return false;
+ }
+
+ p_Port->IsReadPending = false;
+ if (BytesRead > 0)
+ {
+ p_Packet->IsValid = true;
+ p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead));
+ }
+
+ Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus);
+ return p_Packet->IsValid;
+}
+
+bool Dri_Vendor_Func_Write(
+ Dri_Vendor_Struct_Port* p_Port,
+ const QByteArray& ByteArray,
+ QString* p_TextStatus)
+{
+ if (!p_Port->IsOpened)
+ {
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ QStringLiteral("Vendor 读链路未打开,不能发送掩码。"));
+ return false;
+ }
+
+ QStringList Errors;
+ const auto TryWriteTo = [&](HANDLE HandleWrite,
+ quint16 OutputLength,
+ const QString& SuccessText,
+ const QString& ErrorPrefix)
+ {
+ QString TextError;
+ if (Dri_Vendor_Func_TryWritePacket(HandleWrite, OutputLength, ByteArray, &TextError))
+ {
+ Dri_Vendor_Func_SetStatus(p_TextStatus, SuccessText);
+ return true;
+ }
+ if (!TextError.isEmpty())
+ {
+ Errors.append(ErrorPrefix.arg(TextError));
+ }
+ return false;
+ };
+
+ if (TryWriteTo(
+ p_Port->HandleWriteNkro,
+ p_Port->NkroWriteOutputLength,
+ QStringLiteral("掩码已发送到 NKRO 写接口。"),
+ QStringLiteral("NKRO 写接口发送失败:%1")))
+ {
+ return true;
+ }
+ if (TryWriteTo(
+ p_Port->HandleWriteVendor,
+ p_Port->OutputLength,
+ QStringLiteral("掩码已发送到 Vendor 写接口。"),
+ QStringLiteral("Vendor 写接口发送失败:%1")))
+ {
+ return true;
+ }
+
+ Dri_Vendor_Func_SetStatus(
+ p_TextStatus,
+ Errors.isEmpty()
+ ? QStringLiteral("没有可用的写句柄,掩码发送未执行。")
+ : Errors.join(QStringLiteral("\n")));
+ return false;
+}
diff --git a/DRI/Dri_Vendor.h b/DRI/Dri_Vendor.h
new file mode 100644
index 0000000..97233eb
--- /dev/null
+++ b/DRI/Dri_Vendor.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+#include
+#include
+#include
+
+/*
+ * DRI Vendor 层:读写固件的 Vendor/NKRO HID 接口,支撑逻辑层功能诊断。
+ *
+ */
+struct Dri_Vendor_Struct_Port
+{
+ /* 读写句柄:Vendor 写、NKRO 写与通用读各占一个 */
+ HANDLE HandleRead = INVALID_HANDLE_VALUE;
+ HANDLE HandleWriteVendor = INVALID_HANDLE_VALUE;
+ HANDLE HandleWriteNkro = INVALID_HANDLE_VALUE;
+ HANDLE HandleEvent = nullptr;
+ OVERLAPPED OverlappedRead = {};
+ /* 运行状态 + 报文长度缓存,便于 UI/LOGIC 层快速判断 */
+ bool IsOpened = false;
+ bool IsReadPending = false;
+ quint16 InputLength = 0;
+ quint16 OutputLength = 0;
+ quint16 NkroWriteOutputLength = 0;
+ QByteArray ReadBuffer;
+};
+
+/* 关闭全部句柄,安全退出时务必调用以便复用设备。 */
+void Dri_Vendor_Func_Close(Dri_Vendor_Struct_Port* p_Port);
+/* 根据 VID/PID 打开对应 HID 接口,并创建必需句柄。 */
+bool Dri_Vendor_Func_Open(Dri_Vendor_Struct_Port* p_Port,
+ const Mid_Struct_DeviceConfig& DeviceConfig,
+ QString* p_TextStatus);
+/* 读取 Vendor 报文:一次返回一个 Mid_Struct_RawPacket。 */
+bool Dri_Vendor_Func_Read(Dri_Vendor_Struct_Port* p_Port,
+ Mid_Struct_RawPacket* p_Packet,
+ QString* p_TextStatus);
+/* 写入 Vendor/NKRO 报文,ByteArray 按固件定义组包。 */
+bool Dri_Vendor_Func_Write(Dri_Vendor_Struct_Port* p_Port,
+ const QByteArray& ByteArray,
+ QString* p_TextStatus);
diff --git a/LOGIC/Lgc_Consumer.cpp b/LOGIC/Lgc_Consumer.cpp
new file mode 100644
index 0000000..f2c91fa
--- /dev/null
+++ b/LOGIC/Lgc_Consumer.cpp
@@ -0,0 +1,41 @@
+#include "LOGIC/Lgc_Consumer.h"
+
+void Lgc_Consumer_Func_Parse(const QByteArray& ByteArray, Lgc_Consumer_Struct_Result* p_Result)
+{
+ // 当前调用链内部固定传有效结果对象,这里直接清成默认状态。
+ *p_Result = Lgc_Consumer_Struct_Result();
+
+ // DRI 这轮没有交上来数据时,直接给出提示。
+ if (ByteArray.isEmpty())
+ {
+ p_Result->TextExplain = QStringLiteral("Consumer 端口没有收到数据。");
+ return;
+ }
+
+ // 第 0 字节不是 0x03,就说明这不是 Consumer 包。
+ if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Consumer)
+ {
+ p_Result->TextExplain = QStringLiteral("这不是 report id 0x03。");
+ return;
+ }
+
+ p_Result->IsMatch = true;
+
+ // 当前固件里的 0x03 包固定就是 3 字节。
+ if (ByteArray.size() != MID_CONST_PACKET_SIZE_CONSUMER)
+ {
+ p_Result->TextExplain = QStringLiteral("0x03 包长度不对。");
+ return;
+ }
+
+ p_Result->IsLengthOk = true;
+
+ // 第 1、2 字节按 little-endian 规则还原成 16 位 usage。
+ p_Result->Usage =
+ static_cast(ByteArray.at(1)) |
+ (static_cast(static_cast(ByteArray.at(2))) << 8);
+
+ // 当前项目里只保留简短解释,避免调试日志过于冗长。
+ p_Result->TextExplain = QStringLiteral("0x03 Consumer:%1")
+ .arg(Mid_Func_GetConsumerUsageText(p_Result->Usage));
+}
diff --git a/LOGIC/Lgc_Consumer.h b/LOGIC/Lgc_Consumer.h
new file mode 100644
index 0000000..ee4c4f6
--- /dev/null
+++ b/LOGIC/Lgc_Consumer.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+
+/*
+ * 这份文件负责解析 report id 0x03 的 Consumer 包。
+ *
+ * 当前下位机真实结构很简单:
+ * 1. 第 0 字节是 report id,固定为 0x03
+ * 2. 第 1、2 字节拼成一个 16 位 consumer usage
+ * 3. 整包固定长度 3 字节
+ *
+ * 所以这里不需要像 0x01 / 0x04 那样去拆 modifier 和 usage 位图。
+ */
+
+struct Lgc_Consumer_Struct_Result
+{
+ // 这一包是否匹配 report id 0x03。
+ bool IsMatch = false;
+
+ // 如果匹配 0x03,长度是否也正确。
+ bool IsLengthOk = false;
+
+ // 解析得到的 consumer usage。
+ quint16 Usage = 0;
+
+ // 给调试窗口显示的简短中文说明。
+ QString TextExplain;
+};
+
+void Lgc_Consumer_Func_Parse(const QByteArray& ByteArray, Lgc_Consumer_Struct_Result* p_Result);
diff --git a/LOGIC/Lgc_Core.cpp b/LOGIC/Lgc_Core.cpp
new file mode 100644
index 0000000..f22bebf
--- /dev/null
+++ b/LOGIC/Lgc_Core.cpp
@@ -0,0 +1,606 @@
+#include "LOGIC/Lgc_Core.h"
+
+#include "MID/Mid_Def.h"
+#include
+#include
+#include
+
+namespace
+{
+
+/* ---------- 日志与状态文本 ---------- */
+
+QString Lgc_Core_Func_GetTimeText()
+{
+ return QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz"));
+}
+
+void Lgc_Core_Func_AppendLog(Lgc_Core_Struct_State* p_State, const QString& Text)
+{
+ if (Text.isEmpty())
+ {
+ return;
+ }
+
+ if (p_State->TextLog.isEmpty())
+ {
+ p_State->TextLog = Text;
+ }
+ else
+ {
+ p_State->TextLog.append(QStringLiteral("\n\n"));
+ p_State->TextLog.append(Text);
+ }
+
+ if (p_State->TextLog.size() > 24000)
+ {
+ p_State->TextLog = p_State->TextLog.right(20000);
+ }
+}
+
+void Lgc_Core_Func_AppendStatusLog(Lgc_Core_Struct_State* p_State, const QString& Text)
+{
+ if (!Text.isEmpty())
+ {
+ Lgc_Core_Func_AppendLog(
+ p_State,
+ QStringLiteral("[%1] 状态\n%2").arg(Lgc_Core_Func_GetTimeText(), Text));
+ }
+}
+
+void Lgc_Core_Func_AppendPacketLog(
+ Lgc_Core_Struct_State* p_State,
+ const QString& ActionText,
+ const QString& PortName,
+ const QByteArray& Packet,
+ const QString& ExplainText)
+{
+ QString Text = QStringLiteral("[%1] %2\n端口: %3\nHEX: %4")
+ .arg(Lgc_Core_Func_GetTimeText(), ActionText, PortName, Mid_Func_GetHexText(Packet));
+ if (!ExplainText.isEmpty())
+ {
+ Text.append(QLatin1Char('\n'));
+ Text.append(ExplainText);
+ }
+ Lgc_Core_Func_AppendLog(p_State, Text);
+}
+
+struct Lgc_Core_Struct_MaskStateBackup
+{
+ QByteArray FunctionMaskBitmap;
+ QByteArray SwapMaskBitmap;
+ QByteArray KeyboardMaskBitmap;
+ bool IsSwapModeOn = false;
+ quint16 SwapUsageLeft = 0;
+ quint16 SwapUsageRight = 0;
+ bool IsSwapLeftPhysicalPressed = false;
+ bool IsSwapRightPhysicalPressed = false;
+};
+
+/* ---------- 按键状态与掩码 ---------- */
+
+void Lgc_Core_Func_ClearAllKeyStates(Lgc_Core_Struct_State* p_State)
+{
+ p_State->IsVisibleKeyStateValid = false;
+ p_State->VisibleModifier = 0;
+ p_State->VisibleUsageList.clear();
+ p_State->IsPhysicalKeyStateValid = false;
+ p_State->PhysicalModifier = 0;
+ p_State->PhysicalUsageList.clear();
+ p_State->LastPhysicalUsageList.clear();
+ p_State->IsSwapLeftPhysicalPressed = false;
+ p_State->IsSwapRightPhysicalPressed = false;
+}
+
+void Lgc_Core_Func_CloseAllPorts(Lgc_Core_Struct_State* p_State)
+{
+ Dri_NkroRaw_Func_Close(&p_State->DriNkroPort);
+ Dri_Consumer_Func_Close(&p_State->DriConsumerPort);
+ Dri_Vendor_Func_Close(&p_State->DriVendorPort);
+}
+
+Lgc_Core_Struct_MaskStateBackup Lgc_Core_Func_BackupMaskState(const Lgc_Core_Struct_State* p_State)
+{
+ return {
+ p_State->FunctionMaskBitmap,
+ p_State->SwapMaskBitmap,
+ p_State->KeyboardMaskBitmap,
+ p_State->IsSwapModeOn,
+ p_State->SwapUsageLeft,
+ p_State->SwapUsageRight,
+ p_State->IsSwapLeftPhysicalPressed,
+ p_State->IsSwapRightPhysicalPressed
+ };
+}
+
+void Lgc_Core_Func_RestoreMaskState(
+ Lgc_Core_Struct_State* p_State,
+ const Lgc_Core_Struct_MaskStateBackup& Backup)
+{
+ p_State->FunctionMaskBitmap = Backup.FunctionMaskBitmap;
+ p_State->SwapMaskBitmap = Backup.SwapMaskBitmap;
+ p_State->KeyboardMaskBitmap = Backup.KeyboardMaskBitmap;
+ p_State->IsSwapModeOn = Backup.IsSwapModeOn;
+ p_State->SwapUsageLeft = Backup.SwapUsageLeft;
+ p_State->SwapUsageRight = Backup.SwapUsageRight;
+ p_State->IsSwapLeftPhysicalPressed = Backup.IsSwapLeftPhysicalPressed;
+ p_State->IsSwapRightPhysicalPressed = Backup.IsSwapRightPhysicalPressed;
+}
+
+bool Lgc_Core_Func_IsUsageEnabledInMask(const QByteArray& UsageBitmap, quint16 Usage)
+{
+ const int ByteIndex = Usage / 8;
+ if (ByteIndex >= UsageBitmap.size())
+ {
+ return false;
+ }
+
+ return (static_cast(UsageBitmap.at(ByteIndex)) &
+ static_cast(1U << (Usage % 8))) != 0;
+}
+
+void Lgc_Core_Func_SetUsageEnabledInMask(QByteArray* p_UsageBitmap, quint16 Usage, bool IsEnabled)
+{
+ const int ByteIndex = Usage / 8;
+ if (ByteIndex >= p_UsageBitmap->size())
+ {
+ return;
+ }
+
+ const quint8 BitMask = static_cast(1U << (Usage % 8));
+ quint8 Value = static_cast(p_UsageBitmap->at(ByteIndex));
+ Value = IsEnabled ? static_cast(Value | BitMask) : static_cast(Value & static_cast(~BitMask));
+ (*p_UsageBitmap)[ByteIndex] = static_cast(Value);
+}
+
+void Lgc_Core_Func_FillMaskAllEnabled(QByteArray* p_UsageBitmap)
+{
+ *p_UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF));
+}
+
+void Lgc_Core_Func_RebuildKeyboardMask(Lgc_Core_Struct_State* p_State)
+{
+ if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE)
+ {
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap);
+ }
+ if (p_State->SwapMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE)
+ {
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap);
+ }
+
+ p_State->KeyboardMaskBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF));
+ for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index)
+ {
+ p_State->KeyboardMaskBitmap[Index] = static_cast(
+ static_cast(p_State->FunctionMaskBitmap.at(Index)) &
+ static_cast(p_State->SwapMaskBitmap.at(Index)));
+ }
+}
+
+QByteArray Lgc_Core_Func_BuildVendorMaskPacket(const Lgc_Core_Struct_State* p_State)
+{
+ QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR, 0);
+ Packet[0] = static_cast(Mid_Enum_ReportId_Vendor);
+ Packet[1] = static_cast(0xFF);
+ for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index)
+ {
+ Packet[2 + Index] = p_State->KeyboardMaskBitmap.at(Index);
+ }
+ return Packet;
+}
+
+bool Lgc_Core_Func_WriteCurrentMask(
+ Lgc_Core_Struct_State* p_State,
+ QString* p_TextStatus,
+ QByteArray* p_Packet)
+{
+ Lgc_Core_Func_RebuildKeyboardMask(p_State);
+ if (p_Packet != nullptr)
+ {
+ *p_Packet = Lgc_Core_Func_BuildVendorMaskPacket(p_State);
+ }
+ if (!p_State->DriVendorPort.IsOpened)
+ {
+ if (p_TextStatus != nullptr)
+ {
+ *p_TextStatus = QStringLiteral("Vendor 接口未打开,无法同步当前掩码。");
+ }
+ return false;
+ }
+
+ const QByteArray Packet = (p_Packet != nullptr) ? *p_Packet : Lgc_Core_Func_BuildVendorMaskPacket(p_State);
+ return Dri_Vendor_Func_Write(&p_State->DriVendorPort, Packet, p_TextStatus);
+}
+
+bool Lgc_Core_Func_CommitMaskChange(
+ Lgc_Core_Struct_State* p_State,
+ const Lgc_Core_Struct_MaskStateBackup& Backup,
+ const QString& ExplainText)
+{
+ QString TextStatus;
+ QByteArray Packet;
+ if (!Lgc_Core_Func_WriteCurrentMask(p_State, &TextStatus, &Packet))
+ {
+ Lgc_Core_Func_RestoreMaskState(p_State, Backup);
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+ return false;
+ }
+
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+ Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("鍙戦€?"), QStringLiteral("Vendor"), Packet, ExplainText);
+ return true;
+}
+
+/* ---------- 数据包解析 ---------- */
+
+bool Lgc_Core_Func_SyncSystemState(Lgc_Core_Struct_State* p_State)
+{
+ const bool OldNumLock = p_State->IsSystemNumLockOn;
+ const bool OldConnected = p_State->IsConnected;
+ const QString OldConnection = p_State->TextConnection;
+
+ p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0;
+ p_State->IsConnected = p_State->DriVendorPort.IsOpened;
+ p_State->TextConnection = p_State->IsConnected
+ ? QStringLiteral("已连接到 0xFF00 / 0x0002 Vendor 接口。")
+ : QStringLiteral("未连接到目标 Vendor 接口。");
+
+ return (OldNumLock != p_State->IsSystemNumLockOn) ||
+ (OldConnected != p_State->IsConnected) ||
+ (OldConnection != p_State->TextConnection);
+}
+
+void Lgc_Core_Func_HandleNkroPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet)
+{
+ Lgc_Nkro_Struct_Result Result;
+ Lgc_Nkro_Func_Parse(Packet.ByteArray, &Result);
+
+ if (Result.IsMatch && Result.IsLengthOk)
+ {
+ p_State->IsVisibleKeyStateValid = true;
+ p_State->VisibleModifier = Result.Modifier;
+ p_State->VisibleUsageList = Result.UsageList;
+ }
+
+ Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain);
+}
+
+void Lgc_Core_Func_HandleConsumerPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet)
+{
+ Lgc_Consumer_Struct_Result Result;
+ Lgc_Consumer_Func_Parse(Packet.ByteArray, &Result);
+ Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain);
+}
+
+void Lgc_Core_Func_HandleVendorPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet)
+{
+ Lgc_Vendor_Struct_Result Result;
+ Lgc_Vendor_Func_Parse(Packet.ByteArray, &Result);
+
+ if (Result.IsMatch && Result.IsLengthOk && Result.VendorState.IsValid)
+ {
+ p_State->IsPhysicalKeyStateValid = true;
+ p_State->PhysicalModifier = Result.VendorState.Modifier;
+ p_State->PhysicalUsageList = Result.VendorState.UsageList;
+ }
+
+ Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain);
+}
+
+/* ---------- 功能键与交换逻辑 ---------- */
+
+bool Lgc_Core_Func_HandleFunctionButtons(Lgc_Core_Struct_State* p_State)
+{
+ if (!p_State->IsPhysicalKeyStateValid)
+ {
+ p_State->LastPhysicalUsageList.clear();
+ return false;
+ }
+
+ bool IsChanged = false;
+ for (quint16 Usage : p_State->PhysicalUsageList)
+ {
+ if (p_State->LastPhysicalUsageList.contains(Usage) || !Lgc_Core_Func_IsUsageFunctionMode(p_State, Usage))
+ {
+ continue;
+ }
+
+ QString TextStatus;
+ if (!Lgc_Func_Button_Func_HandlePressedUsage(p_State, Usage, &TextStatus) || TextStatus.isEmpty())
+ {
+ continue;
+ }
+
+ p_State->TextFunctionStatus = TextStatus;
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+ IsChanged = true;
+ }
+
+ p_State->LastPhysicalUsageList = p_State->PhysicalUsageList;
+ return IsChanged;
+}
+
+void Lgc_Core_Func_ReleaseSwapOutputs(
+ quint16 UsageLeft,
+ quint16 UsageRight,
+ bool IsSwapLeftPhysicalPressed,
+ bool IsSwapRightPhysicalPressed)
+{
+ if (IsSwapLeftPhysicalPressed)
+ {
+ Lgc_Func_Button_Func_SendUsageToWindows(UsageRight, false);
+ }
+ if (IsSwapRightPhysicalPressed)
+ {
+ Lgc_Func_Button_Func_SendUsageToWindows(UsageLeft, false);
+ }
+}
+
+void Lgc_Core_Func_ReleaseSwapOutputs(Lgc_Core_Struct_State* p_State)
+{
+ Lgc_Core_Func_ReleaseSwapOutputs(
+ p_State->SwapUsageLeft,
+ p_State->SwapUsageRight,
+ p_State->IsSwapLeftPhysicalPressed,
+ p_State->IsSwapRightPhysicalPressed);
+ p_State->IsSwapLeftPhysicalPressed = false;
+ p_State->IsSwapRightPhysicalPressed = false;
+}
+
+bool Lgc_Core_Func_HandleSwapSource(
+ Lgc_Core_Struct_State* p_State,
+ quint16 PhysicalUsage,
+ quint16 TargetUsage,
+ bool* p_IsPhysicalPressed)
+{
+ const bool IsPressedNow = p_State->PhysicalUsageList.contains(PhysicalUsage);
+ if (IsPressedNow == *p_IsPhysicalPressed)
+ {
+ return false;
+ }
+
+ if (!Lgc_Func_Button_Func_SendUsageToWindows(TargetUsage, IsPressedNow))
+ {
+ p_State->TextFunctionStatus = QStringLiteral("按键交换补发失败。");
+ Lgc_Core_Func_AppendStatusLog(p_State, p_State->TextFunctionStatus);
+ return true;
+ }
+
+ *p_IsPhysicalPressed = IsPressedNow;
+ return true;
+}
+
+bool Lgc_Core_Func_HandleSwapMode(Lgc_Core_Struct_State* p_State)
+{
+ const bool HadInjectedKey = p_State->IsSwapLeftPhysicalPressed || p_State->IsSwapRightPhysicalPressed;
+
+ if (!p_State->IsSwapModeOn || !p_State->IsPhysicalKeyStateValid)
+ {
+ Lgc_Core_Func_ReleaseSwapOutputs(p_State);
+ return HadInjectedKey;
+ }
+
+ bool IsChanged = false;
+ IsChanged |= Lgc_Core_Func_HandleSwapSource(
+ p_State,
+ p_State->SwapUsageLeft,
+ p_State->SwapUsageRight,
+ &p_State->IsSwapLeftPhysicalPressed);
+ IsChanged |= Lgc_Core_Func_HandleSwapSource(
+ p_State,
+ p_State->SwapUsageRight,
+ p_State->SwapUsageLeft,
+ &p_State->IsSwapRightPhysicalPressed);
+ return IsChanged;
+}
+
+} // namespace
+
+/* ---------- 对外接口 ---------- */
+
+void Lgc_Core_Func_Init(Lgc_Core_Struct_State* p_State)
+{
+ p_State->DriNkroPort = Dri_NkroRaw_Struct_Port();
+ p_State->DriConsumerPort = Dri_Consumer_Struct_Port();
+ p_State->DriVendorPort = Dri_Vendor_Struct_Port();
+ p_State->DeviceConfig = Mid_Struct_DeviceConfig();
+ p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。");
+ p_State->TextLog.clear();
+ p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。");
+
+ Lgc_Core_Func_ClearAllKeyStates(p_State);
+ p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0;
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap);
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap);
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->KeyboardMaskBitmap);
+
+ p_State->FunctionButtonConfig = Lgc_Func_Button_Struct_Config();
+ p_State->SwapUsageLeft = p_State->FunctionButtonConfig.SwapUsageLeft;
+ p_State->SwapUsageRight = p_State->FunctionButtonConfig.SwapUsageRight;
+ p_State->WindowHandle = nullptr;
+ p_State->IsConnected = false;
+ p_State->IsStarted = false;
+}
+
+void Lgc_Core_Func_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle)
+{
+ p_State->WindowHandle = WindowHandle;
+}
+
+void Lgc_Core_Func_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message)
+{
+ QString TextStatus;
+ Dri_NkroRaw_Func_HandleNativeMessage(&p_State->DriNkroPort, p_Message, &TextStatus);
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+}
+
+void Lgc_Core_Func_Start(Lgc_Core_Struct_State* p_State)
+{
+ if (p_State->IsStarted)
+ {
+ return;
+ }
+
+ p_State->IsStarted = true;
+ Lgc_Core_Func_RefreshDevice(p_State);
+}
+
+void Lgc_Core_Func_Close(Lgc_Core_Struct_State* p_State)
+{
+ Lgc_Core_Func_ReleaseSwapOutputs(p_State);
+ Lgc_Core_Func_CloseAllPorts(p_State);
+
+ p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。");
+ p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。");
+ p_State->IsConnected = false;
+ Lgc_Core_Func_ClearAllKeyStates(p_State);
+}
+
+void Lgc_Core_Func_RefreshDevice(Lgc_Core_Struct_State* p_State)
+{
+ Lgc_Core_Func_ReleaseSwapOutputs(p_State);
+ Lgc_Core_Func_CloseAllPorts(p_State);
+ Lgc_Core_Func_ClearAllKeyStates(p_State);
+
+ const auto OpenPort = [p_State](auto OpenFunc, auto* p_Port, auto... Args)
+ {
+ QString TextStatus;
+ OpenFunc(p_Port, Args..., &TextStatus);
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+ };
+
+ OpenPort(Dri_NkroRaw_Func_Open, &p_State->DriNkroPort, p_State->DeviceConfig, p_State->WindowHandle);
+ OpenPort(Dri_Consumer_Func_Open, &p_State->DriConsumerPort, p_State->DeviceConfig);
+ OpenPort(Dri_Vendor_Func_Open, &p_State->DriVendorPort, p_State->DeviceConfig);
+
+ QString TextStatus;
+
+ TextStatus.clear();
+ Lgc_Core_Func_WriteCurrentMask(p_State, &TextStatus, nullptr);
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+
+ Lgc_Core_Func_SyncSystemState(p_State);
+}
+
+void Lgc_Core_Func_ClearLog(Lgc_Core_Struct_State* p_State)
+{
+ p_State->TextLog.clear();
+}
+
+bool Lgc_Core_Func_Poll(Lgc_Core_Struct_State* p_State)
+{
+ bool IsChanged = false;
+ Mid_Struct_RawPacket Packet;
+ const auto PollPort = [p_State, &Packet, &IsChanged](auto ReadFunc, auto* p_Port, auto HandlePacket)
+ {
+ QString TextStatus;
+ if (ReadFunc(p_Port, &Packet, &TextStatus))
+ {
+ HandlePacket(p_State, Packet);
+ IsChanged = true;
+ }
+ if (!TextStatus.isEmpty())
+ {
+ Lgc_Core_Func_AppendStatusLog(p_State, TextStatus);
+ IsChanged = true;
+ }
+ };
+
+ PollPort(Dri_NkroRaw_Func_Read, &p_State->DriNkroPort, Lgc_Core_Func_HandleNkroPacket);
+ PollPort(Dri_Consumer_Func_Read, &p_State->DriConsumerPort, Lgc_Core_Func_HandleConsumerPacket);
+ PollPort(Dri_Vendor_Func_Read, &p_State->DriVendorPort, Lgc_Core_Func_HandleVendorPacket);
+
+ IsChanged |= Lgc_Core_Func_HandleFunctionButtons(p_State);
+ IsChanged |= Lgc_Core_Func_HandleSwapMode(p_State);
+ IsChanged |= Lgc_Core_Func_SyncSystemState(p_State);
+ return IsChanged;
+}
+
+bool Lgc_Core_Func_SetUsageFunctionMode(Lgc_Core_Struct_State* p_State, quint16 Usage, bool IsEnabled)
+{
+ if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE)
+ {
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap);
+ }
+ if (Lgc_Core_Func_IsUsageFunctionMode(p_State, Usage) == IsEnabled)
+ {
+ return true;
+ }
+
+ const Lgc_Core_Struct_MaskStateBackup Backup = Lgc_Core_Func_BackupMaskState(p_State);
+
+ Lgc_Core_Func_SetUsageEnabledInMask(&p_State->FunctionMaskBitmap, Usage, !IsEnabled);
+
+ const QString ExplainText = IsEnabled
+ ? QStringLiteral("已把 %1 切到功能键模式。").arg(Mid_Func_GetKeyboardUsageText(Usage))
+ : QStringLiteral("已恢复 %1 的普通键模式。").arg(Mid_Func_GetKeyboardUsageText(Usage));
+ return Lgc_Core_Func_CommitMaskChange(p_State, Backup, ExplainText);
+}
+
+bool Lgc_Core_Func_SetSwapMode(
+ Lgc_Core_Struct_State* p_State,
+ quint16 UsageLeft,
+ quint16 UsageRight,
+ bool IsEnabled)
+{
+ if ((UsageLeft == 0) || (UsageRight == 0) || (UsageLeft == UsageRight))
+ {
+ return false;
+ }
+ if ((p_State->IsSwapModeOn == IsEnabled) &&
+ (p_State->SwapUsageLeft == UsageLeft) &&
+ (p_State->SwapUsageRight == UsageRight))
+ {
+ return true;
+ }
+
+ const Lgc_Core_Struct_MaskStateBackup Backup = Lgc_Core_Func_BackupMaskState(p_State);
+
+ p_State->IsSwapModeOn = IsEnabled;
+ p_State->SwapUsageLeft = UsageLeft;
+ p_State->SwapUsageRight = UsageRight;
+ Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap);
+
+ if (IsEnabled)
+ {
+ Lgc_Core_Func_SetUsageEnabledInMask(&p_State->SwapMaskBitmap, UsageLeft, false);
+ Lgc_Core_Func_SetUsageEnabledInMask(&p_State->SwapMaskBitmap, UsageRight, false);
+ }
+
+ p_State->IsSwapLeftPhysicalPressed = false;
+ p_State->IsSwapRightPhysicalPressed = false;
+
+ const QString ExplainText = IsEnabled
+ ? QStringLiteral("已开启按键交换:%1 <-> %2")
+ .arg(Mid_Func_GetKeyboardUsageText(UsageLeft))
+ .arg(Mid_Func_GetKeyboardUsageText(UsageRight))
+ : QStringLiteral("已关闭按键交换:%1 <-> %2")
+ .arg(Mid_Func_GetKeyboardUsageText(UsageLeft))
+ .arg(Mid_Func_GetKeyboardUsageText(UsageRight));
+ if (!Lgc_Core_Func_CommitMaskChange(p_State, Backup, ExplainText))
+ {
+ return false;
+ }
+
+ if (Backup.IsSwapModeOn &&
+ (!IsEnabled || (Backup.SwapUsageLeft != UsageLeft) || (Backup.SwapUsageRight != UsageRight)))
+ {
+ Lgc_Core_Func_ReleaseSwapOutputs(
+ Backup.SwapUsageLeft,
+ Backup.SwapUsageRight,
+ Backup.IsSwapLeftPhysicalPressed,
+ Backup.IsSwapRightPhysicalPressed);
+ }
+ return true;
+}
+
+bool Lgc_Core_Func_IsUsageFunctionMode(const Lgc_Core_Struct_State* p_State, quint16 Usage)
+{
+ if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE)
+ {
+ return false;
+ }
+
+ return !Lgc_Core_Func_IsUsageEnabledInMask(p_State->FunctionMaskBitmap, Usage);
+}
diff --git a/LOGIC/Lgc_Core.h b/LOGIC/Lgc_Core.h
new file mode 100644
index 0000000..d4cf225
--- /dev/null
+++ b/LOGIC/Lgc_Core.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "DRI/Dri_Consumer.h"
+#include "DRI/Dri_NkroRaw.h"
+#include "DRI/Dri_Vendor.h"
+#include "LOGIC/Lgc_Consumer.h"
+#include "LOGIC/Lgc_Func_Button.h"
+#include "LOGIC/Lgc_Nkro.h"
+#include "LOGIC/Lgc_Vendor.h"
+#include
+
+/*
+ * Lgc_Core:贯穿 UI / LOGIC / DRI 的单一状态容器。
+ * 高密注释使得学生在不翻源码的情况下即可看懂所有成员职责。
+ */
+struct Lgc_Core_Struct_State
+{
+ /* DRI 端口:分别承接 RAW NKRO / Consumer / Vendor 通道 */
+ Dri_NkroRaw_Struct_Port DriNkroPort;
+ Dri_Consumer_Struct_Port DriConsumerPort;
+ Dri_Vendor_Struct_Port DriVendorPort;
+
+ /* 设备与 UI 文本状态 */
+ Mid_Struct_DeviceConfig DeviceConfig;
+ QString TextConnection;
+ QString TextLog;
+ QString TextFunctionStatus;
+
+ /* 可视化按键:UI 模型(来自解析 NKRO/Consumer 结果) */
+ bool IsVisibleKeyStateValid = false;
+ quint8 VisibleModifier = 0;
+ QVector VisibleUsageList;
+
+ /* 物理按键:直接来自硬件,供 Swap/功能逻辑判断 */
+ bool IsPhysicalKeyStateValid = false;
+ quint8 PhysicalModifier = 0;
+ QVector PhysicalUsageList;
+ QVector LastPhysicalUsageList;
+
+ /* 键盘模式控制:NumLock、功能掩码、Swap 掩码等 */
+ bool IsSystemNumLockOn = false;
+ QByteArray FunctionMaskBitmap;
+ QByteArray SwapMaskBitmap;
+ QByteArray KeyboardMaskBitmap;
+
+ /* 功能键配置及 Swap 对应的运行期状态 */
+ Lgc_Func_Button_Struct_Config FunctionButtonConfig;
+ bool IsSwapModeOn = false;
+ quint16 SwapUsageLeft = 0;
+ quint16 SwapUsageRight = 0;
+ bool IsSwapLeftPhysicalPressed = false;
+ bool IsSwapRightPhysicalPressed = false;
+
+ /* 互操作:窗口句柄与核心运行状态 */
+ void* WindowHandle = nullptr;
+ bool IsConnected = false;
+ bool IsStarted = false;
+};
+
+/* 初始化核心:清空状态并准备 DRI 端口。 */
+void Lgc_Core_Func_Init(Lgc_Core_Struct_State* p_State);
+/* 告诉 LGC 使用哪个 HWND 接收 RAWINPUT。 */
+void Lgc_Core_Func_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle);
+/* 每个 Windows 消息都交给核心处理。 */
+void Lgc_Core_Func_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message);
+/* 启动:打开设备、刷新按键状态并进入轮询。 */
+void Lgc_Core_Func_Start(Lgc_Core_Struct_State* p_State);
+/* 关闭:释放 DRI 端口与所有资源。 */
+void Lgc_Core_Func_Close(Lgc_Core_Struct_State* p_State);
+/* 当 VID/PID 变化或用户点击刷新时调用。 */
+void Lgc_Core_Func_RefreshDevice(Lgc_Core_Struct_State* p_State);
+/* 清空 LOG 文本,UI 立即同步。 */
+void Lgc_Core_Func_ClearLog(Lgc_Core_Struct_State* p_State);
+/* 轮询 DRI 队列,返回“是否发生变化”以供 UI 决定是否刷新。 */
+bool Lgc_Core_Func_Poll(Lgc_Core_Struct_State* p_State);
+/* 设置单个 Usage 是否是“功能模式” */
+bool Lgc_Core_Func_SetUsageFunctionMode(Lgc_Core_Struct_State* p_State, quint16 Usage, bool IsEnabled);
+/* 打开/关闭 Swap 模式,并指定左右 Usage */
+bool Lgc_Core_Func_SetSwapMode(
+ Lgc_Core_Struct_State* p_State,
+ quint16 UsageLeft,
+ quint16 UsageRight,
+ bool IsEnabled);
+/* 查询给定 Usage 当前是否处于功能模式 */
+bool Lgc_Core_Func_IsUsageFunctionMode(const Lgc_Core_Struct_State* p_State, quint16 Usage);
diff --git a/LOGIC/Lgc_Func_Button.cpp b/LOGIC/Lgc_Func_Button.cpp
new file mode 100644
index 0000000..1fe2280
--- /dev/null
+++ b/LOGIC/Lgc_Func_Button.cpp
@@ -0,0 +1,191 @@
+#include "LOGIC/Lgc_Func_Button.h"
+
+#include "LOGIC/Lgc_Core.h"
+#include
+#include
+#include
+#include
+
+namespace
+{
+
+QString Lgc_Func_Button_Func_GetUsageShortText(quint16 Usage)
+{
+ switch (Usage)
+ {
+ case 0x0056: return QStringLiteral("-");
+ case 0x0057: return QStringLiteral("+");
+ case 0x0059: return QStringLiteral("1");
+ case 0x005A: return QStringLiteral("2");
+ case 0x005B: return QStringLiteral("3");
+ case 0x005C: return QStringLiteral("4");
+ case 0x005D: return QStringLiteral("5");
+ case 0x005E: return QStringLiteral("6");
+ case 0x005F: return QStringLiteral("7");
+ case 0x0060: return QStringLiteral("8");
+ case 0x0061: return QStringLiteral("9");
+ case 0x0062: return QStringLiteral("0");
+ default:
+ return Mid_Func_GetKeyboardUsageText(Usage);
+ }
+}
+
+WORD Lgc_Func_Button_Func_GetWindowsVirtualKey(quint16 Usage)
+{
+ switch (Usage)
+ {
+ case 0x0056: return VK_SUBTRACT;
+ case 0x0057: return VK_ADD;
+ case 0x0059: return VK_NUMPAD1;
+ case 0x005A: return VK_NUMPAD2;
+ case 0x005B: return VK_NUMPAD3;
+ case 0x005C: return VK_NUMPAD4;
+ case 0x005D: return VK_NUMPAD5;
+ case 0x005E: return VK_NUMPAD6;
+ case 0x005F: return VK_NUMPAD7;
+ case 0x0060: return VK_NUMPAD8;
+ case 0x0061: return VK_NUMPAD9;
+ case 0x0062: return VK_NUMPAD0;
+ default:
+ return 0;
+ }
+}
+
+bool Lgc_Func_Button_Func_SendUnicodeText(const QString& Text)
+{
+ if (Text.isEmpty())
+ {
+ return false;
+ }
+
+ QVector InputList;
+ InputList.reserve(Text.size() * 2);
+ for (QChar Character : Text)
+ {
+ INPUT InputDown = {};
+ InputDown.type = INPUT_KEYBOARD;
+ InputDown.ki.wScan = Character.unicode();
+ InputDown.ki.dwFlags = KEYEVENTF_UNICODE;
+ InputList.append(InputDown);
+
+ INPUT InputUp = InputDown;
+ InputUp.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
+ InputList.append(InputUp);
+ }
+
+ return SendInput(static_cast(InputList.size()), InputList.data(), sizeof(INPUT)) ==
+ static_cast(InputList.size());
+}
+
+void Lgc_Func_Button_Func_RunMacroText(Lgc_Core_Struct_State* p_State, QString* p_TextStatus)
+{
+ const QString Text = p_State->FunctionButtonConfig.MacroText.trimmed();
+ if (Text.isEmpty())
+ {
+ *p_TextStatus = QStringLiteral("功能键 0 未配置输出文本。");
+ return;
+ }
+
+ *p_TextStatus = Lgc_Func_Button_Func_SendUnicodeText(Text)
+ ? QStringLiteral("功能键 0 已输出文本:%1").arg(Text)
+ : QStringLiteral("功能键 0 输出文本失败。");
+}
+
+void Lgc_Func_Button_Func_RunSwapKey(Lgc_Core_Struct_State* p_State, QString* p_TextStatus)
+{
+ const quint16 UsageLeft = p_State->FunctionButtonConfig.SwapUsageLeft;
+ const quint16 UsageRight = p_State->FunctionButtonConfig.SwapUsageRight;
+ if ((UsageLeft == 0) || (UsageRight == 0) || (UsageLeft == UsageRight))
+ {
+ *p_TextStatus = QStringLiteral("功能键 1 的交换配置无效。");
+ return;
+ }
+
+ if (!Lgc_Core_Func_SetSwapMode(p_State, UsageLeft, UsageRight, !p_State->IsSwapModeOn))
+ {
+ *p_TextStatus = QStringLiteral("功能键 1 切换按键交换失败。");
+ return;
+ }
+
+ *p_TextStatus = p_State->IsSwapModeOn
+ ? QStringLiteral("功能键 1 已开启按键交换:%1 <-> %2")
+ .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageLeft))
+ .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageRight))
+ : QStringLiteral("功能键 1 已关闭按键交换:%1 <-> %2")
+ .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageLeft))
+ .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageRight));
+}
+
+void Lgc_Func_Button_Func_RunOpenWebsite(Lgc_Core_Struct_State* p_State, QString* p_TextStatus)
+{
+ const QString UrlText = p_State->FunctionButtonConfig.WebsiteUrl.trimmed();
+ const QUrl Url = QUrl::fromUserInput(UrlText);
+ if (UrlText.isEmpty() || !Url.isValid() || Url.isEmpty())
+ {
+ *p_TextStatus = QStringLiteral("功能键 2 的网址配置无效。");
+ return;
+ }
+
+ *p_TextStatus = QDesktopServices::openUrl(Url)
+ ? QStringLiteral("功能键 2 已打开网址:%1").arg(Url.toString())
+ : QStringLiteral("功能键 2 打开网址失败。");
+}
+
+} // namespace
+
+bool Lgc_Func_Button_Func_SendUsageToWindows(quint16 Usage, bool IsPressed)
+{
+ const WORD VirtualKey = Lgc_Func_Button_Func_GetWindowsVirtualKey(Usage);
+ if (VirtualKey == 0)
+ {
+ return false;
+ }
+
+ INPUT InputData = {};
+ InputData.type = INPUT_KEYBOARD;
+ InputData.ki.wVk = VirtualKey;
+ if (!IsPressed)
+ {
+ InputData.ki.dwFlags = KEYEVENTF_KEYUP;
+ }
+
+ return SendInput(1, &InputData, sizeof(INPUT)) == 1;
+}
+
+bool Lgc_Func_Button_Func_HandlePressedUsage(
+ Lgc_Core_Struct_State* p_State,
+ quint16 Usage,
+ QString* p_TextStatus)
+{
+ p_TextStatus->clear();
+ switch (Usage)
+ {
+ case 0x0062:
+ Lgc_Func_Button_Func_RunMacroText(p_State, p_TextStatus);
+ return true;
+
+ case 0x0059:
+ Lgc_Func_Button_Func_RunSwapKey(p_State, p_TextStatus);
+ return true;
+
+ case 0x005A:
+ Lgc_Func_Button_Func_RunOpenWebsite(p_State, p_TextStatus);
+ return true;
+
+ case 0x0056:
+ case 0x0057:
+ case 0x005B:
+ case 0x005C:
+ case 0x005D:
+ case 0x005E:
+ case 0x005F:
+ case 0x0060:
+ case 0x0061:
+ *p_TextStatus = QStringLiteral("功能键 %1 还没有绑定具体功能。")
+ .arg(Lgc_Func_Button_Func_GetUsageShortText(Usage));
+ return true;
+
+ default:
+ return false;
+ }
+}
diff --git a/LOGIC/Lgc_Func_Button.h b/LOGIC/Lgc_Func_Button.h
new file mode 100644
index 0000000..7e2bb44
--- /dev/null
+++ b/LOGIC/Lgc_Func_Button.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+
+struct Lgc_Core_Struct_State;
+
+struct Lgc_Func_Button_Struct_Config
+{
+ QString MacroText = QStringLiteral("HELLO WORLD!");
+ quint16 SwapUsageLeft = 0x005C;
+ quint16 SwapUsageRight = 0x005D;
+ QString WebsiteUrl = QStringLiteral("https://www.deepseek.com/");
+};
+
+bool Lgc_Func_Button_Func_SendUsageToWindows(quint16 Usage, bool IsPressed);
+bool Lgc_Func_Button_Func_HandlePressedUsage(
+ Lgc_Core_Struct_State* p_State,
+ quint16 Usage,
+ QString* p_TextStatus);
diff --git a/LOGIC/Lgc_Nkro.cpp b/LOGIC/Lgc_Nkro.cpp
new file mode 100644
index 0000000..0c128db
--- /dev/null
+++ b/LOGIC/Lgc_Nkro.cpp
@@ -0,0 +1,70 @@
+#include "LOGIC/Lgc_Nkro.h"
+
+void Lgc_Nkro_Func_Parse(const QByteArray& ByteArray, Lgc_Nkro_Struct_Result* p_Result)
+{
+ // 当前调用链内部固定传有效结果对象,这里直接回到默认状态。
+ *p_Result = Lgc_Nkro_Struct_Result();
+
+ // 没有新包时直接返回提示。
+ if (ByteArray.isEmpty())
+ {
+ p_Result->TextExplain = QStringLiteral("NKRO 端口没有收到数据。");
+ return;
+ }
+
+ // 先校验 report id。
+ if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Nkro)
+ {
+ p_Result->TextExplain = QStringLiteral("这不是 report id 0x01。");
+ return;
+ }
+
+ p_Result->IsMatch = true;
+
+ // 当前固件里的 0x01 包固定长度是 31 字节。
+ if (ByteArray.size() != MID_CONST_PACKET_SIZE_NKRO)
+ {
+ p_Result->TextExplain = QStringLiteral("0x01 包长度不对。");
+ return;
+ }
+
+ p_Result->IsLengthOk = true;
+
+ // 第 1 字节是 modifier。
+ p_Result->Modifier = static_cast(ByteArray.at(1));
+
+ // 后面 29 字节是 usage 位图。
+ p_Result->UsageBitmap = ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE);
+
+ // 逐 bit 扫描位图,得到所有按下的 HID usage。
+ for (quint16 Usage = 0; Usage <= MID_CONST_KEYBOARD_USAGE_MAX; ++Usage)
+ {
+ const int ByteIndex = Usage / 8;
+ const int BitIndex = Usage % 8;
+ const quint8 Value = static_cast(p_Result->UsageBitmap.at(ByteIndex));
+
+ if ((Value & static_cast(1U << BitIndex)) == 0)
+ {
+ continue;
+ }
+
+ p_Result->UsageList.append(Usage);
+ p_Result->UsageTextList.append(Mid_Func_GetKeyboardUsageText(Usage));
+ }
+
+ // 这里整理的是适合调试窗口直接阅读的教学化文本。
+ QStringList ExplainList;
+ ExplainList.append(QStringLiteral("0x01 NKRO 可见键盘态"));
+ ExplainList.append(QStringLiteral("修饰键:%1").arg(Mid_Func_GetModifierText(p_Result->Modifier)));
+
+ if (p_Result->UsageTextList.isEmpty())
+ {
+ ExplainList.append(QStringLiteral("按下:无"));
+ }
+ else
+ {
+ ExplainList.append(QStringLiteral("按下:%1").arg(p_Result->UsageTextList.join(QStringLiteral(", "))));
+ }
+
+ p_Result->TextExplain = ExplainList.join(QLatin1Char('\n'));
+}
diff --git a/LOGIC/Lgc_Nkro.h b/LOGIC/Lgc_Nkro.h
new file mode 100644
index 0000000..383ea56
--- /dev/null
+++ b/LOGIC/Lgc_Nkro.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+
+/*
+ * 这份文件负责解析 report id 0x01 的 NKRO 包。
+ *
+ * 这里解析的是“Windows 最终可见的键盘态”:
+ * - 会受下位机遮罩影响
+ * - 不是 0x04 那种物理镜像流
+ *
+ * 当前下位机结构固定为:
+ * [report_id(1) | modifier(1) | usage_bitmap(29)]
+ * 总长度 31 字节
+ */
+struct Lgc_Nkro_Struct_Result
+{
+ // 首字节是否匹配 0x01。
+ bool IsMatch = false;
+
+ // 长度是否符合 31 字节。
+ bool IsLengthOk = false;
+
+ // 第 1 字节修饰键位图。
+ quint8 Modifier = 0;
+
+ // 第 2~30 字节 usage 位图原文。
+ QByteArray UsageBitmap;
+
+ // 展开后的 HID usage 列表。
+ QVector UsageList;
+
+ // 展开后的按键名称列表。
+ QStringList UsageTextList;
+
+ // 给调试窗口用的简短中文说明。
+ QString TextExplain;
+};
+
+void Lgc_Nkro_Func_Parse(const QByteArray& ByteArray, Lgc_Nkro_Struct_Result* p_Result);
diff --git a/LOGIC/Lgc_Vendor.cpp b/LOGIC/Lgc_Vendor.cpp
new file mode 100644
index 0000000..ae47cf9
--- /dev/null
+++ b/LOGIC/Lgc_Vendor.cpp
@@ -0,0 +1,71 @@
+#include "LOGIC/Lgc_Vendor.h"
+
+void Lgc_Vendor_Func_Parse(const QByteArray& ByteArray, Lgc_Vendor_Struct_Result* p_Result)
+{
+ // 当前调用链内部固定传有效结果对象,这里直接回到默认状态。
+ *p_Result = Lgc_Vendor_Struct_Result();
+
+ // 没有包时直接返回提示。
+ if (ByteArray.isEmpty())
+ {
+ p_Result->TextExplain = QStringLiteral("Vendor 端口没有收到数据。");
+ return;
+ }
+
+ // 第 0 字节不是 0x04,就不是当前要解析的 Vendor 镜像包。
+ if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Vendor)
+ {
+ p_Result->TextExplain = QStringLiteral("这不是 report id 0x04。");
+ return;
+ }
+
+ p_Result->IsMatch = true;
+
+ // 当前固件里的 0x04 固定长度也是 31 字节。
+ if (ByteArray.size() != MID_CONST_PACKET_SIZE_VENDOR)
+ {
+ p_Result->TextExplain = QStringLiteral("0x04 包长度不对。");
+ return;
+ }
+
+ p_Result->IsLengthOk = true;
+ p_Result->VendorState.IsValid = true;
+
+ // 第 1 字节是物理 modifier。
+ p_Result->VendorState.Modifier = static_cast(ByteArray.at(1));
+
+ // 第 2~30 字节是物理 usage 位图。
+ p_Result->VendorState.UsageBitmap = ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE);
+
+ // 展开位图,得到当前物理按下的 usage 列表。
+ for (quint16 Usage = 0; Usage <= MID_CONST_KEYBOARD_USAGE_MAX; ++Usage)
+ {
+ const int ByteIndex = Usage / 8;
+ const int BitIndex = Usage % 8;
+ const quint8 Value = static_cast(p_Result->VendorState.UsageBitmap.at(ByteIndex));
+
+ if ((Value & static_cast(1U << BitIndex)) == 0)
+ {
+ continue;
+ }
+
+ p_Result->VendorState.UsageList.append(Usage);
+ p_Result->VendorState.UsageTextList.append(Mid_Func_GetKeyboardUsageText(Usage));
+ }
+
+ // 这里整理的是面向教学和调试窗口的中文解释。
+ QStringList ExplainList;
+ ExplainList.append(QStringLiteral("0x04 Vendor 物理镜像态"));
+ ExplainList.append(QStringLiteral("物理修饰键:%1").arg(Mid_Func_GetModifierText(p_Result->VendorState.Modifier)));
+
+ if (p_Result->VendorState.UsageTextList.isEmpty())
+ {
+ ExplainList.append(QStringLiteral("物理按下:无"));
+ }
+ else
+ {
+ ExplainList.append(QStringLiteral("物理按下:%1").arg(p_Result->VendorState.UsageTextList.join(QStringLiteral(", "))));
+ }
+
+ p_Result->TextExplain = ExplainList.join(QLatin1Char('\n'));
+}
diff --git a/LOGIC/Lgc_Vendor.h b/LOGIC/Lgc_Vendor.h
new file mode 100644
index 0000000..30914ff
--- /dev/null
+++ b/LOGIC/Lgc_Vendor.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "MID/Mid_Def.h"
+
+/*
+ * 这份文件负责解析 report id 0x04 的 Vendor 包。
+ *
+ * 当前 0x04 不是旧 VIA 命令协议,而是“键盘物理状态镜像流”。
+ * 它的结构固定为:
+ * [report_id(1) | modifier(1) | usage_bitmap(29)]
+ *
+ * 也就是说:
+ * - 0x01 代表主机最终可见键盘态
+ * - 0x04 代表下位机物理键盘态
+ */
+struct Lgc_Vendor_Struct_Result
+{
+ // 拆出来的 Vendor 状态。
+ Mid_Struct_VendorState VendorState;
+
+ // 是否匹配到 report id 0x04。
+ bool IsMatch = false;
+
+ // 长度是否符合 31 字节。
+ bool IsLengthOk = false;
+
+ // 给调试窗口看的中文解释。
+ QString TextExplain;
+};
+
+void Lgc_Vendor_Func_Parse(const QByteArray& ByteArray, Lgc_Vendor_Struct_Result* p_Result);
diff --git a/MID/Mid_Def.cpp b/MID/Mid_Def.cpp
new file mode 100644
index 0000000..309f36a
--- /dev/null
+++ b/MID/Mid_Def.cpp
@@ -0,0 +1,241 @@
+#include "MID/Mid_Def.h"
+
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "hid.lib")
+#pragma comment(lib, "setupapi.lib")
+
+/*
+ * MID 层:介于逻辑与 DRI 之间,负责描述 HID 设备匹配与数据格式。
+ * 高密注释用于教学,逐条解释如何把固件接口映射为 Win32 API 调用。
+ */
+
+/* 构造 NKRO 接口的匹配条件(Usage Page / Usage 固定) */
+Mid_Struct_DeviceMatch Mid_Func_GetNkroMatch(const Mid_Struct_DeviceConfig& DeviceConfig)
+{
+ Mid_Struct_DeviceMatch Match;
+ Match.VendorId = DeviceConfig.VendorId;
+ Match.ProductId = DeviceConfig.ProductId;
+ Match.UsagePage = MID_CONST_USAGE_PAGE_NKRO;
+ Match.Usage = MID_CONST_USAGE_NKRO;
+ Match.Name = QStringLiteral("NKRO Keyboard Interface");
+ return Match;
+}
+
+/* 构造 Consumer 接口的匹配条件 */
+Mid_Struct_DeviceMatch Mid_Func_GetConsumerMatch(const Mid_Struct_DeviceConfig& DeviceConfig)
+{
+ Mid_Struct_DeviceMatch Match;
+ Match.VendorId = DeviceConfig.VendorId;
+ Match.ProductId = DeviceConfig.ProductId;
+ Match.UsagePage = MID_CONST_USAGE_PAGE_CONSUMER;
+ Match.Usage = MID_CONST_USAGE_CONSUMER;
+ Match.Name = QStringLiteral("Consumer Interface");
+ return Match;
+}
+
+/* 构造 Vendor(状态镜像)接口的匹配条件 */
+Mid_Struct_DeviceMatch Mid_Func_GetVendorMatch(const Mid_Struct_DeviceConfig& DeviceConfig)
+{
+ Mid_Struct_DeviceMatch Match;
+ Match.VendorId = DeviceConfig.VendorId;
+ Match.ProductId = DeviceConfig.ProductId;
+ Match.UsagePage = MID_CONST_USAGE_PAGE_VENDOR;
+ Match.Usage = MID_CONST_USAGE_VENDOR;
+ Match.Name = QStringLiteral("Vendor State Mirror Interface");
+ return Match;
+}
+
+/*
+ * Mid_Func_FindHidInterface:按照匹配条件扫描系统中的 HID 接口。
+ * 步骤:1) 获取 HID GUID 2) SetupAPI 列举接口 3) CreateFile 查询属性 4) 命中后返回路径和报文长度。
+ */
+bool Mid_Func_FindHidInterface(
+ const Mid_Struct_DeviceMatch& Match,
+ QString* p_DevicePath,
+ quint16* p_InputLength,
+ quint16* p_OutputLength)
+{
+ /* Win32 提供的 HID GUID:列举所有 HID 接口都靠它 */
+ GUID HidGuid;
+ HidD_GetHidGuid(&HidGuid);
+
+ /* 构建“当前存在 + 暴露接口”的设备集合 */
+ HDEVINFO DeviceInfoSet = SetupDiGetClassDevsW(&HidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
+ if (DeviceInfoSet == INVALID_HANDLE_VALUE)
+ {
+ return false;
+ }
+
+ bool IsFound = false;
+ SP_DEVICE_INTERFACE_DATA InterfaceData = {};
+ InterfaceData.cbSize = sizeof(InterfaceData);
+
+ for (DWORD Index = 0;
+ SetupDiEnumDeviceInterfaces(DeviceInfoSet, nullptr, &HidGuid, Index, &InterfaceData);
+ ++Index)
+ {
+ /* Query 设备路径前先得到所需缓冲区长度 */
+ DWORD NeedLength = 0;
+ SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &InterfaceData, nullptr, 0, &NeedLength, nullptr);
+ if (NeedLength == 0)
+ {
+ continue;
+ }
+
+ /* 申请一段缓冲区存储 SP_DEVICE_INTERFACE_DETAIL_DATA_W */
+ QByteArray Buffer(static_cast(NeedLength), 0);
+ auto* p_Detail = reinterpret_cast(Buffer.data());
+ p_Detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
+
+ if (!SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &InterfaceData, p_Detail, NeedLength, nullptr, nullptr))
+ {
+ continue;
+ }
+
+ /* 只需 0 访问权限即可读取属性 */
+ HANDLE HandleQuery = CreateFileW(
+ p_Detail->DevicePath,
+ 0,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr,
+ OPEN_EXISTING,
+ 0,
+ nullptr);
+ if (HandleQuery == INVALID_HANDLE_VALUE)
+ {
+ continue;
+ }
+
+ HIDD_ATTRIBUTES Attributes = {};
+ Attributes.Size = sizeof(Attributes);
+ PHIDP_PREPARSED_DATA p_Preparsed = nullptr;
+ HIDP_CAPS Caps = {};
+ /* 通过 Attributes + Caps 对比 VID / PID / Usage Page / Usage */
+ const bool IsMatch =
+ HidD_GetAttributes(HandleQuery, &Attributes) &&
+ HidD_GetPreparsedData(HandleQuery, &p_Preparsed) &&
+ (HidP_GetCaps(p_Preparsed, &Caps) == HIDP_STATUS_SUCCESS) &&
+ (Attributes.VendorID == Match.VendorId) &&
+ (Attributes.ProductID == Match.ProductId) &&
+ (Caps.UsagePage == Match.UsagePage) &&
+ (Caps.Usage == Match.Usage);
+
+ if (p_Preparsed != nullptr)
+ {
+ HidD_FreePreparsedData(p_Preparsed);
+ }
+ CloseHandle(HandleQuery);
+
+ if (!IsMatch)
+ {
+ continue;
+ }
+
+ /* 命中:写回设备路径及报文长度 */
+ if (p_DevicePath != nullptr)
+ {
+ *p_DevicePath = QString::fromWCharArray(p_Detail->DevicePath);
+ }
+ if (p_InputLength != nullptr)
+ {
+ *p_InputLength = Caps.InputReportByteLength;
+ }
+ if (p_OutputLength != nullptr)
+ {
+ *p_OutputLength = Caps.OutputReportByteLength;
+ }
+
+ IsFound = true;
+ break;
+ }
+
+ /* 枚举结束释放句柄 */
+ SetupDiDestroyDeviceInfoList(DeviceInfoSet);
+ return IsFound;
+}
+
+/* 调试输出:把字节数组格式化成“AA BB CC” */
+QString Mid_Func_GetHexText(const QByteArray& ByteArray)
+{
+ QStringList TextList;
+ for (int Index = 0; Index < ByteArray.size(); ++Index)
+ {
+ TextList.append(QStringLiteral("%1")
+ .arg(static_cast(ByteArray.at(Index)), 2, 16, QLatin1Char('0'))
+ .toUpper());
+ }
+ return TextList.join(QLatin1Char(' '));
+}
+
+/* 把 Modifier 位图翻译成人类可读的组合 */
+QString Mid_Func_GetModifierText(quint8 Modifier)
+{
+ QStringList TextList;
+ if (Modifier == 0)
+ {
+ return QStringLiteral("无");
+ }
+
+ if ((Modifier & 0x01) != 0) TextList.append(QStringLiteral("Left Ctrl"));
+ if ((Modifier & 0x02) != 0) TextList.append(QStringLiteral("Left Shift"));
+ if ((Modifier & 0x04) != 0) TextList.append(QStringLiteral("Left Alt"));
+ if ((Modifier & 0x08) != 0) TextList.append(QStringLiteral("Left GUI"));
+ if ((Modifier & 0x10) != 0) TextList.append(QStringLiteral("Right Ctrl"));
+ if ((Modifier & 0x20) != 0) TextList.append(QStringLiteral("Right Shift"));
+ if ((Modifier & 0x40) != 0) TextList.append(QStringLiteral("Right Alt"));
+ if ((Modifier & 0x80) != 0) TextList.append(QStringLiteral("Right GUI"));
+ return TextList.join(QStringLiteral(", "));
+}
+
+/* 键盘 Usage -> 中文/英文标签,方便 UI 展示 */
+QString Mid_Func_GetKeyboardUsageText(quint16 Usage)
+{
+ switch (Usage)
+ {
+ case 0x0053: return QStringLiteral("Num Lock");
+ case 0x0054: return QStringLiteral("小键盘 /");
+ case 0x0055: return QStringLiteral("小键盘 *");
+ case 0x0056: return QStringLiteral("小键盘 -");
+ case 0x0057: return QStringLiteral("小键盘 +");
+ case 0x0058: return QStringLiteral("小键盘 Enter");
+ case 0x0059: return QStringLiteral("小键盘 1");
+ case 0x005A: return QStringLiteral("小键盘 2");
+ case 0x005B: return QStringLiteral("小键盘 3");
+ case 0x005C: return QStringLiteral("小键盘 4");
+ case 0x005D: return QStringLiteral("小键盘 5");
+ case 0x005E: return QStringLiteral("小键盘 6");
+ case 0x005F: return QStringLiteral("小键盘 7");
+ case 0x0060: return QStringLiteral("小键盘 8");
+ case 0x0061: return QStringLiteral("小键盘 9");
+ case 0x0062: return QStringLiteral("小键盘 0");
+ case 0x0063: return QStringLiteral("小键盘 .");
+ case 0x00E0: return QStringLiteral("Left Ctrl");
+ case 0x00E1: return QStringLiteral("Left Shift");
+ case 0x00E2: return QStringLiteral("Left Alt");
+ case 0x00E3: return QStringLiteral("Left GUI");
+ case 0x00E4: return QStringLiteral("Right Ctrl");
+ case 0x00E5: return QStringLiteral("Right Shift");
+ case 0x00E6: return QStringLiteral("Right Alt");
+ case 0x00E7: return QStringLiteral("Right GUI");
+ default:
+ return QStringLiteral("未知 HID Usage");
+ }
+}
+
+/* Consumer Usage -> UI 标签 */
+QString Mid_Func_GetConsumerUsageText(quint16 Usage)
+{
+ switch (Usage)
+ {
+ case 0x0000: return QStringLiteral("释放");
+ case 0x00E2: return QStringLiteral("Mute");
+ case 0x00E9: return QStringLiteral("Volume Up");
+ case 0x00EA: return QStringLiteral("Volume Down");
+ default:
+ return QStringLiteral("未知 Consumer Usage");
+ }
+}
diff --git a/MID/Mid_Def.h b/MID/Mid_Def.h
new file mode 100644
index 0000000..d8ac1c4
--- /dev/null
+++ b/MID/Mid_Def.h
@@ -0,0 +1,86 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+/*
+ * MID 层公共定义:设备配置、匹配条件、HID 报文常量等都集中于此。
+ * 高密注释放在每个结构/函数附近,方便教学时直接引用。
+ */
+
+/* HID 报文 ID:用于区分 NKRO、Consumer、Vendor */
+enum Mid_Enum_ReportId : quint8
+{
+ Mid_Enum_ReportId_None = 0x00,
+ Mid_Enum_ReportId_Nkro = 0x01,
+ Mid_Enum_ReportId_Consumer = 0x03,
+ Mid_Enum_ReportId_Vendor = 0x04
+};
+
+/* 默认 VID/PID:教学用固件的 USB 身份 */
+const quint16 MID_CONST_VENDOR_ID_DEFAULT = 0x1209;
+const quint16 MID_CONST_PRODUCT_ID_DEFAULT = 0x0001;
+
+/* 上位机配置:允许 UI 快速切换 VID/PID */
+struct Mid_Struct_DeviceConfig
+{
+ quint16 VendorId = MID_CONST_VENDOR_ID_DEFAULT;
+ quint16 ProductId = MID_CONST_PRODUCT_ID_DEFAULT;
+};
+
+/* 匹配条件:Win32 SetupAPI + HidP_Caps 的输入结构 */
+struct Mid_Struct_DeviceMatch
+{
+ quint16 VendorId = 0;
+ quint16 ProductId = 0;
+ quint16 UsagePage = 0;
+ quint16 Usage = 0;
+ QString Name;
+};
+
+/* 统一的“原始包”封装:记录来源端口与字节内容 */
+struct Mid_Struct_RawPacket
+{
+ bool IsValid = false;
+ QByteArray ByteArray;
+ QString PortName;
+};
+
+/* Vendor 状态:逻辑层解析后的结果(Modifier + Usage 列表) */
+struct Mid_Struct_VendorState
+{
+ bool IsValid = false;
+ quint8 Modifier = 0;
+ QByteArray UsageBitmap;
+ QVector UsageList;
+ QStringList UsageTextList;
+};
+
+const quint16 MID_CONST_USAGE_PAGE_NKRO = 0x0001;
+const quint16 MID_CONST_USAGE_NKRO = 0x0006;
+const quint16 MID_CONST_USAGE_PAGE_CONSUMER = 0x000C;
+const quint16 MID_CONST_USAGE_CONSUMER = 0x0001;
+const quint16 MID_CONST_USAGE_PAGE_VENDOR = 0xFF00;
+const quint16 MID_CONST_USAGE_VENDOR = 0x0002;
+const int MID_CONST_KEYBOARD_USAGE_MAX = 0x00E7;
+const int MID_CONST_USAGE_BITMAP_SIZE = 29;
+const int MID_CONST_PACKET_SIZE_NKRO = 31;
+const int MID_CONST_PACKET_SIZE_VENDOR = 31;
+const int MID_CONST_PACKET_SIZE_CONSUMER = 3;
+
+/* 设备匹配函数:针对 NKRO / Consumer / Vendor */
+Mid_Struct_DeviceMatch Mid_Func_GetNkroMatch(const Mid_Struct_DeviceConfig& DeviceConfig);
+Mid_Struct_DeviceMatch Mid_Func_GetConsumerMatch(const Mid_Struct_DeviceConfig& DeviceConfig);
+Mid_Struct_DeviceMatch Mid_Func_GetVendorMatch(const Mid_Struct_DeviceConfig& DeviceConfig);
+/* HID 接口枚举 & 辅助文本转换 */
+bool Mid_Func_FindHidInterface(
+ const Mid_Struct_DeviceMatch& Match,
+ QString* p_DevicePath,
+ quint16* p_InputLength,
+ quint16* p_OutputLength);
+QString Mid_Func_GetHexText(const QByteArray& ByteArray);
+QString Mid_Func_GetModifierText(quint8 Modifier);
+QString Mid_Func_GetKeyboardUsageText(quint16 Usage);
+QString Mid_Func_GetConsumerUsageText(quint16 Usage);
diff --git a/QtMsBuild/Qt.props b/QtMsBuild/Qt.props
new file mode 100644
index 0000000..379fccb
--- /dev/null
+++ b/QtMsBuild/Qt.props
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/QtMsBuild/qt.targets b/QtMsBuild/qt.targets
new file mode 100644
index 0000000..9f40e4c
--- /dev/null
+++ b/QtMsBuild/qt.targets
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/QtMsBuild/qt_defaults.props b/QtMsBuild/qt_defaults.props
new file mode 100644
index 0000000..feca7e8
--- /dev/null
+++ b/QtMsBuild/qt_defaults.props
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/REGENERATE_MIGRATION.md b/REGENERATE_MIGRATION.md
new file mode 100644
index 0000000..e626a08
--- /dev/null
+++ b/REGENERATE_MIGRATION.md
@@ -0,0 +1,202 @@
+# 重新生成项目后的代码移植方法
+
+这份说明的目标是:
+
+1. 重新新建一个干净的 Qt Widgets 工程。
+2. 不沿用旧工程的 `.sln / .vcxproj / .vs / x64`。
+3. 只把当前已经实现好的业务代码移植过去。
+
+这样最稳,也最不容易把旧工程状态带进来。
+
+## 一、建议的新建方式
+
+在 Visual Studio 里重新新建一个 Qt Widgets Application。
+
+建议保持这些配置:
+
+- Qt:`5.13.1`
+- 平台:`x64`
+- 编译器:和你当前环境一致
+- Qt Modules:`core;gui;widgets`
+
+新工程创建完成后,先确保它能空工程编译通过一次。
+
+## 二、不要移植的内容
+
+下面这些不要从旧工程复制过去:
+
+- `.vs/`
+- `x64/`
+- `20260320.*` 这些中间目录
+- 旧的 `.sln`
+- 旧的 `.vcxproj`
+- 旧的 `.vcxproj.filters`
+- 旧的 `.vcxproj.user`
+- `Browse.VC.db` 一类缓存
+
+原则很简单:
+
+只移植源码,不移植工程缓存和生成物。
+
+## 三、需要复制过去的目录和文件
+
+把下面这些直接复制到新工程根目录:
+
+- `APP/`
+- `DRI/`
+- `LOGIC/`
+- `EXTEN/`
+- `main.cpp`
+
+如果你想保留当前这套界面和调试窗口,这些就是最小必需集合。
+
+## 四、可以不要的旧模板文件
+
+如果新工程自动生成了自己的 `MainWindow` 或其他模板文件,而你准备直接用当前这套代码,那么这些模板文件可以删掉或从工程里移除。
+
+当前仓库里这些文件不是主链必需:
+
+- `_new_keyboard.cpp`
+- `_new_keyboard.h`
+- `_new_keyboard.ui`
+- `_new_keyboard.qrc`
+
+它们现在没有参与主入口逻辑。
+
+## 五、把文件加回新工程的方法
+
+推荐用 Visual Studio 的“添加现有项”。
+
+### 1. 源文件加入工程
+
+把这些 `.cpp` 加到新工程:
+
+- `APP\\APP_ChatGptButton.cpp`
+- `APP\\APP_DebugPanel.cpp`
+- `APP\\APP_GlassCard.cpp`
+- `APP\\APP_KeyButton.cpp`
+- `APP\\APP_ThemeSwitch.cpp`
+- `APP\\APP_UIWindow.cpp`
+- `DRI\\DRI_DeviceState.cpp`
+- `DRI\\DRI_HidDevice.cpp`
+- `EXTEN\\EXTEN_Theme.cpp`
+- `LOGIC\\LOGIC_DeviceSession.cpp`
+- `LOGIC\\LOGIC_HidProtocol.cpp`
+- `LOGIC\\LOGIC_KeypadModel.cpp`
+- `main.cpp`
+
+### 2. 头文件加入工程
+
+把这些 `.h` 加到新工程:
+
+- `APP\\APP_ChatGptButton.h`
+- `APP\\APP_DebugPanel.h`
+- `APP\\APP_GlassCard.h`
+- `APP\\APP_KeyButton.h`
+- `APP\\APP_ThemeSwitch.h`
+- `APP\\APP_UIWindow.h`
+- `DRI\\DRI_DeviceState.h`
+- `DRI\\DRI_HidDevice.h`
+- `EXTEN\\EXTEN_Theme.h`
+- `LOGIC\\LOGIC_DeviceSession.h`
+- `LOGIC\\LOGIC_HidProtocol.h`
+- `LOGIC\\LOGIC_KeypadModel.h`
+
+## 六、新工程需要保留的工程设置
+
+请确认新工程至少有这些设置:
+
+### 1. Include 目录
+
+`C/C++ -> Additional Include Directories`
+
+至少包含:
+
+- `$(ProjectDir)`
+
+这样代码里的这类包含才能工作:
+
+- `#include "APP/APP_UIWindow.h"`
+- `#include "DRI/DRI_HidDevice.h"`
+- `#include "LOGIC/LOGIC_HidProtocol.h"`
+
+### 2. Qt 模块
+
+保持:
+
+- `core`
+- `gui`
+- `widgets`
+
+### 3. 字符集
+
+保持:
+
+- `Unicode`
+
+### 4. UTF-8 编译
+
+建议保留:
+
+- `/utf-8`
+
+## 七、这套代码迁移时有一个好处
+
+当前新增代码已经特意改成了“普通 C++ 回调”,不是自定义 Qt 信号/槽项目项。
+
+这意味着迁移时:
+
+- 不需要额外手工加 `QtMoc` 条目
+- 不需要改 `.vcxproj` 去注册新 moc 头
+- 只要把 `.cpp/.h` 作为普通源码文件加进去即可
+
+这一步会明显降低“工程能编但 VS 打不开”这类问题。
+
+## 八、HID 相关设置
+
+`DRI\\DRI_HidDevice.cpp` 里已经用了:
+
+- `#pragma comment(lib, "hid.lib")`
+- `#pragma comment(lib, "setupapi.lib")`
+
+所以一般不需要你再手工去 Linker 里加库。
+
+只要是 Windows 桌面工程即可。
+
+## 九、迁移后你应该先验证什么
+
+迁移完成后,不要先看业务联动,先按这个顺序验证:
+
+1. 工程能正常打开
+2. 工程能 `Debug|x64` 编译通过
+3. 程序能启动
+4. 主界面正常显示
+5. 下方调试窗口正常显示
+6. 能枚举到 `VID=0x1209 / PID=0x0001 / UsagePage=0xFF00 / Usage=0x0002`
+7. 能看到收到的原始包
+8. 能看到 `0x04` 包的中文解析
+
+## 十、最稳的实际操作顺序
+
+推荐直接按下面顺序做:
+
+1. 新建一个全新的 Qt Widgets 工程
+2. 先空工程编译一次
+3. 关闭 VS
+4. 把 `APP/DRI/LOGIC/EXTEN/main.cpp` 复制过去
+5. 删除新模板里不用的 `MainWindow` 一类文件
+6. 打开新工程
+7. 用“添加现有项”把上面列出的 `.cpp/.h` 加入工程
+8. 检查 Qt modules 和 include 目录
+9. 编译
+10. 接设备验证调试窗口
+
+## 十一、如果你想让我继续帮你做
+
+如果你重新生成了一个新工程目录,我可以直接帮你做这几件事:
+
+1. 把当前代码拷过去
+2. 帮你把新 `.vcxproj` 补成可编译状态
+3. 再做一次编译验证
+
+你只要把新工程路径告诉我即可。
diff --git a/main.cpp b/main.cpp
new file mode 100644
index 0000000..949a14b
--- /dev/null
+++ b/main.cpp
@@ -0,0 +1,43 @@
+#include "APP/APP_UIWindow.h"
+#include "APP/APP_Theme.h"
+#include
+#include
+#include
+
+int main(int argc, char *argv[])
+{
+ // 在创建 QApplication 之前开启高 DPI 缩放支持,
+ // 让界面在高分屏(如 125%、150%、200% 缩放)下显示更正常。
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+
+ // 在高 DPI 屏幕下使用更清晰的图片/图标资源,
+ // 避免图标被拉伸后发虚。
+ QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
+
+ // 创建 Qt 图形界面应用程序对象,
+ // argc 和 argv 用于接收命令行参数。
+ QApplication app(argc, argv);
+
+ // 统一使用 Fusion 风格,保证各标准控件外观更稳定。
+ app.setStyle(QStyleFactory::create("Fusion"));
+
+ // 统一套用暗色调色板,不需要写大段样式表字符串。
+ app.setPalette(APP::APP_Theme::App_Func_GetPalette());
+
+ // 设置整个应用程序的默认字体,
+ // 所有控件如果没有单独指定字体,一般都会继承这里的设置。
+ app.setFont(APP::APP_Theme::App_Func_GetBodyFont());
+
+ // 创建主窗口对象,
+ // 这里的 App_UIWindow 是你程序的主界面类。
+ APP::App_UIWindow window;
+
+ // 显示主窗口,
+ // 如果不调用 show(),窗口对象虽然创建了,但不会出现在屏幕上。
+ window.show();
+
+ // 启动 Qt 事件循环,
+ // 程序会在这里持续处理鼠标、键盘、重绘、信号槽等事件,
+ // 直到窗口关闭后才会退出,并返回退出码。
+ return app.exec();
+}
diff --git a/none/none/main.cpp b/none/none/main.cpp
new file mode 100644
index 0000000..b534c98
--- /dev/null
+++ b/none/none/main.cpp
@@ -0,0 +1,10 @@
+#include "none.h"
+#include
+
+int main(int argc, char *argv[])
+{
+ QApplication app(argc, argv);
+ none window;
+ window.show();
+ return app.exec();
+}
diff --git a/none/none/none.cpp b/none/none/none.cpp
new file mode 100644
index 0000000..1772f0f
--- /dev/null
+++ b/none/none/none.cpp
@@ -0,0 +1,11 @@
+#include "none.h"
+
+none::none(QWidget *parent)
+ : QMainWindow(parent)
+{
+ ui.setupUi(this);
+}
+
+none::~none()
+{}
+
diff --git a/none/none/none.h b/none/none/none.h
new file mode 100644
index 0000000..829b8f5
--- /dev/null
+++ b/none/none/none.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+#include "ui_none.h"
+
+class none : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ none(QWidget *parent = nullptr);
+ ~none();
+
+private:
+ Ui::noneClass ui;
+};
+
diff --git a/none/none/none.qrc b/none/none/none.qrc
new file mode 100644
index 0000000..89309c6
--- /dev/null
+++ b/none/none/none.qrc
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/none/none/none.slnx b/none/none/none.slnx
new file mode 100644
index 0000000..93b006e
--- /dev/null
+++ b/none/none/none.slnx
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/none/none/none.ui b/none/none/none.ui
new file mode 100644
index 0000000..8d748a7
--- /dev/null
+++ b/none/none/none.ui
@@ -0,0 +1,29 @@
+
+
+ noneClass
+
+
+ noneClass
+
+
+
+ 0
+ 0
+ 600
+ 400
+
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/none/none/none.vcxproj b/none/none/none.vcxproj
new file mode 100644
index 0000000..792de98
--- /dev/null
+++ b/none/none/none.vcxproj
@@ -0,0 +1,112 @@
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ {0AE1B231-592F-49FC-B852-3EF211706C20}
+ QtVS_v304
+ 10.0
+ 10.0
+ $(MSBuildProjectDirectory)\QtMsBuild
+
+
+
+ Application
+ v143
+ true
+ Unicode
+
+
+ Application
+ v143
+ false
+ true
+ Unicode
+
+
+
+
+
+
+ 5.13.1_msvc2015_64
+ core;gui;widgets
+ debug
+
+
+ 5.13.1_msvc2015_64
+ core;gui;widgets
+ release
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ Level3
+ true
+ true
+
+
+ Windows
+ true
+
+
+
+
+ true
+ Level3
+ true
+ true
+ true
+ true
+
+
+ Windows
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/none/none/none.vcxproj.filters b/none/none/none.vcxproj.filters
new file mode 100644
index 0000000..6ea2b49
--- /dev/null
+++ b/none/none/none.vcxproj.filters
@@ -0,0 +1,48 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;hm;inl;inc;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+ {99349809-55BA-4b9d-BF79-8FDBB0286EB3}
+ ui
+
+
+ {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C}
+ ts
+
+
+
+
+ Resource Files
+
+
+ Form Files
+
+
+ Header Files
+
+
+ Source Files
+
+
+
+
+ Resource Files
+
+
+ Resource Files
+
+
+
+