S-JIS[2009-04-11/2009-04-22] 変更履歴

ポップアップメニュー(Swing)

Windowsでマウスで右クリックした際に、その場所に出てくるメニューがポップアップメニュー(コンテキストメニュー)。
SwingではJPopupMenuクラスで表示する。


ポップアップトリガー

マウスの右クリックが押されたかどうかを判断するにはマウスのイベントを捕捉すればいい。
しかし ポップアップメニューを出すきっかけは、Windowsではマウスの右ボタンを(押した後に)離した時だが、実行される環境(OS)によっては、押した時に処理されることもあるらしい。
そこで、Javaではポップアップ開始かどうかを判断するメソッドが用意されている。これを、ボタン押下時離された時でそれぞれ呼び出す。

class PopupMenuMsouseListener implements MouseListener {
	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
		mousePopup(e);
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		mousePopup(e);
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}
	private void mousePopup(MouseEvent e) {
		if (e.isPopupTrigger()) {
			// ポップアップメニューを表示する
			JComponent c = (JComponent)e.getSource();
			showPopup(c, e.getX(), e.getY());
			e.consume();
		}
	}

	〜
}

ポップアップメニューの表示

ポップアップメニューはJPopupMenuクラスを使って表示する。
JPopupMenuインスタンスにJMenuやJMenuItemを追加していく。これはメニューバー(JMenuBar)を作るのと同様。
JPopupMenuを初期化したら、show()を使って表示してやる。表示されると、show()からは即座に戻ってくる(戻ってきた時点ではまだメニューは選択されていない)。

import javax.swing.JPopupMenu;
//PopupMenuMsouseListenerの続き

	private void showPopup(JComponent c, int x, int y) {
		JPopupMenu pmenu = new JPopupMenu("ポップアップメニューのテスト"); //環境によってはメニューに表示されるらしい

		pmenu.add("メニュー1");
		pmenu.add("メニュー2");
		pmenu.add("メニュー3");

		pmenu.show(c, x, y);	// ポップアップメニューの表示
	}

show()で指定した座標にポップアップメニューが表示される。(ちなみにsetVisible(true)でも表示できるが、座標は(0,0)になる)
(第1引数のコンポーネントは、メニューに登録したアクションが呼ばれた際に取得することが出来る)

JDK1.5では、JComponent#getPopupLocation()という、ポップアップ表示を行う座標を計算する為のメソッドが新設されている。[2009-04-19]
しかしこのメソッド、常にnullを返す(オーバーライドされている形跡も無い)ので、自分が呼び出す意味は全然無い。
getPopupLocation()の使い道 [2009-04-22]

ポップアップメニューのインスタンスは非表示になった後も使い回せるようなので、毎回作らなくてもいいかも。

	private JPopupMenu pmenu = null;

	private void showPopup(JComponent c, int x, int y) {
		if (pmenu == null) {
			pmenu = new JPopupMenu("ポップアップメニューのテスト");
			〜
		}
		pmenu.show(c, x, y);
	}

JPopupMenu#add(String)でメニューを追加すると、ポップアップメニューにはメニューの名前として表示されるが、それを選択しても何も起こらない(アクションを登録してないんだから当たり前)。


メニューのアクション

JPopupMenuに追加するJMenuItemにアクションを設定しておくと、ポップアップメニューで選択された際にそのアクションが呼ばれる(実行される)。

アクションを指定したJMenuItemインスタンス生成・登録は、以下の様に何通りか考えられる。

import javax.swing.JMenuItem;
		JPopupMenu pmenu = new JPopupMenu();

		JMenuItem mi1 = new JMenuItem(new MyAction());
		pmenu.add(mi1);

		JMenuItem mi2 = new JMenuItem();
		mi2.setAction(new MyAction());
		pmenu.add(mi2);

		JMenuItem mi3 = pmenu.add("メニュー3");
		mi3.setAction(new MyAction());

		/* JMenuItem mi4 = */ pmenu.add(new MyAction());

この例では、MyActionのNAMEが「my-action」なので、ポップアップメニュー上に表示される文言は「my-action」になる。
mi3は文言として「メニュー3」をセットしているように見えるが、後からActionをセットした場合は、そちらのNAMEが有効になる。
したがって、Actionを設定し、かつ文言を自由にしたい場合は、ActionをセットしたにJMenuItem#setText()で文言を設定する。

アクションの例:

class MyAction extends AbstractAction {
	private static final long serialVersionUID = 1L;

	public MyAction() {
		super("my-action");		//メニューに表示される名前
		putValue(MNEMONIC_KEY, (int)'A');	//ニーモニックキー
		putValue(ACCELERATOR_KEY,		//アクセラレーター(ショートカット)キー
			KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_DOWN_MASK)
		);
	}

	@Override
	public void actionPerformed(ActionEvent e) {	//メニューが選択されると、このメソッドが呼ばれる
		System.out.println(e);
	}
}

Actionでは、putValue()によって色々な属性が指定できる。
いくつかの属性では、それを指定しておくことによってポップアップメニューに一緒に表示される。

属性の有無に対して表示される内容の例
  NAMEのみ ニーモニックキーあり アクセラレーターキーあり
表示される内容 my-action my-action my-action CTRL+A
説明 文言のみ表示される。 キーに下線が付く。 右側にキーが表示される。
ポップアップ表示時にこのキーを押すと
直接そのメニューが選択される。

テキストアクション

JTextFieldJTextPane等のテキストエリアのコンポーネント(JTextComponentのサブクラス)は、Ctrl+CやCtrl+Vによるクリップボードのコピー&ペーストはデフォルトで出来るが、ポップアップメニューは用意されていない。
ポップアップメニューはアプリケーションによって内容が変わるから、デフォルトのメニューは用意されていないのだろう。

Ctrl+CやCtrl+Vを処理するアクションはSwingで用意されているので、それをJMenuItemに登録すれば、簡単にポップアップメニューで実現できる。

		JTextField text = new JTextField();
		text.addMouseListener(new PopupMenuMsouseListener());
//PopupMenuMsouseListenerの続き

	protected void showPopup(JComponent c, int x, int y) {
		JPopupMenu pmenu = new JPopupMenu();


		ActionMap am = c.getActionMap();

		Action cut = am.get(DefaultEditorKit.cutAction);
		addMenu(menu, "切り取り(X)", cut, 'X', KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK));

		Action copy = am.get(DefaultEditorKit.copyAction);
		addMenu(menu, "コピー(C)", copy, 'C', KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK));

		Action paste = am.get(DefaultEditorKit.pasteAction);
		addMenu(menu, "貼り付け(V)", paste, 'V', KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_DOWN_MASK));

		Action all = am.get(DefaultEditorKit.selectAllAction);
		addMenu(menu, "すべて選択(A)", all, 'A', KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_DOWN_MASK));


		pmenu.show(c, x, y);
	}

	protected void addMenu(JPopupMenu pmenu, String text, Action action, int mnemonic, KeyStroke ks) {
		if (action != null) {
			JMenuItem mi = pmenu.add(action);
			if (text != null) {
				mi.setText(text);
			}
			if (mnemonic != 0) {
				mi.setMnemonic(mnemonic);
			}
			if (ks != null) {
				mi.setAccelerator(ks);
			}
		}
	}

DefaultEditorKit.cutActioncopyAction等は、「cut-to-clipboard」や「copy-to-clipboard」 「paste-from-clipboard」といった文字列。アクションのNAMEになっている。
各コンポーネントのActionMapやInputMapでこの文字列をキーにしてアクションが登録されているので、逆にそれを使ってアクションを取得できる。
なお、これらのアクションには属性はNAMEしか登録されていないので、その他の属性については自分でセットしてやる必要がある。

文字を太字にする「font-bold」や斜体にする「font-italic」は、それを行うアクションクラスはStyledEditorKitで定義されているが、NAME文字列は定数変数になっていない。
使いたいのであれば自分で直接("font-bold"という形で)文字列を指定するしかない。


上記のコーディングでポップアップメニューを使ったコピー&ペーストが出来るが、複数のテキストエリアを用意してそれぞれにポップアップを出すと、違和感のある動作になる。
すなわち、ポップアップメニューを出した位置にあるテキストエリアに対してではなく、フォーカスのあるテキストエリアに対してコピー&ペーストの処理が為される。

これは何故かというと、コピーやペーストのアクションであるDefaultEditorKit$CutAction等では、渡されたActionEventのgetSource()がテキストエリアでない場合は、フォーカスのあるテキストエリアを対象に処理を行う為。
ポップアップメニュー経由の場合、ソースはJMenuItemなので、フォーカスのあるテキストエリアが対象となる。つまりポップアップメニューの位置とは無関係になる。

一番簡単な解決方法は、ポップアップメニューを表示する際に、対象のテキストエリアにフォーカスを与えてやることだろうか。

	protected void showPopup(JTextComponent text, int x, int y) {
		JPopupMenu pmenu = new JPopupMenu();
		〜

		text.requestFocusInWindow();	//フォーカスを与える
		pmenu.show(text, x, y);
	}

コピー&ペーストのアクション

コピー&ペーストのアクションには、上記のDefaultEditorKitのアクションの他に、TransferHandler内に定義されているアクションもある。(NAMEは「cut」「copy」「paste」)
DefaultEditorKitのアクションはテキストエリア共通のコピー&ペーストなので、JTextComponent系のクラスに対してしか使えない。
TransferHandlerのアクションはその他Swingコンポーネント独自のコピー&ペースト処理を呼び出すのに使われる。

//PopupMenuMsouseListenerの続き

	protected void showPopup(JTextComponent text, int x, int y) {
		JPopupMenu pmenu = new JPopupMenu();


		Action cut = TransferHandler.getCutAction();
		addMenu(pmenu, "切り取り(X)", cut, 'X', KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK));

		Action copy = TransferHandler.getCopyAction();
		addMenu(pmenu, "コピー(C)", copy, 'C', KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK));

		Action paste = TransferHandler.getPasteAction();
		addMenu(pmenu, "貼り付け(V)", paste, 'V', KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_DOWN_MASK));


		pmenu.show(text, x, y);
	}

ただ、このアクションも、ActionEvent#getSource()が対象コンポーネントであることを前提としているので、ポップアップメニューから直接呼び出しても何も処理されない。
(ポップアップメニューではgetSource()はJMenuItemなので、JMenuItemそのものに対するコピー&ペーストになってしまう!)


解決策として、他のアクションへ処理を委譲するアクションを作ってみた。

public class PopupMenuDelegateAction extends AbstractAction {
	private static final long serialVersionUID = -69443095646469128L;

	protected Action action;

	public PopupMenuDelegateAction(Action action) {
		this.action = action;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		Object src = e.getSource();			//JMenuItemを取得
		Component c = getTargetComponent(src);
		if (c != null) {
			ActionEvent ae = new ActionEvent(c, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers());
			action.actionPerformed(ae);
		}
	}

	protected Component getTargetComponent(Object c) {
		while (c != null) {
			if (c instanceof JPopupMenu) {
				JPopupMenu pmenu = (JPopupMenu) c;
				c = pmenu.getInvoker();		//対象コンポーネントを取得
				continue;
			}
			if (c instanceof JMenuItem) {
				JMenuItem mi = (JMenuItem) c;
				c = mi.getParent();		//JPopupMenuを取得
				continue;
			}
			if (c instanceof Component) {
				return (Component) c;
			}
			break;
		}
		return null;
	}

	〜
}

ポップアップメニューのJMenuItemにアクションを設定した場合、ActionEventのソースはJMenuItemになる。(ActionEvent#getSource()で取得できる)
JMenuItemはJPopupMenuに対してadd()しているので、親がJPopupMenuとなる。(JMenuItem#getParent()で取得できる)
(もし同一のアクションをJMenuBarに対しても登録している場合は、親はJMenuBarになるだろう)
JPopupMenu#show()の第1引数で渡されていたコンポーネントは、JPopupMenu#getInvoker()で取得できる。
(もしJMenuを介してサブのメニューを作っていた場合、getInvoker()で取得されるコンポーネントは1階層上のJMenuItemとなるので、さらに繰り返す)
これで対象コンポーネントが取れたので、それを元にイベントを作り直す。(new ActionEvent()。元のActionEventに対してsetSource()でソースだけ変えちゃってもいいかも?)
そして、そのイベントを委譲先のアクションへ受け渡す。

使用例:

//PopupMenuMsouseListenerの続き

	protected void showPopup(JTextComponent text, int x, int y) {
		JPopupMenu pmenu = new JPopupMenu();


		Action cut = new PopupMenuDelegateAction(TransferHandler.getCutAction());
		addMenu(pmenu, "切り取り(X)", cut, 'X', KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK));

		Action copy = new PopupMenuDelegateAction(TransferHandler.getCopyAction());
		addMenu(pmenu, "コピー(C)", copy, 'C', KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_DOWN_MASK));

		Action paste = new PopupMenuDelegateAction(TransferHandler.getPasteAction());
		addMenu(pmenu, "貼り付け(V)", paste, 'V', KeyStroke.getKeyStroke(KeyEvent.VK_V, KeyEvent.CTRL_DOWN_MASK));


		pmenu.show(text, x, y);
	}

PopupMenuDelegateActionの全ソース


しかしまぁ、ここまで変なループを組んだActionを作るくらいだったら、DelegateActionに対象コンポーネントも渡しておいて、素直にそれを使って呼び出す方が早そう(爆)
(対象コンポーネント毎にDelegateActionインスタンスを作る必要が出てきちゃうけど)

public class DelegateAction extends AbstractAction {

	protected Component target;
	protected Action action;

	public DelegateAction(Component target, Action action) {
		this.target = target;
		this.action = action;
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		ActionEvent ae = new ActionEvent(target, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers());
		action.actionPerformed(ae);
	}

	〜
}

コンテキストメニューキー

ポップアップメニューは、基本的にマウス駆動(マウスのキーが押された時や離された時)で表示されるが。
キーボードによっては、アプリケーションキーというものがある。(右下に“プルダウンメニューとマウスカーソル”が描かれたキー。右Ctrlキーの左側や右Altキー/Windowsロゴキーの右側などにある)
Windowsでは、このキーを押した時にもポップアップメニューが表示される。

JDK1.5ではこのキーにVK_CONTEXT_MENUが割り当てられているので、このキーイベントを捕捉する。
なお、キーイベントにはマウスカーソルの位置情報は当然入ってこない為、位置は別途取得する必要がある。

class PopupMenuAction extends AbstractAction implements MouseListener {

	public static String NAME = "context-menu";

	public PopupMenuAction() {
		super(NAME);
	}

	// コンテキストメニューキー押下時の処理
	@Override
	public void actionPerformed(ActionEvent e) {
		JComponent c = (JComponent) e.getSource();
		Point pos = c.getMousePosition();		//マウスの位置を取得(JDK1.5以降)
		if (pos != null) {
			showPopup(c, pos.x, pos.y);		//ポップアップ表示ルーチンはマウス駆動の場合と全く同じ
		}
	}

	// マウスイベント処理以降はPopupMenuMsouseListenerと全く同じ
	〜
}
		JTextField text = new JTextField();

		//マウスリスナーの登録
		PopupMenuAction action = new PopupMenuAction();
		text.addMouseListener(action);

		//キーの割り当て
		InputMap im = text.getInputMap();
		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_CONTEXT_MENU, 0), PopupMenuAction.NAME);
		ActionMap am = text.getActionMap();
		am.put(PopupMenuAction.NAME, action);

ポップアップメニューの登録

JDK1.5では、ポップアップメニュー(インスタンス)をJComponentに登録することが出来る。[2009-04-19]
登録しておけば、上記のポップアップトリガーコンテキストメニューキーのイベント処理を自分でコーディングしなくても、自動的にポップアップメニューを表示してくれる。

		JPopupMenu pmenu = new JPopupMenu();
		pmenu.add(〜);

		JTextField text = new JTextField();
		text.setComponentPopupMenu(pmenu);	//ポップアップメニューの登録

当然の事ながら、ポップアップメニューのインスタンス生成は登録前に一度行うだけ。
各メニューに登録されているアクション(のenabled)がポップアップ表示時点で状況に応じて変化していても、インスタンス生成時点の情報を元にしてメニュー表示の可否が決まる(つまり表示時に変化してくれない)。
この場合、ポップアップメニュー表示直前イベントを拾って、enabledを再設定してやればよい。

		JPopupMenu pmenu = new JPopupMenu();
		pmenu.addPopupMenuListener(new PopupMenuHandler());
class PopupMenuHandler implements PopupMenuListener {

	@Override
	public void popupMenuCanceled(PopupMenuEvent e) {
	}

	@Override
	public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
	}

	/**
	 * ポップアップが表示される前に呼ばれる。<br>
	 * ここで各メニューのenabledを再設定する。
	 */
	@Override
	public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
		JPopupMenu pmenu = (JPopupMenu)e.getSource();
		refresh(pmenu);
	}

	protected void refresh(Component c) {
		if (c instanceof JMenuItem) {
			JMenuItem mi = (JMenuItem) c;
			refreshMenuItem(mi);
		}
		if (c instanceof JComponent) {
			JComponent jc = (JComponent) c;
			for (int i = 0; i < jc.getComponentCount(); i++) {
				refresh(jc.getComponent(i));
			}
		}
	}

	protected void refreshMenuItem(JMenuItem mi) {
		Action a = mi.getAction();
		if (a != null) {
			mi.setEnabled(a.isEnabled());
		}
	}
}

なお、setComponentPopupMenu()によってポップアップメニュー(インスタンス)が変更された場合は、プロパティー変更イベントが発生する。[2009-04-19]

		JTextField text = new JTextField();
		text.addPropertyChangeListener("componentPopupMenu", new PopupChangeHandler());

		text.setComponentPopupMenu(pmenu);	//ポップアップメニューの登録(イベントが発生する)
class PopupChangeHandler implements PropertyChangeListener {

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		System.out.println(evt);
	}
}

JDK1.5では、JComponent#getPopupLocation()というメソッドが新設されている。[2009-04-22]
このメソッドを自分がオーバーライドすることにより、setComponentPopupMenu()に登録されたポップアップメニューが表示される際の座標を変えることが出来る。
BasicLookAndFeelにて、ポップアップメニューの表示位置取得の為に呼ばれる。getPopupLocation()のデフォルトは常にnullを返すようになっているが、nullを返した場合はデフォルトの座標が算出される)


Swing目次へ戻る / Java目次へ戻る / 新機能へ戻る
メールの送信先:ひしだま