TITLE:JFrameの終了をキャンセル

JFrameの終了をキャンセル

Posted by terai at 2004-08-09

概要

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

  • &jnlp;
  • &jar;
  • &zip;

#screenshot

サンプルコード

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() {
    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() {
    public void insertUpdate(DocumentEvent e) {
      fireUnsavedFlagChangeEvent(true);
    }
    public void removeUpdate(DocumentEvent e) {
      fireUnsavedFlagChangeEvent(true);
    }
    public void changedUpdate(DocumentEvent e) {}
  });
  saveButton.setEnabled(false);
  saveButton.addActionListener(new ActionListener() {
    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") {
    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);
  }
}

解説

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

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

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

  • 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削除)」と追加、削除をして元の状態に戻っても、タイトルが変化しないのは、仕様です。比較のコストが大きくなってしまいそうで嫌なので避けています。 -- terai
  • 変更をアスタリスクに変更、コードの構成を変更、スクリーンショット更新 -- terai