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

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

                            第五章 字处理的基本操作

    各种编辑软件为用户提供的编辑操作功能是十分丰富多采的,但对于大多数全屏幕编辑软件来说,有一些功能如字符输入、光标移动、删字删行、上下翻屏等几乎是必不可少的。在本章中,分别讲述这些基本操作功能函数的设计编制方法。为了帮助读者对编辑功能模块的编制有一个总的轮廓性的了解,在讲述各具体的功能函数编制方法前,有必要先讲一讲编辑功能函数的编制要点。

                          5.1 编辑功能函数编制的要点

    对文本进行的编辑操作,除了文本文件的读入、存盘和打印外,基本上可以分为两类: 一类是实质性的操作,一类是辅助性的操作。凡是涉及到文本字符增加、删减、替换和字符位置移动、重排的操作都属于实质性的操作。如果进行操作后,文本文件本身没有任何改变,即为辅助性的操作,如光标的移动、翻屏、字块的设置和字符串的搜寻等操作均属于此类操作。
    在设计编制各个编辑功能函数时,应把握以下几点:
    (1) 对实质性的操作,必须完成文本内容的改变,这种改变涉及到的可能是一个字符、一个或几个文本行,也可能是仅仅是字符行尾回车符的改变。辅助性操作的函数则不存在这方面的内容。
    (2) 不论光标怎样移动,光标表示的当前操作位置必须始终在内存的编辑缓冲区数组内。当当前位置移出编辑数组或屏幕顶部或底部的一些行超出编辑数组范围,以及编辑数组中行数将超出编辑数组可容纳的最多行数时,要通过存、读临时文件调整编辑数组在文本文件中的相对位置。当前位置也不允许移出文首和文末,在程序中要进行必要的限定。
    (3) 在任一操作终了都不允许光标停留在全角字符的后半字,因此编制程序时应进行判断和区别情况进行调整。
    (4) 编辑数组的字符串行和字块操作暂时保存字块内容的字符串缓冲区都不允许越界,所以当操作引起文本行的长度超过编辑数组允许的最大行长或读入字块的缓冲区将发生溢出时,应有警告性提示并恢复初始状态,以防止程序运行中发生错乱,给操作者留有采取补救措施的余地。
    (5) 对可能引起字块块首、块尾行列号改变的操作必须计算操作后改变了的块首、块尾行号及列号。
    (6) 必须相应改变操作后光标所在当前位置的文本行列号、字序号、编辑数组行列号和屏幕显示的屏号、行列坐标的值。
    (7) 应对操作完成后的屏幕显示作出判断和处理,根据变化了的文本内容和光标位置更新屏幕的显示内容。可能是仅仅重显光标,也可能是重显一行或一整屏。

                             5.2 字符输入的基本操作

    作为字符写入文本的包括 ASCII 码为 32─126 (0x20─0x7E) 的半角字符和大于 160 (0xA0) 的全角字符。在函数 Chr() 中,执行写入字符功能时,对 cc.ch[0] 中的值先圈定响应范围: ASCII 码取值 32─126 和大于 160。
    字符写入一文本行时,一般分“插入”和“修改”两种情况。两种状态可通过按 Ins 键进行切换,并在屏幕信息行提示,插入开关变量 ins=1 为“插入”状态,ins=0 为“修改”状态。在“插入”状态时,字符写入字符串里和光标对应的当前位置中,字符串中当前位置及以后的字符 (包括软回车符 0x8D 和换行符 0x0A) 全部后移一字符位置。在字符串数组中,字符串末尾以空字符 ''''''''\0'''''''' 结束,其 ASCII 码值为 0。如在编辑数组 *ss[ ] 的 ss_x 行、yy 列插入一字符,字符已用 bioskey(0) 读入 cc.ch[0] 中,字符串原先的长度用 strlen() 函数测得为 g,则其实现过程为先将串尾的''''''''\0''''''''后移一格,再将当前位置 yy 及以后的字符自后至前依次后移一格,最后把 cc.ch[0] 中的字符填入 yy 列处,并将光标位置后移一字节。如输入的是一个全角字符,就要进行两次这样的过程。
    在“修改”状态下输入字符,新输入的字符覆盖当前光标位置处的原字符。由于存在半角字符和全角字符的区别,因此要对下列四种不同情况,作出处理:
    (1) 半角字符覆盖半角字符,只要把输入的字符写入字符串中和当前光标相对应的位置,原字符即被覆盖了。
    (2) 全角字符覆盖全角字符,方法同上。
    (3) 半角字符覆盖全角字符,将输入的字符覆盖全角字符的前半字,全角字符的后半部填空格。
    (4) 全角字符覆盖一个半角字符和一个全角字符的前半部,在将输入字符填入字符串,覆盖原半角字符和原全角字符前半部后,在原全角字符后半部填空格。
    下面是一个简化了的字符输入函数,这个函数省略了输入时的排版处理、字符输入后字块标志位置变化的计算、字序号计算、和用小键盘画中文表格线等功能,只是帮助读者理解字符输入的最基本操作是如何实现的。

int ins;                                  /* 插入状态的开关变量 */
........
Chr()                                     /* 简化了的字符串输入函数 */
{
   int j,g;
   if((cc.ch[0]>31 && cc.ch[0]<127) || cc.ch[0]>160) { /* 屏蔽控制键 */
      g=string_lingth();                  /* 测字符串长(不包括回车换行符) */
      if(ins||yy==g) {                    /* 如为“插入”状态或写在行末时 */
         for(j=g+3;j>yy;j--)              /* 插入处以后字符后移一列 */
            ss[ss_x][j]=ss[ss_x][j-1];
      }
      else {                              /* 如为“修改”状态 */
         if(cc.ch[0]<127 && ss[ss_x][yy]>160)  /* 如为半角覆盖全角 */
            ss[ss_x][yy+1]=32;            /* 全角字符后半字用空格覆盖 */
         if(vs(yy)==0 && cc.ch[0]>160 && ss[ss_x][yy]<127
                && ss[ss_x][yy+1]>160)    /* 如为全角覆盖半角和一个全角前半部 */
            ss[ss_x][yy+2]=32;            /* 全角字符后半字用空格覆盖 */
      }
      ss[ss_x][yy]=cc.ch[0];              /* 输入字符填入字符串 */
      yy++;                               /* 当前光标位置右移一格 */
   }
}

    确定是全角字符前半部还是后半部字节的方法是,统计从字符串首至当前位置大于 160(0xA0) 的字节数,根据其奇偶性作出判断,奇数为全角字符前半字,偶数为后半字。

vs(int a)                         /* 统计全角字节数,a 为字符当前列号 */
{
   int i,v=0;
   for(i=0;i<=a;i++) {
      if(ss[ss_x][i]>160)  v++;   /* 如为全角字符的字节,计数值加 1 */
   }
   return(v%2);                   /* 返回奇偶值 */
}

    % 为取模运算符,vs() 中 return 语句返回的是 v 除以 2 所得的余数,如果 v 能被 2 整除,则返回 0,否则返回 1。
    字符输入后屏幕显示要作相应的改变。通常可用比较简单的办法,即当光标位置仍在本屏内时,重显本行;当光标位置移至本屏外时,清除本屏,显示后一屏。
    程序中的 string_lingth() 函数用于计算除行末回车符和换行符外的当前文本行长度字节数。除文末行外,字节数均为字符串总长减 2。

int string_lingth()                  /* 计算除回车换行符外的当前行长 */
{
  int g;
  g=strlen(ss[ss_x]);                /* 测当前行字符串长 */
  if(ss[ss_x][g-1]==0x0A) g-=2;      /* 去除回车换行符的长度 */
  return g;                          /* 返回计算值 */
}

                              5.3 光标的移动 

    在全屏幕编辑过程中,当前光标位置相对于文本和屏幕都是动态的,对文本的各种操作都会引起光标位置的变动。这里专门介绍一下通过按上、下、左、右四个箭头键移动光标,改变当前操作位置的实现过程。这四个键均为特殊键,它们的扫描码的低位字节值为 0,高位字节的值分别等于 72、80、75、77。

5.3.1 光标的右移
    当按动向右的箭头键时,进行光标右移操作,执行函数 Right()。这时如果不在字符行的行末,编辑数组行的当前列号 yy 加 1,相应地文本当前字序数也加 1。
    如果操作前光标已在编辑数组行的行末,这时如是文末行,光标不能再右移,可跳出本功能。如不是文末行,则光标移至下行首,文本当前行号 xx 和编辑数组当前行号 ss_x 均加 1,列号 yy 变为 0。
    由于不允许光标停留在全角字符的后半部,因此有必要在光标右移函数中设置一个循环,当判定光标落在全角字符后半部时,再进行一次右移光标的操作,这样当按右移光标键时,如果后面是一个全角字符,光标就移动了两个字符的位置。这一点在处理涉及全角字符的操作时十分重要,在下面要讲到的光标左移子函数的编制中,同样采取了这样的处理方法。
    有关光标移动后屏幕显示的处理,将在讲完四种光标键移动模块编制后一并讲述。

Right()                         /* 右移光标键 */
{
  int g;
  g=string_lingth();            /* 计算不包括行末回车换行符的行长 */
  for(;;)  {                    /* 为全角字符设立的循环 */
    if(yy<g) {                  /* 如不在行末 */
      y++;  yy++;  ser++;       /* 右移一字节 */
      ........                  /* (显示) */
    }
    else  {                     /* 如在行末 */
      if(xx<ttl_x) {            /* 如不在文末行 */
        ss_x++;    xx++;        /* 至下一行 */
        if(ss_x>=ss_max)  tj(); /* 如超出缓冲区数组中已存最大行,补充数组行 */
        yy=0;                   /* 至行首 */
        ser+=2;                 /* 计算字序数 */
        ........                /* (显示) */
      }
      else  break;              /* 如在文末行尾,退出循环 */
    }
    if(vs(yy-1)==0)  break;     /* 如不为全角前半字,退出循环 */
  }
}

5.3.2 光标的左移
    左移光标的函数为 Left(),当按动左移光标键时,如果光标位置不在编辑数组的文本行首,文本列号 yy、和文本的字序号 ser 均减 1。如果光标位置原在文本行首,如当前行为文首行,光标处在第 0 行 0 列,不能再前移,应跳出本功能;当不在文首行时,光标移到上行末,文本行号 xx、编辑数组行号 ss_x 均减 1,文本的字序号 ser 应减 2,以减少上行末回车占用的两字节。

Left()                           /* 左移光标键 */
{
  for(;;)  {                     /* 为全角字符设的循环 */
    if(yy>0) {                   /* 如不在行首 */
      yy--;   y--;  ser--;       /* 左移一字节 */
      ........                   /* (显示) */
    }
    else {                       /* 如在行首 */
      if(xx) {                   /* 如在不文首行 */
        xx--;  ss_x--;           /* 移到上一行 */
        ser-=2;                  /* 字序数减少回车占的 2 字节 */
        yy=string_lingth();      /* 光标至行末 */
        ........                 /* (显示) */
      }
    }
    if(vs(yy-1)==0)   break;     /* 如不在全角前半字,退出循环 */
  }
}

5.3.3 光标的上移
    此功能的函数 Up() 如下:

Up()                                /* 上移光标 */
{
  int k;
  if(xx) {                          /* 如不在文首行 */
    if(!ss_x) st();                 /* 如到编辑数组首行,从 fp1 读入 Q3 行 */
    xx--;   ss_x--;                 /* 上移一行 */
    ser-=yy+2;                      /* 计算上行末字序数 */
    k=m;                            /* 保存原屏号 */
    if(x>0)  {                      /* 如不在本屏起始行 */
      x--;                          /* 屏幕坐标上移一行 */
      orien();                      /* 重定光标处列号 */
      if(m!=k)  disp_t();           /* 如不在原屏,重显一屏 */
    }
    else  {                         /* 如在本屏起始行 */
      orien();                      /* 重定光标处列号 */
      if(m==k) roll_scr_down(0,H3); /* 如仍在原屏,向下滚屏 */
      else  disp_t();               /* 否则显示光标所在屏 */
    }
    ser-=string_lingth()-yy;        /* 计算字序数 */
  }
}

    Up() 设计时,限定当光标不在文首行时,当前行前移一行,文本行号 xx 和编辑数组行号 ss_x 均减一。光标移到上一行后,光标列的位置可能出现以下几种情况:
    (1) 光标原列号为 yy,目标行的 yy 列为一半角字符或一全角字符的前半字,则移动后的列号不变。
    (2) 目标行的 yy 列为一全角字符的后半字,要将 yy 减 1,移光标至该全角字符的前半字。
    (3) 目标行的行尾在 yy 列之前,则要计算目标行的行长,并使 yy 等于该行长,定光标于行末。
    函数 orien() 对上述三种可能的情况分别进行判断和处理,并计算因此而引起的屏号 m 、屏幕列坐标等的变化。

orien()                           /* 重定光标列号 */
{
   int g;
   g=string_lingth();             /* 测当前列长(不包括回车换行符 */
   if(yy>g)  yy=g;                /* 如光标在字符串尾后,定光标于行末 */
   if(vs(yy-1))  yy--;            /* 如光标落在后半汉字,前移一字节 */
   y=yy-m*BP;                     /* 计算屏幕 y 坐标 */
   if(y<0) comput();              /* 如 y 为负数,重算各参数 */
}

    如果上移后光标位置超出了现屏幕限定的范围,则必须重新显示屏幕。在函数 orien()中,用 comput() 函数重新计算屏幕显示的有关参数。

5.3.4 光标的下移
    光标下移函数 Down() 和 Up() 函数编制的思路基本上是一样的,不同的是它限定光标在文末行时,不能再下移而返回调用函数。

Down()                            /* 下移光标 */
{
  int k,g;
  g=string_lingth();              /* 计算行长(不包括回车换行符) */
  if(xx<ttl_x)  {                 /* 如不在文末行 */
    ss_x++;   xx++;               /* 下移一行 */
    ser+=g-yy+2;                  /* 计算下行首字序数 */
    if(ss_x>=ss_max) tj();        /* 如超出缓冲区数组,从 fp2 或 fp 补充 */
    k=m;                          /* 保存原屏号 */
    if(x<H3)  {                   /* 如不在屏幕最下一行 */
      x++;                        /* 光标显示行下移一行 */
      orien();                    /* 重定光标处列号 */
      if(m!=k) disp_t();          /* 如不在原屏,重显一屏 */
    }
    else  {                       /* 如光标在屏幕最下一行 */
      orien();                    /* 重定光标处列号 */
      if(m==k) {                  /* 如仍在原屏 */
        roll_scr_up(0,H3);        /* 向上滚屏 */
        disp(ss_x,x);             /* 屏幕最下行补显一行 */
      }
      else  disp_t();             /* 如不在原屏,重显一屏 */
    }
    ser+=yy;                      /* 计算字序数 */
  }
}

    进行以上四种移动光标操作时,当光标将移出原屏幕的文本显示区时都要重新显示屏幕,更新屏幕可以通过调用函数 disp_t() 清除编辑文本显示区并全部重显一屏,这样做编程比较简单,但在一些运行速度较慢并用图形方式显示汉字的微机上使用,会感到显示太慢,有一个明显的“卷帘”的过程。因此,我们区别情况,采用了两种办法,在光标移动引起显示屏号变化,如原显示第 2 屏变为显示第 0 屏时,调用 disp_t() 全部重显;而在屏号不变,仅在屏底或屏顶超出一行时,采用调用 INT10H 第 6 或第 7 号功能,向上或向下滚屏的方法,并在滚屏后屏顶或屏底空出的一行补显当前行,这样做比全部重显一屏要快得多。子函数 roll_scr_up() 和 roll_scr_down() 分别调用 INT10H 的第 6 和第 7 号功能实现向上和向下滚屏,参数 a 和 b 分别为滚屏区上、下的屏幕行坐标:

roll_scr_up(int a,int b)          /* 向上滚屏 */
{
   r.x.ax=0x0601;                 /* 第六号功能,上滚一行 */
   r.h.ch=a;                      /* 左上角行坐标 */
   r.h.cl=0;                      /* 左上角列坐标 */
   r.h.dh=b;                      /* 右下角行坐标 */
   r.h.dl=FH;                     /* 右下角列坐标 */
   r.h.bh=TEXT_COLOR;             /* 字符显示属性 */
   int86(0x10,&r,&r);             /* 调用 INT10H 中断 */
}

roll_scr_down(int a,int b)        /* 向下滚屏 */
{
   r.x.ax=0x0701;                 /* 第七号功能,下滚一行 */
   r.h.ch=a;                      /* 右上角行坐标 */
   r.h.cl=0;                      /* 右上角列坐标 */
   r.h.dh=b;                      /* 左下角行坐标 */
   r.h.dl=FH;                     /* 左下角列坐标 */
   r.h.bh=TEXT_COLOR;             /* 字符显示属性 */
   int86(0x10,&r,&r);             /* 调用 INT10H 中断 */
   disp(ss_x,x);                  /* 当前行补显一行 */
}

                                  5.4 上下翻屏

    几乎所有的全屏幕编辑软件都用 PgUp 和 PgDn 键进行上、下翻屏,或称为前、后翻屏,这两个特殊键的扫描码高位字节的值分别为 73 和 81。当按 PgUp 或 PgDn 键时分别调用子函数 PgUp() 和 PgDn()。

PgUp()                                 /* 上翻一屏 */
{
  long a;
  if(xx-x)  {                          /* 如不在文首屏 */
    a=(xx<=H3-2)0:xx-H3+2;            /* 定目标行号 */
    upto(a);                           /* 光标上移到第 a 行 */
    comp_disp();                       /* 重显一屏 */
  }
}

PgDn()                                 /* 向下翻屏 */
{
  long a;
  if(xx<ttl_x)  {                      /* 如未到文末 */
    a=(ttl_x-xx>=H3-2)xx+H3-2:ttl_x;  /* 定目标行号 */
    dnto(a);                           /* 光标下移至第 a 行 */
    disp_t();                          /* 重显一屏 */
  }
}

    上下翻屏可以一次翻过完整的一屏,如一个编辑软件编辑时屏幕可显示文本 22 行,(屏幕显示第 0 行到第 21 行),按 PgDn 键下翻一屏显示第 22 行到第 43 行,上下两屏没有重复,每次按翻屏键越过的行数等于屏幕的编辑文本显示行数。也可以设计为前屏保留若干行到后一屏,如原屏幕显示的是第 0 行到第 21 行,按 PgDn 键下翻一屏显示第 19 行至第 40 行,每次翻屏越过 19 行,前后两屏重迭 3 行,编辑操作的当前行也相应后移 19 行,BJ 编辑程序就是采取这种形式,其好处是阅读编辑文本时能有助于前后连贯。
    这里有必要建立两个函数,一个是将当前行向文首方向上移到一指定行的函数 upto() 和向文末方向下移到一指定行的函数 dnto()。

upto(long a)                    /* 上移到第 a 行 */
{
   ser-=yy;                     /* 计算当前行首的字序数 */ 
   while(oa && a<=xx-ss_x+x) {  /* fp1 中尚有记录,目标行不在当前屏幕上则循环 */
      while((ss_x-1)>x) {       /* 数组当前行号大于屏幕行坐标,则循环 */
         --xx;    --ss_x;       /* 上移一行 */
         ser-=string_lingth()+2;/* 计算字序数 */
      }
      st();                     /* 移到数组当前行号不足屏幕行坐标,从fp1读入*/
   }
   while((xx-1)>=a) {           /* 目标行在当前行前,则循环 */
      --xx;   --ss_x;           /* 上移一行 */
      ser-=string_lingth()+2;   /* 计算字序数 */
   }
   orien();                     /* 重定列号 */
   ser+=yy;                     /* 计算字序数 */
}

dnto(long a)                    /* 下移到第 a 行 */
{
   int i;
   ser+=string_lingth()-yy+2;   /* 计算下行首字序数 */
   ss_x++;   xx++;              /* 当前行下移一行 */
   while(a>=xx-ss_x+(i=ss_max+x-H3)&&(ob||!fp_end))  {
            /* 目标行超出或屏幕最下行超出数组,且 fp2 或 fp 中尚有记录则循环 */
      while(ss_x<i)  {          /* 当前行下移不会引起屏底超出数组则循环 */
        ser+=string_lingth()+2; /* 计算下行首字序数 */
        ss_x++;   xx++;         /* 当前行移至下行 */
      }
      tj();                     /* 屏底行要超出数组时,从 fp2 或 fp 读入数组 */
      if(fp_end && a>ttl_x)  a=ttl_x;
                                /* 如目标行大于总行数时,目标行取最末行 */
   }
   while(xx<a) {                /* 目标行在当前行后则循环 */
      ser+=string_lingth()+2;   /* 计算下行首字序数 */
      ss_x++;    xx++;          /* 当前行移至下行 */
   }
   orien();                     /* 重定光标列号 */
   ser+=yy;                     /* 计算字序数 */
}

    在 upto() 和 dnto() 里又用到了上一节讲到的函数 orien(),因为当前行移动后,也存在光标落在行末后空区或落在全角字符后半字的可能,所以必须进行适当的处理。这两个函数是十分有用的,后面讲到移动到块首、移动到指定行或文末、块移动、块拷贝等功能时,还要多次用到它们。在上下翻屏的函数 PgUp() 和 PgDn() 中,就是先确定翻屏时当前行移动的目标行,再分别调用 upto() 或 dnto() 函数并显示翻屏后的屏幕来实现的。

                               5.5 回车键的输入

    回车键在文本编辑输入时有十分重要的作用, 通常回车键产生的硬回车符和换行符以 0x0D0A 记入文本,在不进行排版处理非格式文件 (如源程序清单) 中硬回车代表一个文本行的结束,而在格式文件中则把两个硬回车符之间的文本作为一个“段”。下面是回车键的输入函数 Enter()。

char da[]={0x0D,0x0A,0};             /* 只有硬回车换行符的字符串 */
........
Enter()                              /* 回车键输入 */
{
  int g,k;
  chg=1;                             /* 文件已修改标志置为 1 */
  if(!ins) {                         /* 如为非插入状态 */
    g=string_lingth();               /* 计算当前行长(不含回车换行符) */
    strcpy(ss[ss_x]+g,da);           /* 字符串以硬回车结尾 */
    if(xx==ttl_x)   {                /* 最后一行时,下移一行 */
      fp_rd++;                       /* fp 已读出最大行号加 1 */
      ttl_x++;                       /* 文末行行号加 1 */
      ss_max++;                      /* 数组最大行号加 1 */
    }
    ........
    ss_x++;                          /* 数组行号加 1 */
    xx++;                            /* 文本行号加 1 */
    ser+=g-yy+2;                     /* 计算字序数 */
    ........                         /* (显示屏幕) */
  }
  else  {                            /* 如为插入状态 */
    ........
    intercept(yy);                   /* 从当前光标处折断字符串 */
    strcpy(ss[ss_x-1]+yy,da);        /* 折断处加硬回车换行符(0x0D0A) */
    ........                         /* (显示屏幕) */
  }
  yy=0;                              /* 光标至行首 */ 
}

    回车键的输入在“插入”和“修改”两种状态下,设计成有不同的效果。 当处于“修改”状态下,光标在一文本行的任何列位置时按回车键,光标移至下行首,如果原行是以排版形成的软回车换行符 0x8D0A 结尾的,则将其改为硬回车换行符 0x0D0A。当原光标在文末行时按回车键,则将原文末行改由 0x0D0A 结尾,并在下面增一个新的空行,光标移至该新的文末行首。
    当处在“插入”状态下,光标在一文本行某一列时按回车键,则把该行从光标处折断,光标所在字符及其后的部分作为一个新行插在原行下面。回车键输入后,屏幕显示要进行相应地重显。在 Enter() 中调用了折断字符串分成两行的函数 intercept(),这个函数将原行从光标处一分为二,原行在折断处以硬回车换行符 0x0D0A 结尾,光标移至新行的行首,编辑数组内当前行以后各行向后顺移一行。

intercept(int ky)                /* 折断字符串,换行,ky为折断处列号 */
{
  ........                       /* (计算块标变化) */
  xx++;  ss_x++;                 /* 当前行改为下行 */
  fp_rd++;                       /* fp已读出最大行号加 1 */
  ttl_x++;                       /* 文末行行号加 1 */
  ss_max++;                      /* 编辑数组总实用最大行号增 1 */
  ser+=2;                        /* 字序数增加回车换行符占用的 2 字节 */
  ........
  mov(ss_x,1);                   /* 编辑数组当前行以后各行向后顺移一行 */
  strcpy(ss[ss_x],ss[ss_x-1]+ky);/* 折断处后字符放入新行 */
  ........
}

                        5.6 退格键和 Del 键的使用

    退格键和 Del 键通常用于删除字符,可以把退格键设置成删除光标前字符,把 Del 键设置成删除光标所在字符,也可以都设置成删除光标前字符。本文实例采用后一种设置,并规定当光标在一行首时,按这两个删除键,将本行接至上行尾(去除上行末的回车换行符)。这个功能函数 Del() 中调用了删字函数 delc(),并根据返回值进行不同的屏幕显示处理。

int delc()                          /* 删字,如在行首,将本行接在上行尾 */
{
  int i,g,k=0;
  for(;;)  {                        /* 为全角字符设的循环 */
    if(y>0)  {                      /* 如不在本屏行首 */
      ........                      /* (计算块标行、列号变化) */
      strcpy(ss[ss_x]+yy-1,ss[ss_x]+yy);
                                    /* 光标处起字符前移一格,覆盖光标前一字符 */
      y--;    yy--;                 /* 前移一字节 */
      ser--;                        /* 字序号减 1 */
      if(vs(yy-1)==0)  {            /* 如不在全角前半字 */
        if(!k) return 0;            /* 如在屏幕最左列,返回 0 */
        else return 1;              /* 否则返回 1 */
      }
    }
    else  {                         /* 如在屏幕最左列 */
      if(m)  {                      /* 如不为 0 屏 */
        m--;                        /* 退到前一屏 */
        y=yy-m*BP;                  /* 光标到屏中,并前移一字 */
        k=1;                        /* 标志置 1 */
      }
      else  {                       /* 如为 0 屏 */
        ........
        if(xx)  {                   /* 如不在文首行 */
          g=strlen(ss[ss_x]);       /* 测行长 */
          ss_x--;                   /* 至上一行 */
          yy=string_lingth();       /* 定光标于上行尾处 */
          if(g+yy>HC-4)  {          /* 如两行相接后超长 */
            write_prompt(3);        /* 提示超长 */
            yy=0;                   /* 恢复原行列号 */
            ss_x++;                 /* 回原行 */
            return -1;              /* 退出,返回 -1 */
          }
          ........                  /* (计算块标行、列号变化) */
          xx--;                     /* 文本当前行改为上行 */
          strcpy(ss[ss_x]+yy,ss[ss_x+1]);   /* 将下行拷至本行尾回车符前 */
          ser-=2;                   /* 字序数减少回车符的两位 */
          movbk(ss_x+1,1);          /* 数组后续各行前移,覆盖原行 */
          ttl_x--;                  /* 文末行行号减 1 */
          fp_rd--;                  /* fp 已读出行最大行号减 1 */
          ss_max--;                 /* 数组实用最大行号减 1 */
          return 2;                 /* 返回 2 */
        }
        else return 0;              /* 如在文首行,返回 0 */
      }
    }
  }
}

    delc() 被编制成循环执行形式。当删除一个全角字符时,先删除后一字节,此时程序通过调用 vs() 判定光标在全角字符后半部,再循环一次,继续删除前半个汉字。当光标位于行首时 delc() 用 strcpy() 函数把原当前行接到上行末的回车符前,将两行连接成一行,当前行号减 1,编辑数组中后续的各行依次前移一行。数组中各行前移是由函数 movbk() 实现的。
    这专门讲一下用库函数 strcpy() 编程的一些技巧。有些人把 strcpy() 函数的作用仅仅理解为一个字符串对另一个字符串的复盖式更新,这种认识是不全面的。strcpy() 的两个形参都是字符串指针,它们不但可以代表两个字符串变量,也可以是一个字符串变量的不同指针位置。当两个参数是同一个变量的指针时,必须注意,后一个参数表示的指针位置必须在前一个参数表示的指针位置之后。
    例如:有两个字符串 a 和 b:

    char a[ ]="0123456789";
    char b[ ]="ABCDEFG";

    如果在 a 中要删除“3456”几个字符,可用:

    strcpy(a+3,a+7);
   
    得到的字符串为: “012789”,上述 delc() 函数和在后面介绍字符串替代函数 F6()就用到这办法。如果要把字符串 b 接到字符串 a 的字符“7”后面,并覆盖“89”两字符,可用:

    strcpy(a+8,b);

    得到的字符串为:“01234567ABCDEFG”。在光标位于行首按删字键,把当前行接至上行末,又要去掉上行末的回车换行符时,就是采用此方法覆盖回车换行符,实现两行相接的。这样做比用 strcat() 函数更方便一些。在上例中,如用 strcat() 函数,则必须分两步进行,先截去字符串 a 末尾的“89”两字符,再用 strcat() 实现两行相接:

    a[8]=0;
    strcat(a,b);

                      5.7 行的插入、删除和局部删除

5.7.1 插入一个空行
    在当前行前插入一个空行,通过按组合键 Ctrl+N 来实现。有些编辑软件设计为必须在行首按此类功能键才插一空行,使用时甚感不便,因此我们可把它设计为在本行任意位置按 Ctrl+N 键均可在本行前插一空行,这个行只有一个回车换行符 0x0D0A。其实现过程是先将编辑数组中当前行及其后的各行,均后移一行,再用 strcpy() 函数拷贝 0x0D0A 至当前行首,覆盖原行。

Ctrl_N()                         /* 插入一空行 */
{
  chg=1;                         /* 文件已修改标志置为 1 */
  fp_rd++;                       /* fp 已读出最大行号加 1 */
  ttl_x++;                       /* 文末行行号加 1 */
  ........                       /* (计算块标位置变化) */
  ser-=yy;                       /* 计算字序数 */
  ........
  ss_max++;                      /* 缓冲区数组实用最大行号加 1 */
  mov(ss_x,1);                   /* 当前行起各行后移一行 */
  strcpy(ss[ss_x],da);           /* 将一硬回车换行符拷到当前行行首处 */
  ........                       /* (屏幕显示处理) */
  yy=0;                          /* 光标移至行首 */
}

    编辑数组中指定各行的后移由函数 mov() 来完成。

mov(int s,int a)                 /* 编辑数组 s 行起的各行后移 a 行 */
{
   int i;
   for(i=QB-1;i>=ss_x+a;i--)  strcpy(ss[i],ss[i-a]);
}

    函数 mov() 和前一节讲到的 movbk() 在其它不少功能模块中也常常用到。

5.7.2 删除一行
    删除当前行通过按 Ctrl+Y 实现。函数 Ctrl_Y() 主要是通过调用 movbk() 将内存数组中当前行后面的各行均前移一行,原当前行被后一行覆盖,原行即被删除了。

Ctrl_Y(int a)                    /* 删除文本行 */
{
  chg=1;                         /* 文件已修改标志置为真 */
  ........                       /* (计算字块坐标变化) */
  ser-=yy;                       /* 计算字序数 */
  if(xx<ttl_x) {                 /* 如不是文末行 */
    fp_rd--;                     /* fp 已读出最大行号减 1 */
    ttl_x--;                     /* 文末行行号减 1 */
    ss_max--;                    /* 编辑数组实用最大行号减 1 */
  }
  movbk(ss_x,1);                 /* ss_x 后各行前移一行 */
  m=0;                           /* 至 0 屏 */
  yy=0;                          /* 至行首 */
  ........                       /* (显示屏幕) */
}

5.7.3 删至行首
    对于文本行的局部删除,这里提供两个功能子函数 Ctrl_E() 和 Ctrl_T()。Ctrl_E() 通过按组合键 Ctrl+E 删除文本当前行光标前的部分,Ctrl_T() 通过按 Ctrl+T 键删除自光标处起至行末的部分。
    Ctrl_E() 函数用 strcpy() 函数将当前行中光标位置及以后的字符串拷贝到行首,光标前的字符即被删除,并将光标位置定至行首:

Ctrl_E()
{
   .........
   strcpy(ss[ss_x],ss[ss_x]+yy);      /* yy 后部分拷贝至行首 */
   yy=0;                              /* 光标位置定至行首 */
   .........
}

5.7.4 删至行末
    Ctrl_T() 函数把行末的回车换行符拷贝到当前光标处,将光标位置及以后的字符截去,光标当前位置不变:

Ctrl_T()
{
   ........
   g=string_lingth();                 /* 测行长 */
   strcpy(ss[ss_x]+yy,ss[ss_x]+g);    /* 原行尾回车换行符前移至当前光标处 */
   disp(ss_x,x);                      /* 重显本行 */
}

                            5.8 迅速改变光标位置

    在编辑时,可以按箭头键移动光标,但为了提高编辑工作的效率,建立一些迅速移动光标的功能是十分必要的。前面介绍过的用 PgUp 和 PgDn 键翻屏就是一类快速移动光标的方法,下面再介绍几种方法:

5.8.1 快速移至行首
    按 Home 键调用 Home() 函数把光标迅速移到本行行首,Home() 函数置 yy=0,将光标位置定到行首,如原屏幕不为 0 屏,则显示 0 屏。因为光标从 yy 列移至 0 列,所以当前位置的字序数 ser 应减少 yy 字节。

Home()                           /* 移至行首 */
{
  ser-=yy;                       /* 计算字序数 */
  yy=0;                          /* 到行首列 */
  if(m)  {                       /* 如不是 0 屏,显示 0 屏各行 */
    m=0;
    disp_t();
  }
}

5.8.2 快速移至行末
    按 End 键调用 End() 子函数实现,End() 函数先计算本行不包括回车换行符的字符串长度 g,以确定行末列号,再令 yy=g,把光标定到行末。如光标移动后屏号发生了变化,则重显新屏。字序号 ser 相应增加 g-yy 字节。

End()                            /* 到行末 */
{
  int i,g;
  i=m;                           /* 保存屏号 */
  g=string_lingth();             /* 计算字符串长 */
  ser+=g-yy;                     /* 计算字序数 */
  yy=g;                          /* 当前位置移至行末 */
  comput();                      /* 计算各参数 */
  if(i!=m)  disp_t();            /* 如不在原屏,重显一屏 */
}

5.8.3 直接移至文首
    在 5.3 节里语句介绍了上移至指定行的函数 upto(),因此可以把 upto() 的参数定为0x0L 将光标移至第 0 行,因为 upto() 的形参是长实型的,所以实参要用 0x0L。接着用函数 Home() 将光标移至行首。

F9()                              /* 移到文首 */
{
   upto(0x0L);                    /* 上移至第 0 行 */
   Home();                        /* 至行首 */
   comp_disp();                   /* 重显屏幕 */
}

5.8.4 直接移至文末
    在前面讲述上下翻屏时,介绍过一个移光标到指定目标行的子函数 dnto(),只要把文末行号作为 dnto() 的实参,就能将光标移至文末行,再用 End() 子函数将光标移至该行末便能完成。移至文末后要根据列号 yy 重新计算屏幕显示列坐标 y、屏号 m,并根据当前屏幕显示列坐标,计算屏幕显示起始列号,重新更新屏幕显示。本功能的实现是通过按组合键 Shift+F9 运行子函数 Shift_F9() 来完成的。   

Shift_F9()                         /* 移至文末 */
{
   if(xx<ttl_x)  dnto(ttl_x);      /* 如不在文末行,下翻到文末行 */
   End();                          /* 光标移至行末 */
   comp_disp();                    /* 计算参数,重显一屏 */
}

5.8.5 移至指定行
    本功能设计为按 F2 键后,执行子函数 F2()。F2() 函数先根据提问调用输入数字的函数 key_digit() 在屏幕的提问行输入要移到的目标行行号,但这个行号是按通常习惯表述的行号,比我们程序中的文本文件行号大 1,所以要减去 1,变为程序中用的行号。因为习惯上总是把文本的首行首列称为第 1 行第 1 列,字序号也为 1,但程序编写时为了和数组标号相一致,将其定为第 0 行第 0 列,字序号也相应取 0,因此必须进行换算。在图 3.1 中看到的屏幕下部信息行显示的行、列、序号是按习惯表述的,在显示前必须把程序中当前光标所在的行、列、序号各加 1。

F2()                                  /* 移动当前行至指定行 */
{
  long i;
  write_ques(6);                      /* 提问要移到的行号 */
  if((i=key_digit(19))<=0) {          /* 输入行号,如为空串或按 ESC 键 */
    clear_ques();                     /* 清提问区 */
    return;                           /* 退出本功能 */
  }
  i--;                                /* 习惯行号和程序行号间差 1 */
  if(fp_end && i>ttl_x)  i=ttl_x;
                       /* 如 fp 已读完,目标行号大于文末行号,移到全文最后一行 */
  tyy=yy;                             /* 保存原列号 */
  mvto(i);                            /* 移到目标行 */
  comp_disp();                        /* 重显一屏 */
  clear_ques();                       /* 清提问区 */
}

    函数 key_digit() 的作用是从键盘输入一个数,它的实现方法和第 4.1 节介绍的字符串输入函数 key_string() 基本相同,只是对输入的字符限定为数字,并将输入的字符串用库函数 atoi() 转换为数值返回。

int key_digit(int e)                       /* 在 HH 行输入数字,e 为列坐标 */
{
  char a[16];
  int k=0;
  while(1)  {                              /* 为输入数字串设的循环 */
    goto_xy(HH,e+k);                       /* 定光标位置 */
    cc.ii=bioskey(0);                      /* 接收一个按键值 */
    ........
    if(!cc.ch[0])                          /* 如低字节为 ''''''''\0'''''''' */
      switch(cc.ch[1])  {                  /* 判断高字节 */
        case 75:                           /* 如为 Del 键或左移光标键 */
        case 83: goto AA;                  /* 跳转 AA */
      }
    else                                   /* 如低字节不为 ''''''''\0'''''''' */
      switch(cc.ch[0])  {                  /* 判断低字节 */
        case 27: return -1;                /* 如为 ESC 键,返回 -1 */
        case 13: a[k]=0;                   /* 按回车时确认,用 ''''''''\0'''''''' 定界 */
                 if(!k) return 0;          /* 如未输入,返回 0 */
                 return atoi(a);           /* 否则,返回输入的数值 */
        case 8:                            /* 如为退格键 */
 AA:      if(k) {                          /* 如不在首字节 */
            a[--k]=0;                      /* 后退一字节填 ''''''''\0'''''''' */
            write_char(HH,e+k,'''''''' '''''''',PROM_COLOR);   /* 原显示字符用空格复盖 */
          }
          break;                           /* 跳出开关语句 */
        case 3:  bk();                     /* 如按 Ctrl+C 键,退至 DOS 下 */
        default:
          if(cc.ch[0]>47 && cc.ch[0]<58) { /* 如为数字 */
            if(!k) write_space(HH,e,6,PROM_COLOR);
                                           /* 如在首字节,用空格复盖原显示 */
            a[k]=cc.ch[0];                 /* 字符写入字符串中 */
            write_char(HH,e+k,a[k],PROM_COLOR);      /* 显示字符 */
            k++;                           /* 移至下一位置 */
          }
          else  write_prompt(4);           /* 否则响铃,提示“必须输入数字!”*/
    }    
  }
}

   write_prompt() 把本程序所有在屏幕提示区显示的提字符串放在一个指针数组 *prom[]中,write_prompt() 的形参就是指针数组的下标,函数中用 write_string() 函数把该下标指向的字符串在屏幕提示区显示出来。类似的函数还有显示提问的函数 write_ques()。而函数 clear_prompt() 和 clear_ques() 则用一连串的空各复盖提示或提问区。这几个函数比较简单本文就不展开分析了,读者可以看本书附录中的源程序。
    在 F2() 中,目标行号确定后,调函数 mvto(),将光标移到目标行。指定的目标行可能在当前行前,也可能在当前行后,mvto() 总是先进行判断,如判定在当前行前,则调用 upto(),否则调用 dnto()。移至指定行后重显一屏。

mvto(long a)                     /* 移到指定行 */
{
  yy=tty;                        /* 恢复原列号 */
  if(a<=xx)  upto(a);            /* 如目标行在当前行前,前移至目标行 */
  else  dnto(a);                 /* 如目标行在当前行后,后移至目标行 */
}


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

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