#include "main_window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(width), static_cast(height), QImage::Format_RGB888); for (uint32_t y = 0; y < height; ++y) { const uint8_t* src = data + static_cast(y) * bytesPerLine; uint8_t* dst = out.scanLine(static_cast(y)); for (uint32_t x = 0; x < width; ++x) { const uint8_t* p = src + static_cast(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(); 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