在大多数人看来,输入与输出结合得这样紧,以致人们常常说“I/O”或“输入/输出”, 而很少单独提到其中的一个。在这一章里,要补充第5章的内容,我们要讨论输入设备。 本章重点介绍两种使用最普遍的输入设备:键盘和鼠标。键盘由系统所包含的DOS 和BIOS功能操纵,而要使鼠标的功能发挥作用,则必须在系统中添加一个驱动程序。 键盘比大多数人所意识到的要复杂得多。作为整个pc工程的精致片断,键盘能适应 BIOS中的低级功能,并借助缓冲字符输入和提供中断来使键盘操作对于程序几乎是看不 到的,这样就让BIOS能在敲击键盘字符时操纵它们。 因为中断直到第11章"中断处理程序”才讨论,所以本章仅以简单的方式涉及到鼠 标。这也是为一个有趣的程序才这样作的。通过使用鼠标功能(当它们存在时),可以在许 多程序中建立由鼠标所控制的功能。 将键盘及鼠标与中断处理功能结合起来能使键盘和鼠标功能都变得更加有趣。使用热 键来启动的TSR程序,或中断鼠标输入的鼠标驱动程序都对用户的需要进行快速反应。 第11章将更详细地讨论中断。但是,目前需要了解基本的输入功能。 6.1键 盘 人们总是依赖于pC机有键盘作为输入设备——每台Pc机都有,并且大多数程序都 使用它们。尽管替代性的输入设备如鼠标已普遍用于一些类型的程序,但很少有程序不需 键盘输入而进行操作。 本节先看BIOS和DOS功能怎样用于键盘输入。这些功能用来替代高级语言的通常 输入功能时,会带来下列优点: .对输入的最大控制。例如,可以添加用户指定的编辑性能和特殊的帮助功能。 .比使用高级语言所提供的键盘输入例程更乙、的程序。这些例程,用来提供每一种 可能的输入环境和条件,但执行了大量的“开销代码”。 .更敏捷、反应更迅速的输入,因为可以使程序对无论怎样的选择都能产生反应。 在讨论编程之前,先看看键盘的工作方式。 6.1.1了解键盘的工作方式 对程序员来说,键盘可能是计算机上最熟悉但却最不了解的设备。大多数人了解文件 110页 系统和磁盘操作,因为阅读了这方面的书籍。但最少讨论键盘。只有很少的专家了解键盘。 本书不打算描述使键盘工作的硬件,而着重看看如果要使用与键盘相关的DOS和 BIOS设备时用户必须了解的各种事件的顺序。 首先,想象你在通过联系计算机和键盘的电缆看计算机。按键时,可以看到数字(键盘 “扫描码”)由键盘通过电缆到达计算机。 每按一次键,就产生一个独一无二的8位数(最明显的位总是0,以示按键和后面的 显示之间的区别),即使分列左边和右边的Shift键也由不同的数字代表。这些扫描码精确 地指示按的是哪一个键。 无论什么时候放开一个键,都会产生另一个扫描码。这个代码与前面的相同,只是设 置了高序位(即扫描码加上128)。扫描码就以这种方式用信号告诉ROM BIOS:这个键已 经放开了。 尽管有许多不同类型的键盘配置,但大部分键盘都遵循三种基本设计中的一种。这些 设计则都源于IBM为这些不同的计算机开发的键盘设计。这三种设计是PC机(83键)键 盘、个人机AT(84键)键盘,以及增强(101键)键盘。虽然大多数扫描码在三种设计中保 持相同,但还是有些区别。图6.1、6.2和6.3分别显示了PC机、个人机AT及增强键盘上 的扫描码(十六进制)。注意增强键盘利用多个扫描码来识别它的一些附加键。 111页 图6.3十六进制的增强型键盘扫描码 当按住一个键超过半秒钟时,键盘就会产生一系列的扫描码以响应按键动作,并以特 助速率重复出现。 BIOS能够说出哪个键正被按住(不是反复按)。因为在这个系列中键 没有产生任何放开键的代码。 键盘只识别所按的键(不是原符),并且只提供与该键对应的扫描码。(扫描码只是指 示已经按住了某个特定的键)。BIOS翻译扫描码以确定哪个ASCII码字符对应于所按的 键。为旧的85键和84键键盘设计的BIOS ROM忽略了101键键盘上额外的键,所以许 多程序都没有利用这些附加键。 每当按一个键,键盘都会产生扫描码和Int 09h,它告诉ROM BIOS已经按住了一个 键。系统的控制马上传递给中断处理程序,它读端口96(60h)以确定按住了哪个键。中断 服务例程读扫描码,并将它转换成代表所按键的16位代码,该代码的较低字节是所按键 的ASCII码值;较高字节则是扫描码。表6.1就是这些代码的列表。 表6.1 AscII码和扫描代码 键 增强型 键 增强型 Esc 01 01 01 ?/ 35 35 35 !1 02 02 02 右shift 36 36 36 @2 03 03 03 PrtSc * 37 37 E0 12 #3 04 04 04 左Alt 38 38 38 $4 05 05 05 空格 39 39 39 %5 06 06 06 CapsLock 3A 3A 3A ^ 6 07 07 07 F1 3B 3B 3B &7 08 08 08 F2 3C 3C 3C * 8 09 09 09 F3 3D 3D 3D (9 0A 0A 0A F4 3E 3E 3E )0 0B 0B 0B F5 3F 3F 3F _- 0C 0C 0C F6 40 40 40 112页 (续) 键 增强型 键 增强型 PC AT 增强型 PC AT 增强型 += 00 0D 00 F7 41 41 41 Backspace 0E 0E 0E F8 42 42 42 Tab 0F 0F 0F F9 43 43 43 Qq 10 10 10 F10 44 44 44 Ww 11 11· 11 Num Lock 45 45 45 Ee 12 12 12 ScrollLOck 46 46 46 Rr 13 13 13 7 Home 47 47 47 Tt 14 14 14 8向上箭头 48 48 48 Yy 15 15 15 9 PgUp 49 49 49 Uu 16 16 16 Gray- 4A 4A 4A Ii 17 17 17 4向左箭头 48 48 48 00 18 18 18 5 4C 4C 4C Pp 19 19 19 6向右箭头 4D 4D 4D {[ 1A 1A 1A Gray+ 4E 4E 4E }] 1B 1B 1B 1End 4F 4F 4F Enter 1C 1C 1C 2向下箭头 50 50 50 左Ctrl 1D 1D 1D 3 PsDn 51 51 51 Aa 1E 1E 1E 0 Ins 52 52 52 Ss 1F 1F 1F .Del 53 53 53 Dd 20 20 20 仅AT键盘 Ff 21 21 21 SysRq N/A 54 N/A Gg 22 22 22 仅增强型键盘 Hh 23 23 23 GrayEnter N/A N/A E01C Jj 24 24 24 右Ctrl N/A N/A E010 Kk 25 25 25 Gray/ N/A N/A E035 Ll 26 26 26 Gray * N/A N/A 37 :; 27 27 27 Right Alt N/A N/A E038 "' 28 28 28 Gray Home N/A N/A E0 47 ~ 29 29 29 GrayC向上 N/A N/A E048 Shift 2A 2A 2A GaryPgUp N/A N/A E049 |\ 2B 2B 2B GrayC向左 N/A N/A E04B Zz 2C 2C 2C GrayC向右 N/A N/A E04D Xx 2D 2D 2D GrayEnd N/A N/A E04F Cc 2E 2E 2E GrayC向下 N/A N/A E050 Vv 2F 2F 2F Gray PgDn N/A N/A E051 Bb 30 30 30 Gray Ins N/A N/A E052 Nn 31 31 31 Gray De N/A N/A E053 Mm 32 32 32 F11 N/A N/A 57 <, 33 33 33 F12 N/A N/A 58 >. 34 34 34 Pause/Break N/A N/A E1 113页 特殊键,如功能键和数字小键盘上的键,在低字节中有一个零来指示所按的键不是一 个标准的ASCII字符,并且必须被特殊处理。 当BIOs键盘中断处理程序结束处理所按的键时,它将代码放进键盘缓冲区中保存 起来,直到某个程序需要它时。两个特别的键,Ctrl-Break和Shift,printScreen,不以这种方 法处理;相反它们产生的中断请求马上由BIOS的另外部分处理了。Ctrl-Break调用了Int 1Bh,它是DOS设置来迫使Ctrl-C进入缓冲区,以便进行后面的“正常”处理。 Shift- PrintScreen则调用了Int 05h,它执行屏幕打印动作。 在这个水平上,不用书写任何程序去访问键盘数据。相反,本章中的代码只依赖BIOS 或DOS去预处理所有键盘输入数据,以便用户只需要处理通常键的ASCII码和特殊键的 扫描码。 6.1.2用BASIC读键盘 在开始利用DOS和BIOS调用去读键盘之前,让我们先看看使用标准BASIC功能来 进行键盘输入的一个BASIC程序。 KEYBD.BAS程序使用了BASIC解释程序中有用的 功能(见列表6.1) 列表6.1 10 REM...KEYBD.BAS Basic keybOard demonstration 20 c$S=INKEY$:IF C$=" " THEn 20 30 gosub 100:GOTo 20 100 REM···Display Keyboard input 110 IF LEN(C$)>1 THEN GOSUB 200 ELSE GOSUB 300 120 PRINT USING "Character:! ### ###"; CH$;c;SC 130 RETURN 200 REM···KeyStrOke haS SCan code included 210 CH$=".":C=ASC(MID$(c$,1,1)):SC=Asc(MID$(C$,2,1)) 220 RETURN 300 REM···Keystroke is Character only 310 CH$2C$:c=ASC(c$):SC=0 320 RETURN 为达到与BASIC解释程序兼容,该程序书写时带有行号。下面逐步显示这个程序的 简单结构: 1.等待击键。 2.打印键击字符、ASCII码和扫描码。 3.从第一步开始重复。 INKEY$(放在列表6.1第20行的紧密循环中)提供了读键盘的一种方式,并且不 必等待击键。根据字符串是否超过一个字符长,100行的子例程翻译从INKEY$返回的 内容,第一个字符是字符的ASCII值,第二个则是扫描码(起始于第200行的子例程用于 处理所按键的大小写)。通常字符串只有一个字符长。第300行的子例程序则用于翻译字 符串的代码。 如果运行列表6.1中的程序,就会注意到扫描码只在有限的程度上与单个的键对应 (参见图6.1和6.2)。KEYBD.BAS程序存在以下不足: 114页 ·INKEY$只返回“有意义的”字符(在这个例子中字符“有意义”只是针对BASIC 解释程序而言的)。 ·如果完成的击键不是ASCII字符,则只为此击键返回扫描码,不用看Shift键、Ctrl 键或Alt键 从现在开始,本书不继续介绍解释程序兼容的BASIC程序,而只介绍QuickBASIC 编译程序。列表6.2显示了为QuickBASIC而修改的程序,以充分利用编译程序的性能。 列表6.2 KEYBD2.BAS QUickBASIC Keyboard Demonstration Start: C$=INKEY$:IF C$=“” THEN GOTO Start GOSUB Display:GOTO Start Display: Display Keyboard Input IF LEN( C$) > 1 THEN GOSUB Scan ELSE GOSUB Char PRINT USING " Character: ! ### ###" ; CH$;c; SC RETURN Scan: Keystroke includes scan code CH$=".":c=ASC( MID$(C$,1,1)):SC = ASC( MID$( C$,2,1)) RETURN char : Keystroke is Character only CH$=c$:C=ASC(c$) : SC=0 RETURN 从列表6.2中的样本程序开始,将改而采用BIOS功能调用来执行输入。然后我们就 能在键盘上看到更多的击键。 6.1.3使用Int 16h来访问键盘 当准备使用BIOS功能时,要注意到的第一件事是Int 16h的功能0(BIOS键盘输入 功能),它的作用是等待击键(见列表6.3)。 列表 6.3 /* waitkey.c Listing 6.3 of DOS Programmer's Reference*/ #include<stdio.h> #include<dos.h> void main() { unsigned char scancode; unsigned char charcode; union REGS regs; printf("Test of Int 16 keyboard services\n"); printf("press Esc to exit program\n"; while(Charcode != 27){ regs.h.ah = 0; int86(0x16, &regs, &regs); 115页 scancode=regs.h.ah; charcode = regs.h.al; printf("Scan: %.3d ASCII : %.3d [%C] \n", scancode, charcode,charcode); } } 仅当有键按下时,读出击键的内容,而不是等待击键。Int 16h,功能1指示一个击键是 否正在等待。如果没有按键在等待,那么就在处理器的进位标记寄存器中设置0进位标 记。可按照列表6.4所示测试该寄存器。 列表6.4 /* waitkey2.C Listing 6.4 of DOS Programmer'S Reference*/ #include #include<dos.h> void main() { unsigned char scancode; unsigned char Charcode; union REGS regs; printf("Test of Int 16 keyboard services\n"); printf("press Esc to exit program\n"; while(charcode != 27){ while(1) { putchar('.'); regs.h.ah=1; int86(0x16, &regs, &regs); if((regs.x.flags & 0x40) == 0) break; } regs.h.ah=0; int86(0x16, &regs, &regs); scancode = regs.h.ah; charcode = regs.h.al; printf("\nScan: %.3d ASCII: %.3d [%C] \n", scancode, Charcode, charcode); } } 从键盘获取键之前,该程序首先看看是否有键准备好了。若没有,程序只在屏幕上打 印一个点。不论什么时候按键,程序都会停止打印点并显示信息,告诉按的是哪个键。然 后程序又开始打印点。 当程序有某种途径来检查键盘输入的程序在等待击键时,它们不会“闲”着,而要去完 成其它的任务。例如,可以使用这类例程来移动屏幕上的数据,而在击一个键时,终止此操 作。 Int 16h功能2用于确定键的状态:Ins、Caps Lock、Num Lock、Scroll Lock、Alt、Ctrl, 以及左边和右边的Shift键。键盘输入程序的下一个版本则让用户检查这些键的状态(见 列表6.5)。 116页 列表6.5 /* Keystat.c Listing 6.5 of DOS Programmer's Reference */ #include #include #define VIDEO 0x10 /* Prototypes */ void gotoxy(int r, int c); void cls(void); void main() { unsigned char scancode; unsigned char charcode; union REGS regs; int ins, caps, num, scroll; int alt, ctrl, left, right; cls(); gotoxy(0,0); printf("Test of Int 16 keyboard services\n"); printf("Press Esc to exit program\n"); gotoxy(10, 12); puts("INS CAPS NUM SCRL ALT CTRL LEFT RiGHT"); while(charcode != 27) { while(1) { regs.h.ah = 2; int86(0x16, ®s, ®s); ins = regs.h.al&0x80; caps = regs.h.al&0x40; num = regs.h.al&0x20; scroll = regs.h.al&0x10; alt = regs.h.al&0x08; ctrl = regs.h.al&0x04; left=regs.h.al&0x02; right = regs.h.al&0x01; gotoxy(11, 12); printf("%S %S %s %s %S %S %S %S\n" , ins ? " ON " : " OFF", caps ? "ON " : " OFF", num ? " ON " : " OFF", Scroll ? " ON " : " OFF", alt ? " ON " : " OFF", ctrl ? " ON " : " OFF", left ? " ON " : " OFF", right ? " ON " : " OFF"); regs.h.ah = 1; int86(0x16, ®s, ®s); if((regs.x.flags&0x40)==0) break; } regs.h.ah = 0; int86(0x16, ®s, ®s); Scancode = regs.h.ah; charcode = regs.h.al; printf("Scan: %.3d ASCII: %.3d [%C]\n", scancode, charcode, charcode); } cls(); gotoxy(0,0); 117页 } void gotoxy(row,col) int row, col; { union REGS regs; if(row<0||row>24) return; if (col<0||col>79) return; regs.h.ah=2; regs.h.bh=0; regs.h.dh=row; regs.h.dl=col; int86(VIDEO,&regs,&regs); } void cls() { union REGS regs; regs.h.ah = 0x06; regs.h.al = 0; regs.h.bh=7; regs.h.Ch=0; regs.h.cl=0; regs.h.dh = 25; regs.h.dl = 80; int86(VIDEO,&regs,&regs); } 在列表6.5中的程序里,putchar('.')已经被用于检查特殊键的状态,并将此状态显 示在屏幕中心的代码所代替。这种状态信息给用户一个完整的画面,用来说明键盘上正在 发生什么。如果希望自己的程序去检查一个突发事件,如左边和右边的Shift键盘同时被 按住,Int 16h的功能2便能指出这个事件的发生时间。 Int 16h的功能2将单个字节返回到AL寄存器中。每个位都对应于一个特定的击 键。表6.2显示该功能的位赋值。 表6.2键标记字节中各位的含义 位 意义 76543210 0······0 没按住右边的Shift键 ·······1 按住了右边的Shift键 ······0· 没按住左边的Shift键 ···. ··1· 按住了左边的Shift键 ·····1·· 没按住Ctrl键 ·····1·· 按住了Ctrl键 ····0··· 没按住Alt键 ·. ··1··· 按住了Alt键 ···0···· Scroll Lock关闭 ···1···· Scroll Lock打开 ··0····· Num Lock关闭 ··1···. . Num Lock打开 ·0····· . Caps Lock关闭 ·1····· . Caps Lock打开 0. ·····. Insert关闭 1···. ·. · Insert打开 处理键盘输入的BIOS功能代表了不用进行机器访问的最基本的途径。对键盘而言, 118页 BIOS 功能是能够利用的最低层,并且它也能保证某个程序尽可能地与各种PC机兼容。 6.1.4使用Int 21h来访问键盘 当以常规方法在DOS水平上获取键盘输入时,会失去对每个可能的击键的立即访 问。 DOS输入功能指示特殊键是否已被按住(如果第1个字节返回的是0,第2个是扫描 码)。但DOS功能不能告诉用户已有ASCII码的键的扫描码,也不报告Alt、shift等键的 状态。另外,最常使用的DOS输入功能要等待一次击键,这会强迫在一些程序中使用 BIOS接口。在本章稍后的部分,我们看到一个DOS输入功能克服了这种局限性。 Int 21h的功能1(有回显字符输入)是用户可能想用的最基本的DOS字符输入功能 (见列表6.6)。 列表 6.6 /* Keyin.c Listing 6.6 of DOS Programmer's Reference*/ #include<stdio.h> #include #define CTRL_D 0x04 #define LF 0x0a #define ENTER 0x0d /*prototypes*/ Unsigned int keyin(void); void main() { int c; printf("Testing Int 21h input functions\n"); printf("press Ctrl-D to exit program\n"); while((c=keyin())!=CTRL_D) if(c>=256) printf("SPECIAL: %d\n",c-256); else if(c==ENTER) putchar(LF); } unsigned int keyin() { union REGS regs; int offset; offset = 0; regs.h.ah = 0x01; intdos(&regs,&regs); if(regs.h.al==0){ offset = 256; regs.h.ah = 0x01; intdos(&regs,&regs); } return (regs.h.al+offset); } Keyin.c程序采用下列步骤: 119页 1.等待来自键盘的一个字符。 2.如果该字符为特殊字符(例如,功能键或光标键),则打印APECIAt及该键的扫 描码。 3.如果该字符是Enter键,字符输入功能就会在没有换行的情况下输出一个Re- turn;用户必须输出一个换行字符到屏幕上,才会使显示内容换行。 4.从第1步开始重复。 如果用户在没有能将换行字符打印到屏幕上的专门部分的帮助下运行该程序,那么 就会看到这些字符在敲入时会被DOS功能回显出来。但是,当按Enter键时,光标不会移 向下一行。记住:该功能回显所敲的字符,并且Enter键只代表回车—而不是换行(Line feed)字符。 该程序的一个有趣的功能是:它采用的特殊编码能将功能键与普通键区分开。因为 ASCII字符总是小于或等于255,所以这个功能产生一个扩展的字符设置,它是通过将一 个返回0的ASCII码键的扫描码值加上256来达到的。实际上,这个方法常能简单化译出 击键代码的过程,并且不会带来问题。许多程序员不用此方法,因为他们将击键与变量联 系在一起。将返回的字符与整数联系起来,用户就可以有更大范围的特殊代码来代表特殊 击键。 当需要特殊键(如功能键和箭头)时,上述读字符的方法有一个问题,即当进行第二次 调用想获得特殊键的扫描码时,DOS功能会打印出与扫描码相等的ASCII码,就如象想 获得的是ASCII码而不是扫描码。为防止这种情况发生,可以使用Int 21h的功能8,并且 由用户自己回显字符(见列表6.7)。 列表6.7 /* Keyin2.c Listing 6.7 of DOS Programmer's Reference*/ #include <stdio.h> #include <dos.h> #define CTRL_D 0x04 #define LF 0x0a #define ENTER 0x0d /* prototypes*/ unsigned int keyin(void); void main() { int c; printf("Testing Int 21h input functions\0"); printf("press Ctrl-D to exit program\n"); while((c=keyin())!=CTRL_D) if(c>=256){ printf("SPECIAL: %d\n",c-256); }else{ putchar(c); if(C==ENTER) putchar(LF); } } unsigned int keyin() 120页 { union REGS regs; int offset; offset=0; regs.h.ah = 0x08; intdos(&regs, &regs); if(regs.h.al==0){ offset = 256; regs.h.ah = 0x08; intdos(&regs,&regs); } return (regs.h.al+offset); } 这个程序与列表6.6中的程序之间的区别非常细微,如果不仔细看就会忽略。首先, 在Keyin()函数中,设置程序去调用Int 21h的功能8而不是功能1。主例程序必须在敲击 字符是将每个字符回显给屏幕(因为DOS的内核并没为用户回显示字符)。因为特殊键独 立操纵,所以不用为回显假的功能键代码而烦恼—只会回显正在敲的键。 每个用于本章的DOS功能都等待一次击键,但与BIOS功能一起使用时,就可以周 期性地检查,看看是否有一个字符正在等待读取(这个过程叫做查询)。中断21h的功能 0Bh指示一个字符是否正等着被读取。列表6.8显示了当操作者等待一次击键时,怎样使 用这个功能去执行任务。 列表6.8 /* Keyin3.c Listing 6.8 of DOS Programmer's Reference*/ #include<stdio.h> #include #define CTRL_D 0x04 #define LF 0x0a #define ENTER 0x0d /* prototypes*/ unsigned int keyin(void); unsigned int charwait(void); void main() { int c; printf("Testing Int 21h input functions\n"); printf("press Ctrl-D to exit program\n"); while((c =keyin())!=CTRL_D) if(c>=256) { printf("SPECIAL: %d\n",c-256); } else{ putchar(c); if(c==ENTER) putchar(LF); } } 121页 unsigned int keyin() { union REGS regs; int offset; while(!charwait()) putchar('.'); offset = 0; regs.h.ah = 0x08; intdos(&regs, &regs); if(regs.h.al==0) { offset = 256; regs.h.ah = 0x08; intdos(&regs, &regs); } return (regs.h.al+offset); } unsigned int charwait() { union REGS regs; regs.h.ah=0x0b; intdos(&regs, &regs); return (regs.h.al); } 这个程序介绍了一个新的函数,charwait(),由它去检查键盘上的字符。如果已有字 符正在等待读取,它就会返回TRUE;否则就返回FALSE。 该函数以Int 21h的功能0Bh为基础,它设置AL寄存器的值,如果某字符在等待,它 就设置为FFh(255),否则为0。通过返回AL寄存器的值,该函数指示一个字符是否在等 待读取(FALSE即是0,TRUE是非0的值)。 要估价程序能怎样快速地检查字符,可以运行它,并看看程序等待敲入一个字符时经 过屏幕的时间。该程序对输入反应相当迅速。如果用一个长的、复杂的操作来代替简单的 putchar('.'),程序就会变得极端迟钝了。 本章的前面提到了一个克服常规DOS输入的许多局限性的特殊DOS输入功能:普 通的I/O功能—Int 21h的功能06h。 如果调用Int 21h的功能06h时,DL寄存器包含0FFh,那么该功能将从键盘缓冲区 中获得输入(并且如果没有字符可用,就返回0,且进位标志被设置)。 如果在DL寄存器中是其它的内容,它就被输出给CRT并且不进行检查。于是,这个 单一功能提供了使用直接的BIOS输入和输出时所能获得的大部分功能;并且还保持完 整的DOS兼容性。因为它是原始的“CP/M遗产”功能之一,所以很遗憾它没有象它应该 有的那样得到普及。 列表6.9是列表6.8从C语言到TurboPascal的翻译。经过修改,可利用Int 21h的 功能06h来输入和输出。功能和过程名没变,行为也是同样的。该程序没有使用Int 21h的 功能0Bh来检查键的就绪状态,而只是使用由Turbo Pascal的DOS单元提供的ZFlag(零 进位标志)常量来简单地测试零进位标志,这样就消除了charwait函数;该程序不使用 putchar来作单字节输出,它加上了putch函数,借助功能06h来执行输出。 122页 列表6.9 {keyin4.pas } program keyin4; uses DOS; { run-time DOS library } const CTRL_D = $04; LF = $0a; ENTER = $0d; var c : integer; reg : registers; { declared in DOS unit } procedure putch( i : integer ); begin reg.ah := $06; { general I/O function } reg.dl := byte(i) ; { OUTPUT the character } MsDos (reg); end ; function keyin : integer; var i : integer; offset : integer; begin repeat { wait for keystroke } putch($2E) ; { put '.' on CRT } reg. ah := $06; {general I/O function} reg.dl := $FF; {flag for INPUT use } MsDos (reg) ; i := reg.flags AND FZero; until i <> FZero; { declared in DOS unit } if (reg.al = 0) then { flag as.extended key } begin offset := 256; reg.ah := $06; { and go get scan code } reg.dl := $FF; MsDos (reg ) ; end else { set as normal key } offset := 0; keyin := reg.al + offset; end ; begin writelnt('Testing int 21h input functions' ) ; writeln( ' Press Ctrl-D to exit program' ) ; c := keyin; while (keyin <> CTRL_D) do begin if (c >= 256) then begin writeln (' SPECIAL : ',c-256) ; end else begin putch (c) ; if (c= ENTER) then putch (Lr) ; 123页 end; c:= keyin; end; end. (Int 21h,功能0Ah),并让它获得用户的输入,回显它,而且允许进行行编辑。DOS编程方 面的一些书籍指出:该功能通过返回一个两字节序列(包括零字节和扫描码)来返回功能 键、箭头以及其它的特殊键。keyin5.c 程序(见列表6.10)在特殊键返回时会显示它们 —但它们没有返回。 IBM技术手册没有包括两字节的键代码。 列表6.10 /* Keyin5.c Listing 6.10 of DOS Programmer's Reference*/ #include <stdio.h> #include<dos.h> #define CTRL_D 0x04 #define LF 0x0a #define ENTER 0x0d /* prototypes*/ char getline(char*b,int n); void main() { char buffer[11]; printf("Testing Int 21h Input Functions\n"); printf("press Enter with no input to exit program\n"); while(getline(buffer, 10)>0) { printf("<<%>>\n",buffer); } } char getline(buffer,n) char* buffer; int n; { union REGS regs; char locbuf[514]; int i,j; locbuf[0]=n; locbuf[1]=0; regs.h.ah = 0x0a; regs.x.dx=(int)&locbuf; intdos(&regs,&regs); for(i=0,j=0; i<locbuf[1];i++,j++) if(locbuf[i+2]==0) { i++; buffer[j]='x'; } else{ buffer[1] =locbuf[i+2]; } buffer[j]=NULL; return (locbuf[1]); } 124页 对已缓冲的输入功能的需求使它变得特殊化。缓冲区的设计使它不能自然地适合 BASIC、C语言或Pascal处理字符串的途径。调用该功能时,必须将它传递给一个大得足 够用来处理输入字符再加上2个额外字符的缓冲区。缓冲区中的第一个字符是允许输入 的字符的最大数目;由DOS装入的第二个字符则代表被读入的字符数目。 6.1.5识别键盘支持的水平 如果BIOS支持增强键盘,那么有两套BIOS功能可以访问键盘:功能00h、01h和 02h支持旧的键盘,而功能10h、11h和12h支持新键盘。一个程序怎样才能告诉用户它能 否使用增强键盘功能呢? 可以利用功能05h(向键盘缓冲区写入),来测试增强键盘功能的存在。当向缓冲区写 入FFFFh值时,应该会发现AL设置成了00h或0h。如果是这样,就利用功能10h(获得 击键)去读入该缓冲区。如果存在增强键盘功能,就可以在16个调用的最大值中读 FFFFh值(因为在键盘缓冲区中存在16个字)。 在DOS 5.0中,探测增强键盘是自动进行的;功能01h、06h、07h、08h、0Ah、0Bh和 0Ch,如果它们存在,都使用增强键盘功能(见列表6.11)。 列表6.11 page55,132 ;Kbdtype.asm ;Determine whether the BIOS enhanced keyboard functions are ;present. Prints the level supported and returns an errorlevel ;of 0 (no enhanced keyboard) or 1(enhanced keyboard). .model tiny .code .startup check proc ; clear keyboard buffer clear : mov ah, 1 ;Check for keystroke present int 16h jz buffer_clear ;if none,proceed... mov ah,0 ; read the keystroke and discard it int 16h jmp clear ;check again ; stuff 0FFFFh into buffer buffer_clear: mov ax , 5FFh ; stuff FFFF in mov CX,0FFFFh int 16h cmp a1,1 ; success? ja no_ enhanced ; no , BIOS plainly doesn't support it ; try to read 0FFFFh mov cx, 1 ;16 entries in the standard buffer tryit: mov ah,1th ; use enhanced check for keystroke present int 16h jz no_enhanced ; nothing there,it didn't work mov ah , 10h ; use enhanced function to read keyStroke int 16h cmp ax , 0FFFFh ; our keystroke? je enhanced loop tryit 125页 ; no enhanced keyboard no_enhanced: mov dx ,offset no ;print negative message mov al,1 ; Set error code to 1 printit : push ax ; save error code mov ah,9 ;print string int 21h pop ax ; restore error code mov ah ,04CH ; exit with error code int 21h ; exit ; enhanced keyboard enhanced: mov dx,offset yes ;print positive message xor al ,al ;zer error code jmp printit check endp no db “N0” yes db " ENHANCED KEYBOARD FUNCTIONS SUPPORTED$" end 6.2鼠 标 鼠标(或其等价的设备,如跟踪球)作为计算机系统的部分,其应用正不断增长。而且 依赖于鼠标而获得有效使用的软件产品也在增长。Microsoft Windows可以只用键盘来操 作但是图形接口的充分便利则只能来自鼠标。尽管一些程序如Microsoft Paint(在Win- dows之下运行)能够使用键盘,但要轻松地做到这一,点却是不可能的。而Guide(OW1 In- ternational的超级文件系统)没有鼠标就不能使用。 DOS没有包括鼠标驱动程序。读者可能已注意到,DOS没有包含用户程序可能想用 的许多不同类型外设的驱动程序。开发DOS时曾决定让用户能增加设备驱动程序来控制 另外的设备,从而使DOS得到了更广泛的使用。当引导系统时,这些驱动程序之一,Mi- crosoft的MOUSE.SYS将被加进操作系统中(只要购买了Microsoft鼠标,它就会提供 MOUSE.SYS)。本节将介绍鼠标软件的工作方式。 6.2.1了解鼠标的工作方式 鼠标是能够附另到计算机上的一个十分简单的仪器设备。它由一个小球构成,该小球 置于能在平面上滚动的“鼠标”内部。当球转动时,鼠标内的电路就向计算机报告这种移 动,软件则将这种移动解释和翻译成屏幕上的光标移动。除了这个球外,鼠标还包括两个 或三个按钮(开关),可用来发信号告诉计算机。 6.2.2初始化鼠标驱动程序 根据所拥有的软件,可以采取许多方式中的一种去设置鼠标驱动程序。某些软件包能 产生一个TSR去操纵鼠标;其它的则有专门的驱动程序。如果有了驱动程序,就应该把它 126页 安装到系统中,即包含在CONFIG.SYS文件的一行中,这一行告诉系统在引导过程中装入 该驱动程序。如果有存放在C驱动程序器的根目录中的Microsoft的鼠标驱动程序,那 么可以使用下面的行: DEVICE=C:\MOUSE.SYS 6.2.3鼠标位于何处 首先,程序必须确定鼠标是否已装入。Int 33h的功能0能告诉使用者:如果AX的返 回值,非零,那么鼠标是可用的(在早于3.0的DOS版本中,Int 33h的向量没有初始化来 指向一个IRET;使用功能0测试之前,应该证明这个向量不指向地点0000:0000)。 如果已经知道鼠标是可用的,就可以使用它了:可以周期性地查询它有关位置改变和 击键方面的情况,也可以在鼠标移动或按一个按钮时设置一个中断服务例程。为简单起 见,这里只讨论查询技术。 因为鼠标驱动程序监视着鼠标光标在屏幕上的位置,所以用户程序不必做这样的工 作。用户只需知道鼠标是定位在用户想做某种事情的地方。样本程序MOUSE.C,它是以 Keyin3.c为基础的,它显示出当用户等待其它输入时怎样检查鼠标(见列表6.12)。 列表6.12 /*MOuSe.c Listing 6.12 of DOS Programmer'S ReferenCe*/ #include<Stdio.h> #include<Stdlib.h> #include<dOS.h> #include<String.h> #define CTRL_D 0x04 #define LF 0x0a #define ENTER 0x0d #define MOUSE 0x33 #define VIDEO 0x10 static int Buttons=0; /*prototypes*/ unSigned int keyin(void); unSigned int charwait(void); void gotoxy(int r,int c); void cls(void); void mousepos(void); 127页 printf("SPECIAL:%d\n",C-256); }else{ putchar(c); if(C==ENTER) putchar(LF); } mouseoff(); cls(); gotoxy(0,0); } unsigned int keyin() { union REGS regs; int offset; while(!charwait()) mousepos(); offSet=0; regs.h.ah=0x08; intdoS(®s,®s); if(regS.h.al==0){ offset = 256; regS.h.ah=0x08; intdos(®s,®s); } return (regs.h.al+offset); } unsigned int charwait() { union REGS regs; regS.h.ah=0x0b; intdos(®s,®s); return (regs.h.al); } void chk_mouse() { union REGS regs; struct SREGS sregs; regS.x.ax=0x3533; intdosx(®s,®s,&sregs); if((regs.x.bx|sregs.es)==0){ printf("No mouse driver present\n"); exit(255); } regs.x.ax=0; int86(MOUSE,®s,®s); if(regs.x.ax!=0){ Buttons=regs.x.bx; mouseon(); } } void mouseon() { union REGS regs; struct SREGS sregs; if(Buttons){ 128页 regs.x.ax=0x01; int86(MOUSE,®S,®s); } } void mouseoff() { union REGS regs; struct SREGS sregs; if(Buttons){ regs.x.ax=0x02; int86(MOUSE,®s,®s); } } void mousepos() { union REGS regs; char status[6]; gotoxy(12,10); if(Buttons){ regS.x.ax=0x03; int86(MOUSE,®s,®s); switch(regs.x.bx&0x03){ Case 0:/*no buttons*/ strcpy(StatuS,"NONE"); break; caSe 1:/*left button*/ Strcpy(status,"LEFT"); break; case 2:/*right button*/ strCpy(StatuS,"RIGHT"); break; case 3:/*both buttons*/ StrCpy(Status,"BOTH"); break; } printf("X=%4.4d Y=%4.4d %S", regs.x.cx,regs.x.dx,status); }else{ printf("No mouse"); } } void gotoxy(row,col) int row,col; { union REGS regs; if(row<0||row>24)return; if(col<0||col>79)return; regs.h.ah=2; regs.h.bh=0; regs.h.dh=row; regs.h.dl=col; int86(VIDEO,®s,®s); } 129页 void cls() { union REGS regs; regs.h.ah=0x06; regs.h.al=0; regs.h.bh=7; regs.h.ch=0; regs.h.Cl=0; regS.h.dh=25; regS.h.dl=80; int86(VIDEO,&regS,&regs); } Mouse.C包括Chk_Mouse()函数,它检查鼠标驱动程序的存在,如果存在就检查鼠 标。当该程序等待输入时,它将光标移到屏幕中心并显示有关鼠标位置和按钮的信息,而 不显示等待的时间周期。 如果已有一个安装好的鼠标,移动它并看看鼠标状态改变,以反映鼠标的移动。有了 这些基本函数便可构成最低水平的可操作的鼠标功能。 gotoxy()函数也加进了Mouse.c 程序中,它能利用BIOS屏幕定位途径在屏幕上定位光标(第5章“输出设备”解释了这个 函数的操作方式)。 这个样本程序只包括3个基本的鼠标功能。第一个,确定鼠标是否已安装以及它有多 少按钮,该功能在一个全局变量中设置按钮的数目。当调用其它的鼠标功能时,这些功能 会检查这个变量以确定它们是否有事情可做。 如果鼠标已经装入,就可以调用第二个功能(Int 33h功能1)使其在屏幕上显示鼠标 光标。在有鼠标的情况下,调用Int 33h的功能3,在检查(但不会找到)字符时检查鼠标状 态(位置及按钮状态)。该功能返回被编码并置入BX寄存器两个低位的鼠标按钮的状态。 如果设置了位0,那么左边的按钮已经按过了。如果设置的是位1,则是右边的按钮已经按 下了。 鼠标在屏幕上的位置,x坐标方向上从0到639,y坐标轴则从0到199。依据当前的 显示方式,可惜助表6.3以列或坐标来确定鼠标位置。 表6.3鼠标坐标 屏幕方式 坐标 0或1 行=DX/8,列=CX/16 2或3 行=DX/8,列=CX/8 4或5 x=CX/2,y=DX 6 x=CX,y=DX 7 行=DX/8,列=CX/8 14到16 x=CX,y=DX 其他的鼠标功能(在本书接近结束处的一节“中断33h:鼠标功能”中介绍)有助于控 制鼠标光标的大小、形状和它在屏幕上的边界,以及它对鼠标产生反应而在屏幕上移动的 速度。还可以设置一个功能以便任何时候当鼠标事件(如按键或放开键)发生时都能调用 它。我们已经了解过这类例程,叫作中断处理程序,具体请见第11章。 130页 6.3小 结 本章讨论了键盘和鼠标功能,并介绍了一些基本的,可以用来执行输入功能的操作。 借助BIOS所提供的键盘功能,就可以知道键盘上按下了哪个键并追踪操作行为。可以看 到字符输入时的AscII码(就象从高级语言输入功能返回的那些值一样),还可以看到所 按键的扫描码,尽管它们只从某些键上返回。还能够监视shift、Alt、Ctrl、Scroll Lock、 Num Lock和Caps Lock键的状态。 通过利用DOS Int 33h的鼠标功能,就能监视鼠标按钮在鼠标上的位置以及鼠标在 屏幕上的位置。甚至还可以设置中断,用于鼠标功能,这样任何时候当鼠标事件(如击键) 发生时,这些功能都会执行特殊的处理程序。