Linux桌面X11服务-XRecord方案捕获鼠标点击的应用窗口

linux下获取鼠标点击应用的窗口信息,进程信息

查询服务器类型:

shell 复制代码
echo $XDG_SESSION_TYPE

一般有x11和Wayland

查询桌面类型:

shell 复制代码
echo $XDG_CURRENT_DESKTOP

一般有GNome和KDE,国产有deepin,方案对比

对比项 X11 方案(tdisplay 用的) AT-SPI 方案(你要做的)
实现层级 底层窗口管理系统 高层 GUI 无障碍接口
获取对象 只能看到窗口、进程、类名 可以看到每个控件、文本内容
精确度 只能识别窗口 可识别按钮、文本框、菜单项等
依赖库 libX11, libXtst, libXi libatspi, libatk-bridge, D-Bus
性能 快、无需 Accessibility 稍慢、依赖 D-Bus 服务
使用场景 审计系统、录屏、操作跟踪 屏幕阅读器、自动化测试

两个查询窗口id信息的工具命令

shell 复制代码
xwininfo 捕获窗口,类似spi++
xprop -id wid  查询指定窗口id的信息

接下来介绍X11的方案实现:

shell 复制代码
[鼠标点击事件]
       │
       ▼
X Server 生成事件
       │
       │
       └─ 我们使用 XRecord 捕获
                  │   (得到 root_x, root_y)
                  ▼
           坐标定位目标窗口
                  │
                  ▼
         获取鼠标点击的顶层窗口,判断root_x, root_y是否在范围内
                  │
                  ▼
       读取 _NET_WM_PID / _NET_WM_NAME
                  │
                  ▼
         获取应用程序名称 & 窗口属性

代码实现

c++ 复制代码
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <queue>
#include <atomic>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/XTest.h>
#include <X11/extensions/record.h>
#include <X11/extensions/Xfixes.h>
#include <X11/Xproto.h>
#include <X11/Xmd.h>
#include <X11/Xatom.h>
#include <unistd.h>
#include <vector>
#include <pwd.h>
#include "scopedtimer.h"

std::vector<Window> get_all_windows(Display*display);
// ==================== 鼠标点击信息结构 ====================
struct ClickEvent {
    unsigned char button;  // 鼠标按钮号
    short root_x; //鼠标点击坐标x
    short root_y;  //鼠标点击坐标y
    Window wid;  //窗口id
    std::string window_title; //窗口
    pid_t pid;//进程id
    std::string pid_name;//进程名
};

std::vector<Window> global_windows;
// 事件队列与互斥锁
std::queue<ClickEvent> click_queue;
std::mutex queue_mutex;
std::atomic<bool> should_exit(false);

Display *global_display = nullptr;
Display *global_display_event = nullptr;
// ==================== 获取窗口标题 ====================
std::string get_window_title(Display *display, Window window){
    if(!window) return "";
    Atom utf8_string = XInternAtom(display, "UTF8_STRING",True);
    Atom net_wm_name = XInternAtom(display, "_NET_WM_NAME", True);
    if(net_wm_name!=None){
        Atom actual_type;
        int actual_format;
        unsigned long nitems, bytes_after;
        unsigned char *prop = nullptr;
        if(XGetWindowProperty(
                    display,
                    window,
                    net_wm_name,
                    0,
                    (~0L),
                    False,
                    utf8_string!=None?utf8_string:XA_STRING,
                    &actual_type,
                    &actual_format,
                    &nitems,
                    &bytes_after,
                    &prop
                    ) == Success && prop){
            std::string title(reinterpret_cast<char*>(prop),nitems);
            XFree(prop);
            return title;
        }
    }
    XTextProperty prop;
    if(XGetWMName(display, window,&prop) && prop.value){
        std::string title(reinterpret_cast<char*>(prop.value));
        XFree(prop.value);
        return title;
    }
    return "";
}

//return 非0值表示成功
pid_t get_window_pid(Display *display, Window window){
    pid_t pid;
    if(!window) return 0;

    Atom net_wm_pid = XInternAtom(display, "_NET_WM_PID", True);
    if(net_wm_pid!=None){
        Atom actual_type;
        int actual_format;
        unsigned long nitems, bytes_after;
        unsigned char *prop = nullptr;
        if(XGetWindowProperty(
                    display,
                    window,
                    net_wm_pid,
                    0,
                    (~0L),
                    False,
                    XA_CARDINAL,
                    &actual_type,
                    &actual_format,
                    &nitems,
                    &bytes_after,
                    &prop
                    ) == Success && prop){
            pid = *(unsigned long *)prop;
            XFree(prop);
            return pid;
        }
    }
    return 0;
}

std::string get_proc_name(pid_t pid){
    if(pid <= 0)
        return "";
    std::string path = "cat /proc/" + std::to_string(pid) + "/cmdline";
    std::string proc_name;
    FILE *fstream = nullptr;
    char buff[1024];
    memset(buff, 0, sizeof(buff));
    if(NULL != (fstream = popen(path.c_str(), "r")))
    {
        while(NULL!=fgets(buff, sizeof(buff), fstream))
        {
            proc_name += buff;
        }
    }
    if(fstream!=nullptr)
        pclose(fstream);

    return proc_name;
}
Window find_window_at(Display* display, int x, int y){
   //std::vector<Window> windows = get_all_windows(display);
    std::vector<Window> windows = global_windows;
    for(Window win : windows){
        XWindowAttributes attr;
        if(!XGetWindowAttributes(display, win, &attr) || attr.map_state != IsViewable){
            continue;
        }

        int win_x, win_y;
        Window child;
        XTranslateCoordinates(display, win, DefaultRootWindow(display),0,0,&win_x,&win_y,&child);
        if(x >= win_x && x<= win_x + attr.width && y>=win_y &&y<=win_y+attr.height){
            return win;
        }
    }
    return 0;
}

std::vector<Window> get_all_windows(Display*display){
    std::vector<Window> windows;
    Atom client_list_atom = XInternAtom(display, "_NET_CLIENT_LIST_STACKING", false);

    if(client_list_atom != None){
        Atom actual_type;
        int actual_format;
        unsigned long nitems, bytes_after;
        unsigned char *prop = NULL;
        int result = XGetWindowProperty(
                    display,
                    DefaultRootWindow(display),
                    client_list_atom,
                    0,
                    (~0L),
                    False,
                    XA_WINDOW,
                    &actual_type,
                    &actual_format,
                    &nitems,
                    &bytes_after,
                    &prop
                    );
        if(result == Success && prop){
            Window *data = reinterpret_cast<Window*>(prop);
            for(long i=nitems - 1; i>=0; i--){
                Window w = data[i];
                Atom wm_window_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE",False);
                Atom desktop_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DESKTOP",False);
                Atom ddk_dock_type = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DOCK", False);
                Atom actual_type2;
                int actual_format2;
                unsigned long nitems2, bytes_after2;
                Atom *type_data = nullptr;
                if(XGetWindowProperty(
                            display,
                            w,
                            wm_window_type,
                            0,
                            32,
                            False,
                            XA_ATOM,
                            &actual_type2,
                            &actual_format2,
                            &nitems2,
                            &bytes_after2,
                            reinterpret_cast<unsigned char**>(&type_data)
                            ) == Success){
                    bool is_desktop = false;
                    for(unsigned long j = 0; j < nitems2; j++){
                        if(type_data[j] == desktop_type || type_data[j] == ddk_dock_type){
                            is_desktop = true;
                            break;
                        }
                    }
                    XFree(type_data);
                    if(!is_desktop){
                        windows.emplace_back(w);
                    }
                }
            }
            XFree(prop);
        }
    }
    return windows;
}

std::string get_event_button(int btn_no){
    const char* btn_name = "未知";
    switch (btn_no) {
        case 1: btn_name = "左键"; break;
        case 2: btn_name = "中键"; break;
        case 3: btn_name = "右键"; break;
        case 4: btn_name = "滚轮上"; break;
        case 5: btn_name = "滚轮下"; break;
    }
    return btn_name;
}
// ==================== 队列线程处理鼠标点击 ====================
void click_processor_thread() {
    while (!should_exit.load()) {
        ClickEvent ev;
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            if (click_queue.empty()) {
                std::this_thread::sleep_for(std::chrono::milliseconds(5));
                continue;
            }
            ev = click_queue.front();
            click_queue.pop();
        }
        printf("处理鼠标事件开始,队列还有事件个数 %ld 个\n",click_queue.size());
        fflush(stdout);
        scoped_timer s_timer("获取鼠标事件,处理时间 ");
        Window target_window = 0;
        if((target_window = find_window_at(global_display_event, ev.root_x, ev.root_y))!=0){
            printf("\n=== 鼠标点击事件 ===\n");
            printf("点击了窗口中 0x%lx\n",target_window);

            std::string title = get_window_title(global_display_event, target_window);
            printf("窗口标题:%s\n",title.c_str());
            pid_t pid = get_window_pid(global_display_event, target_window);
            std::string proc_name = get_proc_name(pid);
            printf("窗口关联进程id: %d, 进程名:%s\n", pid, proc_name.c_str());
            fflush(stdout);
        }
    }
}

// ==================== XRecord 回调 ====================
extern "C" void event_callback(XPointer closure, XRecordInterceptData *data) {
    if (data->category != XRecordFromServer) {
        XRecordFreeData(data);
        return;
    }
    static int click_count = 0;
    const unsigned char* d = data->data;
    xEvent *xevent = reinterpret_cast<xEvent*>(data->data);
    if (data->data_len < 2) {
        XRecordFreeData(data);
        return;
    }

    if (xevent->u.u.type == ButtonPress && xevent->u.u.detail == 1) { // ButtonPress 只捕获左键按下

        printf("鼠标 [%s] 点击了...%d\n",get_event_button(xevent->u.u.detail).c_str(), click_count++);
        //回调函数获取坐标
        short x = xevent->u.keyButtonPointer.rootX;
        short y = xevent->u.keyButtonPointer.rootY;
        printf("获取鼠标点击坐标(%d, %d)...\n",x, y);

        std::lock_guard<std::mutex> lock(queue_mutex);
        ClickEvent event = {
            .button = xevent->u.u.type,
            .root_x = x,
            .root_y = y
        };
        click_queue.push({event});
        fflush(stdout);
    }

    XRecordFreeData(data);
}

void print_window_id(Window wid){
    Window root, parent;
    Window *children;
    unsigned int nchildren;

    int status = XQueryTree(global_display, wid, &root, &parent, &children, &nchildren);
    if(status){
        printf("根窗口: 0x%lx\n", root);
        printf("父窗口: 0x%lx\n", parent);
        for (unsigned int i=0; i<nchildren; i++){
            printf("子窗口 %d: 0x%lx\n", i, children[i]);
            char * window_name;
            int ret = XFetchName(global_display, children[i], &window_name);
            if(ret){
                printf("子窗口 %d, 标题: %s: 0x%lx\n", i, window_name, children[i]);
            }
        }
        fflush(stdout);
        XFree(children);
    }
    else{
        fprintf(stderr, "查询窗口树失败\n");
    }
    return ;
}
// ==================== main ====================
int main() {
    Display *display = XOpenDisplay(NULL);
    if (!display) {
        std::cerr << "❌ 无法打开 X11 显示" << std::endl;
        return 1;
    }
    global_display = display;
    global_display_event = XOpenDisplay(NULL);
    if (!global_display_event) {
        std::cerr << "❌ 无法打开 X11 global_display 显示" << std::endl;
        return 1;
    }

    int major, minor;
    if (!XRecordQueryVersion(display, &major, &minor)) {
        std::cerr << "❌ XRecord 扩展不可用" << std::endl;
        XCloseDisplay(display);
        return 1;
    }

    XRecordRange *range = XRecordAllocRange();
    if (!range) {
        std::cerr << "❌ 无法分配 XRecordRange" << std::endl;
        XCloseDisplay(display);
        return 1;
    }
    memset(range, 0, sizeof(XRecordRange));
    //只记录鼠标按下的事件
    range->device_events.first = ButtonPress; // 4 ButtonPress
    range->device_events.last  = ButtonPress; // 5 ButtonRelease

    XRecordClientSpec client = XRecordAllClients;
    XRecordContext context = XRecordCreateContext(display, 0, &client, 1, &range, 1);
    if (!context) {
        std::cerr << "❌ 无法创建 XRecord 上下文" << std::endl;
        XFree(range);
        XCloseDisplay(display);
        return 1;
    }

    XFree(range);
    XSync(display, False);

    std::cout << "🖱️ 鼠标点击监听器已启动..." << std::endl;
    std::cout << "点击任意窗口获取信息,按 Ctrl+C 退出" << std::endl;

    // 启动线程处理队列
    std::thread processor(click_processor_thread);

    global_windows = get_all_windows(display);
    for(int i=0; i<global_windows.size(); i++){
        printf("窗口%d,id:0x%lx\n", i, global_windows[i]);
    }
    fflush(stdout);

    // 阻塞监听
    XRecordEnableContext(display, context, event_callback, NULL);

    // 程序退出
    should_exit.store(true);
    processor.join();

    XRecordFreeContext(display, context);
    XCloseDisplay(display);
    return 0;
}

编译命令

shell 复制代码
g++ -o ui_ctl ui_display.cpp  -lX11 -lXtst -lpthread

运行结果

相关推荐
梦昼初DawnDream4 小时前
防火墙规则设置
linux·服务器·windows
LXY_BUAA4 小时前
Linux常见命令
linux·运维·服务器
R&ain4 小时前
Linux目录结构
linux·运维·服务器
帅得不敢出门6 小时前
Linux服务器编译android报no space left on device导致失败的定位解决
android·linux·服务器
用户31187945592186 小时前
申威服务器安装Java11(swjdk-11u-9.ky10.sw_64.rpm)详细操作步骤(附安装包)
linux
ajassi20006 小时前
开源 Linux 服务器与中间件(十一)Emqx服务器消息的订阅和发送(mqtt测试)
linux·服务器·开源
平生不喜凡桃李7 小时前
Linux网络层:IP
linux·运维·tcp/ip
阿方索7 小时前
虚拟化技术实践指南:KVM 与 VMware ESXi 部署全流程
linux·运维·服务器
YouEmbedded8 小时前
解码Linux文件IO之库的制作与应用
linux·动态库·静态库