{ これは Delphi Advent Calendar 2013 の 12/24 分の記事です }
前回はコンポーネントエディタの作り方をやりました…それは TStringGrid 用のコンポーネントエディタだったのですが、「TStringGrid と言えば使っていて思う事が…」今回はそんな話です (多分)。
TStringGrid は基本的に表示用です…もちろん表計算のように入力する事もありますね。表計算のように入力できちゃうと、"計算させる" という要望が当たり前のように出てきますね。そこで思うことは…
- TStringGrid は文字列しか格納できない。
- Objects[] があるのはもちろん解ってる。
- 計算とかどうするの?毎回 StrToFloat() / FloatToStr() とかして書き戻してるの?
という事です。「表が固定」 あるいは 「読み込んだ後は変化しない」 というのなら、Currency の二次元配列とか、二次元動的配列とかで TStringGrid と同じサイズのワークを作ってそちらで計算してやればいいと思うのです…これは多分普通に辿り着く考え方だと思います。
[配列型 (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/XE5/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B#.E9.85.8D.E5.88.97.E5.9E.8B
…でもね。実際にはとんでもない要求があって、縦横 (Row/Col) が条件によって可変しちゃったり、グリッド上には存在しないデータを元に計算しなくちゃいけない事もありますよね?ぇ、そんなのなかったですか?それはとても幸せなことです (^^;A
で、作ったのがコレです (ここに貼るにはリストがちょっと長いので定義部だけ)。
[uExtendedSheet.pas]
type TExtendedDynArray = array of Extended; {$EXTERNALSYM TExtendedDynArray 'System::TExtendedDynArray'} TExtended2DDynArray = array of TExtendedDynArray;
TExtendedSheet = packed record Value: TExtended2DDynArray; // データ実体 private { Access Methods (for Property) } function GetCells(ACol, ARow: Integer): Extended; procedure SetCells(ACol, ARow: Integer; const Value: Extended); function GetColCount: Integer; procedure SetColCount(const Value: Integer); function GetRowCount: Integer; procedure SetRowCount(const Value: Integer); { Private Methods } function GetValidCol(aCol: Integer): Integer; // 有効な列インデックスを返す function GetValidRow(aRow: Integer): Integer; // 有効な行インデックスを返す public { Methods } procedure AddCol; // 列を追加 procedure AddRow; // 行を追加 procedure Clear; // すべての値を 0 クリアする procedure ClearCol(aCol: Integer); // 指定された列を 0 クリア procedure ClearRow(aRow: Integer); // 指定された列を 0 クリア procedure DeleteCol(aCol: Integer); // 指定された列を削除 procedure DeleteRow(aRow: Integer); // 指定された行を削除 procedure Init(Cols, Rows: Integer); // 配列のサイズを初期化する procedure InsertCol(aCol: Integer); // 指定された位置に列を挿入 procedure InsertRow(aRow: Integer); // 指定された位置に行を挿入 function MaxValue(aCol: Integer): Extended; // MaxValue(): 最大値を返す function Mean(aCol: Integer): Extended; // Mean(): 平均を返す procedure MeanAndStdDev(aCol: Integer; var Mean, StdDev: Extended); // MeanAndStdDev(): 平均と標準偏差を返す function MinValue(aCol: Integer): Extended; // MinValue(): 最小値を返す function PopnStdDev(aCol: Integer): Extended; // PopnStdDev() : 母標準偏差を返す function PopnVariance(aCol: Integer): Extended; // PopnVariance(): 母分散を返す procedure SetColValues(aCol: Integer; aValues: array of Extended); // 列データをセット procedure SetRowValues(aRow: Integer; aValues: array of Extended); // 行データをセット function StdDev(aCol: Integer): Extended; // StdDev(): 標準偏差を返す function Sum(aCol: Integer): Extended; // Sum(): 合計値を返す function SumOfSquares(aCol: Integer): Extended; // SumOfSquares() : 2 乗和を返す procedure SumsAndSquares(aCol: Integer; var Sum, SumOfSquares: Extended); // SumsAndSquares() : 合計値と 2 乗和を返す function TotalVariance(aCol: Integer): Extended; // TotalVariance(): 全分散を返す function Variance(aCol: Integer): Extended; // Variance(): (標本)分散を返す { Properties } property Cells[ACol, ARow: Integer]: Extended read GetCells write SetCells; // セル property ColCount: Integer read GetColCount write SetColCount; // 列の数 property RowCount: Integer read GetRowCount write SetRowCount; // 行の数 end;
ミもフタもない言い方をすれば "Extended 型二次元動的配列の高度なレコード型" という事になります。TStringGrid のメソッドやプロパティのような感じで使えます。TStringGrid 用計算ワークというより汎用計算ワークですね。名前を TExtendedMatrix にしようかと思ったのですが、行列計算用という訳ではないので Matrix という名前は避けました。演算子オーバーロードを実装すれば行列計算用途に使えるようにする事もできますけどね (串刺し演算用にもできますが)。
[高度なレコード型 (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B#.E3.83.AC.E3.82.B3.E3.83.BC.E3.83.89.E5.9E.8B.EF.BC.88.E9.AB.98.E5.BA.A6.EF.BC.89
[演算子のオーバーロード(Delphi) (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/%E6%BC%94%E7%AE%97%E5%AD%90%E3%81%AE%E3%82%AA%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AD%E3%83%BC%E3%83%89%EF%BC%88Delphi%EF%BC%89
さて、前置きはここまでにして具体的な使い方を。
uses ..., uExtendedSheet;
procedure TForm1.Button1Click(Sender: TObject); var Sheet: TExtendedSheet; begin with Sheet do begin // 動的配列を初期化 Init(3, 3); Clear;
// 三行のデータをセット Cells[0, 0] := 1; Cells[1, 0] := 2; Cells[2, 0] := 3;
Cells[0, 1] := 4; Cells[1, 1] := 5; Cells[2, 1] := 6;
Cells[0, 2] := 7; Cells[1, 2] := 8; Cells[2, 2] := 9;
// 一列目の値の最大値を表示 ShowMessage(FloatToStr(MaxValue(0)));
// 二列目の値の合計値を表示 ShowMessage(FloatToStr(Sum(1)));
// 三列目の値の平均値を表示 ShowMessage(FloatToStr(Mean(2))); end; end;
配列用関数である Sum() や Mean() 等もメソッドとして実装してありますので簡単な計算ならこなせます。複雑なものはループでグルグルするしかないですけれど、StrToFloat() / FloatToStr() とかで何度も何度も書き戻すよりかは遥かにスマートに書けます。
ShowMessage() のトコですが、XE3 以降だとプリミティブ型にヘルパーがあり、XE4 以降だと…
// 一列目の値の最大値を表示 ShowMessage(MaxValue(0).ToString(ffGeneral, 10, 2)); // XE4 or later
// 二列目の値の合計値を表示 ShowMessage(Sum(1).ToString(ffGeneral, 10, 2)); // XE4 or later
// 三列目の値の平均値を表示 ShowMessage(Mean(2).ToString(ffGeneral, 10, 2)); // XE4 or later
ToString() を用いてこのように書けます。コードエディタの補完が利くので便利ですね。ToString() は Extended 型のヘルパー (TExtendedHelper) のメソッドですが、残念ながら XE3 にはありません。
[System.SysUtils.TExtendedHelper (DocWiki)]
http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.TExtendedHelper
動的配列のリサイズは RowCount / ColCount プロパティで行え、AddCol() / AddRow() / InsertCol() / InsertRow() / DeleteCol() / DeleteRow() メソッドで任意の列と行を追加 (or 挿入) したり削除したりできます。拡張方向にリサイズした場合、拡張した領域は 0 で初期化されます。
動的配列へのアクセスは Cells[] プロパティで行えます。TStringGrid みたいですね…ただ、一件一件値をセットするのは面倒なので、一気にデータを登録する SetRowValues() / SetColValues() も実装してあります。使い方はこんな感じです。
// 三行のデータをセット SetRowValues(0, [1, 2, 3]); SetRowValues(1, [4, 5, 6]); SetRowValues(2, [7, 8, 9]);
SetRowValues() の第一引数は "行インデックス" で、第二引数は Extended 型のオープン配列パラメータです。基本的にオープン配列パラメータのパラメータ数は ColCount と一致させなくてはいけませんが、後方を省略する事も可能です。省略部分の値は更新されません (0 クリアされる訳ではありません)。例えば列数が 3 の場合、以下のコードを実行した後の Value[0] の値は (0, 0, 3) となります。
Sheet.SetRowValues(0, [1, 2, 3]); Sheet.SetRowValues(0, [0, 0]);
逆に、オープン配列パラメータのパラメータ数が ColCount より大きい場合には超過分は無視されます。エラーにはなりません。
「オープン配列パラメータって何ぞ?」という方もいらっしゃるかもしれませんので、一応説明しておきます。オープン配列パラメータで一番よく目にするのは Format() 関数でしょう。Format() 関数の第二引数はオープン配列パラメータとなっています (正確には "型可変オープン配列パラメータ" です)。
[System.SysUtils.Format (DocWiki)]
http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Format
SetRowValues() の定義は以下のようになっています。
procedure SetRowValues(aRow: Integer; aValues: array of Extended);
仮引数に "array of 型" を使った場合、それはオープン配列パラメータとなります。このようにして Extended の配列を渡す事もできます。
procedure TForm1.Button2Click(Sender: TObject); var RowVal: array [0..2] of Extended; begin RowVal[0] := 100; RowVal[1] := 200; RowVal[2] := 300; Sheet.SetRowValues(0, RowVal); end;
オープン配列パラメータに引数として渡せるのは以下のものです。
- 指定した型の静的配列
- 指定した型の動的配列
- オープン配列コンストラクタ
"オープン配列コンストラクタ" というのは[]で括ったアレです。似ていますが集合 (Set of) ではありません。今回、二次元動的配列でオープン配列パラメータの話をしているのでややこしいのですが、配列とオープン配列パラメータに直接の関係はありません。
[集合 (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B#.E9.9B.86.E5.90.88
パラメータにはちょっと特殊な「(オープン)配列パラメータ」というのがあって、この特殊なパラメータの場合には「(オープン)配列コンストラクタ」と呼ばれる機構を使い「配列パラメータを直接作って渡す事ができる」という事になります。この記述方法は "型の配列ではなくパラメータの配列である" というのがミソです。それ故、以下のようなコードは書けません。
procedure foo(Values: array of array of Integer); begin
end;
裏を返せば Format() 関数の第二引数は型可変オープン配列パラメータなのですから、以下のようなコードが通ります。
procedure TForm1.Button1Click(Sender: TObject); var VarRec: array [0..1] of TVarRec; begin // Parameter #1 VarRec[0].VType := vtInteger; VarRec[0].VInteger := 100; // Parameter #2 VarRec[1].VType := vtUnicodeString; VarRec[1].VUnicodeString := PChar('ABC'); // Format() ShowMessage(Format('%d %s', VarRec)); end;
型可変オープン配列パラメータは "型が TVarRec なオープン配列パラメータ" と同義です。
[配列パラメータ (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/パラメータ#.E9.85.8D.E5.88.97.E3.83.91.E3.83.A9.E3.83.A1.E3.83.BC.E3.82.BF
[System.TVarRec (DocWiki)]
http://docwiki.embarcadero.com/Libraries/ja/System.TVarRec
ちょっと話が逸れてしまったので動的配列の話に戻りましょう。通常、動的配列の配列確保には SetLength() を使います。で、二次元動的配列の場合には SetLength() して確保した動的配列を要素毎にさらに SetLength() します。
procedure TForm1.Button1Click(Sender: TObject); var Str2DDynArray: array of array of String; Col: Integer; begin // 一次元目の確保 SetLength(Str2DDynArray, 10); // 二次元目の確保 for Col:=Low(Str2DDynArray) to High(Str2DDynArray) do SetLength(Str2DDynArray[Col], 10); end;
こんな感じに。二次元目の確保でグルグルしなきゃいけない理由はヘルプに書いてあります。
特定の次元の配列の長さがすべて同じではない多次元動的配列を作成することもできます。
[多次元動的配列 (DocWiki)]
http://docwiki.embarcadero.com/RADStudio/ja/%E6%A7%8B%E9%80%A0%E5%8C%96%E5%9E%8B#.E5.A4.9A.E6.AC.A1.E5.85.83.E5.8B.95.E7.9A.84.E9.85.8D.E5.88.97
碁盤状でない二次元動的配列 (ジャグ配列) を作るためですね。表計算のような碁盤状の二次元動的配列 (矩形配列) の確保のためには DynArraySetLength() という関数が別にあります。
[System.DynArraySetLength (DocWiki)]
http://docwiki.embarcadero.com/Libraries/ja/System.DynArraySetLength
…ですが、そこにも書いてあるように SetLength() は多次元動的配列を一度で確保できるのです。
同じ操作を SetLength 手続きを使って行うこともできます(この方法が推奨されています)。
begin
SetLength(A, 3, 4);
end.
[多次元配列の初期化。(全力わはー)]
http://d.hatena.ne.jp/tales/20090816/1250416430
「だったら、SetLength() の方に書いておいてくれよ!」と思うのは私だけでしょうか (^^;A
[System.SetLength (DocWiki)]
http://docwiki.embarcadero.com/Libraries/ja/System.SetLength
ちなみにこの SetLength() の書き方ですが、Delphi 4 から利用可能です(Delphi XE4 からではありません。Delphi 4 の発売は 1998 年ですから…15年前からですね)。
そりゃそうなのですが、動的配列をラッピングするという考え方は以前からあるようで、Delphi Q&A の投稿にも以下のようなものがありました。
[TListでの二次元配列クラスの作成 (Delphi Q&A)]
http://homepage1.nifty.com/MADIA/delphi/delphi_bbs/200405/200405_04050048.html
この中で "ポインタの二次元配列の代わりに pf32bit な TBitmap を使う" というアイデアがレスされていました。(64bit アプリを考えると)今となっては使えないテクニックではありますがとてもユニークですよね。その昔の 8bit /16bit 機では使えるメモリが少なかったので VRAM をメモリとして使っていたのを思い出しました。
メモリが少ない…
リニアにメモリが使えない…
16bit 機にはセグメントが (EMS ってダイエット器具じゃないんやで) …
X68Kとかはメモリをリニアに使えましたけどね (それでも最大 12MB)…
マシン性能も今から考えれば 1/100 以下だしなぁ…
スミマセン…オッサンの昔話です (w
さてさて、今回の記事はここまでとなります。TExtendedSheet は演算ワークとしての基本はそれなりに押さえてあると思いますが、好きなように改変してお使いください。Excel っぽい事をやりたいのであれば、データとして何でも放り込める TVarRec の動的二次元動的配列で作ってみるのも面白いかと思います。面白い機能を持つ演算ワークができたり、「いや、演算用ワークならもっといい手法があるよ!」的なのがあったら是非教えてください♪
この記事で紹介した uExtendedSheet のアーカイブは以下にあります。
Download: http://ht-deko.minim.ne.jp/software/uextendedsheet_110.zip
※このユニットはハラヘッタウェアとなっています。
http://ht-deko.minim.ne.jp/delphiforum/harahettaware/
|