Menu

Nakov.com logo

Thoughts on Software Engineering

Java SWING Error Dialog with Exception Details

I use SWING for long time but I am amazed how slowly this GUI framework evolve and how far is it comparing to Windows Forms, WPF and Flex. One of the small missing things in Swing is that there is not standard way to display an error message with Exception stacktrace. So I created such dialog and I want to share it with everybody using SWING. The result look like this (shrinked form):

swing-error-dialog-shrinked.png

When the dialog is expanded, it shows the exception:

swing-error-dialog-expanded.png

It seems like creating such a dialog is trivial task but unfortunately you need to solve a number of problems related to correct positioning, scrolling issues, etc. When the exception is large, it needs a scroller. When the error description is logn it needs good layout and scroller. This makes the things a little bit challenging. See the code below (the ErrorDilaog class):


import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import javax.swing.*;
import javax.swing.text.*;

/**
 * @author Svetlin Nakov
 */
@SuppressWarnings("serial")
public class ErrorDialog extends JDialog {

 private static final String SHOW_DETAILS_TEXT = "Show Details ...";
 private static final String HIDE_DETAILS_TEXT = "Hide Details";
 private JButton jButtonClose;
 private ImagePanel imagePanelErrorIcon;
 private JButton jButtonShowHideDetails;
 private JPanel jPanelBottom;
 private JPanel jPanelCenter;
 private JPanel jPanelTop;
 private JScrollPane jScrollPaneErrorMsg;
 private JTextPane jTextPaneErrorMsg;
 private JScrollPane jScrollPaneException;
 private JTextArea jTextAreaException;

 private static final String ERROR_ICON_RESOURCE_LOCATION =
  "Error-Icon.gif";

 public ErrorDialog(String errorMessage) {
  this(errorMessage, null);
 }
 
 public ErrorDialog(String errorMessage, Throwable exception) {
  this.setTitle("Error");
  this.setModal(true);
  this.setResizable(false);
  this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);

        jPanelTop = new JPanel();
        imagePanelErrorIcon = new ImagePanel(ERROR_ICON_RESOURCE_LOCATION);
        jPanelTop.setLayout(null);
        jPanelTop.setPreferredSize(new Dimension(480, 100));
        imagePanelErrorIcon.setLocation(new Point(20, 36));
        jPanelTop.add(imagePanelErrorIcon);

        jTextPaneErrorMsg = new JTextPane();
        jTextPaneErrorMsg.setFont(jTextPaneErrorMsg.getFont().deriveFont(
         jTextPaneErrorMsg.getFont().getStyle() | Font.BOLD,
         jTextPaneErrorMsg.getFont().getSize()+1));
        jTextPaneErrorMsg.setBorder(null);
        jTextPaneErrorMsg.setEditable(false);
        jTextPaneErrorMsg.setBackground(null);
        jScrollPaneErrorMsg = new JScrollPane(jTextPaneErrorMsg);
        jScrollPaneErrorMsg.setBorder(null);
        jScrollPaneErrorMsg.setSize(new Dimension(405, 80));
        jScrollPaneErrorMsg.setLocation(new Point(71, 13));
        jPanelTop.add(jScrollPaneErrorMsg);

        jPanelCenter = new JPanel();
        jPanelCenter.setSize(new Dimension(420, 300));
        jTextAreaException = new JTextArea();
        jScrollPaneException = new JScrollPane(jTextAreaException);
        jScrollPaneException.setPreferredSize(new Dimension(470, 300));
        jPanelCenter.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
        jPanelCenter.add(jScrollPaneException);

        jPanelBottom = new JPanel();
        jButtonShowHideDetails = new JButton();
        jButtonClose = new JButton();
        jPanelBottom.setLayout(new FlowLayout(FlowLayout.CENTER, 30, 15));

        jButtonShowHideDetails.setText(SHOW_DETAILS_TEXT);
        jButtonShowHideDetails.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                showHideExceptionDetails();
            }
        });
        jPanelBottom.add(jButtonShowHideDetails);

        jButtonClose.setText("Close");
        jButtonClose.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
             dispose();
            }
        });
        jPanelBottom.add(jButtonClose);
 
        this.setLayout(new BorderLayout());
        this.add(jPanelTop, BorderLayout.NORTH);
        this.add(jPanelCenter, BorderLayout.CENTER);
        this.add(jPanelBottom, BorderLayout.SOUTH);

        this.jTextPaneErrorMsg.setEditorKit(new VerticalCenteredEditorKit());
        this.jTextPaneErrorMsg.setText(errorMessage);
       
        this.jPanelCenter.setVisible(false);
       
        if (exception != null) {
         String exceptionText = getStackTraceAsString(exception);
         jTextAreaException.setText(exceptionText);
         jTextAreaException.setEditable(false);
        } else {
         this.jButtonShowHideDetails.setVisible(false);
        }

  // Make [Escape] key as close button
  this.registerEscapeKey();

        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        setModal(true);
  this.pack();
  centerDialogOnTheScreen();
 }

 private void centerDialogOnTheScreen() {
  Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
  Dimension dialogSize = this.getSize();
  int centerPosX = (screenSize.width - dialogSize.width) / 2;
  int centerPosY = (screenSize.height - dialogSize.height) / 2;
  setLocation(centerPosX, centerPosY);
 }
 
 private void showHideExceptionDetails() {
        if (this.jPanelCenter.isVisible()) {
            // Hide the exception details
            this.jButtonShowHideDetails.setText(SHOW_DETAILS_TEXT);
            this.jPanelCenter.setVisible(false);
      this.pack();
            centerDialogOnTheScreen();
        } else {
            // Show the exception details
            this.jButtonShowHideDetails.setText(HIDE_DETAILS_TEXT);
            this.jPanelCenter.setVisible(true);
      this.pack();
            centerDialogOnTheScreen();
        }
 }

 private String getStackTraceAsString(Throwable exception) {
  Writer result = new StringWriter();
  PrintWriter printWriter = new PrintWriter(result);
  exception.printStackTrace(printWriter);
  return result.toString();
 }

 /**
  * Make the [Escape] key to behave like the [Close] button.
  */
 public void registerEscapeKey() {
  KeyStroke escapeKeyStroke =
   KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false);
  Action escapeAction = new AbstractAction() {
   private static final long serialVersionUID = 1L;

   public void actionPerformed(ActionEvent e) {
    jButtonClose.doClick();
   }
  };

  this.rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
    escapeKeyStroke, "ESCAPE");
  this.rootPane.getActionMap().put("ESCAPE", escapeAction);
 }
 
 public void hideAndDisposeDialog() {
  this.setVisible(false);
  this.dispose();
 }

 public static void showError(String errorMessage,
   Throwable throwable) {
  ErrorDialog errorDialog =
   new ErrorDialog(errorMessage, throwable);
  errorDialog.setVisible(true);  
 }

 public static void showError(String errorMessage) {
  ErrorDialog.showError(errorMessage, null); 
 }
 
 public static void main(String[] args) {
  ErrorDialog.showError("This is an error message.", new Exception());
 }
}

@SuppressWarnings("serial")
class VerticalCenteredEditorKit extends StyledEditorKit {
 public ViewFactory getViewFactory() {
  return new StyledViewFactory();
 }

 static class StyledViewFactory implements ViewFactory {
  public View create(Element elem) {
   String kind = elem.getName();
   if (kind != null) {
    if (kind.equals(AbstractDocument.ContentElementName)) {
     return new LabelView(elem);
    } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
     return new ParagraphView(elem);
    } else if (kind.equals(AbstractDocument.SectionElementName)) {
     return new CenteredBoxView(elem, View.Y_AXIS);
    } else if (kind.equals(StyleConstants.ComponentElementName)) {
     return new ComponentView(elem);
    } else if (kind.equals(StyleConstants.IconElementName)) {
     return new IconView(elem);
    }
   }

   // Default to text display
   return new LabelView(elem);
  }
 }
 
 static class CenteredBoxView extends BoxView {
  public CenteredBoxView(Element elem, int axis) {
   super(elem, axis);
  }

  protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets,
    int[] spans) {
   super.layoutMajorAxis(targetSpan, axis, offsets, spans);
   int textBlockHeight = 0;
   int offset = 0;

   for (int i = 0; i < spans.length; i++) {
    textBlockHeight += spans[i];
   }
   offset = (targetSpan - textBlockHeight) / 2;
   for (int i = 0; i < offsets.length; i++) {
    offsets[i] += offset;
   }
  }
 }
}

The ImageUtils class provides a simplified API for loading images from a file:


import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author Svetlin Nakov
 */
public class ImageUtils {
 
 private static final int BUF_SIZE = 4096;

 public static Image loadImageFromResource(String resourceLocation,
   Component imageOwner) {
  InputStream inputStream = ImageUtils.class.getClassLoader()
    .getResourceAsStream(resourceLocation);
  try {
   byte[] imageBinaryData = readStreamToEnd(inputStream);
   Image image = Toolkit.getDefaultToolkit().createImage(imageBinaryData);
   ImageUtils.ensureImageIsLoaded(image, imageOwner);
   return image;
  } catch (Exception ex) {
   throw new RuntimeException(
    "Cannot load image from resource: " + resourceLocation, ex);
  } finally {
   try {
    inputStream.close();
   } catch (IOException e) {
    // Ignore IO exceptions during file closing
   }
  }
 }

 private static byte[] readStreamToEnd(InputStream inputStream)
 throws IOException {
  ByteArrayOutputStream output = new ByteArrayOutputStream(BUF_SIZE);
  byte[] buf = new byte[BUF_SIZE];
  while (true) {
   int bytesRead = inputStream.read(buf);
   if (bytesRead == -1) {
    // End of stream reached
    break;
   }
   output.write(buf, 0, bytesRead);
  }
  byte[] streamData = output.toByteArray();
  return streamData;
 }

 public static void ensureImageIsLoaded(Image image, Component imageOwner) {
  MediaTracker mediaTracker = new MediaTracker(imageOwner);
  mediaTracker.addImage(image, 0);
  try {
   mediaTracker.waitForAll();
  } catch (InterruptedException intEx) {
   throw new RuntimeException("Image loading was interrupted.", intEx);
  }
 }

}

The class ImagePanel shows image in a JPanel:


import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.JPanel;

/**
 * @author Svetlin Nakov
 */
public class ImagePanel extends JPanel {

 private static final long serialVersionUID = 3813223397371003521L;

 private Image image;
 private int imageWidth;
 private int imageHeight;

 public ImagePanel(String resourceLocation) {
  Image image = ImageUtils.loadImageFromResource(resourceLocation, this);
  setImage(image);
 }

 public ImagePanel(Image image) {
  setImage(image);
 }

 private void setImage(Image image) {
  this.image = image;
  this.imageWidth = this.image.getWidth(null);
  this.imageHeight = this.image.getHeight(null);
  Dimension imageSize = new Dimension(this.imageWidth, this.imageHeight);
  this.setSize(imageSize);
  this.setPreferredSize(imageSize);
 }
 
 @Override
 protected void paintComponent(Graphics gr) {
  super.paintComponent(gr);
  if (this.image != null) {
   gr.drawImage(image, 0, 0, this.imageWidth, this.imageHeight, this);
  }
 }
}

Download the entire example source code (Eclipse project): errordialog.zip.

I hope all this would be helpful to anyone. Enjoy!

Comments (3)

3 Responses to “Java SWING Error Dialog with Exception Details”

  1. Stan Svec says:

    Similar dialog compoment has been already provided by SwingX project.

  2. Pushpendra says:

    for (int i = 0; i & lt; spans.length; i++) {
    textBlockHeight += spans[i];
    }
    offset = (targetSpan – textBlockHeight) / 2;
    for (int i = 0; i < offsets.length; i++) {
    offsets[i] += offset;
    }

    What about variable “It” used in for loop. It’s not resolved and i don’t know how should i declare it.
    Please mail me if you can

  3. nakov says:

    @Pushpendra, “& lt ;” is the HTML entity for “<". The code beatufier is broken and displays this incorrectly. Just replace it with "<". You can also download the entire source code (the errordialog.zip file listed above). --Svetlin

RSS feed for comments on this post. TrackBack URL

LEAVE A COMMENT