程序测试环境是: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
cpp
#ifndef FORM_H
#define FORM_H
#include <QWidget>
#include <QProcess>
namespace Ui {
class Form;
}
class Form : public QWidget
{
Q_OBJECT
public:
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
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_class
std::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
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就能测试出我们确实正确关闭了窗口。