(Gtk window focus failed when opened more than one times in one thread)
一. 背景:
在做Linux取词翻译软件的时候,UI编程使用的是Gtk,翻译界面的逻辑代码被放置于某个线程内,途中会进行多次的创建与销毁,Gtk窗口除了第一次能够成功聚焦,之后就再也无法成功,写了短小的测试用例,发现每次进行进程创建可以保证窗口聚焦。
但是考虑到各个线程中众多的共享数据,另开进程进行数据通信的代价太过于大,主要是进程有独立的地址空间,彼此间通信要借助IPC的方式,而且无论是代码的重构还是之后的调试都将耗费巨大的精力。
二. 解决过程:
而后开始寻找解决办法,尝试了好几个小时在Gtk内部查找问题,换了或增加了各种各样的窗口聚焦相关代码,仍旧不可行。
接着想到了另一个突破口,平时Alt-tab键可以进行窗口切换,本质就是聚焦窗口的改变,巡着这条路想起xdotool好像有这个功能可以激活特定窗口(亦即聚焦窗口)。
然后在终端尝试了一下,果然,用 xdotool windowactivate <窗口id>
可以激活窗口!马上阅读源码,很快找到了相关实现的函数,在里面添加了些log的打印进行进一步定位,或者注释掉一些可能实际起作用的代码后,程序窗口激活功能即刻失效,由此得到几行关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
int xdo_activate_window(const xdo_t *xdo, Window wid) { int ret = 0; long desktop = 0; XEvent xev; XWindowAttributes wattr; if (_xdo_ewmh_is_supported(xdo, "_NET_ACTIVE_WINDOW") == False) { fprintf(stderr, "Your windowmanager claims not to support _NET_ACTIVE_WINDOW, " "so the attempt to activate the window was aborted.\n"); return XDO_ERROR; } /* If this window is on another desktop, let's go to that desktop first */ if (_xdo_ewmh_is_supported(xdo, "_NET_WM_DESKTOP") == True && _xdo_ewmh_is_supported(xdo, "_NET_CURRENT_DESKTOP") == True) { xdo_get_desktop_for_window(xdo, wid, &desktop); xdo_set_current_desktop(xdo, desktop); } memset(&xev, 0, sizeof(xev)); xev.type = ClientMessage; xev.xclient.display = xdo->xdpy; xev.xclient.window = wid; xev.xclient.message_type = XInternAtom(xdo->xdpy, "_NET_ACTIVE_WINDOW", False); xev.xclient.format = 32; xev.xclient.data.l[0] = 2L; /* 2 == Message from a window pager */ xev.xclient.data.l[1] = CurrentTime; printf("\033[0;32mwid=%d \033[0m\n", wid); printf("\033[0;31m窗口聚焦 \033[0m\n"); /* 聚焦关键代码*/ XGetWindowAttributes(xdo->xdpy, wid, &wattr); ret = XSendEvent(xdo->xdpy, wattr.screen->root, False, SubstructureNotifyMask | SubstructureRedirectMask, &xev); /* XXX: XSendEvent returns 0 on conversion failure, nonzero otherwise. * Manpage says it will only generate BadWindow or BadValue errors */ return _is_success("XSendEvent[EWMH:_NET_ACTIVE_WINDOW]", ret == 0, xdo); } |
观察后不难发现,其中最后几句中的XSendEvent基本可以被确定是核心代码。
1 2 3 4 |
ret = XSendEvent(xdo->xdpy, wattr.screen->root,\ False, SubstructureNotifyMask | SubstructureRedirectMask, &xev); |
问题是其中的参数应该如何获取?
1.如果了解一点点X11,应该可以猜出xdo->xdpy就是来自XOpenDisplay(), 当然这个可以回溯找到,这里就不找了。
2.wattr.screen->root来自XWindowAttributes结构体,此结构体的相关属性由以下函数获取:
1 2 3 |
XGetWindowAttributes(xdo->xdpy, wid, &wattr); |
这里的wid由函数参数可以看到是一个窗口id值, 来自Window变量。
3.位掩码大概率是宏定义,不用获取和更改,剩下xev由上文可知是一个XEvent结构体,相关属性的赋值可以不用修改,在代码中很清晰。
如此,我们需要的信息都已经明确,需要在Gtk编程中使用XOpenDisplay()连接到X服务,获取一个xdpy,然后定义两个结构体 —- XWindowAttributes, XEvent 进行属性赋值或者获取,最后调用如下函数使我们的窗口处于聚焦状态:
1 2 3 |
ret = XSendEvent(xdo->xdpy, wattr.screen->root, False, SubstructureNotifyMask | SubstructureRedirectMask, &xev); |
但是我们的窗口是Gtk,而不是X11,Window id如何获取是个问题,带着问题去网上搜索如何将Gtk转化为X11的Window id,很快找到了解决方案,如下:
1 2 3 4 |
GdkWindow *gw = gtk_widget_get_window ( GTK_WIDGET ( window ) ); Window wid = gdk_x11_window_get_xid ( gw ); #头文件 #include <gdk/gdkx.h> |
最后附上项目中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* 本函数代码借鉴自xdotool部分源码*/ void focusOurWindow( WinData *wd ) { /* Get window id of x11*/ GdkWindow *gw = gtk_widget_get_window ( GTK_WIDGET ( wd->window ) ); Window wid = gdk_x11_window_get_xid ( gw ); /* Get window's attributes*/ XWindowAttributes wattr; Display *dpy = XOpenDisplay (NULL); XGetWindowAttributes(dpy, wid, &wattr); XEvent xev; memset(&xev, 0, sizeof(xev)); xev.type = ClientMessage; xev.xclient.display = dpy; xev.xclient.window = wid; xev.xclient.message_type = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False); xev.xclient.format = 32; xev.xclient.data.l[0] = 2L; /* 2 == Message from a window pager */ xev.xclient.data.l[1] = CurrentTime; int ret = XSendEvent(dpy, wattr.screen->root, False, SubstructureNotifyMask | SubstructureRedirectMask, &xev); if ( ret == 0 ) { printf("\033[0;31m窗口聚焦请求失败(focusOurWindow) \033[0m\n"); } XCloseDisplay(dpy); } |
要解决聚焦问题请继续往下看,以上代码只是问题解决的一环,还差一环。
三. 窗口无法聚焦的原因分析尝试:
在使用该函数进行窗口聚焦的时候,发现虽然返回值显示窗口聚焦请求成功,但窗口依旧处于失焦状态,想到很有可能聚焦请求被其他程序所抢占。
程序在同一个线程第二次运行的时候,同样靠键盘输入调出界面。这时接收到键盘信号的终端或者其他应用,与翻译软件一同发送了聚焦请求,使翻译软件的请求被抢占,即使我们的程序聚焦成功过,但很快又被切换到的其他应用打断,看上去就是聚焦失败的效果。
既然我们可以后期请求聚焦,那这个就好办了,直接在运行后注册信号超时回调函数,多执行几次聚焦请求便可解决。
1 2 3 4 5 6 7 8 9 |
int <超时回调函数>(void *arg) { /* 每隔一定时间多次尝试重新聚焦窗口防止聚焦窗口被抢占*/ if ( focustimes++ <= 5 ) { focusOurWindow ( (WinData*)arg ); } } |
注意以上只是作者项目中的部分代码,并不完整,照搬是无用的,只是提供一个思路。