变更详情:feat(kiosk-shell): FreeRDP 3 内嵌客户端跑通,登录页可进入远程桌面 - 按方案 3.2/3.3 节以 libfreerdp3 API 内嵌,不拉起外部进程 - gdi_init 移到 PostConnect(提前调用会段错误) - 去掉 ContextSize 扩展 rdpContext,改用 freerdp* 注册表 + gfx->custom - NLA 认证传空字符串而非 nullptr,修复 SEC_E_NO_CREDENTIALS - 补充 FreeRDP3 内嵌实现笔记文档
355 lines
12 KiB
C++
355 lines
12 KiB
C++
#include "main_window.h"
|
||
|
||
#include <QAction>
|
||
#include <QApplication>
|
||
#include <QCloseEvent>
|
||
#include <QKeySequence>
|
||
#include <QLabel>
|
||
#include <QMenu>
|
||
#include <QMessageBox>
|
||
#include <QScreen>
|
||
#include <QShortcut>
|
||
#include <QStackedWidget>
|
||
#include <QStatusBar>
|
||
#include <QTimer>
|
||
#include <QVBoxLayout>
|
||
|
||
#include <freerdp/codec/color.h>
|
||
|
||
#include "login_page.h"
|
||
|
||
namespace vdi {
|
||
namespace {
|
||
|
||
// FreeRDP 像素格式 -> QImage::Format
|
||
// 注意:FreeRDP 的像素格式值是计算宏(FREERDP_PIXEL_FORMAT),
|
||
// 不能硬编码数值,必须用宏直接比较。
|
||
// FreeRDP 主缓冲是 4 字节/像素(BGRX / BGRA / RGBA),
|
||
// QImage 没有对应的 4 字节 BGRA 格式,因此统一转成 RGB888 输出。
|
||
QImage::Format toQImageFormat(uint32_t freerdpFormat) {
|
||
(void)freerdpFormat;
|
||
return QImage::Format_RGB888;
|
||
}
|
||
|
||
// 把 FreeRDP 的 4 字节/像素缓冲转成 QImage 可用的 RGB888
|
||
// bytesPerLine 是 FreeRDP 的每行字节数(width*4)
|
||
QImage convertFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t bytesPerLine,
|
||
uint32_t freerdpFormat) {
|
||
if (!data || width == 0 || height == 0) {
|
||
return QImage();
|
||
}
|
||
const bool bgr = (freerdpFormat == PIXEL_FORMAT_BGRX32) ||
|
||
(freerdpFormat == PIXEL_FORMAT_BGRA32);
|
||
|
||
QImage out(static_cast<int>(width), static_cast<int>(height), QImage::Format_RGB888);
|
||
for (uint32_t y = 0; y < height; ++y) {
|
||
const uint8_t* src = data + static_cast<size_t>(y) * bytesPerLine;
|
||
uint8_t* dst = out.scanLine(static_cast<int>(y));
|
||
for (uint32_t x = 0; x < width; ++x) {
|
||
const uint8_t* p = src + static_cast<size_t>(x) * 4;
|
||
if (bgr) {
|
||
dst[0] = p[2];
|
||
dst[1] = p[1];
|
||
dst[2] = p[0];
|
||
} else {
|
||
dst[0] = p[0];
|
||
dst[1] = p[1];
|
||
dst[2] = p[2];
|
||
}
|
||
dst += 3;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
constexpr int kReconnectDelayMs = 3000;
|
||
constexpr int kMaxReconnectAttempts = 5;
|
||
|
||
} // namespace
|
||
|
||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||
fsm_ = StateMachine([this](State from, State to, const QString& detail) {
|
||
onStateChanged(from, to, detail);
|
||
});
|
||
setupUi();
|
||
setupHotkey();
|
||
|
||
// BOOT -> CONFIG
|
||
config_ = loadConfig();
|
||
fsm_.transitionTo(State::kConfig, "启动完成");
|
||
showLogin();
|
||
}
|
||
|
||
MainWindow::~MainWindow() {
|
||
stopSession();
|
||
}
|
||
|
||
void MainWindow::setupUi() {
|
||
setWindowTitle(QStringLiteral("VDI 云终端"));
|
||
setWindowState(Qt::WindowFullScreen);
|
||
setStyleSheet(QStringLiteral("QMainWindow { background: #0f1420; }"));
|
||
|
||
stack_ = new QStackedWidget(this);
|
||
stack_->setStyleSheet(QStringLiteral("background: #0f1420;"));
|
||
setCentralWidget(stack_);
|
||
|
||
// 登录页
|
||
config_ = loadConfig();
|
||
auto* login = new LoginPage(config_, this);
|
||
loginPage_ = login;
|
||
login->setStyleSheet(
|
||
QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }"
|
||
"QLineEdit { background: #1a2233; border: 1px solid #2b3654; "
|
||
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
|
||
"QLineEdit:focus { border: 1px solid #2f6feb; }"
|
||
"QComboBox { background: #1a2233; border: 1px solid #2b3654; "
|
||
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
|
||
"QPushButton { background: #232d44; border: 1px solid #2b3654; "
|
||
"border-radius: 6px; color: #e6e9ef; }"
|
||
"QPushButton:hover { background: #2b3654; }"
|
||
"QCheckBox { color: #8a94a6; }"));
|
||
stack_->addWidget(login);
|
||
|
||
connect(login, &LoginPage::connectRequested, this, &MainWindow::onConnectRequested);
|
||
connect(login, &LoginPage::quitRequested, this, &MainWindow::onQuitRequested);
|
||
|
||
// 会话视图
|
||
auto* sessionWidget = new QWidget(this);
|
||
auto* sessionLayout = new QVBoxLayout(sessionWidget);
|
||
sessionLayout->setContentsMargins(0, 0, 0, 0);
|
||
|
||
sessionView_ = new QLabel(sessionWidget);
|
||
sessionView_->setAlignment(Qt::AlignCenter);
|
||
sessionView_->setStyleSheet(QStringLiteral("background: #000000; color: #e6e9ef;"));
|
||
sessionView_->setText(QStringLiteral("正在建立远程会话…"));
|
||
|
||
sessionOverlay_ = new QLabel(sessionWidget);
|
||
sessionOverlay_->setAlignment(Qt::AlignCenter);
|
||
sessionOverlay_->setStyleSheet(
|
||
QStringLiteral("background: rgba(15,20,32,0.82); color: #e6e9ef; "
|
||
"font-size: 20px; padding: 40px;"));
|
||
sessionOverlay_->setText(QStringLiteral("正在连接…"));
|
||
sessionOverlay_->hide();
|
||
|
||
sessionLayout->addWidget(sessionView_);
|
||
sessionLayout->addWidget(sessionOverlay_);
|
||
stack_->addWidget(sessionWidget);
|
||
|
||
// 状态条
|
||
statusBar_ = new QStatusBar(this);
|
||
statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }"));
|
||
setStatusBar(statusBar_);
|
||
|
||
// 重连定时器
|
||
reconnectTimer_ = new QTimer(this);
|
||
reconnectTimer_->setSingleShot(true);
|
||
connect(reconnectTimer_, &QTimer::timeout, this, [this]() {
|
||
if (fsm_.current() == State::kReconnect && reconnectAttempts_ < kMaxReconnectAttempts) {
|
||
++reconnectAttempts_;
|
||
startSession(config_);
|
||
} else if (fsm_.current() == State::kReconnect) {
|
||
fsm_.transitionTo(State::kError, "重连失败,请检查网络");
|
||
}
|
||
});
|
||
}
|
||
|
||
void MainWindow::setupHotkey() {
|
||
// 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单(方案 3.2 节)
|
||
auto* shortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this);
|
||
shortcut->setContext(Qt::ApplicationShortcut);
|
||
connect(shortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
|
||
}
|
||
|
||
void MainWindow::onStateChanged(State from, State to, const QString& detail) {
|
||
Q_UNUSED(from);
|
||
const QString stateName = QString::fromLatin1(stateToString(to));
|
||
updateStatusBar(detail.isEmpty() ? stateName : QStringLiteral("%1 %2").arg(stateName, detail));
|
||
|
||
switch (to) {
|
||
case State::kConfig:
|
||
showLogin();
|
||
break;
|
||
case State::kConnecting:
|
||
if (stack_->currentWidget() != sessionView_->parentWidget()) {
|
||
stack_->setCurrentWidget(sessionView_->parentWidget());
|
||
}
|
||
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …")
|
||
.arg(QString::fromStdString(config_.server)));
|
||
sessionOverlay_->show();
|
||
break;
|
||
case State::kSession:
|
||
sessionOverlay_->hide();
|
||
break;
|
||
case State::kReconnect:
|
||
sessionOverlay_->setText(QStringLiteral("连接已断开,正在重连…"));
|
||
sessionOverlay_->show();
|
||
reconnectTimer_->start(kReconnectDelayMs);
|
||
break;
|
||
case State::kError:
|
||
sessionOverlay_->setText(QStringLiteral("连接失败:%1").arg(detail));
|
||
sessionOverlay_->show();
|
||
if (loginPage_) {
|
||
loginPage_->setBusy(false);
|
||
loginPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true);
|
||
}
|
||
break;
|
||
case State::kBoot:
|
||
break;
|
||
}
|
||
}
|
||
|
||
void MainWindow::updateStatusBar(const QString& text) {
|
||
statusBar_->showMessage(text);
|
||
}
|
||
|
||
void MainWindow::showLogin() {
|
||
stack_->setCurrentWidget(loginPage_);
|
||
if (loginPage_) {
|
||
loginPage_->setBusy(false);
|
||
loginPage_->setStatus(QStringLiteral("就绪"));
|
||
}
|
||
}
|
||
|
||
void MainWindow::onConnectRequested(const vdi::TerminalConfig& cfg) {
|
||
config_ = cfg;
|
||
reconnectAttempts_ = 0;
|
||
startSession(cfg);
|
||
}
|
||
|
||
void MainWindow::startSession(const TerminalConfig& cfg) {
|
||
if (!fsm_.transitionTo(State::kConnecting, QString::fromStdString(cfg.server))) {
|
||
// 已经在 CONNECTING 或 SESSION,忽略重复触发
|
||
if (fsm_.current() != State::kConnecting) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
RdpOptions opts;
|
||
opts.server = cfg.server;
|
||
opts.username = cfg.username;
|
||
opts.password = cfg.password;
|
||
|
||
// 解析分辨率
|
||
int w = 1920;
|
||
int h = 1440;
|
||
if (sscanf(cfg.resolution.c_str(), "%dx%d", &w, &h) != 2) {
|
||
w = 1920;
|
||
h = 1440;
|
||
}
|
||
opts.width = w;
|
||
opts.height = h;
|
||
opts.bpp = 32;
|
||
opts.fullscreen = true;
|
||
// 与 xfreerdp3 相同的 GFX 能力组合语义:
|
||
// FreeRDP 3.32.1 不接受单个管道名,必须成组。
|
||
opts.gfx = "progressive|RFX|AVC420|AVC444";
|
||
opts.ignoreCertificate = true; // POC 阶段;正式产品必须做证书校验
|
||
|
||
RdpCallbacks cb;
|
||
cb.onFrame = [this](const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
|
||
uint32_t format) {
|
||
// 帧数据来自 FreeRDP 线程,切换到 GUI 线程更新
|
||
QMetaObject::invokeMethod(
|
||
this, [this, data, width, height, stride, format]() {
|
||
onFrame(data, width, height, stride, format);
|
||
},
|
||
Qt::QueuedConnection);
|
||
};
|
||
cb.onState = [this](bool connected, const std::string& message) {
|
||
QMetaObject::invokeMethod(
|
||
this,
|
||
[this, connected, message]() {
|
||
onSessionState(connected, QString::fromStdString(message));
|
||
},
|
||
Qt::QueuedConnection);
|
||
};
|
||
|
||
stopSession();
|
||
session_ = std::make_unique<RdpSession>();
|
||
session_->start(opts, cb);
|
||
}
|
||
|
||
void MainWindow::stopSession() {
|
||
if (session_) {
|
||
session_->stop();
|
||
session_.reset();
|
||
}
|
||
}
|
||
|
||
void MainWindow::onSessionState(bool connected, const QString& message) {
|
||
if (connected) {
|
||
reconnectAttempts_ = 0;
|
||
fsm_.transitionTo(State::kSession, message);
|
||
} else {
|
||
if (fsm_.current() == State::kConnecting) {
|
||
// 首次连接失败
|
||
fsm_.transitionTo(State::kError, message);
|
||
} else if (fsm_.current() == State::kSession) {
|
||
fsm_.transitionTo(State::kReconnect, message);
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::onFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
|
||
uint32_t format) {
|
||
if (!data || width == 0 || height == 0) {
|
||
return;
|
||
}
|
||
// FreeRDP 主缓冲是 4 字节/像素,需转换后交给 Qt
|
||
QImage image = convertFrame(data, width, height, stride, format);
|
||
if (image.isNull()) {
|
||
return;
|
||
}
|
||
lastFrame_ = image;
|
||
frameDirty_ = true;
|
||
|
||
if (sessionView_) {
|
||
sessionView_->setPixmap(QPixmap::fromImage(lastFrame_).scaled(
|
||
sessionView_->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
}
|
||
}
|
||
|
||
void MainWindow::onQuitRequested() {
|
||
stopSession();
|
||
QApplication::quit();
|
||
}
|
||
|
||
void MainWindow::onMenuAction(int action) {
|
||
switch (action) {
|
||
case 1: // 重新连接
|
||
reconnectAttempts_ = 0;
|
||
stopSession();
|
||
startSession(config_);
|
||
break;
|
||
case 2: // 断开回登录页
|
||
stopSession();
|
||
fsm_.transitionTo(State::kConfig, "已断开");
|
||
break;
|
||
case 3: // 退出终端
|
||
onQuitRequested();
|
||
break;
|
||
default: {
|
||
QMenu menu(this);
|
||
QAction* reconnect = menu.addAction(QStringLiteral("重新连接"));
|
||
QAction* disconnect = menu.addAction(QStringLiteral("断开并返回登录页"));
|
||
menu.addSeparator();
|
||
QAction* quit = menu.addAction(QStringLiteral("退出终端"));
|
||
QAction* chosen = menu.exec(QCursor::pos());
|
||
if (chosen == reconnect) {
|
||
onMenuAction(1);
|
||
} else if (chosen == disconnect) {
|
||
onMenuAction(2);
|
||
} else if (chosen == quit) {
|
||
onMenuAction(3);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
|
||
return QMainWindow::eventFilter(watched, event);
|
||
}
|
||
|
||
} // namespace vdi
|