サラリーマンが忙しくて、ぜんぜん手が付けられませんでした。
とりあえず「SalesforceエンジニアのためのFlash入門」という需要があるんだか無いんだかまったくわからない連載をスタートさせようと思います。
どうぞよろしくお願いします。
2012年3月12日月曜日
2012年1月31日火曜日
Desktopを見るのが怖い…
いえね、昨日終業間際にDataLoaderを使って急ぎのインポート作業をやったんですけど、なかなかうまくいかなくてですね。
対象オブジェクトは3つ。何だかんだで20回ぐらいリトライしてようやくうまく行ったのです。で、そのままPCをシャットダウンしまして。
Desktopにアイコンが100個ぐらい並んでいるんだろうなー…やだなー…。
--
ところで、開発サンドボックスってレポートは移行してくれないんでしたっけ。昨日それで30分くらい右往左往してました。Force.com IDEでDeploymentすると「Success」で終わるのにフォルダに現れてくれない。foldersも一緒にデプロイしてもダメ。
本筋と関係ないので諦めましたが…。
対象オブジェクトは3つ。何だかんだで20回ぐらいリトライしてようやくうまく行ったのです。で、そのままPCをシャットダウンしまして。
Desktopにアイコンが100個ぐらい並んでいるんだろうなー…やだなー…。
--
ところで、開発サンドボックスってレポートは移行してくれないんでしたっけ。昨日それで30分くらい右往左往してました。Force.com IDEでDeploymentすると「Success」で終わるのにフォルダに現れてくれない。foldersも一緒にデプロイしてもダメ。
本筋と関係ないので諦めましたが…。
2012年1月18日水曜日
Salesforce mobile SDK for iOS +1
■テンプレート以外で作業する場合のTips 1■
SFAuthorizingViewControllerにこんなのが定義されてますが。
これ、使われていません。テンプレートについてる「Authorizing」の画面があまりにもスゴいので当然置き換えを考えるわけですが、置き換えたxibでちょっと小細工をしたものの、どうも思ったとおりの動作をしてくれない。何だかんだ調べたら、oauthViewは全然使われていませんでした。
普通にviewをFile's Ownerにしてください。
SFAuthorizingViewControllerにこんなのが定義されてますが。
@property (nonatomic, retain) IBOutlet UIView *oauthView;
普通にviewをFile's Ownerにしてください。
Batch Apex / Mailでハマった件
![]() |
| 香港にて |
Batch Apexに限らないんだけど…arrayに入れたSingleMailMessageをまとめてどーんと送る時、その中に1通でも不達アドレスが含まれていると一通も送られないでエラーになりますね。
昨日はそれでハマりました。
Contactに関連付けたemailアドレスにメールを送って、それが正常に配信されないとそのアドレスには「確認しろよコラ」ってフラグが立ちます。その状態で同じアドレスにメールを送ろうとすると送信前にエラーになる。まぁそれは当たり前。問題は、不達アドレスだと検出されるタイミングが前回送信から数十分〜数時間かかるってところで、いつ判定されるかわかんない。なので大量に送信してたりすると、エラーがたまたま次回の送信Batchにひっかかってコケてました。
報告をするのにエラーの経緯を再現しなきゃいけないんだけど、そんな神業なタイミングをどーしろとw
それにしても、疲れてるとレアケースの推測がダメだわ。
2012-01-29追記:
EmailBouncedDate / EmailBouncedReasonがnullじゃなかったら不達ですね…。
2011年12月31日土曜日
Salesforce mobile SDK正式版(GA)
■βが取れました■
まぁとりあえず、めでたいです。今後はドキュメントなども充実してくれますよーに。
■では試してみましょう■
というわけで、例によってGitHubから最新版を落としてきます。もしXcodeが立ち上がっていたら、念のため終了しといてください。なお、私の環境ではGitHub for Macでの同期がうまく働きませんでした。ので、zipを落としてきます。
SalesforceMobileSDK-iOS
落としたら、zipを解凍したディレクトリに移り、 ./install.shを実行します。例によって数分後、BUILD SUCCESSFULが出たら無事終了です。
さて、Xcode 4.2を起動します。New Projectを選ぶと、以前よりちょっと派手なテンプレートアイコン「Hyblid Force.com App」と「Native Force.com REST App」が追加されています。今回はサクっと Native を選びます。
次の「Choose options for your new project」でProduct Nameに適当なプロジェクト名、iTunes connectなどに登録したあなたの会社ID、Consumer KeyにはあなたのSalesforce/Database.com組織のリモートアクセスキー、Redirect URLにはその戻り先URLを入力します。
Xcode 4.2だとその下の「Use Automatic Reference Counting」をonにしたいのですが、残念ながらMobile SDKが対応していないのでここはチェックを外してください。外さないとビルドエラーの塊になります。すべて入力したらNextをクリックし、プロジェクト保存先を指定し、プロジェクトを作ります。
以前はここでOther linker flagを設定したりFrameworkを追加する一手間が必要だったのですが、さすがはβでないGA、さくっとそのままcmd+Rで実行できます。起動すると、黄土色の「Authorizing」画面が表示され、その後Salesforce.comのOAuth認証画面になります。ID, Passなどを入力すると、その組織のユーザが10名分表示されます。
■βとの差異■
βはAppDelegateに認証関連の処理を細々と書かなければいけませんでしたし、「認証中です」の画面も自分でイチから作ってやらなければいけませんでした。でも、1.0では、その辺の処理はSFAuthorizingViewControllerとSFNativeRestAppDelegateに実装されていて、AppDelegateにはごく僅かなコードを書くだけで処理してもらえます。本来のAppDelegateに書きたい処理と分離することができて、非常にすっきりしました。
ViewControllerに関しては元々テンプレートが作ってくれるコードは非常に単純なので、あんまりRESTがどうこう考えないで実装することができます。
■そして、ハマる(お約束)■
以下は余談です。
とりあえず、プロジェクトの構造などを検証するために今度はMobile SDKのテンプレートを使わずにアプリを作ってみることにしました。
原因は…上にヒントが書いてあります。
まぁとりあえず、めでたいです。今後はドキュメントなども充実してくれますよーに。
■では試してみましょう■
というわけで、例によってGitHubから最新版を落としてきます。もしXcodeが立ち上がっていたら、念のため終了しといてください。なお、私の環境ではGitHub for Macでの同期がうまく働きませんでした。ので、zipを落としてきます。
SalesforceMobileSDK-iOS
落としたら、zipを解凍したディレクトリに移り、 ./install.shを実行します。例によって数分後、BUILD SUCCESSFULが出たら無事終了です。
さて、Xcode 4.2を起動します。New Projectを選ぶと、以前よりちょっと派手なテンプレートアイコン「Hyblid Force.com App」と「Native Force.com REST App」が追加されています。今回はサクっと Native を選びます。
次の「Choose options for your new project」でProduct Nameに適当なプロジェクト名、iTunes connectなどに登録したあなたの会社ID、Consumer KeyにはあなたのSalesforce/Database.com組織のリモートアクセスキー、Redirect URLにはその戻り先URLを入力します。
Xcode 4.2だとその下の「Use Automatic Reference Counting」をonにしたいのですが、残念ながらMobile SDKが対応していないのでここはチェックを外してください。外さないとビルドエラーの塊になります。すべて入力したらNextをクリックし、プロジェクト保存先を指定し、プロジェクトを作ります。
以前はここでOther linker flagを設定したりFrameworkを追加する一手間が必要だったのですが、さすがはβでないGA、さくっとそのままcmd+Rで実行できます。起動すると、黄土色の「Authorizing」画面が表示され、その後Salesforce.comのOAuth認証画面になります。ID, Passなどを入力すると、その組織のユーザが10名分表示されます。
■βとの差異■
βはAppDelegateに認証関連の処理を細々と書かなければいけませんでしたし、「認証中です」の画面も自分でイチから作ってやらなければいけませんでした。でも、1.0では、その辺の処理はSFAuthorizingViewControllerとSFNativeRestAppDelegateに実装されていて、AppDelegateにはごく僅かなコードを書くだけで処理してもらえます。本来のAppDelegateに書きたい処理と分離することができて、非常にすっきりしました。
ViewControllerに関しては元々テンプレートが作ってくれるコードは非常に単純なので、あんまりRESTがどうこう考えないで実装することができます。
■そして、ハマる(お約束)■
以下は余談です。
とりあえず、プロジェクトの構造などを検証するために今度はMobile SDKのテンプレートを使わずにアプリを作ってみることにしました。
- Single View Appテンプレートでプロジェクトを作る
- さっきNative Force.com REST Appで作ったプロジェクトからDependenciesフォルダとNativeRestAppフォルダをコピー
- 必要なFrameworkを追加
- AppDelegateとViewControllerに必要なコードをコピペ
- cmd+Rでビルド&RUN
アプリは無事シミュレータ上で起動し、ドドメ色の「Authorizing」から認証を得て…オチますorz
これが3日ほど前でした。それから他の作業もしつつ、いろいろ原因究明したんですが、どうやっても同じところで落ちます。以前のパターンで同じようなエラーが出た時にはdelegateのスペルが間違っていたのが原因だったので、今回も何かdelegateを抜かしているのではないかと思い、ソースをにらめっこする日々でした。でも治らない。
その辺のウダウダを書いてもしょうがないので、例によって最終的に解決に至った課程のみ書きます。
- エラーが出ているのは、viewDidLoadの下記の行
[[SFRestAPI sharedInstance] send:request delegate:self];
- SIGABRTで止まった時点でのスタックトレースを見る←最初にやっとけ
- [SFRestRequest description」でググる→SFRestRequest.mのソースがヒット
SFRestRequest.m
- ソースに以下の記述を発見
- _queryParamsはNSDictionaryなので当然JSONRepresentationなどというメソッドはない。遅まきながら、ここで「カテゴリ」が実装されてないことに気づく
原因は…上にヒントが書いてあります。
Other Linker Flagsに-ObjC -all_loadがセットされてない
でした。お粗末さまでした…。
言い訳をするとね…以前これらのLinker Flagsがセットされていない時にはビルドエラーで止まったのよ…。
言い訳をするとね…以前これらのLinker Flagsがセットされていない時にはビルドエラーで止まったのよ…。
2011年12月17日土曜日
FlashでPDF・続き
■続き、っていうか本番■
前回はFetchしてデータを一覧表示するところまででした。
FlashでPDF
使ってみたら日本語が文字化けたりしてまた散々痛い目にあったのですが、そういう経緯は全部スキップします。ここからライブラリをダウンロードします。
Player10.0 でも purePDF で日本語PDFを作る
FlashBuilder上のプロジェクトのlibsフォルダには前回force-air.swcを追加しましたが、今回ダウンロードしたpurePDF.swcとpurePDFont.swcも追加します。
APIなどに関してはFlashとJavaとで実装上の違いはありますが、iTextの方がサンプルも多く用意されていて便利です。
■解説■
ソースは最後に貼ります。基本的な処理の流れとしては
では、実際の処理を見ていきます。
>queryHandler
前回見たようにarrayDataにfetchしたデータを溜めていきます。result.doneになったらfetch完了ですので、dataGridにarrayDataをセットし、pdfの生成を始めます。
PdfWriterでバイトストリームをセットしつつ用紙サイズを設定します。ここではPageSize.A4でA4縦を指定していますが、PageSize.A4.rotate(90)と書くとA4横になります。
>initPdfFont
ベースフォントを生成します。いろいろなフォントを試してみたのですが、結局これ平成明朝しか使えませんでした。ゴシックも指定できるのですが、日本語フォント部分は同じで英数字部分が変わるのみでした。
■おまけ■
前回はFetchしてデータを一覧表示するところまででした。
FlashでPDF
「そもそもなんでFlashなのか?」という話が抜けていました。
- クライアントサイドの有り余るリソース(メモリ, CPU)を使える
- きめ細かく罫線などを制御できる
- Force.comでは実現不可能なグラフも作れる
- Rich GUIで余計な苦労を負わないで済む
- PDF添付メールやファイル出力の制約が少ない
PDF生成そのものに関しては「セルその他の描画を細かく制御できるので、基本的にどんな帳票でもおk」ってのがFlash / purePDFでPDFの長所の一つです。クライアントサイドでは数GBのメモリも当たり前なので、ガバナーの制約を気にしないで好きなことできるのもメリットです。「数万件のデータをドーンと読み込んできてループまわしながら集計し、役員会に出せるような美しい罫線の入ったPDFを作る」ようなケースで使えます。
あとグラフ。Flashのグラフは制約が少ないので複雑なグラフを作りたい場合にはForce.comよりもラクできます。
あとグラフ。Flashのグラフは制約が少ないので複雑なグラフを作りたい場合にはForce.comよりもラクできます。
また最近はWebアプリケーションであってもrichなGUIが求められる案件が増えてきました。Force.comでも売掛入金の消し込み画面で設定した条件が間違っていたらボタンをdisableしたり、jQueryで関連するデータをアニメーション表示したりお客様の多彩なご要望に苦労する日々でございます。
でもFlashはrich GUIの本家です。
先日もあるコンサルタント会社から「100近い検索条件から案件を抽出する検索画面」という依頼を受けましたが、設定した条件が埋もれてしまわないようわかりやすく表示するのもFlashなら自由自在です(簡単です、と書かないところに私の誠実さを感じてください)。
そうそう、それにFlashなら開発の途中でオブジェクトの項目を変更する必要が生じても「それAPEX/VFで使ってるから変更できねぇよw」とForce.comから怒られることもありません(笑)。
■purePDFで■
FlashでPDFを生成するためのライブラリでメジャーなのはAlivePDFとPurePDF。今回はPurePDFを使います。PurePDFはJavaのPDFライブラリiTextをFlashに移植したもので、私はiTextには散々痛い目にあったお世話になったことがあるので、まぁ学習コストがかからないだろう、と。
使ってみたら日本語が文字化けたりしてまた散々痛い目にあったのですが、そういう経緯は全部スキップします。ここからライブラリをダウンロードします。
Player10.0 でも purePDF で日本語PDFを作る
FlashBuilder上のプロジェクトのlibsフォルダには前回force-air.swcを追加しましたが、今回ダウンロードしたpurePDF.swcとpurePDFont.swcも追加します。
APIなどに関してはFlashとJavaとで実装上の違いはありますが、iTextの方がサンプルも多く用意されていて便利です。
■解説■
ソースは最後に貼ります。基本的な処理の流れとしては
- サイズと出力先(バッファ先)を指定してDocumentを生成する
- ベースフォントを作る
- ベースフォントにサイズなどを指定してフォントを生成する
- テキスト、イメージ、別のテーブルなどをセットしたセルを作る
- セルをテーブルにセットする
- テーブルをドキュメントにセットする
- 必要ならページ送り(document.newPage)
では、実際の処理を見ていきます。
>queryHandler
前回見たようにarrayDataにfetchしたデータを溜めていきます。result.doneになったらfetch完了ですので、dataGridにarrayDataをセットし、pdfの生成を始めます。
PdfWriterでバイトストリームをセットしつつ用紙サイズを設定します。ここではPageSize.A4でA4縦を指定していますが、PageSize.A4.rotate(90)と書くとA4横になります。
>initPdfFont
ベースフォントを生成します。いろいろなフォントを試してみたのですが、結局これ平成明朝しか使えませんでした。ゴシックも指定できるのですが、日本語フォント部分は同じで英数字部分が変わるのみでした。
>createPdf
実際に使用するサイズのフォントを生成してからPDFを生成していきます。vecという配列を用意していますが、これはTableのrowをどういう比率で生成するかを指定するためのものです。値が合計いくつになってもかまわないので、私はお客さんからもらってきたサンプル帳票に物差しあててその数字を使ったりしていますw
あとは文字列を元にしてセルを作ってテーブルにせっせと詰め込んでいくだけです。終わったらnewPageでページを閉じて、最後に document.close(); でドキュメントを閉じます。
>cellFromStringFontAndAlignment
セルを生成しています。関数名がやたらと長いのは私が長年MacOS系プログラマだったからです。生成したcellオブジェクトにはデフォルトで罫線がついてます。top, border, right, leftごとに太さ、色などを制御することができます。逆にいえば、セルを作る時点で罫線のことを考えないと意図した表になりません。まぁこれがメリットになるかデメリットになるかは状況次第っすね。
>savePdf
保存ダイアログを表示して、ファイル名や出力先を指定し、ファイルを保存します。
>formatDateMeasure
ここがFlashの困ったちゃんで、DataGridにDateやNumberを表示する際の書式指定でいちいちコールバック関数などを書かないといけないのです。VisualBASICが懐かしいぜ。
>convertToLocaltime
RESTで帰ってくる日付はGMTなのでこれを日本時間に変換してやります。60000はtimezoneOffsetで取ってくる時差が分単位なのでこれをミリセカンド単位に換算するためです。
>formatDecimal
3桁ごとのカンマ区切り、もっと簡単な方法あったら教えてください^^;
■注意■
今回サンプルとして掲載したソースにはID / Passwordが文字列定数として埋め込まれていますが、実際にFlashで作る場合この方法は避けてください。ダンプしてソースをリバースエンジニアリングすることが可能ですので。
FlashとしてForce.com環境上で使うのであればセッションを引き継いで認証できるのでID / Passwordは不要ですし、AIRでデスクトップアプリとして配布する場合はGUIからID / Passwordを入力してもらいpreferenceとしてPC上に保存すればOKです。
■Flash業界のみなさんへ■
データベースにお悩みではありませんか?
force.com / database.comの世界にどうぞ。database.comなら最高にセキュアでスケーラブルなRESTデータベースが3アカウント, 10万レコード, 月5万トランザクションまで無料で使えます。サーバのセットアップもバックアップも不要です。
お願い:ソースでお気づきの通り私はFlash初心者です。いろいろツッコミどころ満載だと思いますので、ご指導ご鞭撻のほどよろしくお願いいたしますm(_ _)m。
個人事業としてiPhoneアプリを書いてます。
現職でForce.comに携わる前はWebObjectsというフレームワークで仕事をしていましたが、WO屋から見るとApexはイマイチ美しくありません(もちろんマルチテナントとしての安全性安定性などを確保するための現実的な実装という面もあると思いますが)。
そんな私にとってForce/Database/Chatter.comをRESTでアクセスするiOSアプリはほぼ理想郷です。
■最後に■
このブログはForce.com Advent Calendarに参加しています。
■ソース■
というわけでソースです。
■ソース■
というわけでソースです。
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:salesforce="http://www.salesforce.com/"
applicationComplete="init()" height="560" width="800">
<fx:Declarations>
<salesforce:AIRConnection id="force"/>
</fx:Declarations>
<fx:Script>
<![CDATA[
import com.salesforce.AsyncResponder;
import com.salesforce.objects.LoginRequest;
import com.salesforce.results.Fault;
import com.salesforce.results.LoginResult;
import com.salesforce.results.QueryResult;
import mx.collections.ArrayCollection;
import mx.formatters.DateFormatter;
import org.purepdf.Font;
import org.purepdf.colors.RGBColor;
import org.purepdf.elements.Element;
import org.purepdf.elements.Phrase;
import org.purepdf.elements.RectangleElement;
import org.purepdf.pdf.PageSize;
import org.purepdf.pdf.PdfDocument;
import org.purepdf.pdf.PdfPCell;
import org.purepdf.pdf.PdfPTable;
import org.purepdf.pdf.PdfWriter;
import org.purepdf.pdf.fonts.BaseFont;
import org.purepdf.pdf.fonts.cmaps.CJKFontResourceFactory;
import org.purepdf.pdf.fonts.cmaps.CMapResourceFactory;
import org.purepdf.resources.BuiltinCJKFonts;
import org.purepdf.resources.CMap;
import org.purepdf.resources.ICMap;
import org.purepdf.utils.IProperties;
import org.purepdf.utils.Properties;
[Bindable]
private var arrayData:ArrayCollection = null;
private var dateTimeFormatter:DateFormatter = null;
private const MaginLeft:int = 20;
private const MarginRight:int = 20;
private const MarginTop:int = 30;
private const MarginBottom:int = 30;
private function init():void
{
dateTimeFormatter = new DateFormatter();
dateTimeFormatter.formatString = "YYYY-MM-DD HH:NN:SS";
var lr:LoginRequest = new LoginRequest();
status = "login by id/password";
lr.username = "youraccount@your.domain";
lr.password = "password"+"securitytoken";
lr.callback = new AsyncResponder(loginHandler, faultHandler);
force.login(lr);
}
private function loginHandler(result:LoginResult):void
{
if (result.userInfo != null) {
status = "login success, query";
force.autoSyncEnabled = false;
force.doCache = false;
var strQuery:String = "Select DateMeasure__c, High__c, Low__c, Beat__c From BloodPressure__c order by DateMeasure__c desc";
force.query(strQuery, new AsyncResponder(queryHandler, faultHandler));
}
}
private function queryHandler(result:QueryResult):void
{
if (result != null && result.records != null && result.records.length > 0)
{
if (arrayData == null)
arrayData = new ArrayCollection();
arrayData.addAll(result.records);
}
if (!result.done)
{
trace("query again");
force.queryMore(result.queryLocator, new AsyncResponder(queryHandler));
}
else
{
status = "query result";
force.syncConnection
dataGrid.dataProvider = arrayData;
var byteArray:ByteArray = new ByteArray();
var pdfWriter:PdfWriter = PdfWriter.create(byteArray, PageSize.A4);
var document:PdfDocument = createPdf(pdfWriter, initPdfFont());
savePdf(byteArray, "血圧報告.PDF");
document = null;
pdfWriter = null;
byteArray = null;
}
}
private function faultHandler(result:Fault):void
{
trace(typeof(result));
trace(result.faultcode);
trace(result.faultstring);
}
private function initPdfFont() : BaseFont
{
var map: ICMap = new CMap( new CMap.UniJIS_UCS2_H() );
CMapResourceFactory.getInstance().registerCMap( BaseFont.UniJIS_UCS2_H,
map );
var prop: IProperties = new Properties();
prop.load( new BuiltinCJKFonts.HeiseiMin_W3() );
CJKFontResourceFactory.getInstance()
.registerProperty(
BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ), prop);
var bf: BaseFont = BaseFont.createFont(
BuiltinCJKFonts.getFontName( BuiltinCJKFonts.HeiseiMin_W3 ),
BaseFont.UniJIS_UCS2_H, BaseFont.NOT_EMBEDDED, true );
return bf;
}
private function createPdf(inWriter:PdfWriter, inBf:BaseFont) : PdfDocument
{
var fontHeader:org.purepdf.Font
= new org.purepdf.Font( org.purepdf.Font.BOLD, 12, -1, null, inBf );
var font:org.purepdf.Font
= new org.purepdf.Font( -1. -1, 10, -1, null, inBf );
var document:PdfDocument = inWriter.pdfDocument;
document.open();
document.setMargins(MaginLeft, MarginRight, MarginTop, MarginBottom);
var table:PdfPTable = null;
var cell:PdfPCell = null;
var lc:int = 99;
var lpp:int = 40;
var i:int = 0;
var n:int = arrayData.length;
var monthBreak:int = 0;
var vec:Vector.<Number> = new Vector.<Number>;
vec.push(50);
vec.push(25);
vec.push(25);
vec.push(25);
while (i < n) {
// 行を取り出し
var line:Object = arrayData[i++];
var dateMeasure:Date = convertToLocaltime(line.DateMeasure__c);
// 改ページ
if (lc >= lpp) {
if (table != null) {
document.add(table);
document.newPage();
table = null;
}
lc = 0;
}
//ヘッダ
if (table == null) {
table = new PdfPTable(vec);
table.addCell(cellFromStringFontAndAlignment("測定日時", fontHeader));
table.addCell(cellFromStringFontAndAlignment("最高血圧", fontHeader));
table.addCell(cellFromStringFontAndAlignment("最低血圧", fontHeader));
table.addCell(cellFromStringFontAndAlignment("心拍", fontHeader));
}
table.addCell(cellFromStringFontAndAlignment(
dateTimeFormatter.format(dateMeasure), font));
table.addCell(cellFromStringFontAndAlignment(
formatDecimal(line.High__c), font, Element.ALIGN_RIGHT));
table.addCell(cellFromStringFontAndAlignment(
formatDecimal(line.Low__c), font, Element.ALIGN_RIGHT));
table.addCell(cellFromStringFontAndAlignment(
formatDecimal(line.Beat__c), font, Element.ALIGN_RIGHT));
lc++;
}
if (table != null) {
document.add(table);
table = null;
document.newPage();
}
document.close();
return document;
}
private function cellFromStringFontAndAlignment(
inStr:String,
inFont:org.purepdf.Font,
inAlignment:int = Element.ALIGN_CENTER)
: PdfPCell {
var str:String = (inStr == null) ? "" : inStr;
var cell:PdfPCell = PdfPCell.fromPhrase(new Phrase(str, inFont));
cell.horizontalAlignment = inAlignment;
return cell;
}
private function savePdf(inByteArray:ByteArray, inFileName:String) : void
{
var fr:FileReference = new FileReference();
fr.save(inByteArray, inFileName);
}
private function formatDateMeasure(inObj:Object,
inDataGridColumn:DataGridColumn)
: String
{
return (inObj != null && inObj.DateMeasure__c != null)
? dateTimeFormatter.format(convertToLocaltime(inObj.DateMeasure__c))
: "";
}
private function convertToLocaltime(inStr:String):Date {
var date:Date = DateFormatter.parseDateString(inStr);
var time:Number = date.getTime() - date.timezoneOffset * 60000;
date.setTime(time);
return date;
}
private function formatDecimal(inDecimal:Number):String
{
if (isNaN(inDecimal))
return "";
else {
var pattern:RegExp = /(\d)(?=(\d{3})+(?!\d))/g;
return String(inDecimal).replace(pattern, "$1,");
}
}
]]>
</fx:Script>
<mx:DataGrid id="dataGrid" top="10" left="23" right="23" bottom="10"
editable="false" enabled="false">
<mx:columns>
<mx:DataGridColumn headerText="測定日時" dataField="DateMeasure__c"
labelFunction="formatDateMeasure" textAlign="center"/>
<mx:DataGridColumn headerText="最高血圧" dataField="High__c"
textAlign="center"/>
<mx:DataGridColumn headerText="最低" dataField="Low__c" textAlign="center"/>
<mx:DataGridColumn headerText="心拍" dataField="Beat__c" textAlign="center"/>
</mx:columns>
</mx:DataGrid>
</s:WindowedApplication>
2011年12月13日火曜日
FlashでPDF
■別に専門というわけではないけれど■
最近は変わりつつありますが、Webアプリにとって「カチッ」とした帳票というのはやや鬼門で、皆さんかなり苦労を強いられてきました。
私自身を振り返ってみてもPDF関連の実装が割と大きな比重を占めています。前職のWebObjectsでもiTextというJavaのライブラリを使っていましたし、現職Force.com屋でもほぼすべての案件にPDF生成が含まれている状況です。
これまでForce.com上で部門別部課別集計、罫線の各種アレンジ、複数ページ請求書などいろいろな帳票を作ってきましたが、日本ビジネス特有のきめ細かい帳票要求については、以下の2点:
を除けば、特に不自由なく生成できるので、ネタとしては面白くありません。
■FlashでPDF■
さて、Flash toolkit for Force.comつかっている人います? 日本のBBSを見てもほとんど話題が出ていないですし、海外の掲示板でもFlashBuilder for Force.comが中心になってしまったようで、大変寂しい思いをしています。toolkitの開発もすっかり止まってしまいましたし、このままfade outしちゃうんですかね…。
というわけで、この機会に少しでも開発者が増えることを願って、Flash+Force.comでのPDF生成について書きます。今回はFlash+Force.comについて、次回はFlash上でのPDF生成について書きます。
■Flashでの開発■
Force.com Toolkit for Adobe AIR and Flex
何よりFlashがないと話しになりません。とりあえず30日間無料でお試しができますので、Adobeから「FlashBuilder 4.6 Standard Edition」をダウンロードしてきてください。
余談ですが、4.5からはiPhone/Androidアプリも書くことができます。試してみたらホントに15分くらいでForce.com対応iPhoneアプリを書けて大笑いしたんですが、GUIがネイティブと異なりすぎることとメモリをバカ食いするので、今のところ本気では使っていません。
さて、お次は、FlashからForce.comをアクセスするためのライブラリが必要です。これは、このページの「download toolkit」から落としてきます。
では、FlashBuilderを起動してください。ユーザ登録の手続きなどは端折ります。起動遅いっす。私の嫌いなEclipse様がベースなので諦めてください。
ファイル>新規プロジェクト>Flexプロジェクト
適当な場所に適当な名前でプロジェクトを作ります。とりあえず「Advent」にしました。アプリケーションの種類をデスクトップにする以外はデフォルトのままでOKです。
できたプロジェクトにForce.comのライブラリを追加します。さっきダウンロードしてきたライブラリの中から「force-air.swc」をプロジェクトのlibsにドラッグ&ドロップしてください。
パッケージエクスプローラの中から
Adbent>src>(default package)>Advent.mxml
を開きます。ソースの先頭にあるs:WindowedApplicationとfx:Declarationsを以下のソースと置き換えてください。
■まずはインポートとグローバル変数■
■初期化/ログイン■
冒頭部分にあるapplicationCompleteから呼び出される初期化ルーチン、ここでログイン処理をします。昔ながらの処理でOAuthなどはありませんorz。なお、これはデスクトップアプリなのでIDとPasswordでログイン処理を行いますが、FlashとしてVisualforceに埋め込む場合にはセッションidなどからそのまま認証を引き継ぐことができるようになっています。
■ログイン成功■
ログインが成功すると、この関数が呼び出されます。ここではすぐに馴染み深いSOQLを呼び出していますが、検索処理などを行う場合には、ここでGUIの「検索ボタン」をenableにする、などの処理を書きます。
■Query成功■
Query成功時にこのコードが呼び出されます。ここは1回に最大2000レコードずつ渡されます。つまり逆にいえば、数万件のレコードがヒットするようなQueryを発行しても、ここでarrayUserにひとまとめにすることができる、ということです。まぁ遅いですけどね。それにFlashは基本的にシングルタスクなので何も配慮しないで数万件のPDF帳票を出力しようとするとUIが一切操作できなくなります。不憫な奴。
で、最後にdataGridに読み込んだデータを一括してセットすれば一丁上がり。
■エラー処理■
SOQLに誤りがあった場合などにこの関数が呼び出されます。
以上でForce.com + Flexの基本はおしまいです。次回はPDF生成編です。それまでよろしかったらSOQLを書き換えたりDataGridのdataFieldをいじったりして遊んでみてください。なお、"Select Id, Name, CurrentStatus From User"などと書いてもAPIが古いのでこのライブラリでは対応していません。あしからず。
--
この記事はForce.com Advent Calendarに参加しています。
Force.com Advent Calendar 2011 : ATND
PhoneGapかiOS SDKについて書くと予告していたのにFlashになったのは準備していたネタが土壇場になって不調だったからさ。以前書いた記事もあるし。
PhoneGap 1.2.0 + Salesforce SDK(hyblid)
以上、50歳と2日のくらはしがお送りしました。
#Bloggerさんがインデントをつぶしてしまったけど、まぁ読めるのでご勘弁
最近は変わりつつありますが、Webアプリにとって「カチッ」とした帳票というのはやや鬼門で、皆さんかなり苦労を強いられてきました。
私自身を振り返ってみてもPDF関連の実装が割と大きな比重を占めています。前職のWebObjectsでもiTextというJavaのライブラリを使っていましたし、現職Force.com屋でもほぼすべての案件にPDF生成が含まれている状況です。
これまでForce.com上で部門別部課別集計、罫線の各種アレンジ、複数ページ請求書などいろいろな帳票を作ってきましたが、日本ビジネス特有のきめ細かい帳票要求については、以下の2点:
- 日本語フォントが1種類しか使えない
- ひらがなカタカナの拗促音がbaselineに揃わない
を除けば、特に不自由なく生成できるので、ネタとしては面白くありません。
■FlashでPDF■
さて、Flash toolkit for Force.comつかっている人います? 日本のBBSを見てもほとんど話題が出ていないですし、海外の掲示板でもFlashBuilder for Force.comが中心になってしまったようで、大変寂しい思いをしています。toolkitの開発もすっかり止まってしまいましたし、このままfade outしちゃうんですかね…。
というわけで、この機会に少しでも開発者が増えることを願って、Flash+Force.comでのPDF生成について書きます。今回はFlash+Force.comについて、次回はFlash上でのPDF生成について書きます。
■Flashでの開発■
Force.com Toolkit for Adobe AIR and Flex
何よりFlashがないと話しになりません。とりあえず30日間無料でお試しができますので、Adobeから「FlashBuilder 4.6 Standard Edition」をダウンロードしてきてください。
余談ですが、4.5からはiPhone/Androidアプリも書くことができます。試してみたらホントに15分くらいでForce.com対応iPhoneアプリを書けて大笑いしたんですが、GUIがネイティブと異なりすぎることとメモリをバカ食いするので、今のところ本気では使っていません。
さて、お次は、FlashからForce.comをアクセスするためのライブラリが必要です。これは、このページの「download toolkit」から落としてきます。
では、FlashBuilderを起動してください。ユーザ登録の手続きなどは端折ります。起動遅いっす。私の嫌いなEclipse様がベースなので諦めてください。
ファイル>新規プロジェクト>Flexプロジェクト
適当な場所に適当な名前でプロジェクトを作ります。とりあえず「Advent」にしました。アプリケーションの種類をデスクトップにする以外はデフォルトのままでOKです。
できたプロジェクトにForce.comのライブラリを追加します。さっきダウンロードしてきたライブラリの中から「force-air.swc」をプロジェクトのlibsにドラッグ&ドロップしてください。
パッケージエクスプローラの中から
Adbent>src>(default package)>Advent.mxml
を開きます。ソースの先頭にあるs:WindowedApplicationとfx:Declarationsを以下のソースと置き換えてください。
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:salesforce="http://www.salesforce.com/" applicationComplete="init()" height="560" width="800">
<fx:Declarations>
<salesforce:AIRConnection id="force"/>
</fx:Declarations>
その後にはコード部分のタグが来て<fx:Script> <![CDATA[ ]]> </fx:Script>最後にデータを表示するためのグリッドが来て最初のタグを閉じます。
<mx:DataGrid id="dataGrid"
top="10" left="23" right="23" bottom="10"
editable="false" enabled="false">
<mx:columns>
<mx:DataGridColumn headerText="id" dataField="Id"/>
<mx:DataGridColumn headerText="氏名" dataField="Name"/>
<mx:DataGridColumn headerText="電子メール" dataField="Email"/>
</mx:columns>
</mx:DataGrid>
</s:WindowedApplication>
以上が基本的なソース構造です。で、「CDATA[」の次の行からコードを書いていきます。■まずはインポートとグローバル変数■
import com.salesforce.AsyncResponder; import com.salesforce.objects.LoginRequest; import com.salesforce.results.Fault; import com.salesforce.results.LoginResult; import com.salesforce.results.QueryResult; import mx.collections.ArrayCollection; [Bindable] private var arrayUser:ArrayCollection = null;
■初期化/ログイン■
冒頭部分にあるapplicationCompleteから呼び出される初期化ルーチン、ここでログイン処理をします。昔ながらの処理でOAuthなどはありませんorz。なお、これはデスクトップアプリなのでIDとPasswordでログイン処理を行いますが、FlashとしてVisualforceに埋め込む場合にはセッションidなどからそのまま認証を引き継ぐことができるようになっています。
private function init():void
{
var lr:LoginRequest = new LoginRequest();
status = "login by id/password";
lr.username = "your.account@your.domain";
lr.password = "password" + "token";
lr.callback = new AsyncResponder(loginHandler);
force.login(lr);
}
■ログイン成功■
ログインが成功すると、この関数が呼び出されます。ここではすぐに馴染み深いSOQLを呼び出していますが、検索処理などを行う場合には、ここでGUIの「検索ボタン」をenableにする、などの処理を書きます。
private function loginHandler(result:LoginResult):void
{
if (result.userInfo != null) {
var strQuery:String = "Select Id, Name, Email From User";
force.query(strQuery,
new AsyncResponder(queryHandler,
faultHandler));
} }
■Query成功■
Query成功時にこのコードが呼び出されます。ここは1回に最大2000レコードずつ渡されます。つまり逆にいえば、数万件のレコードがヒットするようなQueryを発行しても、ここでarrayUserにひとまとめにすることができる、ということです。まぁ遅いですけどね。それにFlashは基本的にシングルタスクなので何も配慮しないで数万件のPDF帳票を出力しようとするとUIが一切操作できなくなります。不憫な奴。
で、最後にdataGridに読み込んだデータを一括してセットすれば一丁上がり。
private function queryHandler(result:QueryResult):void
{
if (result != null && result.records != null
&& result.records.length > 0)
{
if (arrayUser == null)
arrayUser = new ArrayCollection();
arrayUser.addAll(result.records);
}
if (!result.done)
{
trace("query again");
force.queryMore(result.queryLocator,
new AsyncResponder(queryHandler));
}
else
{
status = "検索または実行開始をどうぞ";
force.syncConnection
dataGrid.dataProvider = arrayUser;
}
}
■エラー処理■
SOQLに誤りがあった場合などにこの関数が呼び出されます。
private function faultHandler(result:Fault):void
{
trace(typeof(result));
trace(result.faultcode);
trace(result.faultstring);
}
■基本編終了■以上でForce.com + Flexの基本はおしまいです。次回はPDF生成編です。それまでよろしかったらSOQLを書き換えたりDataGridのdataFieldをいじったりして遊んでみてください。なお、"Select Id, Name, CurrentStatus From User"などと書いてもAPIが古いのでこのライブラリでは対応していません。あしからず。
--
この記事はForce.com Advent Calendarに参加しています。
Force.com Advent Calendar 2011 : ATND
PhoneGapかiOS SDKについて書くと予告していたのにFlashになったのは準備していたネタが土壇場になって不調だったからさ。以前書いた記事もあるし。
PhoneGap 1.2.0 + Salesforce SDK(hyblid)
以上、50歳と2日のくらはしがお送りしました。
#Bloggerさんがインデントをつぶしてしまったけど、まぁ読めるのでご勘弁
2011年12月6日火曜日
湯たんぽ10日目
■年寄りは夜眠れない■
もともと30歳以降は睡眠時間少なくて、その分仕事が片付く…と喜んでいました。
しかし40歳を過ぎてからは疲れていても2-3時間で目が覚めてしまい、そのまま寝直すこともできずに朝まで、そして昼頃にはまた眠気が来る…というのが基本的な生活パターンとして定着する始末。
自営業なら昼寝すればいいものの、2年前からサラリーマンになったので、そういうわけにもいきません。一度会社近所のカプセルホテルで「昼寝パック」というのを試して見ましたが、寝過ごすのが怖くてよく眠れません。それにさすがに昼寝に着替えてベッド、というのは大げさです。
■対策1■
レスタミンというアレルギーの薬があります。アレルギー薬としては古いタイプで、いわゆる「眠くなる風邪薬の成分」、睡眠改善薬「ドリエル」と同じ成分です。私はYS11以外の機内ではあまり眠れないのですが(何その特異体質w)、一度渡米時の機内で食後に飲んでみたら朝(?)まで爆睡してしまって、それ以来愛用していました。ただドリエルは少し成分量が多くて私は頭が痛くなったり12時間ぐらいぼんやりしてしまいます。
でも、レスタミンは1錠あたりの成分がドリエルの1/3なので調整できます。何より値段が遙かに安い。今夜寝ておかないと明日が辛いぞという時に1-2錠のんでからベッドに入るとわりと熟睡できます。寝起きは少しボーッとする程度ですが、すぐに普通のペースで行動できます。
ただ、最近の研究によると服用後24時間ぐらいは反応などが鈍くなっているそうなので、クルマの運転などをする方は週末などに様子を見てから試してください。
このクスリ、私には効果絶大なのですが、やっぱりあまりクスリには頼りたくないです。
■で、湯たんぽ■
私のベッドは北向きの部屋にありまして、そうでなくてもうちは田舎の9Fなので冬は見事に寒い。かといって朝までエアコン付けっぱなしというのも勿体ないし喉痛い。
というわけで、湯たんぽ買ってみました。無印良品のプラスチック製2リットルと一番安いフェイクファーのカバー、合わせて2000円ちょっと。
ほどよい保温性能で熱湯を入れても熱くならないし朝になってもまだ少し温かい。暖かさに関しては期待通りで、かなり寒い日でも暖房なしで過ごせています。自宅作業の時にはヨメを見送った後エアコンを切って足元に置いてます。
とても温かい。
で、睡眠に関しては期待していなかったのですが…湯たんぽを使っている夜は、今のところ5-6時間寝られています。私はもともと高脂肪高血圧で冷え性とは縁遠いので、湯たんぽの必要性はぜんぜん感じていなかったんですが…頭寒足熱ってやっぱり身体に良いんですかね。
もともと30歳以降は睡眠時間少なくて、その分仕事が片付く…と喜んでいました。
しかし40歳を過ぎてからは疲れていても2-3時間で目が覚めてしまい、そのまま寝直すこともできずに朝まで、そして昼頃にはまた眠気が来る…というのが基本的な生活パターンとして定着する始末。
自営業なら昼寝すればいいものの、2年前からサラリーマンになったので、そういうわけにもいきません。一度会社近所のカプセルホテルで「昼寝パック」というのを試して見ましたが、寝過ごすのが怖くてよく眠れません。それにさすがに昼寝に着替えてベッド、というのは大げさです。
■対策1■
レスタミンというアレルギーの薬があります。アレルギー薬としては古いタイプで、いわゆる「眠くなる風邪薬の成分」、睡眠改善薬「ドリエル」と同じ成分です。私はYS11以外の機内ではあまり眠れないのですが(何その特異体質w)、一度渡米時の機内で食後に飲んでみたら朝(?)まで爆睡してしまって、それ以来愛用していました。ただドリエルは少し成分量が多くて私は頭が痛くなったり12時間ぐらいぼんやりしてしまいます。
でも、レスタミンは1錠あたりの成分がドリエルの1/3なので調整できます。何より値段が遙かに安い。今夜寝ておかないと明日が辛いぞという時に1-2錠のんでからベッドに入るとわりと熟睡できます。寝起きは少しボーッとする程度ですが、すぐに普通のペースで行動できます。
ただ、最近の研究によると服用後24時間ぐらいは反応などが鈍くなっているそうなので、クルマの運転などをする方は週末などに様子を見てから試してください。
このクスリ、私には効果絶大なのですが、やっぱりあまりクスリには頼りたくないです。
■で、湯たんぽ■
私のベッドは北向きの部屋にありまして、そうでなくてもうちは田舎の9Fなので冬は見事に寒い。かといって朝までエアコン付けっぱなしというのも勿体ないし喉痛い。
というわけで、湯たんぽ買ってみました。無印良品のプラスチック製2リットルと一番安いフェイクファーのカバー、合わせて2000円ちょっと。
![]() |
| たれぱんだは付いてきません。 |
ほどよい保温性能で熱湯を入れても熱くならないし朝になってもまだ少し温かい。暖かさに関しては期待通りで、かなり寒い日でも暖房なしで過ごせています。自宅作業の時にはヨメを見送った後エアコンを切って足元に置いてます。
とても温かい。
で、睡眠に関しては期待していなかったのですが…湯たんぽを使っている夜は、今のところ5-6時間寝られています。私はもともと高脂肪高血圧で冷え性とは縁遠いので、湯たんぽの必要性はぜんぜん感じていなかったんですが…頭寒足熱ってやっぱり身体に良いんですかね。
2011年12月4日日曜日
Resizeしたい
■ごく単純なこと(そして落とし穴)■
現在開発中のiPhoneアプリはTableViewにカスタマイズしたCellを置いてそこにdatabase.comから取ってきたデータを表示するだけのごく単純なものなのです。ええごく単純。
要件としてはorientation(画面の向き)に応じてcellの幅を変える必要があるぐらい。Autoresizingを使えばほんと簡単。
あと表示するデータが可変長文字列なので、cellの高さを変える必要があります。これも単純な話で、以下のコードをview controllerに書いてやればおk
なのですが…。このboundsがくせ者でした。当然、当然cell上に置いたUILabelの幅を使います。このLabelにもautoresizingを設定してますから、iPhoneを横向きにすると当然リサイズされます。しかし…boundsに入ってくるwidth、つまりUILabelのサイズは初期設定値のままでリサイズ後のwidthを返してくれません。
■毎度おなじみ試行錯誤地獄■
そこから約4時間ほど試行錯誤を繰り返しました。覚えている範囲で列挙すると:
で、最終的にわかったことは、「cellの中に置いたLabelの幅がわかるのは、tableViewのheightForRowAtIndexPathが呼ばれた後」ってことでした。
描画の仕組みから考えて、そんなことはないと思うのですが…現状でそれを知る方法が見つかりませんでした。とほほ。
現在開発中のiPhoneアプリはTableViewにカスタマイズしたCellを置いてそこにdatabase.comから取ってきたデータを表示するだけのごく単純なものなのです。ええごく単純。
要件としてはorientation(画面の向き)に応じてcellの幅を変える必要があるぐらい。Autoresizingを使えばほんと簡単。
あと表示するデータが可変長文字列なので、cellの高さを変える必要があります。これも単純な話で、以下のコードをview controllerに書いてやればおk
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = (MyCell *)[self tableView:self.tableView
cellForRowAtIndexPath:indexPath];
float width = cell.status.frame.size.width;
CGSize bounds = CGSizeMake(width, self.tableView.frame.size.height);
CGSize size = [cell.status.text sizeWithFont: cell.status.font
constrainedToSize: bounds
lineBreakMode: cell.status.lineBreakMode];
return MAX(size.height, ConstMinCellHeight);
}
■毎度おなじみ試行錯誤地獄■
そこから約4時間ほど試行錯誤を繰り返しました。覚えている範囲で列挙すると:
- とりあえずautoresizingのモードをあれこれ試す
- 友人のアドバイスに従いboundsへのkey value notificationを使用
- UILabelのサブクラスを作ってsetBounds/setFrame/drawRectなどをオーバーライド
で、最終的にわかったことは、「cellの中に置いたLabelの幅がわかるのは、tableViewのheightForRowAtIndexPathが呼ばれた後」ってことでした。
描画の仕組みから考えて、そんなことはないと思うのですが…現状でそれを知る方法が見つかりませんでした。とほほ。
■美しくない妥協案■
というわけで、deviceOrientationDidChangeでiPhoneの向きが変わったことを検出したら、そこで定数からラベル幅を算出する、という方法にしました…。一応、デバイスの規定値が変わってもそのまま動くようにはしてあるけど…どうも美しくないっす。
問題は、この方法は最初からわかっていたということで…「美しくないから」という理由だけで4時間試行錯誤してしまったのはどーなの?というあたり。まぁ時間単価で仕事しているわけじゃないから良いんだけど…困ったもんだ。
まぁ、あれだ、4時間の試行錯誤でいろいろ分かったこともあったから、そのうちそれらが花開く日も来るだろう、ということで…。
2011年11月18日金曜日
Visualforceが憎いいいい
■簡単なこと■
てな感じの罫線入った表を表示したいとします(今回はグラデーション入れませんが)。まぁ日本のWeb屋さんにはごく日常的な作業で、処理系によって違いはあるけど
- ループを回しながらコントロール・ブレークで行を出力していく
- 先にコントロール・ブレーク済みの行データを配列にいれておいて出力
のどっちかで対応しますわな。
Visualforceだとループ内側では動的な評価ができないので後者で作ります。作りました。
■Visualforce困ったちゃん■
で、分類と詳細の行はレイアウトが違うので、分類の行だったら分類用のタグ、詳細行には詳細用のタグ…を出力させます。
そのために多くのフレームワークには「この内側のタグsを出力するか否か」をスイッチする機能が用意されてます。史上最高のWebフレームワークたるWebObjectsにはWOConditionalがあって(以下長いので省略
しかし。
Force.comってかVisualforceにはありません。ので、<apex:include>とか<apex:component>などを用いてコンポーネントとして作った要素を埋め込みrenderedでon/offします。
これでHTMLを生成すると…見事に表が崩れます。ソースを読んでみると…componentの前後に勝手に<span></span>が挿入されており、結果として以下のようなソースが生成されてしまいます。
<Table>そりゃ崩れるわなorz。
<TR><SPAN><TH>品目</TH><TH>金額</TH></SPAN></TR>
<TR><SPAN><TD>Mac</TD><TD>128,000</TD></SPAN></TR>
</Table>
■しかし天才はいる■
で、困った時のGoogle先生ってことで apex:component span で検索してみたら、一発でworkaroundが見つかりました。「apex:component rendered in SPAN」、いやー、その手は思いつかなかった。まさに天才です。
さっそく実装してみたら、
<Table>
<TR>
<SPAN></SPAN>
<TH>品目</TH><TH>金額</TH>
<SPAN></SPAN>
</TR>
<TR>
<SPAN></SPAN>
<TD>Mac</TD><TD>128,000</TD>
<SPAN></SPAN>
</TR>
</Table>
となり、ブラウザ上では意図した通りの表示が出ました。
喜び勇んでrenderAs="PDF"を実行したら…。
…はい、終了。お疲れ様でした。まぁ、そらそうよね、<TR>の内側だしねorz
登録:
投稿 (Atom)







