

Background

Why choose Qt + MQTT for implementation?
Initially, I used Electron + WebRTC + SRS for implementation, but WebRTC requires exchanging protocols, and obtaining protocol content in Electron’s WebRTC is done through Chromium, which is difficult to control. The key issue is that the Electron package is hard to manage, so I abandoned Electron.
Later, I researched a JavaFX solution based on JavaFX + JavaCV + Zlmediakit. The problem here is that merging images for streaming causes significant latency unless Zlmediakit is optimized, but this is a small feature that needs to consider both development and operational costs. Moreover, the Chinese character garbling in the JavaFX tray and the issues with packaging into an EXE are hard to handle; I didn’t succeed on my end. Even though the Wix environment was clearly installed, it said it couldn’t be found.
I also briefly researched the Flutter Windows solution, but considering that desktop screen capture is required and the current Flutter solution is not mature enough, I abandoned it as well.
Ultimately, I decided to use the Qt solution. Since the requirement is just screen sharing, I had previously researched remote desktop control solutions, which are actually based on desktop screen capture, so I directly used the MQTT solution for this.

MQTT

The Qt environment does not include MQTT by default, so we need to compile it ourselves and add it to the environment.
Source code link: https://github.com/qt/qtmqtt
Switch to the branch that matches the local Qt version and build it.
After the build is complete, simply copy the relevant files to the Qt environment.
The main directories include:
-
bin
-
include
-
lib
-
mkspecs
-
modules
Note: In the bin folder, only the DLL files need to be copied.

Project Code

01
mainwindow.h
#ifndef MAINWINDOW_H#define MAINWINDOW_H
#include <QLabel>#include <QMainWindow>#include <QMenu>#include <QMqttClient>#include <QScreen>#include <QSettings>#include <QSystemTrayIcon>
QT_BEGIN_NAMESPACEnamespace Ui { class MainWindow; }QT_END_NAMESPACE
class MainWindow : public QMainWindow{ Q_OBJECT
public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); QString ipV4(); void connectMqtt(); QSettings *settings; void screenMonitor();
protected: void closeEvent(QCloseEvent *event) override;
private: Ui::MainWindow *ui; QScreen *screen; QLabel *imageLabel; QSystemTrayIcon *trayIcon; QMenu *trayMenu; QString ipv4; QMqttClient *mqttClient; bool monitoring;};#endif // MAINWINDOW_H
02
mainwindow.cpp
#include "mainwindow.h"#include "./ui_mainwindow.h"
#include <QSystemTrayIcon>#include <QMenu>#include <QCloseEvent>#include <QDebug>#include <QHostInfo>#include <QLabel>#include <QCoreApplication>#include <QGuiApplication>#include <QTimer>#include <QBuffer>#include <QMqttTopicName>#include <QMqttClient>#include <QImage>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); settings = new QSettings("config.ini", QSettings::IniFormat); qDebug() << "mqtt hostname:" << settings->value("mqtt/hostname").toString(); ipv4 = this->ipV4(); mqttClient = new QMqttClient(this); this->setWindowTitle("Screen Sharing:" + ipv4);
imageLabel = new QLabel(this);
imageLabel->setScaledContents(true); // Set QLabel size policy to auto-adjust size imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); // Set QLabel alignment in layout imageLabel->setAlignment(Qt::AlignCenter);
trayIcon = new QSystemTrayIcon(this); trayIcon->setIcon(QIcon(":/resources/assets/logo.png")); trayIcon->setToolTip("Screen Sharing"); trayMenu = new QMenu(this);
trayMenu->addAction("Open Window", this, [&]() { this->show(); }); trayMenu->addAction("Start", this, [&]() { monitoring = true; }); trayMenu->addAction("Stop", this, [&]() { monitoring = false; }); trayMenu->addAction("Exit", this, [&]() { QCoreApplication::quit(); });
trayIcon->setContextMenu(trayMenu); trayIcon->show();
screen = QGuiApplication::primaryScreen(); QPixmap screenshot = screen->grabWindow(0);
imageLabel->setPixmap(screenshot); screenshot.scaled(this->size(), Qt::KeepAspectRatio);
this->setCentralWidget(imageLabel); this->setMinimumWidth(920); this->setMinimumHeight(540);
connectMqtt(); screenMonitor();}
MainWindow::~MainWindow() { delete ui;}
void MainWindow::closeEvent(QCloseEvent *event) { if (trayIcon->isVisible()) { hide(); event->ignore(); } else { event->accept(); }}
void MainWindow::screenMonitor() { QTimer *timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, [&]() {
// Capture screen content QPixmap screenshot = screen->grabWindow(0);
// Set QLabel image imageLabel->setPixmap(screenshot); if (monitoring) { QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); QImage image = screenshot.toImage();
qDebug() << "origin size" << image.sizeInBytes(); int ratio = settings->value("image/ratio").toInt(); screenshot.save(&buffer, "JPEG", (ratio > 0 ? ratio : 20)); // Can be replaced with other image formats like "PNG"
QString base64 = byteArray.toBase64(); // qDebug() << base64; QMqttTopicName topic("desktop/" + ipv4); qDebug() << mqttClient->state(); qDebug() << base64.toUtf8().size(); if (mqttClient->state() == QMqttClient::Connected) { mqttClient->publish(topic, base64.toUtf8()); }
}
});
int interval = settings->value("timer/interval").toInt(); timer->setInterval(interval > 0 ? interval : 200); timer->start();}
QString MainWindow::ipV4() { QString hostName = QHostInfo::localHostName();
// Get host information based on hostname QHostInfo hostInfo = QHostInfo::fromName(hostName);
// Get IP address list QList <QHostAddress> addressList = hostInfo.addresses();
// Iterate through IP address list foreach( const QHostAddress &address, addressList) {
// Check if it is an IPv4 address if (address.protocol() == QAbstractSocket::IPv4Protocol) { qDebug() << "IPv4 Address:" << address.toString(); return address.toString(); }
} return NULL;}
void MainWindow::connectMqtt() {
mqttClient->setClientId("desktop_" + ipv4); mqttClient->setHostname(settings->value("mqtt/hostname").toString()); mqttClient->setPort(settings->value("mqtt/port").toInt()); mqttClient->setUsername(settings->value("mqtt/username").toString()); mqttClient->setPassword(settings->value("mqtt/password").toString()); mqttClient->setAutoKeepAlive(true);
mqttClient->connectToHost(); QObject::connect(mqttClient, &QMqttClient::connected, [&]() { qDebug() << "Connected successfully"; });
}
03
The front end uses React + MQTT.js to display the image.
import {useEffect, useState} from "react";import * as mqtt from "mqtt";import {Image} from "antd";
export const IndexPage = () => { const [client, setClient] = useState<mqtt.MqttClient>() const [image,setImage] = useState<any>()
useEffect(() => { const mqttClient = mqtt.connect("mqtt://localhost:8083/mqtt") mqttClient.on("connect", () => { mqttClient.subscribe("desktop/192.168.247.1", (err) => { }); }); mqttClient.on('message',(topic, payload, packet)=>{ setImage(`data:image/jpeg;base64,${payload}`); })
setClient(mqttClient) }, [])
return <> <Image src={image}/> </>}

Effect

On the left is the effect obtained by the front end, and on the right is the screen capture effect.

Deployment

Configure the Windows deployment environment variables.
Set up the deployment folder.
Execute the deployment.
Then you will see the generated EXE file in the target folder.
Use windeployqt6 to automatically copy the required dependency files.



