编写鼠标敏感应用程序

作者:Alessandro Rubini

Linux 文本控制台不仅仅是一个简单的终端。其最重要的功能之一是鼠标设备的可用性。除了支持选择之外,鼠标还可以用于与用户程序交互。如果您的计算机运行 gpm 服务器,您的程序可以轻松地从 Linux 控制台和 xterm 下的鼠标可用性中获益,并在其他环境中无报错运行。

Writing a Mouse-Sensitive Application

图 1. 使用 libgpm 的应用程序

传统文本程序和鼠标敏感应用程序之间的主要区别在于它们处理输入的方式——前者从 stdin 读取并写入 stdout,而后者必须多路复用来自不同来源的输入——我们可以称之为“事件驱动”应用程序。我将始终使用“程序”来指代 stdin 驱动的进程,使用“应用程序”来指代事件驱动的进程。

gpm 客户端库旨在通过仅更改几行原始源代码,使程序员能够轻松地将程序转换为应用程序。或者,它为从头开始设计应用程序的开发人员提供完整的支持。可移植性是这里的一个主要问题,因为您可能会试图为 Linux 控制台构建一个功能齐全的应用程序,但当您从 xterm 或裸 vt100 中远程登录到您的 PC 时,它会变得完全无法使用。必须小心避免这种情况,因为联网的 Linux 计算机很容易从与其自身控制台无关的 tty 使用。

图 1 中表示了控制台应用程序的内部结构,该图概述了原始程序中需要进行的更改,以便它可以响应鼠标事件。正如您所看到的,所有鼠标支持代码都可以隐藏在一个单独的模块中,并且源代码主体中的鼠标相关代码仅限于以下调用

  • Gpm_Open() 打开函数应在读取任何输入之前调用。它用于连接到守护进程的套接字,并执行从 gpm 守护进程获取事件所需的所有设置。

  • Gpm_Getc()getc()getch() 的任何调用都应替换为 Gpm_ 前缀的函数。替换代码管理多个输入并根据需要调度鼠标事件——稍后会详细介绍。

  • Gpm_Close()exit() 之前,应关闭鼠标连接。可以省略此调用,尽管这样做不太好。

在编写可移植代码时,可以按照以下代码片段中的建议屏蔽掉这些少量修改。它的作用是定义独立于鼠标可用性的函数名称。这种预处理器特定的代码最好驻留在头文件中,以避免在实际源代码中出现丑陋的 #ifdef。选择的方法是将 Gpm_Open 隐藏在 local_mouse_init 中,因为设置不仅仅是一个函数调用;相反,local_mouse_close 是一个语法占位符。

任何其他引用鼠标的代码都可以放在与通用应用程序代码不同的源文件中。正确的 Makefile(可能通过 autoconf)可以轻松选择需要编译的文件以及所需的预处理器定义,而不会使代码因 #ifdef/#endif 而变得混乱。

#ifdef CFG_MOUSE<\n>
#    define local_wgetch(w) Gpm_Wgetch(w)<\n>
     extern int local_mouse_init(void);<\n>
#    define local_mouse_close() Gpm_Close()<\n>
#else<\n>
#    define local_wgetch(w) wgetch(w)<\n>
#    define local_mouse_init()  /* nothing */<\n>
#    define local_mouse_close() /* nothing */<\n>
#endif
通过 Gpm_Open 连接

为您的鼠标设备选择良好的连接很棘手。问题是在避免过多的上下文切换的同时获得最佳的事件分辨率。一些简单的应用程序只需要被告知按钮按下事件,并且可以将光标绘制留给服务器程序;相反,更复杂的应用程序可能希望被告知鼠标的任何单次移动,以及按钮按下和按钮释放。

Gpm_Open 函数获取一个结构作为参数,该结构标识请求的连接类型。连接类型又以事件掩码为特征——识别事件类型的位图。使用 gpm,需要两个掩码——您想要获取的事件掩码和您希望以默认方式处理的事件掩码。

双掩码很有用,因为默认方式是已知的。特别是,由于您知道鼠标移动会导致光标被绘制,因此您通常可以将运动事件留给默认管理,从而减轻应用程序处理鼠标的大部分工作。

除了事件掩码之外,连接应用程序还必须指定两个“修饰符集”,即键盘修饰符集,例如 shift、control、meta (alt) 等。在 gpm 服务器中,键盘修饰符用于在单个控制台上多路复用应用程序。能够将选定的文本粘贴到鼠标敏感的应用程序中是很方便的,而完全控制用户指针的应用程序会激怒大多数客户。

每个 gpm 客户端都被要求指定一个“最小集”和一个“最大集”。客户端特别要求不要被告知带有小于最小集或大于最大集附加修饰符的鼠标事件。对于大多数客户端,最小集将为 0。gpm-root 菜单绘制器是一个具有非 0 最小掩码的客户端。当没有其他客户端时,这会给出选择鼠标专用事件。因此,当运行 Emacs 时,您可以使用 Emacs 鼠标工具(通过加载库 t-mouse.el,在 gpm 发行版中)并访问选择和 gpm-root;lisp 包接受鼠标专用和 alt-鼠标,gpm-root 服务器获取 ctrl-鼠标,内部选择机制获取任何其他事件。在此方案中,选择是一个包罗万象的,就好像它具有无限的最大修饰符掩码一样。

然后,Gpm_Open 保留连接掩码堆栈,因此您可以重新打开连接以修改掩码,并在下次调用 Gpm_Close 时恢复到以前的行为。此功能可用于增加或减少您获得的事件数量。例如,Emacs 以这种方式禁用事件报告,当它停止时,允许您正常地使用 shell 进行选择。相反,绘制菜单的应用程序只能重新打开连接以在菜单保持按下状态时获取运动事件。这种堆栈式功能在客户端库中进行管理,以便行为不端的应用程序不会锁定服务器。

使用 mev

为了在不浪费您青春的情况下测试库功能,在编译-执行-理解-重新编译循环中,mev 程序与 gpm 服务器一起分发。此工具基于 xev 的思想,X 事件报告器。 mev 报告它在当前控制台上获得的任何事件,并且您可以在命令行上指定要使用的事件和修饰符掩码。因此,mev 可以帮助在您将连接参数硬编码到应用程序之前测试它们。 Mev 还可以通过从标准输入获取 push 和 pop 命令来演示连接堆栈的使用。

虽然 mev 最初被设计为调试 gpm 服务器的测试用例,但它现在本身非常有用,我将其用作 emacs 库的引擎。

从多个来源获取输入

连接到服务器后,应用程序必须响应键盘事件和鼠标事件。为了简化这一点,libgpm 为 getc 及其相关函数提供了替换函数,但应用程序设计者并非被迫使用它们。

想要自主管理两个输入通道的程序员必须始终使用 select() 系统调用,除非应用程序将输入文件置于非阻塞模式并保持轮询它们。如果您的应用程序需要很长时间才能完成其工作,并且您希望用户能够通过单击键或鼠标按键重新获得控制权,则轮询可能是明智的。 Netscape WWW 浏览器中可以找到此技术的良好示例。

相反,许多面向屏幕的应用程序大部分时间都在等待用户输入,并且可以很好地受益于 libgpm 中的函数。从外部来看,当接收到键盘事件时,Gpm_Getc() 及其相关函数表现得就像原始函数一样;在内部,它们可以接收鼠标事件并通过用户定义的函数处理它们。

您可以想象,这些输入过程是围绕 select() 构建的。这是使用户免于在应用程序主体中使用 select 的唯一方法。当 stdin 报告为可读时,将调用原始的 getc 函数;当鼠标连接可读时,将调用 Gpm_GetEvent()

如果应用程序需要,它们可以自行调用 Gpm_GetEvent(),但您必须记住,Get_Event 基于 read() 调用,因此是阻塞的。 libgpm 中的普通 gpm 输入函数(通常是 Gpm_Getc)仅在有数据要读取时才调用 Gpm_GetEvent。然后,getc 替换函数将事件传递给应用程序指定的鼠标处理函数。请注意,Gpm_GetEvent() 仅负责从当前源读取事件,而不将事件传递给鼠标处理函数。

负责处理鼠标事件的用户函数——我们称之为“鼠标处理程序”——在调用输入函数之前由用户在全局变量中注册,并且其调用不会干扰正在运行的 Gpm_Getc() 调用。但是请注意,输入函数正在等待处理程序完成;长时间运行的任务不太适合鼠标处理程序。

伪造按键

通常,鼠标只是用作键盘的快捷方式:例如,单击菜单按钮就像按 f1,单击列表框项目就像输入其突出显示的字母,在活动菜单外按下按钮就像发出 esc 键,以及使用滚动条就像多次按下箭头键。如果鼠标可以将按键返回到输入子系统,则应用程序设计将大大简化——双输入机制再次加入到单个输入流中,从而大大减少了要管理的状态信息量。

在 libgpm 中,此行为由鼠标处理程序的返回值强制执行。处理函数返回一个整数值,该值按以下方式解释

  • EOF 此值用于指示致命错误,并将导致输入函数向调用者返回相同的值。

  • 0 返回值零表示输入函数应像以前一样继续,而无需返回给调用者。该事件被视为被处理程序占用,并且不模拟任何按键。

  • 其他任何值 任何其他值都被视为“模拟”的按键字符,并返回给调用者。请注意,这些值不限于 ASCII 字符——可以返回任何整数值。

在返回伪造按键之前,输入函数设置一个全局变量,该变量指示该按键不是真正的按键;在返回键盘生成的字符之前,该标志被清除。应用程序可以自由使用或忽略此信息。就我个人而言,我从未使用过它。

请注意,返回任何整数值的能力非常强大,并且与 libc 环境完全兼容,因为根据定义,getc() 返回一个整数。超出字符范围的返回值可用于将鼠标活动封装到通用的“事件”整数实体中,并且主循环中的相同 switch 构造可以处理应用程序获得的任何输入。

使用伪造按键功能,任何鼠标事件都可以打包在一个整数值中,以便稍后在应用程序的主循环中进行解释。就我个人而言,我更喜欢在鼠标处理程序内部解释事件,并且仅向调用者返回属于一小组操作的整数。

鼠标处理程序还可以注册其返回更多按键的意图,因此可以在无需等待新的鼠标事件的情况下调用它。因此,滚动条可以通过向调用者返回正确数量的箭头按键轻松地在鼠标处理程序中实现。

巧妙地使用伪造按键机制可以极大地简化复杂应用程序的设计,而计算开销却可以忽略不计。在实践中,当您编写无法适应伪造按键机制的鼠标处理代码时,您必须小心;您必须确保坐在没有任何指针设备的 vt100 tty 上的用户不会因为陷入在没有鼠标的情况下无法恢复的状态而失去对应用程序的控制。如果不幸的无鼠标用户尽管输入能力有限,但仍可以充分利用您的应用程序,那将是最好的。

堆叠应用程序

通常,智能程序允许自己临时停止,或者为用户提供生成 shell 的选项。在程序开发过程中,此功能经常被忽略,因为程序员倾向于专注于应用程序本身,而不是如何从中逃脱。在放弃 tty 控制之前,任何鼠标敏感程序都应释放鼠标,以避免从尝试在 shell 环境中运行选择机制的用户那里窃取事件。在这种情况下,释放鼠标的首选方法是使用连接参数调用 Gpm_Open,指示所有事件都传递给下一个服务。当程序恢复用户焦点时,它可以简单地 Gpm_Close 以恢复以前的事件掩码。如果应用程序在释放 tty 之前忘记释放鼠标,则会发生奇怪的事情。

使用 curses

通常,鼠标敏感应用程序使用 curses 或兼容的 ncurses 库管理屏幕。[有关 ncurses 的介绍,请参见第 ?? 页 - 编辑] 从鼠标处理的角度来看,这没有太大区别。您只需要调用 Gpm_Getch()Gpm_Wgetch() 来代替 getc 或 getchar。这些替换函数采用与原始 curses 调用相同的参数。

从鼠标处理的角度来看,功能齐全的 curses 应用程序和使用普通 tty 的应用程序之间的唯一区别在于屏幕可能细分为不同的窗口。如果屏幕分为多个窗口,则使用单个鼠标处理程序会使管理变得非常复杂。这种情况由所谓的高级库处理,该库是一组简单而有效的函数,用于管理“感兴趣区域”堆栈,从而简化了将事件分派给多个接收者的过程。

高级库

gpm 库的高级部分提供了到集中式数据结构的入口点,该结构负责将事件传递给多个鼠标处理程序。

实际上,维护了一个 ROI(感兴趣区域)的双向链表,每个 ROI 负责使用特定的“客户端数据”处理特定用户函数的事件。每个区域都由其矩形限制以及最小修饰符集和最大修饰符集标识。因此,您可以选择根据事件位置或使用的修饰符,以类似于先前描述的在单个控制台上多路复用应用程序的方式,将事件传递到不同的窗口。

当您使用窗口界面时,您可以通过创建与每个 curses 窗口关联的一个或多个 ROI 来充分利用高级库。除了 ROI 中发生的事件之外,与区域关联的处理程序还将在鼠标光标进入区域时获得“进入”事件,并在离开区域时获得“离开”事件。这意味着单个鼠标移动可以生成多个回调,以帮助保持一致的屏幕状态,而无需大量的全局状态变量。

不幸的是,高级库仅在 gpm 1.0 版本之后才可用。如果您有旧版本的 gpm,最好升级。缺少高级库是 gpm 版本号长期以来为 0.x 的主要原因。

Xterm 支持

在 X Window 系统中,终端应用程序在 xterm 中运行,而 xterm 是您在大多数工作站上可以找到的唯一可用的 tty——通常工作站非常慢,并且在 X-Windows 启动之前无法使用。

幸运的是,xterm 能够报告鼠标事件,这些事件由转义序列组成,并通过与普通数据相同的通道报告给客户端应用程序。

不幸的是,它能够报告的事件范围受到严重限制。此外,由于事件通过与键盘事件相同的流报告,因此所有多输入通道的良好设计都会中断,并且任何想要独立感知鼠标和键盘事件的应用程序都会失败。

幸运的是,使用 Gpm_Getc() 及其朋友效果很好,您可以通过在 xterm 下运行 mev 来检查这一点。

如果您考虑在 xterm 下运行应用程序,则必须确保不要依赖完整的事件报告。具体来说,您不会收到任何运动或拖动事件的通知,并且按钮释放事件不会指定已释放哪组按钮。这实际上意味着,如果您需要精确报告双按钮按下,则您的应用程序在 xterm 下将无法正常工作。

我强烈建议您小心;如果应用程序只能在 Linux 控制台下运行,则其用途有限,并且您肯定会比您预期的更早地诅咒自己。相反,如果应用程序能够在 xterm 下运行,则最好利用通过简单的鼠标按键(至少)调用按钮的能力,而不是强迫用户仅使用键盘交互。

使用 GNU autoconf

如果环境不是 Linux 计算机怎么办?一对好的设计选择和少量的时间投入可以使您成为 autoconf 包的熟练用户,并且您的应用程序可以轻松适应以下环境

  • 安装了 gpm 的 Linux 机器。 这是最佳环境,并且应用程序将在控制台和 xterm 下编译,并具有完整支持。在无鼠标 tty 中调用时,应用程序将在仅键盘模式下运行,而无需运行时条件。

  • 没有 gpm 的 Linux 机器。 如果应用程序以二进制形式分发,则 gpm 库将静默检测到缺少服务器,并在控制台上以仅键盘模式运行。在 xterm 下,一切都将正常工作。如果应用程序以源代码形式分发,因此无法链接到 gpm 库,则以下情况将适用。

  • 另一个类 Unix 操作系统。 应用程序将在内置 xterm 支持的情况下编译,因为 autoconf 会将 gpm-xterm.c 包含在要编译的文件集中。此源替换了您在 libgpm 中找到的最有用的函数(即 open、close 和 getc 函数)和 Gpm_Repeat(),这是一个支持函数,用于在按钮保持按下状态时提供事件重复。“鼠标处理程序”的概念仍然有效。

  • 非 Unix 操作系统。 看起来像一场失败的战斗... 无论如何,您都必须包含大量条件编译的代码。您确定需要鼠标敏感应用程序吗?在任何情况下,它都不会比使任何应用程序在显着不同的操作系统之间可移植更难。

清单 12 中的代码摘录包括用于创建在 gpm 包中分发的“可移植”示例应用程序的 configure.inMakefile.in 的相关部分。此处重新生成它们是为了让您了解设置可移植编译环境有多么容易。实际上,您不必成为 autoconf 专家即可设置这样的环境,因为少量的文档和大量的剪切和粘贴可以轻松地工作。

此 configure.in 检查是否在 libgpm 中找到了 Gpm_Repeat,并选择是链接到 libgpm 还是应编译 gpm-xterm.c。请注意,高级库虽然未在此 configure.in 中管理,但独立于低级机制,因此也可以包含在可移植应用程序中。

Gpm_Repeat 是一种软件辅助工具,可以在及时的基础上重复事件,直到按钮释放。它也可以在 xterm 下工作,并且在此处用作测试,因为它仅在库和服务器相当稳定时才出现。我想您不希望将您的应用程序与 libgpm 0.01 链接,以防万一某些早期 alpha 测试人员在其硬盘驱动器上有一个。

这种痛苦值得付出努力吗?

但是,在您实际开始编码之前,值得了解使用 libgpm 进行鼠标编程的优缺点,并对常见的陷阱发出警告。

如果您需要编写友好的界面,那么与编写 Tk 脚本相比,使用 libgpm 真的 很困难。如果您的界面将在功能强大的工作站上运行,那么您最好运行 X-Windows 和 Tk。此外,它是完全可移植的——Tk 的免费 Macintosh 和 MS Windows 端口正在开发中。

如果您的应用程序将在不运行 X-Windows 的通用工作站上运行,则应考虑升级现有硬件的趋势。因此,如果您的应用程序是中长期项目,那么您最好还是从 Tk 开始。

但是,如果作者本人不鼓励使用 libgpm,那么哪些应用程序需要 libgpm 呢?作为一个简单的规则,我建议在您需要支持整个范围的 Linux 计算机时编写纯文本应用程序。系统管理工具是 libgpm 的良好候选者——请记住,Linux-1.2 仍然可以在 2MB RAM 和 10MB 磁盘上愉快地运行。

另一个可以从简单的鼠标敏感前端中受益的领域是嵌入式系统和专用机械领域。例如,廉价的 Linux 盒子可以用作小型公司的 NFS(网络文件系统)或 WWW 服务器,并且新手用户将查询使用情况报告。避免 X-Windows 并编写基于 gpm 的界面在这里是一个胜利。

如果我没有阻止您使用 libgpm,那就去吧,但请记住要注意可移植性、简单性和用户的熟练程度。

可移植性是开发 Unix 应用程序时的主要问题。特别是,请记住构建与 tty 无关的应用程序——这意味着您必须始终为鼠标事件提供键盘替代方案。有数百种 tty 类型,您不能强迫用户使用 Linux 控制台。此外,用户可能需要通过 stdin 在“无人值守模式”下驱动您的应用程序。

另一个重要的问题是保持简单:例如,不要依赖于同时按下两个按钮之类的东西,这在 xterm 下不起作用。

最后,请记住,用户必须感觉到鼠标。您应该在每次写入屏幕后重绘鼠标光标(可能通过 Gpm_DrawPointer)。这很重要,因为用户倾向于使用鼠标选择文本,并且以与 selection 相同的方式使用鼠标敏感应用程序可能会造成灾难。

在哪里获取软件

gpm 软件包可通过 ftp 从 sunsite.unc.edu/pub/Linux/system/Daemons/gpm-1.06.tar.gz 获取。有时,小的改进不会到达 sunsite,因为我不想打扰维护者。最新的版本始终可以从 iride.unipv.it/pub/gpm/gpm-1.06.tar.gz 获取。

源代码包包括完整的 info 文件和 PostScript 手册,更彻底地描述了该库。还包括一个示例可移植应用程序。

该软件包也以二进制形式分发(但带有完整的文档),与 Slackware 一起分发。如果您通过软盘获得了 Slackware 发行版,则可能需要获取源代码;否则它在 cdrom 中。最近,我还听说有人提议“debianize”gpm,因此它可能会在不久的将来出现在 Debian 发行版中。

对于文档中未解答的任何问题,请随时与我联系。

清单 1. 鼠标感知应用程序的简单 configure.in

dnl configure.in for sample gpm client
dnl This will only run with autoconf-2.0. or later
AC_INIT(rmev.c)
AC_PROG_CC
AC_PROG_CPP
CFLAGS="-O"
LIBS=""
dnl look for libgpm.a; if found assume to have
dnl <gpm.h> as well. Gpm_Repeat is only present
dnl after gpm-0.18
AC_CHECK_LIB(gpm, Gpm_Repeat,[
    GPMXTERM=""
    LIBS="$LIBS -lgpm"],[
    GPMXTERM="gpm-xterm.o"
    if test "-uname-" = Linux
      then AC_MSG_WARN("libgpm.a is missing or old")
    fi
    ])
dnl subsitute @GPMXTERM@ in Makefile
AC_SUBST(GPMXTERM)

清单 2. 鼠标感知应用程序的简单 Makefile.in

# simple Makefile.in - autoconf will
# replace any @symbol@ with the right value
# include standard stuff
srcdir = @srcdir@
VPATH = @srcdir@
CC = @CC@
CFLAGS = @CFLAGS@
LDFLAGS = @LDFLAGS@
LIBS = @LIBS@
prefix = @prefix@
OBJS = rmev.o @GPMXTERM@
TARGET = rmev
all: configure Makefile $(TARGET)
$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) \
               -o $(TARGET) ($LIBS)
clean:
    rm -f *.o $(TARGET) config.*
### rules to automatically rerun autoconf
Makefile: Makefile.in
    ./configure
configure: configure.in
    autoconf
    configure
distrib: clean
    rm -f config.* *~ core
    autoconf
    rm -f Makefile

Alessandro Rubini (rubini@ipvvis.unipv.it) 正在攻读计算机科学博士学位课程,并在家饲养了两台小型 Linux 机器。他天性狂野,热爱徒步旅行、划独木舟和骑自行车。他编写了 gpm

加载 Disqus 评论