I am trying to make an image editing application in Java using the MVC design pattern. So, event handling is in a controller, state and operations relevant to the state are stored in models, and everything the user sees is stored in views.
When I open an image, there is a zoom box that displays the zoom level of the image being displayed. The zoom is automatically calculated when it is first rendered in paintComponent()
(see step #3). When I open the image, I want the zoom level to be set to what it was calculated to be. The problem is that the zoom level shows 0, and I know why. Let me explain:
1. The ActionListener
for the open menu item is fired
in JPSController:
class MenuBarFileOpenListener implements ActionListener {
public void actionPerformed(ActionEvent event) {
File fileChooserReturnValue = view.showAndGetValueOfFileChooser();
if (fileChooserReturnValue != null) {
try {
// irrelevant code omitted
view.addDocument(newDocument);
} catch(IOException ex) {
ex.printStackTrace();
}
}
}
}
2. view.addDocument()
is called
Note: this is the root of the problem
in JPSView:
public void addDocument(DocumentModel document) {
// irrelevant code omitted
// CanvasPanelView extends JPanel
documentsTabbedPane.add(newDocumentView.getCanvasPanelView());
// THIS IS THE ROOT OF THE PROBLEM
double currentZoomFactor = getCurrentCanvasPanelView().getZoomFactor();
// formatting the text
String zoomLevelText = statusBar_zoomLevelTextField_formatter.format(currentZoomFactor);
// setting the text of the text field
statusBar_zoomLevelTextField.setText(zoomLevelText);
}
3. Some time later, paintComponent()
is run
in CanvasPanelView extends JPanel:
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (initialRender) {
initialRender = false;
// calculates a good zoom level
setZoomFit();
}
// irrelevant code omitted
g.drawImage(image, destinationX1, destinationY1, destinationX2,
destinationY2, sourceX1, sourceY1, sourceX2, sourceY2, null);
}
In part 2, we have this line of code:
double currentZoomFactor = getCurrentCanvasPanelView().getZoomFactor();
When getZoomFactor()
is called, the current CanvasPanelView
must not have a size, because it returns 0
. I had this problem before, and my solution was with these lines of code in #3:
if (initialRender) {
initialRender = false;
setZoomFit();
}
When paintComponent() is called, the CanvasPanelView
must have been given a size by then, but not when getZoomFactor() was called. paintComponent(), and therefore also setZoomFit(), obviously come after getZoomFactor().
How can I correctly show the zoom level of the image when it is opened?
Basically, you have a kind of race condition (as much as you can in single thread).
What's happening is you are adding the new view, expecting it to be laid out immediately. This is not how this works in Swing. When you add a new component to a container, a request is made to update the layouts of all the components in the hierarchy, and layout all those containers that have been marked as invalid. This won't happen until the current call cycle has completed and the Event Dispatching Thread has had time to process all the requests.
Instead, you can schedule a look up at a later time by adding a request to the end of the Event Dispatching Thread. This ensures that your request will be executed after all the current tasks on the EDT have been executed before it (there may be others to follow, but you've squeezed in).
The following example demonstrates the point.
It adds a new panel to the JTabbedPane
, dumps it's current size, use SwingUtilities.invokeLater
to request a callback at a time in the future and dumps the panels size again. You should find that the first request is 0x0
and the second will be a valid value (depending on the size of the frame and the look and feel)
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TestTabbedPane01 {
public static void main(String[] args) {
new TestTabbedPane01();
}
private JTabbedPane tabbedPane;
public TestTabbedPane01() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
tabbedPane = new JTabbedPane();
JButton btnAdd = new JButton("Add");
btnAdd.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final JPanel panel = new JPanel(new GridBagLayout());
panel.add(new JLabel(String.valueOf(tabbedPane.getComponentCount())));
tabbedPane.add(String.valueOf(tabbedPane.getComponentCount()), panel);
System.out.println("New Panel Size = " + panel.getSize());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
System.out.println("New Panel Size (later) = " + panel.getSize());
}
});
}
});
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(tabbedPane);
frame.add(btnAdd, BorderLayout.SOUTH);
frame.setSize(200, 200);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}