加载和显示大型文本文件

时间:2014-08-27 12:09:43

标签: java swing text latency

在Swing应用程序中,我有时需要支持对加载缓慢的大型面向行的文本文件的只读访问:日志,转储,跟踪等。对于少量数据,{{3} } DocumentJTextComponent很好,如suitable所示。我理解浏览大量数据的人为限制,但有问题的东西似乎总是在最大的文件中。对于10-100兆字节,百万行范围内的大量文本,是否有任何实用的替代方案?

3 个答案:

答案 0 :(得分:23)

由于尺寸原因,您肯定希望在后台加载文件以避免阻止event dispatch thread; SwingWorker是一种常见的选择。请考虑更新Document并在TableModel的行中显示文本行,而不是使用JTable。这提供了几个优点:

  • 结果将立即开始显示,并且感知延迟会减少。

  • JTable使用rendering的flyweight模式,可以很好地扩展到数兆字节的百万行范围。

  • 您可以在读取输入时解析输入以创建任意列结构。

  • 您可以利用JTable的{​​{3}}功能,sorting and filtering

  • 您可以使用example专注于一行。

附录:为方便起见,下面的示例使用DefaultTableModel。要缩小TablePopupEditor,请展开AbstractTableModel并管理List<String>List<RowData>,如图overhead所示。该示例显示了不确定的进度;显示中间进度的更改显示为here

代码:

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

/**
 * @see https://stackoverflow.com/a/25526869/230513
 */
public class DisplayLog {

    private static final String NAME = "/var/log/install.log";

    private static class LogWorker extends SwingWorker<TableModel, String> {

        private final File file;
        private final DefaultTableModel model;

        private LogWorker(File file, DefaultTableModel model) {
            this.file = file;
            this.model = model;
            model.setColumnIdentifiers(new Object[]{file.getAbsolutePath()});
        }

        @Override
        protected TableModel doInBackground() throws Exception {
            BufferedReader br = new BufferedReader(new FileReader(file));
            String s;
            while ((s = br.readLine()) != null) {
                publish(s);
            }
            return model;
        }

        @Override
        protected void process(List<String> chunks) {
            for (String s : chunks) {
                model.addRow(new Object[]{s});
            }
        }
    }

    private void display() {
        JFrame f = new JFrame("DisplayLog");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        DefaultTableModel model = new DefaultTableModel();
        JTable table = new JTable(model);
        JProgressBar jpb = new JProgressBar();
        f.add(jpb, BorderLayout.NORTH);
        f.add(new JScrollPane(table));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
        LogWorker lw = new LogWorker(new File(NAME), model);
        lw.addPropertyChangeListener((PropertyChangeEvent e) -> {
            SwingWorker.StateValue s = (SwingWorker.StateValue) e.getNewValue();
            jpb.setIndeterminate(s.equals(SwingWorker.StateValue.STARTED));
        });
        lw.execute();
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            new DisplayLog().display();
        });
    }
}

答案 1 :(得分:6)

我会将问题分开。

第一个是模型 - 文档构建速度

第二个是Document rendering - 构建视图树来表示Document。

问题是你是否需要像关键字一样的字体效果?

我将从文档构建部分开始。通过EditorKit.read()读取文件的IMHO即使对于大文件也应该快速。我会使用PainDocument来检查纯模型是否构建得足够快,适合您的应用程序。如果是的话,只需使用Document作为模型。如果没有实现自己的Document接口,因为AbstractDocument有很多方法可以进行更新处理(例如writeLock)。

当我们足够快地加载文档时,我们必须解决文档渲染问题。默认情况下,javax.swing.text中使用的视图非常灵活。它们被设计为要扩展的基类 - 因此有很多我们不需要的代码。例如。测量。

对于我将使用Monospaced字体的功能,我们不需要换行,因此视图的测量值很快=最长行字符数* char widht。

高度也是字符高度*行数。

所以我们的PLainTextViewReplacement非常快。此外,我们不必渲染整个视图,只需在滚动窗格中显示一个片段。因此,渲染速度可能会快得多。

当然,应该有很多工作来提供正确的插入符导航,选择等。

答案 2 :(得分:0)

当我在努力解决类似的用例时,我实现了一个简单的分页解决方案。它远非完美,但也许有人发现它有用。

与jtextarea结合使用可以正常工作,但与JEditorPane结合使用时,性能会很糟糕。

如果有人想提供更好的解决方案,我想知道。

package net.ifao.tools.arcticrequester.gui.panel;


import java.awt.Adjustable;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.StringReader;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.JTextComponent;


/**
 * A class that manages the visibility of file content visualized with a textarea within a scrollbar.
 * Approx. 2050 lines from the file are visible at a time. Data is loaded from a file and
 * displayed while the user is scrolling. The chunks are loaded dynamically.
 *
 * @author dostricki
 *
 */
public class VisibilityManager
   implements AdjustmentListener
{

   private int lastLoadedLineFrom;
   private int lastLoadedLineTo;
   private int numberOfLines = 0;
   private File file;

   private boolean enabled = false;
   private boolean showLines = false;

   // load 1000 lines before the first visible line
   // and 1000 lines after the last vissible line
   private static final int LOAD_LINES_BEFORE_AND_AFTER_VIEWPORT = 1000;

   // margin until when no load is triggered.
   // moving the viewport more then 900 lines up or down should trigger a reload
   private static final int VIEWPORT_LINES_MOVE_THRASHOLD = 900;


   private JScrollPane scrollPane;
   private JTextComponent textComponent;

   private final BlockingQueue<Adjustable> queue;

   public VisibilityManager(JScrollPane scrollPane, JTextComponent textArea)
   {
      this.scrollPane = scrollPane;
      this.textComponent = textArea;
      queue = new LinkedBlockingDeque<>();
      startConsumer();

      scrollPane.getVerticalScrollBar().addAdjustmentListener(this);
   }

   private void startConsumer()
   {
      Thread scrollEventConsumer = new Thread()
      {

         @Override
         public void run()
         {
            while (true) {
               try {

                  // if multiple events occured just process one
                  queue.take();
                  if (!queue.isEmpty()) {
                     List<Adjustable> events = new ArrayList<>();
                     queue.drainTo(events);
                     //System.out.println("Handling scroll event. " + events.size() + " queued events dropped");
                  }

                  doHandleScrollEvent();

               }
               catch (InterruptedException e) {
                  e.printStackTrace();
               }
            }
         }
      };
      scrollEventConsumer.start();
   }

   public void setFile(File file)
   {
      this.file = file;

      try {
         this.numberOfLines = countNumberOfLines(file);
      }
      catch (IOException e1) {
         e1.printStackTrace();
      }
      int showLineMax = Math.min(getNumberOfLines(), 100);

      // show the first chunk immediately
      showLinesBuffererdReader(1, showLineMax, 0);

      this.enabled = true;
   }

   /**
    * precalculates the number of lines in the document - necessary
    * to replace the correct amount of preceeding and following
    * lines with EOL's so that the height of the scrollpane does never change.
    *
    * @param file
    * @return
    * @throws IOException
    */
   private int countNumberOfLines(File file)
      throws IOException
   {

      int numberOfLines = 0;

      //@formatter:off
      try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file),StandardCharsets.UTF_8));) {
         while (reader.ready()) {
            reader.readLine();
            ++numberOfLines;
         }
      }
      //@formatter:on

      return numberOfLines;
   }


   /****************************************
    *                Getter
    ****************************************/


   public int getNumberOfLines()
   {
      return numberOfLines;
   }

   public int getNumberOfLinesBuffer()
   {
      return LOAD_LINES_BEFORE_AND_AFTER_VIEWPORT;
   }

   public boolean isEnabled()
   {
      return enabled;
   }

   /****************************************
    *              Setter
    ****************************************/


   public void setLastLoadedLines(int lineFrom, int lineTo)
   {
      this.lastLoadedLineFrom = lineFrom;
      this.lastLoadedLineTo = lineTo;
   }

   public void setEnabled(boolean enabled)
   {
      this.enabled = enabled;
   }

   public void setShowLines(boolean showLines)
   {
      this.showLines = showLines;
   }


   /****************************************
    *           Calculation
    ****************************************/


   private boolean needsUpdate(int fromLine, int toLine)
   {
      boolean isBefore = fromLine < (this.lastLoadedLineFrom - VIEWPORT_LINES_MOVE_THRASHOLD);
      boolean isAfter = toLine > (this.lastLoadedLineTo + VIEWPORT_LINES_MOVE_THRASHOLD);

      if (isBefore || isAfter) {
         return true;
      } else {
         return false;
      }
   }

   private void showLinesBuffererdReader(int from, int to, int firstLineVisible)
   {
      //load also the buffer lines before
      from = from - getNumberOfLinesBuffer();
      //make sure it's valid
      from = Math.max(1, from);

      // load also the buffer lines after
      to = to + getNumberOfLinesBuffer();
      //make sure it's valid
      to = Math.min(getNumberOfLines(), to);

      FileChannel fileChannel = null;
      try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {

         StringBuffer content = new StringBuffer();
         int newCaretPosition = 0;

         // fill leading empty lines
         for (long i = 1; i < from; ++i) {
            if (i == firstLineVisible) {
               newCaretPosition = content.length() + 1;
            }
            if (showLines) {
               content.append(i).append(": ");
            }
            content.append('\n');
         }

         // read/write lines with content
         int j = 0;
         while (reader.ready() && j <= to) {
            ++j;
            String line = reader.readLine();
            if (j >= from && j <= to) {
               if (j == firstLineVisible) {
                  newCaretPosition = content.length() + 1;
               }
               if (showLines) {
                  content.append(j).append(": ");
               }
               content.append(line).append('\n');
            }
         }

         // fill trailing empty lines
         for (int i = to + 1; i <= getNumberOfLines(); ++i) {
            if (i == firstLineVisible) {
               newCaretPosition = content.length() + 1;
            }
            if (showLines) {
               content.append(i).append(": ");
            }
            content.append('\n');
         }

         updateTextInUI(content);

         // workaround for page up/down - it changes the caret position
         // so we are re-setting it to the first visible line
         // scrolling by scrollbars does not change the caret
         //textComponent.setCaretPosition(newCaretPosition);
      }
      catch (IOException e) {
         e.printStackTrace();
      }
      finally {

         try {
            if (fileChannel != null) {
               fileChannel.close();
            }
         }
         catch (IOException e) {
            e.printStackTrace();
         }
      }
   }

   /**
    * @param content
    * @throws IOException
    */
   private void updateTextInUI(StringBuffer content)
      throws IOException
   {
      if (textComponent instanceof JEditorPane) {
         JEditorPane edit = ((JEditorPane) textComponent);
         EditorKit editorKit = edit.getEditorKit();
         Document createDefaultDocument = editorKit.createDefaultDocument();
         createDefaultDocument.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
         try {
            editorKit.read(new StringReader(content.toString()), createDefaultDocument, 0);
         }
         catch (Exception e) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(out));
            edit.setText(new String(out.toByteArray()));
         }
         edit.setDocument(createDefaultDocument);
      } else {
         textComponent.setText(content.toString());
      }
   }


   /****************************************
    *           Eventing
    ****************************************/


   /**
    * fired when scrolling happens in any of the cases and ways.
    * Events are cached through a queue so that simultanious events
    * don't trigger unnecessary update actions
    * @see java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event.AdjustmentEvent)
    */
   @Override
   public void adjustmentValueChanged(AdjustmentEvent evt)
   {
      Adjustable source = evt.getAdjustable();
      if (evt.getValueIsAdjusting()) {
         return;
      }

      if (source != null) {
         try {
            queue.put(source);
         }
         catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }


   private void doHandleScrollEvent()
   {
      // determine which lines to request to be loaded into the
      int height = this.scrollPane.getVerticalScrollBar().getMaximum();
      int lines = getNumberOfLines();
      if (lines == 0) {
         return;
      }
      float heightPerLine = height / lines;
      int visibleLines = Math.round(this.scrollPane.getVerticalScrollBar().getVisibleAmount() / heightPerLine);
      int firstLineVisible = (int) Math.ceil(this.scrollPane.getVerticalScrollBar().getValue() / heightPerLine);

      int fromLine = Math.max(firstLineVisible, 1);
      if (fromLine > lines) {
         fromLine = lines;
      }

      int toLine = Math.min(firstLineVisible + visibleLines, lines);
      if (needsUpdate(fromLine, toLine)) {
         if (enabled) {
            setLastLoadedLines(fromLine, toLine);
            showLinesBuffererdReader(fromLine, toLine, firstLineVisible);
         }
      }
   }
}

用法:

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;
import javax.swing.text.DefaultCaret;

import net.ifao.tools.arcticrequester.gui.panel.VisibilityManager;


public class TestFrame
   extends JFrame
   implements MouseListener
{

   private VisibilityManager visibilityManager;


   public static void main(String[] args)
   {
      EventQueue.invokeLater(new Runnable()
      {
         @Override
         public void run()
         {
            try {
               TestFrame frame = new TestFrame();
               frame.setVisible(true);
            }
            catch (Exception e) {
               e.printStackTrace();
            }
         }
      });
   }

   /**
    * Create the frame.
    */
   public TestFrame()
   {
      try {
         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
      }
      catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e2) {
         // TODO Auto-generated catch block
         e2.printStackTrace();
      }

      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      setBounds(100, 100, 650, 500);

      JPanel contentPane = new JPanel();
      contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
      setContentPane(contentPane);
      contentPane.setLayout(new BorderLayout(0, 0));

      JTextArea textArea = new JTextArea();
      textArea.setEditable(false);
      textArea.addMouseListener(this);
      textArea.setAutoscrolls(false);

      textArea.setCaretPosition(0);
      DefaultCaret caret = (DefaultCaret) textArea.getCaret();
      caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);

      JScrollPane scrollPane = new JScrollPane(textArea);

      contentPane.add(scrollPane);

      visibilityManager = new VisibilityManager(scrollPane, textArea);

      visibilityManager.setShowLines(true);
      File file = new File("C:/iFAO/workspaces/polaris2/git/requester/ArcticRequester/src/test/java/responseview_20200603.tmp");
      visibilityManager.setFile(file);


      this.dispose();
   }


   /**
    * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
    */
   @Override
   public void mouseClicked(MouseEvent e)
   {
      boolean doScroll = !visibilityManager.isEnabled();
      this.visibilityManager.setEnabled(doScroll);
      System.out.println("scrolling set to " + doScroll);
   }

   /**
    * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
    */
   @Override
   public void mousePressed(MouseEvent e)
   {
      // TODO Auto-generated method stub

   }

   /**
    * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
    */
   @Override
   public void mouseReleased(MouseEvent e)
   {
      // TODO Auto-generated method stub

   }

   /**
    * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
    */
   @Override
   public void mouseEntered(MouseEvent e)
   {
      // TODO Auto-generated method stub

   }

   /**
    * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
    */
   @Override
   public void mouseExited(MouseEvent e)
   {
      // TODO Auto-generated method stub

   }

}