在实现OSD Lyrics的滚动模式的时候,我发现在拖动改变窗口大小时,窗口内容会变得支离破碎。进一步实验发现,只要是直接在窗口上绘图,必然会导致这个结果。例如下面的python代码:
在OSD Lyrics中,我使用以下代码,来实现鼠标穿透功能:
gdk_drawable_get_size (osd->osd_window, &w, &h); GdkPixmap *input_mask = gdk_pixmap_new (NULL, w, h, 1); GdkGC *gc = gdk_gc_new (input_mask); GdkColor color; color.pixel = 0; /* black */ gdk_gc_set_foreground (gc, &color); gdk_draw_rectangle (input_mask, gc, TRUE, 0, 0, w, h); gdk_window_input_shape_combine_mask (osd->osd_window, input_mask, 0, 0); g_object_unref (input_mask); g_object_unref (gc);
简单来说,就是生成一个w*h的黑白pixmap,用绘制矩形的方法将它的所有像素置零,然后将它作为OSD窗口的input shape mask,使得在其上的所有鼠标事件都不会被OSD窗口捕捉。
这段代码在之前一直工作很正常,但是最近有人报告说穿透功能失效了。然而这项功能在我的Ubuntu 9.10上很正常,于是我怀疑是与Gnome 2.30自带的GTK 2.20有兼容性问题。
最近把系统升级到了10.04,GTK升级到了 2.20,果然问题出现了。无论color的pixel设为0还是1,都没法产生效果。进一步实验发现,如果去掉gdk_draw_rectangle一句,也就是生成pixmap后不绘制任何东西,那么input mask是可以生效的。一旦在上面绘制矩形,不论怎么绘制,绘制的部分都会没有穿透效果,也就是说,被绘制的部分成了有遮挡的标记。
但是不能直接把绘制矩形的部分去掉,因为在低版本的GTK中,gdk_pixmap_new返回的pixmap并不是所有像素都置零的,也就是mask是一个随机的效果,这也是为什么我在实现的时候要画个矩形。一个比较丑陋的解决方法就是判断GTK的版本,如果版本小于2.20,就绘制矩形,否则不绘制。这个方法很恶心,而且天知道新的GTK出来后会不会出问题,至少在文档中没有对gdk_pixmap_new返回的pixmap作任何保证。
后来想到notify-osd在lucid下鼠标穿透的功能依然正常,查看了它的代码,发现对input_shape部分进行了大幅地修改,参考之后代码修改为如下形式:
GdkRegion *region = gdk_region_new (); gdk_window_input_shape_combine_region (osd->osd_window, region, 0, 0); gdk_region_destroy (region);
就是创建一个空区域,然后将它作为input shape,因为是空区域,所以整个OSD窗口都是镂空的。
虽然问题解决了,但是还是不明白,为什么之前的方法在GTK 2.20上不能用了(应该说是在GDK 2.20上不能用了),2.20在input shape mask和pixmap上有什么改动,上面的代码要作什么改动才能正常工作呢?如果有朋友知道,请告诉我:)
最近写了些GTK+的Widget,稍微了解了一下其内部结构,先记录下来,用以备忘。
GtkWidget的基本结构是这样的:
其中最重要的是它的window属性,每个GtkWidget都必须有一个window。Widget是围绕着window转的,只有有了window,Widget的存在才有意义。
要注意这里的window是一个GdkWindow,而不是GtkWindow。GdkWindow是对X的window的封装,大致上是屏幕上的一块矩形区域,可以在上面画画,可以接收事件。
一个Widget从创建、显示到销毁,大致要经过这么几个过程:
这是调用gtk_xxx_new时所触发的。它干的活很简单,用gobject的对象系统创建一个相应widget的实例。
当创建实例时,gobject会自动调用指定的初始化(init)函数(在get_type时指定),init函数负责把widget的各字段都初始化(把标题文字什么的设为NULL之类的)。
注意此时window并没有被创建,其实只是有了个widget的架子而已。
创建之后就可以对widget进行各种属性的设置了。
实例化的过程,就是将window创建出来的过程。这其中包括几个阶段:
所谓映射,就是将已经创建好的window映射(显示)到屏幕上。需要做的事是用gdk_window_show将window给显示出来。和实例化时类似,需要用GTK_WIDGET_SET_FLAGS设置GTK_MAPPED标志,表示已经映射好了。
要注意的是map时需要判断widget是否已经实例化(用GTK_REALIZED),如果没有,应该首先实例化widget,这样才能显示window。
同样可以用gtk_widget_map手动映射一个widget。
用gtk_widget_show来显示一个widget的本质,就是将widget实例化,并将其映射。当然每一步都要判断是否已经做过,重复实例化和映射会造成资源泄漏(window被多次创建)和其他问题。
以上就是一个widget从创建到显示的过程。当然其中还有其父widget的流程。一个widget当且仅当其父widget被实例化后才能实例化,映射亦然(放心,这个流程是GTK+自动判断的)
接下来就是销毁一个widget时要做的事了。
当隐藏一个widget时,其实就是取消这个widget的映射。具体做法是用gtk_window_hide来隐藏window,并用GTK_WIDGET_UNSET_FLAGS来取消(GTK_MAPPED)。
销毁一个widget之前会自动要求将其反实例化。反实例化就是将window给销毁(记得把window指针设回NULL),并取消(GTK_REALIZED)标志。
有时可能会需要用gtk_widget_unrealize来手动反实例化一个widget。
和new对应,把剩下的资源释放,最后用gobject的相应函数释放整个widget
下面是取自GtkEntry中的典型代码:
创建:
大小分配:
大小请求:
实例化:
映射:
反映射:
反实例化:
销毁:
三月实习回来之后就有点手痒痒,和舍友sarlmolapple一起说想做个自由小项目,一则练习一下Linux下的编程,二则有点项目经验,三则可以自己用。敲定的项目是做桌面歌词,也就是在屏幕上显示的OSD歌词,我们规划了一下,基本应该具备的功能有:
项目用C来编写,基于GTK+。死党simplyzhao听了之后也很有兴趣,加入了进来。
其实现在已经有了挺不错的外挂歌词脚本lrcdis,为什么我们还要弄一个呢?其一是因为想自己做点东西,丰富经验,而做项目最大的动力就是自己要用。其二是因为lrcdis的OSD功能还很弱,而且由于是用bash脚本写的,应该很难增强(lrcdis是调用gnome-osd来实现OSD的)。
至于为什么用C,其实我觉得这个项目用Python来写会比较好,用C的唯一理由是……大家都想好好学学C-_-|||
其实这玩意计划了挺久了,现在原形设计得差不多了,技术问题也基本解决了,才敢公开出来。等到有可用代码的时候我会把项目地址给放出来的^ ^
写OSD控件时希望能实现这么种操作:平时OSD是镂空显示在屏幕上,但是可以用鼠标移动。一旦鼠标移动到文字的包围矩形之后,显示它的包围矩形作为OSD的背景(也就是说,不镂空了),以提示用户可以移动它。
实现的思路很简单。有三个窗口,第一个是显示OSD的窗口,它是不规则形状的,因此它对鼠标事件的感应也是不规则形状的,这就使得我们要创建第二个窗口,它是一个透明窗口,窗口大小就是OSD的包围矩形,用来接收鼠标事件,一旦接收到了鼠标事件,我们就显示第三个窗口,也就是背景窗口。注意这里的窗口是GdkWindow,而不是GtkWindow。这三个窗口都是顶层窗口,而且是弹出式窗口,在所有工作区都处于最顶端,不会被窗口管理器管理,也就是使用GTK_WINDOW_POPUP创建的那种窗口。一般来说,输入法啊弹出气泡啊都是弹出窗口,OSD也不例外。
创建一个GdkWindow需要两个参数:GdkWindowAttr结构的attr,表示窗口的各项属性;GdkWindowAttributesType枚举的attr_mask,标志创建时考虑attr中的哪些成员的值。
一般来说,创建一个弹出窗口的关键属性是:
可有可无的有:
于是attr_mask一般为:
表示创建时需要考虑x、y、visual、colormap的值(其他的是一定会考虑的,这也说明了这四个值是可有可无的)
设置好了属性之后就可以创建窗口了:
在成功创建一个顶层可见窗口后,再来看看透明窗口怎么弄的。要让一个窗口不可见,只需要把wclass属性改为GDK_INPUT_ONLY就行了。GDK_INPUT_ONLY表示这个窗口只接受用户交互事件,不会在屏幕上绘制任何东西。由于不会显示任何东西,visual和colormap属性也没有用了,attr_mask只需要为GDK_WA_X | GDK_WA_Y。
这样修改了之后,对于子窗口是没问题的,但是对于顶层窗口就不对劲了。如果把上面的代码这作出这些修改,在compiz下运行时会发现窗口会略往下偏。改用metacity下更严重,窗口的装饰(也就是边框啊标题栏啊什么的)被显示出来了,而且任务栏上有这个窗口的相应图标。首次移动后位置也不对。这样是完全不能达到我的要求的。
翻了翻GTK的文档,发现了这么一个控件:GtkInvisible,就是一个透明的窗口。查看源代码发现对于顶层窗口,还需要设置override_redirect为TRUE,同时attr_mask也要相应加上GDK_WA_NOREDIR,即:
改成这样后,就可以正常使用顶层透明窗口了。override_redirect指定窗口管理器忽略这个窗口,这样就不会附加窗口装饰,也不会显示在任务栏上了。
如果要用 GTK+ 写一些自定义界面的程序(如QQ),免不了要实现一个背景透明的 widget。一个简单的办法是GtkEventBox+GtkImage,不过这种方法太丑陋了点。然而用通常的方法来创建GTK+ widget 的话,会自动给背景填上颜色,设置了 gdk_window_set_back_pixmap (window->window, NULL, FALSE); 也不行。