3

I submit the following implemented component in the spirit of giving back. The component satisfies my client's requirements, so I am not begging for help per se. But the component provides interesting functionality that might be useful to others.

Also, it raises some questions about apparently necessary hacks, and it's possible that my implementation is horrendously more complicated than necessary. Any suggested alternative solutions could be useful to others as well.

In a nutshell, a large table of data is split into multiple JTables, one in each page a multitabbed JTabPane.

If you run the program and resize it in various ways, you'll see how the component is supposed to behave. Explicitly, the requirements are:

  1. A tabular dataset with a varying number of records (1 to 500) is to be shown.

  2. A JTabbedPane contains one or more tabs, each containing only a JScrollPane.

  3. JScrollPanes can't have vertical scrollbars, may have horizontal scrollbars.

  4. Each JScrollPane contains a JTable.

  5. There are always just enough tabs in the JTabbedPane to collectively contain all the data rows in their JTables.

  6. The JTabbedPane size varies with the application JFrame size.

  7. The application may be freely resized by dragging with the mouse.

  8. At initialization, and whenever the application is resized, the JTabbedPane is rebuilt with just enough tab pages to hold all the records. E.g. for 100 records, if at a particular panel size a tab's table can hold 8 records, then 13 tabs are created.

  9. If the the JTabbedPane's size is reduced so as to be too small to hold the number of tabs needed to hold all the data rows, nothing (or just a warning) is shown.

A person with an academic interest in Java, MigLayout, and their interaction might want to consider four points:

  1. At a certain point in the source code is the following function call:

    tb = tabbedPane.getBoundsAt(0);

It is a hack. I cannot see what earthly need there is for this call; yet it (or something) is necessary.

  1. Theoretically, ScrollPane.getViewportBorderBounds() should give me the information to compute a tab page's table size, but I instead have to hack a value. Am I wrong, or is it returning incorrect information where I am using it?

  2. There is a bewildering set of functions - paint(), repaint(), validate(), invalidate(), revalidate(), update(). I have found that particular functions need to be called at just the right time. Order of calls is often, though not always obviously, very important. This set of functions could really use rigorous but clear documentation of their interactions with AWT, Swing, and each other. Their interaction with layout managers in general and MigLayout in particular could also use explanation.

  3. is there a solution to the requirements using generic Java that is a lot simpler than the approach I used? Did I reinvent the wheel just to end up with a tractor tread?

make: javac -classpath ScrollTableTest.java

usage: java -classpath ScrollTableTest [total data rows]

import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.util.*;
import java.io.*;
import java.text.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.filechooser.*;
import javax.swing.table.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableColumn;
import net.miginfocom.swing.MigLayout;

public class ScrollTableTest
  extends    JFrame
{
  public final static int APPWIDTH = 500; 
  public final static int APPHEIGHT = 300;
  public final static String[] CLIENT_COL_NAMES = { "Col 1", "Col 2", "Col 3", "Col 4" };
  public final static int COLS = CLIENT_COL_NAMES.length; 
  public final static int MAXTABS = 50; // arbitrary limit
  public final static int arbitraryTweek1 = 20;    

  String migDebugString = "";

  int[] dataRowsPerTabCount = new int [MAXTABS];
  JPanel topPane = null;
  DefaultTableModel clientsTableModel;
  String[][] clientData;
  JScrollPane scrollPane;
  Rectangle viewportBounds;
  JTable clientsTable;
  JTabbedPane tabbedPane;
  int dataRows, maxVisibleRow = -1;
  int rowsToShow = 1;
  int dataRowHeight;

  void printBasics()
  {
    if (scrollPane == null)
      return;
    System.out.println("");
    System.out.println("clientsTable height " + clientsTable.getHeight());
    System.out.println("topPane height:  " + topPane.getHeight());
    System.out.println("tabbedPane height " + tabbedPane.getHeight());
    System.out.println("scrollPane height:  " + scrollPane.getHeight());
    System.out.println("viewport bounds:  y " + viewportBounds.getY() +
          " height " + (int)viewportBounds.getHeight());
  }
  void printDims()
  {
    printBasics();
    double diff = viewportBounds.getHeight() - clientsTable.getHeight();
    System.out.println("dataRowHeight: " + dataRowHeight);
    System.out.println("differential:  " + diff);
  }
  void getGuiMetrics()
  {
    double diff;
    Rectangle tb;
    int clientRows = 20;
    int viewable = 0;
    int bottom;
    int computedSpHeight;
    int tabIx;
    boolean scrollbarHeightSet = false;
    int scrollbarHeight = 0;
    String title;
    topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
    setContentPane(topPane);
    validate();

    tabbedPane = new JTabbedPane();
    topPane.add(tabbedPane, "cell 0 0, grow");

    // create a temporary table of nominal size to use for table metrics
    clientData = new String[clientRows][COLS];
    clientsTableModel = new DefaultTableModel(clientData, CLIENT_COL_NAMES);
    clientsTable = new JTable(clientRows, COLS);
    clientsTable.setModel(clientsTableModel);
    clientsTable.setPreferredScrollableViewportSize(null);
    clientsTable.getTableHeader().setReorderingAllowed(false);
    clientsTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    clientsTable.getSelectionModel().setSelectionInterval(0, 0);

    // created scroll pane containing table, and contained in tabbed pane
    scrollPane = new JScrollPane(clientsTable);
    scrollPane.setVerticalScrollBarPolicy(scrollPane.VERTICAL_SCROLLBAR_NEVER);
    scrollPane.setHorizontalScrollBarPolicy(scrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    tabbedPane.setMaximumSize(new Dimension(topPane.getWidth(),  topPane.getHeight() - arbitraryTweek1));

    // For the entire allowed range of tabbed pages, calculate the area
    // within the tabbed pane available to hold a table.
    for (tabIx = 0; tabIx < MAXTABS; ++tabIx)
    {
      JPanel panel = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
      title = "Page " + (tabIx +1);
      tabbedPane.addTab(title, panel);
      panel.add(scrollPane, "cell 0 0, grow");
      if (tabIx == 0)
      {
        validate();
        dataRowHeight = clientsTable.getHeight() / clientRows;
      }
      else
        tabbedPane.revalidate();

      // we need to know how high the hz scrollbar is 
      if (!scrollbarHeightSet)
      {
        JScrollBar hzScrollBar = scrollPane.getHorizontalScrollBar();
        if (hzScrollBar != null)
          scrollbarHeight = hzScrollBar.getHeight();
        else
          scrollbarHeight = 0;
        scrollbarHeightSet = true;
      }
      // pick one
      boolean useViewport = false;
      boolean compViewport = false;
      boolean compViewport2 = true; // this one works best.

      // this presumptively correct method barely works
      if (useViewport)
      {
        viewportBounds = scrollPane.getViewportBorderBounds();
        viewable = ((int)viewportBounds.getHeight()) / dataRowHeight;
      }
      // this hack works better
      if (compViewport) 
      {
        tb = tabbedPane.getBoundsAt(0);
        bottom = (int)(tb.getY() + tb.getHeight());
        computedSpHeight = tabbedPane.getHeight() - (dataRowHeight + bottom);
        viewable = (computedSpHeight - scrollbarHeight) / dataRowHeight;
      }
      // this works well.  But what does JTabbedPane.getBoundsAt() have to do with it?
      if (compViewport2)
      {
        tb = tabbedPane.getBoundsAt(0); // !!! Worse Than Failure - this must be here!
        viewable = (scrollPane.getHeight() - scrollbarHeight) / dataRowHeight;
      }
      if (viewable > 0)
        viewable -= 1; // take out the title row

      dataRowsPerTabCount[tabIx] = viewable;
    }      
  } // getGuiMetrics

  void updateTable()
  {
    int tabIx, numTabs, rowsPerTab = 0, maxDisplayableRows = 0, rowsAdded, rowsThisTime;
    boolean accepted = false;

    getGuiMetrics();

    topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
    setContentPane(topPane);

    // how many tabs are needed to display all the data rows?
    for (tabIx = 0; !accepted && tabIx < MAXTABS; ++tabIx)
    {
      rowsPerTab = dataRowsPerTabCount[tabIx];
      maxDisplayableRows = rowsPerTab * (tabIx +1);
      if (maxDisplayableRows >= dataRows)
      {
        accepted = true;
        numTabs = tabIx +1;
      }
    }
    // did we find a best fit solution?
    if (!accepted)
    {
      topPane.add(new JLabel("Not enough space for all data rows"));
      return;
    }
    tabbedPane = new JTabbedPane();
    validate();
    tabbedPane.setMaximumSize(new Dimension(topPane.getWidth(),  topPane.getHeight() - arbitraryTweek1));
    topPane.add(tabbedPane, "cell 0 0, grow");

    // create and fill the tab pages
    for (tabIx = 0, rowsAdded = 0; rowsAdded < dataRows; ++tabIx)
    {
      if (rowsAdded + rowsPerTab > dataRows)
        rowsThisTime = dataRows - rowsAdded;
      else
        rowsThisTime = rowsPerTab;

      // create the table for the page
      clientData = new String[rowsThisTime][COLS];
      clientsTableModel = new DefaultTableModel(clientData, CLIENT_COL_NAMES);
      clientsTable = new JTable(rowsThisTime, COLS);
      clientsTable.setModel(clientsTableModel);
      clientsTable.setPreferredScrollableViewportSize(null);
      clientsTable.getTableHeader().setReorderingAllowed(false);
      clientsTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
      clientsTable.getSelectionModel().setSelectionInterval(0, 0);

      // fill the table with test data
      for (int row = 0; row < rowsThisTime; ++row)
      {
        for (int col = 0; col < COLS; ++col)
        {
          String cellVal = "tab " + (tabIx +1) + " cell row " + (row+1) + " col " + (col+1);
          clientsTableModel.setValueAt(cellVal, row, col);
        }
      }
      // create scroll pane holding table
      scrollPane = new JScrollPane(clientsTable);
      scrollPane.setVerticalScrollBarPolicy(scrollPane.VERTICAL_SCROLLBAR_NEVER);
      scrollPane.setHorizontalScrollBarPolicy(scrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

      // create tab panel holding the scroll pane
      JPanel panel = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
      String title = "Page " + (tabIx +1);
      tabbedPane.addTab(title, panel);
      panel.add(scrollPane, "cell 0 0, grow");

      rowsAdded += rowsPerTab;
    }
    tabbedPane.revalidate();
  } // updateTable

  void init(String[] args)
  {
    //  uncomment this to see the migLayout component border highlighting
    //  migDebugString = ", debug";

    // total of how many data rows?
    if (args.length < 1)
    {
      dataRows = 20;
    } 
    else
    {
      dataRows = Integer.valueOf(args[0]);
      if (dataRows <= 0)
      {
        System.out.println("bad arg");
        System.exit(0);
      } 
    }
    setSize(APPWIDTH, APPHEIGHT);
    addComponentListener(new ComponentAdapter()
    {
      public void componentShown(ComponentEvent evt)
      {
      }
      public void componentHidden(ComponentEvent evt)
      {
      }
      public void componentResized(ComponentEvent evt)
      {
        updateTable();
      } // componentResized()
    }); // addComponentListener

    topPane = new JPanel(new MigLayout("fill" + migDebugString, "[100%]", "[100%]"));
    setContentPane(topPane);

    // center app window
    GraphicsConfiguration gc = getGraphicsConfiguration();
    Rectangle bounds = gc.getBounds();
    setLocation((int)((bounds.width-APPWIDTH) /2),
                           (int)((bounds.height - APPHEIGHT) /2));
    setVisible(true);
  }
  public static void main(String[] args)
  {
    try
    {
      ScrollTableTest thisTest = new ScrollTableTest();
      thisTest.init(args);
    }
    catch (Exception e)
    {
      System.out.println("runTest caught exception:  " + e.getMessage());
      e.printStackTrace();
    }
  }
} // class test
nferguso
  • 91
  • 5

0 Answers0