主页 > 创业  > 

【QT常用技术讲解】国产Linux桌面系统+window系统通过窗口句柄对窗口进行操作

【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"; } 结尾

        本篇主要是根据项目需求,提供关键的解决思路及方案,不方便提供全部代码,希望能帮到有相关需求场景的读者。

标签:

【QT常用技术讲解】国产Linux桌面系统+window系统通过窗口句柄对窗口进行操作由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【QT常用技术讲解】国产Linux桌面系统+window系统通过窗口句柄对窗口进行操作