Technote

by sizuhiko

CakeにTestがやってきた

<!– more –>というタイトルでLTをしてきました。

第3回CakePHP勉強会です。

第1回には参加できていたのに、前回は既に予定があり、残念ながら参加できず、久々の参加です。

発表イメージはほぼ完成していたのに、結局スライドが完成したのは当日昼:ko:

しかも当初枠の10分ギリギリの内容&進行がおしていたこともあり、超特急でやらせてもらいました。

聴いている人は速過ぎるかな?と思いながら懇親会では暖かい声をかけていただき、ありがとうございました:cheers:

もうちょっと時間の枠があれば詳しくテストについて話せそうなので、そういう機会があればまたお邪魔したいと思います。

他の人の発表もたいへん興味深い内容ばかりで、参考になりました。エディターのプラグインは確かにあったら便利そう。PCのリソースを使いまくってブラウザでもエディタでもファイルを開きっぱなしというのは良くないのですかね?:p

あっ、SimpleTestなどについて書かれたテストのまとめ本も、もうしばらくで出ると思いますので、よろしくお願いします。:rolleyes:

それとCake1.2本にご協力いただけるかたがいましたら、こちらもよろしくお願いします。

しかしいつもながらにSlideShareにUpすると、ヒラギノ文字がダメダメになりますね。何とかして欲しい・・・・

CakePHP 1.2 RC1でTreeBehaviorの絞込み範囲指定が変更となった

<!– more –>TreeBehavior自体1.2から追加された機能なのに、変更になるなんて。。。

1.2βでは

$this->Category->setScope(array('company_id'=>'1'));
$categories = $this->Category->children();

のように書くと、company_idが1のカテゴリだけ階層構造で取得できていたのだが、RC1ではsetScopeがDeprecatedになってしまった。で、上記方法を使っている画面を表示すると、「(TreeBehavior::setScope) Deprecated - Use BehaviorCollection::attach() to re-attach with new settings」というエラーメッセージが出るが、お世辞にもどう変更していいかわかるメッセージじゃないし、ドキュメントにも書いてない。

なので、ソースを読み込むことに。まぁソース読めばわかります。結果としては、

$this->Category->$this->Behaviors->attach('Tree', array('scope'=>array('company_id'=>'1')));
$categories = $this->Category->children();

のように記述するのですが、これを使用箇所で全部変更するのは大変ですね。そこで、Categoryのモデルクラスに以下のようなメソッドを追加してあげました。

function setScope($scope = array()) {
    return $this->Behaviors->attach('Tree', array('scope'=>$scope));
}

ま、AppModelに追加しても良かったんですが、全部がTreeBehavior使っている訳ではないと思うので、その場合は、setTreeScopeのような名前に変更した方が良いかな?と思っています。

CakePHP 1.2 RC1からfindの条件指定方法が変更となった

<!– more –>CakePHP1.2 も RC1 となり、現在開発中のプロジェクトでβからRC1に変更したところ、様々なトラブルが。。。

1.2βでは1.1の検索方法を踏襲しており、以下のように記述できる。

$conditions = array("Post.title" => "This is a post");

//Example usage with a model:
$this->Post->find($conditions);

この例は完全一致(=)なので問題はなかったのだが、それ以外の<や>、LIKEやBETWEENなど記述方法が変更となっている。 なおINの場合は、

$conditions = array("Post.id" => array(1,2,3,4,5));

//Example usage with a model:
$this->Post->find($conditions);

のように記述でき、1.1などからの変更はない。 ではLIKEについてこれまでの記法をおさらいすると、

$conditions = array("Post.title" => "LIKE %post%");

//Example usage with a model:
$this->Post->find($conditions);

となっていた。しかしこれを1.2 RC1で実行すると、WHERE句は「'Post.title’ = ‘LIKE %post%'」のようになってしまう。そこで、dbo_source.phpを見て解析してみた。結果、LIKEなどの記法は

$conditions = array("Post.title LIKE" => "%post%");

//Example usage with a model:
$this->Post->find($conditions);

のように変更となっていた。この変更はかなり驚きだが、カラム名の方にLIKEや<=などの条件を移動することで、様々なSQLインジェクションを考えると、従来のようにvalue側を正規表現で切り分けるより安全な気がする。

値が1つの場合は良さそうだ。ではBETWEENの場合はどうだろう?従来は、

$conditions = array("Post.date" => "BETWEEN 2008-1-1 AND 2009-1-1");

//Example usage with a model:
$this->Post->find($conditions);

のように記述していたが、これもそのままだとWHERE句は「'Post.data’ = ‘BETWEEN 2008-1-1 AND 2009-1-1'」のようになってしまう。ではLIKEと同じようにしてみると、

$conditions = array("Post.date BETWEEN" => "2008-1-1 AND 2009-1-1");

//Example usage with a model:
$this->Post->find($conditions);

WHERE句は「'Post.data’ BETWEEN ‘2008-1-1 AND 2009-1-1'」のようになってしまう。これも正しくない。このようなケースはさらに変更されており、以下のようになる。

$conditions = array("Post.date BETWEEN ? AND ?" => array("2008-1-1", "2009-1-1"));

//Example usage with a model:
$this->Post->find($conditions);

BETWEENなど値が複数ある場合は、カラム名上で?を使うと配列の値を順番にセットしてくれる。WHERE句は「'Post.data’ BETWEEN ‘2008-1-1’ AND ‘2009-1-1'」のように期待通りの結果となる。

なおLIKEなどでも

$conditions = array("Post.title LIKE ?" => array("%post%"));

//Example usage with a model:
$this->Post->find($conditions);

と書くことが可能だが、そこまで見栄えに統一感を持たせなくても良いかな?と思う。

Flex2でCoverFlowクローンを作る (5) ~並べて台形変形させる

<!– more –>現在、オブジェクト倶楽部のメールマガジンで連載している「Flexで体験するリッチクライアント」を補足するTechnoteです。

第五回は、画像を横に並べて、中央の画像以外は台形表示する手順を整理します。

■今回サンプルの完成イメージ

前回は画像を鏡面効果表示するサンプルを紹介しました。今回はその画像いくつか並べて中央以外は台形加工して表示します。今回は、カスタムコントロール(ImageReflector.as)とmxmlファイルを修正していきます。

■ファイルの準備

プロジェクトファイルをダウンロードして、任意のディレクトリで解凍します。

  • reflector.as3proj
  • reflector.mxml
  • ImageReflector.as
  • TransformUtil.as

reflector.as3projをダブルクリックすると、FlashDevelopが実行されます。

■要素を並べて、繰り返し表示するには

Flashでは、Boxを使うとBox内に要素を並べることができます。BoxにはHBox(横並び)とVBox(縦並び)の実装があり、CoverFlowでは横並びを利用します。

HBoxを使って以下のようなコードを書けばよいのですが、

<HBox>
  <local:ImageReflector id="imageReflector0" />
  <local:ImageReflector id="imageReflector1" />
  <local:ImageReflector id="imageReflector2" />
  <local:ImageReflector id="imageReflector3" />
</HBox>

これだとImageReflectorを必要な分、記述しなければいけないのでかなり面倒です。そこで、繰り返しを便利にしてくれるRepeaterという要素があります。今回のmxmlでは以下のように記述しています。

<HBox id="h_frame" height="400" width="500" backgroundColor="0x000000" paddingLeft="200" paddingRight="200" verticalScrollPolicy="off">
    <Repeater id="rp" dataProvider="{dp}">
        <local:ImageReflector id="imageReflector" asinId="{rp.currentItem}" creationComplete="onCreationComplete(event)"/>
    </Repeater>
</HBox>

Repeaterで繰り返したい要素はdataProviderというパラメータで指定することができます。dpは同じmxmlのスクリプト部分に記述しています。

[Bindable]
private var dp:ArrayCollection =  new ArrayCollection(["482228350X", "4774133655", "4274066940", "4822283143", "4839923221", "4873113202", "4797333502", "4822282295"]);

配列要素(ArrayCollection)に、amazonのasinidを指定していけば、値が変更になった場合も簡単です。後々XMLファイルに外だしにする場合もそれほど変更が必要になりません。

■画面とActionScriptで情報を交換する

dataProviderで指定した、asinidをActionScriptに引き渡すには、[Bindable]を宣言すれば良いだけです。この宣言は変数でも関数でも、直前に記述すれば、MXMLとActionScriptの間で情報を交換することができます。本サンプルでは、asinIdと、選択表示(今回は中央)のIndexをMXMLからActionScriptに対して引き渡しています。引渡し方法は、タグの中で指定する方法や、MXMLのScriptからID指定で渡す方法があります。

タグの中で指定するには、

<local:ImageReflector asinId="{rp.currentItem}"/>

と書くことで、ImageReflectorクラスのasinId変数の初期値として、rp.currentItemの値がセットされます。rp.currentItemはRepeater(idがrp)の現在のdataProvider配列の値です。繰り返している現在の値を引き渡しています。

スクリプトの中で指定するには、

imageReflector[0].selectedIndex = dp.length / 2;

と書くことで、idがimageReflectorの要素の0番目のselectedIndex変数に値をセットすることができます。imageReflectorはRepeaterで繰り返しているImageReflector要素のidで同じIDが複数出現すると、自動的に配列要素となってくれます。

■measureを修正して、個々の横幅を指定する

Flashが個々の要素の幅を決定するときに呼ぶmeasure関数で、選択要素は等幅、それ以外は台形変形したときに横幅を指定するようにします。

if(selectedIndex == repeaterIndex) {
    measuredWidth = _content.getExplicitOrMeasuredWidth();
}
else {
    measuredWidth = tiltingWidth;
}

selectedIndexはMXMLとバインディングしている変数ですが、repeaterIndexはどこにも出てきません。これは自分のコントロールがRepeaterの中にある場合に有効な変数で、自分がRepeaterの何番目かを知ることができるのです。ですからMXMLから指定された選択Indexである場合は、画像の幅を指定し、それ以外の場合はtiltingWidthを指定します。このtiltingWidthも変数宣言していませんが、今回追加したコードに関数が記述されています。

public function set tiltingWidth(value:Number):void
{
    // no set
}
public function get tiltingWidth():Number
{
    var contentWidth:Number = _content.getExplicitOrMeasuredWidth();
    var contentHeight:Number = _content.getExplicitOrMeasuredHeight();
    var radians:Number = Math.atan2(-tiltingMargin, maxTiltingWidth);
    var rotation:Number = radians * 180 / Math.PI;
    var w:Number = Math.abs(contentWidth * Math.cos(rotation * 180 / Math.PI));
    return Math.min(maxTiltingWidth, w);
}

functionの後にset,getと書くとプロパティ値のsetter,getterの意味を持つことができ、あたかもtiltingWidthというメンバ変数があるように見せかけることができます。

台形変形したときの横幅は、前回も出たMathの三角関数を使って出しています。図のように台形に変形したときに切り落とされる三角形の底辺となる幅を想定すればよいでしょう。最初に上のように高さがtiltingMarginで、幅がmaxTiltingWidthの三角形で斜辺への角度を求めます。

同じ角度で斜辺の長さが実際の画像幅になった場合に、底辺がどのくらいの幅になるかを求めます。そこで小さいほうの幅を採用すると幅の異なる様々な画像でもうまく幅を指定することができます。

■実際に描画する

updateDisplayListを修正して、ビットマップを台形変形するなどの描画を行います。

前回まではupdateDisplayListで直接画像を表示していましたが、transformImageという関数に変形後の幅を渡して画像を描画してもらうようにします。今回追加したtransformImage関数では、Kazuhiko Arase氏の四角形の自由変形という記事に掲載されている、TransformUtilクラスを使っています。台形変形すると様々な考慮が必要となるのですが、この変形ツールを指定すれば、4点の座標を指定するだけで良いので便利です。
transformImage関数では、図のように6つの点を計算しています。位置が計算できたら、drawImageShape関数でTransformUtilを呼び出して、描画領域へ表示するようにします。

■クロスドメインの注意事項

前回も書いたのですが、amazonの画像サーバのcrossdomain.xmlは画像サーバ内からだけしか参照できないので、ここで作成したSWFファイルをサーバにアップロードしてもうまく表示できません。

本記事上の完成イメージは、PHPサイトでプロキシしています。

まずPHPのコードは前回記事を参考にしてください。

■次回予告

アニメーション表示に取り掛かりたいと思います。

Flex2でCoverFlowクローンを作る (4) ~鏡面加工する

<!– more –>現在、オブジェクト倶楽部のメールマガジンで連載している「Flexで体験するリッチクライアント」を補足するTechnoteです。

第四回は、画像を鏡面加工して表示する手順を整理します。

■今回サンプルの完成イメージ

前回はカスタムコントロールを使って、画像を表示するサンプルを紹介しました。今回はその画像を加工して表示します。画像の加工はいろいろありますが、Macユーザとしては外せない鏡面効果を付けてみましょう。今回は、カスタムコントロール(ImageReflector.as)を修正してい きます。

■ファイルの準備

プロジェクトファイルをダウンロードして、任意のディレクトリで解凍します。

  • reflector.as3proj
  • reflector.mxml
  • ImageReflector.as

reflector.as3projをダブルクリックすると、FlashDevelopが実行されます。

■commitProperties()の修正

以下のようなコードに修正となります。

override protected function commitProperties():void
{
    if (_imageLoader == null) {
        initLoader();
    }
    invalidateDisplayList();
}

具体的な変更イメージが掴めないと思うので、initLoader関数の処理も見てみましょう。

前回のサンプルでは、

_content = UIComponent(new Image());

の部分で、Image.dataに値をセットすることで画像を出していましたが、今回はかなり異なります。これはオリジナルの画像を鏡面加工するには、元の画像の読み込みが完了していなければならないためです。Imageを使って画像を表示する方法は便利なのですが、読み込み状況まで制御することができません。このような場合は、Loaderクラスを使って画像を読み込むことができます。

_imageLoader.load(new URLRequest("http://ec1.images-amazon.com/images/P/4839923221.09._SL200_SCLZZZZZZZ_.jpg"));

で画像の読み込みを命令すると、完了イベント(Event.COMPLETE)を受け取れます。イベントを受け取ると、addEventListenerに記述された内容を実行します。

_imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, function():void {
     // ここに記述された内容を実行
}

次に、

_imageShape = new Shape();
_content.addChild(_imageShape);

のようにして描画領域を作成します。描画領域はShapeと呼ばれる単純な描画コンポーネントを使います。Flex本ではSpriteを使うケースも多いのですが、使い分けとしては、さらに子供の要素を追加する場合はSprite。単純な描画枠ならShapeという理解でよいでしょう。Shapeの方がSpriteよりも使用メモリが少ないという利点もあります。

取得した画像をビットマップデータに変換しておきます。

_imageShapeBitmap = new BitmapData(_imageLoader.width, _imageLoader.height, true, 0x000000);
_imageShapeBitmap.draw(_imageLoader);

のように書くと、ロードした画像を簡単にビットマップとして扱えるようになります。

var reflectionBitmap:Bitmap = createReflectionBitmap(_imageShapeBitmap);

で鏡面画像を作成して、_contentの子オブジェクトの更新イベントでinvalidateReflection()が呼ばれるようにしておきます。

■鏡面画像を作成する createReflectionBitmap()

この関数のポイントはグラデーションパターンを作成するところです。

var alphaGradientBitmap:BitmapData = new BitmapData(tw, th, true, 0x00000000);
var gradientMatrix: Matrix = new Matrix();
var gradientShape: Shape = new Shape();
gradientMatrix.createGradientBox(tw, th * kFalloff, Math.PI/2, 0, th * (1.0 - kFalloff));
gradientShape.graphics.beginGradientFill(GradientType.LINEAR, [0xFFFFFF, 0xFFFFFF], [0, 1], [0, 255], gradientMatrix);
gradientShape.graphics.drawRect(0, th * (1.0 - kFalloff), tw, th * kFalloff);
gradientShape.graphics.endFill();
alphaGradientBitmap.draw(gradientShape, new Matrix());

Math.PIとは、円周率を表していて、Math.PI/2は90度を意味します。通常の座標軸は、左から右にむかうので、Math.PI/2を適用した場合は、時計回りに90度つまり上から下に向かって描画するように制御できます。またグラデーションには、線状(LINEAR)と放射線状(RADIAL)があり、段々薄くなっていく鏡面効果の場合は、線状を使います。

var reflectionData:BitmapData = new BitmapData(tw, th, false, 0x00000000);
reflectionData.fillRect(rect, 0x00000000);
reflectionData.copyPixels(target, rect, new Point(), alphaGradientBitmap);

のように引数で渡されたオリジナル画像と、グラデーションパターンの画像を重ね合わせますが、copyPixelsを使うと、重ね合わせる透過ビットマップを指定できるので、便利です。 最後に

reflectionBitmap = new Bitmap(reflectionData);
reflectionBitmap.alpha = .85;

で使いやすいようにBitmapDataに変換し、透過度を指定しておきます。背景を白に変更した場合など、透過度を調整すると、お好みの鏡面効果にすることができます。

■鏡面効果を開始する

initLoaderで設定した、Imageの描画が完了したときに呼ばれるメソッドを以下のように定義します。

private function invalidateReflection(event:Event):void
{
    _invalidatedReflection = true;
    invalidateSize();
    invalidateDisplayList();
}

このタイミングでようやく鏡面画像を書いてよし、というタイミングになるので_invalidatedReflectionフラグをオン(true)に設定します。 その後、画面の再描画を即すために、invalidateDisplayList()を呼び出します。

■実際に描画する

updateDisplayListを修正して、ビットマップの描画を行います。

ビットマップを描画領域に書き出すには、graphicsオブジェクトを使用します。

_imageShape.graphics

graphicsオブジェクトはShape上にベクター描画するためのプロパティです。beginBitmapFillはビットマップで塗りつぶしを開始する、という宣言で、描画範囲をdrawRectで決定します。graphicsオブジェクトで描画を終了する場合は、描画の種類に関係なくendFillで終了します。

鏡面画像はそのままだと、オリジナル画像にグラデーションが重なっているだけの画像です。実際に鏡面効果に見せるためには、反転させて、位置を下げる必要があります。

var m:Matrix = new Matrix();
m.scale(1.0, -1.0);
m.ty = 2 * _imageShapeBitmap.height;
_imageShape.graphics.beginBitmapFill(_reflectionShapeBitmap, m);
_imageShape.graphics.drawRect(0, _imageShapeBitmap.height, _reflectionShapeBitmap.width, _reflectionShapeBitmap.height);
_imageShape.graphics.endFill();

本サンプルではMatrixクラスを使って反転させています。画像の反転など3D効果を出すのには、行列計算を行う必要があります。Matrixは表示オブジェクトの回転、拡大縮小、平行移動を行列を使って計算することができます。今回のケースは単純に反転するだけなので、拡大縮小(scale)メソッドを使います。なぜ反転するのに拡大縮小?という疑問があるかもしれません。なぜなら鏡面効果を考えてください。画像を180度回転させると左右が逆になります。するとその後左右反転を呼ばなければなりません。これでは二度手間ですね。

つまり単純に反転させる効果はscaleメソッドを使い実施します。上下反転はscale(1.0, -1.0)、左右反転は scale(-1.0, 1.0)で実現できます。行列計算というと難しいイメージしかありませんが、100x200画像を拡大縮小させる場合は、四点[(0,0)(200,0)(0,100)(200,100)]で、x,yそれぞれをscaleで指定された値でかけるだけです。つまりscale(1.0,-1.0)の結果は[(0,0)(200,0)(0,-100)(200,-100)]となるので、y軸0の値を中心に画像が上方向 に向いていることがわかると思います。結果として上下反転したように見えるのです。

■クロスドメインの罠

amazonではAPI用にFlashから接続可能なcrossdomain.xmlが定義されています。例えばhttp://xml.amazon.com/などのルートにはcrossdomain.xmlが設置されています。ところが、画像サーバのcrossdomain.xmlは画像サーバ内からだけしか参照できないので、ここで作成したSWFファイルをサーバにアップロードしてもうまく表示できません。

そこで、PHPスクリプトなどを使って回避することができます。なお本記事上の完成イメージは、PHPサイトでプロキシしています。

まずPHPのコードは以下のようになります。例えば、crossdomain.phpとしておきましょう。

<?php
if($_GET["file"]){
    header("Content-Type:image/jpeg;");
    readfile($_GET["file"]);
}
?>

次にcrossdomain.xmlをドキュメントルートに配置します。

<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
    <allow-access-from domain="*"/>
</cross-domain-policy>

後は、ImageReflector.asのload部分を以下のように書き換えます。

var context:LoaderContext = new LoaderContext(true);
_imageLoader.load(new URLRequest("http://hoge.com/crossdomain.php?file=http://images.amazon.com/images/P/4839923221.09._SL200_SCLZZZZZZZ_.jpg"), context);

すると、PHPサイトがプロキシとなってくれ、PHPサイトのcrossdomain.xmlをFlashは解釈して、SWFから画像が表示できるようになります。

クロスドメイン対応の記述は、添付のプロジェクトに含まれないので注意してください。

※なおローカルで試す分には、クロスドメインの対策を入れなくても動作します。