Home > Tags > GUI

GUI

Java: イベント駆動によるModelとViewの分離 - Observer パターン

よくGUIやWebアプリの簡単なサンプルソースなどは、UIとアプリケーションのロジックが同じクラスまたはメソッドに書かれている場合が多いです。それはそのサンプルがある特定の機能や関数の紹介の為に簡潔に書いているのですが、仮にいざそのソースを元にアプリを作りこんで機能の追加を行っていくとUIとアプリのロジックは分離したほうが保守・拡張と共に行いやすいです。

下記のプログラムは1秒毎に数値をカウントし、それを2進数と10進数でGUI上のラベルに出力する機能をモデルとビューに分けています。すなわち、数値のカウントをするモデルと数値をUIに表示するビューに。(いくつかの言語ではGUIの部品としてタイマーがあるようですが。。)

実行結果

count_timer01.png

クラス図

countmodel_view_class.png

インスタンスの生成はコンストラクタで行うのがいいのですが、はしょっています。さて、これまでに何らかのフレームワークを使っていた方には上クラス図はMVCの説明図として見慣れているかもしれません。今回ModelとViewを繋ぐControllerの役割はCountListenerが担っています(下記コードではCountChangeEvent経由でModelインスタンスを渡すのみですが)。

処理手順

ビュー側で自身のインスタンスをモデルに登録し(モデル.addCountListener(ビュー))、モデルのデータが更新された場合、モデル→ビューへイベントを送出。イベント内部にモデルのデータがあり、そのデータでビューがUIを更新します。イベントの発生順序等は下図のシーケンス図のようになります。

シーケンス図

countmodel_view_sequence.png

無限ループに注意

ビューを更新するイベントリスナーのメソッド(ここではcountChanged())で再度モデルのプロパティ変更メソッド(ここではsetCount())を用いると、再度イベント通知処理(notifyToListeners())が発生することで、無限ループになるので注意が必要です。

ビューの追加手順

ビューを追加する際の手順は、ビューのインスタンスをモデルのリスナーに登録し(addCountListener())、ビュー自身がリスナーのメソッドを実装することで(countChanged())、モデルからのイベントを受け取ることが出来ます。

そういえば最近はIDEの方でバインド設定したり、ObservableList等があるのであまり意識することがなくなってきたなぁ。

ソースコード

import java.awt.Container;
import java.awt.GridLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;

class CountView extends JFrame implements CountListener{

	private final JLabel binaryLabel = new JLabel("0");
	private final JLabel decimalLabel = new JLabel("0");
	private CountModel cModel = new CountModel(0);

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

	public CountView() {
		super("Counter");
		Container c = getContentPane();
		c.setLayout(new GridLayout(2, 2));
		c.add(new JLabel(" 2進数:"));
		c.add(binaryLabel);
		c.add(new JLabel("10進数:"));
		c.add(decimalLabel);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		cModel.addCountListener(this); // ModelにViewを登録

		pack();
		setVisible(true);
	}

	public void countChanged(CountChangeEvent e) {
		if (e.getSource() == cModel) {
			binaryLabel.setText(Integer.toString(cModel.getCount(), 2));
			decimalLabel.setText(Integer.toString(cModel.getCount(), 10));
		}
	}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * カウントするModel
 */
public class CountModel {

	private int count;
	private final List<CountListener> listeners = new ArrayList<CountListener>();
	private Timer t = new Timer("Count Timer", false);

	CountModel(int i) {
		count = i;
		t.scheduleAtFixedRate(new CountTime(), 0, 1000);
	}

	// Viewを登録
	public void addCountListener(CountListener listener) {
		listeners.add(listener);
	}

	public int getCount() {
		return count;
	}

	public void setCount(int i) {
		count = i;
		notifyToListeners();
	}

	// Viewへの通知
	private void notifyToListeners() {
		for (CountListener listener : listeners) {
			listener.countChanged(new CountChangeEvent(this));
		}
	}

	class CountTime extends TimerTask {
		@Override
		public void run() {
			setCount(getCount() + 1);
		}
	}
}
/**
 * View側で実装する(Model側から呼び出し)
 */
public interface CountListener {
	public void countChanged(CountChangeEvent e);
}

/**
 * 通知内容を表すイベント(Modelが生成しViewが受け取る)
 */
public class CountChangeEvent {
	private final CountModel source;

	public CountChangeEvent(CountModel count) {
		this.source = count;
	}

	public CountModel getSource() {
		return source;
	}
}

AIR: Webサーバ、Socketの接続状況を検知 - URLMonitor、SocketMonitorクラス

任意のアドレスのWebサイト[サービス]のネットワーク状況を検知するURLMonitorと、任意のサーバ+ポートに接続可能か否かを検知するSocketMonitorクラスの動作サンプルを下記に示します。

ソースコード

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
  applicationComplete="startup()">
  <mx:Script>
    <![CDATA[
    import air.net.SocketMonitor
    import air.net.URLMonitor;
    import flash.net.URLRequest;
    import flash.events.StatusEvent;

    private var SERVER_URL:String = "http://www.yukun.info/";
    private var SOCK_ADRR:String = "yukun.info";
    private var PORT:int = 6667;
    private var INTERVAL_TIME:int = 3000; // ms
    private var serviceMonitor:URLMonitor = null;
    private var socketMonitor:SocketMonitor = null;

    private function startup():void {
      var endpoint:URLRequest = new URLRequest(SERVER_URL);
      serviceMonitor = new URLMonitor(endpoint);
      serviceMonitor.addEventListener(StatusEvent.STATUS, onStatusEvent);
      serviceMonitor.pollInterval = INTERVAL_TIME;
      serviceMonitor.start();

      socketMonitor = new SocketMonitor(SOCK_ADRR, PORT);
      socketMonitor.addEventListener(StatusEvent.STATUS, onSocketStatusChange);
      socketMonitor.pollInterval = INTERVAL_TIME;
      socketMonitor.start();
    }

    // ネットワークサービスの状態の検知
    private function onStatusEvent(e:StatusEvent):void {
      var date:Date = new Date();
      trace(date.toLocaleTimeString());
      trace(SERVER_URL + "に" + (serviceMonitor.available ? "接続可" : "切断中"));
    }

    private function onSocketStatusChange(e:StatusEvent):void {
      trace(SOCK_ADRR + "のポート" + PORT + "は" +
        (socketMonitor.available ? "接続可" : "切断中"));
    }
    ]]>
  </mx:Script>
</mx:WindowedApplication>

実行結果


http://www.yukun.info/に接続可

yukun.infoのポート6667は切断中

URLMonitorはネットワーク状況を検知する為にサーバへGETリクエストを送出して、レスポンスのステイタスコードを確認して判断しているようです↓。

リファレンス

AIR: SQLiteでデータの挿入と検索 - SQLConnection、SQLStatementクラス

AIRアプリケーションからSQLiteのDBにアクセスするには主にSQLConnectionとSQLStatementクラスを用います。基本的な処理の流れは、

  1. SQLConnection#openAsync(<Fileオブジェクト>, <モード>)でDBに接続
  2. SQLStatement#textプロパティにSQL文を代入しexecute()で実行
  3. その際、フィールド値はSQLStatement#parameters["@<定義されたパラメータ>"]で代入する
  4. SELECT文の検索結果データははSQLStatement#getResult()で取得
  5. 結果データの型はSQLResultで、データそのものはdataプロパティ(Array型)に入っている。e.g. <SQLResult>.data[i].<フィールド名>

下のプログラムはDBに接続してテーブルの作成、レコードの追加、検索を行うサンプルです。

ソースコード

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute" creationComplete="dbConnect()">
<mx:Script>
  <![CDATA[
  import flash.data.SQLConnection;
  import flash.filesystem.File;
  import flash.events.SQLErrorEvent;
  import flash.events.SQLEvent;

  private var conn:SQLConnection;
  private var dbFile:File;
  private var sql:SQLStatement;

  // DBに接続
  private function dbConnect():void {
    conn = new SQLConnection();
    conn.addEventListener(SQLEvent.OPEN, onDBOpen);
    conn.addEventListener(SQLErrorEvent.ERROR, onDBError);
    dbFile = File.desktopDirectory.resolvePath("test01.db");
    trace("フィアルは存在" + (dbFile.exists ? "します。:" : "しません。") + dbFile.nativePath);
    conn.openAsync(dbFile, SQLMode.CREATE);
  }

  // 接続完了
  private function onDBOpen(e:SQLEvent):void {
    trace("Connect DB");
    conn.removeEventListener(SQLEvent.OPEN, onDBOpen);
    conn.removeEventListener(SQLErrorEvent.ERROR, onDBError);
    createTable(); // テーブルを作成
  }

  private function onDBError(e:SQLErrorEvent):void {
    trace("Can't connect DB: " + e.error.message);
    trace("Error detals: " + e.error.details);
  }

  // テーブルを作成する関数
  private function createTable():void {
    sql = new SQLStatement();
    sql.sqlConnection = conn;
    var sqlTxt:String = "CREATE TABLE IF NOT EXISTS addresses " +
        "(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
    sql.text = sqlTxt;
    sql.addEventListener(SQLEvent.RESULT, onCreate);
    sql.addEventListener(SQLErrorEvent.ERROR, onCreateError);
    sql.execute();
  }

  // テーブル作成完了
  private function onCreate(e:SQLEvent):void {
    trace("Can create table");
    sql.removeEventListener(SQLEvent.RESULT, onCreate);
    sql.removeEventListener(SQLErrorEvent.ERROR, onCreateError);
    insertData(); // データを挿入
  }

  private function onCreateError(e:SQLErrorEvent):void {
    trace("Can't create table: " + e.error.message);
    trace("Error details: " + e.error.details);
  }

  private function insertData():void {
    sql = new SQLStatement();
    sql.sqlConnection = conn;
    var sqlTxt:String = "INSERT INTO addresses (id, name, age) " +
      "VALUES (@id, @name, @age)";
    sql.text = sqlTxt;
    sql.parameters["@id"] = 1;
    sql.parameters["@name"] = "名無し";
    sql.parameters["@age"] = 18;
    sql.addEventListener(SQLEvent.RESULT, onInsert);
    sql.addEventListener(SQLErrorEvent.ERROR, onInsertError);
    sql.execute();
  }

  // データ挿入完了
  private function onInsert(e:SQLEvent):void {
    trace("Can insert data");
    sql.removeEventListener(SQLEvent.RESULT, onInsert);
    sql.removeEventListener(SQLErrorEvent.ERROR, onInsertError);
    selectData(); // データを検索
  }

  private function onInsertError(e:SQLErrorEvent):void {
    trace("Can't insert data: " + e.error.message);
    trace("Error details: " + e.error.details);
  }

  private function selectData():void {
    sql = new SQLStatement();
    sql.sqlConnection = conn;
    var sqlTxt:String = "SELECT * FROM addresses";
    sql.text = sqlTxt;
    sql.addEventListener(SQLEvent.RESULT, onSelect);
    sql.addEventListener(SQLErrorEvent.ERROR, onSelectError);
    sql.execute();
  }

  // 検索結果を取得完了
  private function onSelect(e:SQLEvent):void {
    trace("Can select data");
    sql.removeEventListener(SQLEvent.RESULT, onSelect);
    sql.removeEventListener(SQLErrorEvent.ERROR, onSelectError);
    var result:SQLResult = sql.getResult();
    var num:int = result.data.length;
    var id:int;
    var name:String;
    var age:String;

    for (var i:int = 0; i < num; i++) {
      var row:Object = result.data[i];
      id = row.id;
      name = row.name;
      age = row.age;
      var str:String = "id=" + id + ", name=" + name + ", age=" + age;
      trace(str);
    }
  }

  private function onSelectError(e:SQLErrorEvent):void {
    trace("Can't select data: " + e.error.message);
    trace("Error details: " + e.error.details);
  }

  ]]>
</mx:Script>

</mx:WindowedApplication>

実行結果

フィアルは存在しません。/Users/yukun/Desktop/test01.db
Connect DB
Can create table
Can insert data
Can select data
id=1, name=名無し, age=18

リファレンス

AIR: テキストファイルに書き込み - openAsync()、writeMultiByte()

AIR: テキストファイルに書き込み
AIRコンポーネントではローカルのファイルにアクセスすることができます。下記のコードは日本語を含むマルチバイトの文字列をテキストファイルに書き込む処理をします。

処理の手順

  1. FileStream#openAsync()かopen()メソッドの引数にFileインスタンスとFileModeのプロパティを設定して実ファイルのパイプに接続
  2. FileStream#writeMultiByte()でファイルに書き込み
  3. FileStream#close()でストリームを閉じる

ソースコード

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" title="シンプルテキストメイカー">
<mx:Script>
  <![CDATA[
  import mx.controls.Alert;

  private var choDir:File = File.documentsDirectory; // ダイアログの初期ディレクトリ
  private var saveFile:File;
  private var stream:FileStream;

  private function onSaveFileBut():void {
    choDir.addEventListener(Event.SELECT, onSelectSaveFile);
    choDir.browseForSave("テキストファイルに保存");
  }

  private function onSelectSaveFile(e:Event):void {
    saveFile = e.target as File; // 選択されたファイル
    choDir.removeEventListener(Event.SELECT, onSelectSaveFile);
    try {
      stream = new FileStream();
      stream.addEventListener(IOErrorEvent.IO_ERROR, onIOErrorWriteFile);
      stream.openAsync(saveFile, FileMode.WRITE); // 書き込みmodeで開く(フツーのopen()でもOK)
      var str:String = txtArea_.text;
      // 改行文字と文字コードをOS標準のものに置き換えて書き込み
      str = str.replace(/\n/g, File.lineEnding);
      stream.writeMultiByte(str, File.systemCharset); // 実際に書き込み
    } catch (err:IOError) {
      progLab_.text = "IOError : " + err;
    } finally {
      if (stream != null) {
        stream.close();
      }
    }
  }

  // ファイル書き込みに失敗した場合
  private function onIOErrorWriteFile(e:IOErrorEvent):void {
    Alert.show("ファイルの書き込みに失敗", "エラー", Alert.OK, this);
    if (stream != null) {
      stream.close();
    }
  }
  ]]>
</mx:Script>
  <mx:VBox x="0" y="0" height="100%" width="100%">
    <mx:HBox width="100%">
      <mx:Button label="ファイルに保存" id="saveBut_" click="onSaveFileBut();"/>
      <mx:Label id="progLab_"/>
    </mx:HBox>
    <mx:TextArea width="100%" height="100%" id="txtArea_"/>
  </mx:VBox>
</mx:WindowedApplication>

リファレンス

AIR: テキストファイルを非同期に読み込む - openAsync()、readMultiByte()

AIR: テキストファイル読み込みの実行結果
AIRコンポーネントではローカルのファイルにアクセスすることができます。下記のコードは日本語を含むマルチバイトのテキストファイルを読み込み、画面い表示する処理を行います。

大まかな手順

  1. FileStreamのコンストラクタの引数に対象のファイルへのパスが設定されたFileインスタンスを渡す。
  2. FileStream#openAsyncで実ファイルへのパイプ接続。
  3. この時、非同期の読み込み完了/エラーを取得するためにイベントを登録しておく。
  4. 実際の文字の読み取り(どれだけ読むか、文字コードの変換など)はFileStream#readMultiByteで行う。
  5. ストリームのインスタンスには接続時にpositionプロパティ(何処読んでいるかのポインタみたいなもの)からファイル末尾までのサイズ(bytesAvailable)を取得してるので、読み込みサイズにそれを指定。
  6. FileStream#close()でストリームを閉じる

ソースコード

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
  title="テキストビュワー">
  <mx:Script>
    <![CDATA[
    import flash.filesystem.*;
    import mx.events.*;
    import mx.controls.Alert;

    private var choDir:File = File.documentsDirectory; // 開くディレクトリを指す
    private var curFile:File; // 選択されたファイル
    private var stream:FileStream;

    private function onOpenFileBut():void {
      choDir.addEventListener(Event.SELECT, onSelectFile);
      choDir.browseForOpen("開く"); // ファイル選択ダイアログの表示
    }

    // ファイルが選択されたイベント
    private function onSelectFile(e:Event):void {
      txtArea_.text = "";
      stream = new FileStream();
      curFile = e.target as File;
      stream.addEventListener(Event.COMPLETE, onCompleteReadFile);
      stream.addEventListener(IOErrorEvent.IO_ERROR, onIOErrorReadFile);
      stream.addEventListener(ProgressEvent.PROGRESS, onProgReadFile);
      stream.openAsync(curFile, FileMode.READ); // 非同期読み込み
      curFile.removeEventListener(Event.SELECT, onSelectFile);
    }

    private function onCompleteReadFile(e:Event):void {
      try {
        // OS標準の文字コードで読み込み
        var str:String = stream.readMultiByte(stream.bytesAvailable, File.systemCharset);
        // OS標準の改行文字への変換
        var pat:RegExp = new RegExp(File.lineEnding, "g");
        str = str.replace(pat, "n");
        txtArea_.text = str; // テキストエリアに表示
        stream.removeEventListener(Event.COMPLETE, onCompleteReadFile);
        stream.removeEventListener(IOErrorEvent.IO_ERROR, onIOErrorReadFile);
        stream.removeEventListener(ProgressEvent.PROGRESS, onProgReadFile);
      } catch (err:Error) {
        progLab_.text = "IOError: " + err;
      }
      finally {
        // パイプのクローズ
        if (stream != null) {
          stream.close();
        }
      }
    }

    private function onIOErrorReadFile(e:IOErrorEvent):void {
      Alert.show("ファイルを読み込み不可", "Error", Alert.OK, this); // 第4引数には親オブジェトを渡す
      if (stream != null) {
        stream.close();
      }
    }

    private function onProgReadFile(e:ProgressEvent):void {
      progLab_.text = "Progress: " +  e.bytesLoaded + " / " + e.bytesTotal + " bytes";
    }
    ]]>
  </mx:Script>
  <mx:VBox x="0" y="0" height="100%" width="100%">
    <mx:HBox width="100%">
      <mx:Button label="ファイルを開く" id="openBut_" click="onOpenFileBut();"/>
      <mx:Label id="progLab_"/>
    </mx:HBox>
    <mx:TextArea width="100%" height="100%" id="txtArea_"/>
  </mx:VBox>

</mx:WindowedApplication>

リファレンス

Page 1 of 212

Home > Tags > GUI

バックナンバー
最近のコメント
最近のトラックバック
メタ情報

Return to page top