当前位置:网站首页>>电脑技术>>C、C++语言>>我的著作 双击自动滚屏
中西文全屏幕编辑程序的设计和编制(四)

发表日期:2006年1月1日      已经有4479位读者读过此文

                                3.3 左右移屏的实现

    在上一节里已经谈到,对于全屏幕编辑软件来说,不仅要有相对于文本行的前后移动,而且要有相对于文本列方向的左右移屏。例如,程序设定屏幕一行显示文本 76 个字节,而假定某一文本行有 150 字节,当前屏幕显示的是第 0 至 75 字节,当我们要在第 100 字节后插入一个字符,就必须移动光标到列号 101 处,光标移至列号 76 处时就超出了现屏幕文本显示区,这时就需要把屏幕右移一屏。如果一次右移 38 个字节,右移一屏后,屏幕将显示第 38 至 113 字节,这样便把包含该行第 100 字节的一个文本区域“搬”到屏幕上来了。
    在第 3.2 节中,已经引入了“屏号”的概念,因此要实现左右移屏,只要根据光标列号 yy 计算出光标列显示时所在的屏号,显示该屏时光标在屏幕上的列坐标 y,调用显示一整屏的函数 disp_t() 即可。
    disp_t() 为重显当前编辑屏幕的函数,它依靠循环调用显示一文本行的函数 disp() 来实现整屏文本的显示,对于函数 disp() 我们将在下一节中专门介绍。disp_t() 中使用了一种判断击键暂停屏幕显示的技巧,程序中用库函数 bioskey(1) 判断是否击键,当发现连续按 PgUp、PgDn 键翻屏或连续按 F4 键排版时退出本显示函数,暂停显示,这对一些显示速度较慢的微机,操作时是大有好处的。在第 2.2 节中已经介绍过当 bioskey() 函数的实参为 0 时,程序处于等待击键状态,击键后返回按键的扫描码。在这里的 bioskey() 带参数1,其功能是测试是否击键,如果未按键,返回 0,否则返回按键的扫描码。它不会使程序停在那里等待击键,它判断按键的键值仍保留,不影响下次调用 bioskey(0) 时被俘获。
disp_t()                                /* 显示一屏 */
{
   int i;
   for(i=0;i<=H3;i++) {                 /* 为逐行显示设置的循环 */
      cc.ii=bioskey(1);                 /* 判断是否按键 */
      if(xx-x && xx<ttl_x && cc.ch[0]==0 &&
            (cc.ch[1]==81 || cc.ch[1]==73 || cc.ch[1]==62))
         return;                        /* 如连续按 PgUp,PgDn,F4 键,且不在文                                           首屏或文末屏,则停止显示,返回 */
      else {                            /* 如果不是上述情况 */
         if(xx-x+i>ttl_x) write_space(i,0,80,TEXT_COLOR);
                                        /* 如超出文末行,清文末行后屏幕 */
         else disp(ss_x-x+i,i);         /* 否则,显示一行 */
      }
   }
}

                          3.4 文本行显示函数 disp() 的设计

    disp_t() 调用的显示一行的函数 disp() 是实现屏幕文本显示十分关键的一个函数:

#define BLCK_COLOR 0x1E                 /* 字块色彩 */
#define TEXT_COLOR 0x02                 /* 文本字符色彩 */
#define CHAR_COLOR 0x03                 /* 行末示意符色彩 */
#define ZS 76                           /* 每行显示文本字节数 */
#define BP (ZS/2)                       /* 每次移屏字节数 */
........
disp(int a,int i)                       /* 在屏幕 i 行显示编辑数组第 a 行 */
{
   char u=0;
   unsigned char *s;
   int q,j,n,g,k,jv;
   long ux;
   k=m*BP;                              /* 本屏首列的列号 */
   s=malloc(HC);                        /* 给 s 分配内存 */
   strcpy(s,ss[a]);                     /* 将要显示的文本行拷入 s */
   g=strlen(s);                         /* 测字符串总长 */
   if(*(s+g-1)==0x0A)  g-=2;            /* 如串尾为回车换行符,串长减 2 */
   n=g>ZS+k ? ZS+k : g;                 /* 如串尾在以后屏,本屏行尾的文本列号取                                           本行前字符数之和,否则取字符串长 */
   if(g<=k)  *s=0;                      /* 如字符串尾在前面屏,本屏为空串 */
   else {                               /* 如字符串尾不在前面屏 */
      for(j=0;j<n;j++) {               
         if(*(s+j)>0xA0)  u++;          /* 计算汉字字节数 */
         if(m && j==k-1 && u%2) {       /* 本屏第一字节是汉字后半字节时 */
            *(s+k)=32;                  /* 本屏第一字节用空格代替 */
            u++;                        /* 汉字字节数增 1 */
            j++;                        /* 后移一字节 */
         }
      }
      if(u%2)  n++;             /* 如本屏串尾是汉字前半字节,再多显示一字节 */
      *(s+n)=0;                         /* 用 ''''\0'''' 定界,截除本屏后字符 */
      strcpy(s,s+k);                    /* 去除本屏前字符,s 即为本屏显示的串 */
   }
   ux=xx-x+i;                           /* 计算本行的文本行号 */
   q=strlen(s);                         /* 测 s 字符串长 */
   for(;;)  {
      if(ux<ksx || ux>kwx) {            /* 如本行在块首行前或块尾行后 */
         write_string(i,0,s,TEXT_COLOR);/* 以文本色显示本行 */
         break;                         /* 跳出循环 */
      }
      if(ux==kwx)  {                    /* 如本行为块尾行 */
         if(kwy>=k)  {                  /* 如块尾在本屏首字节之后 */
            if(kwy<=n)  {               /* 如块尾在本屏末字节之前 */
               if(blck==1) {            /* 如只设置了块首(块首块尾重合) */
                  if(*(s+kwy-k)>0xA0)  jv=kwy-k+2;
                        /* 如块首后一字节为全角,后移二字节开始显示文本色 */
                  else jv=kwy-k+1;      /* 如为半角,后移一字节开始 */
               }
               else  jv=kwy-k;          /* 如已完整定义块,从块尾位置开始 */
               write_string(i,jv,s+jv,TEXT_COLOR);  /* 用文本色显示块后字串 */
               *(s+jv)=0;               /* 截除块尾后字节 */
            }
         }
         else  {                        /* 如块尾在本屏首字节前 */
            write_string(i,0,s,TEXT_COLOR);         /* 用文本色显示字符串 */
            break;                      /* 跳出 for 循环 */
         }
      }                       
      if(ux==ksx && ksy>=k)  {          /* 如本行为块首行,块首列在本屏首列后 */
         if(ksy<=n)  {                  /* 如块首列在本屏末字节前 */
            jv=ksy-k;                   /* 从块首位置开始显示 */
            write_string(i,jv,s+jv,BLCK_COLOR);     /* 用字块色显示字块 */
            *(s+jv)=0;                  /* 在 s 中截除块首后字节 */
         }
         write_string(i,0,s,TEXT_COLOR);/* 用文本色显示剩余部分 */
      }
      else write_string(i,0,s,BLCK_COLOR);
                             /* 如块首列在前屏或为块的其它行,用字块色显示 */
      break;                            /* 跳出循环 */
   }
   write_space(i,q,FH+1-q,TEXT_COLOR);  /* 用空格复盖本行串尾后屏幕 */
   if(q>=ZS)  write_char(i,FH,''''+'''',CHAR_COLOR);
                                        /* 如串尾在以后屏,标志行显示“+” */
   else  {                              /* 如串尾不在以后屏 */
      if(ss[a][g]==0x8D)   write_char(i,FH,''''.'''',CHAR_COLOR);
                                        /* 如串尾为软回车,标志行显示“.” */
      if(ss[a][g]==0x0D)   write_char(i,FH,''''<'''',CHAR_COLOR);
                                        /* 如串尾为硬回车,标志行显示“<” */
   }
   free(s);                             /* 释放 s 占用的内存空间 */
}

    函数中的  n=g>ZS+k ? ZS+k : g; 是用三目运算符 ? : 的表达式,这种简洁高效的表?达方式希望读者能够掌握,它相当于下面的  if-else 基本结构。

    if(g>ZS+k)   n=ZS+k;
    else   n=g;

    disp() 有两个形参 a 和 i,a 是要显示行的编辑数组行号,i 是显示该行的屏幕行坐标。disp() 先将要显示的文本行拷贝到一个字符形指针 s 中,再根据要显示的屏号,将其“斩头去尾”,把字符串本屏前和本屏后的部分去掉,剩下的字符串 s 就是文本行的屏幕显示部分。要“斩头去尾”就需先算出本屏左边首列的文本列号 k 和确定本屏显示字符串尾的文本列号 n。k 用下式计算:

    k=m*BP

    式中 ZS 为设定的每次移屏的字节数。当文本行的长度小于 k 时,说明文本行的行尾在本屏之前,本屏显示的字符串 s 应为空串,需将 s 的首字节置为 ''''\0''''。n 要根据文本行行尾的位置来定,如果行尾在本屏之后,取 n=ZS+k,否则 n 取文本行长。
    在汉字编辑屏幕显示时并不是简单地把从第 k 字节到 n 字节前的字符串取出来显示就行了,还必须考虑屏幕显示首列字节是否是全角字符的后半字,如果是的话,必须用一个空格覆盖它,否则这个字节就会和下个字节组成一个别的全角字,屏幕显示就发生了混乱。一些中文字处理软件一左右移屏,便显示混乱,就是没有处理好屏幕行首、尾处半个汉字的问题。 因此,在 disp() 中要通过计算汉字字节数来判断,如屏首列为后半个汉字,就用空格写入 s 中对应位置。另一方面,还要判断屏幕显示文本最右列是否是全角字符的前半字,如是,则要将 n 加 1,把后半个汉字也列入本屏显示。经过这样的处理,就可以实施“斩头去尾”了,令 *(s+n)=0 即截去了本屏末以后的部分,用库函数strcpy()将 第 k 列后的字符串拷贝至串首,覆盖本屏前的部分,s 中剩下的便是本屏要显示的字符串。
    取得本屏显示字符串后,就能在屏幕上进行显示了。如果编辑程序中没有字块处理功能,则只要用一个 write_string() 函数把字符串显示在屏幕上的指定行就可以了,但在有字块处理功能的编辑程序里,就不那么简单了。下面先简要叙述一下有关字块的一些基本概念。
   字块是为了提高编辑工作效率而在编辑过程中人为地设置的,字块是被编辑文件中连续的一部分文本,它可以短至一个字节,也可以由许多个文本行组成。在 BJ 中,用全局变量 ksx、ksy、kwx、kwy 分别记录字块的块首行号、块首列号和块尾行号、块尾列号。在不同的编辑软件中字块的显示方式可能不同,一些软件如 中文 Ward star 用在块首前和块尾后插入显示一个标记来表示的,本文则采用改变色彩显示的方法来标识字块,这样做的好处是字块部分一目了然,也不会因插入额外的标记符而使屏幕上一些字符移位、或行长改变而造成视觉上的差错。但这样做,在屏幕上不同的行,甚至一行中不同的部分要用不同的色彩来显示,显示函数的编制要相对复杂一些。
    disp() 中把字块和其余文本的显示设置为不同的色彩,我们简称为字块色和文本色。程序中先进行判断,如果本屏显示行不包含字块,则用文本色显示;如果本屏显示行全部在字块中,则用字块色显示,如显示行只有部分在字块中,则必须分段显示。下面就一种比较典型的情况,讲述分段显示的实现过程。
    假如显示字符串 s 的第 0 至 8 字节用文本色显示,第 9 至 20 字节为字块,用字块色显示,第 21 字节后又用文本色显示,就需把 s 分三次显示。我们采用的办法是从后到前分段显示,即先用文本色从屏幕本行第 21 列起显示 s 中第 21 字符后的部分,然后将串 s 第 21 字节改为 ''''\0'''',从 s 中截除已显示的部分,再从屏幕第 9 行起用字块色显示 s 中第 9 字节后的部分,即 s 中第 9 至 20 字节,再将 s 第 9 字节改 ''''\0'''',截除第 9 字节及以后部分,最后从屏幕第 0 列起显示剩余的字符串,完成整行的显示。
    设计 disp() 时还考虑到一种特殊情况,即只定义了块首,尚未定义块尾的情况下(块首块尾重合,blck=1),在屏幕上要能看到块首。解决的办法是将块首后的一个半角或全角字符用字块色显示,当然显示前先要判断紧接在块首后的是半角还是全角字符。
    屏幕最右列是示意符列,在 3.2 节中,已经介绍了示意符的含义,在 disp() 里通过判断确定示意符的类型,并调用 write_char() 在屏幕本行最右列用设定的示意符显示色显示选定的示意符。

                     第四章 文件名输入和文件参考目录显示

    要进入编辑程序进行编辑,先要输入被编辑的文件名。同时,为了给操作者提供方便,在输入时可以在屏幕上开辟一个文件目录列表区,把当前目录中可能是文本文件的文件名列出来,这通过 C 语言来实现并不困难。

                          4.1 字符串输入函数的建立

    文件名的输入实际上是字符串的输入。虽然在 C 语言中已经提供了现成的格式输入函数 scanf(),可以用来输入字符串,但是用 scanf() 函数输入字符串有明显的不足之处:一是这个函数本身不能设置字符色彩;二是中途返还困难,而且在输入时,一旦无意中误按了 Esc 键,光标就会移下行首,并显示一反斜杠,使屏幕画面遭到破坏;三是在等待输入的过程中,不能接收 PgUp、PgDn 等特殊功能键来实现屏幕上显示的文件名参考目录的翻屏等操作,因此,建立一个自己的字符串输入函数是必要的。这个字符串输入函数在程序执行查找、替代、字块存盘等操作,进行人机对话时,也是十分有用的。这个函数的清单如下:

union inkey {                                   /* 定义一个放击键值的联合 */
   unsigned char ch[2];
   int ii;
} cc;
........
key_string(int j,int e,unsigned char a[],int d) /* 输入字符串函数 */
{                             /* j 屏幕行号,e 屏幕列号,a 字符串,d 显示属性 */
   int i,v,k=0;
   while(1)  {                                  /* 为输入多个字符建的循环 */
      goto_xy(j,e+k);                           /* 光标定位 */
      cc.ii=bioskey(0);                         /* 读键值至联合中 */
      if(!cc.ch[0])                             /* 如低位字节为 ''''\0'''' */
         switch(cc.ch[1])  {                    /* 判断高位字节 */
            case 75:                  /* 如为删除键或向左单箭头键,跳转 AA */
            case 83: goto AA;
            case 73: a[0]=0;                    /* 如为 PgDn 键,清字符串 */
                     return -4;                 /* 返回 -4 */
            case 81: a[0]=0;                    /* 如为 PgUp 键,清字符串 */
                     return -5;                 /* 返回 -5 */
         }
      else                                      /* 如低位字节不为 0 */
         switch(cc.ch[0]) {                     /* 判断低位字节 */
            case 27: return -1;                 /* 如为 ESC 键,返回 -1 */
            case 13: a[k]=0;        /* 如为回车键,确认,字符串尾加 ''''\0'''' 定界 */
                     return k;                  /* 返回输入的字节数 */
            case 3:  bk();                      /* Ctrl+C 键,返回 DOS 下 */
            case 8:                             /* 如为退格键 */
AA:                  if(k) {                    /* 如不为空串 */
                       for(;;) {                /* 为删除全角字符设置的循环 */
                         a[--k]=0;              /* 前移一字节,填 ''''\0'''' */
                         write_char(j,e+k,32,d);/* 用空格复盖显示字符 */
                         v=0;                   /* v 置初值 0 */
                         for(i=0;i<k;i++)
                           if(a[i]>0xA0) v++;   /* 计算全角字节数 */
                         if(v%2==0)  break;     /* 如为偶数,退出 for 循环 */
                        }
                     }
                     continue;                  /* 继续循环 */
            default: if(cc.ch[0]>31) {          /* 如不为控制字符 */
                        a[k++]=cc.ch[0];        /* 写入字符串 */
                        a[k]=0; a[k+1]=0;       /* 字符串以 ''''\0'''' 定界 */
                        write_string(j,e,a,d);  /* 重显字符串 */
                     }
         }
   }
}

    可以看出这个字符串输入函数除接收半角的标准 ASCII 字符和全角汉字外,还响应回车、Esc、Ctrl+C、PgUp、PgDn、Del、退格键和左移光标键。当判断是 Esc 键时,中止输入,返回 -1;当判断是 Del 键、退格键或左移光标键时,执行同样的操作,即删除光标前字符,光标左移一字节;当为 PgUp 或 PgDn 键时,中止输入,但返回值不同,为 PgUp 键时,返回 -5,为 PgDn 键时返回 -4;当判断为回车键时,结束输入,返回输入的字节数。调用本函数的程序根据返回值作出判断,进行相应的操作。例如返回值为 -4 时,对屏幕信息区显示的当前目录中的参考文件名列表进行向下翻屏。当接收到的是 Ctrl+C 键时,返回DOS 下,此时调用的函数 bk() 关闭打开的文件,删除临时文件,全部屏幕清屏,移动光标至屏幕左上角,程序退出运行,对于函数 bk() 将在 10.3 节介绍。

                       4.2 当前目录中文件名的列表显示

    在输入要编辑的文件名时,在屏幕上把当前目录中的文件名列出来供参考,这无疑是很有帮助的。Turbo C 中的 findfirst() 和 findnext() 函数可以帮助我们找出当前目录中的文件名,把找出的这些文件名中带 EXE、COM、OBJ、DBF 等扩展名,明显不会是文本文件的文件名去掉,把剩下的文件名放入一个字符型二维数组里,在屏幕上指定区域列表显示,一次显示不完的,可按 PgUp、PgDn 键翻屏。
    首先,应找出文件名,进行比较筛选后,赋给一个数组:

#define HH 24                                       /* 屏幕最下行行坐标 */
#define PG ((HH-16)*6)                              /* 每页可显示目录数 */
struct ffblk pt[1];                                 /* 读文件目录用的结构 */
........
int make_dir()                     /* 找出当前目录中合适的文件名记入数组 */
{
   int i=0;
   if(findfirst("*.*",pt,0)==0 && compare())        /* 寻找第一个文件名并比较 */
      strcpy(ss[i++],pt[0].ff_name);    /* 如不是要忽略的文件名,将它赋给数组 */
   while(findnext(pt)==0)  {                        /* 继续找文件名 */
      if(compare())  strcpy(ss[i++],pt[0].ff_name); /* 如比较为真,记入数组 */
   }
   return (i-1)/PG+1;                               /* 返回可供显示页数 */
}

   由于此时还没有进入实际的编辑,因此临时借用已分配内存空间的编辑数组 *ss[ ]作为文件名数组。在 make_dir() 函数清单中,i 为适用的文件名数,HH-16 为目录列表区显示总行数,每行可显示 6 个文件名。ffblk 是 findfirst() 和 findnext() 这两个磁盘文件查找函数存放盘文件信息的结构,这里只用了 ff_name 这一个成员。该结构定义在头文件 dir.h 中:

struct ffblk {
   char ff_reserved[21];  /* 留给 DOS 用的信息 */
   char ff_attrib;        /* 文件属性 */
   unsigned ff_ftime;     /* 文件最后一次修改的时间 */
   unsigned ff_fdate;     /* 文件最后一次修改的日期 */
   long ff_fsize;         /* 文件长度 */
   char ff_name[13];      /* 找到的文件名 */
}

    make_dir() 调用的 compare() 函数用于进行比较筛选,比较是通过库函数 strstr() 进行的,strstr() 判断找到的文件名字符串是否包含肯定不为文本文件的扩展名字符串,如包含,则返回指向该字符串所在位置的指针,否则返回 NULL。当 strstr() 返回值为真时,compare() 返回 0,否则返回 1。

compare()    /* 如为可能是文本文件的文件名,返回 0,否则返回 1 */
{
   if(strstr(pt[0].ff_name,".EXE") || strstr(pt[0].ff_name,".COM")
           || strstr(pt[0].ff_name,".OV") || strstr(pt[0].ff_name,".OBJ")
           || strstr(pt[0].ff_name,".LIB") || strstr(pt[0].ff_name,".BAK")
           || strstr(pt[0].ff_name,".FOX") || strstr(pt[0].ff_name,".DBF")
           || strstr(pt[0].ff_name,".IDX"))
      return 0;
   return 1;
}

    数组建立后,要在屏幕上分页显示,须建立以下显示目录的函数:

disp_dir(int a)                         /* 显示目录,a 为页号 */
{
   int i=16,j=1,k;                      /* i为行坐标,j为列坐标,k为数组项 */
   for(k=0;k<80;k++)                    /* 循环 80 次 */
      write_char(14,k,''''_'''',0x02);        /* 在屏幕第 14 行画一横线 */
   k=(a-1)*PG;                          /* 根据页号确定数组显示的起始项 */
   while(k<a*PG)  {                     /* 为显示一页文件目录建立的循环 */
      write_string(i,j,ss[k++],0x0E);   /* 显示一个文件名 */
      j+=13;                            /* 右移 13 列 */
      if(j>67)  {                       /* 如列号大于 67 */
         ++i;                           /* 下移一行 */
         j=1;                           /* 列号置 1 */
      }
   }
}

                 4.3 文件名的输入和辅助文件名的建立

    主函数 main() 用 argc 和 argv 两个参数来与操作系统通信,这两个参数称为命令行参数。argv 是一个字符串指针数组,argv[0] 中放的是命令自身的名字,argv[1] 是命令后的第一个参数,argc 是命令行中包括命令自身的实际参数个数,它总是大于或等于 1。根据 argc 的值可以判断是否带命令行参数和带几个参数。
    文件名的输入有两种方式,一种是在 DOS 提示符下键入 BJ AAA.TXT 即进入了文本编辑状态,命令行参数 AAA.TXT 被放在形参 argv[1] 中,它就是要编辑的文件名,这时可将 argv[1] 赋给字符串变量 mfile。
    另一种方法是如果进入编辑程序时未带参数,微机显示如图 4.1 的屏幕画面,并调用文件名输入函数 filename(),用前面讲过的字符串输入函数输入文件名。


          ┌──────────────────────────────┐
          │┌─────────┐                                      │
 软件标志 ││ BJ全屏幕编辑工具 │                                      │
          ││     Ver 1.0      │                                      │
          ││                  │                                      │
          ││     沈 建 威     │                                      │
          │└─────────┘                                      │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
  输入区  │                                                            │
          │         请输入要编辑的文件名:                              │
          │━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━│
          │ AAA.TXT   BBB.TXT   README    SUNDAY1   DATA1   DATA2      │
   参考   │ SUNDAY2                                                    │
目录列表区│                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          │                                                            │
          └──────────────────────────────┘


                             图 4.1 文件名输入时的屏幕


#define QB 500                        /* 编辑数组最大行数 */
#define Q2 (QB*3/5)                   /* 第一次读入编辑数组的行数 */
int old=1;                            /* 老文件标志,old=1 老文件,old=0新文件*/
FILE *fp,*fopen();                    /* 定义正文文件指针 */
........
filename()                            /* 输入要编辑的文件名 */
{
  int i,k,tatol,page=1;               /* tatol 目录总页数, page 目录显示页号 */
  if(!*mfile) {                       /* 如 mfile 为空串 */
    mark();                           /* 显示软件标志 */
    write_string(11,20,"请输入要编辑的文件名:",0x0A);
    tatol=make_dir();                 /* 建目录数组,总页数放入 tatol */
    while(1)  {                       /* 为目录翻屏设置的循环 */
      disp_dir(page);                 /* 显示目录清单 */
      write_space(11,42,20,0x0A);     /* 用 20 个空格清输入区屏幕 */
      if((k=key_string(11,42,mfile,0x0A))>0) break;/* 输入文件名,成功跳出循环*/
      if(k==-1 || k==0)  bk();        /* 空串或按 ESC 退出至 DOS 下 */
      if(k==-4 && page>1)  page--;    /* 按 PgUP,如不在首页,显示上页目录 */
      if(k==-5 && page<tatol) page++; /* 按 PgDn,如不在最后页,显示下页目录*/
    }
  }
  clss(0,HH);                         /* 清全部屏幕 */
  for(i=0;i<QB;i++)  *ss[i]=0;        /* 清内存数组 */
  if((fp=fopen(mfile,"rb"))==NULL) {  /* 只读方式打开文件,如失败为新文件 */
    old=0;                            /* old=0 为新文件 */
    fp_rd=0;                          /* 已从 fp 中读入行最大行号置为 0 */
    write_string(H1,40,"新",0x05);    /* 在信息行提示新文件 */
  }
  else  fp_rd=read_from(0,Q2,fp)-1;   /* 如为老文件,读 Q2 行到编辑数组 */
  write_string(H1,42,"文件名:",0x05); /* 在信息行提示文件名 */
  write_string(H1,50,mfile,0x07);     /* 在信息行显示文件名 */
  if(fp_rd<Q2-1) {                    /* 如读入不足 Q2 行, fp 已读完 */
    ttl_x=fp_rd;                      /* 定文末总行号 */
    fp_end=1;                         /* fp 已读完标志置 1 */
  }
  ss_max=fp_rd;                       /* 编辑数组实用最大行号 */
  f_name();                           /* 建辅助文件名 */
}

    输入文件名后,清全部屏幕,并将编辑数组初始化。要把字符串初始化为空串,只要把字符串的定界符 ''''\0'''' 放入该字符指针的第一个元素。
    filename() 用只读方式打开文件,如打开失败,表示未找到此文件,应判定为新文件。如打开文件成功,则为老文件,文件指针为 fp。这里设置了一个标志变量 old,old=1为老文件,old=0 为新文件。如判定为老文件,应从打开的文件中读 Q2 行至编辑数组里,如实际读出数小于 Q2,表示文件已读完,这时应把老文件已读完标志 fp_end 置为 1,这样,在编辑过程中将不再从原文件进行读操作。Q2 的值根据内存数组的大小确定,一般可比编辑数组总行数的一半大一些,BJ 中取编辑数组总行数 QB 的五分之三。
    在程序中设置了存放文本文件文末行行号的变量 ttl_x,它代表着文本的下界,光标下移、向下翻屏等操作时起一个限界的作用。对新文件来说,ttl_x 不难确定。当打开一个新文件开始编辑操作时,ttl_x 便从 0 开始计数,编辑中每增加一行,ttl_x 的值增加 1。对于一个老文件,如果第一次就能全部读入编辑数组,则读入行数就是 ttl_x 的值。但如果这个老文件很长,一开始就要确定其文末行行号就比较麻烦了,因为文本行在文件里是顺序存放的,并以换行符 (0x0A) 作为一行的结束标志,只有文末行是以文件结束符结尾的。要统计文件的总行数,就必须把文件一部分一部分地移入内存,计算换行符的数目,这样做费工又费时。其实,完全没有必要在一开始就去统计文件的总行数,因为 ttl_x 的限界作用仅对文本的下限,此时必然老文件已全部读完,所以 BJ 编辑软件编制时,先将 ttl_x 的初值置为一个相当大的数,并另外设置了一个变量 fp_rd,对老文件已读出的最后行的文本行号进行计数。当文末行移入编辑数组时,将此时 fp_rd 的值赋给 ttl_x,代替开始时置的那个大数。此时的 ttl_x 才是准确的文末行行号。
    程序里还设置了一个变量 ss_max 表示编辑数组实用行的最大行号,把它和数组定义的最大下标 QB-1 相比较,用以判断编辑数组行数是否超出,以便通过临时文件进行调节。
    程序中 clss() 为清若干行屏幕的子程序,通过调用 BIOS INT10H 中断的第 6 号功能实现:

union REGS r;
........
clss(int a,int b)                 /* 从 a 行至 b 行清屏 */
{
   r.x.ax=0x0600;                 /* 第 6 号功能 */
   r.x.bx=0x0700;                 /* 显示属性 */
   r.h.ch=a;                      /* 左上角行坐标 */
   r.h.cl=0;                      /* 左上角列坐标 */
   r.h.dh=b;                      /* 右下角行坐标 */
   r.h.dl=79;                     /* 右下角列坐标 */
   int86(0x10,&r,&r);             /* 调 INT10H 中断 */
}

    文本文件的读入是通过函数 read_from() 完成的。文件名建立后,还要建立两个扩展名分别为 $1$ 和 $2$ 的临时文件名及一个扩展名为 BAK 的后备文件名,这些辅助文件名在编辑和存盘时将要用到。以读写方式打开两个临时文件,文件指针分别为 FILE 类型的指针 fp1、fp2。此工作是由函数 f_name() 完成的。要注意,文件在读写前必须打开,FILE类型的指针和 fopen() 在使用前必须加以说明。

char *bfile,*file1,*file2;
FILE *fp1,*fp2,*fopen();
........
f_name()                      /* 建立临时文件和后备文件名 */
{
   int i;
   bfile=malloc(16);          /* 为后备文件名字符串分配内存空间 */
   file1=malloc(16);          /* 为临时文件名字符串分配内存空间 */
   file2=malloc(16);          /* 为临时文件名字符串分配内存空间 */
   for(i=0;*(mfile+i)!=''''.'''' && *(mfile+i);i++) {
      *(bfile+i)=*(mfile+i);  /* 截取文件名中“.”前部分放入 bfile */
   }
   *(bfile+i)=0;              /* 字符串以 ''''\0'''' 结尾 */
   strcpy(file1,bfile);       /* 将 bfile 中的字符串拷入 file1 */
   strcpy(file2,bfile);       /* 将 bfile 中的字符串拷入 file2 */
   strcat(bfile,".BAK");      /* bfile 加后缀.BAK */
   strcat(file1,".$1$");      /* 临时文件 file1 加后缀.$1$ */
   strcat(file2,".$2$");      /* 临时文件 file2 加后缀.$2$ */
   fp1=fopen(file1,"wb+");    /* 以二进制读写方式打开临时文件 file1 */
   fp2=fopen(file2,"wb+");    /* 以二进制读写方式打开临时文件 file2 */
}

    上述程序中用库函数 fopen() 打开文件,都是采用了二进制方式,在 fopen() 所带的描述打开文件方式的字符组合参数中加有小写的 b。这种方式和以文本方式打开文件的区别在于,按文本方式打开文件时,字符流的输入/输出 (I/O) 函数对换行符 ''''\n'''' 作转义处理;而二进制方式打开文件,则不作处理,将它作为一个字节对待,这正适应了 BJ  编辑软件需保留换行符的要求。


相关专题: 我的著作
专题信息:
  C语言速成(第三章 程序控制语句)(2006-1-1 13:51:28)[9675]
  C语言速成(第七章 联合、枚举和自定义数据类型)(2006-1-1 13:51:46)[10387]
  C语言速成(第六章 结构)(2006-1-1 13:52:05)[9054]
  C语言速成(第五章 指针)(2006-1-1 17:28:54)[4807]
  C语言速成(第四章 数组)(2006-1-1 13:52:42)[4964]

相关信息:
 没有相关信息
  打印本页
设为首页 | 加入收藏 | 联系我们 | 管理入口
皖ICP备05018956号
Copyright © 2003 J.W.SHEN All Rights Reserved
后台管理系统 V1.0 制作
皖公网安备 34018102340322号