2008年12月16日火曜日

[VC++ 2008 Express] Formのnamespaceを変更するとランタイムエラーが出る

VC++ 2008 Express Editionでフォームアプリケーションを作成すると、formのソースコードにnamespaceが定義される。このnamespaceはプロジェクト名になっているのであるが、変更したくなることもあるだろう。

namespaceを変更してもビルドも成功するのであるが、実行時に「指定されたカルチャまたはニュートラル カルチャに対して適切なリソースが見つかりませんでした。…」のようなエラーが発生する。
このエラーの発生しているのは、formのなかでリソースを読み取るところ、実際のコードでは「System::ComponentModel::ComponentResourceManager^ resources = …」となっている場所になる。

なぜエラーになるかというと、ComponentResourceManagerを作成するところで、typeidをベースに読み取るのであるが、typeidはnamespaceに依存する。すなわち、<namespace>.<クラス名>となっている。
しかしながら、コンパイルされたリソース(マネージリソース)は、ソースコードで変更されたnamespaceが反映されないため、ランタイム時にリソースを読み込むことができずエラーとなるという仕組みなようだ。

そこで、マネージリソースのコンパイル設定を変えればうまくいくはずで、プロジェクトのプロパティでは、リソースファイル名の設定が「$(IntDir)\$(RootNamespace).$(InputName).resources」となっている。
すなわち、「$(RootNamespace)」の部分が変更したnamespaceになっていればよいということになる。

ひとつの解決方法としては、RootNamespaceの値の参照をやめて、変更したnamespaceに変えてしまうというのがある。要するに「$(RootNamespace)」を書き換えるということである。

どうせなら、RootNamespaceの値を変更したほうがよさそうだと思うのだが、IDE上で変更できそうな画面が見つからない。仕方がないので、プロジェクトの定義ファイル(.vcproj)をテキストエディタで変更する。変更箇所は簡単に見つかるはずで、RootNamespace="…"となっているところを書き換える。

といっても、ある意味無理やり変更しているので、IDE上でなんだかの不都合が生じる可能性もあるので、あくまでも自己責任の範囲でやってみてください。

2008年11月28日金曜日

[VC++ 2008 Express] FormのresXにリソースを追加しても消されてしまう

VC++のフォームアプリケーションで、フォームとは無関係なリソース(例えばイメージ)をそのフォームのresXに追加することはできるのですが、何かの拍子に消えてしまいます(おそらく、フォームデザイナがヘッダーファイルを生成ときのタイミングだと思います)。

VC++のフォームプロジェクトにあるフォームは、ソリューションエクスプローラー内においては、ソースコードは「<フォーム名>.h」でその下の階層に「<フォーム名>.resX」という表示になっています。このresXを開くとリソースの一覧が表示されて、編集可能となっています。そこで、イメージなどのリソースを追加できるのですが、何故か消えてしまうのです。

そこで、フォームとは別なところにリソースを定義して、プログラムで読み取ろうということですが、少なくとも「rcファイル」はmanagedなプロジェクトでは意味がありません。なぜ、Formアプリケーションのプロジェクトに存在しているかはわかりませんのは、おそらくアプリケーションのアイコンの設定するためなのだが、.NETのドキュメントを読むとrcファイルのリソースは利用できませんと書いてあります。

managedなモジュール(Assemblyといったほうが適切か…)にこの手のリソースを組み込む方法はいくつか存在するようですが、新たにresXファイルを追加してそこにリソースを定義するという方法をやってみました。

空のresXファイルを作る

プロジェクトに組み込むresXファイルを用意します。resXといっても所詮XMLファイルなので、ベタな方法として「メモ帳」で作ることにします。ただし、中身がresXの定義に則してないとIDE上で編集できないですし、ビルド時にエラーになるので、最低限の定義だけを書いておきます。

必要な部分だけ、プロジェクト内にあるresXからコピペするのですが、まず、プロジェクトにある(エラーになっていない)resXファイルをXMLエディタで開きます(コンテキストメニューから「ファイルを開くアプリケーションの選択」というのを選ぶ)。そのresXファイルから、最初の「<?xml version="1.0" encoding="utf-8"?>」から、「</resheader>」の部分まで(たぶん、「<resheader name="writer">」の対になっているのが最後のなので、そこまでをコピー、ペーストします。そのあとの行に「</root>」でrootを閉じればOKです。
ファイルは、拡張子をresXにしてUTF-8で保存します。拡張子よりも前の部分(いわゆるbasename)はプログラムで読み込むときに指定するので、わかり易いものにしておいたほうがよいでしょう。

プロジェクトに追加する

作った空のresXファイルをプロジェクトのコンテキストメニューで「追加→既存の項目」で追加すると、ソリューションエクスプローラー上でプロジェクトの下の「リソースファイル」というツリーの中に現れるはずです。そうしたら、そのresXファイルを開くと、resX用のリソース定義用のビュー(マネージリソースエディタ)が表示されます。
そこで、好きなリソースを追加してください。

リソースの読み込み

ManagedなC++のコードの中でリソースを読み込む方法ですが、まずResourceManagerでresXに定義されたリソースを読み込みます。resXファイル名のbasenameを「リソース名」ということにすると(「hoge.resX」なら「hoge」)。

//アセンブリを取得する
System::Reflection::Assembly^ assembly = System::Reflection::Assembly::GetExecutingAssembly();
//リソースを読み込む
System::Resources::ResourceManager^ resources = 
 gcnew System::Resources::ResourceManager("<アセンブリ名>.<リソース名>", assembly);

ここでの「アセンブリ名」といっているのは(この言い方が正しいかは不明)、普通はプロジェクト名になっているのでプロジェクト名にします。

あとは、ResourceManagerなどのドキュメント、サンプルを見ると使い方がわかりますが、例として、イメージを読み込む場合は以下のような感じになります。

//リソースからイメージを読み込む
System::Drawing::Bitmap^ image = (System::Drawing::Bitmap^)(resources->GetObject(L"<リソースオブジェクト名>"));

ここでの「リソースオブジェクト名」は、resXの定義で命名したものを指定します。マネージリソースエディタでイメージファイルを読み込むとファイル名のbasenameになりますが、エディタ上で変更できます。

この場合、リソースファイルを手動で追加しましたが、いわゆる国際化(カルチャー毎の設定)も対応しているはずなので、ファイル名にカルチャー名を付加したresXファイルを用意しておけば、ResourceManagerで適切に処理されるはずです。

2008年11月26日水曜日

[.NET]バインドされたDataGridViewに追加した項目を選択状態にする

先ほどの記事でDataGridViewとBindingSourceおよびDataTableの関係について書いたが、それを調べるための目的は、DataGridViewにバインドされたDataTableにプログラムで行を追加したときにその項目を選択させるためだったので、その方法を書いてみることにする。

実際にこのような動作をするプログラムを書かないと、状況がつかめないかもしれないが、DataGridViewは、バインドされたDataTableに新しく行を追加しても、選択位置は一番最初のままである。そのため、新しく追加された項目は、スクロールアウトされていると表示されない。しかも、DataGridViewは選択されている項目を表示させようとするため、さらに行が追加されると、選択されている行がスクロールアウトされていても選択行を表示させるためスクロールする。

すなわち、新しい行を表示させるためにはDataGridViewの選択行を変更しなければならない。ここで前提としては、DataGridViewの選択モードは、FullRowSelectになっているものとする。CellSelectでもうまくいくと思われるが試してはいない。

以下のコードはC++で書くことにするが、VC++2008 Express EditionでDataTableやDataSetをデータデザイナを使って定義する方法は、こちらの記事を参照されたい。

まず、データデザイナで定義をするとヘッダファイルにDataSetのサブクラスが作られて、その中にDataTableのサブクラスを定義するようなコードを生成するが、ここでは、"MyDataSet"や"MyDataTable"のように"My"+クラス名というものを定義したとする。

あと、DataTableの定義の前提として、主キーでAutoIncrementされる列(Column)を定義し、カラム名は"ID"とする。あと、定義されたDataSet(すなわち"MyDataSet")は、Formのメンバー変数として、「MyDataSet^ myDataSet;」と定義され、コンストラクタのどこかで、「myDataSet = gcnew MyDataSet();」のようにインスタンスを生成しており、また、フォームデザイナでは、BindingSourceを"myBindingSource"と定義したとする。

とりあえず、コードは以下の通り

//新しい行を生成する
MyDataSet::MyDataTableRow^ newRow = myDataSet->MyDataTable->NewMyDataTableRow();

//ここでnewRowのメンバー(カラム)にデータを代入する
//newRow-><カラム名> = …;
//カラム"ID"は追加時に設定されるので何もしなくてよい

//行をDataTableに追加する
myDataSet->MyDataTable->Rows->Add(newRow);
myDataSet->MyDataTable->AcceptChanges();

//追加した行のBindingSourceの位置を取得する
int position = myBindingSource->Find(L"ID", newRow->ID);

//BindingSourceの現在の位置を変更する
myBindingSource->Position = position;

ここでの最大のポイントは、BindingSourceの現在の位置はDataGridViewの選択行に一致するのだが、項目がソートされているとDataTableの行(Row)のインデックスとは一致しない。そのため、新しく追加した項目がBindingSourceでどの位置にあるかを調べてから設定するということである。

あと、このコードでの"newRow"はAdd();で"MyDataTable"に追加すると、"ID"にAutoIncrementされた値が代入される。そのため、この値を使って"myBindingSource"のFind();を呼び出せば、位置がわかるということになる。

[.NET]DataGridViewのデータについて

前記事でVC++ 2008 ExpressでDataGridViewにDataSetをバインドする方法について書いたが、その後DataGridViewを操作しようとしたところ、多少はまってしまった。ドキュメントを読んでわかったことだが、DataGridViewと表示・編集しようとしているデータとの関係について書いてみることにする。

DataGridViewに表示されるデータがどこにあるのか、という意味で3つのパターンがある。

普通のモード
普通という言い方が正しいかどうかはわからないが、単にDataGridViewのインスタンスを生成した場合の状態で、対象となるデータはDataGridViewの中に存在する。具体的には、DataGridView.RowsプロパティであるDataGridViewRowCollectionの中に行のデータが格納されている。
バインディングモード
これは、前記事で書いた、DataSetなどに存在するデータをDataGridViewに表示・編集する方法で、具体的には、DataGridView.DataSourceプロパティにデータの実体があるオブジェクトを設定する。VC++やVC#のIDEでデータデザイナとフォームデザイナを使った場合には、これらの設定はIDEによってコード化されるので、特にコードを書く必要はない。
仮想モード
このモードについては、MSDNなどのドキュメントにいろいろ書かれているので、詳細はそちらへ譲るが、基本的にはDataGridViewが表示・編集のためにデータの参照・設定が必要になった場合、イベントが発生する。そのイベントの処理でデータを受け渡しなどをする。この場合、データの実体はプログラマー任せとなる。

実際には、編集などではもっと複雑なデータのやりとりをするようだが、特殊なことをしない限り、あまり意識する必要はないもよう。この手のことは、DataGridViewのドキュメントの中に「共有モード」という記述があるので、それを参照されたい。

ここのでポイントは、バインディングや仮想モードになっているときには、DataGridView.Rowsプロパティにデータが存在しないということである。要するにこのプロパティを使ってデータを操作しているドキュメント上のサンプルなどは、普通のモードで動いているDataGridViewでの話であって、バインディングなどの場合には通用しない。

ところで、バインディングの場合、DataSourceは、基本的にはBindingSourceのインスタンスを設定する。このBindingSourceに実際のデータをBindingSource.DataSourceに設定する。このことは、ドキュメントに詳しく書かれているので詳細は割愛するが、BindingSource.DataSourceには、DataSetなどの複雑な構造のデータや、単純なオブジェクトのリスト(System.Collections.Generic.Listなど)を設定することができる。

DataGridViewで編集などをする場合には、行やセルの選択をいう操作が必要になるのだが、BindingSource.DataSourceに設定されるデータは、列挙可能(IListが実装されている)なものでしかないので、選択された位置という情報は持っていない。すなわち、BindingSourceが選択や項目の並び替え(ソート)を担っているということになる。

プログラムで項目を選択したい場合、普通のモードのDataGridViewときには、Rowsプロパティから行に対するデータを取得して、セルや行(Row)に対して選択を設定する。ところが、バインディングモードの場合、DataGridView.Rowsプロパティを使うのではなく、DataSourceプロパティに設定されたBindingSourceを操作する。具体的にはPositionプロパティやMoveFirstメソッドなどで現在の位置を変更する。

DataGridView.DataSourceにBindingSourceを設定している場合、プログラムでBindingSourceの現在の位置を変更すると、DataGridViewの表示上の選択位置が変更され、マウス操作などでDataGridViewの選択位置が変更されるとBindingSourceの現在の位置が変更される、という仕組みになっている。

2008年10月12日日曜日

VC++ 2008 Express でDataGridViewとDataSourceを使ってみる

C#でDataSourceを使ってDataGridViewに表示させるサンプルは、ドキュメントに書かれているようですが、C++については載ってなかったので、勘でコードを書いて見たのですが、それでもちゃんと動いているようなので、簡単なやり方を書いてみます。ただし、この方法が最適なものかはわかりません。とりあえずです…。

まずは、テーブルの定義をどこかでやるのですが、VC++でサポートされているデータベースであれば、接続してテーブル定義を引っ張ることができるようですが、そうでない場合、データデザイナを使って定義したいところですが、VC++でサポートされていないということになってます。
とりあえず、VC# Express Editionであれば、データデザイナでテーブル定義ができるのそれで定義します。そうすると、xsdファイルが生成されるので、これをVC++のプロジェクトに追加します(プロジェクトのディレクトリにコピーして、既存追加したほうがよいかも)。

そうすると、xsdファイルを開くとデータデザイナらしき画面が表示されます。ここでも編集が可能なようです。さらにxsc、xssとヘッダファイルが生成されます。xscとxssはソリューションエクスプローラー上では、エラーを示すアイコンが表示されているのですが、C++のビルドにはこれらは使わなくてもよさそうなので、そのまま放置しておきます。
生成されるヘッダーですが、中身をみる限りちゃんと定義されたテーブルどおりのDataSetやDataTableのコードになっていて、コンパイルもできます。ただ、このヘッダファイルをプリコンパイルヘッダ(stdafx.h)に追加するようですが、そのままだと何故か致命的なコンパイルエラーが出て失敗するのですが、それを外してコンパイルすればビルドが成功します(これらのクラスを使うソースコードに普通にインクルードする)。

DataGridViewなどに表示されるためには、BindingSourceを生成して、テーブルデザイナで定義したDataSetを割り当てればよいです。この操作は、フォームのデザイナを使って、BindingSourceをツールボックスなどで作成して、DataSourceプロパティに定義したDataSetを設定すればよいです。DataSetがうまくコンパイルできていれば一覧に表れるはずです。

ここまでうまくいけば、C#でBindingSourceを使ってデータを表示される手順と同じ要領で、Formのコンストラクタかどこかで、BindingSourceのDataSourceプロパティにDataSetのインスタンスを設定します。すなわち、生成されたヘッダに定義されているDataSetのサブクラスをgcnewしたものをフォームデザイナで生成したBindingSourceのインスタンスのDataSourceに代入するということです。ちなみにフォームのデザイナは、BindingSourceのDataSourceに定義したDataSetのタイプIDを代入していますが、これはそのままにしておいて、そのコードの後に実行されるどこかで、インスタンスをDataSourceに代入してしまってもよいみたいです。

その後コードについては、ドキュメントに書いてあるC#のコードをC++に書き直すつもりでコーディングしていけばうまくいくはずです。簡単な例とすれば、DataSetのサブクラスに定義したDataTableのサブクラスのインスタンスが取得できるプロパティがあるので、そのDataTableのRowsプロパティに、定義されたDataRowのサブクラスのインスタンス(生成はDataTableのNewRowに相当するメソッドが定義されているのでそれを呼び出す)をAddすると項目が追加されると、いった感じです。

2008年8月4日月曜日

Managed DirectXでのサウンドキャプチャ(2)

前記事でVC++でManaged DirectSoundを使ってサウンドをキャプチャする方法について書いたのだが、アンマネージドでキャプチャされたデータを取得するところが曖昧だったので、ちゃんと書いてみる。

キャプチャバッファからデータを取得する場合、読み取り用のメソッド(CaptureBuffer::Read)でArrayかStreamのいずれかを得ることができるが、最終的にアンマネージドなコードで処理するために、キャプチャされたデータが格納されているバッファへのポインタ(const unsigned char*)を取得したいとする。

マネージドなデータをアンマネージドなコードから参照する手段は、pin_ptr<>を使用するとして、その場合、array<>が必要になるわけだが、メソッドとして、ArrayかStreamのいずれかの選択肢がある中、どれを使ってpin_ptr<>を得るのがよいのかということを試行した結果、MemoryStreamを使って、CaptureBuffer::Readを呼び出す方法が無難であるという結論になった。

以下のコードは前回の最後のコードの続きとなるが、最終的には、16bitデータ(const short*)へのポインタを得ている。

int size;//キャプチャされたデータのサイズ[byte]
array<unsigned char>^ dataBuffer = gcnew array<unsigned char>(size);
MemoryStream^ dataStream = gcnew MemoryStream(dataBuffer);
captureBuffer->Read(lastReadPos, dataStream, size, LockFlag::None);
pin_ptr<unsigned char> ptr = &dataBuffer[0];
const short* sampleBuffer = (const short*) ptr;

ここで注意することとしては、const short*にキャストしているので、実際のデータはsizeの半分(sizeof(short)分の1)になっていることと、キャプチャしているスレッドで時間がかかる処理をすると、データを取りこぼす可能性があるということである。
キャプチャの取りこぼしを避けるための無難な手段としては、取り込んだデータ(array<unsigned char>^)をキュー(Queue)に入れて、別のスレッドで取り込む方法が考えられる。ただし、CLIで用意されているキューは同期化はサポートされているものの、空の状態のブロッキングはしてくれないので、なんだかの同期オブジェクト(AutoResetEventなど)を利用したほうがよいかもしれない。

2008年7月15日火曜日

Managed DirectXでサウンドをキャプチャする

とりあえず、VC++ 2008 Express Editionでサウンドをキャプチャするコードを書いてみることになったのだが、折角なのでManaged DirectX(MDX)のDirectSoundを使ってみることにした。
コードは、VC++ではあるが、CLRで動作するので、C#などの他言語でもほぼ同じコードになるはずである。
MDXのDirectSoundを利用するには、「Microsoft.DirectX.DirectSound」という、.NETの参照をプロジェクトに追加しなければならない。もしこれが「参照の追加」の一覧で見つからなければ、DX9以降のランタイムかSDKをインストールする。

MDXとはいえ、所詮DirectXをラップしたようなものであるがゆえに、基本的な概念はDirectSoundに由来する。もし、DirectSound関連のドキュメントを読んだことがなければ、一読することをおすすめする。
というわけで、ManagedなDirectSoundを利用するに当たり、知っていたほうがよいと思われること(アンマネージドのDirectSoundとの違いも含む)は、以下のことであると思われる。

CaptureはIDirectSound8
サウンドデバイス関連を司っていると考えてよい。
CaptureBufferはIDirectSoundCaptureBuffer8に相当
ただし、使い方はManaged風になっているため多少違う。
非同期用のイベント(IDirectSoundNotify8)はNotify
イベントの処理は、System.Threadingにあるものを使う。

まずは、Captureクラスだが、IDirectSound8とほとんど同じ様なものなので、特に説明はしない。単純にデフォルトキャプチャデバイス(コントロールパネルのサウンドで定義されている)を使って、キャプチャするには以下のようなコードをになる。

Capture^ capture = gcnew Capture();

次にCaptureBufferクラスだが、これもやっていることは同じで、「CaptureBufferDescription」という構造体(DSCBUFFERDESCに相当)に必要な情報を入れて、インスタンスを作成すればよい。エフェクトを利用しない場合のバッファの作成は以下のようになる。

CaptureBufferDescription desc;
desc.ControlEffects = false;
desc.WaveMapped = true;
//GetWaveFormat()はWaveFormat構造体を返す関数
desc.Format = GetWaveFormat(44100, 2, 16);
//BufferSecondsはバッファの秒数
desc.BufferBytes = desc.Format.AverageBytesPerSecond * BufferSeconds;

CaptureBuffer^ captureBuffer = gcnew CaptureBuffer(desc, capture);

ちなみに上記のGetWaveFormat()は、誰が書いても同じようなコードになると思われるが、いちおうこんな感じ。MDXの「WaveFormat」は、DXの「WAVEFORMATEX」と同じ。

WaveFormat GetWaveFormat(int rate, int channel, int rez)
{
 WaveFormat format;
 format.FormatTag = WaveFormatTag::Pcm;
 format.SamplesPerSecond = rate;
 format.Channels = channel;
 format.BitsPerSample = rez;
 format.BlockAlign = format.Channels * format.BitsPerSample / 8;
 format.AverageBytesPerSecond = format.SamplesPerSecond * format.BlockAlign;
 return format;
}

バッファの更新通知の設定については、DXの「IDirectSoundNotify8::SetNotificationPositions()」を使って設定する方法とイメージは同じだが、登録するためのイベントハンドルは、Managedなものを使う。基本的には、「AutoResetEvent」を使うようなのでここでもこれを使って登録する。

AutoResetEvent^ autoResetEvent = gcnew AutoResetEvent(false);
// BufferSecondsはバッファの秒数
// CapturePerSecondは1秒間に通知してほしい回数
int notifications = BufferSeconds * CapturePerSecond;
array^ bufferPositionNotifies = gcnew array(notifications);
for (int i=0; i<notifications; i++) {
 bufferPositionNotifies[i].EventNotifyHandle = autoResetEvent->Handle;
 bufferPositionNotifies[i].Offset = 
  format.AverageBytesPerSecond * (i+1) / CapturePerSecond - 1;
}
Notify^ notify = gcnew Notify(captureBuffer);
notify->SetNotificationPositions(bufferPositionNotifies);

通知を登録したので、別スレッドでイベントを待って、キャプチャされたデータを取得する。以下の関数は、スレッド用の関数となる。
ちなみにDXの場合、「IDirectSoundCaptureBuffer8::Lock()」を使ってデータを得るのであるが、この関数は露骨にリングバッファから読み取る仕様になっている。MDXの場合は、単純にArrayまたはSystem::IO::Streamに読み込むことができる。以下の例は、shortが格納されているArrayに読み込むようにしている。

void CaptureThreadProc()
{
 int bufferSize = captureBuffer->Caps.BufferBytes;
 int capturePos, lastReadPos;
 captureBuffer->GetCurrentPosition(capturePos, lastReadPos);
 do {
  autoResetEvent->WaitOne();
  int readPos;
  captureBuffer->GetCurrentPosition(capturePos, readPos);
  int size = readPos - lastReadPos;
  if (size == 0) continue;
  if (size < 0) size += bufferSize;
  // shortで読む場合、読み込むサイズをsizeof(short)で割らないとエラーになる
  Array^ captureData = 
   captureBuffer->Read(lastReadPos, Type::GetType("System.Int16"), LockFlag::None, size / 2);
  //ここでcaptureDataにあるデータを処理する
  lastReadPos = readPos;
 } while(captureBuffer->Capturing);
}

ちなみにこの関数では、キャプチャが停止するとループを抜けてスレッドが終了するようにしている。そのため、キャプチャ開始前に(CaptureBuffer::Start()を呼ぶ前)にスレッドを作成して走らせておく必要がある。
この場合、Arrayにデータが入ってくるのであるが、実際のデータはGC内にあるため、ネイティブ(アンマネージド)で処理をする場合には、ArrayのGetEnumerator()でEnumeratorを得てからひとつずつ取り出すか、array<>にしてから、pin_ptr<>を使ってポインタを得て処理するという方法が考えられる。

あと、キャプチャの開始、停止は、CaptureBuffer::Start()、CaptureBuffer::Stop()を呼び出せばよい。この場合にはスレッドで読み込み続けるのでStart(true)となる。

2008年4月27日日曜日

GCCを使ったJNIなどのDLLでSSE命令を使うときの問題点

JavaからJNIでDLLを呼び出せるので、JavaVMで処理に時間がかかりそうな処理をC/C++で開発してみようと思うのは普通だと思うし、どうせならMMXやSSEなどで高速に演算させたいと思うこともある。
GCCには、MMX、SSEなどの命令を呼び出すためのAPI(__builtin_ia32_hogehoge())があるので、これらを使えば、わざわざアセンブラのお世話にならずとも実現することが可能である。ということで、JNIなDLLをSSE命令を使って開発するのであるが、これが一筋縄に行かないようなのである。

SSEには、メモリー上のデーターをアクセスするための命令に128bitアライメントされているデーター用とそうでないもの用の2つがある(MOVAPSとMOVUPSなど)。要するに、128bitアライメントされているほうが高速にアクセスできるということであるが、もし、アライメントがされていないデーターにアクセスすれば、いわゆるソフトウェア割り込みが発生してアプリケーションエラーとなってしまう。
これらのアライメントを前提とする命令を使うには、コード上データーがアライメント上にあることが保証されているなければならないということになる。

普通、C/C++でプログラムするときには、データーがメモリ上どう配置されるかということを気にすることなく、もし、なんだかの制約があるとすれば、コンパイラーが何とかしてくれる(それ用のオプションをつけて実行するとか)ことにしている。
ただし、デフォルトでない特殊なアライメントなどを利用したい場合には、GCCでは__attribute__((aligned(n)))のような属性をつけられる。これは例えば、char data[256] __attribute__((aligned(16))) = {0};のように使うのだが、この例では、このchar配列を128bit(16byte)上に配置するようにしていている。

ただし、このように書けるのはC/C++上での静的な変数に対してであり、動的に得る場合(mallocやnewで生成する領域)では、このような属性では制御できない。ただ、C/C++の場合、ポインターはメモリーアドレスそのものであると考えられるので、ポインターをずらして、合わせてしまうという事が可能である(本質的にはAPI側でなんとかしてほしいところだが)。

このようにすれば、SSEでのアライメントの問題が解決するということにしたいのだが、実際にはそういう訳にはいかない。というのが、本題であったりする。
ここで根が深いのは、基本的にはコンパイラーのオプションや属性の指定で解決されているべきものが、実際にはアライメントされていないということである。
アセンブラ以外でアライメントなどを考慮しなくてはいけないのは、C/C++であるとしてでもおかしな話であると思うが、CPU固有の命令を使おうとしているので、なんだかの特殊な処理が必要であると考えても仕方がないかもしれない。確実に動くコードが書ければ、それはそれで問題ないといえるだろう。

ところで、C/C++で変数といわれるものは2つあり、それはいわゆるstaticで宣言されるものと、関数やブロックのなかで宣言されるいわゆるauto変数といわれるものである。
staticな変数は、モジュールがロードされるときに確保され、その領域はモジュールがなくなるまで存在する。この変数は、その領域のアドレスをどこかに保持し、領域内の相対アドレスをコード内に持って、それを足してアドレスを確定してアクセスする。
ここでアライメントを保持するには、領域の先頭のアドレスをアライメントされるように配置して、相対アドレスもアライメントを保持するように割り当てるようにする。
コンパイラーでオプションや属性で指定するアライメントは、相対アドレスにを指定するものであり、領域のアドレスはモジュールのロードにアライメントされて確保されるので、問題なく使える。

auto変数は、スタックといわれる領域に確保される。これは、関数が呼び出されるときのスタックのアドレス(スタックポインター)を基に相対的に割り当てられる。
autoな変数のアライメントは、staticと同様に相対アドレスは、コンパイラーが割り当てて、スタックは、モジュールのロード後、最初の関数を呼び出すときにアライメントされるように割り当てられるようになっている。その後、呼び出される関数もアライメントするように配置することによって、アライメントを維持するようにしている。
そうすると、staticな変数と同様にauto変数も問題なく動くはずなのだが…。

最初に述べたのだが、DLLでSSEを使うときの問題があるということなのだが、それは、DLLのスタックは、呼ばれる元のモジュールのスタックを引き継いで利用する。これは、C/C++で関数を呼び出すために必要な仕組みである。

もし、C/C++コード上でアライメントが不明であれば、アライメントを前提としない命令を使えばよいのだが、コンパイラーはauto変数に割り当てられたSSE用の変数を、アライメントされているものとして、読み書きするような命令を出力するのである。
ということで、おわかりであると思うが、DLLを呼び出すモジュールがアライメントを維持しないようにスタックを使っている場合、DLLでアライメントを保持するようになっていても、うまく動かないということになるわけである。それは、JNIがJavaVMから呼び出されるときも起きていて、JNIのDLLでSSE命令を使おうとすると、ほとんどうまくいかない(うまくいくのは、SSE用の変数がすべてレジスタ上に配置されたときか、運良くスタックがアライメントされているときだけ)。

どうやら、このことを問題だと思っている人が多く居るらしく、GCCでこれを回避するオプションを作ることが想定されているようだが、まだ実装されていないもよう。ましては、MinGWでの移植にも時間がかかるため、おそらくMinGWのGCC環境では、うまく動くコードをはくコンパイラーはしばらく先になるだろう。

2008年4月14日月曜日

PHPでAJAXをやってみる

PEARにHTML_AJAXというパッケージがあるが、それを使わずにいわゆるAJAXをやってみるということです。
とりあえずということで、基本的なことだけかもしれませんが、いちおう動いているコードを書いてみます。PHPとかいっている割には、大半がJavascriptな内容かもしれません。

AJAXとはなにか?ということは、世の偉い人があちこちで解説されているので、ここでは説明しませんが、基本的には「非同期でサーバーから情報を取る」というのと、 「サーバーがXMLドキュメントを送る」、「XMLドキュメントをJavascriptで処理をしてブラウザに表示」という3つであると思って書きます。

非同期でサーバーから読み出す

まずは、Javascriptの「XMLHttpRequest」のオブジェクトを作成して、それを使ってデーターを読み込むという手順になります。XMLHttpRequestを作成するコードは以下のような感じ(どこかのコードのパクリかもしれないが…)。

function createHttpRequest(){
 if(window.ActiveXObject){
  try {
   return new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
   try {
    return new ActiveXObject("Microsoft.XMLHTTP");
   } catch (e2) {
                return null;
            }
         }
    } else if(window.XMLHttpRequest){
        return new XMLHttpRequest();
    } else {
  return null;
    }
}

という関数を定義して、var request = createHttpRequest();とすれば、XHTMLRequestのオブジェクトが得られる。

次にデータを取得するわけだが、クラス化されたJavascriptで処理をするために以下のような関数を作ってみた。

function sendRequest(obj, callback, url) {
 var request = createHttpRequest();
 if (request == null) return false;
 request.onreadystatechange = function() {
  callback.call(obj, request);
 }
 request.open('GET', url, true);
 request.send(null);
 return true;
}

データーの受信は、非同期で行われるため、コールバック関数を用意して、読み込みが完了したら、コールバック関数が呼び出されて、そこからデーターを読み取るという処理になる。
この関数を使うためのクラスは、以下のようになる。

function MyClass(){
 this.callback = function(request) {
  if (request.readyState == 4) {
   if (request.status == 200) {
    var xmlDoc = request.responseXML;
    //このxmlDocから情報を読み取る
   }
  }
 }
}

ちなみにrequest.readyStateというのは、1から4までが設定されて呼び出されるのだが、4というのが完了したということになる。あと、request.statusを確認するも必須で、これはHTTPのリザルトコードになっている。すなわち、200で正常に通信したということであり、そのほかはなんだかのエラーということになる(エラー処理はここでは省略…)。このクラスと先ほどの関数を使ったコードは以下の通り。

var url;//データーを持ってくるためのURL
var obj = new MyClass();
sendRequest(obj, obj.callback, url);

この関数は、MyClassの中で定義してしまってもよいかと思います。そのときは、objはthisになります。

サーバー側の処理

サーバーから送るデーターはXMLになっていれば、Javascriptで値が取り出せるということになるわけだが、例えばa=1とt=testというデーターを送りたいのであれば、

<?xml version="1.0"?>
<result>
 <a>1</a>
 <t>test</t>
</result>

というXMLを送ればよいということになる。ちなみにコード系がUTF-8以外ならば、versionのあとにcharsetを設定しなければならないだろう。
要するに、このようなXMLを返すのであれば、なんでもいいのであるが、PHPでなんだかの処理をして返すということにした場合、PHPのソースのどこかで、HTTPヘッダを設定しなければならず、header("Content-Type: text/xml");と書けばよいということになる(application/xmlというタイプもあるが、使い分けはよくわかってません…)。

XMLドキュメントをPHPでどう生成するかということになるわけだが、ごりごりと自分で上記のようなXMLを出力するコードを書いて、送るのでもよい。そのへんは規模にもよるとは思うが、PHPに組み込まれている関数を使ったほうがよい場合もあると思う。
ここでは、本来の使い方ではないかもしれないが、「DOM XML 関数」というPHPの組み込み関数を使うことにする。具体的には「DOMDocument」というクラスのオブジェクトを生成して、そこにXMLのデーターをセットして、saveXML();というメソッドでXMLの文字列を得る。それをechoして、クライアントに送るという手順になる。
先ほどのXMLと同じ内容を出力するコードは以下のようになる。

header("Content-Type: text/xml");
$doc = new DOMDocument();
$resultElement = $doc->createElement('result');
$doc->appendChild(resultElement);
$aElement = $doc->createElement('a');
$aElement->appendChild($doc->createTextNode('1'));
$resultElement->appendChild($aElement);
$tElement = $doc->createElement('t');
$tElement->appendChild($doc->createTextNode('test'));
$resultElement->appendChild($tElement);
echo $doc->saveXML();

Javascriptでデーターを得る

まず、いきなりいい訳だが…、以下の手順はちゃんと動いているものの正しいかどうかはあまり自信がない。ネットでいろいろ調べてみたものの、いくつかのコードが見受けられた。
とりあえずは、取得したXMLドキュメントに同じタグ(エレメント)が存在しない(上記の例では<a>1</a>という'a'タグがひとつしかない)というのを前提とする。

以下のようなJavascriptの関数を用意してみた。

function getElement(parent, name) {
 return parent.getElementsByTagName(name).item(0);
}
function getNodeValue(element) {
 return element.childNodes.item(0).nodeValue;
}

この関数を使って、先ほどのXMLドキュメントのオブジェクト(xmlDocという変数)から"a"という値を得るコードは以下の通り。

var aElement = getElement(xmlDoc, 'a');
var a = getNodeValue(aElement);

あとは、これらの得られたデーターをブラウザ上に表示するわけだが、これはいわゆるDOMといわれている仕組みを使って表示するのだが、ここではさらに長くなりそうなので割愛させていただく。

というわけで、とりあえずながらも、PHPを使ってAJAXをやってみた。あとは、いわゆる配列のようなデーターをJavascriptのXMLDocumentから取得できるようにする必要があると思う。これについては、いずれ書きたいと思う。

はてな

はてなスターをつけてみたよ
↑と書いてみるテストwww

はてなから訪問される方が多いので、はてなに登録して、はてなスターをつけてみました。
ちなみにBloggerもOpenIDに対応しているので、はてなのIDでコメントなどができるようです(実際にやってないので、どんなもんだかは不明)。

もっと、ちゃんとした記事を書いていかなくては、いけないなぁと感じております。
おしえて君以外のコメント等は歓迎ですので、よろしくおねがいします。

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

投稿のテスト

MicrosoftのWindows LiveにWriterという、ブログの編集ツールがあるのだが、それを使ってためしに投稿してみるテスト。
しかし、結局のところWYSIWYGなエディタを使わずに、テキストモードでHTMLタグを打ち込んでいる始末。

ちゃんと、ブログを読み込んで、レイアウトなどを保存して、プレビューしてくれるので、Bloggerのプレビューよりはよいかもしれない。
このツールの良し悪しは、しばらく使ってみてから、レビューしてみることにする。

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」を選択)。