ラベル Swing の投稿を表示しています。 すべての投稿を表示
ラベル Swing の投稿を表示しています。 すべての投稿を表示

2008年3月28日金曜日

Java SwingでUndo/Redoを実装する

Swingのなかにあるjavax.swing.undoというパッケージを使ってUndo/Redoを実装するということなのだが、使い方については"The Java Tutorial"に記載されてはいるものの実態がよくわからなかった。いつものごとく、やっつけでサンプルを作成して、どうなっているかを調べてみた。ちなみにサンプルソースは、どうにもならないほど煩雑になってしまったため、公開はしません。
念のためですが、Undo/Redoというのは、なんだかの操作によって変更された履歴を保存して、一つ前の状態に戻すことがUndo、Undoで戻したものを元の戻すのがRedoということである。

The Java Tutorialでも書かれているが、一般的な実装方法は、以下のような感じになっている。

UndoManagerを生成
これは単純に、UndoManagerをクラスの変数にして、new UndoMnager();を代入すればよい。
UIの生成
これは、Undo/Redoをさせるためのメニューアイテムなどを作っておくということであるが、ドキュメントの例ではAbstractActionをサブクラス化して、メニュに追加しているが、普通にMenuItemを作ってもかまわない。
メニューアイテムが選択されたときの動作(すなわち、ActionListenerのpublic void actionPerformed(ActionEvent e)の実装)は、Undoのときは、UndoManager.undo();でRedoのときは、UndoManager.redo();を呼び出せばよい。
あとは、メニューアイテムは、UndoManager.canUndo();やUndoManager.canRedo();でUndo/Redoが可能かどうか判断して制御する。
Undo/Redo対象となるオブジェクトの監視
これは、Undo/Redoの状態に変化したときのイベントを拾ってくるためのものである。
ここで、変更された履歴のデーターをUndoManagerに登録するのものであるが、同時にUIの制御のためにUndo/Redoが可能かどうかを調べて、UIに反映させる(enable/disableする)。
UndoableEditEventというイベントを受けて取るためのUndoableEditListenerをどこかに実装する。具体的には、
void undoableEditHappened(UndoableEditEvent e) {
    //このundoManagerは、クラスメンバー
    undoManager.addEdit(e.getEdit());
    //このあとUIの制御をする undoAction,redoActionもクラスメンバー
    undoAction.setEnabled(undoManager.canUndo());
    redoAction.setEnabled(undoManager.canRedo());
}
といった感じになる。

といったところが、ドキュメントから読み取れることなのだが、さらにDocumentというものがあるといっている。これらの内容は、どうやらjavax.swing.text.JTextComponentに特化した話のように見える。
ドキュメント内のサンプルコードでは、Undo/Redoの対象となるオブジェクトに対して、doc.addUndoableEditListener(new MyUndoableEditListener());という記述がされているが、"doc"にあたるものは、例えば、JTextAreaの場合、JTextArea.getDocument()から得られるjavax.swing.text.Documentとなっている。getDocument()は、JTextComponentで定義されている。

しかしながら、JTextComponent以外のUIコンポーネントでUndo/Redoをするにはどうすればいいのかというのが疑問となる。おそらく、javax.swing.text.Documentに実装されているUndo/Redo関連の処理を自前でやらないといけないのであろうと考えてみた。
そうすると、javax.swing.undoのパッケージにあるクラスやインタフェースをどう使うのか?ということになる。

それをいろいろやってみた結果、「履歴を吐き出し、読み込む仕組み」「Undo/Redoの状態の変化を通知する仕組み」の2つが必要であるということにたどり着いた。
何か変化があったときにモデルの状態(履歴)をUndoMangerに記憶させておき、Undo/Redoの操作により、UndoManagerから履歴を受け取って、状態を戻すというのが、基本的な動作となる。

履歴を吐き出し、読み込む仕組み
ストレートにいってしまうと、StateEditableインターフェースを実装しているオブジェクトを用意することである。ここでいうオブジェクトは、いわゆるモデルと思ってよい。
void storeState(Hashtable<Object,Object> state)は、そのオブジェクトの状態を送り出すときに呼び出される。パラメタのHashtableに適当なKeyとデータを追加する(putする)。
void restoreState(Hashtable<?,?> state)は、Undo/Redoにより、状態を受け取るときに呼び出される。パラメタのHashtableは、storeStateで渡したものと同じものである(はず)。
Hashtableになっているのは、おそらくオブジェクト内部に複数のデータが存在しているときに、Keyとデータをセットで管理するためであると考えられる。例えば、オブジェクトの中に文字列しかなければ、Keyは"Text"で値はその文字列のStringにすればよいということだろう。Keyはなんでもいいので、"Text"でなくても"String"でもいいでしょう。
Undo/Redoの状態の変化を通知する仕組み
これは、javax.swing.text.Documentに相当するものだが、基本的には、UndoableEditListenerを登録、削除とリスナーへの通知をするオブジェクトである。UndoableEditSupportというクラスを使うと、リスナーの管理などをしてくれるので、これを使ったほうが便利である。
具体的には、モデルの状態を変更してリスナーに通知する方法は、ドキュメントにも記述されている通り、以下のようにすればよい。
//dataはStateEditableを実装しているオブジェクト
StateEdit stateEdit = new StateEdit(data);
//ここでコンポーネントを操作するメソッドを呼ぶ
data.doAnything();
stateEdit.end();
//undoEditSupportは、イベントリスナーを保存している
undoEditSupport.postEdit(stateEdit);
ちなみにこの操作により、前記のvoid storeState(Hashtable<Object,Object> state)が呼び出される。

というわけで、実際の実装では、UIコンポーネントと内部にあるデータ(すなわちモデル)を分離する。そのモデルにStateEditableインターフェースを実装する。UIコンポーネントにUndoableEditListenerを登録する方法を追加する。モデルが変更されたときにリスナーに通知する処理の追加をする(上記のStateEditを使ったコード)。ということになる。

2008年2月28日木曜日

Swingで独自のボタンを作る

Swingでは、UI委譲でLook&Feelが統一されているので、独自のViewを持つ部品を作るのもどうかと思うが、いちおうできるようなのでやってみた。 ボタンといっても、いわゆるプッシュボタン以外も、ラジオボタンとかチェックボックスもボタンなので、クリックなどの操作で状態が変わるUI部品の類は、ボタンということになる。そういう意味では、独自の外観を持つボタンを作りたくなることもあるかもしれない。

Swingには、ボタンの機能が実装されている便利なクラス「AbstractButton」があるので、これを使う。AbstractButtonクラスのドキュメントを読めば、だいたいの作り方はわかるかもしれないが、とりあえず、最低限すべきであろうと思われる処理について書くことにする。

UIの描画はもちろん自前でするわけだが、AbstractButtonクラスを使うとしても、マウスなどの入力処理も書かなくてはならない。プッシュボタンの場合、マウスが押されたときに概観が変わるので当然といえばそうなのだが…

描画の処理
「protected void paintBorder(Graphics g);」を記述する。理由は定かではないが、printComponentなどを使わずにこのメソッドを書く。ボタンの状態によって描画する処理になる。
マウスイベントを拾う処理
MouseListenerインターフェースを実装して、コンストラクタでaddMouseListener(this);を呼び出す。
クリックされたときの処理
public void mouseClicked(MouseEvent e);に処理を書く。マウスボタンの切り分けは、MouseEventのgetButton();で「MouseEvent.BUTTONx」か、getModifiersEx();で「MouseEvent.BUTTONx_DOWN_MASK」で(xは1とか2)判断する。左クリックのときは、BUTTON1でいいのだが、右クリックの場合、マウスボタン数を意識する必要があるかもしれない。 UIの変更があるのであれば、内部的な状態の変更を保持して、repaint();を呼び出して再描画させる。ただし、場合によってはgetGraphics();でGraphicsを取得して、直接描画する必要もあるかもしれない。
ボタン押下の処理
もし、ボタンが押された状態でUIが変化するのであれば、public void mousePressed(MouseEvent e);とpublic void mouseReleased(MouseEvent e);を記述する。場合によっては、public void mouseEntered(MouseEvent e);とpublic void mouseExited(MouseEvent e);も記述する必要があるかもしれない。
キーイベントの処理
ここはでは実装してません(すみません)。たぶん、キー入力をしようとすると、フォーカスを意識しなくてはならないはずなので、処理はちょっと面倒かもしれない。いずれ、フォーカスのねたもわかり次第、どこかで記事を書くかもしれない。

あんまり、意味がないかもしれないが、ここまでのコードは以下のとおりになる。

public class MyButton extends AbstractButton implements MouseListener {
 public MyButton() {
  initialize();
 }
 private void initialize() {
  addMouseListener(this);
 }
 protected void paintBorder(Graphics g) {
  //状態に応じた描画をする
 }
 public void mouseClicked(MouseEvent e) {
  //マウスがクリックされたときの処理
 }
 public void mouseEntered(MouseEvent e) {
  //場合によっては何かをする
 }
 public void mouseExited(MouseEvent e) {
  //場合によっては何かをする
 }
 public void mousePressed(MouseEvent e) {
  //押されたときにUIが変化する場合
 }
 public void mouseReleased(MouseEvent e) {
  //押されたときにUIが変化する場合
 }
}

2008年2月14日木曜日

SwingとAWTの混在はだめなのか?

SwingがいちおうAWTと共存して利用できるようになっているが、だめなパターンがある。どうやら、SwingのJMenuの類(JManuBarとか)とAWTのPanelをいっしょに使うととPanelがJMenuを隠してしまう。

とりあえず、EclipseのVisual Editorを使って、やっつけでサンプルを作ってみた。JFrameにJMenuとJTabbedPaneを置いて、そのうえにJPanelを置いたものである。 上の画像のウィンドウは、メニューはちゃんと表示されていて、これは特に問題ない。

つぎの画像は、JPanelの上にPanelを置いたものである。見た目あまりわかりづらいかもしれないが、メニューのセパレーターの下からが隠れてしまっている。AWTのPanelがメニューの上にかぶさってしまって、隠れて見えなくなっている。JTabbedPaneの青いところの上にメニューがあるようなので、Panelだけがメニューを隠しているようだ。

参考になるコードではないが、折角作ったので載せておくw。

import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;

import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JSeparator;
import javax.swing.JTabbedPane;
import java.awt.GridBagLayout;
import javax.swing.JLabel;
import java.awt.GridBagConstraints;
import java.awt.Label;
import java.awt.Panel;

/**
 * Main.java
 *  Swing and AWT Component test
 * 
 * @author finky
 *
 */
public class Main extends JFrame {

 private static final long serialVersionUID = 1L;

 private JPanel jContentPane = null;

 private JMenuBar frameMenuBar = null;

 private JMenu fileMenu = null;

 private JMenuItem openMenuItem = null;

 private JTabbedPane jTabbedPane = null;

 private JPanel jPanel = null;

 private JLabel jLabel = null;

 private JMenuItem exitMenuItem = null;

 private JPanel jPanel1 = null;

 private Panel panel = null;

 private Label label = null;

 /**
  * This method initializes frameMenuBar 
  *  
  * @return javax.swing.JMenuBar 
  */
 private JMenuBar getFrameMenuBar() {
  if (frameMenuBar == null) {
   frameMenuBar = new JMenuBar();
   frameMenuBar.add(getFileMenu());
  }
  return frameMenuBar;
 }

 /**
  * This method initializes fileMenu 
  *  
  * @return javax.swing.JMenu 
  */
 private JMenu getFileMenu() {
  if (fileMenu == null) {
   fileMenu = new JMenu();
   fileMenu.setText("File");
   fileMenu.setMnemonic(KeyEvent.VK_F);
   fileMenu.add(getOpenMenuItem());
   fileMenu.add(new JSeparator());
   fileMenu.add(getExitMenuItem());
  }
  return fileMenu;
 }

 /**
  * This method initializes openMenuItem 
  *  
  * @return javax.swing.JMenuItem 
  */
 private JMenuItem getOpenMenuItem() {
  if (openMenuItem == null) {
   openMenuItem = new JMenuItem();
   openMenuItem.setText("Open");
   openMenuItem.setMnemonic(KeyEvent.VK_O);
  }
  return openMenuItem;
 }

 /**
  * This method initializes jTabbedPane 
  *  
  * @return javax.swing.JTabbedPane 
  */
 private JTabbedPane getJTabbedPane() {
  if (jTabbedPane == null) {
   jTabbedPane = new JTabbedPane();
   jTabbedPane.addTab("Panel1", null, getJPanel(), null);
   jTabbedPane.addTab("Panel2", null, getJPanel1(), null);
  }
  return jTabbedPane;
 }

 /**
  * This method initializes jPanel 
  *  
  * @return javax.swing.JPanel 
  */
 private JPanel getJPanel() {
  if (jPanel == null) {
   GridBagConstraints gridBagConstraints = new GridBagConstraints();
   gridBagConstraints.gridx = 0;
   gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
   gridBagConstraints.weighty = 1.0;
   gridBagConstraints.weightx = 1.0;
   gridBagConstraints.gridy = 0;
   jLabel = new JLabel();
   jLabel.setText("This is JPanel");
   jPanel = new JPanel();
   jPanel.setLayout(new GridBagLayout());
   jPanel.add(jLabel, gridBagConstraints);
  }
  return jPanel;
 }

 /**
  * This method initializes exitMenuItem 
  *  
  * @return javax.swing.JMenuItem 
  */
 private JMenuItem getExitMenuItem() {
  if (exitMenuItem == null) {
   exitMenuItem = new JMenuItem();
   exitMenuItem.setText("Exit");
   exitMenuItem.setMnemonic(KeyEvent.VK_X);
   exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
    public void actionPerformed(java.awt.event.ActionEvent e) {
     System.out.println("actionPerformed()"); // TODO Auto-generated Event stub actionPerformed()
     dispose();
    }
   });
  }
  return exitMenuItem;
 }

 /**
  * This method initializes jPanel1 
  *  
  * @return javax.swing.JPanel 
  */
 private JPanel getJPanel1() {
  if (jPanel1 == null) {
   GridBagConstraints gridBagConstraints1 = new GridBagConstraints();
   gridBagConstraints1.gridx = 0;
   gridBagConstraints1.fill = GridBagConstraints.BOTH;
   gridBagConstraints1.weightx = 1.0;
   gridBagConstraints1.weighty = 1.0;
   gridBagConstraints1.gridy = 0;
   jPanel1 = new JPanel();
   jPanel1.setLayout(new GridBagLayout());
   jPanel1.add(getPanel(), gridBagConstraints1);
  }
  return jPanel1;
 }

 /**
  * This method initializes panel 
  *  This panel hides Menus.
  * @return java.awt.Panel 
  */
 private Panel getPanel() {
  if (panel == null) {
   GridBagConstraints gridBagConstraints2 = new GridBagConstraints();
   gridBagConstraints2.weightx = 1.0;
   gridBagConstraints2.anchor = GridBagConstraints.NORTHWEST;
   gridBagConstraints2.weighty = 1.0;
   label = new Label();
   label.setText("This is Panel on JPanel");
   panel = new Panel();
   panel.setLayout(new GridBagLayout());
   panel.add(label, gridBagConstraints2);
  }
  return panel;
 }

 /**
  * @param args
  */
 public static void main(String[] args) {
  // TODO Auto-generated method stub
  SwingUtilities.invokeLater(new Runnable() {
   public void run() {
    Main thisClass = new Main();
    thisClass.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    thisClass.setVisible(true);
   }
  });
 }

 /**
  * This is the default constructor
  */
 public Main() {
  super();
  initialize();
 }

 /**
  * This method initializes this
  * 
  * @return void
  */
 private void initialize() {
  this.setSize(300, 200);
  this.setJMenuBar(getFrameMenuBar());
  this.setContentPane(getJContentPane());
  this.setTitle("Swing Test");
 }

 /**
  * This method initializes jContentPane
  * 
  * @return javax.swing.JPanel
  */
 private JPanel getJContentPane() {
  if (jContentPane == null) {
   jContentPane = new JPanel();
   jContentPane.setLayout(new BorderLayout());
   jContentPane.add(getJTabbedPane(), BorderLayout.CENTER);
  }
  return jContentPane;
 }

}