添加项目文件。

This commit is contained in:
2026-03-26 10:45:29 +08:00
parent 10441f488c
commit b576d3f19d
48 changed files with 4812 additions and 0 deletions

622
APP/APP_UIWindow.cpp Normal file
View File

@@ -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 <QtGui/QMouseEvent>
#include <QtGui/QPainter>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QFormLayout>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTabBar>
#include <QtWidgets/QTabWidget>
#include <QtWidgets/QVBoxLayout>
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<int>(&QComboBox::currentIndexChanged));
ConnectConfigSignal(appFunctionComboSwapRight, qOverload<int>(&QComboBox::currentIndexChanged));
/*
* 功能键按钮统一交给 QButtonGroup 管,
* 这样既能减少每颗按钮各写一段 lambda
* 也正好把 Qt 里“按钮分组 + id 分发”的知识点展示出来。
*/
connect(appFunctionButtonGroup, qOverload<int>(&QButtonGroup::buttonClicked), this, [this](int ButtonId)
{
App_Func_OnFunctionKeyClicked(static_cast<quint16>(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<void*>(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<quint16>* 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<quint16>(appFunctionComboSwapLeft->currentData().toUInt());
appLgcState.FunctionButtonConfig.SwapUsageRight =
static_cast<quint16>(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<APP_KeyInfo>& 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<APP_KeyInfo>& 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<int>(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<APP_KeyInfo>& FunctionKeyList = appKeypadModel.App_Func_GetFunctionKeyList();
for (const APP_KeyInfo& Key : FunctionKeyList)
{
appFunctionComboSwapLeft->addItem(Key.label, static_cast<uint>(Key.usage));
appFunctionComboSwapRight->addItem(Key.label, static_cast<uint>(Key.usage));
}
const auto SetComboIndex = [](QComboBox* Combo, quint16 Usage)
{
const int Index = Combo->findData(static_cast<uint>(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