C++ - Event-Driven Programming
Introduction
In the classical approach to programming, events happen in apre-defined sequence. In fact, classical programmers go the extent ofdefining programs as sets of instructions that are executedsequentially by the computer. Of course there are loop and jumps butthese are still programmed sequentially. However, there are timeswhen you want the program to respond to events on its own rather thancheck diligently for the event. A typical example is theincorporation of so-called "hotkeys" into a program. In theclassical approach, the programmer has to constantly check for thesekeys, every time a key is pressed. This means a lot of extra effortfor the programmer and it only gets worse as the complexity of theprogram increases.
An ideal situation would be one where the program responds of itsown accord to such events like hotkeys. It can be done using inputfilters and other such innovations but if the program isobject-oriented, conversion to event-driven programming is mucheasier.
Event-driven programming is where the program responds to eventsrather than follow a sequential course of instructions. Event-drivenprogramming is especially useful in object-oriented environments andwhere graphical user interfaces are being constructed. Mosthigh-level GUI APIs (OWL, MFC, Turbo Vision, etc) use event-drivenprogramming to embed basic features into every program.
Principles of Event-driven Programming
Firstly, the computer is a sequential device and therefore cannot,on its own accord, respond to events. To simulate thisauto-responding, a kernel must be constructed. This kernel must knowabout all objects in the program and must allow communication withthese objects.
Events can be defined as any external influence on the computer.Keystrokes are events as are mouse movements and clicks. Events canalso be explicitly sent from one object to another in the form of amessage. These are useful for intra-process and inter-processcommunication. Idle events can also be generated automatically by thekernel to allow for background processing.
Lastly, a basic set of objects must be built, following strictguidelines in terms of interfacing with the kernel. Each top-levelobject must have the ability to respond to events.
Thus, instead of executing a sequential program, you simplyexecute the kernel. The kernel will then collect all events,including various forms of input, and pass it on to the relevantobjects for processing.
GUIs
In a graphical user interface, the basic object is usually thewindow. Windows should therefore be capable of processing input. Thusthey can process events. All descendants of the basic window objectmust also have the capability of processing events.
The kernel is normally part of the compiler's libraries (TurboVision) or the operating system (OWL/Microsoft Windows). This kernelprovides the basis for all activity on the computer by gathering anddispatching events.
Event-driven programming lends itself to multi-tasking since thewindows do not themselves have the input focus. The kernel alwayscontrols the program and can just as easily control two programs onthe screen. Events can be filtered from one window, where they arenot needed, to another where they are. Also, events can be bufferedand processed when the kernel is idle. Since most graphical operatingsystems are multi-tasking, their programming APIs are invariably OOand event-driven (eg. OWL, MFC) so as to most accurately model theOS.
Windows as objects can either be self-regenerative or overlapped.MS-Windows uses the former technique where every window must be ableto redraw itself at any time. Some older text-based GUIs used thelatter technique of each window storing its background - thedisadvantage of this is that you cannot change the window order -advantage is that its easier to program. Each window and other objectmust process events as fast as possible. If the event takes long toprocess, it must be shifted into an internal queue and processed inthe background. This enables cooperative multi-tasking.
Events can be packages into objects where each type of event isidentified by a code. Simple GUIs can use keystrokes to denoteevents.
The kernel can be run in the background or as the main process. Ifthe kernel is part of the operating system, it is always stable andcannot be corrupted by incorrect interfacing on the part of theprogrammer. However, if the kernel is the main process for theprogram, all objects created must conform to the interface - this isessential for cooperative multi-tasking - if one object misbehavesall the others will fail to perform satisfactorily.
Sample Event-Driven GUI
This program creates a kernel in the object called Application.The basic Window object is kept in a linked list attached to thekernel. The kernel continuously searches for keystrokes and, if itfinds any, passes these to the topmost window. The topmost windowwill try to process the keystroke, and if it fails will return thisstatus to the kernel, which continues to traverse the linked listuntil it encounters success.
Window has two event-processing routines which are virtual,ProcessKey and IdleAction. The former processes keystrokes and thelatter does background processing when no events are being created.Mouse events and inter-object events are not supported.
BWindow is derived from Window and enhances it by drawing a borderaround the window. It also shrinks the window by one block all aroundto exclude the border.
HorzMenu and VertMenu are basic horizontal and vertical menus thatfit within a Window. They each take a list of menu items defined by aMenuData object. From these are derived the main menu and file menuof the program.
The StatusLine and MessageWindow are specialist derivation fromBWindow, which display a scrolling status bar and a modal messagebox.
上面一大堆,来看一下,真正的实现吧,这个感觉跟Qt的实现很相似,都是创建一个QApplication,
然后创建一些窗体及子窗体,然后再进入QApplicaion的事件循环中。
//**********************************************************************
// main program body
//**********************************************************************
void main ()
{
// createapplication kernel
ApplicationMyApp;
// make initialwindows
MyApp.OpenWindow(new BWindow (1, 4, 80, 24, 1));
MyApp.OpenWindow(new StatusLine (25, "Window Demonstration :: Event-drivenprogramming :: H. Suleman :: 1996"));
MyApp.OpenWindow(new TopMenu);
// start mainevent-processing loop
MyApp.Run (); //这里进入事件循环应该是不返回的是阻塞的,直接用户退出。
}
再来分析一下Application的实现:
//**********************************************************************
// main kernel class
//**********************************************************************
class Application
{
public:
Window *Head; // ptr to topmost window
Application ();
~Application ();
void OpenWindow (Window *p );
void CloseWindow();
void Run ();
};
extern Application*anApp; // global variable to access kernel
Application *anApp; // global variable to access kernel
// initialize kernelwith no windows
Application::Application()
{
Head=NULL; 这个Head应该是一个链表的头,用于将所有的窗体串联起来。这个Application就能顺着这个串找到所有的窗体啦。
anApp=this; 这个应该是使用全局变量记录Application,在Qt中也有,就是qApp这个指针,是全局的。
}
// kill kernel bydestroying all windows
Application::~Application()
{
while(Head!=NULL) //顺着这个链表,依将销毁所有窗体。这个链表还真好使呀。
CloseWindow();
}
// attach a newwindow to the kernel
voidApplication::OpenWindow ( Window *p )
{
p->Next=Head; 将新窗口加入到链表中
Head=p;
}
// remove a windowfrom the kernel's list
voidApplication::CloseWindow ()
{
if (Head!=NULL)
{
Window*p=Head;
Head=Head->Next;
delete p; 关闭窗体就是从链表上摘除,并释放内存
}
}
// mainmessage-processing loop
voidApplication::Run () 事件循环了,这里很关键!!!
{ 这里是阻塞式的工作,依读按键值(阻塞),依将将调用链表上的每个部分的处理函数,
直到有窗体将这个按键成功处理并返回1,则停止搜索链表。
char ch;
int Status=0;
do {
if (kbhit ()) // check for key pressed
{
ch=getch(); // get keystoke
if(ch!='Q')
{ // pass it to all windows until it is //依将调用链表上所有窗体的处理函数,直到有人处理了。
Window*p=Head; // .. processed
while((p!=NULL) && ((Status=(p->ProcessKey (ch)))==1))
p=p->Next;
}
}
else // otherwise, do idle processing //如果没有按键则执行链表中的所有窗体的idel()函数,这个有什么用呀???
{
Window*p=Head; // .. for all windows
while(p!=NULL)
{
p->IdleAction ();
p=p->Next;
}
}
} while(Status!=2); // until a windows returns exit state //直到链表中处理函数的返回值为2才退出事件循环,
//这应该就相当于用户按Q退出键,某个窗体处理后,并返回2则退出。
}
上面应该就是最根本的事件产生(按键),事件分发(由Applicaion::run()函数读取到按键后,顺着链表依次分发给所有的窗体,
直到有人处理了。事件处理(就是调用链表的按键处理函数)。
这么一看效率并不高,因为如果用户按键过快,但处理速度过慢的话,可以会有按键丢失响应,所以
应该使用按键输入缓冲区,再加上多线程的按键消息队列才行。
下面来继续分析子窗体,每一个是所有窗体的基类:
//**********************************************************************
// base class forall windows and window-elements
//**********************************************************************
class Window
{
public:
byte *Store; // storage for background image
byte x1, y1, x2,y2; // windows coordinates
byte oldx1,oldy1, oldx2, oldy2; // old window coordinates
byte oldx, oldy,oldattr; // old cursor position/colours
Window *Next; // ptr to window underneath
Window ( bytexx1, byte yy1, byte xx2, byte yy2 );
~Window ();
virtual intProcessKey ( char ); 虚函数,子类可以重载
virtual voidIdleAction (); 虚函数,子类可以重载
};
// create a newwindow
Window::Window (byte xx1, byte yy1, byte xx2, byte yy2 )
{
x1=xx1; y1=yy1;x2=xx2; y2=yy2; // save coordinates
struct text_infoti; // save information about old window
gettextinfo(&ti);
oldx1=ti.winleft;
oldy1=ti.wintop;
oldx2=ti.winright;
oldy2=ti.winbottom;
oldx=ti.curx;
oldy=ti.cury;
oldattr=ti.attribute;
Store=(byte*)malloc(4000); // get memory and store background image
gettext (x1, y1,x2, y2, Store);
window (1, 1, 80,25); // set up window
window (x1, y1,x2, y2);
textattr (7);
}
// destroy a window
Window::~Window ()
{
window (1, 1, 80,25); // restore window coordinates
window (oldx1,oldy1, oldx2, oldy2);
puttext (x1, y1,x2, y2, Store); // restore background image
free (Store); // free image memory
textattr(oldattr); // restore cursor/colours
gotoxy (oldx,oldy);
}
// placeholder forkey handling routines in descendants
intWindow::ProcessKey ( char )
{ 按键处理函数,这个很关键,子类可以重载实现自己的函数,在c++的
动态联编的时候,会调用子类的函数。这里的返回值很关键,
因为Application就是检测的这个返回值,如果是2的话就退出事件循环
整个程序退出。
// return values :
// 1 = key was notused - pass to next window 按键没有使用,传递到另一个窗口
// 0 = key was usedup 按键已经被使用
// 2 = applicationhas terminated 请求退出
return 1;
};
// placeholder foridle processing in descendants
voidWindow::IdleAction ()
{
};
下面再来看看窗体B,这是一个有边框的窗体BorderWindow.
这个好像没有什么,就是为窗体的四周画了一个边框,估计下面还会有
具体的类继续它,以实现更加复杂的功能。
class BWindow :public Window
{
public:
BWindow ( bytexx1, byte yy1, byte xx2, byte yy2, byte block );
void directput (int x, int y, byte cha, byte col );
void Block ();
};
BWindow::BWindow (byte xx1, byte yy1, byte xx2, byte yy2, byte block )
: Window ( xx1,yy1, xx2, yy2 )
{
if (block==1)
Block ();
}
// output acharacter directly to the video memory
voidBWindow::directput ( int x, int y, byte cha, byte col )
{
unsigned intxo=x;
unsigned intyo=y;
unsigned intoffset=(yo-1)*160+(xo-1)*2;
pokeb (0xB800,offset, cha);
pokeb (0xB800,offset+1, col);
}
// make a blockaround the current window and shrink it
void BWindow::Block()
{
int a;
directput (x1,y1, 218, 7);
directput (x2,y1, 191, 7);
directput (x1,y2, 192, 7);
directput (x2,y2, 217, 7);
for ( a=1;a<x2-x1; a++)
{
directput(x1+a, y1, 'ฤ',7);
directput(x1+a, y2, 'ฤ',7);
}
for ( a=1;a<y2-y1; a++)
{
directput (x1,y1+a, 'ณ',7);
directput (x2,y1+a, 'ณ',7);
}
window (x1+1,y1+1, x2-1, y2-1);
clrscr ();
}
下面是具体的继承具有边框的窗体的状态栏了:
class StatusLine :public BWindow
{
public:
char Data[100]; // string for status line
int waitcount; // delay in-between moves
StatusLine ( bytey, char *s );
virtual voidIdleAction ();
};
StatusLine::StatusLine( byte y, char *s )
: BWindow (1, y,80, y, 0)
{
strcpy (Data, s);
while (strlen(Data)<80) // pad line with spaces on left/right
{
strcat (Data," ");
if (strlen(Data)<80)
{
chart[100]=" ";
strcat (t,Data);
strcpy(Data, t);
}
}
waitcount=0;
IdleAction (); // display initial string
}
现在才明白idle的意思了,idle就是空闲的时间,从前面的Application的事件循环中可以看到
当检测到一个按键的时候就顺着链表调用ProcessKey()通知每一个窗体,看看谁能处理这个按键。
如果是没有按键的时候,这个时候总不能让CPU空转的吧???
好吧,所以就有了一个空闲函数idle()用于处理一些平常非紧急的任务。
从下面的代码可以看出,这个StatusLine的idel()函数就是移动了任务栏上的字,使它有动态的效果。
俺大老张现在才明白过来呀,记得以前MFC的时候,的确是不理解为什么要这么写。
看来有些事情还得看看底层实现才行。这也是俺不喜欢高层软件的原因,像Java,.Net这都不是俺的强项。
好多年不用Windows了。唉。
// display stringand move it one space to the left
voidStatusLine::IdleAction ()
{
waitcount++; // divide-by-N counter to slow down
if(waitcount!=600) // .. iterations
return;
waitcount=0;
for ( int a=0;a<strlen (Data); a++ ) // display string
directput(a+1, y1, Data[a], 112);
char ch=Data[0]; // rotate string left
for ( a=0;a<strlen (Data)-1; a++ )
Data[a]=Data[a+1];
Data[strlen(Data)-1]=ch;
}
今天分析到这里,明天继续吧。
By zhangshaoyan atMay 28,2015.
//
好,今天来继续分析菜单。
1.水平菜单
class HorzMenu :public BWindow
{
public:
MenuData *Menu; // pointer to a list of menu items
byte Separator; // distance between menu items
byte Position; // current position of selector block
HorzMenu ( bytexx1, byte yy1, byte xx2, byte yy2,
byteblock );
void InitMenu (MenuData *md );
void Draw ( intPos, int State );
virtual intProcessKey ( char ch ); 这个是继承的虚函数,用于处理按键。
};
HorzMenu::HorzMenu (byte xx1, byte yy1, byte xx2, byte yy2,byte block )
: BWindow ( xx1,yy1, xx2, yy2, block )
{
}
// initialize menuand draw items on screen
voidHorzMenu::InitMenu ( MenuData *md )
{
Position=0;
Menu=md;
MenuItem*p=Menu->Head; // find maximum length of item
int MaxLength=0;
while (p!=NULL)
{
if (strlen(p->Data)>MaxLength)
MaxLength=strlen (p->Data);
p=p->Next;
}
Separator=MaxLength+2; // set separator distance
for ( int a=0;a<Menu->Count; a++ ) // write items
Draw (a, 0);
Draw (Position,1); // highlight first item
}
// write orhighlight an item
void HorzMenu::Draw( int Pos, int State )
{
MenuItem*m=Menu->Head; // search linked list for item
int a=Pos;
while (a>0)
{
a--;
m=m->Next;
}
gotoxy(Pos*Separator+1, 1); // set cursor position & colour
if (State==0)
textattr (7);
else
textattr(112);
for ( a=0;a<strlen (m->Data); a++ ) // output string
putch(m->Data[a]);
}
// process left andright arrows
intHorzMenu::ProcessKey ( char ch )
{
switch (ch)
{
case 75 : if(Position>0) // left arrow 就是根据按键,画出来的呀。
{
Draw (Position, 0); // move left
Position--;
Draw (Position, 1);
};
return 0; //该按键被成功处理,返回0通知Application不用再继续分发了。
case 77 : if(Position < (Menu->Count-1)) // right arrow
{
Draw (Position, 0); // move right
Position++;
Draw (Position, 1);
}
return 0; //该按键被成功处理,返回0通知Application不用再继续分发了。
default :return 1; // pass keystroke to next window
//返回1通知Application,该按键值,我不关心,我也不处理,你继续分发给别人吧。
}
}
2、竖向菜单
class VertMenu :public BWindow
{
public:
MenuData *Menu; // pointer to a list of menu items
byte Position; // current position of selector block
VertMenu ( bytexx1, byte yy1, byte xx2, byte yy2,
byteblock );
void InitMenu (MenuData *md );
void Draw ( intPos, int State );
virtual intProcessKey ( char ch );这个是继承的虚函数,用于处理按键。
};
VertMenu::VertMenu (byte xx1, byte yy1, byte xx2, byte yy2,
byte block )
: BWindow ( xx1,yy1, xx2, yy2, block )
{
}
// initialise menuand draw items on screen
voidVertMenu::InitMenu ( MenuData *md )
{
Position=0;
Menu=md;
for ( int a=0;a<Menu->Count; a++ ) // write items
Draw (a, 0);
Draw (Position,1); // highlight first item
}
// write orhighlight an item
void VertMenu::Draw( int Pos, int State )
{
MenuItem*m=Menu->Head; // search linked list for item
int a=Pos;
while (a>0)
{
a--;
m=m->Next;
}
gotoxy (1,Pos+1); // move cursor and set colour
if (State==0)
textattr (7);
else
textattr(112);
for ( a=0;a<strlen (m->Data); a++ ) // output string
putch(m->Data[a]);
}
// process up anddown arrows
intVertMenu::ProcessKey ( char ch )
{
switch (ch)
{
case 72 : if(Position>0) // up arrow
{
Draw (Position, 0); // move up
Position--;
Draw (Position, 1);
};
return 0;
case 80 : if(Position < (Menu->Count-1)) // down arrow
{
Draw (Position, 0); // move down
Position++;
Draw (Position, 1);
}
return 0;
case 27 :anApp->CloseWindow (); // ESC = close window 这个可了不得呀。
Return 0; //anApp可是Application的全局指针,调用它的CloseWinow()会将链表上所有窗体都删除掉。
default :return 1; // pass keystroke to next window
}
}
现在只是简单的分析一下按键在链表上各个窗体之前进行传输的规定,等会儿再过来分析菜单具体是如何一笔一画的进行绘制出来的呀。
再来看看菜单的数据项:
class MenuItem
{
public:
char Data[100]; // actual data for menu
MenuItem *Next; // pointer to next item
MenuItem ( char*p )
{
strcpy (Data,p);
Next=NULL;
}
};
数组用于存储该菜单项目的文本,同时具有next指针用于指向一下菜单条目,难道也要将菜单条目组织成链表???
真让俺说准了,看下面
class MenuData
{
public:
MenuItem *Head,*Tail; // linked list pointers 记录链表中菜单项目的头和尾。
unsigned intCount; // number of items in list
MenuData () // initialize list
{
Head=NULL;
Tail=NULL;
Count=0;
};
void AddItem (MenuItem *m ) // add an item to the list 添加一个菜单条目到链表中。
{
if(Head==NULL)
Head=m;
else
Tail->Next=m;
Tail=m;
Count++;
}
~MenuData ();
};
遍历链表,删除掉所有的菜单条目。
MenuData::~MenuData()
{
while(Head!=NULL)
{
MenuItem*p=Head;
Head=Head->Next;
delete p;
}
}
来看一下实现的顶层菜单
class TopMenu :public HorzMenu
{
public:
MenuData *md; // menu item linked list
TopMenu ();
~TopMenu () {delete md; };
virtual intProcessKey ( char ch );
};
// set menu item anddisplay menu
TopMenu::TopMenu ()
: HorzMenu (1, 1,80, 3, 1)
{
md=new MenuData; 这个是主菜单,根了呀!!
md->AddItem(new MenuItem ("File"));这个使用了new但是不会造成memoryleak,因为内存会挂到链表上退出时自动删除。
md->AddItem(new MenuItem ("Help"));
md->AddItem(new MenuItem ("About"));
InitMenu (md);
};
// process arrowsand hotkeys
intTopMenu::ProcessKey ( char ch )
{
switch (ch)
{
case 'f' :
case 'F' :anApp->OpenWindow (new FileMenu); 按F键时,将打开文件菜单,这里为什么要使用new呢???
//OpenWindow()的实现是将新窗体挂到链表上,如果返回按F的话,那不创建N多个?一个就够了!
return 0;
case 'h' :
case 'H' :anApp->OpenWindow (new MessageWindow ("no HELP available"));
return 0;
case 'a' :
case 'A' :anApp->OpenWindow (new MessageWindow ("Windows Demonstrationprogram"));
return 0;
case 13 :switch (Position) // process ENTER
{
case 0 : anApp->OpenWindow (new FileMenu);
return 0;
case 1 : anApp->OpenWindow (new MessageWindow ("no HELPavailable"));
return 0;
default : anApp->OpenWindow (new MessageWindow ("WindowsDemonstration program"));
return 0;
}
default :HorzMenu::ProcessKey (ch); // process arrows
return 0;
}
}
再来看一下FileMenu的实现
class FileMenu :public VertMenu
{
public:
MenuData *md; // menu item linked list
FileMenu ();
~FileMenu () {delete md; };
virtual intProcessKey ( char ch );
};
// set menu itemsand display menu
FileMenu::FileMenu()
: VertMenu (3, 3,12, 7, 1)
{
md=new MenuData;
md->AddItem(new MenuItem ("Open"));
md->AddItem(new MenuItem ("-------"));
md->AddItem(new MenuItem ("Exit"));
InitMenu (md);
}
// process arrowsand hotkeys
intFileMenu::ProcessKey ( char ch )
{
switch (ch)
{
case 'e' :
case 'E' :return 2; 这是退出的意思呀!!!
case 'O' :
case 'o' :anApp->OpenWindow (new MessageWindow ("Not yetimplemented"));
return 0;
case 13 :switch (Position) // process ENTER
{
case 0 : anApp->OpenWindow (new MessageWindow ("Not yetimplemented"));
return 0;
case 2 : return 2;
default : return 0;
}
default :VertMenu::ProcessKey (ch); // process arrows
return 0;
}
}
再来看一下消息窗体的实现
class MessageWindow: public BWindow
{
public:
MessageWindow (char *s );
virtual intProcessKey ( char );
};
// create windowwith border and display message
MessageWindow::MessageWindow( char *s )
: BWindow (5, 10,75, 14, 1)
{
intstart=(68-strlen (s))/2;
for ( int a=0;a<start; a++ )
putch (' ');
for ( a=0;a<strlen (s); a++ )
putch (s[a]);
gotoxy (22, 3);
cout <<"press any key to continue";
}
// close window whenuser presses any key
intMessageWindow::ProcessKey ( char )
{
刚才又看了一下Application的CloseWindow()的实现,发现不是以前想的那样
不是删除链表上所有的窗体,则是删除当前窗体,所以调用这个anApp->CloseWindow()就是删除自身呀!!!!
anApp->CloseWindow ();
return 0;
}
好了,现在分析完了事件驱动的GUI设计方法,俺估计MFC和Qt以及Java的GUI最初的原始也不过如此。
Application是最重要的一个对象了,包括一个链表用于将所有的子窗体组织起来,
同时包括一个事件循环,用于读取按键值并分发到链表上的窗体上。
当然要实现一个牛B的GUI是需要技巧与经验的,当然也得需要建模头脑。
不过有一点得明确,再好的GUI也是在屏幕上一点一线一面的绘制出来的。
By zhangshaoyan atMay 28,2015.