「30天自制操作系统」Day24 - 窗口操作

窗口切换

首先我们实现一个简单一点的功能:当按下 F11 时,将最下面那个窗口放到最上面。

在 bootpack.c 里加上对 F11 的处理就可以了,代码如下:

1
2
3
if (i == 256 + 0x57 && shtctl->top > 2) {	/* F11 */
sheet_updown(shtctl->sheets[1], shtctl->top - 1);
}

注意第 0 层是背景,第 top 层是鼠标。所以所谓的最下面指的是第 1 层,最上面指的是第 top - 1 层。

看一下效果吧:

这里有意思的一点是按下 F11 没反应,然后去 14.4 节查看按键数值表,发现 F1 键到 F10 键的按键编码是 0x3b ~ 0x44,而 F11 键变成了 0x57。我猜测应该是键盘布局不同所以导致的,所以我就把代码改成了 0x44,也就是在按下 F10 的时候实现窗口切换的功能。

现在我们来实现用鼠标点击来切换窗口的功能,当鼠标点击画面的某个地方时,按照从上到下的顺序,判断鼠标的位置落在哪个图层的范围内,并且还需要确保该位置不是透明色区域。程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}
}
}

运行一下试试看:

移动窗口

现在我们来实现窗口的移动。当鼠标左键点击窗口时,如果点击位置位于窗口的标题栏区域,则进入「窗口移动模式」,使窗口的位置追随鼠标指针的移动,当放开鼠标左键时,退出「窗口移动模式」,返回通常模式。

这个思路其实和我在第 14 天中提到的思路还是很像的,当时我写的代码如下:

1
2
3
4
5
if ((mdec.btn & 0x01) != 0
&& sht_win->vx0 <= mx && mx <= sht_win->bxsize + sht_win->vx0
&& sht_win->vy0 <= my && my <= sht_win->bysize + sht_win->vy0) {
sheet_slide(sht_win, sht_win->vx0 + mdec.x, sht_win->vy0 + mdec.y);
}

这里作者的做法如下,其中 mmx 和 mmy 记录的是移动之前的坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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; /* 返回通常模式 */
}

我们运行一下看看效果:

用鼠标关闭窗口

现在我们实现点击「×」来关闭窗口的功能,处理方法与刚刚类似,代码如下:

1
2
3
4
5
6
7
8
9
10
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
if (sht->task != 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();
}
}

效果如下:

将输入切换到应用程序窗口

我们规定在按下 Tab 键时将键盘输入切换到当前输入窗口下面一层的窗口中,若当前窗口为最下层,则切换到最上层窗口。

这里我们用 key_win 来存放当前处于输入模式的窗口地址。

当窗口处于输入模式时被关闭的话,我们让系统自动切换到最上层的窗口。

为了分辨窗口是不是由应用程序生成的,我们需要通过 SHEET 结构中的 flags 成员进行判断(以 0x10 比特位进行区分)。此外,只有命令行窗口需要控制光标的 ON/OFF,应用程序窗口不需要,这一区别也是通过 flags 来进行判断的(以 0x20 比特位进行区分)。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 + 0x0e) { /* 退格键 */
if (key_win == sht_win) { /* 发送至任务 A */
if (cursor_x > 8) {
putfonts8_asc_sht(sht_win, cursor_x, 28, COL8_000000, COL8_FFFFFF, " ", 1);
cursor_x -= 8;
}
} else { /* 发送至命令行窗口 */
fifo32_put(&key_win->task->fifo, 8 + 256);
}
}
if (i == 256 + 0x1c) { /* Enter */
if (key_win != sht_win) { /* 发送至命令行窗口 */
fifo32_put(&key_win->task->fifo, 10 + 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);
}
} else if (512 <= i && i <= 767) {
if (mouse_decode(&mdec, i - 512) != 0) {
if ((mdec.btn & 0x01) != 0) {
if (mmx < 0) {
for (j = shtctl->top - 1; j > 0; j--) {
if (0 <= x && x < sht->bxsize && 0 <= y && y < sht->bysize) {
if (sht->buf[y * sht->bxsize + x] != sht->col_inv) {
if (sht->bxsize - 21 <= x && x < sht->bxsize - 5 && 5 <= y && y < 19) {
/* 点击 x 按钮 */
if ((sht->flags & 0x10) != 0) { /* 是由应用程序生成的窗口吗 */
}
}
break;
}
}
}
} else {

}
} else {

}
}
}

上面的代码中调用了 keywin_on 和 keywin_off 两个函数,功能是控制窗口标题栏的颜色和 task_a 窗口的光标,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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); /* 命令行窗口 ON */
}
}
return cur_c;
}

change_wtitle8 函数的功能是改变窗口标题栏的颜色,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 也进行修改,通过 flags 的 0x10 比特位来判断应用程序结束时是否自动关闭窗口。修改部分如下:

1
2
3
4
5
6
7
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。代码如下:

1
2
3
4
5
6
7
8
9
10
   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;
}

现在我们运行一下看看效果:

### 用鼠标切换输入窗口

刚才我们实现了用 Tab 键来切换输入窗口,现在我们实现用鼠标也能够切换。我们在处理鼠标数据的部分中加入如下代码即可:

1
2
3
4
5
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,如下所示:

获取定时器(alloc)

寄存器
EDX 16
EAX 定时器句柄(由操作系统返回)

设置定时器的发送数据(init)

寄存器
EDX 17
EBX 定时器句柄
EAX 数据

定时器时间设定(set)

寄存器
EDX 18
EBX 定时器句柄
EAX 时间

释放定时器(free)

寄存器
EDX 19
EBX 定时器句柄

程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
if (edx == 15) {
for (;;) {
if (i >= 256) { /* 键盘数据(通过任务 A)等 */
reg[7] = i - 256;
return 0;
}
}
} 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);
}
return 0;
}

应用程序编写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_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

然后我们在主程序中加入以下代码,就可以启动并显示出我们的定时器了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
if (api_getkey(1) != 128) {
break;
}
sec++;
if (sec == 60) {
sec = 0;
min++;
if (min == 60) {
min = 0;
hou++;
}
}
}
api_end();
}

这里的 128 是定时器超时时产生的值,如果不是这个值,表示用户按下了其他案件,应用程序结束退出。

运行一下,效果如下:

取消定时器

刚刚的定时器还有一个问题,就是当定时器超时时会向任务发送事先设置的数据,但如果此时应用程序已经结束了的话,定时器的数据就会被发送到命令行窗口。要解决这个问题,我们需要取消待机中的定时器,从而在应用程序结束的同时取消定时器。

用于取消指定定时器的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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; /* 不需要取消处理 */
}

然后我们编写在应用程序结束时取消全部定时器的函数,其中 flags2 是一个标记,用来区分该定时器是否需要在应用程序结束时自动取消。

1
2
3
4
5
6
7
8
9
10
11
12
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; /* 没有找到 */
}

注意要将应用程序所申请的定时器的 flags2 设为 1。

最后我们编写一个函数,来取消应用程序结束时不需要的定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

我们来看看效果吧!

大功告成了。