第24天
我们已经可以让应用程序显示窗口了,如果一个应用程序显示了N个窗口,我们也应该让各个窗口可以现切换。实现鼠标切换难度比较大。我们简化为:如果按下f11那么将最下面的窗口放到最上面。
F11的按键编码是0x57。只要在task_a中判断键盘中断输入的数据为这个,就调用sheet_updown(shtctl->sheets[1], shtctl->top - 1)。最下面一层是桌面,最上面一层是鼠标。执行这个函数就可以将最下面的操口移到最上面。
要实现点击鼠标切换窗口基本的逻辑是这样。当鼠标点击画面的某个地方时,我们要按照从上到下的顺序,判断鼠标的位置落在哪个图层的范围内,并且还需要考虑该位置不是不透明的。
if ((mdec.btn & 0x01) != 0) {
/* �接下左键*/
/* �按照从上到下的顺序寻找鼠标所指向的图层 */
for (j = shtctl->top - 1; j > 0; j--) {
sht = shtctl->sheets[j];
x = mx - sht->vx0;
y = my - sht->vy0;
if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {
if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {
sheet_updown(sht, shtctl->top - 1);
break;
}
}
}
}
以上程序中mx,my分别表示鼠标所在屏幕上的x座标和y座标,相当于屏幕来说的,也就是说左上角为(0,0)。sht->vx0和sht->vy0表示该图层左上角相对于屏幕的座标。两个相减得到的x,y其实就是这个图层的相对座标,也就是说如果(x,y)=(0,0)就相当于在图层的左上角而不是屏幕的左上角了。
再实现窗口的移动功能。如果鼠标点击窗口的标题栏不放,移动鼠标那么窗口也跟着鼠标移动,直到鼠标放开。
首先明确窗口标题栏的位置,我们每次创建窗口标题栏左上角在距离窗口左右角(3,3)像素处,而标题栏右下角分别为(窗口右边-3, 21)处。
我们增加两个变量mmx和mmy,这两个变量初始值为-1。每次鼠标左键按下的时候判断该值是否为负数,如果为负数说明鼠标处理通常模式。当鼠标左键在标题栏按下时候保存当前鼠标x,y的坐标,这2个值也就变为正数了,鼠标处理窗口移动模式。当鼠标处于窗口称动模式时,每次移动鼠标都要调用sheet_slide(sht, sht->vx0 + x, sht->vy0 + y)函数移动窗口。
我们用这样的方法判断鼠标点击时到底是处于窗口的哪个位置,突然想起其实我们在显示窗口的时时候也画了个关闭窗口的按钮,只是一直没有用起来。我们可以用相同的方法判断鼠标点击是否在关闭窗口按钮的位置,如果在这个位置那么就关闭窗口。
我们之前已经实现了task_a窗口和console窗口的互相切换,使用了key_to变理,现在使用命令行也可以打开一个窗口,那么我们切换窗口的逻辑就要变化了。先实现tab切换窗口的功能:如果按下tab将键盘输入窗口切换到当前输入窗口的下一层,如果当前输入窗口是最下层那么换切到了上层。如果当前输入窗口关闭,那么当前输入窗口变为下一层。
用key_win这个变量存放当前处理输入模式的窗口地址。操作系统一开始是先运行task_a,产生一个窗口,然后打开console窗口,这里的key_win初始化为task_a的窗口。然后把之前使用key_to的地方全部修改为使用新变量。然后按下Tab时候的代码如下:
if (i == 256 + 0x0f) { /* Tab */
cursor_c = keywin_off(key_win, sht_win, cursor_c, cursor_x);
j = key_win->height - 1;
if (j == 0) {
j = shtctl->top - 1;
}
key_win = shtctl->sheets[j];
cursor_c = keywin_on(key_win, sht_win, cursor_c);
}
上面两个keywin_off和keywin_on是控制窗口标题栏颜色和task_a的光标和task_a窗口的光标。
int keywin_off(struct SHEET *key_win, struct SHEET *sht_win, int cur_c, int cur_x)
{
change_wtitle8(key_win, 0);
if (key_win == sht_win) {
cur_c = -1; /* 删除光标 */
boxfill8(sht_win->buf, sht_win->bxsize, COL8_FFFFFF, cur_x, 28, cur_x + 7, 43);
} else {
if ((key_win->flags & 0x20) != 0) {
fifo32_put(&key_win->task->fifo, 3); /* 命令行窗口光标OFF */
}
}
return cur_c;
}
int keywin_on(struct SHEET *key_win, struct SHEET *sht_win, int cur_c)
{
change_wtitle8(key_win, 1);
if (key_win == sht_win) {
cur_c = COL8_000000;
} else {
if ((key_win->flags & 0x20) != 0) {
fifo32_put(&key_win->task->fifo, 2);
}
}
return cur_c;
}
如果想要让鼠标点击窗口就把所点击的窗口切换为当前窗口也很简单。
for (j = shtctl->top - 1; j > 0; j--) {
sht = shtctl->sheets[j];
x = mx - sht->vx0;
y = my - sht->vy0;
if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {
if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {
sheet_updown(sht, shtctl->top - 1);
if (sht != key_win) {
cursor_c = keywin_off(key_win, sht_win, cursor_c, cursor_x);
key_win = sht;
cursor_c = keywin_on(key_win, sht_win, cursor_c);
}
if (3 <= x && x < sht->bxsize - 3 && 3 <= y && y < 21) {
mmx = mx;
mmy = my;
}
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
if ((sht->flags & 0x10) != 0) {
cons = (struct CONSOLE *) *((int *) 0x0fec);
cons_putstr0(cons, "\nBreak(mouse) :\n");
io_cli();
task_cons->tss.eax = (int) &(task_cons->tss.esp0);
task_cons->tss.eip = (int) asm_end_app;
io_sti();
}
}
break;
}
}
}
这段程序跟之前比想差不多,首先从最上的图层开始一直往下找,直到点到鼠标点击的点所对应的图层,如果找到则把现在的图层key_off。再把找到的图层key_on就行了。
我们已经在操作系统层面实现了定时器的功能,但应用程序还不能调用,接下来我们写定时器的API,先设计API。
获取定时器(alloc)
- edx = 16
- eax = 定时器句柄(由操作系统返回)
设置定时器的发送数据(init)
- edx = 17
- ebx = 定时器的句柄
- eax = 发送的数据
定时器的时间设定(set)
- edx = 18
- ebx = 定时器句柄
- eax = 时间
释放定时器(free)
- edx = 19
- ebx = 定时器句柄
下面看代码:
else if (edx == 16) {
reg[7] = (int) timer_alloc();
} else if (edx == 17) {
timer_init((struct TIMER *) ebx, &task->fifo, eax + 256);
} else if (edx == 18) {
timer_settime((struct TIMER *) ebx, eax);
} else if (edx == 19) {
timer_free((struct TIMER *) ebx);
}
_api_alloctimer: ; int api_alloctimer(void);
MOV EDX,16
INT 0x40
RET
_api_inittimer: ; void api_inittimer(int timer, int data);
PUSH EBX
MOV EDX,17
MOV EBX,[ESP+ 8] ; timer
MOV EAX,[ESP+12] ; data
INT 0x40
POP EBX
RET
_api_settimer: ; void api_settimer(int timer, int time);
PUSH EBX
MOV EDX,18
MOV EBX,[ESP+ 8] ; timer
MOV EAX,[ESP+12] ; time
INT 0x40
POP EBX
RET
_api_freetimer: ; void api_freetimer(int timer);
PUSH EBX
MOV EDX,19
MOV EBX,[ESP+ 8] ; timer
INT 0x40
POP EBX
RET
现在来看应用程序怎么写
#include <stdio.h>
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_putstrwin(int win, int x, int y, int col, int len, char *str);
void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
void api_initmalloc(void);
char *api_malloc(int size);
int api_getkey(int mode);
int api_alloctimer(void);
void api_inittimer(int timer, int data);
void api_settimer(int timer, int time);
void api_end(void);
void HariMain(void)
{
char *buf, s[12];
int win, timer, sec = 0, min = 0, hou = 0;
api_initmalloc();
buf = api_malloc(150 * 50);
win = api_openwin(buf, 150, 50, -1, "noodle");
timer = api_alloctimer();
api_inittimer(timer, 128);
for (;;) {
sprintf(s, "%5d:%02d:%02d", hou, min, sec);
api_boxfilwin(win, 28, 27, 115, 41, 7 );
api_putstrwin(win, 28, 27, 0 /* �• */, 11, s);
api_settimer(timer, 100); /* 1秒*/
if (api_getkey(1) != 128) {
break;
}
sec++;
if (sec == 60) {
sec = 0;
min++;
if (min == 60) {
min = 0;
hou++;
}
}
}
api_end();
}
这段程序先的开一个窗口,然后设置定时器,超时时间为1秒,超时之后向消息队列发送数据为128,如果应用程序接收到的中断不是128说明用户按下了其他键,那就退出循环,程序结束。
这里有一个问题,如果已经设置了定时器但是在这1秒中间应用程序已经关闭了,那么这1秒的定时器超时之后还是会向消息队列发送128这个数据,那么console就会以为是一个键盘输入中断,会在窗口上打印一个乱码。
我们应该在应用程序关闭之后也把定时器给关掉。
先写一个关闭定时器的函数
int timer_cancel(struct TIMER *timer)
{
int e;
struct TIMER *t;
e = io_load_eflags();
io_cli(); /* �在设置过程中禁止改变定时器状态 */
if (timer->flags == TIMER_FLAGS_USING) { /* 是否需要取消 */
if (timer == timerctl.t0) {/* 第一个定时器的取消处理 */
t = timer->next;
timerctl.t0 = t;
timerctl.next = t->timeout;
} else {
/* 非第一个定时器的取消处理 */
/* 找到timer前一下定时器 */
t = timerctl.t0;
for (;;) {
if (t->next == timer) {
break;
}
t = t->next;
}
t->next = timer->next;
}
timer->flags = TIMER_FLAGS_ALLOC;
io_store_eflags(e);
return 1; /* 取消处理成功 */
}
io_store_eflags(e);
return 0; /* 不需要取消处理 */
}
实际一个应用程序可以设置任意多个定时器,所以当应用程序结束的时候应该关闭应用程序设置的所有定时器。为了方便我们给TIMER结构体中新设一个标志位,用来标志这个定时器是否在应用程序结束时也一起销毁。
else if (edx == 16) {
reg[7] = (int) timer_alloc();
((struct TIMER *) reg[7])->flags2 = 1; /* 允许自动取消 */
在api_allock中直接把这个标志位设置有1,表示允许自动取消。
再定一个函数用于取消所有函数
void timer_cancelall(struct FIFO32 *fifo)
{
int e, i;
struct TIMER *t;
e = io_load_eflags();
io_cli(); /* �在设置过程中禁止改变定时器的状态 */
for (i = 0; i < MAX_TIMER; i++) {
t = &timerctl.timers0[i];
if (t->flags != 0 && t->flags2 != 0 && t->fifo == fifo) {
timer_cancel(t);
timer_free(t);
}
}
io_store_eflags(e);
return;
}
然后在cmd_app函数中start_app这个函数调用结束之后再补上timer_cancell(&task->fifo)就可以了。