linux桌面软件(wps)内嵌到主窗口后的关闭问题
程序测试环境是:slackware系统,属于linux系统,有桌面(Xface Session)。系统镜像是:slackware64-15.0-install-dvd.iso。qt、c++代码实现。
问题描述:延续上一篇文章,将wps软件窗口内嵌到qt的窗口后,出现一个棘手的问题,就是无法正常的关闭wps进程,即使表面上关闭了,再次打开并内嵌到主窗口时,会出现打不开wps软件的问题,会发现有wps的僵尸进程。
必要条件:slackware系统里需要安装wps、qt5开发工具,本篇文章不做详述。
程序编译:编译还是和上一篇文章保持一致。
我直接说问题的原因,当我们通过fork打开wps软件时,linux系统会出现两个关于该软件的wps进程(wpspdf),通过linux命令查看下进程信息如下:
/office6/wpspdf 这个进程就是打开pdf文件的软件wpspdf进程。
/bin/bash/ 这个进程是wpspdf进程的父进程。qt主窗口创建该进程,该进程又创建打开pdf的进程。
这俩进程都必须正常退出,才能保证我们下一次正确的再次打开软件。
第一种情况:当我们关闭了wps的qt父窗口,而不关闭wps软件界面(比如通过wps右上角的那个关闭按钮)时,虽然wps软件界面没了,但是后台的两个进程都没有关闭,这就导致wps的软件进程处于一种异常状态,这就导致下次打开时,发现完全打不开软件了。
第二种情况:如上图,当我们关闭了wps软件(比如通过wps右上角的那个关闭按钮),也关闭了其qt父窗口,这时我们linux命令查看进程信息会发现,wps进程剩了一个,而且还是个僵尸进程。其实就是/bin/bash/ 那个进程变成了僵尸进程。主要原因就是我们qt主窗口在创建该进程时,我们没有做wait操作,所以该进程没有正常退出,成为了僵尸进程。(上面两张图的进程号不一样的原因是,这是两次测试,所以id值不一样)。
以下是我修改后的最终代码:
form.h
#ifndef FORM_H
#define FORM_H#include <QWidget>
#include <QProcess>namespace Ui {
class Form;
}class Form : public QWidget
{Q_OBJECTpublic:explicit Form(QWidget *parent = nullptr);~Form();void open_wps();static void open_wps_thread();void close_wps();void close_wps2();void find_window_id_in_tree();void find_window_id_by_class();void add_window_in_qt();private:Ui::Form *ui;QProcess *process;uint32_t window_id;
};#endif // FORM_H
form.cpp
#include "form.h"
#include "ui_form.h"
#include <QWindow>
#include <QVBoxLayout>
#include <QtX11Extras/QX11Info>
#include <xcb/xcb.h>
#include <QDebug>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <fstream>
#include <unistd.h>
#include <sys/wait.h>
#include <thread>Form::Form(QWidget *parent) :QWidget(parent),ui(new Ui::Form)
{ui->setupUi(this);
}Form::~Form()
{delete ui;std::cout << "Form deleted" << std::endl;
}void Form::open_wps_thread(){pid_t pid = fork();if(pid == 0){const char* wpsCommand[] = {"wpspdf", nullptr};//wps(word)、et(excel)、wpspdf(pdf)、wpp(ppt)std::cout << "in sub process:" << pid << std::endl;if(execvp(wpsCommand[0], (char* const*)wpsCommand) == -1){perror("failed to exec WPS");}}else if(pid > 0){ std::cout << "in main process:" << pid << std::endl;int status;waitpid(pid, &status, 0);}else{perror("fork failed");}
}// 打开默认软件
void Form::open_wps(){std::thread t(open_wps_thread);t.detach();
}// 这个函数不太好,只关闭了窗口,没关闭进程
void Form::close_wps(){// 连接到X服务器xcb_connection_t *connection = xcb_connect(NULL, NULL);const xcb_setup_t *setup = xcb_get_setup(connection);xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup);xcb_screen_t *screen = iter.data;// 创建一个cookie,用于请求服务器关闭窗口xcb_void_cookie_t cookie = xcb_destroy_window_checked(connection, window_id);// 等待请求完成xcb_generic_error_t *error = xcb_request_check(connection, cookie);if (error) {// 处理错误printf("Error code: %d\n", error->error_code);free(error);}// 断开连接xcb_disconnect(connection);
}void Form::close_wps2(){Display *display = XOpenDisplay(NULL);if (!display) {fprintf(stderr, "Cannot open display.\n");return;} // 创建一个临时窗口来获取默认的事件掩码Window root_return;int x_return, y_return;unsigned int width_return, height_return, border_width_return, depth_return;XGetGeometry(display, window_id, &root_return, &x_return, &y_return,&width_return, &height_return, &border_width_return, &depth_return);// 发送 WM_DELETE_WINDOW 消息XEvent event;memset(&event, 0, sizeof(event));event.type = ClientMessage;event.xclient.display = display;event.xclient.window = window_id;event.xclient.message_type = XInternAtom(display, "WM_PROTOCOLS", False);event.xclient.format = 32;event.xclient.data.l[0] = XInternAtom(display, "WM_DELETE_WINDOW", False);event.xclient.data.l[1] = CurrentTime; // 当前时间戳XSendEvent(display, window_id, False, NoEventMask, &event);XFlush(display);XCloseDisplay(display);
}// 通过系统窗口树形结构去一层层遍历窗口(我这里没有遍历子窗口,如果找不到某个窗口,或许是因为是子窗口)
void Form::find_window_id_in_tree()
{Display *display = XOpenDisplay(nullptr);if (!display) {std::cerr << "Cannot open display\n";return;}Window root = DefaultRootWindow(display);Window parent, *children;unsigned int num_children;if (!XQueryTree(display, root, &root, &parent, &children, &num_children)) {return;}std::vector<Window> windows;windows.push_back(root);for (unsigned int i = 0; i < num_children; ++i) {windows.push_back(children[i]);}XFree(children);for (Window win : windows) {char* name;int status = XFetchName(display, win, &name);if (status == 1) {// 这里的win就是窗口的id,也是窗口的句柄值find_window_id_by_classstd::cout << "Found window name: " << name << " " << win << std::endl;if (std::string(name).find("wpspdf") != std::string::npos) {
// XFree(name);
// return;}XFree(name);}// 递归检查子窗口(如果有的话)// 注意:这里为了简化示例,没有实现递归}
}// 将第三方软件(wps)窗口内嵌到qt窗口里
void Form::add_window_in_qt()
{QWindow *win = QWindow::fromWinId(window_id);QWidget *widget = QWidget::createWindowContainer(win);widget->setParent(this);QVBoxLayout *layout = new QVBoxLayout();layout->addWidget(widget);this->setLayout(layout);
}// 获取窗口id(句柄)(找到的另外一种比较快捷的拿到窗口句柄的方式)
void Form::find_window_id_by_class()
{Display* display = XOpenDisplay(NULL);if (!display) {std::cerr << "Failed to open display" << std::endl;return;}Atom netClientListAtom = XInternAtom(display, "_NET_CLIENT_LIST", False);Atom actualType;int format;unsigned long numItems, bytesAfter;unsigned char* data = NULL;int status = XGetWindowProperty(display, DefaultRootWindow(display),netClientListAtom, 0, ~0UL, False,AnyPropertyType,&actualType, &format, &numItems, &bytesAfter,&data);if (status == Success && actualType == XA_WINDOW) {Window* windows = reinterpret_cast<Window*>(data);for (unsigned long i = 0; i < numItems; ++i) {Window win = windows[i];Atom actualType;int format;unsigned long nitems;unsigned long bytes_after;unsigned char* prop_data = nullptr;// 获取WM_CLASS属性if (XGetWindowProperty(display, win, XInternAtom(display, "WM_CLASS", False), 0, 1024, False, XA_STRING,&actualType, &format, &nitems, &bytes_after, &prop_data) == Success) {std::string className(reinterpret_cast<char*>(prop_data));if (actualType == XA_STRING && className == "wpspdf") {std::cout << "Window class name for window " << win << ": " << className << std::endl;XFree(prop_data);window_id = win;}}}} else {std::cerr << "Failed to get window list property" << std::endl;}if (data != NULL) {XFree(data);}XCloseDisplay(display);
}
1、重点关注 open_wps_thread线程函数,在该函数里,waitpid(pid, &status, 0)行代码,就是qt主窗口单起个新线程来创建和等待wps的进程,这样该进程就能正常结束了。
2、重点关注close_wps2函数,该函数可以通过窗口id来正常的关闭wps(wpspdf)软件进程。
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "form.h"
#include <unistd.h>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::on_pushButton_clicked()
{if(!f){f = new Form();}f->show();f->open_wps();sleep(2);f->find_window_id_by_class();
}void MainWindow::on_pushButton_2_clicked()
{if(f){f->close_wps2();}
}//
void MainWindow::on_pushButton_3_clicked()
{if(f){// f->showFullScreen(); // 全屏显示f->add_window_in_qt();// f->find_window_id_in_tree();}
}
1、mainwindow.cpp也是对应到一个窗口,上面有三个按钮,分别用来调form.h的方法,用来测试的。先调用on_pushButton_clicked,再调用on_pushButton_3_clicked,最后调用on_pushButton_2_clicked就能测试出我们确实正确关闭了窗口。