• title: JFrameの終了をキャンセル tags: [JFrame, WindowListener] author: aterai pubdate: 2004-08-09

概要

JFrameを閉じる前に、本当に終了してよいか、終了をキャンセルするかなどを確認するダイアログを表示します。

サンプルコード

public static final String ASTERISK_TITLEBAR = "unsaved";
private final JTextArea textarea = new JTextArea();
private final JButton saveButton = new JButton("save");
private final JFrame frame;
private final String title;
public MainPanel(final JFrame frame) {
  super(new BorderLayout());
  this.frame = frame;
  this.title = frame.getTitle();
  frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
  addPropertyChangeListener(new PropertyChangeListener() {
    @Override public void propertyChange(PropertyChangeEvent e) {
      if(ASTERISK_TITLEBAR.equals(e.getPropertyName())) {
        Boolean unsaved = (Boolean)e.getNewValue();
        frame.setTitle((unsaved?"* ":"")+title);
      }
    }
  });
  frame.addWindowListener(new WindowAdapter() {
    @Override public void windowClosing(WindowEvent e) {
      System.out.println("windowClosing");
      maybeExit();
    }
    @Override public void windowClosed(WindowEvent e) {
      System.out.println("windowClosed");
      System.exit(0); // webstart
    }
  });
  textarea.setText("Test Test Test");
  textarea.getDocument().addDocumentListener(new DocumentListener() {
    @Override public void insertUpdate(DocumentEvent e) {
      fireUnsavedFlagChangeEvent(true);
    }
    @Override public void removeUpdate(DocumentEvent e) {
      fireUnsavedFlagChangeEvent(true);
    }
    @Override public void changedUpdate(DocumentEvent e) {}
  });
  saveButton.setEnabled(false);
  saveButton.addActionListener(new ActionListener() {
    @Override public void actionPerformed(ActionEvent ae) {
      System.out.println("Save(dummy)");
      fireUnsavedFlagChangeEvent(false);
    }
  });
  add(new JScrollPane(textarea));
  Box box = Box.createHorizontalBox();
  box.add(Box.createHorizontalGlue());
  box.add(new JButton(new AbstractAction("exit") {
    @Override public void actionPerformed(ActionEvent e) {
      System.out.println("exit button");
      maybeExit();
    }
  }));
  box.add(Box.createHorizontalStrut(5));
  box.add(saveButton);
  add(box, BorderLayout.SOUTH);
  setPreferredSize(new Dimension(320, 240));
}

private void maybeExit() {
  if(title.equals(frame.getTitle())) {
    System.out.println("The document has already been saved,"+
                       " exit without doing anything.");
    frame.dispose();
    return;
  }
  java.awt.Toolkit.getDefaultToolkit().beep();
  Object[] options = { "Save", "Discard", "Cancel" };
  int retValue = JOptionPane.showOptionDialog(frame,
        "<html>Save: Exit & Save Changes<br>"+
           "Discard: Exit & Discard Changes<br>"+
            "Cancel: Continue</html>",
        "Exit Options",
        JOptionPane.YES_NO_CANCEL_OPTION,
        JOptionPane.WARNING_MESSAGE, null, options, options[0]);
  if(retValue==JOptionPane.YES_OPTION) {
    System.out.println("exit");
    //boolean ret = dummyDocumentSaveMethod();
    //if(ret) { //saved and exit
    //  frame.dispose();
    //}else{ //error and cancel exit
    //  return;
    //}
    frame.dispose();
  }else if(retValue==JOptionPane.NO_OPTION) {
    System.out.println("Exit without save");
    frame.dispose();
  }else if(retValue==JOptionPane.CANCEL_OPTION) {
    System.out.println("Cancel exit");
  }
}
private void fireUnsavedFlagChangeEvent(boolean unsaved) {
  if(unsaved) {
    saveButton.setEnabled(true);
    firePropertyChange(ASTERISK_TITLEBAR, Boolean.FALSE, Boolean.TRUE);
  }else{
    saveButton.setEnabled(false);
    firePropertyChange(ASTERISK_TITLEBAR, Boolean.TRUE, Boolean.FALSE);
  }
}
View in GitHub: Java, Kotlin

解説

上記のサンプルでは、アプリケーションの終了時に、ドキュメントが保存されているかどうかで処理を変更するために、ウィンドウイベントを受け取るためのリスナーを設定しています。

  • WindowAdapter#windowClosing(WindowEvent e)
    • システムメニューでウィンドウを閉じようとしたときに呼び出されるリスナーのメソッド
      • OSWindowsなら、Alt+F4キーを押す
      • タイトルバー左上にあるアイコンをクリックし、ポップアップメニューで閉じるを選択
      • タイトルバー右上の×ボタンをクリック
      • JButtonJMenuなどをクリックした時に、対象となるframewindowClosingを呼び出したい場合は、frame.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
    • frame.dispose();では、このメソッドは呼び出されない
  • WindowAdapter#windowClosed(WindowEvent e)
    • frame.dispose()で、ウィンドウがクローズされたときに呼び出されるリスナーのメソッド
    • windowClosingの後、自動的にwindowClosedが呼び出されるのは、WindowConstants.DISPOSE_ON_CLOSEの場合のみ
    • このサンプルでは、Web Startから起動しても終了できるように、frame.dispose()すれば必ず呼び出されるこのメソッド中でSystem.exit(0);を使い、JVMごとシャットダウンしている

JFrame#setDefaultCloseOperationメソッドで、タイトルバー右上の×ボタンをクリック(=デフォルトの終了処理)し、windowClosingが呼ばれた後(このためwindowClosing中で変更しても有効)の動作を設定できます(これらの動作については、JFrame#processWindowEvent(WindowEvent)のソースを参照)。

  • WindowConstants.DO_NOTHING_ON_CLOSE
    • windowClosingが呼ばれた後になにもしない(終了しない)
    • return;と同じ
    • このサンプルでは、WindowConstants.DO_NOTHING_ON_CLOSEを設定しているが、システムメニューでウィンドウを閉じても、下のexitボタンと同じ処理になるように、windowClosingの中で終了処理を行うメソッド(maybeExit())を呼び出し、そこでドキュメントの保存状態によってframe.dispose();を呼んでいる
  • WindowConstants.HIDE_ON_CLOSE
    • windowClosingが呼ばれた後でウィンドウは非表示になる
    • setVisible(false);と同じ
    • 初期値
  • WindowConstants.DISPOSE_ON_CLOSE
    • windowClosingが呼ばれた後でウィンドウは破棄される
    • dispose();と同じ
    • dispose()されるので、この後windowClosedが呼び出される
  • WindowConstants.EXIT_ON_CLOSE
    • windowClosingが呼ばれた後でJVMがシャットダウンれさる
    • System.exit(0);と同じ
    • dispose()されないので、windowClosedは呼び出されない

テキストが変更された場合、タイトル文字列の先頭にアスタリスクを付けることで、保存状態の可視化と保持を行っています。

  • ドキュメントに文字列が追加されたとき、ソース側からfirePropertyChangeなどで、リスナーに変更をイベントで報告
  • リスナー側ではこのイベントを受け、JFrameのタイトルを変更

参考リンク

コメント

  • 私は以前 この終了をキャンセルするかどうかなどを確認するダイアログを作成したことがあります。あなたのソースコードは 参考のかいがあると思います。でも ひとつの問題があるんですけど、textareaに入力した文字列を削除する場合は JFrameのタイトルが変化されていません、どうですか? -- そうがい
    • こんばんは。「123->12345(45追加)->123(45削除)」と追加、削除をして元の状態に戻っても、タイトルが変化しないのは、仕様です。比較のコストが大きくなってしまいそうで嫌なので避けています。 -- aterai
  • 変更をアスタリスクに変更、コードの構成を変更、スクリーンショット更新 -- aterai
  • メモ: シャットダウンフック API の設計 -- aterai