MOSA Developer News[MOSADeN=モサ伝]第138号
2004-11-30
目次
- SqueakではじめるSmalltalk入門 第20回 鷲見 正人
- 小池邦人の「Carbon API 徒然草」
- 「Behind the WebObjects」 第32回 田畑 英和
SqueakではじめるSmalltalk入門 第20回 鷲見正人
本連載では、名前は知っていてもなかなか触れる機会のないSmalltalkについて、最近話題のSqueakシステムを使って紹介しています。今回は、残された“払い戻し”メソッドを定義してBankAccountを完成させます。
メソッド#withdraw:は、基本的に#deposit:と逆、つまりパラメータの値を#balanceの返値から差し引く…という挙動を示せばよいので、メッセージパターン(あるいはメソッド名)を「deposit:」から「withdraw:」に、メソッド中の計算の「+」を「-」に置き換えさえすれば、とりあえずは動きます。
deposit: aNumber
self balance: self balance + aNumber
↓
withdraw: aNumber
self balance: self balance – aNumber
ただこれだけではつまらないので、self balanceの結果が負にならないようにする制約を設けましょう。負にならないようにする方法はいくつか考えられます。ちょうどよい復習になるので、実際にコードをいろいろと考えてみてください。
一番単純なのは、前述メソッドに、「self balanceが負なら、self balanceが0になるようにする」というメッセージ式を付け加える方法。既出のメッセージの組み合わせで記述できるはずです。
withdraw: aNumber
self balance: self balance - aNumber.
self balance < 0 ifTrue: [self balance: 0]
メソッド本体は追加した式を含めて2行になります。Smalltalkでは式の区切りは「.」(ピリオド)であることを思い出してください。改行は原則としてスペース以上の意味を持ちません。1行目の終わりのピリオドを入れ忘れるとaNumberにselfというメッセージを送る式と解釈されてしまいます。さらに、前の行のselfに対する「balance: … ifTrue: …」というメッセージを送信する、2行全体がひとつのメッセージ式であると判断されてしまいます。もちろん、#balance:ifTrue:などというメソッドは定義されたことがないので、コンパイラが気を利かせてスペルチェッカを起動します。
[fig.A]1行目の終わりのピリオドを忘れたときの様子
http://squab.no-ip.com:8080/mosaren/uploads/20a.png
ピリオドや括弧の括り忘れは、このようにスペルチェッカの起動により気付かされることが多いです。スペルチェッカが、単純なスペルミスではなく、見慣れないメソッド名を指摘してきたらよく注意して勘違いや書き損じがないかコードをよく見直すべきでしょう。
他にも、self balance - aNumberの結果を一時変数に束縛しておく手があります。
withdraw: aNumber
| newBalance |
newBalance _ self balance - aNumber.
newBalance > 0
ifTrue: [self balance: newBalance]
ifFalse: [self balance: 0]
Smalltalkでは、制御構造もメッセージ式で表現し、同時にそれは“文”ではなく“式”なので返値を持ちます。したがって、まったく同じ挙動を次のように書き換えることもできます。
withdraw: aNumber
| newBalance |
newBalance _ self balance - aNumber.
self balance: (newBalance > 0 ifTrue: [newBalance] ifFalse: [0])
レシーバとパラメータを比較して大きなほうを返値とする#max:というメソッドを知っていれば、一時変数も不要になり、式自体ももっとシンプルにできそうです。
withdraw: aNumber
self balance: (self balance - aNumber max: 0)
さあ、これでBankAccountは完成です。当初のスクリプトが期待通りに動作するか確かめてみましょう。
| account |
World findATranscript: nil. "transcriptを開く"
account _ BankAccount new. "a BankAccount作成"
account balance: 1000. "1000円預金"
Transcript cr; show: account balance. "残高照会 => 1000 "
account withdraw: 600. "600円払戻"
Transcript cr; show: account balance. "残高照会 => 400 "
account deposit: 60. "60円預入"
Transcript cr; show: account balance. "残高照会 => 460 "
account withdraw: 700. "700円払戻"
Transcript cr; show: account balance. "残高照会 => 0 "
お詫びと訂正:第15回の上のスクリプトの出力結果(コメントの内容)が間違っていました。正しくはここに示したとおりです。申しわけありません。
バックナンバー:
http://squab.no-ip.com:8080/mosaren/
小池邦人の「Carbon API 徒然草」(2004/11/26)
データブラウザ・コントロールの初期化
今回は、setupCatalogWindow()から呼ばれているsetupDataBrowser()とその関連ルーチンを調べてみます。サンプルアプリケーションのメインウィンドウには、ファイル一覧を表示するためのデータブラウザ(Data Browser)コントロールが配置されています。まずは、それを初期化するための作業から解説します。
データブラウザとは、Carbon FrameworkのControl Managerに属し、何らかのデータ一覧をウィンドウ上でリスト表示やカラム表示するためのコントロールです。Finderのウィンドウではファイルやフォルダの一覧をリスト表示していますが、その時に使われているのがこのコントロールです。昔はToolBoxのList ManagerというAPI群を用いていましたが、最近ではこちらを使うのが主流です。データブラウザは純粋なコントロールなのですが、その機能を本格的に活用しようとすると、通常のコントロール(ボタンやチェックボックスなど)には存在しない複雑な処理をソースコードで記述する必要があります。
このデータブラウザを扱うためのAPI群ですが、Control Managerの中でも特にMacintosh開発者泣かせの代物でした。その第一の理由は、随分最近まで正式な解説ドキュメントが存在していなかったためです。なんと! テクニカルノートの2009番(現在はLegacyドキュメントに分類)が一番詳しい解説書であり、それのみに頼って開発するしか方法がありませんでした。そのテクニカルノートに「実装済み」と記述されている機能でも、使用してみると実に不安定で(使えるのか?使えないのか?よく分からなかった...)、加えてMac OS Xに移行した時の処理速度の遅さは尋常ではありませんでした。Mac OS X 10.0や10.1でのFinderのリスト表示の遅さを記憶されている方も多いでしょう(笑)。
データブラウザは、Mac OS X 10.2(Panther)が登場して、やっとまともに使えるようになったわけです。そして2004年の3月、待ちに待った2つの解説ドキュメントがApple社のDeveloperサイトに登録されました。そのうちの「Displaying Data in a Data Browser」には、データブラウザの使い方とそのサンプルソースが載っています。もう片方の「Data Browser Reference」では、関連しているAPIがすべて個別に解説されています。データブラウザ関連のAPIが定義されているUniversal Interfacesは、ControlDefinitions.hとControls.hです。アプリケーションの開発でデータブラウザを十二分に活用したい方は、こちらも参照してみてください。
「Displaying Data in a Data Browser」(PDFあり)
http://developer.apple.com/documentation/Carbon/Conceptual/display_databrowser/index.html
「Data Browser Reference」(PDFあり)
http://developer.apple.com/documentation/Carbon/Reference/databrow_reference/index.html
アプリケーションの開発では、ソースコード内でAPIを記述し、ダイレクトにデータブラウザを作成することも可能です。しかし、通常はボタンなどのコントロールと同様に、Interface Builderで雛形を作成してNibファイルとして保存しておきます。今回は、縦スクロールバーを持ち、テキスト用のカラムが2つあるリスト表示形式のデータブラウザを用意しました。この2つのカラムには、データブラウザに登録する画像ファイルの「ファイル名」と「ファイルタイプ」を表示することになります。カラムとは、Finderのリストウィンドウ上部で「名前」「変更日」「サイズ」などに分けられている領域のことです。カラムには、テキストの他にアイコン、時間、メニュー、チェックボックス、プログレスバー、なども表示できるのですが、今回はそれらを使用しません。後から個々のカラムを認識するためには、それぞれのカラムに識別子として4バイトのProperty ID(今回は'name'と'type')を設定しておく必要があります。
この前処理により、Nibファイルからウィンドウが作成された時点でデータブラウザも作成、配置されます。後はそれを初期化するのですが、その作業をsetupCatalogWindow()から呼ばれているsetupDataBrowser()が実行しているわけです。
#define BROW_ID 100 // データブラウザのコントロールID
short setupDataBrowser( WindowRef window )
{
ControlRef browser;
short ret=1;
DataBrowserCallbacks call; // コールバックルーチンセット用の構造体
getMyControlRef( window,BROW_ID,&browser ); // ブラウザのControlRefを得る
call.version=kDataBrowserLatestCallbacks; // コールバックのバージョン設定
if( ! InitDataBrowserCallbacks( &call ) ) // コールバックルーチンを初期化
{
call.u.v1.itemDataCallback=mySetGetItemData; // データ表示用コールバック
call.u.v1.itemNotificationCallback=myNotification; // 連絡用コールバック
SetDataBrowserCallbacks( browser,&call ); // 2つのコールバックをセット
ret=0;
}
return( ret );
}
getMyControlRef()ルーチンでは、データブラウザのID番号(100)を指定することで、そのControlRefを得ています。この仕組みを使うためには、Interface BuilderのInfoダイアログで、先んじてコントロールのSignatureとIDを設定しておく必要があります。メインウィンドウのデータブラウザには、Signatureに'MosA'、IDに100が設定されています。続いて、InitDataBrowserCallbacks()でデータブラウザ用コールバックルーチンを初期化し、SetDataBrowserCallbacks()で2つのコールバックルーチン(mySetGetItemData()とmyNotification())を登録しています。
データブラウザは、リストデータの表示や変更、ブラウザ上で起こったマウスイベント処理など、それぞれの目的に応じたコールバックルーチンを持つことでユーザの操作に対処する仕組みとなっています。そのため、目的に応じた数多くのコールバックルーチンを独自に登録する必要があるわけです。以下が、ControlDefinitions.hに定義されているコールバックルーチン登録用のDataBrowserCallbacks構造体です。現時点では10種類のルーチンが登録可能ですが、今回はこのうち、itemDataCallbackとitemNotificationCallbackの2つだけを使用します。残りのDrag&DropやContextual Menuの操作を担当するルーチンについては使用しないので登録する必要はありません。
struct DataBrowserCallbacks {
UInt32 version;
union {
struct {
DataBrowserItemDataUPP itemDataCallback;
DataBrowserItemCompareUPP itemCompareCallback;
DataBrowserItemNotificationUPP itemNotificationCallback;
DataBrowserAddDragItemUPP addDragItemCallback;
DataBrowserAcceptDragUPP acceptDragCallback;
DataBrowserReceiveDragUPP receiveDragCallback;
DataBrowserPostProcessDragUPP postProcessDragCallback;
DataBrowserItemHelpContentUPP itemHelpContentCallback;
DataBrowserGetContextualMenuUPP getContextualMenuCallback;
DataBrowserSelectContextualMenuUPP selectContextualMenuCallback;
} v1;
} u;
};
登録する2つのコールバックルーチンのうち、itemDataCallbackの方へ代入するルーチンは、リスト表示させるデータをシステムへ与える(セット)処理と、変更されたデータをシステムから得て(ゲット)アプリケーションで再保存するための処理を担当します。また、itemNotificationCallbackへ代入するルルーチンの方は、データブラウザ上で発生した各種イベント(データ選択やクリック、キー入力など)の処理を担当しています。
次回は、DataBrowserCallbacks構造体に代入してSetDataBrowserCallbacks()で登録した2種類のコールバックルーチン、mySetGetItemData()とmyNotification()について詳しく解説したいと思います。
つづく
「Behind the WebObjects」 第32回 田畑 英和
今年の湘南ミーティング無事終了しました。WebObjectsのセッションに参加していただいた方々ありがとうございます。ここ数年は毎年WebObjectsのセッションを担当してきましたが、はたして来年はどうなることでしょう。
次期Mac OS X(Tiger)ではEOFの再来ともいえるCore Dataが搭載されることになっていますので、適切な次期がきたらこの連載でも取り上げてみたいと思います。
さて、湘南ミーティングではカスタムEOの生成方法やテスト手法について解説しましたが、今回はカスタムEOの実装方法について取り上げてみたいと思います。
カスタムEOの初期化
まずはカスタムEOの初期化について解説します。EOを新規に作成したときに特定のアトリビュートを初期化したい場合があります。例えばカスタムEOを作成した日時を設定するとか、nullを許可していないアトリビュートになにか適切な値をデフォルト値として設定するなどの処理が考えられます。
通常オブジェクトの初期化をおこなうにはコンストラクタの利用が考えられますが、カスタムEOの場合コンストラクタに初期化のコードを記述してはいけません。例えばカスタムEOのMovieクラスに以下のようなコードを実装したとしましょう。
public class Movie extends EOGenericRecord {
public Movie() {
super();
setRevenue(new BigDecimal(0));
}
}
このコンストラクタでは、オブジェクトが生成されたときに"revenue"アトリビュートを初期化しています。たしかにEOを*新規*に追加する場合はこれでよいのですが、このコンストラクタはEOを新規に追加したときだけではなく、データベースから既存のレコードを取得してEO化したときにも呼び出されます。
ですので、既存のレコードにまでコンストラクタで初期化処理をおこなってしまうと、データを上書きしてしまうことになります。
EOを新規に追加するたびに以下のようなコードを実装してもよいのですが、これでは同様のコードを何カ所でも実装することになり、毎回同じ初期化処理をおこなうには効率的ではありません。
Movie movie = new Movie();
session().defaultEditingContext().insertObject(movie);
movie.setRevenue(new BigDecimal(0));
新規追加時の初期化
そこでEOCustomObjectクラス(EOGenericRecordの親クラス)にはEOを新規に追加する場合にのみ呼び出される、以下の初期化用メソッドが用意されています。つまりカスタムEOの内部に初期化処理を埋め込むことができるのです。
・新規追加時の初期化メソッド
public void awakeFromInsertion(EOEditingContext ec)
このメソッドは、EOをEditingContextに追加する時に呼び出されますので、既存のレコードをEO化したときには呼び出されません。このメソッドを利用すると初期化のコードは以下のように書き換えることができます。このときまず親クラスのawakeFromInsertion()を呼び出すように実装してください。
public class Movie extends EOGenericRecord {
public Movie() {
super();
}
public void awakeFromInsertion(EOEditingContext ec) {
super.awakeFromInsertion(ec);
setRevenue(new BigDecimal(0));
}
}
既存データの初期化
またデータベースから取得したデータをEO化したときにのみ呼び出されるメソッドも用意されています。このメソッドを利用すればデータベースからデータを取得するたびに、任意の処理を適用することができます。
・既存のデータの初期化メソッド
public void awakeFromFetch(EOEditingContext ec)
まとめ
カスタムEOのコンストラクタはEOを新規に追加する場合と既存データからEOを作成する場合の両方で呼び出されるため初期化の処理には不適当です。そこでEOCustomObjectクラスで用意されている初期化のメソッドを用いることになります。初期化用のメソッドはEOの新規追加用と、既存のデータ用の両方が用意されています。
さらに初期化処理を工夫するには、初期化用のデータをPropertiesファイルから読み込む方法や、EOModelerのEOEntity UserInfo Inspectorで設定した値を読み込む方法などが考えられます。状況に応じて初期化の値を動的に変更するようなケースも考えられますが、このような処理をおこなうには独自に対応する必要があります。
MOSAからのお知らせと編集後記は割愛します
Apple、Mac OSは米国アップルコンピュータ社の登録商標です。またそのほかの各製品名等はそれぞれ各社の商標ならびに登録商標です。
このメールの再配信、および掲載された記事の無断転載を禁じます。
http://www.mosa.gr.jp/
Copyright (C)2004-2006 MOSA. All rights reserved.