1:缘起
在传统的程序中,一个具有交互功能的应用程序的主体无疑是如下的一段循环代码:不断读取用户的输入,根据输入采取相应的动作,完成所需的功能。例如:
bool quit=false;
char ch;
while(!quit) {
ch=read_input();
switch(ch) {
case 'i':
// ... …
break;
case 'q':
quit=true; // … …
break;
default:
// ... …
} }
而基于事件驱动机制的程序则不大相同,主程序可能如下这般:
int main()
{
TMyApp myApp;
myApp.run();
return 0;
}
在整个程序中,唯一与事件有点关系的要属handleEvent函数了,它可能并不像你我想像的那样(注:以Turbo Vision中的应用程序为例,根据类的继承关系,函数的调用序列依次为:TMyApp::run() TApplication::run() TGroup::execute() TMyApp::handleEvent() 来处理事件):
void TMyApp::handleEvent(TEvent& event) {
TApplication::handleEvent(event); // act like base!
if( event.what == evCommand ) {
switch( event.message.command ){
case cmMyNewWin: // but respond to additional commands
myNewWindow(); // define action for cmMyNewWin
break;
default:
// … …
return;
}
clearEvent( event ); // clear event after handling
}
}
从这里,我们明显可以看到一份简单,一个鸿沟:没有了令人头疼的循环,程序中的对象只需要处理自己收到的事件,程序的结构变得异常简单清晰;但也正是这份简单,让本来清楚明白的流程变得神秘起来,没有了读取用户输入,摆放在面前的已经是送到的事件,这多少有点让人措手不及,前面的路是谁铺就的?一切的一切,都缘起于事件驱动机制:读取了用户的输入,并把其包装成事件,然后按照一定的规则分发给不同的对象;正是它,造就了简单,也成就了神秘;有了它,用户才可以把所有的精力集中于事件的处理上,而不用分心其他琐事。例如:在一个非活动窗口上按下鼠标左键,用户只需确保该窗口在接收到该消息时,能够把自己调到前台,正确地显示即可。在鼠标轻点与窗口响应之间,系统在幕后默默地做了许多事情:读取鼠标的动作并形成的事件,按照一定的规则把事件发送给该非活动窗口。
系统替我们完成的这许多事情,给事件驱动蒙上几分神秘、几分诱惑。不论是好奇心、还是求知欲,都使人有揭开面纱,一探究竟的冲动。最直接的方法是去看源码,借用一句话:“源码之前,了无秘密”。源码之中,会清楚地显示系统是如何收集用户的输入,又是如何把用户的输入打包成事件,分发给程序中的各个对象的。追踪到源码级别,相信已经足以理解事件驱动机制。然而,去哪儿找到这样的源码呢?Windows操作系统是基于事件驱动的,但哪儿有其源码?Linux操作系统倒是有源码,然而,面对其浩如烟海的文件,如何简洁、干练地抽出事件驱动的源码而不至于陷于泥沼无法自拔?因此,最好是能够有一个基于事件驱动的、微型的应用程序框架,既能把事件驱动机制讲个清楚明白又不至于吓退登山者。Turbo Vision无疑是最佳人选。
Turbo Vision是一个应用程序框架,为基于DOS的应用程序开发提供了全新的方法,可以为用户生成完整的界面,包括窗口、对话框、菜单、鼠标以及简单的编辑器等。Borland C++3.1中就附带有Turbo Vision的源码,网上也可以找到Turbo Vision的源码。
Turbo Vision应用程序的框架如图:
TMyApp-->TApplication-->TProgram-->TGroup-->TView-->TObject
-->TProgInit -->TStreamable
图1:Turbo Vision应用程序框架图
一个Turbo Vision应用程序是视图、事件及哑对象的组合。下面,分别对视图和哑对象作一简单的介绍,至于事件,后面会有详尽的介绍。
视图是屏幕上可见的程序元素对象。在Turbo Vision程序中,凡可见的,都是视图。标题、窗口边框、滚动条、菜单条等都是视图。视图也可以组合,形成复杂的程序元素,如对话框。这些视图的集合称为组。组作为特殊的视图,由其它称为其子视图的元素组成,而组自身也可以作为单一的视图与其他视图形成更复杂的元素,这样形成了一个复杂的视图链。
哑对象是程序中非视图的对象,在屏幕上不显示,无法与用户交互,故称为其哑对象。它们完成计算、与外围设备的通讯等后台任务,当其需要显示信息时,必须通过视图来进行。
这是只对Turbo Vision做一简单的介绍,在后面用到某些概念时,会略加注释,帮助读者理解。
2:事件的介绍
事件是一个无法再细分的信息包,描述了应用程序必须响应的具体事件。每一次击键、每一个鼠标动作以及程序的某一部分引起的某种特定情况都构成一个单独的事件。因此,用户输入一个单词不是一个事件,而是一系列独立的击键事件的集合。
不同的事件具有不同的性质,具有不同的结构,因此,有必要先了解不同种类的事件。事件的结构如下:
struct TEvent{
ushort what;
union{
MouseEventType mouse;
KeyDownEvent keyDown;
MessageEvent message;
};
void getMouseEvent();
void getKeyEvent();
};
由事件TEvent的结构可以清楚地看到,从本质上讲,有四类事件:鼠标事件,键盘事件,消息事件和空事件,对应于TEvent::what成员的四个可能取值,evMouse,evKeyboard,evMessage,evNothing。根据TEvent::what的值,TEvent对象可以决定自身的类型,使用相应的成员函数getMouseEvent()或getKeyEvent()来获得事件。
鼠标事件包括四种:鼠标键的按下与松开导致的evMouseDown、evMouseUp事件,移动鼠标产生的evMouseMove事件,保持按下某一键产生的evMouseAuto事件等,故鼠标事件的结构如下:
struct MouseEventType{
uchar buttons; // 鼠标的按键
Boolean doubleClick; // 是否双击
TPoint where; // 发生鼠标事件时,鼠标的位置
};
键盘事件则简单得多。当按下某一键时,产生一个evkeyDown事件,记录按键信息。其结构如下:
struct KeyDownEvent{
union{
ushort keyCode;
CharScanType charScan;
};
};
struct CharScanType{
uchar charCode; // 字符代码
uchar scanCode; // 扫描码
};
消息事件分命令、广播与用户信息三种,evCommand、evBroadcast对应于命令与广播,用户信息可以表示用户定义的常量。三种事件的不同之处在于其传送的方式,关于事件的传送方式在后面会有更详细地讨论。消息事件的结构如下:
struct MessageEvent{
ushort command;
union{
void *infoPtr;
long infoLong;
ushort infoWord;
short infoInt;
uchar infoByte;
char infoChar;
};
};
空事件表示一个已经处理完毕的事件。该事件内含有的信息不再有用,可以忽略。当Turbo Vision对象处理完一个事件后,会调用成员函数void clearEvent(TEvent &e)将事件设置为空事件。
许多事件最后都转换成某种命令而结束。例如:在状态行里的某个状态项上按下鼠标键,会产生一个鼠标事件。状态行对象在处理此事件时,通过产生一个挂起的命令事件响应鼠标的动作,绑定在该状态项对象上的命令决定了命令事件的command数据成员,在下次调用getEvent获取事件时,获得刚才产生的命令事件。
对事件有了简单的概念之后,下面进入正题:事件驱动。
3:事件驱动机制
面对事件驱动的千头万绪,哪里才是最佳的切入点呢?我们想了解的主要有两个方面:(1)系统是如何收集用户的输入,把它包装成事件的?(2)系统是如何去分发这些事件的?因为这是事件驱动机制中最神秘的地方,至于程序中的对象是如何处理这些事件的,则并非我们关心的重点。
我们还是从主程序main()开始吧,依照函数的调用序列,顺藤摸瓜,把系统在幕后替我们做的事件逐一拿到前台,把事件驱动机制的神秘面纱慢慢揭开。
TMyApp::run()-->TApplication::run()-->TProgram::run()-->TGroup::execute()
图3:Turbo Vision程序的调用序列
类TGroup的部分源码摘录如下:
ushort TGroup::execute(){
do {
endState = 0;
do {
TEvent e;
getEvent( e ); // 读取用户的输入
handleEvent( e ); // 处理用户的输入
if( e.what != evNothing )
eventError( e ); // 如若事件没有被处理,则废弃此事件
} while( endState == 0 ); // 若该组是模态,则会一直执行,直到调用
// endModal成员函数终止组的模态
} while( !valid(endState) );
return endState;
}
在这里,我们又看到了熟悉的流程:不断地读取用户的输入,然后进行处理,直到用户退出。这是在意料之中的,正是这样的循环完成了交互。即使是基于事件驱动的,也少不了这段循环代码。
3.1:事件的产生
事件从何而来,系统是如何将用户的输入收集起来,并加以包装的,这要归功于神秘的void getEvent(TEvent& event)函数了。getEvent函数是Turbo Vision中唯一必须关心事件来源的函数。由类的继承关系可知:TGroup继承自TView,而自己并未重载基类的getEvent。因此,在TGroup::execute()中,调用的是TView::getEvent,追踪可知:
void TView::getEvent( TEvent& event ){
if( owner != 0 )
owner->getEvent(event);
}
视图类TView调用其所有者的getEvent成员函数,而在一个Turbo Vision应用程序中,一个TProgram(或TApplication)对象是所有视图类的最终拥有者,因此,在缺省情况下,每个getEvent调用都终止于TProgram::getEvent(除非用户自己修改了某个视图类的getEvent成员函数)。TProgram::getEvent的源码如下:
void TProgram::getEvent(TEvent& event){
if( pending.what != evNothing ){
event = pending;
pending.what = evNothing;
} // 考虑是否有上一个事件产生的挂起事件
else{
event.getMouseEvent();
if( event.what == evNothing ){
event.getKeyEvent();
// 依次考虑是否有鼠标事件、键盘事件
if( event.what == evNothing )
idle();
}
}
if( statusLine != 0 ) {
// 对于状态行的键盘和鼠标事件,直接转换成相应的命令事件
// 从而避免一个无谓的循环。
if( (event.what & evKeyDown) != 0 ||
( (event.what & evMouseDown) != 0 &&
firstThat( hasMouse, &event ) == statusLine ) )
statusLine->handleEvent( event );
}
}
在TProgram::getEvent中,首先检查是否有由TProgram::putEvent产生的挂起事件,然后依次检查鼠标事件、键盘事件,如若都没有,则调用idle函数,表示当前没有事件发生。如果用户想增加新型事件(如:从串行口读取字符),可以在应用程序对象中重载TApplication::getEvent。在getEvent的循环代码中增加一个getComEvent(event)调用即可。如果用户不想修改循环代码,可以重载idle函数,在没有鼠标、键盘事件时,执行getComEvent。有一点需要注意的是:在idle中执行的任务不能挂起应用程序,否则会堵塞用户的输入,造成程序不能响应的假象。
至此,事件的获取已经逐渐明朗,依旧云遮雾罩的是getMouseEvent、getKeyEvent等其实现细节。下面列出其源码,略作解释:
1:挂起事件(pending)
挂起事件由putEvent产生,视图的putEvent调用其所有者的putEvent,因此,缺省情况下,所有视图的putEvent调用都终止于TProgram::putEvent。TProgram::putEvent将event备份到类的静态变量pending中去,供下次TProgram::getEvent调用时返回。
void TProgram::putEvent( TEvent & event )
{ pending = event; }
2:鼠标事件
鼠标事件由TEvent::getMouseEvent函数获得,追踪其调用序列可知:
TEvent::getMouseEventTEventQueue::getMouseEvent,而TEventQueue::getMouseEvent的源码如下 (TEventQueue为鼠标事件的FIFO队列,可容纳的事件个数为16个) :
void TEventQueue::getMouseEvent( TEvent& ev ){
if( mouseEvents == True ){
getMouseState( ev ); // 获得当前鼠标的状态
if( ev.mouse.buttons == 0 && lastMouse.buttons != 0 ){
ev.what = evMouseUp;
lastMouse = ev.mouse;
return; // 获得一松开按键事件(evMouseUp)
}
if( ev.mouse.buttons != 0 && lastMouse.buttons == 0 ){
if( ev.mouse.buttons == downMouse.buttons &&
ev.mouse.where == downMouse.where &&
ev.what - downTicks <= doubleDelay )
ev.mouse.doubleClick = True; // 判断事件双击
downMouse = ev.mouse;
autoTicks = downTicks = ev.what;
autoDelay = repeatDelay;
ev.what = evMouseDown;
lastMouse = ev.mouse;
return; // 返回一鼠标双击事件(evMouseDown)
}
ev.mouse.buttons = lastMouse.buttons;
if( ev.mouse.where != lastMouse.where ){
ev.what = evMouseMove;
lastMouse = ev.mouse;
return; // 返回一鼠标移动事件(evMouseMove)
}
if( ev.mouse.buttons != 0 && ev.what - autoTicks > autoDelay ){
autoTicks = ev.what;
autoDelay = 1;
ev.what = evMouseAuto;
lastMouse = ev.mouse;
return; // 按下鼠标按键不松开,则产生evMouseAuto事件
}
}
ev.what = evNothing; // 无鼠标事件
}
个人以为,追踪到这一步,已可初窥鼠标事件的端倪,再细究下去,就追踪到Turbo Vision的鼠标类TMouse及其低级支持类THWMouse,在那里,调用中断程序int86(0x33,®s,®s)要求系统提供关于鼠标的支持,非本文之初衷。
3:键盘事件
相比之下,键盘事件则简单得多,直接调用中断处理程序来获取键盘上的按键信息:
void TEvent::getKeyEvent()
{ // 调用中断获得键盘事件
asm {
MOV AH,1;
INT 16h;
JNZ keyWaiting;
};
what = evNothing;
return;
keyWaiting:
what = evKeyDown;
asm {
MOV AH,0;
INT 16h;
};
keyDown.keyCode = _AX;
return;
}
4:状态行(TStatusLine)的特殊处理
如果鼠标事件或键盘事件发生在状态行上,在TProgram::getEvent返回之前,直接调用TStatusLine的事件处理程序handleEvent,把鼠标事件或键盘事件转换成命令事件,调用putEvent把事件重新放入到事件队列中,供下次调用getEvent时返回,从而减少了把鼠标或键盘事件发送到状态行、再转换成命令事件、再返回的这个无谓循环。
3.2:事件的传送
事件已经由TProgram::getEvent取了出来,各个对象也知道如何去处理它们到接收到的事件,中间的桥梁无疑是事件传送机制。这需要对组的概念、对事件的本质进行更深入一层的了解。
组是一个含有并管理其他视图(称为组的子视图)的对象。组把其管理区域中的一部分授权给自己的一个子视图使用。例如:TApplication是一个组对象,管理着整个屏幕,含有三个子视图,TMenuBar、TDeskTop、TStatusLine,屏幕顶行分给TMenuBar,屏幕底行分给TStatusLine,其余的部分归TDeskTop拥有。这样诚然不错,然而,不允许子视图重叠是没有道理的,也是行不通的。那么如何处理重叠的子视图呢?
把子视图连接到一个组上的处理,称为插入过程。创建子视图后,将其插入到组中。组记录着子视图的插入顺序,即Z序。Z序决定着组中子视图的显示顺序与事件的传递顺序。组中的子视图链为一循环链表,指针TView *last指向最后视图链中的最后一个子视图。每次插入,都将新的子视图插入到链首。显示的时候,按照插入的顺序逐一显示,因此,最后插入的子视图显示在最前面,覆盖前面插入的子视图。
Turbo Vision的视图可以被定义为模态视图,即只有该视图和其子视图能够与用户交互,程序中的其他对象无法接收到用户的输入。例如:对话框就是一个模态视图。当一个对话框活动时,对话框之外的一切都无法接收到用户的输入。不过,状态行(TStatusLine)是一个例外:在程序运行过程中,状态行一直处于可选用状态,即使程序正在执行一个不含状态行的模态视图时。
在前面,将事件分为鼠标事件、键盘事件、消息事件和空事件,其依据是事件的来源;根据事件的传递方法,可以对事件进行重新分类:位置事件(即鼠标事件)、焦点事件(即键盘事件和命令事件)和广播事件(广播事件和用户定义的信息)。
事件总是从当前的模态视图开始传递的。一般情况下,当前的模态视图是程序对象(TApplication)。当执行一个模态对话框时,该对话框是当前的模态视图。
事件的传递过程位于TGroup::handleEvent函数中,其源码及相应的辅助函数如下:
void TGroup::handleEvent( TEvent& event ){
TView::handleEvent( event ); // 处理鼠标的选择操作
handleStruct hs( event, *this ); // 把事件、视图包装成一个结构
if( (event.what & focusedEvents) != 0 ){ // 考虑焦点事件
phase = phPreProcess;
forEach( doHandleEvent, &hs );
phase = phFocused;
doHandleEvent( current, &hs );
phase = phPostProcess;
forEach( doHandleEvent, &hs );
}
else{
phase = phFocused;
// 考虑位置事件
if( (event.what & positionalEvents) != 0 )
doHandleEvent( firstThat( hasMouse, &event ), &hs );
else // 考虑广播事件
forEach( doHandleEvent, &hs );
}
} // end_of_handleEvent
struct handleStruct
{ // 把事件、视图形成一个处理结构
handleStruct( TEvent& e, TGroup& g ) : event( e ), grp( g ) {}
TEvent& event;
TGroup& grp;
};
static void doHandleEvent( TView *p, void *s )
{ // 子视图p完成焦点事件的处理
handleStruct *ptr = (handleStruct *)s;
// 如果视图p不存在,或者视图p被禁止且事件为位置事件或焦点事件
if( p == 0 || ( (p->state & sfDisabled) != 0 &&
(ptr->event.what & (positionalEvents | focusedEvents)) != 0) )
return;
switch( ptr->grp.phase ){
case TView::phPreProcess:
if( (p->options & ofPreProcess) == 0 )
return;
break;
case TView::phPostProcess:
if( (p->options & ofPostProcess) == 0 )
return;
break;
}
// 如果本视图能够处理此事件,则调用自己的handleEvent进行处理
if( (ptr->event.what & p->eventMask) != 0 )
p->handleEvent( ptr->event );
}
void TGroup::forEach( void (*func)(TView*, void *), void *args )
{ // 以Z序为视图链中的每一个视图调用func函数,参数为args
TView *term = last;
TView *temp = last;
if( temp == 0 ) return;
TView *next = temp->next;
do {
temp = next;
next = temp->next;
func( temp, args );
} while( temp != term );
}
TView *TGroup::firstThat( Boolean (*func)(TView *, void *), void *args )
{ // 在视图链中,以Z序找到第一个调用func、参数为args,
// 返回值为真的子视图
TView *temp = last;
if( temp == 0 ) return 0;
do {
temp = temp->next; // 得到第一个子视图
if( func( temp, args ) == True )
return temp;
} while( temp != last );
return 0;
}
由上面那些长长的源码,我们可以抽出三类事件的传递方式:
1:位置事件(positionalEvents)
从模态视图开始,在子视图链中,以Z序搜寻,找到第一个含有事件发生位置的子视图,然后,模态视图将事件传递给该子视图,由其调用自己的事件处理程序进行处理。由于视图可以重叠,因此,可能有多个前后重叠的子视图都包含有此鼠标事件的位置,以Z序进行搜寻能够保证接收该事件的视图位于最前面(最靠近用户),即用户在其上按动鼠标的视图。
这种过程一直向下进行,直到到达终端视图或者因为事件发生的位置没有视图时才会停止。如有接收视图,则接收视图会处理该事件;如若没有视图,则该事件已到达产生该事件的对象,这个对象会处理自己产生的事件。
2:焦点事件(focusedEvents)
模态视图首先获得焦点事件,然后,按照以下三个阶段发送该焦点事件:
(1)焦点事件以Z序送给视图链中每一个选项中ofPreProcess标志为1的子视图(焦点事件没有被视图链中位于前面的子视图所处理);
(2)如若焦点事件没有被处理,则被传送给程序中当前具有焦点的子视图;
(3)如若焦点事件仍旧没有被处理,则依旧以Z序送给视图链中的每一个选项中的ofPostProcess标志为1的子视图。
程序如下寻找具有焦点的子视图:从模态视图开始,寻找其被选择子视图,该过程一直进行到终端视图,该终端视图即为具有焦点的视图。
3:广播事件
广播事件的传递比较容易理解,以Z序向模态视图的每一个子视图都传递,如若子视图本身就是一个组,则递归进去,保证模态视图的所有子视图都接收到该广播事件。
对于用户定义的新型事件,缺省情况下,Turbo Vision广播这些事件,但用户也可以修改TGroup::handleEvent,自定义其传递方式。
3.3:事件的处理
应该说,应用程序中的对象是如何处理它们接收到的事件并不属于本文讨论的范畴,但由于一些对象在处理事件的过程中,会调用putEvent产生挂起事件,或者调用message函数来发送消息事件,因此,在这里只以TStatusLine为例进行简单的讨论。
TStatusLine的handleEvent(…)及辅助函数摘录如下:
void TStatusLine::handleEvent( TEvent& event ){
TView::handleEvent(event); // 处理视图的选择操作
switch (event.what){
case evMouseDown:{
TStatusItem *T = 0;
do {
TPoint mouse = makeLocal( event.mouse.where );
if( T != itemMouseIsIn(mouse) )
drawSelect( T = itemMouseIsIn(mouse) );
} while( mouseEvent( event, evMouseMove ) );
// 直到出现一个松开鼠标事件,完成一个选择动作
if( T != 0 && commandEnabled(T->command) )
{ // 如果鼠标点击的状态项未被禁用
event.what = evCommand;
event.message.command = T->command;
event.message.infoPtr = 0;
putEvent(event); // 把鼠标事件转换成挂起的命令事件
}
clearEvent(event); // 鼠标事件处理完毕,清除
drawView();
break;
}
case evKeyDown:{
for( TStatusItem *T = items; T != 0; T = T->next ){
if( event.keyDown.keyCode == T->keyCode &&
commandEnabled(T->command)){
event.what = evCommand;
event.message.command = T->command;
event.message.infoPtr = 0;
return; // 对于按键事件,直接将事件转换成命令事件返回
}
}
break;
}
case evBroadcast:
if( event.message.command == cmCommandSetChanged )
drawView(); // 对于广播,改变了命令集,则重新显示状态行
break; } }
Boolean TView::mouseEvent(TEvent& event, ushort mask)
{ // 一直从取事件,直到出现mask、evMouseUp之中的任意一个为止。
do {
getEvent(event);
} while( !(event.what & (mask | evMouseUp)) );
// 如若出现evMouseUp事件,则返回0;否则,返回1
return Boolean(event.what != evMouseUp);
}
这里不再详细罗列出不相关函数的源码,只是给出简单地介绍以助读者能够理解程序的流程,由于其函数的命名相当清楚,当不影响读者对程序的理解。主要想说明的是putEvent的使用:
TStatusLine继承自视图类TView,在其事件处理程序中,首先调用基类的handleEvent处理视图的选择操作;然后,把鼠标事件、键盘事件转换成相应的命令事件,放入到TProgram的静态变量pending中,供下次getEvent调用时返回(即响应相应的鼠标动作或按键动作);对于命令集发生变化的广播,调用显示成员函数drawView重新显示状态行。
至此,事件驱动机制已经清清楚楚、明明白白呈现于读者的面前。最后,以一个简单的例子结束本文:
在如BC++3.1的环境中,在状态行的某项上(例如:Alt-X)按下鼠标左键,事件循环的流程如下(假设此时无挂起事件):
此时,程序中的对象有application,其有三个子对象:menuBar、deskTop、statusLine,其中,deskTop包括两个window对象,每个windows对象包括一个frame、两个scrollBar、一个scroller。事件的循环如下(在无法用图表示的情况下,希望能够讲清楚):
在TGroup::execute()循环中,
getEventTProgram::getEvent(event1)TEvent::getMouseEvent(event1)TEventQueue::getMouseEvent(event1),event1被设置成鼠标按下事件(evMouseDown),然后原路返回至TProgram::getEvent,由于鼠标的这个动作是在TStatusLine上的,故在返回至TGroup::execute之前,首先调用TStatusLine::handleEvent(event1)对鼠标事件进行转换;在TStatusLine::handleEvent(event1)中,将这个鼠标按下动作产生的一系列evMouseAuto事件取出,直到取出evMouseUp事件(完成对鼠标按键动作的响应),此时,event1为evMouseUp事件;然后,生成一挂起的命令事件,命令cmQuit为按键所处的状态项Alt+x上绑定的命令,并清除此按键事件(event1为空),退出TStatusLine::handleEvent(event1),返回至TProgram::getEvent(event1)。在TGroup::handleEvent(event1)中,由于event1已经为空,故直接返回;然后,再次调用TProgram::getEvent(event2),得到刚才产生的挂起的命令事件cmQuit,交由TGroup::handleEvent(event2)发送给其子对象(焦点事件),导致TProgram::handleEvent调用endModal(cmQuit),终止当前应用程序的模态,关闭整个程序,从用户的角度完成对鼠标动作的响应。
本文地址:http://com.8s8s.com/it/it29344.htm