Files
0417_QT_code/APP/APP_UIWindow.cpp
2026-03-26 10:45:29 +08:00

623 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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