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_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

#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

#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就能测试出我们确实正确关闭了窗口。

上一篇:曾黎亮相巴黎时装周,状态超绝!将中国茶文化带出国门


下一篇:【C++指南】类和对象(二):类的默认成员函数——全面剖析 :构造函数