I've created a simple Java application that each second for for 10 seconds consecutive seconds adds a new row to a JTable
. It consists of three classes.
The main class that gets called once the program is started
public class JarBundlerProblem {
public static void main(String[] args)
{
System.err.println("Initializing controller");
new Controller();
}
}
A controller that creates the GUI and alters it through doWork()
public class Controller {
public Controller()
{
doWork(null);
}
public static void doWork(String s)
{
GUI gui = new GUI();
for (int i=0; i<10; i++)
{
gui.addRow("Line "+(i+1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
And finally, the GUI
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
public class GUI {
private JFrame frame = new JFrame();
private DefaultTableModel model = new DefaultTableModel();
private JTable table = new JTable(model);
private JScrollPane pane = new JScrollPane(table);
public GUI()
{
model.addColumn("Name");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(pane);
frame.pack();
frame.setVisible(true);
}
public void addRow(String name)
{
model.addRow(new Object[]{name});
}
}
Since I'm developing for OS X, and I need to be able to associate my application with a certain file type (let's say .jarbundlerproblem
), I have to bundle my JAR
file into an APP
using Apple Jar Bundler. I have done this successfully, my application opens, counts to ten, writing out each second.
By default, double-clicking a .jarbundlerproblem
, and associating the file with my application, will not pass the file I double-clicked as an argument to the application. Apparently, this is just Java on OS X works.
Since I need to be able to see what file was double-clicked, I'm using OSXAdapter which is a Java library made by Apple for the purpose. This, I've implemented by altering the constructor of my Controller
class and added another method registerForMacOSXEvents()
:
public Controller()
{
registerForMacOSXEvents();
//doWork(null);
}
public void registerForMacOSXEvents() {
try {
OSXAdapter.setFileHandler(this, getClass().getDeclaredMethod("doWork", new Class[] { String.class }));
} catch (Exception e) {
System.err.println("Error while loading the OSXAdapter:");
e.printStackTrace();
}
}
But after this (minor) modification, my application starts acting up. Sometimes, it doesn't open, even though I can see in the Console that it just started (Initializing controller
is written), but after a few attempts, it will eventually start, but the windows will be completely blank for the first 10 seconds, and after that, the 10 rows will be added.
Now, I've struggled with this quite a bit, and it seems like there isn't a lot of documentation regarding neither OSXAdapter nor Jar Bundler. What am I doing wrong? Or shouldn't I be using OSXAdapter or Jar Bundler in the first place?
It looks like you're blocking the event dispatch thread(EDT). SwingWorker
would be a better choice, but this example implements Runnable
.
Addendum: You might look at this project for an example of MVC architecture. It also shows how to construct a Mac OS application bundle without using JAR Bundler. More on MVC may be found here.
As an aside, this example shows one approach to auto-scrolling a JTable
. Click on the thumb to suspend scrolling; release to resume.
Addendum: Your application lags for 10 seconds on startup. As this is the exact time for which the Controller
sleeps, it's surely sleeping on the EDT. An sscce would be dispositive. Instead, do the work on another thread and update the model on the EDT. SwingWorker
has a process()
method that does so automatically, or you can use invokeLater()
as shown below. Until your application is correctly synchronized, there's little hope of getting Apple events to work.
Addendum: You can invoke isDispatchThread()
in the Controller
to check. The project cited includes a .dmg
with a Mac application and an ant
file that builds the bundle in situ via target dist2
.
Addendum: See also the alternate approaches shown here.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
/** @seehttps://stackoverflow.com/questions/7519244 */
public class TableAddTest extends JPanel implements Runnable {
private static final int N_ROWS = 8;
private static String[] header = {"ID", "String", "Number", "Boolean"};
private DefaultTableModel dtm = new DefaultTableModel(null, header) {
@Override
public Class<?> getColumnClass(int col) {
return getValueAt(0, col).getClass();
}
};
private JTable table = new JTable(dtm);
private JScrollPane scrollPane = new JScrollPane(table);
private JScrollBar vScroll = scrollPane.getVerticalScrollBar();
private JProgressBar jpb = new JProgressBar();
private int row;
private boolean isAutoScroll;
public TableAddTest() {
this.setLayout(new BorderLayout());
jpb.setIndeterminate(true);
this.add(jpb, BorderLayout.NORTH);
Dimension d = new Dimension(320, N_ROWS * table.getRowHeight());
table.setPreferredScrollableViewportSize(d);
for (int i = 0; i < N_ROWS; i++) {
addRow();
}
scrollPane.setVerticalScrollBarPolicy(
JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
vScroll.addAdjustmentListener(new AdjustmentListener() {
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
isAutoScroll = !e.getValueIsAdjusting();
}
});
this.add(scrollPane, BorderLayout.CENTER);
JPanel panel = new JPanel();
panel.add(new JButton(new AbstractAction("Add Row") {
@Override
public void actionPerformed(ActionEvent e) {
addRow();
}
}));
this.add(panel, BorderLayout.SOUTH);
}
private void addRow() {
char c = (char) ('A' + row++ % 26);
dtm.addRow(new Object[]{
Character.valueOf(c),
String.valueOf(c) + String.valueOf(row),
Integer.valueOf(row),
Boolean.valueOf(row % 2 == 0)
});
}
private void scrollToLast() {
if (isAutoScroll) {
int last = table.getModel().getRowCount() - 1;
Rectangle r = table.getCellRect(last, 0, true);
table.scrollRectToVisible(r);
}
}
@Override
public void run() {
while (true) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
addRow();
}
});
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
scrollToLast();
}
});
try {
Thread.sleep(1000); // simulate latency
} catch (InterruptedException ex) {
System.err.println(ex);
}
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TableAddTest nlt = new TableAddTest();
f.add(nlt);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
new Thread(nlt).start();
}
});
}
}
After doing it, I'm not fully convinced a SwingWorker is a simpler (aka: better) solution - still requires additional thread synching (between the worker thread and the "outer" thread which passes in the file/names). Anyway (taking the opportunity to learn, and be it by errors :), below is a crude proof of concept example for the basic idea:
open issues
Feedback welcome :-)
public class GUI {
private JFrame frame = new JFrame();
private DefaultTableModel model = new DefaultTableModel();
private JTable table = new JTable(model);
private JScrollPane pane = new JScrollPane(table);
public GUI() {
model.addColumn("Name");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(pane);
frame.pack();
frame.setVisible(true);
}
public void addRow(String name) {
model.addRow(new Object[] { name });
}
/**
* Controller is a SwingWorker.
*/
public static class Controller extends SwingWorker<Void, String> {
private GUI gui;
private List<String> pending;
public Controller() {
gui = new GUI();
}
public void doWork(String newLine) {
if (pending == null) {
pending = new ArrayList<String>();
pending.add(newLine);
execute();
} else {
pending.add(newLine);
}
}
@Override
protected Void doInBackground() throws Exception {
while (pending.size() > 0) {
publish(pending.remove(0));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
/**
* @inherited <p>
*/
@Override
protected void process(List<String> chunks) {
for (String object : chunks) {
gui.addRow(object);
}
}
}
/**
* Simulating the adapter.
*
* Obviously, the real-thingy wouldn't have a reference
* to the controller, but message the doWork refectively
*/
public static class Adapter implements Runnable {
Controller controller;
public Adapter(Controller controller) {
this.controller = controller;
}
@Override
public void run() {
for (int i=0; i<10; i++)
{
controller.doWork("Line "+(i+1));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args)
{
System.err.println("Initializing controller");
new Adapter(new Controller()).run();
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(GUI.class.getName());
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With