Creating a custom TableModel with multiple column headers and row headers - java

I'm attempting to create a JTable that looks like the mockup below:
The green corner is basically buffer-space for the red column and row headers. The cells don't need to be rendered in the colours pictured; however they need to be distinguishable from the rest of the 'white' cells in the table.
This table also is not editable or selectable; it's merely viewed by a user whilst it is updated.
I know this can be achieved using a DefaultTableModel with custom renders for rows 1,2 && cols 1,2 and adding +2 when setting and getting table values (accounting for the rows and columns that are being used as headers).
My questions are as follows:
Is there a cleaner way of doing this without polluting my table model with these static values used in headers?
I've read about extending table models but I'm not sure which class should I extend (DefaultTableModel, AbstractTableModel) and what methods I should override.

Input is limited to 20x20 so including the headers that's 22x22.
Also consider a JScrollPane containing a JPanel having GridLayout and containing 22x22 instances JLabel, or a suitable subclass. This scales easily to several thousand cells.
Addendum: If the need arises, CellRendererPane makes a good flyweight renderer, as suggested here.
If you go with JTable for rendering scalability,
This is no abuse; it is exactly how TableModel is intended to be used. TableModel models a rectangular matrix of whatever you decide. JTable is just an (efficiently rendered) view of that model.
I prefer AbstractTableModel, shown here, because Vector is rarely the desired data structure. Use whatever container makes your indexing most convenient. DefaultTableModel is handy and serves as a guide to extending AbstractTableModel. In particular, you'll need a setValueAt().
#Override
public void setValueAt(Object aValue, int row, int col) {
... // update your data structure
this.fireTableCellUpdated(row, col); // notify the view
}

longer comment, everything depends
1) if is possible for Columns
resize
reordering
2) if is possible for Columns
filtering
sorting
a. then you have look at two JTables, first JTable only with TableHeader, simple with removed rows and second full sized JTable with TableHeader and Columns and rows,
b. for interactions betweens two JTableHeader is there
TableColumnModelListener#columnMarginChanged(ChangeEvent e) and columnMoved(TableColumnModelEvent e)
c. everyting put to one JPanel inside JScrollPane
d. if you'll change numbers of rows or colums (or filtering / sorting) then you have to notified JPanel for rezize JTable#getPreferredScrollableViewportSize() + Dimension for ontop JTable only with TableHeader
very similair way as there (is everything that you needed)
(endless kudos for Rob)
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public class TableFilterRow extends JFrame implements TableColumnModelListener {
private static final long serialVersionUID = 1L;
private JTable table;
private JPanel filterRow;
public TableFilterRow() {
table = new JTable(3, 5);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
JScrollPane scrollPane = new JScrollPane(table);
getContentPane().add(scrollPane);
table.getColumnModel().addColumnModelListener(this);
// Panel for text fields
filterRow = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
for (int i = 0; i < table.getColumnCount(); i++) {
filterRow.add(new JTextField(" Sum at - " + i));
}
columnMarginChanged(new ChangeEvent(table.getColumnModel()));
getContentPane().add(filterRow, BorderLayout.SOUTH);
}
// Implement TableColumnModelListener methods
// (Note: instead of implementing a listener you should be able to
// override the columnMarginChanged and columMoved methods of JTable)
#Override
public void columnMarginChanged(ChangeEvent e) {
TableColumnModel tcm = table.getColumnModel();
int columns = tcm.getColumnCount();
for (int i = 0; i < columns; i++) {
JTextField textField = (JTextField) filterRow.getComponent(i);
Dimension d = textField.getPreferredSize();
d.width = tcm.getColumn(i).getWidth();
textField.setPreferredSize(d);
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
filterRow.revalidate();
}
});
}
#Override
public void columnMoved(TableColumnModelEvent e) {
Component moved = filterRow.getComponent(e.getFromIndex());
filterRow.remove(e.getFromIndex());
filterRow.add(moved, e.getToIndex());
filterRow.validate();
}
#Override
public void columnAdded(TableColumnModelEvent e) {
}
#Override
public void columnRemoved(TableColumnModelEvent e) {
}
#Override
public void columnSelectionChanged(ListSelectionEvent e) {
}
public static void main(String[] args) {
JFrame frame = new TableFilterRow();
frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
3) otherwise look How to Use Raised Borders in the prepareRederer
4) this question has nothing to do with type of TableModel

Related

Is there a good way to detect a (double) mouse click on the border between JTable columns?

I would like to implement the common feature of resizing a column when my user double-clicks on the right border of the column. The thing I'm having trouble with is detecting that he has clicked there.
We have methods for determining the column and row of a mouse position, but I am not finding anything to help determine that the cursor was clicked on the border between columns. I suppose I can get the position of the cursor relative to the table origin, and then use the widths of the rows, etc., to figure out if the cursor is on one of the borders, but it seems like a lot of code for the purpose. Something in the UI runtime knows the cursor is on that border, because it changes the cursor shape when the cursor moves over the border.
I tried this:
JTableHeader tableHeader = table.getTableHeader();
tableHeader.addMouseListener(new MouseAdapter()
{
public void mousePressed(MouseEvent mouseEvent)
{
int modifiers = mouseEvent.getModifiers();
int modifiersX = mouseEvent.getModifiersEx();
int row = table.rowAtPoint(mouseEvent.getPoint());
int col = table.columnAtPoint(mouseEvent.getPoint());
System.out.printf("m1 %x, m2 %x, row/col %d,%d%n", modifiers, modifiersX, row, col);
}
}
);
but the modifiers and extended modifiers are the same whether I click on a border or not, so those don't help with this.
So how can I ask the system whether that mouse click was on a border, without having to calculate the position of the borders each time?
If I understood you correctly, the thing you are trying to check is whether the mouse is on a JTable column header border or not?
The thing that you already mentioned is that, if the cursor is on the border, it will change its shape.
I don't actually see any disadvantages using that functionality for your purposes.
So why not simply check for the current cursor on the mouseClicked() event in the table header mouselistener?
Quick demo:
public static void main(String args[]) {
Test t = new Test();
SwingUtilities.invokeLater(() -> {
t.buildGui();
});
}
void buildGui() {
JFrame frame = new JFrame();
String[] header = { "col1", "col2" };
Object[][] data = { { "data1", "data2" } };
JTable table = new JTable(data, header);
table.getTableHeader().addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (table.getTableHeader().getCursor().getType() == Cursor.E_RESIZE_CURSOR) {
if (e.getClickCount() == 2 && !e.isConsumed()) {
e.consume();
System.out.println("Double click on border!");
}
}
}
});
JScrollPane pane = new JScrollPane(table);
frame.add(pane);
frame.pack();
frame.setVisible(true);
}
On mouseClicked() on the table header the cursor is checked if it is the "resize cursor".
Additionally, it is checked if it's a double click.
I tested this and it seems to work without any disadvantages. Might not be the prettiest solution, but I couldn't think of a better one as of now.

JTable appearing blank

I will only post the part that matters.
I have created a JFrame with a JPanel in it, that contains some JTextFields, JTextAreas and a JList. I know want to add a JTable to show some results, but it will appear blank. I tried checking out some posts, but I wasn't able to fix it.
I entered 2 rows manually, but they wouldn't appear. Nor would the column names. Please help!
import javax.swing.*;
import javax.swing.table.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
public class GUI_Automata_Ex_1 extends JFrame {
public static int ScreenWidth = Toolkit.getDefaultToolkit().getScreenSize().width;
public static int ScreenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
public static int WindowWidth = ScreenWidth*3/4;
public static int WindowHeight = WindowWidth*9/16;
public static int unit = WindowWidth/160;
// tables used
static String[] columnNames = {"word", "length", "result"};
static Object[][] data = {{"abbaa", new Integer (5), "belongs"}, {"baabbb", new Integer (6), "does not belong"}};
public static JTable table_saved_words = new JTable(data, columnNames);
public static DefaultTableModel dtm_saved_words = new DefaultTableModel();
public static JScrollPane sp_saved_words;
public GUI_Automata_Ex_1 () {
// this will only run on ultrawide screens (e.g. 21:9 or 32:9) because the window is 16:9 optimized
if (ScreenWidth/2 > ScreenHeight) {
WindowHeight = ScreenHeight*3/4;
WindowWidth = WindowHeight*16/9;
unit = WindowWidth/160;
}
this.setTitle("Automata Theory 1st Excercise");
this.setBounds(ScreenWidth/2-WindowWidth/2,ScreenHeight/2-WindowHeight/2,WindowWidth,WindowHeight);
this.setResizable(false);
this.setVisible(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setBackground(colorBG);
Board board = new Board();
this.setContentPane(board);
this.setLayout(null);
// TABLES
// settings for table_saved_words
table_saved_words.setBackground(Color.white);
table_saved_words.setFont(fontM);
table_saved_words.setModel(dtm_saved_words);
table_saved_words.setPreferredScrollableViewportSize(new Dimension(unit*86, unit*50));
table_saved_words.setFillsViewportHeight(true);
sp_saved_words = new JScrollPane(table_saved_words);
board.add(sp_saved_words);
sp_saved_words.setBounds(unit*68, unit*32, unit*86, unit*50);
sp_saved_words.setWheelScrollingEnabled(true);
sp_saved_words.setViewportView(table_saved_words);
sp_saved_words.setVisible(false);
dtm_saved_words.addRow(new Object[]{"aabbbbaa", 5, "belongs"});
}
public class Board extends JPanel {
public void paintComponent (Graphics g) {
}
}
}
}
Here is a screenshot:
https://i.stack.imgur.com/AHtLf.png
The JTable is on the bottom-right. I did not include the code part for the other J-components.
What I want to do after I get the lists to show is simply add some rows as the window runs (words that the user enters and either belong to a dictionary or not and their length), but the part I'm stuck on is getting the JTable to show the columns and data.
Your table model has no columns, so it never shows the data you add to it.
From the documentation for the zero-argument DefaultTableModel constructor
Constructs a default DefaultTableModel which is a table of zero columns and zero rows.
Initially, your table has a valid table model created automatically:
JTable table_saved_words = new JTable(data, columnNames);
But then you create a new model, with no columns:
DefaultTableModel dtm_saved_words = new DefaultTableModel();

Preferred height of JPanel is lower then combined height of its children in table renderer

I have a JTable for which the renderer returns a JPanel composed of multiple JLabel instances. One of those JLabels can contain HTML used among other things to split the output over multiple lines using <br/> tags.
To show the multiple lines in the table, the renderer calls in the getTableCellRendererComponent method
table.setRowHeight(row, componentToReturn.getPreferredSize().height);
to dynamically update the row height, based on the contents. This only works correctly if componentToReturn indicates a correct preferred size.
It looks however that the getPreferredSize returns bogus values. The preferred height of the returned component is smaller than the sum of the heights of the labels inside the component.
Here is a little program illustrating this behaviour (without using a JTable)
import java.awt.*;
import javax.swing.*;
public class SwingLabelTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
LabelPanel renderer = new LabelPanel();
Component component = renderer.getComponent(false);
//asking for a bigger component will not
//update the preferred size of the returned component
component = renderer.getComponent(true);
}
});
}
private static class LabelPanel {
private final JPanel compositePanel;
private final JLabel titleLabel = new JLabel();
private final JLabel propertyLabel = new JLabel();
public LabelPanel() {
JPanel labelPanel = new JPanel();
labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));
labelPanel.add(titleLabel);
labelPanel.add(propertyLabel);
compositePanel = new JPanel(new BorderLayout());
//normally it contains more components,
//but that is not needed to illustrate the problem
compositePanel.add(labelPanel, BorderLayout.CENTER);
}
public Component getComponent( boolean aMultiLineProperty ) {
titleLabel.setText("Title");
if ( aMultiLineProperty ){
propertyLabel.setText("<html>First line<br/>Property: value</html>");
} else {
propertyLabel.setText("Property: value");
}
int titleLabelHeight = titleLabel.getPreferredSize().height;
int propertyLabelHeight = propertyLabel.getPreferredSize().height;
int compositePanelHeight = compositePanel.getPreferredSize().height;
if ( compositePanelHeight < titleLabelHeight + propertyLabelHeight){
throw new RuntimeException("Preferred size of the component returned "
+ "by the renderer is incorrect");
}
return compositePanel;
}
}
}
As I am aware that the previous example is a bit far-fetched, here an example which includes a JTable
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
public class SwingTableTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
DefaultTableModel tableModel = new DefaultTableModel(0, 1);
JTable table = new JTable(tableModel);
table.setDefaultRenderer(Object.class, new DataResultRenderer());
tableModel.addRow(new Object[]{new Object()});
tableModel.addRow(new Object[]{new Object()});
tableModel.addRow(new Object[]{new Object()});
JFrame testFrame = new JFrame("TestFrame");
testFrame.getContentPane().add(new JScrollPane(table));
testFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
testFrame.setSize(new Dimension(300, testFrame.getPreferredSize().height));
testFrame.setVisible(true);
}
});
}
private static class DataResultRenderer implements TableCellRenderer {
private final JPanel compositePanel;
private final JLabel titleLabel = new JLabel();
private final JLabel propertyLabel = new JLabel();
public DataResultRenderer() {
JPanel labelPanel = new JPanel();
labelPanel.setOpaque(false);
labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));
labelPanel.add(titleLabel);
labelPanel.add(propertyLabel);
compositePanel = new JPanel(new BorderLayout());
//normally it contains more components,
//but that is not needed to illustrate the problem
compositePanel.add(labelPanel, BorderLayout.CENTER);
}
#Override
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
titleLabel.setText("Title");
if ( row == 2 ){
propertyLabel.setText("<html>Single property: value</html>");
} else {
String text = "<html>";
text += "First property<br/>";
text += "Second property<br/>";
text += "Third property:value";
text += "</html>";
propertyLabel.setText(text);
}
int titleLabelHeight = titleLabel.getPreferredSize().height;
int propertyLabelHeight = propertyLabel.getPreferredSize().height;
int compositePanelHeight = compositePanel.getPreferredSize().height;
if ( compositePanelHeight < titleLabelHeight + propertyLabelHeight){
throw new RuntimeException("Preferred size of the component returned "
+ "by the renderer is incorrect");
}
table.setRowHeight(row, compositePanel.getPreferredSize().height);
return compositePanel;
}
}
}
I am looking for a way to update the row height of the table to ensure that the multi-line content is completely visible, without knowing up front how many lines each row will contain.
So either I need a solution to retrieve the correct preferred size, or my approach is completely wrong and then I need a better one.
Note that the above examples are simplified. In the real code, the "renderer" (the code responsible for creating the component) is decorated a few times. This means that the outer renderer is the only with access to the JTable, and it has no knowledge about what kind of Component the inner code returns.
Because setRowHeight() "Sets the height, in pixels, of all cells to rowHeight, revalidates, and repaints," the approach is unsound. Absent throwing an exception, profiling shows 100% CPU usage as an endless cascade of repaints tries to change the row height repeatedly. Moreover, row selection becomes unreliable.
Some alternatives include these:
Use TablePopupEditor to display multi-line content on request from a TableCellEditor.
Update an adjacent multi-line panel from a TableModelListener, as shown here.

JTable all column aligned right

Is there a way to align all the column in jtable at the same time?
using this:
DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
rightRenderer.setHorizontalAlignment( JLabel.RIGHT );
JTAB_TABLE.getColumnModel().getColumn(0).setCellRenderer( rightRenderer );
will let me align only one column but i need to align all.
Normally, a table contains different kinds of data, (Date, Number, Boolean, String) and it doesn't make sense to force all types of data to be right aligned.
If however you have a table with all the same type of data and you want to force the renderering of all columns to be the same, then you should probably use the same renderer. Assuming you are using the default renderer you can use:
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)table.getDefaultRenderer(Object.class);
renderer.setHorizontalAlignment( JLabel.RIGHT );
You can do so by overriding prepareRenderer(...) in JTable. This assumes that any custom renderers are JLabels (they're JLabels by default). You'd have to guard against it otherwise.
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
public class TableDemo implements Runnable
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new TableDemo());
}
public void run()
{
JTable table = new JTable(5, 5)
{
#Override
public Component prepareRenderer(TableCellRenderer renderer,
int row, int col)
{
Component comp = super.prepareRenderer(renderer, row, col);
((JLabel) comp).setHorizontalAlignment(JLabel.RIGHT);
return comp;
}
};
table.setPreferredScrollableViewportSize(table.getPreferredSize());
JScrollPane scrollPane = new JScrollPane(table);
JFrame frame = new JFrame();
frame.getContentPane().add(scrollPane);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}

JTable won't scroll in JScrollPane

I've been having trouble trying to get the table to scroll; it just won't. I've looked at other stack answers and tried them, but they aren't working; and I'm not sure if I have something conflicting with those solutions.
tableModel = new TableModel(); //Custom Table Model
table = new JTable();
table.setBorder(null);
table.setFillsViewportHeight(true);
table.setModel(tableModel);
JScrollPane tblScrollPane = new JScrollPane(table);
tblScrollPane.setBorder(new EtchedBorder(EtchedBorder.LOWERED, null, null));
tblScrollPane.setBounds(245, 17, 560, 425);
frmServerAdministration.getContentPane().add(tblScrollPane);
EDIT: More info
So a window opens, which is the server program. Client programs connect to the server and when they do, a method is triggered in my table model class which adds a new row into the table. (I can see the row) Then at the end of that method it calls another but nothing changes in the ScrollPane. Do I need to do some kind of repainting? -
Server.updateTableScroll();
public static void updateTableScroll() {
System.out.println("Here"); //NOTE: I do see this printed out
int last = table.getModel().getRowCount() - 1;
Rectangle r = table.getCellRect(last, 0, true);
table.scrollRectToVisible(r);
}
EDIT 2: Thoughts
So in Eclipse I use Window Builder to make the GUI, and the following for loop will display the table with the scrollbar! But when I run the same addClient() method at another point, then the scroll bar won't appear.
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
Server window = new Server();
window.frmServerAdministration.setVisible(true);
for(int i = 0; i < 20; i++){
tableModel.addClient(i, String.valueOf(i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
Instead of setBounds(), override getPreferredScrollableViewportSize(), as suggested here, and pack() the enclosing top-level container. Also, the API authors "recommend that you put the component in a JPanel and set the border on the JPanel."
Addendum: As a JTable listens to its TableModel, verify that the correct event is fired from the model. If you extend AbstractTableModel, one of the fireTableXxx() methods will be appropriate.
All I needed to do was call this after I added data (I'm very new to table models :P )
this.fireTableStructureChanged();

Categories