当今,许多的图形软件为了能处理汉字,都采用直接读取汉字库文件或小汉字库技术来显示汉字。这种方法虽有效但总有许多的不足:直接读取汉字库文件,显示速度太慢而且对磁盘磨损太大;而小汉库技术虽提高了一定的显示速度,但并没有从根本上解决"快速显示"这一问题,而且也仅限于处理少量汉字。其实,现在微机不仅仅只有1M的内存空间,只要我们将HZK16读入扩展内存,再从扩展内存中取字模,便能从根本上解决"快速显示"。
最初的PC机,采用8086/8088为CPU,由于它有20位地址总线,因而最多能管理1MB的地址空间,以后的一系列80X86,地址总线已不止20位,CPU采用两种工作模式:实地模式和保护虚地模式。在实地模式下,象80286只用到24位地址总线中的20位,因而寻址在1M以内,DOS即是工作在这种模式下。而在保持模式下,80286则可达16M。
在保护模式下访问存储器,比实地模式下复杂得多,实地模式下只需指出段址和偏移地址即可,而在保护模式下,还需建立全局描述符GDT表,它由6种描述符组成,结结构及说明如下:
而每一种描述符又由8B组成,各意义为:
0-1B: 该字指定段长的字节数
2-3B: 基地址低位字。
4B :基地址高位字节。
在保护模式下80286可访访问16M(224)空间,此字节为基地址的高8位,而低16位,则有基地址低位字的16位决定。
5B :这一字节称为存储段存取权字,一般将其置成93H。
6-7B:这两字节均保留,它总是置成0。
为了以后说明方便,我们用C语言将其结构定义如下:
#define WORD unsigned int
#define BYTE unsigned char
#define LONG unsigned long
typedef struct Des{
WORD size; /*数据段长 */
WORD BaseLow16; /*基地址低16位*/
BYTE BaseHigh8; /*基地址高8位*/
BYTE attr; /*存取权或属性值*/
WORD NoUse; /*保留*/
} DES;
typedef struct GDTHead
{ DES BlankDsc; /*空白描述符*/
DES GDTDsc; /*该GDT的描述符*/
DES SrcDsc; /*源数据块描述符*/
DES DstDsc; /*目标数据块描述符*/
DES BiosCs; /*BIOS代码段描述符*/
DES BiosSs; /*BIOS堆栈段描述符*/
} GDT;
在我们的程序中,要访问1M以上的扩展内存,则用到了BIOS INT15H中断中的87H子功能。该子功能可将0 ̄崐16M之间各个存储器的数据相互交换,使用方法如下:
入口参数:AH=87H
CX=以字为单位一次传送的数据块数
ES:SI=全局描述表GDT段址:偏移地址。
用C语言实现则为:
void EmsMoveData(GDT *mcb,WORD size)
{ /*作用: 利用BIOS INT 15H 实现源数据到目
的数据之间的数据传送*/
struct REGPACK r;
r.r_es=FP_SEG(mcb);
r.r_si=FP_OFF(mcb);
r.r_cx=size>>1;
r.r_ax=0x8700;
intr(0x15,&r);
}
懂得以上扩展内存的使用方法,我们需要访问扩展内存,只需改变源和目标块的传输段长和基地址,然后调用87H号功能。其中,改变源和目标块的描述符源程序如下:
void SetSrcAddr(GDT *e,LONG addr,WORD size)
{ /**********************************
作 用:置源数据块描述符值
入口参数:e 指向GDT的指针
addr 基地址
size 字节大小
***********************************/
e->SrcDsc.BaseLow16=addr & 0x0ffff;
/*置基地址的低16位*/
e->SrcDsc.BaseHigh8=addr>>16;
/*置基地址的高8位*/
e->SrcDsc.size=size;
}
void SetDstAddr(GDT *e,LONG addr,WORD size)
{ /*********************************
作 用:置目标数据块描述符值
入口参数:e 指向GDT的指针
addr 基地址
size 字节大小
*********************************/
e->DstDsc.BaseLow16=addr & 0x0ffff;
e->DstDsc.BaseHigh8=addr >> 16;
e->DstDsc.size=size;
}
实际生活中,并不是所给的扩展内存大小都符合要求,因此,我们有必要在程序中获取扩展内存的大小,灵活地处理各种出现的现象,这时,则要用到INT 15H的88H号子功能。该功能表示取1M以上扩展内存容量(以1K为单位),调用为:
入口参数:AH= 88H
返 回 值:AX=扩展内存的块数(每块=1K)
值得说明的是,若用户在DOS的config.sys文件中配制了emm386.exe文件,则此功能号被屏蔽,但可用EMM386.exe文件提供的INT87H 42号功能取可用页数(每页16K)。
WORD GetEMSSize(void)
/*取得当前扩展内存大小,返回多少K值*/{ _AH=0x88;
geninterrupt(0x15);
if (_AX==0)/*若当前功能号被EMM386程序占用*/
{ _AH=0x42;/*调用EMM38642H号功能,取可用页数*/
geninterrupt(0x67);
return(_BX*16);
}
else
return(_AX);}
因此,我们要利用扩展内存快速显示汉字,则必须做以下几项工作。
⑴、将HZK16读入扩展内存中。我们不防将HZK16读入由HZK16ADDR指定的扩展内存中,则实现方法如下:
int LoadHZK16ToEms(void)
{ /*************************************
函数作用:将文件HZK16的内容送入指定的扩展内存中
***********************/
FILE *fp;
LONG FileSize,EmsSize,SrcAddr;
WORD size,num;
char *buf;
fp=fopen("HZK16","rb"
![](images/smilies/wink.gif)
if(fp==NULL)/*文件不能打开,返回打开文件出错*/
return(OpenFileError);
EmsSize=((LONG)GetEMSSize())*1024;
/*取当前扩展内存的大小并化为以字节为单位*/
fseek(fp,0L,SEEK_END); /*将文件指针移到文件尾,
测试文件的大小*/
FileSize=ftell(fp);
rewind(fp);
if (EmsSize<FileSize)
/*若文件的大小超过当前扩展内存有大小,
则返回文件太大的错误信息*/
{ fclose(fp);
return(FileToBigger);}
if(EmsSize-(HZK16ADDR-0x10000)<0) /*若保存的起始地址到最大扩展内存之间的空间不够装入该文件,则返回指出的基地址太大错误*/
{ fclose(fp);
return(AddrToBigger);}
buf=(char *)malloc(0x8000); /*为传输文件内容分配内存*/
if (buf==NULL) /*内存分配失败,返回内在内存分配错误*/
{ fclose(fp);
return(MallocError);}
SrcAddr=FP_SEG(buf); /*计算源数据块地址*/
SrcAddr=(SrcAddr<<4)+FP_OFF(buf);
SetSrcAddr(&mes,SrcAddr,0x8000);
do /*每次读取8000H字节内容到基本内存,交将传到扩展内存中*/
{ num=fread(buf,1,0x8000,fp);
SetDstAddr(&mes,addr,num);
EmsMoveData(&mes,num);
addr+=0x8000;
} while(num==0x8000);
free(buf);
fclose(fp);
return(NoError);/*成功返回无错误*/
}
以上程序中用到了一些宏定义和一个结构为GDT的全局变量mes,它们可以定义在头文件中,初始化代码如崐下:
#define HZK16ADDR 0x110000L
/*汉字库在扩展内存在起始地址*/
enum ErrMsg{ /*错误信息定义*/
NoError , /*没有错误*/
MallocError , /*内存分配错误*/
FileToBigger , /*文件太大,无法装入扩展内存*/
OpenFileError , /*打开文件错误*/
AddrToBigger };
GDT mes={{0,0,0,0 ,0},/*伪描述符初始为0*/
{0,0,0,0 ,0},/*GDT的描述符*/
{0,0,0,0x93,0},/*源和目标块描述符*/
{0,0,0,0x93,0},/*存取权均置为93H*/
{0,0,0,0 ,0},/*BIOS代码段描述符置为0*/
{0,0,0,0,0}};/*BIOS堆栈段描述符置为0*/
⑵、将汉字库读入扩展内存后,我们则要根据给定的汉字区位码,取出该汉字对应的字模。这一过程较简单,首先算出该汉字在汉字库中的偏移地址,然后加上汉字库在扩展内存中的起始地址,即是该汉字在扩展内存中的存储地址。接下来,就利用INT 15H的87H子功能传到需要的基本内存中。
void GetHzMode(WORD c,WORD *buf)
{ /********************************
函数作用:取汉字的字模到BUF处,
入口参数:c 全角字符的区位码
buf 字模存放的地址
********************************/
LONG addre,offset;
register int i;
addre=FP_SEG(buf);/*将存放字模的地址化成符合要求的基地址*/
addre=(addre<>8)-0xa1)*94L+((c&0xff)-0xa1))*32l;
/*计算该字符在整个汉字库中的偏移地址*/
offset+=HZK16ADDR;
/*将基偏移地址加上汉字库装入时的起始地 址,即是该字符在扩展内存的地址*/
SetSrcAddr(&mes,offset,32);
/*从扩展内存中读32个字节,存放入由buf 指定的内存中*/
SetDstAddr(&mes,addre,32);
EmsMoveData(&mes,32);
}
⑶、根据取出的字模,在屏幕上显示汉字有许多方法,常见的有描点法、画线法、修改IMG结构法、直接写屏等等。这几种方法各有千秋,笔者在此仅给出用画线法显示汉字。用画线方法显示汉字巧妙地利了画线类型定义函数setlinestyle(int linestyle,usigned upatten,int thickness)。当linstyle为USERBIT_LINE(用户定义的崐线型)时,其参量patten是一个无符号整数,若某一位为1,则表示在屏幕上描点。而一般用于屏幕显示的是16×16点阵的汉字,因此我们可以将其看成16条不同线型的线组成的图形。
/********************************************
入口参数:cwords 显示的字符串
x,y 显示时的坐标
color 显示的色彩
*********************************************/
void ListCstring(unsigned char *cwords,
int x,int y,int color)
{ WORD mode[16];
BYTE w[2];
WORD c;
register int i;
int xx,yy;
struct linesettingstype OldLineSetting;
getlinesettings(&OldLineSetting);
/*保存原线型设置值*/
setcolor(color);
xx=x;yy=y;
for(;*cwords
![](images/smilies/wink.gif)
if(*cwords<0x7f) /*若为英文字符,则用C语提供的outtextxy函数输出它*/
{ w[0]=*(cwords++); w[1]='\0';
outtextxy(xx,yy,w);
xx+=8;} else /*是全角字符*/
{ c=(*cwords<<8) | *(cwords+1);
GetHzMode(c,mode);/*取字符的字模*/
for(i=0;i>8&0x00ff )
|(mode <<8&0xff00);
for(j=0;ibase=SaveScreenAddr;
addr=0xa0000L+s->y1*80L+s->x1/8;
for(y=s->y1;yy2;y++,addr+=80L)
for(i=0;ix2-s->x1)/8+3);
SetSrcAddr(&mes,addr,(s->x2-s->x1)/8+3);
EmsMoveData(&mes,(s->x2-s->x1)/8+3);
SaveScreenAddr+=(s->x2-s->x1)/8+3;
}
outportb(0x3ce,4); /*允许第0个位面读*/
outportb(0x3cf,0);
}
void RestoreMsgToScreen(SSH *s)
/*将640*480*16色模式下的屏幕内容从基地址
addr开始的扩展内存处恢复到屏幕上*/
{ BYTE i;
WORD y;
LONG addr,ToAddr;
addr=s->base;
ToAddr=0xa0000L+s->y1*80L+s->x1/8;
for(y=s->y1;yy2;y++,ToAddr+=80L)
{for(i=1;i<=8;i<x2-s->x1)/8+3);
SetDstAddr(&mes,ToAddr,
(s->x2-s->x1)/8+3);
EmsMoveData(&mes,(s->x2-s->x1)/8+3);
addr+=(LONG)((s->x2-s->x1)/8+3);
}
}
outportb(0x3c4,2); /*允许第4个位面写*/
outportb(0x3c5,0x0f);
SaveScreenAddr=s->base;
}
虽然此方法能有效的处理大屏幕区域内容的保存与恢复,但在使用时,请一定要注意这一点:后保存的区域一定要先恢复,这一点与栈是相同的。若不需后面保存的区域,可将其置之不理。
用户在使用这些函数时,将它们全部放入一文件中,然后在自己的程序头加入#include 即可。