编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Emacs 折腾日记(十五)——窗口(emacs切换窗口)

wxchong 2025-05-02 13:57:13 开源技术 4 ℃ 0 评论

在上一节提到,当前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-windowwith-selected-window。它的作用是在执行语句之后,选择的窗口回到之前选择的窗口。with-selected-windowsave-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-heightwindow-body-height 两个函数,后者返回把 mode-line 和 header line 排除后 的高度

(window-body-height) ;; ==> 53
(window-height) ;; ==> 54

滚动条和 fringe 不包括在窗口的亮度里,window-width 返回窗口的宽度。所以 window-body-widthwindow-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-edgeswindow-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 窗口布局的树状结构。为了说明它的返回值,我们先来举一个例子。

  1. 首先打开emacs,此时看到只有一个窗口,暂时叫它窗口A
  2. 在窗口上垂直分割一个窗口,新生成的窗口叫做窗口B,此时左侧的窗口是A,右侧的是B
  3. 在B窗口上水平分割一个窗口,生成一个新的C窗口

此时应该有3个窗口,它们的布局如下:

+---------------+
|                |       |
|         A     | B    |
|                 |----|
|                 | C   |
|                 |      |
+---------------+

如果用树来表示这个布局,可以组成这么一颗树

        frame
     /         \
   left       right
  (win A)      / \
                 /     \
	            top   bottom
              win 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 等等。

这里的函数实在是太多了,我想暂时不用都记住,现在又有各种大模型,到时候有需求直接使用问就行。或者记住这一个函数,后面要扩展自己去查文档

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表