JAR Bundler que utiliza OSXAdapter provoca que la aplicación se retrase o finalice
He creado una aplicación Java simple que cada segundo durante 10 segundos consecutivos agrega una nueva fila a un archivo JTable
. Consta de tres clases.
La clase principal a la que se llama una vez que se inicia el programa.
public class JarBundlerProblem {
public static void main(String[] args)
{
System.err.println("Initializing controller");
new Controller();
}
}
Un controlador que crea la GUI y la modifica a través dedoWork()
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();
}
}
}
}
Y finalmente, la 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});
}
}
Como estoy desarrollando para OS X y necesito poder asociar mi aplicación con un determinado tipo de archivo (digamos .jarbundlerproblem
), tengo que agrupar mi archivo JAR
en un Apple Jar Bundler . Lo hice con éxito, mi aplicación se abre, cuenta hasta diez y escribe cada segundo.APP
Ahora, para el problema
De forma predeterminada, hacer doble clic en .jarbundlerproblem
y asociar el archivo con mi aplicación no pasará el archivo en el que hice doble clic como argumento a la aplicación. Aparentemente, esto es solo que Java funciona en OS X.
Como necesito poder ver en qué archivo se hizo doble clic, estoy usando OSXAdapter , que es una biblioteca de Java creada por Apple para ese propósito. Esto lo implementé alterando el constructor de mi Controller
clase y agregué otro método 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();
}
}
Pero después de esta modificación (menor), mi aplicación comienza a funcionar mal. A veces, no se abre, aunque puedo ver en la Consola que acaba de iniciar ( Initializing controller
está escrito), pero después de algunos intentos, eventualmente se iniciará, pero las ventanas estarán completamente en blanco durante los primeros 10 segundos, y después de eso, se agregarán las 10 filas.
Ayuda
Ahora, he luchado bastante con esto y parece que no hay mucha documentación sobre OSXAdapter ni Jar Bundler. ¿Qué estoy haciendo mal? ¿O no debería usar OSXAdapter o Jar Bundler en primer lugar?
Parece que estás bloqueando el hilo de envío de eventos (EDT). SwingWorker
Sería una mejor opción, pero este ejemplo implementa Runnable
.
Anexo: puede consultar este proyecto para ver un ejemplo de arquitectura MVC . También muestra cómo construir un paquete de aplicaciones para Mac OS sin utilizar JAR Bundler. Puede encontrar más información sobre MVC aquí .
Además, este ejemplo muestra un enfoque para el desplazamiento automático de un archivo JTable
. Haga clic en el pulgar para suspender el desplazamiento; suelte para reanudar.
Anexo: Su aplicación tiene un retraso de 10 segundos al iniciarse. Como esta es la hora exacta a la que Controller
duerme, seguramente estará durmiendo en el EDT. Una sscce sería dispositiva. En su lugar, trabaje en otro hilo y actualice el modelo en el EDT. SwingWorker
Tiene un process()
método que lo hace automáticamente, o puede usarlo invokeLater()
como se muestra a continuación. Hasta que su aplicación esté sincronizada correctamente, hay pocas esperanzas de que los eventos de Apple funcionen.
Anexo: Puede invocarlo isDispatchThread()
en el Controller
para verificar. El proyecto citado incluye una .dmg
aplicación para Mac y un ant
archivo que crea el paquete in situ a través de target dist2
.
Anexo: consulte también los enfoques alternativos que se muestran aquí .
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();
}
});
}
}
Después de hacerlo, no estoy completamente convencido de que SwingWorker sea una solución más simple (también conocida como: mejor); todavía requiere sincronización de subprocesos adicional (entre el subproceso de trabajo y el subproceso "externo" que pasa los archivos/nombres). De todos modos (aprovechando la oportunidad para aprender, y ya sea por errores :), a continuación se muestra un ejemplo burdo de prueba de concepto para la idea básica:
- implementar el controlador como SwingWorker, que canaliza la entrada del hilo externo al EDT
- hacer que acepte entradas (del adaptador, fi) a través de un método doWork(..) que pone en cola la entrada para su publicación
- implementar doInBackground para publicar sucesivamente la entrada
problemas abiertos
- sincronizar el acceso a la lista local (no soy un experto en concurrencia, pero estoy bastante seguro de que es necesario hacerlo)
- detectar de manera confiable el final del hilo externo (aquí simplemente se detiene cuando la cola de entrada está vacía)
Comentarios bienvenidos :-)
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());
}
Aquí hay una variación del ejemplo de @kleopatra en el que una ejecución continua Controller
acepta nuevas entradas en doWork()
, mientras que SwingWorker
procesa las pending
entradas de forma asincrónica en su hilo de fondo. ArrayBlockingQueue
maneja la sincronización.
import java.awt.EventQueue;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;
public class GUI {
private static final Random rnd = new Random();
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.
*/
private static class Controller extends SwingWorker<Void, String> {
private static final int MAX = 5;
private GUI gui;
private BlockingQueue<String> pending =
new ArrayBlockingQueue<String>(MAX);
public Controller() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
gui = new GUI();
}
});
}
private void doWork(String newLine) {
try {
pending.put(newLine);
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
@Override
protected Void doInBackground() throws Exception {
while (true) {
// may block if nothing pending
publish(pending.take());
try {
Thread.sleep(rnd.nextInt(500)); // simulate latency
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}
@Override
protected void process(List<String> chunks) {
for (String object : chunks) {
gui.addRow(object);
}
}
}
/**
* Exercise the Controller.
*/
private static class Adapter implements Runnable {
private Controller controller;
private Adapter(Controller controller) {
this.controller = controller;
}
@Override
public void run() {
controller.execute();
int i = 0;
while (true) {
// may block if Controller busy
controller.doWork("Line " + (++i));
try {
Thread.sleep(rnd.nextInt(500)); // simulate latency
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
}
}
}
public static void main(String[] args) {
System.out.println("Initializing controller");
// Could run on inital thread via
// new Adapter(new Controller()).run();
// but we'll start a new one
new Thread(new Adapter(new Controller())).start();
}
}