该指南的发布为当前svn主干提供了功能。那些在以前版本中查找信息的人应该查看发布的文档。
HSSF允许从XLS文件中写入或读取数字、字符串、日期或公式单元格值。在这个版本中,还包括行和列的大小、单元样式(粗体、斜体、边框等),并支持内置和用户定义的数据格式。还有一个基于事件的API来读取XLS文件。它与读/写API有很大的不同,它是为需要更小内存占用的中级开发人员设计的。
写一个新文件#
高水平的API(包:org.apache.poi.ss.usermodel)是大多数人应该使用。使用非常简单。
org.apache.poi.ss.usermodel.Workbook的手册是由创建一个实例。直接创建一个具体的类(org.apache.poi.hssf.usermodel。HSSFWorkbook或org.apache.poi.xssf.usermodel.XSSFWorkbook),或使用方便的工厂类org.apache.poi.ss.usermodel.WorkbookFactory。
从工作簿的现有实例中调用createSheet()来创建表单,创建的表单会自动添加到工作簿的序列中。表单本身并没有表单名称(底部的选项卡);您可以通过调用Workbook.setSheetName(sheetindex,“SheetName”,编码)来设置与表单相关联的名称。对于HSSF,名称可能是8位格式(HSSFWorkbook.ENCODING_COMPRESSED_UNICODE)或Unicode(HSSFWorkbook.ENCODING_UTF_16)。HSSF的默认编码是每字符8比特。对于XSSF,名称将自动处理为unicode。
行是通过将createRow(rowNumber)从一个现有的表实例中创建出来的。只有具有单元格值的行才应该被添加到表中。要设置行的高度,只需在行对象上调用setRowHeight(height)。高度必须用两种方法来表示,即1 / 20。如果您喜欢,还有一个setRowHeightInPoints方法。
单元格是通过从现有的行调用createCell(列,类型)创建的。只有具有值的单元格应该添加到行中。单元格应该将其单元格设置为单元格。CELL_TYPE_NUMERIC或细胞。CELL_TYPE_STRING取决于它们是否包含数字或文本值。单元格还必须具有一个值集,通过使用字符串或double作为参数来调用setCellValue来设置值。单个细胞没有宽度;您必须在表单对象上调用setColumnWidth(colindex,width)(在字符的1 / 256中使用单位)。(你不能在GUI中单独使用它)。
单元格使用CellStyle对象进行样式化,而CellStyle对象又包含对字体对象的引用。这些是通过调用createCellStyle()和createFont()通过工作簿对象创建的。创建对象后,必须设置其参数(颜色、边框等)。为CellStyle调用setFont(fontobj)设置字体。
一旦生成了工作簿,就可以从工作簿的实例中调用write(outputStream)来编写它,将它传递给outputStream(例如,FileOutputStream或ServletOutputStream)。你必须自己关闭OutputStream。HSSF不会为你关闭它。
下面是一些示例代码(摘录和改编自org.apache.poi.hssf.dev.HSSF测试类):
行数:// 创建一个新的文件 FileOutputStream out = new FileOutputStream("workbook.xls"); // 创建一个新的工作簿 Workbook wb = new HSSFWorkbook(); // 创建一个新的表单 Sheet s = wb.createSheet(); // 声明行对象引用 Row r = null; //声明单元格引用 Cell c = null; // 创建3个不同类型的单元格 CellStyle cs = wb.createCellStyle(); CellStyle cs2 = wb.createCellStyle(); CellStyle cs3 = wb.createCellStyle(); DataFormat df = wb.createDataFormat(); // 创建两个字体的对象 Font f = wb.createFont(); Font f2 = wb.createFont(); //设置字体1到12点类型 f.setFontHeightInPoints((short) 12); //使用蓝色 f.setColor( (short)0xc ); //使用黑色 //arial是默认字体 f.setBoldweight(Font.BOLDWEIGHT_BOLD); //设置字体2到10点类型 f2.setFontHeightInPoints((short) 10); //设置红色 f2.setColor( (short)Font.COLOR_RED ); //设置黑色 f2.setBoldweight(Font.BOLDWEIGHT_BOLD); f2.setStrikeout( true ); //设置单元格风格 cs.setFont(f); //舍子单元格格式 cs.setDataFormat(df.getFormat("#,##0.0")); //设置边界 cs2.setBorderBottom(cs2.BORDER_THIN); //填充w fg填充颜色 cs2.setFillPattern((short) CellStyle.SOLID_FOREGROUND); //将单元格格式设置为文本,以查看完整列表的DataFormat cs2.setDataFormat(HSSFDataFormat.getBuiltinFormat("text")); // 设置字体 cs2.setFont(f2); // 在Unicode中设置表单名称 wb.setSheetName(0, "\u0422\u0435\u0441\u0442\u043E\u0432\u0430\u044F " + "\u0421\u0442\u0440\u0430\u043D\u0438\u0447\u043A\u0430" ); //如果是普通ascii // wb.setSheetName(0, "HSSF Test"); // create a sheet with 30 rows (0-29) int rownum; for (rownum = (short) 0; rownum < 30; rownum++) { // 创建一行 r = s.createRow(rownum); //每隔一行 if ((rownum % 2) == 0) { // make the row height bigger (in twips - 1/20 of a point) r.setHeight((short) 0x249); } //r.setRowNum(( short ) rownum); // create 10 cells (0-9) (the += 2 becomes apparent later for (short cellnum = (short) 0; cellnum < 10; cellnum += 2) { // create a numeric cell c = r.createCell(cellnum); // 演示一下 c.setCellValue(rownum * 10000 + cellnum + (((double) rownum / 1000) + ((double) cellnum / 10000))); String cellValue; // create a string cell (see why += 2 in the c = r.createCell((short) (cellnum + 1)); // on every other row if ((rownum % 2) == 0) { // set this cell to the first cell style we defined c.setCellStyle(cs); // set the cell's string value to "Test" c.setCellValue( "Test" ); } else { c.setCellStyle(cs2); // set the cell's string value to "\u0422\u0435\u0441\u0442" c.setCellValue( "\u0422\u0435\u0441\u0442" ); } // make this column a bit wider使这一列宽一些 s.setColumnWidth((short) (cellnum + 1), (short) ((50 * 8) / ((double) 1 / 20))); } } //draw a thick black border on the row at the bottom using BLANKS在底部的行上画一个厚实的黑色边框,使用空格 // advance 2 rows提前2行 rownum++; rownum++; r = s.createRow(rownum); // define the third style to be the default定义第三种样式为缺省值 // except with a thick black border at the bottom除了底部有一条粗黑的边 cs3.setBorderBottom(cs3.BORDER_THICK); //create 50 cells创建50个单元格 for (short cellnum = (short) 0; cellnum < 50; cellnum++) { //create a blank type cell (no value) c = r.createCell(cellnum); // set it to the thick black border style将其设置为粗黑的边框样式 c.setCellStyle(cs3); } //end draw thick black border最后画出厚实的黑边 // demonstrate adding/naming and deleting a sheet // create a sheet, set its title then delete it s = wb.createSheet(); wb.setSheetName(1, "DeletedSheet"); wb.removeSheetAt(1); //end deleted sheet // write the workbook to the output stream将工作簿写入输出流 // close our file (don't blow out our file handles关闭文件 wb.write(out); out.close();读取或修改现有文件
在文件中阅读同样简单。在一个文件中读取,创建一个新的org . apache.poifs。文件系统,在打开的InputStream中传递,比如XLS的FileInputStream,到构造函数。org.apache.poi.hssf.usermodel构造一个新实例。HSSFWorkbook将文件系统实例传递给构造函数。通过它们的评估方法(workbook.getSheet(sheetNum)、sheet.getRow(rownum)等),您可以访问所有的高级模型对象。
修改你读过的文件很简单。您通过一个评估器方法来检索对象,通过父对象的remove方法(sheet.removeRow(hssfrow))删除该对象,并创建与创建新xls时一样的对象。当您完成修改单元格时,只需调用workbook.write(outputstream),就像上面做的那样。
一个例子中可以看到org.apache.poi.hssf.usermodel.examples.HSSFReadWrite。
事件API比用户API更新。它是为那些愿意学习少量底层API结构的中级开发人员设计的。它使用起来相对简单,但需要对Excel文件的部分(或愿意学习)有一个基本的了解。所提供的优势是,您可以读取具有相对较小内存占用的XLS。
基本事件API需要注意的一件重要事情是,它只触发事件,而这些事件实际上存储在文件中。对于XLS文件格式,对于那些还没有被编辑的东西,在文件中根本不存在是很常见的。这意味着在记录流中很可能存在明显的“空白”,您可能需要在此工作,或者使用事件API的记录感知扩展。
你使用这个API构建org.apache.poi.hssf.eventmodel.HSSFRequest的实例。一个类创建支持org.apache.poi.hssf.eventmodel登记。HSSFListener接口使用hss频率。addListener(yourlistener recordsid)。recordsid应该是静态参考号码(如BOFRecord.sid)包含在org.apache.poi.hssf.record类。诀窍是你必须知道这些记录是什么。或者你可以叫HSSFRequest.addListenerForAllRecords(mylistener)。为了了解这些记录,您可以在org . apache.poi.hssf中读取所有的javadoc。记录包或你可以破解org.apache.poi.hssf.dev.EFHSSF副本和适应您的需要。TODO:更好的记录文档。
一旦你注册你的听众在HSSFRequest对象可以构造org.apache.poi.poifs.filesystem的实例。文件系统(参见POIFS howto)并将其传递给您的XLS文件inputstream。您可以通过HSSFEventFactory向HSSFEventFactory的实例传递这个请求,以及您构造的请求。processWorkbookEvents(请求、文件系统)方法,或者你可以得到一个实例的DocumentInputStream Filesystem.createDocumentInputStream(“工作手册”)并将其传递给HSSFEventFactory。processEvents(请求,inputStream)。一旦您进行了这个调用,您所构造的侦听器就会接收到它们的processRecord(记录)方法,并在每个记录中记录它们,直到文件完全被读取为止。
public class EventExample implements HSSFListener { private SSTRecord sstrec; /** * This method listens for incoming records and handles them as required. * @param record The record that was found while reading. */ public void processRecord(Record record) { switch (record.getSid()) { // the BOFRecord can represent either the beginning of a sheet or the workbook case BOFRecord.sid: BOFRecord bof = (BOFRecord) record; if (bof.getType() == bof.TYPE_WORKBOOK) { System.out.println("Encountered workbook"); // assigned to the class level member } else if (bof.getType() == bof.TYPE_WORKSHEET) { System.out.println("Encountered sheet reference"); } break; case BoundSheetRecord.sid: BoundSheetRecord bsr = (BoundSheetRecord) record; System.out.println("New sheet named: " + bsr.getSheetname()); break; case RowRecord.sid: RowRecord rowrec = (RowRecord) record; System.out.println("Row found, first column at " + rowrec.getFirstCol() + " last column at " + rowrec.getLastCol()); break; case NumberRecord.sid: NumberRecord numrec = (NumberRecord) record; System.out.println("Cell found with value " + numrec.getValue() + " at row " + numrec.getRow() + " and column " + numrec.getColumn()); break; // SSTRecords store a array of unique strings used in Excel. case SSTRecord.sid: sstrec = (SSTRecord) record; for (int k = 0; k < sstrec.getNumUniqueStrings(); k++) { System.out.println("String table value " + k + " = " + sstrec.getString(k)); } break; case LabelSSTRecord.sid: LabelSSTRecord lrec = (LabelSSTRecord) record; System.out.println("String cell found with value " + sstrec.getString(lrec.getSSTIndex())); break; } } /** * Read an excel file and spit out what we find. * * @param args Expect one argument that is the file to read. * @throws IOException When there is an error processing the file. */ public static void main(String[] args) throws IOException { // create a new file input stream with the input file specified // at the command line FileInputStream fin = new FileInputStream(args[0]); // create a new org.apache.poi.poifs.filesystem.Filesystem POIFSFileSystem poifs = new POIFSFileSystem(fin); // get the Workbook (excel part) stream in a InputStream InputStream din = poifs.createDocumentInputStream("Workbook"); // construct out HSSFRequest object HSSFRequest req = new HSSFRequest(); // lazy listen for ALL records with the listener shown above req.addListenerForAllRecords(new EventExample()); // create our event factory HSSFEventFactory factory = new HSSFEventFactory(); // process our events based on the document input stream factory.processEvents(req, din); // once all the events are processed close our file input stream fin.close(); // and our document input stream (don't want to leak these!) din.close(); System.out.println("done."); } }这是对正常事件API的扩展。有了这个,您的监听器就会被调用额外的假记录。这些虚拟记录应该提醒您记录文件中不存在的记录(如尚未编辑的单元格),并允许您处理这些记录。
你的HSSFListener将被调用三个虚拟记录:
org.apache.poi.hssf.eventusermodel.dummyrecord.MissingRowDummyRecord
这在行记录阶段(通常发生在单元格记录之前)调用,并指出给定行的行记录不在文件中。
org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord
这在细胞记录阶段被调用。当遇到一个单元记录时,它会被调用,这将在它与前一个记录之间留下一个空白。你可以在真正的细胞记录之前得到这些。
org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord
这是在给定行的最后一个单元格之后调用的。它表示没有更多的单元格,也告诉你有多少个单元格。对于没有单元格的行,这将是您获得的唯一记录。
要使用记录清楚事件API,您应该创建一个org.apache.poi.hssf.eventusermodel。你HSSFListener MissingRecordAwareHSSFListener,通过它。然后,注册MissingRecordAwareHSSFListener事件模型,正常开始。
这个API的一个例子是编写一个CSV输出器,它总是输出最少的列数,即使文件不包含一些行或单元格。它可以发现在/ src / / src /org/apache/poi/hssf/eventusermodel/examples/XLS2CSVmra例子。java,可以在命令行中调用,也可以在自己的代码中调用。最新版本总是可以从subversion中获得。
在版本3.0.3之前的POI版本中,这段代码一直存在于scratchpad部分。如果您使用的是这些旧版本的POI,您将需要在类路径中包含scratchpad jar,或者从subversion签出中构建。
如果内存占用是一个问题,那么对于XSSF,您可以获取底层XML数据,并自己处理它。这是为那些愿意学习低层次结构的中级开发人员设计的。xlsx文件,以及在java中处理XML的人。它使用起来相对简单,但需要对文件结构有基本的了解。所提供的优势是您可以读取一个内存占用相对较小的XLSX文件。
基本事件API需要注意的一件重要事情是,它只触发事件,而这些事件实际上存储在文件中。对于XLSX文件格式,在文件中根本不存在的东西是很常见的。这意味着在记录流中很可能存在明显的“空白”,您需要对此进行处理。
你使用这个API构建org.apache.poi.xssf.eventmodel.XSSFReader的实例。这将可选地在共享的字符串表和样式上提供一个漂亮的接口。它提供了从文件的其余部分获取原始xml数据的方法,然后将这些数据传递给SAX。
这个例子展示了如何获取一个已知的表单,或者在文件中的所有页。它是基于中的示例svn src / / src /org/apache/poi/xssf/eventusermodel/examples/FromHowTo.java例子
import java.io.InputStream; import java.util.Iterator; import org.apache.poi.xssf.eventusermodel.XSSFReader; import org.apache.poi.xssf.model.SharedStringsTable; import org.apache.poi.openxml4j.opc.OPCPackage; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.XMLReaderFactory; public class ExampleEventUserModel { public void processOneSheet(String filename) throws Exception { OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader( pkg ); SharedStringsTable sst = r.getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); // To look up the Sheet Name / Sheet Order / rID, // you need to process the core Workbook stream. // Normally it's of the form rId# or rSheet# InputStream sheet2 = r.getSheet("rId2"); InputSource sheetSource = new InputSource(sheet2); parser.parse(sheetSource); sheet2.close(); } public void processAllSheets(String filename) throws Exception { OPCPackage pkg = OPCPackage.open(filename); XSSFReader r = new XSSFReader( pkg ); SharedStringsTable sst = r.getSharedStringsTable(); XMLReader parser = fetchSheetParser(sst); Iterator<InputStream> sheets = r.getSheetsData(); while(sheets.hasNext()) { System.out.println("Processing new sheet:\n"); InputStream sheet = sheets.next(); InputSource sheetSource = new InputSource(sheet); parser.parse(sheetSource); sheet.close(); System.out.println(""); } } public XMLReader fetchSheetParser(SharedStringsTable sst) throws SAXException { XMLReader parser = XMLReaderFactory.createXMLReader( "org.apache.xerces.parsers.SAXParser" ); ContentHandler handler = new SheetHandler(sst); parser.setContentHandler(handler); return parser; } /** * See org.xml.sax.helpers.DefaultHandler javadocs */ private static class SheetHandler extends DefaultHandler { private SharedStringsTable sst; private String lastContents; private boolean nextIsString; private SheetHandler(SharedStringsTable sst) { this.sst = sst; } public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // c => cell if(name.equals("c")) { // Print the cell reference System.out.print(attributes.getValue("r") + " - "); // Figure out if the value is an index in the SST String cellType = attributes.getValue("t"); if(cellType != null && cellType.equals("s")) { nextIsString = true; } else { nextIsString = false; } } // Clear contents cache lastContents = ""; } public void endElement(String uri, String localName, String name) throws SAXException { // Process the last contents as required. // Do now, as characters() may be called more than once if(nextIsString) { int idx = Integer.parseInt(lastContents); lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString(); nextIsString = false; } // v => contents of a cell // Output after we've seen the string contents if(name.equals("v")) { System.out.println(lastContents); } } public void characters(char[] ch, int start, int length) throws SAXException { lastContents += new String(ch, start, length); } } public static void main(String[] args) throws Exception { ExampleEventUserModel example = new ExampleEventUserModel(); example.processOneSheet(args[0]); example.processAllSheets(args[0]); } }SXSSF(包:org.apache.poi.xssf.streaming)是一种API-compatible流扩展的XSSF时使用非常大的电子表格制作,和堆空间是有限的。SXSSF通过限制进入滑动窗口内的行来实现其低内存占用,而XSSF可以访问文档中的所有行。在窗口中不再出现的旧行变得不可访问,因为它们被写到磁盘上。
您可以指定在工作簿窗口大小建设时间通过新的SXSSFWorkbook(int windowSize)或通过SXSSFSheet #你可以设置每张setRandomAccessWindowSize(int windowSize)
当一个新行通过createRow()创建时,未刷新记录的总数将超过指定的窗口大小,那么具有最低索引值的行将被刷新,并且不能通过getRow()访问。
默认窗口大小为100,由SXSSFWorkbook.DEFAULT_WINDOW_SIZE定义。
窗口大小为- 1表示访问不受限制。在这种情况下,所有未被调用flushRows()的记录都可以随机访问。
请注意,通过调用dispose方法,SXSSF会分配您必须经常清理的临时文件。
SXSSFWorkbook默认使用内联字符串而不是共享的字符串表。这是非常有效的,因为没有文档内容需要保存在内存中,但是也可以生成与一些客户机不兼容的文档。在共享的字符串中,文档中的所有唯一字符串都必须保存在内存中。根据文档内容,可以使用比使用共享字符串禁用的更多的资源。
请注意,仍然有一些东西仍然可能消耗大量的内存,这是基于您正在使用的特性,例如合并区域、超链接、注释、……仍然只存储在内存中,因此可能需要大量的内存。
在决定是否启用共享字符串之前,请仔细检查内存预算和兼容性需求。
下面的示例使用100行窗口编写了一个表。当行数达到101时,与rownum = 0的行被刷新到磁盘并从内存中删除,当rownum达到102时,与rownum = 1的行被刷新等。
import junit.framework.Assert; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.streaming.SXSSFWorkbook; public static void main(String[] args) throws Throwable { SXSSFWorkbook wb = new SXSSFWorkbook(100); // keep 100 rows in memory, exceeding rows will be flushed to disk Sheet sh = wb.createSheet(); for(int rownum = 0; rownum < 1000; rownum++){ Row row = sh.createRow(rownum); for(int cellnum = 0; cellnum < 10; cellnum++){ Cell cell = row.createCell(cellnum); String address = new CellReference(cell).formatAsString(); cell.setCellValue(address); } } // Rows with rownum < 900 are flushed and not accessible for(int rownum = 0; rownum < 900; rownum++){ Assert.assertNull(sh.getRow(rownum)); } // ther last 100 rows are still in memory for(int rownum = 900; rownum < 1000; rownum++){ Assert.assertNotNull(sh.getRow(rownum)); } FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx"); wb.write(out); out.close(); // dispose of temporary files backing this workbook on disk wb.dispose(); }The next example turns off auto-flushing (windowSize=-1) and the code manually controls how portions of data are written to disk
import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.streaming.SXSSFWorkbook; public static void main(String[] args) throws Throwable { SXSSFWorkbook wb = new SXSSFWorkbook(-1); // turn off auto-flushing and accumulate all rows in memory Sheet sh = wb.createSheet(); for(int rownum = 0; rownum < 1000; rownum++){ Row row = sh.createRow(rownum); for(int cellnum = 0; cellnum < 10; cellnum++){ Cell cell = row.createCell(cellnum); String address = new CellReference(cell).formatAsString(); cell.setCellValue(address); } // manually control how rows are flushed to disk if(rownum % 100 == 0) { ((SXSSFSheet)sh).flushRows(100); // retain 100 last rows and flush all others // ((SXSSFSheet)sh).flushRows() is a shortcut for ((SXSSFSheet)sh).flushRows(0), // this method flushes all rows } } FileOutputStream out = new FileOutputStream("/temp/sxssf.xlsx"); wb.write(out); out.close(); // dispose of temporary files backing this workbook on disk wb.dispose(); } SXSSF在临时文件(每个表的临时文件)中刷新表数据,这些临时文件的大小可以增长到非常大的值。例如,对于20 MB的csv数据,temp xml的大小超过了1gb。如果temp文件的大小是一个问题,您可以告诉SXSSF使用gzip压缩:SXSSFWorkbook wb = new SXSSFWorkbook(); wb.setCompressTempFiles(true); // temp files will be gzipped如果您希望从某些XML生成一个XLS文件,那么可以编写自己的XML处理代码,然后使用用户API来编写文档。
另一种选择是使用蚕茧。在Cocoon中,有一个HSSF序列化器,它以XML(在gnumeric格式)中获取,并为您输出一个XLS文件。
HSSF有许多工具可以帮助开发人员使用HSSF调试/开发一些东西(更多的是XLS文件)。我们已经讨论了测试HSSF读/写/修改功能的应用程序;现在我们要讲一下BiffViewer。在HSSF的早期开发过程中,我们决定知道在记录中什么是错误的,什么是错误的,等等在可用的工具中几乎是不可能的。所以我们开发了BiffViewer。你可以找到在org.apache.poi.hssf.dev.BiffViewer。它有两个基本的函数和一个导数。
第一个是“biffview”。为此,您运行它(假设您的类路径中有所有的设置,并且您知道您所做的事情足以让您考虑这一点),并将xls文件作为参数。它将提供所有已知记录的列表,其中包含它们的数据和一个没有数据的未被理解的记录列表(因为它不知道如何解释它们)。这个清单对一些事情很有用。首先,您可以查看这些值,并了解准英语中哪些是错误的。第二,您可以将输出发送到一个文件并进行比较。
第二个函数是“大的freakin转储文件”,只需传递一个文件和一个匹配“bfd”的第二个参数。这将会是文件的一个大的hexdump。
最后,还有“混合”模式,它与常规的biffview一样,只包括某些记录的十六进制转储。要使用它,只需传递一个文件,而第二个参数与“on”完全匹配。
在下一个发布周期中,我们还会有一个称为公式查看器的东西。这个类已经在那里了,但是还不是很有用。当它做某件事时,我们会记录它。