SwingでUIアーキテクチャパターン MVC編

前回に引き続き、今回はMVCパターンです。


まずはモデルですが、BMIの計算に関するドメインオブジェクトとして実装します。
また、MVCにおけるモデルは、ビューとオブザーバパターンの関係を作る必要があるため、オブザーバを保持するためのEventListenerListや、オブザーバにイベントを通知するための発火メソッド(fireModelChanged()メソッド)を実装しています。
BMIを表示するテキストボックスの背景色を表すbmiColor変数は、本来プレゼンテーション層の状態を表すオブジェクトですが、テストのことを考えるとあまりビューにロジックなどを持たせたくないため、今回のサンプルではモデルに実装しています。
とはいえ、このモデルはほぼBMIの計算に関する処理だけになったので、かなりすっきりした感じになりました。

public class Model{

    private EventListenerList listenerList = new EventListenerList();

    private String height = "";
    private String weight = "";
    private String bmi = "";
    private Color bmiColor = Color.WHITE;

    public String getWeight(){
        return weight;
    }

    public String getHeight(){
        return height;
    }

    public String getBmi(){
        return bmi;
    }

    public Color getBmiColor(){
        return bmiColor;
    }

    public void setWeight( String weight ){
        this.weight = weight;
        updateBMI();
        fireModelChanged();
    }


    public void setHeight( String height ){
        this.height = height;
        updateBMI();
        fireModelChanged();
    }

    private void updateBMI(){

        float height;
        float weight;

        try{
            height = Float.valueOf( this.height );
            weight = Float.valueOf( this.weight );
        } catch ( NumberFormatException e ){
            return;
        }

        if( height == 0f ){
            return;
        }

        float bmi = weight / ( height * height );

        this.bmi = Float.toString( bmi );

        if( bmi < 18.5f ){
            bmiColor = Color.WHITE;
        } else if( bmi < 20.0f ){
            bmiColor = Color.YELLOW;
        } else if( bmi < 30.0f ){
            bmiColor = Color.ORANGE;
        } else{
            bmiColor = Color.RED;

        }
    }


    protected void fireModelChanged(){
        ModelListener[] listeners = listenerList.getListeners( ModelListener.class );
        ModelEvent event = new ModelEvent( this );
        for( ModelListener l : listeners ){
            l.modelChanged( event );
        }
    }


    public void addModelListener( ModelListener listener ){
        listenerList.add( ModelListener.class, listener );
    }

    public void removeModelListener( ModelListener listener ){
        listenerList.remove( ModelListener.class, listener );
    }
}


続いてビューです。
こちらは画面の表示に関する処理、モデルとコントローラの初期化、モデルへのリスナの登録だけのクラスになりました。
モデルの状態が更新されるとmodelChanged()メソッドがモデルから呼び出されるので、その中で画面の更新処理を行なっています。

public class View extends JFrame implements ModelListener{

    private final Controller controller = new Controller();

    public static void main( String args[] ){

        SwingUtilities.invokeLater( new Runnable(){
            public void run(){
                Model model = new Model();
                View view = new View( model );
                view.setVisible( true );
            }
        } );
    }

    /**
     * @param model
     */
    public View( Model model ){
        initComponents();
        setModel( model );
        addHeightChangeListener( new DocumentListener(){
            public void insertUpdate( DocumentEvent e ){
                controller.heightChanged( e );
            }

            public void removeUpdate( DocumentEvent e ){
                controller.heightChanged( e );
            }

            public void changedUpdate( DocumentEvent e ){
            }
        } );

        addWeightChangeListener( new DocumentListener(){
            public void insertUpdate( DocumentEvent e ){
                controller.weightChanged( e );
            }

            public void removeUpdate( DocumentEvent e ){
                controller.weightChanged( e );
            }

            public void changedUpdate( DocumentEvent e ){
            }
        } );

    }


    public void setModel( Model model ){
        Model oldModel = controller.getModel();
        if( oldModel != null ){
            oldModel.removeModelListener( this );
        }
        controller.setModel( model );
        model.addModelListener( this );

    }

    public void modelChanged( ModelEvent e ){
        Model model = controller.getModel();
        if( !controller.isNotifyEvent() ){
            weightField.setText( model.getWeight() );
            heightField.setText( model.getHeight() );
        }
        bmiField.setText( model.getBmi() );
        bmiField.setBackground( model.getBmiColor() );

    }

    public void addHeightChangeListener( DocumentListener l ){
        heightField.getDocument().addDocumentListener( l );
    }

    public void addWeightChangeListener( DocumentListener l ){
        weightField.getDocument().addDocumentListener( l );
    }

    /**
     * This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings( "unchecked" )
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents(){
        //省略
    }
}

モデル登録するリスナのインターフェイスです。上述の通り、モデルの更新時にはmodelChanged()が呼び出されます。

public interface ModelListener extends EventListener{

    void modelChanged( ModelEvent e );
}


最後にコントローラです。
ユーザからのイベント(ここではビューのJTextField)から受け取ったイベントを適切に加工し、モデルに渡す処理を行なっています。もっとも、今回のサンプルではほとんど何もしないクラスになってしまいました。
notifyEvent変数は、イベントの発生元となったJTextFieldが自分自身を変更しないようにするためのフラグです。正直このコードは不細工な感じがしますが、他にいい方法が思いつきませんでした。

public class Controller{

    private boolean notifyEvent;
    private Model model;

    public Controller(){
    }


    public void setModel( Model model ){
        this.model = model;
    }

    public Model getModel(){
        return model;
    }

    public boolean isNotifyEvent(){
        return notifyEvent;
    }

    public void heightChanged( DocumentEvent e ){
        String text = getTextFromDocumentEvent( e );
        heightChanged( text );
    }

    public void heightChanged( String newHeight ){
        notifyEvent = true;
        try{
            model.setHeight( newHeight );
        } finally{
            notifyEvent = false;
        }
    }

    public void weightChanged( DocumentEvent e ){
        String text = getTextFromDocumentEvent( e );
        weightChanged( text );
    }

    public void weightChanged( String newWeight ){
        notifyEvent = true;
        try{
            model.setWeight( newWeight );
        } finally{
            notifyEvent = false;
        }
    }

    private String getTextFromDocumentEvent( DocumentEvent e ){
        try{
            return e.getDocument().getText( 0, e.getDocument().getLength() );
        } catch ( BadLocationException ignored ){
            return "";
        }
    }
}

ユーザがキーを入力した時のこれらのコードの処理の流れですが、以下のようになります。

  1. イベントがビューを経由してコントローラが受け取る
  2. コントローラは受け取ったイベントを加工し、モデルを更新
  3. 更新されたモデルは、自身のオブサーバであるビューにモデルが更新されたことを通知
  4. モデルが更新された事を通知されたビューは、モデルから情報を取り出して自分自身を更新


また、同じモデルを共有するビューを二つ生成すると、同じ値を表示する二つの画面が表示することができます。

public void run(){
    Model model = new Model();
    View view = new View( model );
    view.setVisible( true );
    View view2 = new View( model );
    view2.setVisible( true );
}

さて、前回のコードと比べるとドメインオブジェクトのコードとプレゼンテーションのコードが分離されて綺麗になりましたが、依然プレゼンテーションの状態やロジックがモデル内に混在してます。
次回、監視コントローラ(Supervising Controller)パターンに続きます。

完全なコードは以下からどうぞ。
ソース一式(github)

SwingでUIアーキテクチャパターン

@ITさんのこちらの記事「開発者が知っておくべき、6つのUIアーキテクチャ・パターン」に触発されて、あとは自分の考えを纏めるために、以上の記事で紹介されているアーキテクチャパターンをSwingとJavaで実装して見ようかと思います。
サンプルは同じく「BMIによる肥満度判断」を使用させて頂きます。


まずは「フォームとコントロール」から、全体のコードは以下の通り。

import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.Color;

public class Form extends javax.swing.JFrame implements DocumentListener{

    public static void main( String args[] ){

        SwingUtilities.invokeLater( new Runnable(){
            public void run(){
                new Form().setVisible( true );
            }
        } );
    }

    /**
     * Creates new form Form
     */
    public Form(){
        initComponents();
        initListeners();
    }

    private void initListeners(){
        heightField.getDocument().addDocumentListener( this );
        weightField.getDocument().addDocumentListener( this );
    }

    public void insertUpdate( DocumentEvent e ){
        textChanged();
    }

    public void removeUpdate( DocumentEvent e ){
        textChanged();
    }

    public void changedUpdate( DocumentEvent e ){
    }

    public void textChanged(){
        float height;
        float weight;

        try{
            height =Float.valueOf( heightField.getText().trim() );
            weight = Float.valueOf( weightField.getText().trim() );

        } catch ( NumberFormatException ignored ){
            bmiField.setText( "" );
            bmiField.setBackground( Color.WHITE );
            return;
        }

        if( height == 0f ){
            return;
        }

        float bmi = weight / ( height * height );
        bmiField.setText( Float.toString( bmi ) );

        Color bg;
        if(bmi < 18.5f){
            bg = Color.WHITE;
        } else if( bmi < 20.0f){
            bg = Color.YELLOW;
        }else if( bmi < 30.0f ){
            bg = Color.ORANGE;
        }else{
            bg = Color.RED;
        }

        bmiField.setBackground( bg );
    }

    /**
     * This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings( "unchecked" )
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents(){
        java.awt.GridBagConstraints gridBagConstraints;

        bmiPanel = new javax.swing.JPanel();
        heightLabel = new javax.swing.JLabel();
        heightField = new javax.swing.JTextField();
        mLabel = new javax.swing.JLabel();
        weightLabel = new javax.swing.JLabel();
        weightField = new javax.swing.JTextField();
        kgLabel = new javax.swing.JLabel();
        bmiLabel = new javax.swing.JLabel();
        bmiField = new javax.swing.JTextField();

        setDefaultCloseOperation( javax.swing.WindowConstants.EXIT_ON_CLOSE );
        setTitle( "BMI checker" );
        setLocationByPlatform( true );
        setResizable( false );

        bmiPanel.setBorder( javax.swing.BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) );
        bmiPanel.setLayout( new java.awt.GridBagLayout() );

        heightLabel.setText( "Height:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( heightLabel, gridBagConstraints );

        heightField.setName( "heightLabel" ); // NOI18N
        heightField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( heightField, gridBagConstraints );

        mLabel.setText( "m" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 2 );
        bmiPanel.add( mLabel, gridBagConstraints );

        weightLabel.setText( "Weight:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( weightLabel, gridBagConstraints );

        weightField.setName( "weightLabel" ); // NOI18N
        weightField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( weightField, gridBagConstraints );

        kgLabel.setText( "kg" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 2 );
        bmiPanel.add( kgLabel, gridBagConstraints );

        bmiLabel.setText( "BMI:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( bmiLabel, gridBagConstraints );

        bmiField.setBackground( new java.awt.Color( 255, 255, 255 ) );
        bmiField.setEditable( false );
        bmiField.setDisabledTextColor( new java.awt.Color( 0, 0, 0 ) );
        bmiField.setName( "bmiLabel" ); // NOI18N
        bmiField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( bmiField, gridBagConstraints );

        getContentPane().add( bmiPanel, java.awt.BorderLayout.CENTER );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JTextField bmiField;
    private javax.swing.JLabel bmiLabel;
    private javax.swing.JPanel bmiPanel;
    private javax.swing.JTextField heightField;
    private javax.swing.JLabel heightLabel;
    private javax.swing.JLabel kgLabel;
    private javax.swing.JLabel mLabel;
    private javax.swing.JTextField weightField;
    private javax.swing.JLabel weightLabel;
    // End of variables declaration//GEN-END:variables
}

今回、SwingのGUI部分はNetBeansGUIビルダー Matisseを使用して作成してます。
Matisseは非常に使いやすいので、Swingをやる方にはNetBeansがお勧めです。
閑話休題、以下解説。


処理はすべてイベントハンドラへ、ということなので、BMIの計算・画面を更新する処理はtextChanged() メソッドに実装しています。
このメソッドはinsertUpdateメソッド及びremoveUpdateメソッドから呼び出しています。
insertUpdateメソッド及びremoveUpdateメソッドは、インターフェイス DocumentListener が宣言するメソッドであり、これはJTextFieldのモデルとなるDocumentに登録するリスナーです。

   public void textChanged(){
        float height;
        float weight;

        try{
            height =Float.valueOf( heightField.getText().trim() );
            weight = Float.valueOf( weightField.getText().trim() );

        } catch ( NumberFormatException ignored ){
            bmiField.setText( "" );
            bmiField.setBackground( Color.WHITE );
            return;
        }

        if( height == 0f ){
            return;
        }

        float bmi = weight / ( height * height );
        bmiField.setText( Float.toString( bmi ) );

        Color bg;
        if(bmi < 18.5f){
            bg = Color.WHITE;
        } else if( bmi < 20.0f){
            bg = Color.YELLOW;
        }else if( bmi < 30 ){
            bg = Color.ORANGE;
        }else{
            bg = Color.RED;
        }

        bmiField.setBackground( bg );
    }


そしてこのリスナーを身長の入力欄と、体重の入力欄のJTextFieldに登録しているので、どちらかの入力値が変更されるごとにinsertUpdateメソッド及びremoveUpdateメソッド経由で、textChanged()メソッドが呼び出され、画面が更新されることになります。

    private void initListeners(){
        heightField.getDocument().addDocumentListener( this );
        weightField.getDocument().addDocumentListener( this );
    }


このコードのメリットとしては、クラスが一つで済むのと、すべての処理がtextChanged()にあるので(この程度の規模の処理であれば)読みやすいということです。
反面、画面を構築するコードとBMIを計算するコード、画面を更新するコードが混ざり合っているためコードの共有やユニットテストがやりにくいことです。また、リソースを使う画面を表示する処理が実際に動いてしまうため、テストコードの実行が非常に遅くなってしまいます。


次回、MVCに続きます。

XMLファイルからXPathで内容を取り出すワンライナー

JavaXPathを簡単に使えないかと聞かれたので、試しにやってみたらこうなった。

NodeList nodes = (NodeList)XPathFactory.newInstance().newXPath().compile( "xpath" ).evaluate( DocumentBuilderFactory.newInstance().newDocumentBuilder().parse( "filename" ), XPathConstants.NODESET );

for( int i = 0; i < nodes.getLength(); i++ ) {
    System.out.println( nodes.item( i ).getTextContent() );
}

JavaFXとtwitter4jで変態発見用Twitterクライアントを作ってみた

変態アドベントカレンダー11月16日分です。

大して面白いネタが思い浮かびもしなかったので、以前途中まで作りかけたTwitterクライアントに手を加えて、自分のフォローしている人たちの発言から"変態"を含む発言を表示するクライアントを作って見ました。
「お巡りさん、この人です!」するボタンを付けたかったのですが、間に合わなかったので是非付けてあげてください。
しかし、慣れていないとなかなか思い通りのデザインにするのには時間がかかりますね。


以下簡単な解説。
まずはアプリケーション開始用のクラス、JavaFXのお約束通りApplicationを継承し、オーバーライドしたstart()中でStageを初期化しています。

Startup.java

package hr;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import twitter4j.TwitterException;
import twitter4j.auth.AccessToken;
import twitter4j.auth.RequestToken;

import java.io.File;
import java.io.IOException;

public class Startup extends Application {

    private TwitterService twitterService;
    private StatusListView statusListView;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() {
        try {
            File userFile = new File("user.xml");
            twitterService = TwitterService.createTwitterService( userFile );
        } catch (IOException ignored) {
        }

    }

    @Override
    public void start(Stage stage) throws Exception {


        statusListView = new StatusListView();
        Scene scene = new Scene(statusListView, 400, 640);
        stage.setResizable(false);

        stage.setScene(scene);
        stage.setTitle("HENTAI Report");
        stage.show();

        if (twitterService != null) {
            twitterService.addHomeTimelineListener(statusListView.getController());
        } else {
            authlization();

        }

    }

    private void authlization() throws TwitterException {
        final RequestToken token = TwitterService.authlization();
        String url = token.getAuthenticationURL();

        BorderPane borderPane = new BorderPane();
        HBox hbox = new HBox();
        final TextField textField = new TextField();
        Button button = new Button("認証");

        hbox.getChildren().addAll( textField,button);


        final WebView webView = new WebView();
        final Stage stage = new Stage(StageStyle.UTILITY);

        borderPane.setCenter(webView);
        borderPane.setBottom(hbox);
        Scene scene = new Scene(borderPane, 400, 640);
        stage.setScene(scene);
        stage.setTitle("認証");
        stage.show();

        webView.getEngine().load(url);

        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                String pin = textField.getText();
                AccessToken accessToken = TwitterService.enterPin(token, pin);
                twitterService = TwitterService.createTwitterService(accessToken);
                twitterService.addHomeTimelineListener(statusListView.getController());
                stage.hide();
            }
        });

    }

}


StatusViewは一つ一つのStatusを表示するクラスです。
BorderPaneの左側にプロフィール画像、中央にテキストを表示しています。


StatusListViewクラスは、上のStatusViewを新しいものを上から順に並べて表示するクラスです。
ScrollPane を継承し、内部のコンテンツには要素を縦に並べるVBoxを配置しています。

StatusListView.java

package hr;

import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;

public class StatusListView extends ScrollPane {

    private final StatusListController controller;
    private VBox scrollPaneContent;

    public StatusListView() {
        setFitToWidth(true);
        setVbarPolicy(ScrollBarPolicy.ALWAYS);
        setContent(getscrollPaneContent());
        controller = new StatusListController(this);
    }

    private VBox getscrollPaneContent() {
        if (scrollPaneContent == null) {
            scrollPaneContent = new VBox();
            scrollPaneContent.setFillWidth(true);
        }
        return scrollPaneContent;
    }

    public StatusListController getController() {
        return controller;
    }

    public void addStatusView(Node view, int index) {
        getscrollPaneContent().getChildren().add(index, view);
    }

    public void removeStatusView(int maxStatusNum) {
        getscrollPaneContent().getChildren().remove(maxStatusNum);
    }
}


StatusListControllerクラスはStatusListViewクラスのコントローラです。
StatusList、StatusListViewは見ての通りsetter/getter程度の単純なメソッドしか持たせず、Viewとロジックを分離しています。
StatusListController.java

package hr;

import javafx.application.Platform;
import javafx.scene.image.Image;
import twitter4j.Status;
import twitter4j.UserStreamAdapter;

import java.util.ArrayList;
import java.util.List;

public class StatusListController extends UserStreamAdapter {
    private static final int MAX_STATUS_NUM = 150;
    private final StatusListView view;
    private List<Long> sorter = new ArrayList<Long>(MAX_STATUS_NUM);

    public StatusListController(StatusListView view) {
        this.view = view;
    }

    public void addStatus(Status status) {
        if (status.isRetweet()) {
            status = status.getRetweetedStatus();
        }

        if (!status.getText().contains("変態")) {
            return;
        }

        int index = getIndexToView(status.getId());
        if (index < 0) {
            return;
        }

        StatusView newStatusView = createStatusView(status);
        view.addStatusView(newStatusView, index);

        int count = sorter.size();
        if (count > MAX_STATUS_NUM) {
            view.removeStatusView(MAX_STATUS_NUM);
        }

    }

    private StatusView createStatusView(Status status) {
        ProfileImageManager imageManager = ProfileImageManager.getInstance();

        Image image = imageManager.getProfileImage(status.getUser());
        String text = status.getText();

        StatusView newStatusView = new StatusView();
        newStatusView.setProfileImage(image);
        newStatusView.setText(text);

        return newStatusView;
    }

    @Override
    public void onStatus(final Status status) {
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                addStatus(status);
            }
        });

    }

    /**
     * @param id status の ID
     * @return 挿入位置
     */
    private int getIndexToView(long id) {
        int len = sorter.size();
        if (len == 0) {
            sorter.add(0, id);
            return 0;
        }
        int index = 0;
        for (; index < len; index++) {
            if (id > sorter.get(index)) {
                break;
            }
        }

        if (index < MAX_STATUS_NUM) {
            sorter.add(index, id);
            return index;
        }

        return -1;
    }
}

githubはこちら
https://github.com/hakurai/hentaireport

以下使い方。
まず動かすためにはJavaFXおよびTwitterのCONSUMER_KEYとCONSUMER_SECRETが必要なので、ダウンロード及び取得してください。ここで既にハードルが高い気がしなくもない。


まず起動すると、初回は認証画面がWebViewで表示されるので、ログインしてください。


ログインが成功すると、PINコードが表示されるので、左下の入力欄に入力して、認証ボタンをクリックします。

認証が成功すると、"変態"の文字を含む発言が表示され始めるはずです。しかしそのような発言をする方がフォローしている人たちの中に居なければ、残念(?)ながら何も表示されませんが・・・。

JavaFX2.0入門1 Hello JavaFX!

JavaFXのインストールを済ませた所で、早速JavaFXを利用したコードを書いてみます。
まず手始めとしてウィンドウの中央に"Hello JavaFX!"という文字列を表示する簡単なプログラムから。


JavaFXアプリケーションを作成するには、Applicationクラスを継承したクラスを作成してstartメソッドをオーバーライドする必要があります。*1
startメソッドには引数としてウィンドウの外枠部分となるStageオブジェクトが渡されますが、このままだとウィンドウ内には何も表示されません。なので、シーングラフ(ウィンドウ内で描画する要素を木構造で表した物)を持つSceneオブジェクトを生成して、このSceneオブジェクトを生成してStageオブジェクトに設定してあげる必要があります。


今回は、シーングラフのルートとして、ラベルを中央に表示するためのBorderPaneを生成し、そのcenterに"Hello JavaFX!"と表示するラベルを設定しました。
シーングラフを画像にするとこんな感じです。


あとは、mainメソッド内でlaunchメソッドを呼び出すようにすればこのアプリケーションは完成です。
アプリケーションを起動すると、mainメソッドから呼び出されたlaunchメソッドが以下の順番でApplicationクラスのメソッドを呼び出します。

  1. コンストラクタ
  2. initメソッド
  3. startメソッド

なお、launchメソッドに渡した引数は、ApplicationクラスのgetParametersメソッドで取り出すことが出来るようです。

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start( Stage primaryStage ) {
        BorderPane borderPane = new BorderPane();
        borderPane.setCenter( new Label( "Hello JavaFX!" ) );
        
        Scene scene = new Scene( borderPane, 200, 150 );
        primaryStage.setScene( scene );

        //ウィンドウを表示
        primaryStage.show();
    }
}



ちなみにSwingで同じ内容のプログラムを書くとこうなります。
AWT/SwingではContainerの属性であったレイアウトが、JavaFXではそれぞれ独立した部品になったのが大きなポイントでしょうか。 また、JavaFXではSwingのJPanelに相当する部品も無いみたいです。

import java.awt.BorderLayout;
import javax.swing.*;

public class Main{

    public static void main(String[] args) {
        SwingUtilities.invokeLater( new Runnable() {

            public void run() {
                Main thisClass = new Main();
                thisClass.start();
            }
        });
    }

    public void start() {
        JFrame frame = new JFrame();
        frame.setSize( 200, 150 );
        JPanel panel = new JPanel( new BorderLayout() );
        panel.add(  new JLabel( "Hello JavaFX!", SwingConstants.CENTER ), BorderLayout.CENTER );
        
        frame.add( panel );
        frame.setVisible( true );
    }
}

*1:他にApplicationクラスはinitメソッドとstopメソッドがありますが、必要がなければオーバーライドしなくてもかまいません。

JavaFX 2.0をMacにインストール

待ちに待ったJavaFX 2.0の Mac版がベータながら公開されたのでインストールしてみました。
まずはJavaFXのサイトからzipファイルをダウンロードします。
展開した javafx-sdk2.0.2-beta ディレクトリ内の "rt/lib" の中のファイルを全て "/System/Liblary/Java/Extensions"ディレクトリの中に、"rt/bin" の中のファイルを"/System/Liblary/Java/bin"にコピーすればインストールは完了です。

「お題:ある金額になるコインの組み合わせ」をJavaで挑戦してみた

お題:ある金額になるコインの組み合わせ - No Programming, No Life

TL眺めてたらまたお題が出てたので取り敢えず正攻法?でやってみた

import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

public class Odai{

    public static List<List<Integer>> hoge( int sum, List<Integer> coinList ){
        
        Deque<Integer> stack = new LinkedList<Integer>();
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        Collections.sort( coinList );
        hoge( sum, coinList, stack, res );
        
        return res;
        
    }
    
    private static void hoge( int sum, List<Integer> coinList, Deque<Integer> stack, List<List<Integer>> res ){
        int max = stack.size() > 0 ? stack.peek() : coinList.get( coinList.size() - 1 ) < sum ? coinList.size() - 1  : sum;
        
        if( sum == 0 ){
            res.add( new ArrayList<Integer> ( stack ) );
            return;
        }
        for( int coin : coinList ){
            if( coin > max )continue;
            stack.push( coin );
            sum -= coin;
            hoge( sum, coinList, stack, res );
            sum += stack.pop();
        }
    }
}

テストコード

import java.util.Arrays;
import java.util.List;
import junit.framework.TestCase;

public class OdaiTest extends TestCase{
    
    public void testHoge(){
        List<List<Integer>> res;
        
        res = Odai.hoge( 10, Arrays.asList( 1, 5, 10, 50, 100, 500 ) );
        
        assertEquals( Arrays.asList( 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ), res.get( 0 ) );
        assertEquals( Arrays.asList( 1, 1, 1, 1, 1, 5 ), res.get( 1 ) );
        assertEquals( Arrays.asList( 5, 5 ), res.get( 2 ) );
        assertEquals( Arrays.asList( 10 ), res.get( 3 ) );

        res = Odai.hoge( 16, Arrays.asList( 2, 8, 10 ) );

        assertEquals( Arrays.asList( 2,2,2,2,2,2,2,2 ), res.get( 0 ) );
        assertEquals( Arrays.asList( 2,2,2,2,8 ), res.get( 1 ) );
        assertEquals( Arrays.asList( 8,8 ), res.get( 2 ) );
        assertEquals( Arrays.asList( 2,2,2,10 ), res.get( 3 ) );

        res = Odai.hoge( 15, Arrays.asList( 2, 8, 10 ) );

        assertEquals( 0, res.size() );

    }
}

2011-09-05追記
確かにご指摘のあった通り、上のコードは明らかにおかしい。テストコード通ったのが奇跡的なレベル。
コインの最大値を返す所(下のコードの削除部)が、coinList.size() - 1 というインデックスとなるべき値を返してしまってました。
int max = stack.size() > 0 ? stack.peek() : coinList.get( coinList.size() - 1 ) < sum ? coinList.size() - 1 : sum;

あともう一点、このままだとstack.peek() の値がsum(残りの金額)の値を超えてしまう可能性があります。
あえて一行のままで直すなら下記のコードに置き換えればいいはず。
int max = stack.size() > 0 ? stack.peek() < sum ? stack.peek() : sum : coinList.get( coinList.size() - 1 ) < sum ? coinList.get( coinList.size() - 1 ) : sum;

教訓:テスト大事!