[FileMaker] フォルダ階層ツリー表示 仕組みの詳細

FileMakerで作るメディア管理データベースで採用している機能の詳細についての投稿です。登録したメディアファイルの場所をフォルダ階層のツリー表示して、フィルタします。

この投稿は FileMakerメディア管理 作り方 R11 以降を補完する内容でもあります。ここで取り上げる機能は、添付の実作ファイル「MediaDB_R11」に準じますのでご参照ください。

FileMakerメディア管理 R11 階層ツリー、LibVer、付属情報

フォルダ階層ツリー表示

フォルダ階層ペイン

ビューアタイプの画像管理アプリで見かけるフォルダ階層のペインです。ボリュームトップから階層を辿り、好きな階層位置で検索しレコードを絞り込んだりするアレです。

フォルダ階層ツリー表示

フォルダ階層が表示され、クリックで展開・折り畳みがされます。選んだフォルダでフィルタもできます。フィルタは、右側の一定範囲がボタンのエリアになっていて、そこをクリックします。

 

 

本来なら ► ▼ 部分で展開・格納、文字部分で検索されるのが望ましいのですが、► ▼ とテキストでボタンを分ける簡素な仕組みがが思いつかず、現時点ではこのようにちょっと不細工な仕上がりになっています。

これを FileMakerメディア管理 R11 階層ツリー、LibVer、付属情報 で実装したので、詳細をこちらで補完します。

仕組みの概要

メインデータを扱うmainData と、今回新たに作成した Folders がリレーションされています。リレーションの照合フィールドはmainDataのファイル名を記入したグローバルフィールドで、つまりこのリレーションは、無条件に全レコードが繋がります。

出来上がったツリー表示から辿って説明すると次のようになります。

  1. ツリー表示は、「Folders」テーブルのレコードをポータルで表示しています。
  2. 折り畳みや階層の表現は、ポータル行に配置したボタンバーの「名前」に書かれた計算式で成立しています。
  3. Foldersテーブルのレコードは、メインの「mainData」テーブルの「フォルダ階層」フィールドを集計して分配したものです。
  4. mainDataテーブルの「フォルダ階層」は、ファイルパスを階層で分割した改行区切りのテキストです。

これを逆に成立順で書くとこうなります。

  1. mainDataテーブルのレコードにはメディアファイルのパスがあり、パスを元に改行区切りテキストを生成し「フォルダ階層」フィールドに記入されます。
  2. 全レコード分の「フォルダ階層」をまとめてリストし、ユニーク化して、それから Foldersテーブルにレコードとして分配します。
  3. Foldersテーブルのレコードをメインレイアウトでポータルに表示します。行に配置したボタンバーの設定を行い、階層の表現、折り畳みの仕組み、検索スクリプトの実行をまとめて制御します。

仕組み

mainData フォルダ階層フィールド

データファイルのテーブル mainData の「フォルダ階層」というフィールドです。パスの階層すべてを分離しリストしたテキストフィールドで、ファイルパスから計算で作成します。

mainData テーブルはメディアファイルをレコードで管理しているテーブルですので、ファイルパスのフィールドがすでにあります。例えばパスがこうであるとします。

Volumes/DiskA/works/image/pict001.jpg

「フォルダ階層」は自動計算によりこうなります。

DiskA
DiskA/works
DiskA/works/image

「フォルダ階層」を作る計算式は次の通り。計算元になるパスは POSIXですが、計算途中にFileMaker形式のパスも使い、足りない情報を補い合います。

計算式
// POSIX(ファイル)から階層を分解してリストする関数

While ( [ 

posix = POSIX ;
fileName = GetValue ( Substitute ( posix ; "/" ; ¶  ) ; ValueCount ( Substitute ( posix ; "/" ; ¶  ) ) ) ;

toFM = ConvertToFileMakerPath ( posix ; PosixPath );
Path = Substitute ( toFM ; [ "file:/" ; "" ] ; [ "/" & fileName ; "/" & "" ]) ;
vc = PatternCount ( Path ; "/" )  ;
c = 0 ;
KList = ""

 ] ;

 c < vc ; [ 

c = c + 1 ;
posi = Position ( Path ; "/" ; 1 ; c );
line = Replace ( Path ; posi ; 1 ; ¶ );
line = GetValue ( line ; 1 )  ;
KList = List ( KList ; line )

 ] ; 

KList )

 

POSIXでは起動ディスクのユーザーフォルダ内の場合、 /Users/userName/… こういうはじまりになってしまいボリュームが現れませんし。
FMパスは余計な接頭詞が邪魔ですがそれさえ外すと一律に /VomumeName/… で始まります。FMパスからボリュームをいただき、書式はPOSIXに準じるという、二つのパス書式で補い合うっていうのはこのことです。

Folders テーブル

Foldersテーブルは、メインレイアウトにポータル表示するためにあるテーブルです。このテーブルに mainData::フォルダ階層 を全レコード分集計してリストしてレコードに分配します。

「フォルダ階層」フィールド1行分を納めるのが「FolderPath」フィールドです。これが主たるフィールドになります。他、ポータル表示に必要な「階層数」「フォルダ名」「ラスト」といったフィールドも用意しています。
「フォルダ名」はパスの最後のディレクトリで、ポータルではラベルとなります。「ラスト」というのは、元のパスの最後のディレクトリかどうか、最後のディレクトリなら 1 が入ります。

mainData のパスが Volumes/DiskA/works/images/pict_001.jpg なら次の3つのレコードになります。

pathField 階層 フォルダ名 ラスト
DiskA 1 DiskA
DiskA/works 2 works
DiskA/works/images 3 images 1

「ラスト」は mainData から引っ張ってこれそうでしたが、それは難しく、リストを作る過程の計算式で取得したほうが効率が良いのでした。

フォルダ階層を分解してレコード化するスクリプト

大きく二つのスクリプトになります。

  1. 「フォルダ階層」フィールドを集計(一覧)してユニーク化するスクリプト
  2. それをFoldersテーブルのレコードに分配するスクリプト

まず集計ですが、集計フィールドを作れば済む話でもあります。でもそれをせず、スクリプトの中で計算して作ります。いつもいつも集計が必要なわけでもないし、フィールドに置いて常時計算させるのも無駄な感じがしたので。実際どちらが効率良いのかはわかりません。

集計フィールドのように特定フィールドを全レコード分リスト化

ここで余談を挟みたいけどいいですか?集計フィールドでなくてももっと簡単に同じ目的を果たせる機能があります。値一覧です。同じ目的があるとき、常に値一覧を利用してきました。でもどうしてもフォルダ階層だけは値一覧では上手く機能しないんです。これが長年の謎でした。今は克服していますが、ここまで到達するのに数々の試練を経てきました。その冒険の記録は別の機会に譲るとして(この投稿の下の方にオマケで書きました)結論だけ。次のような計算式を使っています。

集計フィールドと同様の結果 + ユニーク化の計算式
// 集計フィールドのように、対象レコードの指定フィールド内容をリストするスクリプト(レイアウト内フィールドのみ有効)
While ([
  field = フィールド ;
  FL = "" ;
  vc = Get ( 対象レコード数 ) ;
  c = 0
 ] ;
  c < vc ;
 [
  c = c+1 ;
  GNR = GetNthRecord ( field ; c ) ;
  FL = List ( FL ; GNR )
 ] ;
 FL 
)

↑ これでリスト化したあと UniqueValues() でユニーク化

集計フィールドの「一覧」と同じように機能しつつ、ユニークな値のみリストしています。カスタム関数FoundFieldListUnique() として保存し、他のファイルでもよく使っています。

リストをレコード化

全レコード分のフォルダ階層が値一覧的なリストになりました。あとはFoldersテーブルに分配してレコード化します。ただ分配するだけでなく、主軸のFolderPsthフィールド以外のフィールドを同時に埋めていきます。

「階層数」はそのまま階層数、「 / 」を数えると計算できますね。「folder」フィールドは最後尾のディレクトリ名です。ポータルではラベルとして機能します。これも取得は簡単。

「ラスト」は元のパスの最後尾のディレクトリ名です。これより下にフォルダはないよということですね。元パスの最後尾なら「1」を入れます。これは取得する計算式がちょっとだけ難しいですね。リストをソートしておいて「次の行」に「自身 + / 」が含まれていればまだ下層にフォルダがある、含まれなければラストです。

スクリプトの流れは、集計を保存したリストを1行ずつFoldersテーブルに新規レコードしつつ、上記処理を同時に行います。

こうして、全レコードのメディアファイルパスを階層で分解した集計のユニーク値がFoldersテーブルにレコード化されました。

メインレイアウトのポータルで表示

Foldersテーブルをメインレイアウトにポータルで配置します。

Foldersポータルを配置

ポータルフィルタにはこう書かれます。

Folders::階層 = 1 or Folders::表示 = 1

階層が 1 であるか、または「表示」が 1 であるレコードを表示します。階層が 1 であるレコードとは、ボリュームのみが書かれたレコードですね。

ポータル表示に期待するのは、まずは折り畳み表現です。折り畳みは階層を掘り進めるインターフェイスで必須です。「表示」フィールドに 1 を入れたり消したりすれば折り畳みみたいな表現が可能と予想できますね。

Folders ポータル 折り畳み表現

もうひとつ、階層クリックでその階層に含まれるレコードが検索されるフィルタ機能も期待される動作です。ただ、折り畳み表現とフィルタ実行を混在させると動作が鈍くなるので一緒にはしないほうが良さそうでした。それで、フィルタ実行は残念ながら行に配置した別のボタンで機能させるほかありませんでした。

分けるのはいいとして、ボタン機能は ▼ ► 部分とフォルダ名部分で分けるのがほんとうなら筋です。でもそれを実現できる方法を思いつけなくて、現状の中途半端なインターフェイスになってしまっています。

ポータルでの折り畳み表示

Foldersテーブルには主となるフォルダパス以外に、表示や制御に必要なフィールドがいくつかあります。

ラベルとなる「フォルダ名」元パスの最後のディレクトリ「ラスト」、その他に「表示」と「stat」というフィールドを用意しています。「表示」フィールドは 「1」が入る数字フィールド、「stat」フィールド は「open」または「close」が入るフィールドです。

「表示」が1ならポータルフィルタにより表示され、stat が open なら「フォルダが開いている」と判断され、一つ下の階層に計算で「1」がつきます。
「ラスト」が記録されているので、「次の階層」がもうないことも判断できます。

ポータルの行にはボタンバーがあるだけです。フィールドを置いていません。ボタンバーの名前の計算式欄で表示をすべてコントロールしています。階層数による位置のずれ、▼ ► とその方向、フォルダ名表記などを計算しています。展開とフィルタを▼位置で分離できないのはこのためです。

ボタンバー名前の計算式

While ( [ 
 txt = "";
 vc = Folders::階層;
 c = 1
] ; c < vc ; [ 
c = c + 1 ;
 txt = txt & " "
] ; txt )
&
If ( Folders::ラスト = 1 ; " " & Folders::Folder ;  
  If ( Folders::stat = "open" ; "▼ " & Folders::Folder ;
  "► " & Folders::Folder
  )
)

この計算式は MediaDB R12 ではさらに進化し、フォルダの絵文字を付け加えました(笑)

While ( [ 
 txt = "";
 vc = Folders::階層;
 c = 1
] ; c < vc ; [ 
 c = c + 1 ;
 txt = txt & " "
] ; txt )
 &
If ( Folders::ラスト = 1 and GetValue ( Substitute ( Folders::Now ; "/" ; ¶ ) ; ValueCount ( Substitute ( Folders::Now ; "/" ; ¶ ) ) ) = Folders::Folder ;
	TextSize ( "    " ; 9 ) & " 📂 " & Folders::Folder ;  
	If ( Folders::ラスト = 1 ;
		TextSize ( "    " ; 9 ) & " 📁 " & Folders::Folder ;
		If ( Folders::stat = "open" ; TextColor ( TextSize ( "▼ " ; 9 ) ; RGB ( 121 ; 121 ; 121 ) ) & " 📂 " & Folders::Folder ;
			TextColor ( TextSize ( "► " ; 9 ) ; RGB ( 121 ; 121 ; 121 ) ) & " 📁 " & Folders::Folder
		 )
	)
)

 

そしてこのボタンバークリックで発動するスクリプトですが、次のような仕事をします。

行のフォルダが「ラスト」かどうかを見て、ラストなら検索スクリプトの「フィルタする」を実行して終了。

stat が open なら、Foldersテーブルを開いて自分自身と同じパスを含み、かつ「表示」が1のレコードを検索して「表示」を0、「stat」を closeに全置換。つまり「折り畳み」動作を行う。

stat が close なら、Foldersテーブルを開いて階層数と自分自身のパスが等しく、かつ「表示」が 0 のレコードを検索して「表示」を 1 に全置換。つまり「展開」動作を行う。

こうして書いても何のこっちゃという感じですが一応書いときましたが。ポータルの表示を一身に引き受けているボタンバーでした。

巻末付録:Folder階層表示実現に至る試練

最後に、オマケの読み物です。別投稿にと思ってたけどばからしいのでここに書いておきます。

フィルタペインと同じ仕組み

別口ですでに「フィルタペイン」があります。特定一意のフィールドをポータルに表示しフィルタ実行する仕組みです。フィルタの分類項目をタイトル代わりに、クリックで折り畳みと展開します。展開すると値が並ぶので、選んでクリック、フィルタするという機能です。

FileMaker メディア管理 — フィルタの詳細

フォルダ階層は、これとまったく同じ仕組みです。

フィルタペインを作りながら、フォルダ階層もついでに作っていました。同じ仕組みだし。違うのは「フォルダ階層」を作る計算式や検索実行の末端部分だけの筈です。の、筈でした。

仕組みの理屈はシンプルで「フィールドの内容を全レコード分リスト化する」「テーブルに展開する」「ポータルに表示する」です。

値一覧

最初の、フィールド内容をリストするのに最も適した機能は「値一覧」です。というか望んでいる結果はモロ値一覧です。値一覧に作ることで、リストは勝手に完成しているし、フィールド入力でポップアップやチェックボックスが選べるし、関数を使ってリストを取得しレコード化もできます。

ここの作者はすごく値一覧が好きで、リスト化する場合のデフォルトとして値一覧を使っております。

「フォルダ階層」だけ上手くいかない

そうやって、他のフィールドと同じように「フォルダ階層」を値一覧に作って、同じようなスクリプトでリストしポータルに表示しました。が、どうやってもちゃんとレコードが作れません。何度やってもどうテストしても、他のフィールドなら問題ないのに「フォルダ階層」だけはリストが不完全にしかつくれないんです。最初はフィールドに問題があるのかと思い、たとえば「/」を他の文字に置き換えてみたりもしました。全然だめです。リストが完成しません。

リストは、中途半端にしか作られないんです。途中で失敗したかのように、パスの途中までのリストしか作られません。いったいこれは何事でしょう。待てよ。中途半端?途中で失敗?パス?

中途半端・・・途中・・・パス・・・
中途半端・・・途中・・・パス・・・???

あっ。

あっ。あっ。これかっ。これなのか! と、閃いたのがこれでございました。

FileMakerで正しくリレーションできない照合の問題、原因と解決【重要】

値一覧100文字制限

どうやら値一覧の値にも100文字制限があるようです。だからパスの値が保持されなかったんですね。

このことを知らず、数年間にわたり苦しみ、フォルダ階層をリスト化するのを諦めていたのでした。

値一覧関数作ったろか

リスト化のデフォルト、値一覧が信頼できないと判ってショックな筆者ですが、これは値一覧と同じ機能をカスタム関数化するしかないなと決意を新たにしております。

フォルダ階層レコード化の最初の数ステップですでに実現できているので、フィールドのリスト化とユニーク化を組み合わせれば簡単に「シン・値一覧()」関数が作れます。もちろんこの作り方で100文字制限に引っかかることはありません。

 

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください