在上一节提到,当前buffer不一定是当前显示在屏幕上的那个buffer,想要修改显示的buffer,可以使用窗口相关的api。这节来介绍一些窗口的操作。
窗口是屏幕上用于显示一个缓冲区 的部分。和它要区分开来的一个概念是 frame。frame 是 Emacs 能够使用屏幕的 部分。可以用窗口的观点来看 frame 和窗口,一个 frame 里可以容纳多个(至 少一个)窗口,而 Emacs 可以有多个 frame。不知道各位读者是否学习过MFC或者QT,这里的窗口就是MFC中的View,而frame则是整个界面框架,包括菜单栏工具栏、标题栏、状态栏等等部分。而窗口仅仅是最中间显示buffer的那一部分。
分割窗口
刚启动时,emacs 都是只有一个 frame 一个窗口。多个窗口都是用分割窗口的函 数生成的。分割窗口的内建函数是split-window。这个函数的参数如下:
(split-window &optional window size horizontal)
这个函数的功能是把当前或者指定窗口进行分割,默认分割方式是水平分割,可 以将参数中的 horizontal 设置为 non-nil 的值,变成垂直分割。如果不指定 大小,则分割后两个窗口的大小是一样的。分割后的两个窗口里的缓冲区是同 一个缓冲区。使用这个函数后,光标仍然在原窗口,而返回的新窗口对象:
(split-window) ;; ==> #<window 7 on *scratch*>
根据前面对于 optional
后参数的介绍,要填入 horizontal
的值实现竖直切分,需要填充前面的几个参数,如果不给则默认是nil。实际上上面的代码传入的可选参数都是nil,那么我们可以进行如下调用实现竖直分割窗口:
(split-window nil nil 1) ;; ==> #<window 10 on *scratch*>
我们也可以使用 selected-window
来获取当前选中的窗口,当前选中的窗口就是光标所在的窗口
(split-window (selected-window) nil 1) ;; ==> #<window 11 on *scratch*>
在进行实验的时候发现,分割的时候是在当前窗口的基础之上分割的,它是类似于这样的一个过程,它只在Win1所在的窗口区域进行划分,除非改变当前窗口。
+---------------+ +---------------+| | | | || win1 | | win1 | win2 || | --> | | || | | | || | | | |+---------------+ +---------------+|v+---------------+ +---------------+| 4 | 5 | | | | || | | win2 | | win1 | win2 ||--------| | <-- |-------| || win3 | | | win3 | || | | | | |+---------------+ +---------------+
可以看成是这样一种结构
(win1) -> (win1 win2) -> ((win1 win3) win2) -> (((win4 win5) win3) win2)
删除窗口
如果要让一个窗口不显示在屏幕上,要使用 delete-window 函数。如果没有指定 参数,删除的窗口是当前选中的窗口,如果指定了参数,删除的是这个参数对应 的窗口。删除的窗口多出来的空间会自动加到它的邻接的窗口中。如果要删除除 了当前窗口之外的窗口,可以用 delete-other-windows 函数。
当一个窗口不可见之后,这个窗口对象也就消失了
(setq foo (selected-window))
(delete-window foo)(windowp foo) ;; ==> t
(window-live-p foo) ;; ==> nil(delete-other-windows foo) ;; ==> error, 因为先删除foo所对应的窗口,现在已经无法找到这个窗口了,所以这里删除它以外的会报错
窗口配置
窗口配置(window configuration) 包含了 frame
中所有窗口的位置信息:窗口 大小,显示的缓冲区,缓冲区中光标的位置和 mark
,还有 fringe
,滚动条等等。 用 current-window-configuration
得到当前窗口配置,用 set-window-configuration
来还原。
(setq foo (selected-window))
(split-window foo nil t)
(split-window)
(setq wc (current-window-configuration))
(delete-other-windows foo)
(set-window-configuration wc)
我们一行一行的执行上述代码,会发现调用 delete-other-windows
删除之前的窗口之后再次调用 set-window-configuration
会恢复上次保存的结果。看到这里各位读者是否有这么一个想法:利用这两个函数实现一个自动保存和恢复窗口结构的功能呢?
但是经过测试,current-window-configuration 得到的对象并不能持久化的保存的到文件中,即使写到文件中,读取的时候也会报错。下面是我的测试代码
(setq workspace-file-path "~/.session");; 保存窗口的配置
(defun my/save-current-workspace ()(with-temp-file workspace-file-path(print (current-window-configuration) (current-buffer))));; 加载窗口的配置
(defun my/load-current-workspace ()(when (file-exists-p workspace-file-path)(with-temp-buffer(insert-file-contents workspace-file-path)(set-window-configuration (read (current-buffer))))))
在执行保存之后,我们查看文件得到的是一个类似于 #<window-configuration>
的字符串,并没有别的内容,在调用 set-window-configuration
的时候会报错。
选择窗口
前面提到过可以使用 selected-window
来获取当前光标所在的窗口。
我们可以使用 select-window
来选择某个窗口作为当前窗口。使用 other-window
来选择另外的窗口。该函数是一个在不同窗口之间快速跳转的一个函数,它按照窗口创建的时间的逆序进行排序,根据传入的整数参数来决定跳转到第几个窗口。
(progn(setq foo (selected-window))(message "Original window: %S" foo)(other-window 1)(message "Current window: %S" (selected-window))(select-window foo)(message "Back to original window: %S" foo))
这里有两个特殊的宏 save-selected-window
和 with-selected-window
。它的作用是在执行语句之后,选择的窗口回到之前选择的窗口。with-selected-window
和 save-selected-window
几乎相同, 只不过 save-selected-window
选择了其它窗口。这两个宏不会保存窗口的位置 信息,如果执行语句结束后,保存的窗口已经消失,则会选择最后一个选择的窗口
(save-selected-window(select-window (next-window))(goto-char (point-min)))
上述代码会选择另一个窗口并将光标移动到缓冲的开始位置。
当前 frame
里所有的窗口可以用 window-list
函数得到。可以用 next-window
来得到在 window-list
里排在某个 window 之后的窗口。对应的用 previous-window
得到排在某个 window 之前的窗口
walk-windows
可以遍历窗口,相当于 (mapc proc (window-list))
。 get-window-with-predicate
用于查找符合某个条件的窗口
窗口大小信息
窗口是一个长方形区域,所以窗口的大小信息包括它的高度和宽度。用来度量窗 口大小的单位都是以字符数来表示,所以窗口高度为 45 指的是这个窗口可以容 纳 45 行字符,宽度为 140 是指窗口一行可以显示 140 个字符
mode line 和 header line 都包含在窗口的高度里,所以有 window-height
和 window-body-height
两个函数,后者返回把 mode-line 和 header line 排除后 的高度
(window-body-height) ;; ==> 53
(window-height) ;; ==> 54
滚动条和 fringe 不包括在窗口的亮度里,window-width
返回窗口的宽度。所以 window-body-width
和 window-width
返回的结果一样
(window-body-width) ;; ==> 234
(window-width) ;; ==> 234
也可以用 window-edges 返回各个顶点的坐标信息。window-edges
返回的区域包含了 滚动条、fringe、mode line、header line 在内,如果单纯的想要返回文本所在区域可以使用 window-inside-edges
(window-edges);; ==> (0 0 238 54)
(window-inside-edges) ;; ==> (1 0 236 54)
如果需要的话也可以得到用像素表示的窗口位置信息,这里用到的函数是 window-pixel-edges
和 window-inside-pixel-edges
(window-pixel-edges) ;; ==> (0 0 1908 922)
(window-inside-pixel-edges) ;; ==> (8 0 1884 905)
到目前为止,我们有了手段可以遍历窗口以及获取窗口的坐标,那么利用这些数据就可以做到记录和恢复之前的窗口布局了。
我最开始的思路是采用 walk-windows
来遍历窗口,并且使用 window-pixel-edges
来记录每个窗口的区域。但是这么做有一些问题无法解决:首先还原的时候创建窗口只能采用 split-window
,而 split-window
是基于之前的窗口来创建的,walk-windows
无法反映出这种层级关系。另外就是emacs 中没有函数来设置窗口左上角的坐标,我们只能通过函数来改变窗口的宽和高,窗口的位置在使用 split-window
创建的时候已经决定了。所以我们需要一种能表示层级关系的结构来存储窗口的信息。
这个时候就要引入 window-tree
函数了。这个函数可以返回当前 frame
窗口布局的树状结构。为了说明它的返回值,我们先来举一个例子。
- 首先打开emacs,此时看到只有一个窗口,暂时叫它窗口A
- 在窗口上垂直分割一个窗口,新生成的窗口叫做窗口B,此时左侧的窗口是A,右侧的是B
- 在B窗口上水平分割一个窗口,生成一个新的C窗口
此时应该有3个窗口,它们的布局如下:
+---------------+
| | |
| A | B |
| |-------|
| | C |
| | |
+---------------+
如果用树来表示这个布局,可以组成这么一颗树
frame/ \left right(win A) / \/ \top bottomwin B win C
对于叶子节点来说,window-tree
返回的数据形式是 (DIR EDGES CHILD1 CHILD2 ...)
各部分代表的含义如下:
- DIR,表示分割类型,t表示竖直分割,nil表示水平分割
- EDGES, 表示窗口区域的坐标,格式为
(LEFT TOP RIGHT BOTTOM)
,以字符为单位 - CHILDREN, 子节点列表,可以是分支节点或叶子节点
而叶子节点是一个窗口对象。
上面的窗口布局,使用 window-tree
得到的结果如下
((nil(0 0 84 35)#<window 3 on *scratch*>(t(42 0 84 35)#<window 7 on *scratch*>#<window 9 on *scratch*>))
#<window 4 on *Minibuf-0*>)
去除掉minibuffer部分,着重分析一下文本区域的分割
(nil(0 0 84 35)#<window 3 on *scratch*>(t(42 0 84 35)#<window 7 on *scratch*>#<window 9 on *scratch*>))
首先水平分割,占区域大小为 (0 0 84 35)
。此时上面一个部分是 win3。下半部分右进行了分割。下半部分采用竖直方式进行分割,占区域为 (42 0 84 35)。这个部分有两个子窗口win7 和 win9。
感觉分割的顺序与我们的直觉相悖。但是仔细想想好像又能产生之前那种结果
(42 0)
+---------------+
| | |
| win3 | win7 |
| |-------|
| | win9 |
| | |
+---------------+ (84 35)
我们可以写下如下代码来进行这个结构的解析
(defun my-current-window-configuration ();; pai chu minibuffer de shu ju(my-window-tree-to-list (car (window-tree))))(defun my-window-tree-to-list (tree)(if (windowp tree)'win(let ((dir (car tree))(children (cddr tree)))(list (if dir 'vertical 'horizontal)(if dir(my-window-height (car children))(my-window-width (car children)))(my-window-tree-to-list (car children))(if (> (length children) 2)(my-window-tree-to-list (cons dir (cons nil (cdr children))))(my-window-tree-to-list (cadr children)))))))(defun my-window-height (win)(if (windowp win)(window-height win)(let ((edge (cadr win)))(- (nth 3 edge) (nth 1 edge)))))(defun my-window-width (win)(if (windowp win)(window-width win)(let (edge (cadr win))(- (nth 2 edge) (car edge)))))
根据这个结构编写一个还原的功能
(defun my-list-to-window-tree (conf)(when (listp conf)(let (newwin)(setq newwin (split-window nil (cadr conf)(eq (car conf) 'horizontal)))(my-list-to-window-tree (nth 2 conf))(select-window newwin)(my-list-to-window-tree (nth 3 conf)))))(defun my-set-window-configuration (winconf)(delete-other-windows)(my-list-to-window-tree winconf))
可以使用如下代码进行调用
(setq foo (my-current-window-configuration))
;; do something
(my-set-window-configuration foo)
窗口对应的缓冲区
窗口对应的缓冲区可以用 window-buffer 函数得到:
(window-buffer) ;; ==> #<buffer *scratch*>
缓冲区对应的窗口也可以用 get-buffer-window 得到。如果有多个窗口显示同一 个缓冲区,那这个函数只能返回其中的一个,由window-list 决定。如果要得到 所有的窗口,可以用 get-buffer-window-list
(get-buffer-window (get-buffer "*scratch*"))
(get-buffer-window-list (get-buffer "*scratch*"))
让某个窗口显示某个缓冲区可以用 set-window-buffer 函数。 让一个缓冲区可见可以用 display-buffer。默认的行为是当缓冲区已经显示在某个窗口中时,如果不是当前选中窗口,则返回那个窗口,如果是当前选中窗口, 且如果传递的 not-this-window 参数为 non-nil 时,会新建一个窗口,显示缓 冲区。如果没有任何窗口显示这个缓冲区,则新建一个窗口显示缓冲区,并返回 这个窗口。
display-buffer 是一个比较高级的命令,用户可以通过一些变量来改 变这个命令的行为。比如控制显示的 pop-up-windows, display-buffer-reuse-frames,pop-up-frames,控制新建窗口高度的 split-height-threshold,even-window-heights,控制显示的 frame 的 special-display-buffer-names,special-display-regexps, special-display-function,控制是否应该显示在当前选中窗口 same-window-buffer-names,same-window-regexps 等等。
这里的函数实在是太多了,我想暂时不用都记住,现在又有各种大模型,到时候有需求直接使用问就行。或者记住这一个函数,后面要扩展自己去查文档