【QT常用技术讲解】国产Linux桌面系统+window系统通过窗口句柄对窗口进行操作
前言
本人的国产化项目涉及在国产Linux系统(宿主机)与window系统(虚拟机)的应用窗口交互功能:国产Linux系统与window生成一对一匹配的虚拟应用窗口,当点击虚拟应用窗口时,window端需要从任务栏中激活并显示到桌面最顶端。本篇将分别讲解国产桌面系统(统信UOS和麒麟kylin系统)和window10上通过窗口句柄实现对窗口的操作。
国产Linux桌面系统(宿主机)
1、创建窗口应用
QT中创建一个QWidget项目,在初始化函数中,启动窗口的任务栏点击事件、设置按钮和尺寸等属性。
ui->setupUi(this);
// 启用窗口的任务栏点击事件
setWindowFlags(Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint);
// 设置窗口标志,移除最大化、最小化和关闭按钮
this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
setWindowOpacity(0); // 设置窗口为完全透明
resize(1, 1); // 设置窗口为一个最小的尺寸
2、发送(打开指定应用程序命令)信息到虚拟机端
获取到当前程序的进程ID,以及要打开的window应用绝对路径,一起发送给虚拟机window端的QT编写的win服务器进程。
#include "networkclient.h"
void sendpack(const QString& Ip,netPackQ packet){
// 创建一个客户端连接
NetworkClient client(Ip, PORT);
// 发送数据
client.sendData(packet);
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
pid_t pid = QCoreApplication::applicationPid();
.....
}
发送端的NetworkClient类如下:
//"networkclient.h"头文件
#ifndef NETWORKCLIENT_H
#define NETWORKCLIENT_H
#include <QObject>
#include <QTcpSocket>
#include <QDataStream>
#include <QByteArray>
#include <QString>
#include <iostream>
#include <QTimer>
//请求包结构
typedef struct netPackQ
{
char opCode;
char keyword[254];
netPackQ(){
memset(opCode,0,1);
memset(keyword,0,254);
}
} netPackQ;
class NetworkClient : public QObject
{
Q_OBJECT
public:
explicit NetworkClient(QObject *parent = nullptr);
NetworkClient(const QString &host, int port);
~NetworkClient();
void sendData(const netPackQ &data) ;
bool checkPortOpen(const QString &host, quint16 port, int timeout = 1);
bool m_blink_status;
private:
QTcpSocket *socket;
};
#endif // NETWORKCLIENT_H
//networkclient.cpp文件
#include "networkclient.h"
NetworkClient::NetworkClient(const QString &host, int port)
{
m_blink_status=false;
socket = new QTcpSocket(this);
// 连接到服务器
socket->connectToHost(host, port);
if (!socket->waitForConnected(2000)) {
std::cerr << "连接服务器失败!" << std::endl;
return;
}else m_blink_status=true;
std::cout << "已连接到服务器!" << std::endl;
}
NetworkClient::~NetworkClient() {
if (socket->isOpen()) {
socket->close();
}
}
void NetworkClient::sendData(const netPackQ &data) {
if (socket->state() == QAbstractSocket::ConnectedState) {
// 创建数据流
QByteArray byteArray;
QDataStream out(&byteArray, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_5_12); // 设置版本,避免Qt版本变化导致问题
// 写入数据结构
out.writeRawData(data.opCode, sizeof(data.opCode));
out.writeRawData(data.keyword, sizeof(data.keyword));
// 发送数据
socket->write(byteArray);
socket->flush(); // 确保数据立即发送
std::cout << "数据已发送!" << std::endl;
}
}
qint64 getcurtime(){
// 获取当前时间戳(秒级)
std::time_t timestamp = std::time(nullptr);
return timestamp;
}
bool NetworkClient::checkPortOpen(const QString &host, quint16 port, int timeout) {
qint64 bgtime=getcurtime();
QTcpSocket socket;
QTimer timer;
// 设置超时机制
timer.setSingleShot(true);
// 设置连接超时为1秒
QObject::connect(&timer, &QTimer::timeout, [&]() {
qDebug() << "Connection attempt to" << host << "on port" << port << "timed out!";
socket.abort(); // 取消当前连接尝试
});
// 尝试连接目标主机和端口
socket.connectToHost(host, port);
// 启动计时器来控制超时
timer.start(timeout);
// 等待连接状态变化
if (socket.waitForConnected(timeout)) {
// 连接成功
qDebug() << "Successfully connected to" << host << "on port" << port;
socket.disconnectFromHost();
return true; // 表示端口可用
} else {
// 连接失败或超时
return false; // 表示端口不可用
}
qint64 endtime=getcurtime();
std::cout << " checksqlonline use time:" << endtime-bgtime << std::endl;
return false;
}
需要在.pro文件中加入network
QT += network
3、点击宿主机任务栏上的窗口应用图标时,发送(指定应用激活并置顶到桌面)信息到虚拟机端
需要重写事件函数event(),增加发送事件函数sendevent();
// 重写事件处理函数
bool event(QEvent *event) override{
// 判断是否是窗口激活事件
if (event->type() == QEvent::WindowActivate) {
//发送窗口激活命令到window系统端
sendevent();
return true; // 事件已处理
}
// 调用基类的事件处理
return QWidget::event(event);
}
void sendevent(){
// 创建一个客户端连接
NetworkClient client(m_Ip, PORT); //
if (client.m_blink_status==false){
std::cout << client.m_blink_status << std::endl;
QMessageBox::information(this, "提示框", "无法链接对端进程管理器,程序将退出");
this->close();
}
else {
// 创建一个数据包实例并填充数据
netPackQ packet;
packet.opCode=1;//发送应用发布窗口信息
sprintf(packet.keyword, "%d",m_Pid); // 设置关键字
// 发送数据
client.sendData(packet);
}
}
深入的优化需求场景一
当虚拟机客户端缩小到宿主机(国产系统)的任务栏时,用户进行以上的激活操作是无法让虚拟机窗口显示在宿主机桌面的,以下介绍三种“国产系统(统信UOS、麒麟kylin桌面系统)让缩小到任务栏的第三方应用窗口,激活并显示到桌面”的解决方案(只有最后一个有效)。
方案一:QT的Xlib库
#include <QtCore/qtextstream.h>
#include <QApplication>
#include <QtWidgets/QWidget>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/extensions/shape.h>
#include <iostream>
#include <QDebug>
void activateWindow(Window windowId) {
std::cout << __LINE__ << std::endl;
// 打开 X11 显示连接
Display *display = XOpenDisplay(nullptr);
if (!display) {
qDebug() << "Cannot open X11 display!";
std::cout << __LINE__ << std::endl;
return;
}
// 获取窗口信息
XWindowAttributes winAttributes;
if (!XGetWindowAttributes(display, windowId, &winAttributes)) {
qDebug() << "Cannot get window attributes!";
std::cout << __LINE__ << std::endl;
XCloseDisplay(display);
return;
}
// 将窗口显示到桌面(确保它不被最小化)
XMapWindow(display, windowId); // 将窗口显示
// 将窗口置顶
XRaiseWindow(display, windowId);
// 激活窗口(通过设置焦点)
XSetInputFocus(display, windowId, RevertToParent, CurrentTime);
// 确保修改生效
XFlush(display);
XCloseDisplay(display);
std::cout << __LINE__ << std::endl;
}
int main(int argc, char *argv[])
{
if (argc < 2) {//外部传入windowId方式
std::cout << "Usage: " << argv[0] << " <windowId>" << std::endl;
return -1;
}
// 从命令行参数获取窗口 ID,并将其从字符串转换为 Window 类型
QByteArray windowIdStr = argv[1]; // 获取参数,假设它是一个十六进制字符串
Window windowId = strtoul(windowIdStr.constData(), nullptr, 16); // 将字符串转换为 Window ID(十六进制)
//Window windowId = 0x07a00006;//可以用这条进行测试
// 激活该窗口并将其置顶
activateWindow(windowId);
return 0;
}
需要在.pro文件中加入库
LIBS += -lX11
以上程序需要的传入的参数windowId是通过命令行wmctrl -x -l获取的第一列信息(后文专项讲解)。经过测试,以上功能可以让缩小到任务栏的窗口应用有响应(变黄色并且闪烁),但未显示到桌面,不符合项目需求(后面有符合需求的解决方案)。
方案二:QT的<Qwindow>
#include <QApplication>
#include<QWindow>
void showwindow(Window windowId){
QWindow *window = QWindow::fromWinId(windowId);
if(window){
window->show();
window->requestActivate();
//QGuiApplication::focusWindow();
window->raise();
std::cout << __LINE__ << std::endl;
}else{
std::cout << __LINE__ << std::endl;
}
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
if (argc < 2) {
std::cout << "Usage: " << argv[0] << " <windowId>" << std::endl;
return -1;
}
// 从命令行参数获取窗口 ID,并将其从字符串转换为 Window 类型
QByteArray windowIdStr = argv[1]; // 获取参数,假设它是一个十六进制字符串
Window windowId = strtoul(windowIdStr.constData(), nullptr, 16); // 将字符串转换为 Window ID(十六进制)
// 假设我们从 wmctrl 获取到的窗口句柄是 0x06400002
//Window windowId = 0x07a00006;
// 激活该窗口并将其置顶
showwindow(windowId);
return a.exec();
}
以上程序需要的传入的参数windowId是通过命令行wmctrl -x -l获取的第一列信息(后文专项讲解)。经过测试,以上功能可以让缩小到任务栏的窗口应用有响应(变黄色并且闪烁),但未显示到桌面,不符合项目需求(后面有符合需求的解决方案)。
方案三:系统工具wmctl
第一列(例如:0x0c000006,标识为CLASSID)是当前的窗口句柄,第三列(peony-qt-desktop.桌面,标识为CLASS)是窗口类,第四列(xxxx 桌面,标识为win_name)是应用窗口的名称。
系统级工具wmctrl,通过命令行:
wmctrl -x -r <CLASS> -b remove,hidden && wmctrl -x -a <CLASS>
可以让激活任务栏中的指定类型的窗口激活并显示到桌面,当时有一个缺点,一个类型有多个窗口打开时,只能激活其中一个(窗口ID最小的),比如多个文件管理器缩小到任务栏,此命令只能激活一个显示到桌面,如果某个应用是唯一的,可以使用此命令完成交互效果(比如我需要激活vbox客户端,在我的项目中它是唯一显示到桌面的)。
如果需要根据窗口句柄进行激活,可以达到精准的激活,以下是命令行:
wmctrl -i -r <CLASSID> -b remove,hidden && wmctrl -i -a <CLASSID>
深入的应用场景二
如果用户右键点击宿主机(国产系统)任务栏上虚拟应用窗口的右键“退出”功能时,通常系统会直接把此窗口杀死,此时同步发送关闭应用命令给到虚拟机进行关闭,这样能达到初级的事件同步闭环。
深入的场景时,如果发送关闭事件给到虚拟机(window系统),虚拟机的窗口关不掉呢,比如正在打开的记事本有编辑的内容未保存时,会弹窗提示是否关闭,如果选择“不关闭”,此时虚拟机中的应用窗口就会“遗留”。解决这个问题的方法,就需要在宿主机(工程系统)重载窗口的关闭事件closeEvent()来先阻止窗口关闭,等待虚拟机(window)正在的关闭窗口之后,再同步发送真是关闭窗口的信息回来之后,再关闭/杀掉虚拟机窗口。
// 重写 closeEvent 事件---接收在任务栏上右键点击“关闭所有”的菜单功能事件---20250218
void closeEvent(QCloseEvent *event) override {
std::cout << "====截获关闭窗口命令,等待window返回关闭指令====" << std::endl;
event->ignore(); // 拒绝关闭窗口
sendkill();
}
voidsendkill(){
// 创建一个客户端连接
NetworkClient client(m_Ip, PORT);
if (client.m_blink_status==false){
std::cout << client.m_blink_status << std::endl;
QMessageBox::information(this, "提示框", "无法链接对端进程管理器,程序将退出");
this->close();
}
else {
// 创建一个数据包实例并填充数据
netPackQ packet;
packet.opCode = 2; // 关闭窗口
sprintf(packet.keyword, "%d",m_Pid); // 设置关键字
// 发送数据
client.sendData(packet);
}
}
另外,“等待虚拟机(window)正在的关闭窗口之后,再同步发送真是关闭窗口的信息回来之后,再关闭/杀掉虚拟机窗口”这个功能,我另外写了一个TCP服务,接收虚拟机端主动的关闭的窗口进程,然后同步杀死宿主机端对应的进程ID(这一块没有技术难点,这里就不展示了,参照下文window端的服务器部分来写即可)。
window系统(虚拟机)
1、创建TCP服务
创建tcp服务进行侦听
void Mytcpserver::runserver(){
// 创建 TCP 服务器
server = new QTcpServer(this);
// 连接新连接信号到槽函数
connect(server, &QTcpServer::newConnection, this, &Mytcpserver::onNewConnection);
// 绑定到 10001 端口
if (!server->listen(QHostAddress::Any, 10001)) {
qDebug() << "Server could not start!";
} else {
qDebug() << "Server started on port 10001.";
}
}
2、接收打开应用程序命令,通过后台命令行启动进程
void Mytcpserver::onNewConnection() {
QTcpSocket *socket = server->nextPendingConnection();
// 获取对端的IP地址
QHostAddress clientAddress = socket->peerAddress();
//qDebug() << "New connection from:" << clientAddress.toString();
m_peerIp=clientAddress.toString();
// 连接读取数据信号
connect(socket, &QTcpSocket::readyRead, [socket,this]() {
netPackQ packet;
qint64 bytesReceived = socket->read(reinterpret_cast<char*>(&packet), sizeof(netPackQ));
if (bytesReceived == sizeof(netPackQ)) {
// 处理接收到的数据
qDebug() << "Received date";
if(packet.opCode== 1){//执行后台命令,打开指定应用。比如:C:\Windows\notepad.exe
qDebug() << "执行指定程序(PID|PATH). keyword:" << packet.keyword;
bool bfind=false;
QStringList recvlist=QString(packet.keyword).split("|");
if(recvlist.size()>1){
QString pid=recvlist[0];
QString execpath=recvlist[1];
//1.运行程序
cmdrun act;
act.Run(execpath);
//2.获取execpath打开的窗口句柄,并进行绑定
//因为执行程序之后,获取窗口句柄存在延时,需要加入轮询机制
for(int num=0;num<5;num++){//给停止4秒的轮询机会
qDebug() << "num:" << num;
//QThread::sleep会导致QTimer定时器休眠,这里要主动调用getwinHwnd()
QMap<HWND,QString> hwmdmap=getwinHwnd();//更新窗口句柄清单
for (auto &key : hwmdmap.keys()) {
HWND hwnd = key;
QString hw_execpath = hwmdmap[key];
//qDebug() << "hw_execpath:" << hw_execpath << "==>execpath:" << execpath;
//if(hw_execpath==execpath && m_ignorehwndlist.count(hwnd)==0){//判断执行程序路径是否一样
if(checkpath(hw_execpath,execpath) && m_ignorehwndlist.count(hwnd)==0){//判断执行程序路径是否一样
//判断窗口是否已经绑定过,多一个执行程序是有可能开多个窗口的
if(m_hwndpidmap.count(hwnd)==0){//如果没有绑定过就进行绑定操作
stHwnd sthd;
sthd.hwnd=hwnd;
sthd.pid=pid;
//sthd.path=execpath;
sthd.path=hw_execpath;
m_hwndpidmap[hwnd]=sthd;
quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
QString msg = QString("执行新程序 hwnd:%1==>pid:%2 path:%3").arg(valueAsUInt).arg(pid).arg(execpath);
qDebug() << msg;
writeLog(msg);
bfind=true;
break;
}
}
}
if(bfind) break;
QThread::sleep(1);
}
}
if(bfind)
socket->write("sucess\n"); // 回复客户端--绑定成功
else
socket->write("fail\n"); // 回复客户端----绑定失败
}
} else {
qDebug() << "Received incomplete data";
}
});
// 连接断开信号
connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
}
3、让指定应用窗口激活并指定显示到桌面
else if(packet.opCode==2){//让指定的应用窗口置顶---接收宿主机的命令
qDebug() << "执行窗口前置事件(PID). keyword:" << packet.keyword;
bool bfind=false;
QString pid=packet.keyword;
for (auto &key : m_hwndpidmap.keys()){
HWND hwnd = key;
//QString hw_pid = m_hwndpidmap[hwnd];
stHwnd sthd = m_hwndpidmap[hwnd];
QString hw_pid = sthd.pid;
if(hw_pid==pid){
restoreNotepad(hwnd);
bfind=true;
}
}
if(bfind)
socket->write("sucess\n"); // 回复客户端--找到pid
else
socket->write("fail\n"); // 回复客户端----未找到pid
}
// 从任务栏恢复 Notepad 窗口
void Mytcpserver::restoreNotepad(HWND hwnd) {
//qDebug() << __FUNCTION__ << __LINE__;
qDebug() << __FUNCTION__ << "触发置顶事件操作";
ShowWindow(hwnd, SW_RESTORE); // 恢复窗口
SetForegroundWindow(hwnd); // 将窗口置于最前--只能从任务栏中激活,如果没有缩小到任务栏,无法展现在最前面
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);// 将窗口窗口置顶
QThread::sleep(0.5);//有些应用需要延时才能达到置顶+取消置顶的效果,比如资源管理器
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);//取消窗口置顶
}
4、监测应用窗口关闭,同步发送信息给宿主机
else if(packet.opCode == 9){//让指定的应用窗口关闭--接收宿主机的命令
qDebug() << "关闭指定程序(PID). keyword:" << packet.keyword;
bool bfind=false;
QString pid=packet.keyword;
for (auto &key : m_hwndpidmap.keys()){
HWND hwnd = key;
//QString hw_pid = m_hwndpidmap[hwnd];
stHwnd sthd = m_hwndpidmap[hwnd];
QString hw_pid = sthd.pid;
if(hw_pid==pid){
closeWindowByHwnd(hwnd);//通过窗口句柄关闭窗口时,如果需要保存,是无法直接关闭窗口的
bfind=true;
quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
QString msg = QString("关闭指定程序(PID) hwnd:%1==>pid:%2").arg(valueAsUInt).arg(pid);
writeLog(msg);
}
}
if(bfind)
socket->write("sucess\n"); // 回复客户端--找到pid
else
socket->write("fail\n"); // 回复客户端----未找到pid
}
void Mytcpserver::checkkillproc(){
QMap<HWND,QString> hwmdmap=getwinHwnd();
for (auto &key : m_hwndpidmap.keys()) {
stHwnd sthd = m_hwndpidmap[key];
QString pid = sthd.pid;
QString execpath = sthd.path;
//句柄不存在,说明窗口被关闭了
if(hwmdmap.count(key)==0){
bool bret=false;
bool bpro=false;
for (auto &hkey : hwmdmap.keys()) {
HWND hwnd = hkey;
QString hw_execpath = hwmdmap[hwnd];
//窗口关闭之后,如果还能匹配当当前开启的窗口的执行路径一样,并且不在过滤清单以及之前的记录中时,对句柄进行迁移(有些应用打开多个窗口)
if(hw_execpath==execpath && m_ignorehwndlist.count(hwnd)==0 && m_hwndpidmap.count(hwnd)==0){//判断执行程序路径是否一样
m_hwndpidmap.remove(key);
sthd.hwnd=hwnd;
m_hwndpidmap[hwnd]=sthd;
bret=true;
quintptr old_valueAsUInt = reinterpret_cast<quintptr>(key);
quintptr valueAsUInt = reinterpret_cast<quintptr>(hwnd);
QString msg = QString("句柄迁移old hwnd:%1==>new hwnd:%2").arg(old_valueAsUInt).arg(valueAsUInt);
qDebug() << msg;
writeLog(msg);
break;
}
}
if(bret==false && bpro==false){
sendkillpid(pid);
m_hwndpidmap.remove(key);
quintptr valueAsUInt = reinterpret_cast<quintptr>(key);
QString msg = QString("sendkillpid hwnd:%1==>pid:%2").arg(valueAsUInt).arg(pid);
writeLog(msg);
}
}
}
//处理被忽略的窗口
for (auto it = m_ignorehwndlist.begin(); it != m_ignorehwndlist.end(); ) {
if(hwmdmap.count(*it)==0){
qDebug() << "忽略名单中的" << *it << "已经被释放";
it = m_ignorehwndlist.erase(it); // erase返回下一个有效的迭代器
} else {
++it;
}
}
}
设置定时器,及过滤已经打开的窗口
Mytcpserver::Mytcpserver(QObject *parent) : QObject(parent) {
m_bfirst_status=true;
getwinHwnd();//只取一次已经存在的窗口名单,这是被过滤的名单
m_bfirst_status=false;
qDebug() << "已经存在的窗口句柄:";
QString msg="已经存在的窗口句柄:\n";
for (const HWND &value : m_ignorehwndlist) {
//std::cout << value << std::endl;
qDebug() << "m_ignorehwndlist hwnd:" << value;
// 转换 HWND 为 quintptr
quintptr valueAsUInt = reinterpret_cast<quintptr>(value);
stHwnd sthd=m_ignorehwndmap[value];
msg += QString("%1 => %2 => %3\n").arg(valueAsUInt).arg(sthd.name).arg(sthd.path);
}
writeLog(msg);
m_settinghwnd=0;
addtimer = new QTimer(this);
addtimer->setInterval(1000);
connect(addtimer,&QTimer::timeout,this,&Mytcpserver::checkkillproc);
addtimer->start();
//m_peerIp="192.168.10.92";
}
结尾
本篇主要是根据项目需求,提供关键的解决思路及方案,不方便提供全部代码,希望能帮到有相关需求场景的读者。