最近写了些GTK+的Widget,稍微了解了一下其内部结构,先记录下来,用以备忘。
GtkWidget的基本结构是这样的:
typedef struct {
GtkStyle *GSEAL (style);
GtkRequisition GSEAL (requisition);
GtkAllocation GSEAL (allocation);
GdkWindow *GSEAL (window);
GtkWidget *GSEAL (parent);
} GtkWidget;
其中最重要的是它的window属性,每个GtkWidget都必须有一个window。Widget是围绕着window转的,只有有了window,Widget的存在才有意义。
要注意这里的window是一个GdkWindow,而不是GtkWindow。GdkWindow是对X的window的封装,大致上是屏幕上的一块矩形区域,可以在上面画画,可以接收事件。
一个Widget从创建、显示到销毁,大致要经过这么几个过程:
1、创建(new)
这是调用gtk_xxx_new时所触发的。它干的活很简单,用gobject的对象系统创建一个相应widget的实例。
当创建实例时,gobject会自动调用指定的初始化(init)函数(在get_type时指定),init函数负责把widget的各字段都初始化(把标题文字什么的设为NULL之类的)。
注意此时window并没有被创建,其实只是有了个widget的架子而已。
创建之后就可以对widget进行各种属性的设置了。
2、实例化(realize)
实例化的过程,就是将window创建出来的过程。这其中包括几个阶段:
- 询问大小请求(size_request):
GTK在实例化一个widget之前,会询问这个widget希望的大小是多大。widget可以根据自己的情况(例如属性什么的),计算出自己所需要的大小,也可以返回一个默认值,反正就是widget自己定啦:)。
- 分配大小(size_allocate):
GTK获得大小请求后就会给widget分配一个大小。要注意的是分配的大小不一定和请求的大小相同。一般来说,在分配大小时widget需要做几件事。
将分配的大小记录在自己的allocation中。
如果自己的window已经创建了,那么要改变自己所拥有的window的大小,使之符合所分配的大小。
如果widget是一个容器(container),那么对其所有的子widget也要相应地计算它们的大小并重新给它们分配大小。
分配大小可能发生在实例化之前,也可能在实例化后因为所属容器的大小、位置发生变化而被重新分配,因此widget的window可能已经被创建,也可能是NULL,需要进行判断。
- 实例化
这才是真正的实例化阶段。实例化所需要做的事只有一个:用gdk_window_new创建window。创建好window后需要用GTK_WIDGET_SET_FLAGS来给widget设置GTK_REALIZED标志。设置之后用GTK_REALIZED宏检查widget是否已经被实例化时会返回TRUE,表示该widget已经被实例化了。
可以用gtk_widget_realize手动实例化一个widget
3、映射(map)
所谓映射,就是将已经创建好的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时要做的事了。
4、反映射(unmap)
当隐藏一个widget时,其实就是取消这个widget的映射。具体做法是用gtk_window_hide来隐藏window,并用GTK_WIDGET_UNSET_FLAGS来取消(GTK_MAPPED)。
5、反实例化(unrealize)
销毁一个widget之前会自动要求将其反实例化。反实例化就是将window给销毁(记得把window指针设回NULL),并取消(GTK_REALIZED)标志。
有时可能会需要用gtk_widget_unrealize来手动反实例化一个widget。
6、销毁(destroy)
和new对应,把剩下的资源释放,最后用gobject的相应函数释放整个widget
下面是取自GtkEntry中的典型代码:
创建:
GtkWidget*
gtk_entry_new (void)
{
/* 返回类型为GTK_TYPE_ENTRY的对象(Gobject的工作) */
return g_object_new (GTK_TYPE_ENTRY, NULL);
}
/* 初始化函数,在g_object_new时自动调用 */
static void
gtk_entry_init (GtkEntry *entry)
{
GtkEntryPrivate *priv = GTK_ENTRY_GET_PRIVATE (entry);
/* 设置widget标识 */
GTK_WIDGET_SET_FLAGS (entry, GTK_CAN_FOCUS);
/* 初始化各字段 */
entry->text_size = MIN_SIZE;
entry->text = g_malloc (entry->text_size);
entry->text[0] = '\0';
/* …… */
/* 设置拖放 */
gtk_drag_dest_set (GTK_WIDGET (entry),
GTK_DEST_DEFAULT_HIGHLIGHT,
NULL, 0,
GDK_ACTION_COPY | GDK_ACTION_MOVE);
gtk_drag_dest_add_text_targets (GTK_WIDGET (entry));
/* 输入法context */
entry->im_context = gtk_im_multicontext_new ();
/* 信号 */
g_signal_connect (entry->im_context, "commit",
G_CALLBACK (gtk_entry_commit_cb), entry);
/* …… */
}
大小分配:
static void
gtk_entry_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
{
GtkEntry *entry = GTK_ENTRY (widget);
/* 保存到allocation中 */
widget->allocation = *allocation;
/* 判断是否实例化 */
if (GTK_WIDGET_REALIZED (widget))
{
/* 计算窗口大小…… */
/* 改变窗口大小 */
gdk_window_move_resize (widget->window, x, y, width, height);
/* …… */
}
}
大小请求:
static void
gtk_entry_size_request (GtkWidget *widget,
GtkRequisition *requisition)
{
/* 计算所需大小…… */
/* 设置所城大小 */
if (entry->width_chars < 0)
requisition->width = MIN_ENTRY_WIDTH + xborder * 2 + inner_border.left + inner_border.right;
else
{
/* …… */
requisition->width = char_pixels * entry->width_chars + xborder * 2 + inner_border.left + inner_border.right;
}
requisition->height = PANGO_PIXELS (entry->ascent + entry->descent) + yborder * 2 + inner_border.top + inner_border.bottom;
/* …… */
}
实例化:
static void
gtk_entry_realize (GtkWidget *widget)
{
/* …… */
/* 设置标志 */
GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED);
/* …… */
/* 创建window */
widget->window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask);
gdk_window_set_user_data (widget->window, entry);
/* …… */
}
映射:
static void
gtk_entry_map (GtkWidget *widget)
{
/* …… */
/* 判断是否可以且需要显示 */
if (GTK_WIDGET_REALIZED (widget) && !GTK_WIDGET_MAPPED (widget))
{
/* 调用父类的map函数,也就是GtkWidget的,这样就不用自己设置GTK_MAPPED和显示widget->window了 */
GTK_WIDGET_CLASS (gtk_entry_parent_class)->map (widget);
/* …… */
/* 显示需要显示的window */
gdk_window_show (icon_info->window);
/* …… */
}
反映射:
static void
gtk_entry_unmap (GtkWidget *widget)
{
/* …… */
/* 判断是否需要隐藏 */
if (GTK_WIDGET_MAPPED (widget))
{
/* …… */
/* 隐藏需要显示的window */
gdk_window_hide (icon_info->window);
/* …… */
/* 调用父类的unmap函数,也就是GtkWidget的,这样就不用自己取消GTK_MAPPED和隐藏widget->window了 */
GTK_WIDGET_CLASS (gtk_entry_parent_class)->unmap (widget);
}
}
反实例化:
static void
gtk_entry_unrealize (GtkWidget *widget)
{
/* …… */
/* 调用父类的unrealize函数来销毁widget->window和取消GTK_REALIZED标识 */
GTK_WIDGET_CLASS (gtk_entry_parent_class)->unrealize (widget);
/* …… */
}
销毁:
static void
gtk_entry_destroy (GtkObject *object)
{
/* 销毁为成员分配的空间…… */
/* 用父类的object销毁函数自动调用gobject来销毁 */
GTK_OBJECT_CLASS (gtk_entry_parent_class)->destroy (object);
}
今天终于搞定了osd-lyrics对复合窗口管理器的支持,终于可以使用其RGBA半透明功能来平滑歌词的边缘了
可惜在不支持复合的窗口管理器(metacity等)上还是很难看的锯齿边缘。这是外部限制,没办法了
上图一张,哈哈

之前我说我和两个同学在做一个Linux下做一个OSD歌词显示的软件,明天终于有了第一个能用的版本。所谓能用,就是说可以与播放器交互,根据播放的时间与歌曲来显示歌词极其进度。
当然,现在还非常简陋,画出来的歌词还不好看,无法设置,歌词也只能从指定目录以指定格式读取,播放器只支持Banshee,无法自动下载歌词,还有N多的Bug……
Anyway,至少使用了自己的作品,心里还是很有成就感的
废话少说,我说过有可用代码后会放出项目地址的:
http://code.google.com/p/osd-lyrics/
附上一张截图:

现在的一些限制:
- 仅支持Banshee
- 歌词必须手动放在~/.lyrics目录里,以“歌手名-歌名.lrc”的形式命名,例如“王菲-红豆.lrc”
- 歌词必须是UTF8编码
下一步的计划:
- 美化文字渲染
- 增加支持的播放器(首先考虑AmarOK1.4,因为simplyzhao在用,哈哈)
- 自动下载歌词
- 消除太部分的Bug
之后的计划:
之后的之后……
争取在毕业前能做出0.1版,嗯嗯
看到simplyzhao配置好了他的vim,我也忍不住要配置一下我的emacs了,目前的目标是让我的emacs在程序开发上更顺手
暂时先学学这些吧:
- [c/e]tags
- cscope
- cc-mode
- doxymacs
三月实习回来之后就有点手痒痒,和舍友sarlmolapple一起说想做个自由小项目,一则练习一下Linux下的编程,二则有点项目经验,三则可以自己用。敲定的项目是做桌面歌词,也就是在屏幕上显示的OSD歌词,我们规划了一下,基本应该具备的功能有:
- 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中的哪些成员的值。
一般来说,创建一个弹出窗口的关键属性是:
//设置窗口类型为TEMP,这样创建的就是弹出窗口了(GTK_WINDOW_POPUP类型的窗口和菜单项都是这种类型窗口)
attr.window_type = GDK_WINDOW_TEMP;
//设置窗口类为输入输出,这样才会在屏幕上显示出来
attr.wclass = GDK_INPUT_OUTPUT;
//设置窗口的大小和坐标
attr.x = widget->allocation.x;
attr.y = widget->allocation.y;
attr.width = widget->allocation.width;
attr.height = widget->allocation.height;
可有可无的有:
//colormap和visual,这两个是和显示与色彩有关的属性
attr.visual = gtk_widget_get_visual (widget);
attr.colormap = gtk_widget_get_colormap (widget);
//以及接收的事件
attr.event_mask = gtk_widget_get_events (widget);
attr.event_mask |= (GDK_BUTTON_MOTION_MASK |
GDK_ENTER_NOTIFY_MASK |
GDK_LEAVE_NOTIFY_MASK |
GDK_BUTTON_PRESS_MASK |
GDK_BUTTON_RELEASE_MASK |
GDK_EXPOSURE_MASK);
于是attr_mask一般为:
attr_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;
表示创建时需要考虑x、y、visual、colormap的值(其他的是一定会考虑的,这也说明了这四个值是可有可无的)
为了使创建的窗口是一个顶层窗口,我们必须得到根窗口作为它的父窗口:
parent_window = gtk_widget_get_root_window (widget);
//如果只使用GDK的话可以用gdk_get_default_root_window来获取根窗口
设置好了属性之后就可以创建窗口了:
widget->window = gdk_window_new (parent_window, &attr, attr_mask);
//widget->window 可以换成其他GdkWindow*类型的变量
在成功创建一个顶层可见窗口后,再来看看透明窗口怎么弄的。要让一个窗口不可见,只需要把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,即:
attr.wclass = GDK_INPUT_ONLY;
attr.override_redirect = TRUE;
attr_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_NOREDIR;
改成这样后,就可以正常使用顶层透明窗口了。override_redirect指定窗口管理器忽略这个窗口,这样就不会附加窗口装饰,也不会显示在任务栏上了。
C-x RET f
输入目标编码就行了,我是用utf-8-unix
对应命令是set-buffer-file-coding-system
今天给mm安装Ubuntu,一切顺利。装完后重启,在GDM下输入用户名密码后登录,登录框消失后过了几秒机子就死机了。重启N次后问题依旧,在GDM下按Ctrl+Alt+F1切换到虚拟终端下登录,一切正常。
之前从来没遇到过这种情况,在网上查了一下,没有找到答案,只好自己分析了。能正常启动GDM,说明显卡驱动基本是没有问题的;能在CLI下登录,说明也是不硬盘文件损坏或者空间不足;登录后,硬盘灯狂闪了一两秒钟,此时键盘灯还是能控制的,之后才死机,说明是进入桌面后启动了某个东西才死机的。很自然地,Compiz是最大的嫌疑。mm的机子是05年买的,集成显卡,跑不起Compiz很正常。
问题在于,现在进不了桌面,如何禁用Compiz?我只会在系统->外观里换回metacity,却不知道如何在CLI下配置。网上查了一下“禁用Compiz”“Gnome配置窗口管理器”,没有发现什么有价值的东西。
就要放弃的时候,无聊在/home下du发现了一个目录:.gconf/desktop/gnome/applications/window_manager/,用vim打开下面的%gconf.xml文件,果然是compiz,把两个/usr/bin/compiz换成/usr/bin/metacity,保存,切换回图形界面登录,熟悉的桌面终于又出现了~
长期依赖于GUI的后果就是没有GUI的时候束手无策啊……
如果要用 GTK+ 写一些自定义界面的程序(如QQ),免不了要实现一个背景透明的 widget。一个简单的办法是GtkEventBox+GtkImage,不过这种方法太丑陋了点。然而用通常的方法来创建GTK+ widget 的话,会自动给背景填上颜色,设置了 gdk_window_set_back_pixmap (window->window, NULL, FALSE); 也不行。
这两天跑图书馆,做题做不下去了就玩emacs的配置解闷,还没完成,不过还是顺手贴一下好了
;;;F11:打开ecb
(global-set-key [(f11)] 'ecb-activate)
;;;F12:关闭ecb
(global-set-key [(f12)] 'ecb-deactivate)
;;;M-g 转到指定行号
(global-set-key [(meta g)] 'goto-line)
;;;设置选择起点
(global-set-key (kbd "C-x <SPC>") 'set-mark-command)
;;;高亮选择
(transient-mark-mode t)
;;;显示行号
(setq line-number-mode t)
;;;显示列号
(setq column-number-mode t)
;;打开括号匹配显示模式
(show-paren-mode t)
;;括号匹配时可以高亮显示另外一边的括号,但光标不会烦人的跳到另一个括号处。
(setq show-paren-style 'parenthesis)
;;与系统剪贴板互通
(setq x-select-enable-clipboard t)
;; "C-,"设为屏幕左移命令
(global-set-key (kbd "C-.") 'scroll-left)
;; "C-."设为屏幕右移命令
(global-set-key (kbd "C-,") 'scroll-right)
(global-set-key (kbd "M-p") 'scroll-down)
(global-set-key (kbd "M-n") 'scroll-up)
;;;; CC-mode配置 http://cc-mode.sourceforge.net/
(require 'cc-mode)
;;设置缩进,默认的不好看
(c-set-offset 'inline-open 0)
(c-set-offset 'friend '-)
(c-set-offset 'substatement-open 0)
(defun my-c-mode-common-hook()
(setq tab-width 4 indent-tabs-mode nil)
;;; hungry-delete and auto-newline
(c-toggle-hungry-state 1)
;(c-toggle-auto-newline 0)
;(c-toggle-electric-state 0)
;;按键定义
(define-key c-mode-base-map [(control \`)] 'hs-toggle-hiding)
(define-key c-mode-base-map [(return)] 'newline-and-indent)
(define-key c-mode-base-map [(f7)] 'my-compile)
(define-key c-mode-base-map [(f9)] 'my-run)
(define-key c-mode-base-map [(f12)] 'my-edit-input-data)
(define-key c-mode-base-map [(control c) (control a)] 'my-copy-whole-file)
;(define-key c-mode-base-map [(meta \`)] 'c-indent-command)
;(define-key c-mode-base-map [(tab)] 'my-indent-or-complete)
(define-key c-mode-base-map [(tab)] 'c-indent-line-or-region)
(define-key c-mode-base-map [(control c) (control /)] 'indent-for-comment)
;(define-key c-mode-base-map [(meta ?/)] 'semantic-ia-complete-symbol-menu)
;;预处理设置
(setq c-macro-shrink-window-flag t)
(setq c-macro-preprocessor "cpp")
(setq c-macro-cppflags " ")
(setq c-macro-prompt-flag t)
(setq hs-minor-mode t)
(setq abbrev-mode t)
;;doxymacs
;;(doxymacs-mode)
)
;;;;;;做题用的小工具
;;编译当前编辑的程序
(defun my-compile()
(interactive)
(compile (concat "g++ " (file-name-nondirectory buffer-file-name)))
)
;;运行当前程序,以程序名+".in"作为输入文件
(defun my-run()
(interactive)
(shell-command (concat "./a.out" " < " (file-name-sans-extension buffer-file-name) ".in") "Output")
(display-buffer "Output")
)
;;新开一个水平分割window显示输入文件
(defun my-edit-input-data()
(interactive)
(split-window-horizontally)
(find-file-other-window (concat (file-name-sans-extension buffer-file-name) ".in"))
)
;;复制当前文件的内容,用于提交程序到OJ上
(defun my-copy-whole-file()
(interactive)
;;(mark-whole-buffer)
(kill-ring-save (point-min) (point-max))
)
;;重新载入.emacs文件
(defun my-reload-conf()
(interactive)
(load-file "~/.emacs")
)
(add-hook 'c-mode-common-hook 'my-c-mode-common-hook)
;;;;我的C++语言编辑策略
(defun my-c++-mode-hook()
(setq tab-width 4 indent-tabs-mode t)
(c-set-style "stroustrup")
;; (define-key c++-mode-map [f3] 'replace-regexp)
)
(put 'scroll-left 'disabled nil)
(defun my-html-mode()
(define-key html-mode-map [(return)] 'newline-and-indent)
;;(define-key html-mode-map [(/)] 'indent-according-mode)
)
(add-hook 'html-mode 'my-html-mode)