About My Editor 2    Afritxia 01.13.2003 
 引言2 
    这篇文章并不太适合Java高手和刚要开始学Java的人看.如果你刚弄清楚Java编程是怎么回事,并且想用Java提供swing组件做一些简单的程序,以此来巩固对Java编程学习的话,那你算是找对了.我会披露swing组件中的一些鲜为人知的方法.希望这几篇文章能够成为你在学习Java程序设计道路中的一块铺路石,助你顺利攀到Java程序设计颠峰.     [email protected] 
    本篇话题: 文本编辑区设计. 
 
 
    我的JMDEditor的编辑区使用的是非常简单的JTextArea组件.说它简单是因为它已经们实现了很多常用的功能:Cut,Copy,Paste,Select,SelectAll...甚至还可以设置被选取文本的背景色.所以我们并不用费心去写这些功能. 
    但是,要做一个象样的记事本程序,光有这点功能显然是不够的.看看JDK自带的记事本程序,就连那个还有多次"撤消"与"重做"的功能呢.可它是怎样实现的呢?做了一大堆的AbstractAction类的派生类,其中的UndoAction与RedoAction就是这样来的: 
 class UndoAction extends AbstractAction  {      public UndoAction(){      } 
     public void actionPerformed(ActionEvent e){          // Action Code          // 执行撤消操作      } 
     public void update(){          // Update Code          // 如果当前文本无法再进行撤消,则菜单中的"Undo"就不能被选择了      }  } 
当然,还要有这些东西才能成事: 
 JTextArea editor=new JTextArea();  UndoableEditListener undoHandler=new UndoHandler(); // ??  UndoManager undo=new UndoManager(); // 撤消管理器?  UndoAction undoAction=new UndoAction();  editor.getDocument().addUndoableEditListener(undoHandler); 
 class UndoHandler implements UndoableEditListener // ?  {    public void undoableEditHappened(UndoableEditEvent e){      undo.addEdit(e.getEdit());         // ...    }  } 
 JMenuItem undoMenuItem=new JMenuItem("Undo");  undoMenuItem.addActionListener(undoAction); 
 try{ // UndoAction中的撤消操作代码      undo.undo();  }catch(CannotUndoException ex){      // Throws Exception  } 
我已经乱了!虽然功能比较完善,但是很容易就会让象我一样的初学者晕头转向.所以我千方百计的简化了此操作. 
 JTextArea editor=new JTextArea();  UndoManager undo=new UndoManager(); // 撤消管理器?  undo.setLimit(5); // 5步撤消  editor.getDocument().addUndoableEditListener(new UndoableEditListener(){      public void undoableEditHappened(UndoableEditEvent e) {          undo.addEdit(e.getEdit());      }  }); 
 JMenuItem undoMenuItem=new JMenuItem("Undo");  undoMenuItem.addActionListener(...); 
 public void actionPerformed(ActionEvent e){      String cmd=e.getActionCommand();      // ...      if(cmd.equals("Undo"))          try{       undo.undo(); // Call undo.redo() if you need redo          }catch(CannotUndoException ex){          }      // ...  } 
其实看起来也没简化多少.不过我省掉了UndoableEditListener,这样看起来就没有那么多的弯弯绕了.(更简单的方式?目前我是找不到了) 
    作为一个程序员,在进行编码时经常会遇到编译器给出的错误提示: 
 Error! ... ...          ... ... (17) 
最后给出的是错误的所在行.那么我要做的就是将光标移到文件第一行,然后一下下的数出17行来,再然后解决问题.可是如果错误是在第1234行怎么办?还用土办法?那无异于徒步登月!最好来个行列显示功能.起初,我写了一个行列显示的算法.那不值一提,因为随着文本中的字数渐多时,这个算法几乎是以死机的方式运行的.我想JTextArea中应该有这样的方法,可是我寻觅了大半天也是一无所获.最后,所有的嫌疑都被归到 
 getLineOfOffset(int) 和 getLineStartOffset(int) 
两个函数身上.这两个是什么意思?...看来,只有象搭积木一样把他们搭来看看了: 
// import javax.swing.event.*; 
 JTextArea editor=new JTextArea();  editor.addCaretListener(new CaretListener(){      public void caretUpdate(CaretEvent e){          int dot=e.getDot();          int ln, col;          ln=col=0;          try{              ln=editor.getLineOfOffset(dot);              col=dot-editor.getLineStartOffset(ln);              System.out.println("["+(ln+1)+","+(col+1)+"]");          }catch (BadLocationException Ex){          }      }  }); 
至于getLineOfOffset(int)与getLineStartOffset(int),是个什么地噶活: 
// 摘录自: SUN Microsystem jdk1.3.1 / src.jar / JTextArea.java 
 public int getLineOfOffset(int offset) throws BadLocationException {      Document doc = getDocument();      if (offset < 0) {          throw new BadLocationException("Can't translate offset to line", -1);      } else if (offset > doc.getLength()) {          throw new BadLocationException("Can't translate offset to line", doc.getLength()+1);      } else {          Element map = getDocument().getDefaultRootElement();          return map.getElementIndex(offset);      }  } 
 public int getLineStartOffset(int line) throws BadLocationException {      Element map = getDocument().getDefaultRootElement();      if (line < 0) {          throw new BadLocationException("Negative line", -1);      } else if (line >= map.getElementCount()) {          throw new BadLocationException("No such line", getDocument().getLength()+1);      } else {          Element lineElem = map.getElement(line);          return lineElem.getStartOffset();      }  } 
恩!大大地好!可以把他们塞到JEditorPane,JTextPane里去,继续效忠我们Java爱好者.没看懂?各位只管拿去改改随便用就成了. 
    本篇最后登场的是一个重量级话题:查找与替换 
    首先,要做一个查找与替换对话框.它继承自JDialog类,并且是可以和主窗体并行的. 
 public class FindDlg extends JDialog  {      public FindDlg(JFrame f){          super(f, "Find and Replace.", false); // 用false就能并行          // ... ...      }      // Find and Replace Code ...  } 
然后就是最重要的查找与替换功能的实现了: 
// KEY: Find function ////////////////////////////////////////////////////////// // Algorithm is ideological: From 'pos' location, cut out 'findStrLen' character // and 'findStr' to compare. If be identical to return, it is different and con- // -tinued to cut out 'findStrLen' character from next location compare with // 'findStr'. editTxtAra: The text area that has been sought. findStr: Find Str- // -ing. direction: Find direction. 
 public boolean find(JTextArea editTxtAra, String findStr,                      int direction, boolean checkCase){      if(findStr.equals("")) return(false);      pos=editTxtAra.getSelectionEnd();      int findStrLen=findStr.length();      int editTxtAraLen=editTxtAra.getText().length();      String temp="";      if(direction==-1) pos=editTxtAra.getSelectionStart()-1;      while(pos>=0&&pos<editTxtAraLen){          try{              temp=editTxtAra.getText(pos, findStrLen);              if(checkCase&&(temp.compareToIgnoreCase(findStr)==0)                  ||temp.equals(findStr)){                  editTxtAra.select(pos, pos+findStrLen);                  return(true);              }          }catch(Exception e){   }   pos+=direction;      }      return(false);  } 
// Why return a boolean value ? 
// KEY: Replace function /////////////////////////////////////////////////////// // Algorithm is ideological: If exist selected text, replace it. // editTxtAra: The text area that has been sought. // replaceStr: Use 'replaceStr' replace selection. 
 private void replace(JTextArea editTxtAra, String replaceStr){      if(editTxtAra.getSelectionStart()!=editTxtAra.getSelectionEnd())          editTxtAra.replaceSelection(replaceStr);  } 
// KEY: Replace function /////////////////////////////////////////////////////// // Algorithm is ideological: Circulate to seek replacement. // editTxtAra: The text area that has been sought. // findStr: Find String. // replaceStr: Use 'replaceStr' replace selection. 
// Why function find return a boolean value ? Are you see ? 
 private void replaceAll(JTextArea editTxtAra, String findStr,      String replaceStr, boolean checkCase){      if(findStr.equals("")) return;      int i;      editTxtAra.select(0, 0);      for(i=0; find(editTxtAra, findStr, +1, checkCase); ++i) // Are you see ?!          replace(editTxtAra, replaceStr);      JOptionPane.showMessageDialog(FindDlg.this,          "Replaced "+i+" occurence(s) in this file.",          "INFORMATION",          JOptionPane.INFORMATION_MESSAGE);  } 
// 别怪我的E文不正确,只怪现在的翻译软件都是直来直去的(注释没看懂?别急!翻译软件能看懂. 
// 按理说翻译软件是可以再直译回原文的...不成?!...那可就好玩儿了). 
我并不想解释我的算法中的每一句话到底是什么意思,因为那是属于算法与数据结构的范畴,非计算机专业的Java爱好者恐怕不会在意什么算法,而且这也离我的文章的主题远了点.不过我还是很希望能有人跟我讨论一下这个算法.(如果你是Java高手,你应该发现这里的替换方法与之前的撤消联起来有点毛病,我还不知道怎么解决) 
    每当查找完事以后,应该让JTextArea的对象选中一段文本表明已经找到.但是结果是不行!可以找到文本,但无法选中.我用一般的requestFocus()就是这个结果.后来我用的是比request生硬的多的grab, grabFocus(),这才解决了文本无法选中的问题. 
    一切查找工作都完事了(?),总觉得少了点什么东西.是什么呢?没有默认键(就是对话框刚一出现就被(也永远被)选中的那个按钮).我试了JButton类的setDefaultButton(boolean)的方法,可是这不是我期望的那种效果.不过我还是找到了解决方法: 
 JDialog dlg=new JDialog(...);  JButton  OK=new JButton("OK");  dlg.getContentPane().setLayout(new FlowLayout());  dlg.getContentPane().add(OK);  dlg.getRootPane().setDefaultButton(OK); // 默认键  dlg.setSize(480, 320);  dlg.show(); 
    搞定关键字高亮显示?要是搞定了我肯定会告诉各位的.那是个很难的课题. 
    要继续写下去吗? (下次是文件I/O)  
 
  |