说明

INT 33h 各功能号的速查可以看《DOS 鼠标中断 33h》,配合调用方式的讲解可以看《Turbo C键盘鼠标交互编程详解》。这篇写一个能直接跑起来的完整鼠标程序,包含自定义光标和事件回调两种常见写法。

完整示例:鼠标画图程序

用左键拖动画线,右键退出,这是最基础也最常见的鼠标交互程序结构。

 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
66
67
68
69
70
#include <dos.h>
#include <graphics.h>
#include <conio.h>

void init_mouse(int *has_mouse, int *buttons) {
    union REGS regs;
    regs.x.ax = 0x00;
    int86(0x33, &regs, &regs);
    *has_mouse = (regs.x.ax == 0xFFFF);
    *buttons = regs.x.bx;
}

void show_cursor(void) {
    union REGS regs;
    regs.x.ax = 0x01;
    int86(0x33, &regs, &regs);
}

void hide_cursor(void) {
    union REGS regs;
    regs.x.ax = 0x02;
    int86(0x33, &regs, &regs);
}

void get_mouse_state(int *x, int *y, int *button) {
    union REGS regs;
    regs.x.ax = 0x03;
    int86(0x33, &regs, &regs);
    *button = regs.x.bx;
    *x = regs.x.cx;
    *y = regs.x.dx;
}

int main(void) {
    int gd = DETECT, gm;
    int has_mouse, buttons;
    int x, y, btn;
    int last_x = -1, last_y = -1;

    init_mouse(&has_mouse, &buttons);
    if (!has_mouse) {
        printf("未检测到鼠标驱动,请先加载 MOUSE.COM\n");
        return 1;
    }

    initgraph(&gd, &gm, "");
    show_cursor();

    while (1) {
        get_mouse_state(&x, &y, &btn);

        if (btn & 1) {              /* 左键:画线 */
            if (last_x >= 0) {
                line(last_x, last_y, x, y);
            }
            last_x = x;
            last_y = y;
        } else {
            last_x = -1;             /* 松开左键,断开线段连续性 */
        }

        if (btn & 2) break;          /* 右键:退出 */

        delay(10);
    }

    hide_cursor();
    closegraph();
    return 0;
}

几个容易踩的坑:

  1. INT 33h 的功能号在 AX 里,不是 AH——这是历史遗留的特殊约定,跟大多数 BIOS 中断不一样,写错了功能不会触发但也不会报错,调试起来很隐蔽。
  2. 鼠标坐标系统是独立的虚拟坐标,默认范围通常是 0-639 x 0-199(对应文本模式或某些图形模式),不一定和你 initgraph() 之后的实际分辨率一致。进入图形模式后最好显式调用 AX=07h/AX=08h 设置移动范围,锚定到当前分辨率:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void set_mouse_range(int max_x, int max_y) {
    union REGS regs;
    regs.x.ax = 0x07;   /* 水平范围 */
    regs.x.cx = 0;
    regs.x.dx = max_x;
    int86(0x33, &regs, &regs);

    regs.x.ax = 0x08;   /* 垂直范围 */
    regs.x.cx = 0;
    regs.x.dx = max_y;
    int86(0x33, &regs, &regs);
}
  1. 每次画完线都调用 line(),鼠标指针本身也需要重绘——驱动会自己处理指针的显示/隐藏刷新,不需要手动 hide_cursor() 再 show_cursor(),否则拖动时指针会闪烁得很明显。

自定义图形光标(AX=09h)

默认的箭头指针在某些场合(比如做 CAD 类精确取点的软件)不够用,需要自定义十字光标或者位图光标。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <dos.h>

/* 光标掩码:16x16,屏幕掩码和光标掩码各16个字(word) */
unsigned int screen_mask[16] = {
    0xFFFF, 0xF81F, 0xF81F, 0xF81F, 0xF81F, 0xF81F, 0x0000, 0x0000,
    0x0000, 0x0000, 0xF81F, 0xF81F, 0xF81F, 0xF81F, 0xF81F, 0xFFFF
};
unsigned int cursor_mask[16] = {
    0x0000, 0x07E0, 0x07E0, 0x07E0, 0x07E0, 0x07E0, 0xFFFF, 0xFFFF,
    0xFFFF, 0xFFFF, 0x07E0, 0x07E0, 0x07E0, 0x07E0, 0x07E0, 0x0000
};

void set_custom_cursor(int hot_x, int hot_y) {
    struct SREGS sregs;
    union REGS regs;

    regs.x.ax = 0x09;
    regs.x.bx = hot_x;    /* 热点X偏移(光标位图内,鼠标实际点击的坐标位置) */
    regs.x.cx = hot_y;    /* 热点Y偏移 */
    regs.x.dx = FP_OFF(screen_mask);
    sregs.es   = FP_SEG(screen_mask);
    int86x(0x33, &regs, &regs, &sregs);
}

热点(hot spot)的概念很重要:一个 16x16 的十字光标,视觉中心是第 8 行第 8 列,但如果你希望"点击坐标"对应的是光标最尖端的那个像素(比如画图软件里的笔尖),热点就要设成尖端的偏移量,不是图形的几何中心。


事件驱动:鼠标回调函数(AX=0Ch)

上面的例子都是轮询(每次循环主动查询鼠标状态),CPU 占用高。更高效的方式是注册一个回调函数,鼠标驱动检测到事件时自动调用:

 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
#include <dos.h>

#define MOUSE_MOVE      0x01
#define MOUSE_LBUTTON_DOWN 0x02
#define MOUSE_LBUTTON_UP   0x04
#define MOUSE_RBUTTON_DOWN 0x08
#define MOUSE_RBUTTON_UP   0x10

volatile int event_x, event_y, event_flags;

void far mouse_callback(unsigned int flags, unsigned int x, unsigned int y,
                         unsigned int buttons) {
    /* 注意:这是鼠标驱动内部调用的,运行环境类似中断上下文
       不能调用 DOS 服务、不能用 printf,只能读写简单变量 */
    event_flags = flags;
    event_x = x;
    event_y = y;
}

void install_mouse_callback(void) {
    union REGS regs;
    struct SREGS sregs;

    regs.x.ax = 0x0C;
    regs.x.cx = MOUSE_MOVE | MOUSE_LBUTTON_DOWN | MOUSE_LBUTTON_UP;
    regs.x.dx = FP_OFF(mouse_callback);
    sregs.es   = FP_SEG(mouse_callback);
    int86x(0x33, &regs, &regs, &sregs);
}

int main(void) {
    install_mouse_callback();

    while (1) {
        if (event_flags & MOUSE_LBUTTON_DOWN) {
            /* 在主循环里处理复杂逻辑,不要放进回调里 */
            event_flags = 0;
        }
    }
}

这个回调函数的执行环境跟《DOS下TSR程序实现原理》里讲的中断服务程序限制几乎一样——鼠标驱动是通过 IRQ 触发内部逻辑再转调你注册的回调,本质上处于类中断上下文,DOS 服务不可重入的限制同样适用。标准做法同样是"回调只做最简单的标志位/坐标记录,复杂逻辑丢到主循环处理"。


相关阅读