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

2008年4月1日火曜日

EclipseとMinGWでJNIモジュールを開発する

ずいぶん前の記事で、JNIモジュールをEclipseでビルドやデバッグする方法を書いたのだが、久しぶりにJNIモジュールを作ってみようとしたら、同じようにはまってしまったw。今回は、はまったついでに、手順についてまとめることにする

前提としては、WindowsでEclipseを使って、JNIモジュールを作るということで、コンパイラはMinGWのG++を使う。ネイティブ側はC++で書くことにする(Cでもそれほど違いはないはずだが)。
あとは、JDKドキュメントにあるJava Native Interface 仕様とMinGWのFAQぐらいは読んでいるという前提になる。EclipseはJavaとC/C++の開発環境(CDT)の両方がインストールされているものとする。

プロジェクトの作成

まずは、Javaのプロジェクトを作成する。Javaの方はJDKのドキュメントにあるとおり、nativeなメソッドを持つクラスを作成し、static{}の中でSysytem.LoadLibrary();を記述する。LoadLibraryの引数は、パッケージがある場合、"package_class"のように"."(ドット)を"_"(アンダースコア)に置き換えた文字列にする。
例)myproject.testというパッケージでクラスがMyclassの場合、"myproject_test_Myclass"となる

C++のプロジェクトを作成するのだが、そのときにプロジェクトタイプは、"Shared Library"を指定する。蛇足かもしれないが、C++でJNIのAPIを呼び出すときには、JNIEnvのオブジェクトのメンバー関数を呼ぶ形になる。例えば、JNIのAPIがvoid hoge(JNIEnv* env, jstring str);という風にドキュメントに書いてあったら、C++では、env->hoge(str);という風に呼び出す。
次にビルドの設定で、出力ファイルはLoadLibrary();で設定した文字列に拡張子".dll"が付くファイルが出力されるように指定する。makeコマンドの設定のところで、MinGWであれば、デフォルトを外して"mingw32-make -k"とする。
ツールの設定(g++などの設定)のところを設定する、C++のコンパイルの設定で、プリプロセッサ(-D)のところに"_JNI_IMPLEMENTATION_"というdefineを追加する。あと、インクルードパス(-I)にJDKにあるincludeとinclude/win32の二つを設定する。もし、JDK_HOMEとかの環境変数が設定されていれば"${JDK_HOME}\include"と"${JDK_HOME}\include\win32"となる。この手の環境変数を設定したいならば、環境変数のところで設定できる。
次にリンカの設定のところで、"-Wl,--kill-at"というオプションを設定するのであるが、適当なところがなさそうなので、コマンドラインのところで"g++"のあとにスペースを空けて設定する。リンクするライブラリ(-l)のところには、"jvm"を設定し、ライブラリの検索パスのところには、JDKにあるlibを設定する。JDK_HOMEを設定していれば"${JDK_HOME}\lib"となる。

javahを実行する設定

Javaのソースでnativeを含むクラスファイルをjavahコマンドに渡すと、JNIようのCのヘッダファイルを出力するのだが、javahをどうやって起動するのか?ということです。一度javahを実行すれば、ファイルが出来上がるので、DOS窓でコマンドを打ってもいいわけですが、きっと何度かコマンドを実行するハメになるので、どこかに書いておいて、簡単に実行できるようにしておきたい、と考えてみた。
その方法はいくつかあるが、とりあえず思いついたのは

  • Javaのプロジェクトにantを書いて、手で実行する
  • Cのビルド設定のステップビルドのところで、ビルド前のコマンドのところでjavahを書いて、ビルド時に実行する
  • ビルド前コマンドのころで、javahを起動するmakefileを書いて、ビルド時にmakeを実行する

といったところだが、どれか楽そうなのを選べばよいと思う。ちなみにantは、antを実行するjavaコマンドがJDKのものでないと、javahタスクは実行できないもよう。あと、ビルド前コマンドは、ビルドを実行するときに毎回起動されるので、場合によってはうざいかもしれない。少なくとも自動ビルド向きではない。
makefileを書いて、DOS窓から手動実行するぐらいが無難なのかもしれない…。

Javaの実行設定

nativeを含むクラスを読み込むと、LoadLibrary();でDLLをロードする仕組みなので、EclipseでJavaの実行を設定して、実行(Run)すればよい。ここでの問題は、DLLをロードしようとしたときにjava.library.pathというシステムプロパティに登録してあるパスの中から、DLLを探し出して、ロードするという仕組みになっているので、それらのパスに含まれてないと、実行時に失敗するということである。これを解決する方法もいくつか考えられる。

  • java.library.pathにあるパスにDLLをコピーする
  • 実行時にjava.library.pathをDLLが存在するパスに設定する(起動時オプションのVMの設定で"-D java.library.path=…"と書く)
  • 実行時にPATH変数(SolarisとかはLD_LIBRARY_PATH)にDLLが存在するパスを追加する

とりあえず、ここではデバッグをすることを目標とすることにして、実行時にビルドされたデバッグようのDLLのパスをPATH変数を追加するという方法をとることにする。
具体的には、実行時の環境変数の設定で、"PATH"と変数を追加して、値を「${workspace_loc:<Cのプロジェクト名>/Debug}」とする。このとき、環境変数は追加するの方にしておいたほうがよい。

この段階で、Javaプロジェクトのほうが実行できなければ、デバッグもできないので、普通に実行できるようにしておく。

JNIモジュールのデバッグ

実行(Run)で動いていることが前提として、デバッグをするのだが、あとはC側のデバッグの設定をする。
アタッチをするデバッグを定義して、アプリケーションのところには、Debugフォルダに出力されたDLLを設定する(普通は"Debug/<LoadLibrary();に設定した文字列>.DLL")。これで、設定は完了。
デバッグの手順は以下の通り。

  • デバッグしたいCのソースにブレイクポイントを貼る
  • JavaのLoadLibrary()が実行された後でかつ、デバッグしたいCの関数が呼び出される前のどこかにブレイクポイントを貼る
  • Javaをデバッグ実行する
  • Javaのブレイクポイントで止まったら、Cをデバッグ実行する(アタッチをする)。アタッチコマンドは"javaw"であるが、Eclipseも"javaw"で動いているので、間違えないこと。タスクマネージャをみて、EclipseのPIDを確認しておいたほうがよいかも。
  • Cの実行が止まるので、Cのモジュール(gdb/mi)を選んで、再開させて実行中(Running)の状態にする
  • Javaのモジュールを選んで、ブレイクポイントで止まっているJavaを再開させる
  • Cのほうがブレイクポイントで止まる

と、いった具合でちょっと面倒だがこういった感じになる。
JavaのブレイクポイントのところでLoadLibrary()のあと…といっているところは、LoadLibrary();のところにブレイクポイントを貼って、LoadLibrary()をステップ実行させるという方法が無難かもしれない(手順は増えてさらに面倒になるけども)。
あと、アタッチのあとで、コンソールにエラーのような文字列が現れるが、そのまま、実行を再開させても問題ないもよう。場合によっては、問題が生じるかもしれないが。
アタッチ直後にCのデバッグが停止したり、エラーを出力するのは、g++、gdbのバージョンなどで変わる可能性がある。

ということで、なんとかEclipseとMinGWでJNIのモジュールが開発できるようになった。上記にも述べたが、MinGWのバージョン(g++やgdbなど)によって、状況は変わる可能性がある。ここではMinGWは5.1.3を使っている。

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年3月23日日曜日

JavaのRegexのパターンマッチがうまくいかない?

明らかに小ネタな話であるが、PHPとかのつもりでJavaのRegexでパターンマッチさせると、マッチしないと勘違いするかもしれないという話。
Javaで単純にRegexに文字列がマッチしているか否かを知るには、以下のようなコードを使う。

  • java.lang.String str = "…";
    str.matches(regex);
  • java.util.regex.Pattern.matches(regex, str);
  • java.util.regex.Pattern pattern = java.util.regex.Pattern.complie(regex);
    java.util.regex.Matcher matcher = pattern.matcher(str);
    matcher.matches();

いずれにしろ、java.util.regex.Patternがregexで記述されたパターンを処理している。

ところで、PHPでパターンマッチさせるときには、だいたい、PCRE(Perl互換正規表現)を使うわけだが、そこで単純なパターンマッチは、pcre_match(pattern, str);となる。
例えば、文字列の先頭が"prefx"という文字列で始まっているかどうかを知るには、PHPではpcre_match('/^prefix/', str) > 0と書く。

そこで、同じ感じでJavaで書くと、java.util.regex.Pattern.matches("^prefix", str);と書きたくなる。しかし、こう書いてしまうとうまくマッチしてくれないのである。
それは、Regexでは、パターンに完全にマッチしてないとだめなのである。

すなわち、Javaではjava.util.regex.Pattern.matches("^prefix.*", str);と書いてあげないとマッチしないということになる。

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;
 }

}

2008年2月12日火曜日

ResourceBundleを使って国際化する

UIに表示される文字列をソースコードに書き込む(いわゆるハードコーディング)をするのではなく、ソースコードとは別なところに定義しておいて、プログラム的に読み込む方法の話です。 Windowsならリソースを定義するわけなのだが、Javaではどうするのかということです。Javaの場合、Cocoaのそれと同じ感じで、ロケールごとに文字列を定義する方法をとります。

まずは、プログラム側ですが、ResourceBundleを使って表示しようとしているロケールの定義を呼び出します。そこから、定義されている文字列を取得する。

ResourceBundle bundle = ResourceBundle.getBundle(<リソース名>);
bundle.getString(<キー文字列>);

ここで、<リソース名>といっているのは、リソースを定義するファイル名だが、デフォルトは「<リソース名>.properties」というファイル名になり、言語ごとの定義は「<リソース名>_<ロケール名>.properties」となる。ちなみに日本語の場合、「<リソース名>_ja.properties」でよいだろう(「ja」でよいかどうか心配なら、JDKドキュメントの国際化とのころを参照されたし)。あと、パッケージの中に定義ファイルを置くときは、「<パッケージ名>.<リソース名>_<ロケール名>.properties」とすればよい。

リソースの定義ファイルの中身は「<キー文字列>=<リソース文字列>」となる。例えばこんな感じ。

FileMenuItem=ファイル(F)
EditMenuItem=編集(E)

ただし、このファイルを作成するに当たり、気をつけないといけないことは、コード系を「ISO-8859-1」(すなわち、ASCII)で保存しなければならない。日本語のリソースの場合どうすればいいかというと、Unicodeエスケープ(\udddd 表記)で保存する。エスケープされた文字列を手で打ち込んでもかまわないが、あまりにも面倒なので日本語として可視なファイルから変換したほうがよさそうだと思う。

そこで、どうするかというと「native2ascii」というツールがJDKにあるのでこれを使って変換する。

Windowsの場合だと、Windowsネイティブ(Shift_JIS)で書いておいて、ASCIIコードに変換するという手順になるだろう。例としてのコマンド実行は以下にような感じ

> native2ascii -encoding=ShiftJIS myBundle_ja.sjis myBundle_ja.properties

もちろん、この方法でコマンド実行すればよいのだが、いわゆるバッチファイルを使ったり、makeファイルを書いたほうが便利だと思う。ここでは、Eclipseを使っているとして、折角なのでAntで書いてみる。
ちなみにターゲットとなるpropertyiesファイルと同じ名前で、どこかのフォルダに入れておいて、そのフォルダのパスを"src"に設定する。

<project name="MyProject" default="bundle">
<target name="bundle">
<native2ascii src="<入力パス>" dest="<出力パス>" encoding="Shift_JIS" includes="**/*.properties" />
</target>
</project>

というような内容のファイルを「build.xml」という名前で保存して、実行する(パッケージエクスプローラーで「build.xml」のコンテキストメニューからRun(実行)をメニューから「Ant」を選択)。

2007年7月23日月曜日

JAVAでファイルのSHA-1を計算する

ダウンロードしたファイルが正しいかチェックするためにSHA-1のチェックサムを利用していることはよくあることですが、WindowsでSHA-1とかのダイジェストを得るためにはフリーウェアとかを使ったりします。その手のツールは便利でいいのですが、インストールするのが気が引けたりするので、そのくらいならJAVAでさくっとできないものかと思って、とりあえず作ってみました。

とりあえずなので、基本的なコンセプトは以下のような感じ。

Windowsなので結果をウィンドウで表示する
いわゆるメッセージボックスのようなもので計算結果を表示する。
機能は限りなくシンプル
こういうツールを作ろうとすると、いろいろ便利にしたくなるのですが、とりあえずは簡単なものにする。
エラーぐらいは表示
エラーが出ても突っ走って、何も表示されないと悲しいので、エラーメッセージぐらいは表示しようということ。

とりあえず、ソースコードはこちら。ちなみにソースコメントは入れてないです。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import javax.swing.JOptionPane;

public class FileDigest {
 protected static int READ_SIZE = 1024;
 protected static String DIGEST_ALGORITHM = "SHA-1";
 protected static String DIALOG_TITLE = "File Digest Tool";
 protected MessageDigest messageDigest;
 public static void main(String[] args) {
  if (args.length == 0) {
   messageOut("No taget file.", true);
   System.exit(1);
  }
  FileDigest fileDigest = new FileDigest();
  try {
   fileDigest.setFile(args[0]);
  } catch (FileNotFoundException e) {
   messageOut(String.format("File not found (%s).", args[0]), true);
   System.exit(2);
  } catch (IOException e) {
   messageOut("File read error.", true);
   System.exit(3);
  }
  messageOut(fileDigest.digestString(), false);
 }
 public FileDigest() {
  try {
   messageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
  } catch (NoSuchAlgorithmException e) {
   System.exit(4);
  }
 }
 public void setFile(String pathName) throws FileNotFoundException, IOException {
  FileInputStream inStream = new FileInputStream(pathName);
  DigestInputStream digestStream = new DigestInputStream(inStream, messageDigest);
  byte readBuffer[] = new byte[READ_SIZE];
  while (digestStream.read(readBuffer) > 0);
 }
 public String digestString() {
  byte codeList[] = messageDigest.digest();
  String digest = new String();
  for (byte code : codeList) {
   digest += String.format("%x", code);
  }
  return digest;
 }
 protected static void messageOut(String message, boolean isError) {
   JOptionPane.showMessageDialog(
     null, message, DIALOG_TITLE, 
     isError ? JOptionPane.ERROR_MESSAGE : JOptionPane.INFORMATION_MESSAGE);  
 }
}
あえてどう使うかという説明をするほどのものではないとは思っているのですが、デスクトップにショートカットをおいて、ドラッグ&ドロップで結果がわかるといいかと思ったので、そうするには以下のようにしてみてください。
ソースをコンパイルして.classファイルを作成
その方法は説明するまではないと思いますが、普通にやれば「FileDigest.class」というファイルができるはず。
デスクトップにショートカットを作成
explorerで右クリックして、「送る」のサブメニューのデスクトップを選択する。
できあがったショートカットのプロパティを表示
リンク先の変更
このときリンク先は、FileDigest.classのフルパスになっているので、「javaw FileDigest」(FileDigest.classではない)に変更する。 作業フォルダはそのままにしておく(CLASSPATHの代わり)。

これで、ショートカットから、ツールが起動できるはず。うまくいけば、アイコンがJAVAのものに変わる。 ちなみに単にそのショートカットで起動すると「No target file.」と書いてあるメッセージボックスが表示されます。ダイジェストを計算したいときは、explorerからこのショートカットへドラッグ&ドロップすると、計算結果がメッセージボックスで表示されます。

2007年4月25日水曜日

JavaでWAVファイルを再生する(2)

前回のJavaでWAVファイルを再生するでは、あまりにもとりあえず過ぎたので、ちょっとはましなコードにしてみました。今回は、再生を一時停止しても読み込んだデータをスキップせずにちゃんと出力するようにしました。なお、LineListenerは今回ははずしました。必要があれば、前回のソースを参照してください。

public class RenderAudioFile implements Runnable {
protected AudioInputStream fileStream = null;
protected SourceDataLine sourceDataLine = null;
private byte sampleBuffer[]; //データ受け渡し用のバッファ
private ByteBuffer sampleByteBuffer; //バッファ参照用のByteBuffer

public void openFile(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
 //出力するファイルを受け取るメソッド
 fileStream = AudioSystem.getAudioInputStream(file);
 AudioFormat format = fileStream.getFormat();
 sourceDataLine = AudioSystem.getSourceDataLine(format);
 sourceDataLine.addLineListener(this);
 sourceDataLine.open(format);
 sampleBuffer = new byte[sourceDataLine.getBufferSize()];
 sampleByteBuffer.wrap(sampleBuffer);
}
public void close() {
 //ファイルを閉じるメソッド
 fileStream.close();
 fileStream = null;
}
public void play() {
 // 再生開始、再開用メソッド
 if(sourceDataLine == null || fileStream == null) {
  return;
 }
 sourceDataLine.start();
 new Thread(this).start();
}
public void pause() {
 // 一時停止メソッド
 if(sourceDataLine == null || fileStream == null) {
  return;
 }
 sourceDataLine.stop();
}
public void run() {
 //再生スレッドのメソッド
 do {
  if (sampleByteBuffer.position() == 0) {
   try {
    int readSize = inputStream.read(sampleBuffer);
    if (readSize <= 0) {       sourceDataLine.stop();       break;      }      sampleByteBuffer.position(readSize);     } catch (IOException e) {      sourceDataLine.stop();      break;     }    }    soundByteBuffer.flip();    int writeSize = dataLine.write(sampleBuffer, 0, sampleByteBuffer.remaining());    if (writeSize >0) {
   putSampleData(sampleBuffer, writeSize);
   sampleByteBuffer.position(writeSize);
  }
  sampleByteBuffer.compact();
 } while (sourceDataLine.isRunning());
}
}

とりあえず、普通に再生するには問題ないのですが、まだ、手を抜いてあるところがあります。

  • ファイルをすべて再生しきったときの処理
  • 再生位置の変更

いずれも、自由に再生を開始する位置を変更できればよいのですが、これらを実現するために使えそうなメソッドとして、AudioInputStream.mark()とAudioInputStream.reset()というのがあります。これは、mark()で再生位置を保存しておいて、reset()でその位置に移動するというものです。 ただし、これらのメソッドはInputStreamによって、使えたり使えなかったりしますが、それを調べるためにAudioInputStream.markSupported()というメソッドがあります。再生しようとするファイルや環境に依存するかもしれませんが作ったプログラムでは、markSupported()はfalseを返してきます。すなわち、mark(),reset()が使えないということです。

とすると、どうすればいいかというと、再生位置を先頭に戻すには、一度close()してから再度open()するという処理しかなさそうです。さらに任意の位置に移動させるには、skip()を使うという方法しかないようです。

2007年4月18日水曜日

EclipseでJNIをデバッグする

前回、EclipseでJNIの開発について書いたが、その後デバッグをするにはどうするのかというのをゴリゴリとやっていたが、例のごとくはまってしまってので、忘れないように書いておくことにする。 まずは、JNIモジュールのデバッグは次のような基本手順を踏むということする。

  1. JNIのソースにブレイクポイントを設定
  2. 呼び出すJavaソースにブレイクポイントを設定
  3. Javaをデバッグ実行
  4. JNIモジュールをデバッガでアタッチ

まずはJavaのブレイクポイントは、System.LoadLibrary();を呼び出した後でかつJNIのブレイクポイントが呼び出される前に設定する。Javaのデバッグ実行でこの設定したブレイクポイントで止まった状態で、JNIモジュールをアタッチする。

JNIモジュールのデバッグ設定は、Eclipseの「構成およびデバッグ」のダイアログで、「ローカルアプリケーションへの接続」という構成で設定できる。このとき、アタッチの対象となるモジュール(ここではWin32なのでDLL)を「メイン」のC/C++アプリケーションというところに書くのだが、ここにはビルドしたモジュールを設定する(デフォルトだと「デバッグ/<DLLのファイル名>」)。

あと、「デバッガー」の項目のところは、「gdb/mi」を選択して、gdbのコマンドのところは、実行できる適切なパスが設定されていればよい。あと、このときに「詳細コンソールモード」というチェックをONにしておくと、デバッガーが何をしているのかわかるので最初のうちはONにしておいたほうがよいだろう。

この設定で指定したモジュール(DLL)がJavaから呼び出されないとアタッチできないので、Javaのデバッグ構成の管理で、引数のところでプログラムの引数かVMの引数のところで、「-Djava.library.path=<デバッグするモジュールのパス>」を設定をする。

これでEclipseからの操作でデバッグができる。DLLのデバッグで、gdbのアタッチ後に中断状態になるが、そのまま再開すればモジュールは実行される。

あとは、MinGWの固有だと思われるが、gdbのバージョンが古いとEclipseから投げるMIのコマンドの不一致か、ブレイクポイントがうまく設定されないようだった。そのため、MinGWからgdb6.xをダウンロードしてインストールすればよいようだ。

もうひとつ、これはうちの固有の問題かもしれないが、gdbでデバッグしようとすると「LdrAccessResource」のところでセグメンテイションエラーが発生して、gdbが落ちてしまう。これについては、いろいろ調べているものの解決方法がわからず、現状デバッグ不能である。どうやら、ntdllとgdbの相性の問題のようで詳細はわかりませんでした。

2007年4月12日木曜日

EclipseでJNIモジュールの開発をする

プログラミングTIPSではないのですが、EclipseでJNIのモジュールを開発を試みたときにプロジェクトをどう設定すればよいかという話です。要するにちょっと設定にはまったので、忘れないようにメモっておこうということです。ただし、以下の方法はベストな方法とは思ってません。とりあえず、こうやったらできましたっていう程度のものだと思ってください。ちなみにですが、Win32でEclipseとMinGWを使って開発するというお話しです(他のプラットホームでは違うところがあるかもしれません)。

蛇足かもしれませんが、JNIの開発そのものは、JDKのドキュメント(Java5のJDKのリンクですが)に載っていて、MinGWでJNIのモジュールをコンパイルするにはどうするかというのは、MinGWのFAQに書いてあります。

まずは、JNIのモジュールを開発するには、nativeの宣言を含むJavaのソースをまず書いてコンパイルする。classファイルからjavahでC(++)のヘッダーを出力して、C(++)のコードを書いてコンパイルする。出来上がったモジュールをJavaから実行させる、という手順であると思っていることが前提となります。

何が話題かというと、JavaのモジュールはJavaのプロジェクトで開発して、JNIのモジュールはCDTを使って、C(++)のプロジェクトで開発するのですが、javahで作られるヘッダーをどうやって、C(++)のプロジェクトへもってくるか、できたモジュールをどこにコピーして、実行するかということです。

C(++)のプロジェクトのほうがビルドの設定の自由度が高いと踏んで、C(++)のプロジェクト側でがんばるということにします。依存関係としては、C(++)のプロジェクトがJavaのプロジェクトを参照しているというふうにします(お互いに参照していてもよいとは思います)。

C(++)のプロジェクトの設定で「C/C++ビルド」という項目の中に、ビルドステップというのあって、ここにビルドの前後に何かコマンドが実行できるので、これを利用することにします。 まずは、javahをビルド前に実行するためにビルド前のステップのところに以下のように入力します。

<JDKのフルパス>\bin\javah -jni -d .. -classpath ../../<Javaのプロジェクト名> <パッケージ名>.<クラス名>

※もし、「\」がバックスラッシュに見えたら、「¥」の半角だと思ってください このとき、 「<パッケージ名>.<クラス名>」は、複数記述することができます。

これで、JavaのプロジェクトからJNIのモジュールを読み込んで実行できるわけです(もちろん、何かバグってたらだめですが…)。デバッグや実行もEclipseからやりたいので、「構成および実行」の設定をして、Javaを実行させればよいのですが、JNIのモジュールがjava.library.pathにないと実行できません。 そのときにどうするかというと、JNIモジュールを読み込むjavahを実行したJavaプロジェクトとは別に、これらを使うJavaプロジェクトを作成します。 この新しいほうのプロジェクトのビルドの構成の「プロジェクト(P)」でJNIのJavaプロジェクトを参照するように設定すれば、実行できます。この設定に「ネイティブ・ライブラリーのロケーション」というサブツリーがあると思いますが、ここでJNIのネイティブモジュール(Win32だとDLL)のパスを設定すればよいです。

リリースしたときにjava.library.pathの設定をどうするのかというところは気になるところですが、VMの実行時にパラメタで渡さなくても、いわゆるpath変数で見つけられるところにモジュールがあれば実行できるようです。

2007年4月10日火曜日

JavaでWAVファイルを再生する

javax.sound.sampledというパッケージを使って、とりあえずWAVファイル(AIFFなども可能)をサウンドデバイスに出力する。とりあえずということで、前提と条件は以下の通り

  • デフォルトのデバイスに出力する
  • 制御は、開始、一時停止、再開だけ

とりあえず、いきなりソースコード。

public class RenderAudioFile implements LineListener, Runnable {
 protected AudioInputStream fileStream = null;
 protected SourceDataLine sourceDataLine = null;
 private byte sampleBuffer[];//データ受け渡し用のバッファ

 public void openFile(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException {
  //出力するファイルを受け取るメソッド
  fileStream = AudioSystem.getAudioInputStream(file);
  AudioFormat format = fileStream.getFormat();
  sourceDataLine = AudioSystem.getSourceDataLine(format);
  sourceDataLine.addLineListener(this);
  sourceDataLine.open(format);
  sampleBuffer = new byte[sourceDataLine.getBufferSize()];
 }
 public void play() {
  // 再生開始、再開用メソッド
  if(sourceDataLine == null) {
   return;
  }
  sourceDataLine.start();
  new Thread(this).start();
 }
 public void pause() {
  // 一時停止メソッド
  if(sourceDataLine == null) {
   return;
  }
  sourceDataLine.stop();
 }
 public void update(LineEvent ev) {
  // LineEvent用ハンドラ UI制御用に使うとよい
  LineEvent.Type type = ev.getType();
  if( type == LineEvent.Type.OPEN) {
  }
  else if( type == LineEvent.Type.START) {
  }
  else if( type == LineEvent.Type.STOP) {
  }
 }
 public void run() {
  //再生用のスレッド用メソッド
  int readSize = 0;
  do {
   try {
    readSize = fileStream.read(sampleBuffer);
   } catch (IOException e) {
    sourceDataLine.stop();
    break;
   }
   if (readSize < 0) {
    sourceDataLine.stop();
    break;
   }
   }
   sourceDataLine.write(sampleBuffer, 0, readSize);
  } while (sourceDataLine.isRunning());
 }
}

とりあえず、こんな感じです。大したコードでなくて申し訳ないですが、いくつかポイントがあるので、説明させていただきますと。

public void update(LineEvent ev);ですが、いらないときはメソッドごと消してもらって、implementsからLineListenerを消して、、sourceDataLine.addLineListener(this);も消してください。あと、このイベントハンドラーで「START」がくるタイミングですが、sourceDataLine.start();を呼び出した時には呼ばれずに、sourceDataLine.write();でデーターを書き込んだときに呼ばれるようです。UIの制御にこれを使ったほうがよいかというと、startやstopの処理が完了するとこれらのイベントが呼ばれてくるので、連打されたりしたときにおかしなことにならないようにこのタイミングで制御したほうがよいかと思われるからです。

あと、とりあえずで手を抜いてしまってあるところがあるので、白状しておきますが。sourceDataLine.write();はstopが呼ばれると書き込みのブロッキングを解除して復帰してくるのですが、このときの戻り値に書き込みされたデーターのサイズとなります。この戻り値が渡したバッファーのサイズより小さいときには、その差分のデーターは書き込みされなかったわけで、本当なら再開されたときにその分のデーターを書き込んでから、ファイルから読み込むという処理をしないと、そのデーターはスキップされて再生されるということになります。

これを回避するには、この戻り値をメンバー変数にとっておいて、再度スレッドが呼ばれたときにファイルから読み込まず、残りのデーターを渡すという処理を入れればよいでしょう。

2007年4月8日日曜日

Java3Dをやってみる

Java3Dは、SunからJREと同じように配布されているライブラリであるが、JREやJDKには含まれてはいない。どうやら、大きすぎていっしょにいれなかったようだ。 まずプログラムするには、Java 3D APIへいって、APIをダウンロードしてインストールするわけだが、JDKのバージョンもあわしておく必要があるようなので、ついでに最新のJDKを入れておくのがよいかもしれない。APIドキュメントもダウンロードできるが、オンラインでも見れるので、お好みで入れればいいかも。

APIドキュメントだけでは、途方にくれるだけなのでjava.netのJava3DのWikiにあるJava3DBooksにチュートリアル(英語)があるのでそれを読むか、日本語の書籍も出ているようなのでそれを読めばよいのだが…。

とりあえず、何か動かしたいのであれば、チュートリアルのChapter1に載っているソース(HelloJava3D)を打つか、Java3DにおいてあるExampleを動かしてみるとかでよいかもしれない。

とりあえず、おそらくこの段階でわかっておくことは、Java3Dのすべてのオブジェクトはツリー構造で定義される。3次元オブジェクトをツリー構造で記述することは、普通なことなのであえて説明は不要であるとおもうが。 ツリー構造の根っこにあるのが、VirtualUniverseというものであるのだが、そこに3次元のオブジェクト(ドキュメントではSceneGraphといってる)やそれらを表示するためのCanvas3Dも含まれている。

3次元で表示するためには視線(投射法の概念も含むのでカメラというようだが)を定義するためのViewやViewPlatformなどが必要なのだが、VirtualUniverseやViewとかをひっくるめてSimpleUniverseがやってくれるようだ。 すなわち、表示するためのCanvas3Dを作って、それを渡してSimpleUniverseを作成する。あとは、3次元オブジェクトの類をSimpleUniverseに渡すとCanvas3Dに表示される(もちろん、視線に入るものだけだが)ということである。

オブジェクトの定義は、ドキュメントのHelloJava3DだとColorCubeというものを表示しているが、com.sun.j3d.utils.geometryにあるConeやCylinder、Sphereなどを使えば、とりあえず3Dっぽいものは表示できる。今回はとりあえずなので、Geometryについてはいずれまた…。