#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