本章承接之前的窗口相关内容进行完善,增加了一些诸如拖动鼠标移动窗口、点击鼠标切换窗口等等的操作,使得操作系统用起来更像Windows的表现。最后还增加了几个定时器API的编写,供外部应用程序调用。
一 窗口切换
加入两个功能,可以切换指定窗口到最上层图层:
【1】按 F11 键时可以将最下面的图层切换到最上面:
if(256 <= i && i <= 511) { /* 键盘数据 */
if(i == 256 + 0x57 && shtctl->top > 2) { /* 若为F11键 */
sheet_updown(shtctl->sheets[1], shtctl->top - 1); /* 将最下面的窗口移到最顶层 */
}
}
【2】点击窗口区域,使得该窗口切换到最上面(这就需要根据鼠标点击的位置,从上到下判断该区域位于哪个图层的范围内,并且还需要确保该位置不是透明色区域):
int j, x, y;
struct SHEET *sht;
/* 中略 */
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;
}
}
}
}
当然,做到这一步仅仅是将图层移动到最上层,然而标题栏的颜色、输入的光标等并没有真正切换过来,后面小节会逐步完善。
二 窗口移动和窗口关闭
加入两个功能,分别可以拖拽窗口和关闭窗口:
【1】类似Winsows,拖拽窗口的标题栏,可以移动窗口(这就需要记录鼠标指针移动的距离,为此增加了两个变量 mmx 和 mmy ,记录的时移动之前的坐标。当这两个变量都是负数时,不处于移动模式):
int j, x, y, mmx = -1, mmy = -1;
struct SHEET *sht = 0;
/* 中略 */
if ((mdec.btn & 0x01) != 0) { /* 按下左键 */
if (mmx < 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);
if (3 <= x && x < sht->bxsize - 3 && 3 <= y && y < 21) { /* 标题栏 */
mmx = mx; /*进入窗口移动模式*/
mmy = my;
}
break;
}
}
}
} else { /* 处于移动模式 */
x = mx - mmx;
y = my - mmy;
sheet_slide(sht, sht->vx0 + x, sht->vy0 + y); /* 平移窗口 */
mmx = mx;
mmy = my;
}
} else { /* 没有按下左键 */
mmx = -1; /* 返回通常模式 */
}
【2】类似Windows,点击窗口右上角的“X”,可以关闭该窗口(结束部分可以参考之前强制结束部分代码【操作系统】30天自制操作系统--(21)用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);
/* 【1】点击标题栏 */
if (3 <= x && x < sht->bxsize - 3 && 3 <= y && y < 21) {
/* 中略 */
}
/* 【2】点击“×”按钮 */
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;
}
}
}
三 切换窗口输入
之前【操作系统】30天自制操作系统--(16)命令行窗口1已经介绍过通过 Tab 键切换窗口输入的操作,但是只能在两个窗口间进行切换,那现在多个窗口也有相互切换的需求怎么办呢?作者的想法是用一个 key_win 变量存放当前属于输入模式的窗口地址,当应用程序窗口处于输入模式时被关闭需要系统切换到最上层的窗口。
我们使用 SHEET 结构中的 task 来判断数据发送对象的 FIFO,为此也要在 sht_cons->task 中加入 TASK 结构的地址,这样我们无法判断窗口是不是由应用程序产生的,于是我们要通过 SHEET 结构中的 flags 成员经行判断。只有命令行需要光标 on/off,这也是通过 flags 判断的
flags的作用:
(1)是不是由应用程序生成的窗口 —— 以 0x10 比特位来判断;
(2)命令行窗口需要光标闪烁 —— 以 0x20 比特位来判断;
void HariMain(void){
struct SHEET *sht = 0, *key_win; /*这里!*/
// ...
key_win = sht_win;
sht_cons->task = task_cons;
sht_cons->flags |= 0x20; /*有光标*/
for(;;){
// ...
if (key_win->flags == 0) { /* 输入窗口被关闭 */
key_win = shtctl->sheets[shtctl->top - 1];
cursor_c = keywin_on(key_win, sht_win, cursor_c);
}
if(256 <= i && i <= 511){ /* 键盘数据 */
// ...
if (s[0] != 0) { /*一般字符*/
if (key_win == sht_win) { /*发送给任务A */
if (cursor_x < 128) {
/*显示一个字符之后将光标后移一位*/
s[1] = 0;
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, s, 1);
cursor_x += 8;
}
} else { /*发送给命令行窗口*/
fifo32_put(&key_win->task->fifo, s[0] + 256);
}
}
// ...
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);
}
// ...
if ((sht->flags & 0x10) != 0) { /*该窗口为应用程序窗口*/
// ...
}
}
}
}
上述代码中的 keywin_on 和 keywin_off 的作用都是控制窗口标题栏的颜色和光标闪烁:
/* 【1】关闭聚焦窗口----标题栏变灰,删除闪烁光标 */
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;
}
/* 【2】聚焦窗口----标题栏变蓝,使能闪烁光标 */
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); /*命令行窗口光标ON */
}
}
return cur_c;
}
/* 改变窗口标题栏的颜色 */
void change_wtitle8(struct SHEET *sht, char act)
{
int x, y, xsize = sht->bxsize;
char c, tc_new, tbc_new, tc_old, tbc_old, *buf = sht->buf;
if (act != 0) {
tc_new = COL8_FFFFFF;
tbc_new = COL8_000084;
tc_old = COL8_C6C6C6;
tbc_old = COL8_848484;
} else {
tc_new = COL8_C6C6C6;
tbc_new = COL8_848484;
tc_old = COL8_FFFFFF;
tbc_old = COL8_000084;
}
for (y = 3; y <= 20; y++) {
for (x = 3; x <= xsize - 4; x++) {
c = buf[y * xsize + x];
if (c == tc_old && x <= xsize - 22) {
c = tc_new;
} else if (c == tbc_old) {
c = tbc_new;
}
buf[y * xsize + x] = c;
}
}
sheet_refresh(sht, 3, 3, xsize, 21);
return;
}
同时,cmd_app 中也做了修改,主要修改了应用程序结束是自动关闭窗口的部分。因为没有运行应用程序的命令行窗口,其task也不为0,所以需要通过flags的0x10比特位来判断是否自动关闭:
for(i = 0;i < MAX_SHEETS; i ++)
{
sht = &(shtctl->sheets0[i]);
if ((sht->flags & 0x11) == 0x11 && sht->task == task)
{
/* 找到应用程序的遗留窗口并且关闭 */
sheet_free(sht); /* 关闭 */
}
}
我们还修改了hrb_api,为了在打开窗口的地方启用自动关闭窗口的功能,我们将flags和0x10进行OR运算:
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
/* 中略 */
} else if (edx == 5) {
sht = sheet_alloc(shtctl);
sht->task = task;
sht->flags |= 0x10; /* 这里! */
sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
sheet_slide(sht, 100, 50);
sheet_updown(sht, 3); /* 图层高度3位于task_a之上 */
reg[7] = (int) sht;
} else if (edx == 6) {
/* 中略 */
}
上面是通过TAB键进行切换,在其中添加一小段代码即可实现鼠标操作切换:
void HariMain(void){
/* 中略 */
for(;;){
/* 中略 */
if(256 <= i && i <= 511){ /* 键盘数据 */
/* 中略 */
}else if(512 <= i && i <= 767){ /* 鼠标数据 */
/*中略*/
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);
}
/*中略*/
}
}
}
以上,便完善了第一节提到的图层切换过程中,对于标题栏的颜色、输入的光标等的处理。
四 定时器API
回顾前面的API编写步骤:
【1】设计API的功能和形参;
【2】调用操作系统内部的函数实现这个API;
【3】提供 GLOBAL 的函数,指定入参,通过 INT 0X40 的调用,并传递EDX的数值,可以执行上一步实现的功能;
【4】外部编写应用程序,调用 ,nas 文件中的函数,实现预期的功能。
再设计几个定时器API以备后用:
【1】设计几个API如下:
【2】依据此修改 console.c :
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);
}
【3】接下来几个API的汇编:
_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
【4】最后是应用程序的实现:
/* noodle.c */
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秒 */
/* 定时器超时产生128 */
/* 如果不是128,说明是用户按下回车键或者是其他的什么键,这时候应用程序结束退出 */
if (api_getkey(1) != 128) {
break;
}
sec++;
if (sec == 60) {
sec = 0;
min++;
if (min == 60) {
min = 0;
hou++;
}
}
}
api_end();
}
至此,定时器API基本编写并调用完成。
【注意】:这边延伸出一个小问题,就是在应用程序中调用定时器,一段时间之后应用程序退出,需要立即关闭定时器,否则定时器的数据会发送给命令行窗口。简单来说,就是我们需要在结束应用程序的同时取消定时器。
我们需要在定时器增加一个标记 flags2 ,用来标记该计时器是否需要在应用程序结束时自动取消,通过没有的话,命令行中用来控制光标闪烁的计时器会被取消掉了:
struct TIMER {
struct TIMER *next;
unsigned int timeout;
char flags, flags2; /* 这里! */
struct FIFO32 *fifo;
int data;
};
timer_alloc 时将 flags2 设为0, hrb_api 中应用程序申请定时器时置为1,这样就能区别操作系统内部的定时器和应用程序申请的定时器,以便在结束应用程序时,精准消灭它申请的定时器:
/* timer_alloc 时将 flags2 设为0 */
struct TIMER *timer_alloc(void)
{
int i;
for (i = 0; i < MAX_TIMER; i++) {
if (timerctl.timers0[i].flags == 0) {
timerctl.timers0[i].flags = TIMER_FLAGS_ALLOC;
timerctl.timers0[i].flags2 = 0; /* 这里! */
return &timerctl.timers0[i];
}
}
return 0;
}
/* hrb_api 中应用程序申请定时器时置为1 */
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax){
} else if (edx == 16) {
reg[7] = (int) timer_alloc();
((struct TIMER *) reg[7])->flags2 = 1; /* 允许自动取消 */
} else if (edx == 17) {
}
接下来编写取消指定定时器timer_cancel、取消所有不需要定时器函数timer_cancelall:
/* timer.c */
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;
}
// 将下一个 timer 指向 timer 的下一个
t->next = timer->next;
}
timer->flags = TIMER_FLAGS_ALLOC;
io_store_eflags(e);
// success
return 1;
}
io_store_eflags(e);
// no need
return 0;
}
最后在cmd_app中结束应用程序的地方调用该timer_cancelall即可。