FileMakerでファイルを作るときに使い回す定番の小ネタ・小技があります。今回はソートボタンのスクリプト。
何がしたいかというと、リストのタイトル部分をクリックしたらソートされるようにしたいだけです。大概のリスト表示があるプログラムでよく見かけます。クリックでソートするだけでなくトグルで昇順降順が入れ替わります。
ポイントは二点あります。
- ソートを実行するスクリプト
- ボタンクリックごとに昇順・降順を切り替える仕組み
- 表示を司る仕組み
すいません。三点でした。ということでソートのスクリプトについてです。
ここではかつて二つの方法を記していました。
その1は汎用スクリプト1個 + 個別にソートのスクリプトを作りまくる方法、
その2はスクリプト一個(か2個)のみで済む代わりにフィールドを2個追加する二つの方法を記しました。
時を経て、決定打的な三つめの方法を追加しました。
その3は、偉人から知恵を授かったので試してみたらすこぶる良い調子だったんでこれで決め打ちかという方法です。フィールドを使います。
はっきり言ってその1は昔使っていて今は使っていない駄目な例です。失敗のひとつとして残していますがすっ飛ばして その2、または現時点で最良と思われる その3にお進みください。
その1 個別にソートスクリプトを作りまくる方法
その1は、汎用的なソートスクリプトを一個作り、さらに個別ソートのスクリプトを作りまくる方法です。以前この方法を好んで使っていましたが今は使わなくなりました。駄目な例の一つと言ってもいいでしょう。いちおう残していますが畳んでしまいます。いずれ消します。これはすっ飛ばしてください。
動作の流れ
流れというか仕組みというか、必要な手続きは以下です。
- 汎用的なソートスクリプトを一個作成します。
- 個別に、ソートのスクリプトを昇順降順セットでガシガシ作成します。
- 個別ソートのスクリプトの命名にルールを設け、汎用スクリプトが判断する仕組みです。
- レイアウト上でボタンを作って割り当てます。ボタンにスクリプト引数を与えます。
個別のソートスクリプト
個別にソートスクリプトを昇順降順2個セットで作ります。中身はコピペの書き換えで良いのですが、スクリプトの名前にルールがあります。ルールそのものが重要ってわけではなく、ルールに厳格なことが重要ってだけです。
個別ソートスクリプトの命名のルール
個別にせこせこと作るソートスクリプトです。スクリプト名の付け方に決まりを設けます。
ラベル+セパレータ+ソート順
ここではこうします。
ラベル
ソートスクリプトを作るときに付けたくなる普通の名前です。これが「ラベル」部分です。ラベルにルールはありません。ただし、次の「セパレータ」を含むことだけは避けます。
セパレータ
セパレータなしでもいいんですがここではセパレータがあるということで話を進めます。セパレータは「 – 」「 : 」「 _ 」「 | 」などです。ラベルに含まれる可能性がある文字は避けます。何にせよ、セパレータを決めてそれを守り抜きます。
ソート順
個別ソートスクリプトは二個セットで作ります。昇順と降順の二つです。命名の最後はソート順がどっちなのかを示す言葉をくっつけます。昇順、降順でもいいしASC、DESCでも↑↓でもいいでしょう。それを決めて、必ず付けます。スクリプト作りまくりで鬱陶しいですが一個作ればコピペ改編で済むのでさほどの手間ではないです。
てなわけで個別スクリプトの命名ルールでした。例えば こんな感じです。
ボタンにスクリプト引数
レイアウト上でボタンにソートスクリプトを割り当てるとき、スクリプト引数をセットします。そこには「ラベル」を書書きます。上図のIDなら「ID」、ファイル名なら「ファイル名」です。スクリプト名の、セパレータより前の部分をセットします。
個別ソートスクリプト作成のルール
スクリプトの中身はスクリプトステップが2行。簡単です。
1行目はスクリプト名に相応しいソートを「レコードのソート」スクリプトステップのダイアログで作って保存します。
2行目はグローバル変数をセットします。ここでは「$$sort」という名前にしておきましょう。$$sort に値をセットしますが、書き方にまたしてもルールがあります。ルールというか、これは決め打ちなので個別に改編もせずこのまま全部同じです。
変数を設定 [ $$sort = Get ( スクリプト名 ) ] です。ソートしたらスクリプト名がそのまま入るだけです。スクリプト名とは?「ラベル – ソート順」ですね。そのままグローバル変数になって、現在のソート状況が保存されるという案配です。
「ファイル名-ASC」という名前のスクリプトを実行させると、ファイル名で昇順ソートして、グローバル変数$$sortに「ファイル名-ASC」と記録します。これが個別ソートスクリプトの全貌です。
ソートボタンスクリプト
さて肝心のソートスクリプト行きます。ソートボタンスクリプトはすべてのソートボタンにセットする汎用的なソートスクリプトです。このスクリプトに書くステップを順に書きます。
まず変数を設定します。
変数を設定 [ $sep ; 値: "-" ]
命名ルールで定めたセパレータを変数に保存しました。念のための措置。
変数を設定 [ $label ; Get(スクリプト引数) ]
次にスクリプト引数を取得しました。ルール上、それは必ずラベルになっているはずですね。
次は条件分岐です。グローバル変数 $$sort の値の中に、ラベルがあるかどうかを調べて処理を分けます。
if ( PatternCount ( $$sort ; $label ) ) #あるとき〜 else #ないとき〜 end if
$$sort に自分と同じラベルが含まれていれば、次はソート順を見ます。
あるとき〜
$$sortには必ず ASC か DESC が入っているので、ASCなら逆であるDESCスクリプトを、DESC なら逆であるASCスクリプトを発動させます。
if [ patternCount ( $$sort ; "ASC" ) ] スクリプト実行 [名前で ; $label & $sep & "DESC" ] else スクリプト実行 [名前で ; $label & $sep & "ASC" ] end if
個別ソートのスクリプトを名前で実行することで、全てのソートボタン対応の汎用性を手に入れています。
ないとき〜
$$sort に自分と同じラベルがないときは未ソートであるからして、ASCかDESCのどちらか好きなほうをデフォルトとして発動させます。
ということで汎用ソートスクリプトは以上です。一応まとめときますか。
if ( PatternCount ( $$sort ; $label ) ) #あるとき〜 if [ patternCount ( $$sort ; "ASC" ) ] スクリプト実行 [名前で ; $label & $sep & "DESC" ] else スクリプト実行 [名前で ; $label & $sep & "ASC" ] end if else #ないとき〜 スクリプト実行 [名前で ; $label & $sep & "ASC" ] end if
たったこれだけのシンプルな話でした。
レイアウトでボタンを使う
ソートボタンスクリプトは書いた。はい。個別ソートのスクリプトも作った。はい。あとはレイアウトで実装するだけ。ボタンにソートボタンスクリプトの実行をセットして引数を書きます。
リスト的な表示(リストやポータル)のヘッダにタイトルのラベルを置きたくなります。このラベルをボタンにするんです。
ボタンバー
ボタンバーを使うと計算でボタンラベルを変更できるから昇順降順を示せます($$sortに応じてテキストを変える)
ボタンラベルの計算式はこんな感じです。
Let ( [ ラベル = "作成日" ; label = If ( PatternCount ( $$sort ; "ASC" ) and PatternCount ( $$sort ; ラベル ) ; ラベル & " ▲" ; If ( PatternCount ( $$sort ; "DESC" ) and PatternCount ( $$sort ; ラベル ) ; ラベル & " ▼"; ラベル & " " ) ) ]; label )
最初の「ラベル = “”」にボタンスクリプトの Label を指定するだけです。
表示上の項目名とラベル(スクリプト名のラベル)を分けたいときは、表示用ラベルを追加しておくとより良いですね。その場合こうなりますか。ラベルと表示ラベルだけ、ボタンごとに書き換えます。
Let ( [ ラベル = "作成日" ; 表示ラベル = "日付" ; label = If ( PatternCount ( $$sort ; "ASC" ) and PatternCount ( $$sort ; ラベル ) ; 表示ラベル & " ▲" ; If ( PatternCount ( $$sort ; "DESC" ) and PatternCount ( $$sort ; ラベル ) ; ラベル & " ▼"; 表示ラベル & " " ) ) ]; label )
その2 専用フィールドを作成する方法
その2のやり方は、個別ソートのスクリプトを作りまくったりしません。スクリプトは1個または2個で済みます。
こっちのやりかたはファイルメーカーのヘルプにも載っているやり方で、専用フィールドを作成して利用する方法です。
ヘルプに載ってるまともで簡素なやり方ですが、そのままではちょっと実用に耐えないので、少し工夫が必要な部分もあります。また、大きな弱点もあります。
追記: その3の方法を追記しました。今となってはその2の方法も面倒ですので、最良の方法をお探しの方はすっ飛ばしてください。 その3へ ↓
材料
グローバルフィールドを一つ、計算フィールドを一つ追加します。
例としてグローバルフィールドを「GLBfieldName」、計算フィールドを「sortContent」と名付けてみます。
あとはソートのスクリプトが一つあれば良いです。実際の運用では、このソートスクリプトを発動させるための「ソートボタンのスクリプト」をもう一つ追加すれば良いでしょう。
理屈
グローバルフィールド GLBfieldName は、ソートする「フィールド名」を記入する入れ物です。ここにフィールド名を入れることで「おいお前、このフィールド名の内容でソートするんだぞ」と命じることになります。
計算フィールド sortContent は「あいよ、おまいさん。こうかい?」と、グローバルフィールドで指定したフィールド名の「フィールド内容」をそっくりそのまま転記してクローンと化します。
例えばグローバルフィールドに「氏名フィールド」と書いたら、計算フィールドは実際の「氏名フィールド」の内容をレコード分転記して「氏名フォールド」と同じになります。
スクリプトは、常に計算フィールド sortContent でソートするように仕込みます。個別にソートスクリプトなど作る必要ありません。sortContentのソート指定一個ですみます。sortContent の中身がその都度変わるのだという仕組みです。
計算フィールドの計算式
理屈の通り、sortContent フィールドにフィールド内容を計算して転記するわけですが、基本は FileMaker のヘルプに載っているとおり、GetField ( GLBfieldName ) の一行でOKです。
この簡単な1行でフィールド内容は転記されますが、このままではソートに問題があって駄目ですので改良します。どういう問題か、どういう改良か、それは以下です。
結果のタイプ
計算式では、計算結果のタイプを「テキスト」や「数字」など事前に指定しなければなりません。
でも実際にソートしたいフィールドの内容はテキストだったり数字だったりします。
計算フィールドの計算式の結果を「テキスト」にすると、数字のソートが上手くいきません。1 から 10 の数字だとすれば、1の次に10が来てその次に 2 が来てしまいます。 結果を 「数字」にすると、当然ながらテキストが返りません。
FieldType
取りあえず計算結果は「テキスト」にしておきます。その上で、ソートフィールドが数字や日付の場合に備えましょう。
備えるのは良いのですが、その前にソートする指定フィールドのタイプをまず調査しなければなりません。大丈夫。FieldType という関数があります。
FieldType ( ファイル名 ; フィールド名)
これを使うと指定フィールドの情報がスペース区切りのテキストで返ってきます。スペースを¶に置換して GetValue すると2行目がフィールドタイプです。ですので具体的に次のように書きます。
GetValue ( Substitute ( FieldType ( get(ファイル名) ; GLBfieldName ) ; " " ; ¶ ) ; 2 )
結果は「text」「Number」「Date」「Timestamp」「Container」といった文字列になります(Container はオブジェクト)
タイプが Number だったら、無理矢理テキストとしてソートできる形にします。つまり 0 を追加して桁数を増やします。
Right ( "0000000" & フィールド内容 ; 8 )
例えばこんな風に8桁にしてしまえば、テキストフィールドとしてソートしても期待通りの結果となるでしょう。桁数はご自由に。
日付フィールドの場合は、各々お好きに工夫します。私の場合は書式を揃えているのでそのままテキストとしてソートしても問題ありません。秒でズレますが細けえことは気にしません。気にする場合は、タイムスタンプでもテキストとして桁数が揃うように整形します。
計算フィールドはその都度本物フィールドから転記してくるだけのクローンフィールドですから、ソートしやすいように整形しまくっても大丈夫、本物データに何の影響もありません。
Let 関数
ということで、sortContent フィールドの計算式は具体的こうなりました。太字のGLBfieldName はグローバルフィールドです。
Let ( [ content = GetField ( GLBfieldName ) ; fieldtype = GetValue ( Substitute ( FieldType ( get(ファイル名) ; GLBfieldName ) ; " " ; ¶ ) ; 2 ) ; content = case ( fieldtype = "number" ; Right ( "0000000" & content ; 8 ) ; content ) ]; content )
GLBfieldName にフィールド名を記入することで、sortContent にはソートできる形でフィールド内容が転記されます。あとはスクリプトでソートするだけです。
ソートするスクリプト
ソートスクリプトは、計算フィールド sortContent フィールドをソートさせれば良いだけです。
ダイアログでsortContent フィールドを指定します。
しかしこれだけでは貧弱ですね。状態把握のためのグローバル変数の作成をを交えつつ、次のようなスクリプトを作成しました。
ソートスクリプトの動き
まず、$$sort を読んで一行目のフィールド名がグローバルフィールドに書かれた指定フィールドと同じかどうか調べます。つまり現在のソートと、今からソートしようとしているソートが同じフィールドなのか、違うのかを最初に見ます。
同じなら、現在昇順なのか降順なのかを調べ、現在と逆の行動を起こします。現在が昇順ならば降順、降順ならば昇順でソートします。
ソートを実行するとグローバル変数 $$sort を更新し「フィールド名 ¶ ソート順」を記録します。
$$sort のフィールドと今からソートしようとしてるフィールドが別であるなら、新たに指定フィールドでソートすることになります。デフォルトのソートです。デフォルトを昇順・降順、どちらにセットしておくかはお好み次第です。この例ではデフォルトを降順としています。
そしてソートしたあとは $$sort を更新、「フィールド名 ¶ ソート順」 を記録します。
ソートボタンのスクリプト
GLBfieldName にフィールド名を記入すればそのフィールドでソートされるようになりました。
実際の運用ではフィールド名を手入力しません。してもいいけど。普通しません。大抵はボタンに仕込みたいです。GLBfieldName にフィールド名を記入し、ソートスクリプトを実行するスクリプトを作りましょう。
ソートボタンに割り当てるスクリプトを追加しました。
ソートボタンにセットするスクリプト引数
ボタンの設定で、スクリプト引数にフィールド名をセットします。フィールド名は ” ” で囲みます。ここで書いたスクリプト引数が、グローバルフィールドにそのまま入る仕組みです。”” で囲まないと、そのフィールド名のフィールド内容が書かれてしまいます。フィールド名としてセットしたいので必ず括ります。あとはソートスクリプトを実行させ、最後にレコードの先頭に移動させています。
ソートするスクリプトとボタンにセットするスクリプトをこのように分けることで、他の処理と併用しても面倒なことになりませんのでおすすめです。
普通のボタンじゃなくボタンバーを使うことによってボタンタイトルを計算式で作れます。これは半ば必須かもしれません。ソート順を表示できますから。
ボタンバーのラベル
ボタンバーでは計算式でラベルを作れますので重宝します。例として、次のようなラベルの計算式を挙げておきます。
Let ( [ fieldName = "ファイル名" ; label = "ファイル名" ; asc = TextSize ( TextFont ( " ▵" ; "Apple Symbols" ) ; 11 ) ; desc = TextSize ( TextFont ( " ▿" ; "Apple Symbols" ) ; 11 ) ; none = TextSize ( TextFont ( " " ; "Apple Symbols" ) ; 11 ); GLBfld = GetValue ( Substitute ( $$sort ; ": " ; ¶ ) ; 1 ) ; GLBsort = GetValue ( Substitute ( $$sort ; ": " ; ¶ ) ; 2 ) ; sort = Case ( GLBfld ≠ fieldName ; none; GLBsort = "ASC" ; asc ; GLBsort = "DESC" ; desc ); disp = If ( GLBfld = fieldName ; label & sort ; label & none ) ]; disp )
最初の fieldName は、ボタンがターゲットとするフィールド名、スクリプト引数と同じです。
次の label は、表示させるテキスト。
asc, desc, none はテキストの後ろに付ける矢印や三角。この例では小さな三角を指定しています。小さな三角はフォントやサイズの指定をしないと表示サイズが定まらなかったので指定しています。
あとは 現在の $$sort(1行明がフィールド名、2行目が昇順・降順 )を読んで、case で表示を決定しています。
FileMakerのヘルプにも載っているやり方でした。この方法はソートの度に大量の転記が発生しますからレコードが多いとちょっとエグいです。
その3 フィールドそのものを使う方法
しれっと追記しております。その3の方法はすごいです。こんなこと思いつきもしませんでしたが、FileMaker の講演ビデオを見て知りました。早速取り入れます。
参考になったビデオ→ 効率的な カスタム App開発の基本手法を考察する – 2023年版
是非上記ビデオをご覧ください。以下、参考ビデオを自分なりに飲み込んで作った例にすぎません。ただし、参考ビデオでは触れられていない重要なポイントがひとつあります。その件にも触れておきます。
材料
- ソートボタン … ソートしたいフィールド
- スクリプト … 汎用的なスクリプト 1個
- グローバル変数 … ソート状態を保存するグローバル変数
- ボタンバー … 昇順降順を表示するためだけのボタンバー
概要
ソートボタンの代わりにソートしたいフィールドそのものを使います。ボタン化もしません。オブジェクトトリガ OnObjectEnter をセットし、ソートのスクリプトを走らせます。
ソートスクリプトは、スクリプトステップ「レコードをフィールド順でソート」が基本です。あとは昇順降順を使い分けるため、グローバル変数を作ったり解釈したりします。
それ以外には表示用に昇順降順を表示するボタンバーを作り、それを隠すためのフィールドを配置して見た目を整えます。
詳細
ボタンのグループはこんな仕上げになります。
分解するとこういうものが配置されています。
では上から順に説明します。
テキスト(ラベル)
単なるテキストです。ラベルとして使用します。
ソートするフィールド 目隠し用
ソートしたいフィールドと同じフィールドです。すぐ下のボタンバーを隠したり見せたりするためだけにここに置いています。
グローバル変数に、現在ソートしているフィールド名とオーダー(昇順か降順)を記録しており、そのフィールド名と己が同じならこのフィールドの背景を透明にします。
↑↓を表示するためのボタンバー
何もしないボタンバーを配置して名前だけ計算式で作っています。グローバル変数 $$_order にASCと記録されていれば ↑ 、DESC と記録されていれば ↓と表示します。矢印より三角とか別のが判りやすいのかな。いずれにしても、アイコンではなくテキストが望ましいです。名前欄ですから。
ソートするフィールド
これが主人公、ソートしたいフィールドです。これをボタン代わりに使いますが、ボタン化もしません。フィールドのままです。
ただしボタンみたいなデザインにはします。例えばフォントサイズを1にして色を背景色に合わせフィールドの外に追い出し、スクロールをオンにします。このあたりは参考ビデオでおっしゃっている通りの措置です。それに加えて押したときの背景色なんかを整えます。
押したときに凹んだように見える背景色をセットするわけですが、色を黒にして不透明度10%とか、そういう配色をするのがよろしいです。不透明な色をセットすると、押したときに上に配置したラベルテキストが消えたように見えます。
さて、このフィールドに、OnObjectEnterトリガをセットします。OnObjectEnterですから、クリックした途端にスクリプトが発動し、フィールド内容に関与しません。
ソートスクリプトとグローバル変数
ソートのスクリプトは、スクリプトステップ「レコードをフィールド順でソート」を使用します。
「レコードをフィールド順でソート」は、指定したフィールドでソートするステップです。フィールドを指定しない場合はアクティブフィールドが採用されるということです。そうだったんですか。
フィールドをボタン化せずにボタンのように使用することをこれまで思いつかなかったので、「レコードをフィールド順でソート」を使えない奴。と思ってました(以前これをやろうとしてフィールドをボタン化したら動かなかった)
このステップでは昇順または降順をセットするので、分岐して配置します。現在昇順ソートされていれば降順に、降順ソートされていれば昇順にソートします。
if [ 現在のソート = "ASC" ] レコードをフィールド順でソート[ "降順" ] else レコードをフィールド順でソート[ "昇順" ] end if
単純に書くとこんな感じです。で「現在のソート」が必要なのでグローバル変数 $$_order を想定して記録しましょう。
if [ $$_order = "ASC" ] レコードをフィールド順でソート[ "降順" ] 変数を設定 [ $$_order ; "DESC" ] else レコードをフィールド順でソート[ "昇順" ] 変数を設定 [ $$_order ; "ASC" ] end if
単純化して書くとこんな感じです。
これだけでは足りないので、現在ソート対象となっているフィールド名も記録しなければなりません。ソートしようとしたとき「現在ソートされている対象と同じなら昇順降順の入れ替え、そうでないなら改めてソート」の動きにしたいからです。
厳密にやりだすと、過去のソートを逐一記録して、対象フィールドが変わる度に「以前このフィールドでソートしたときのオーダーはこうであったので…」とか、非常に面倒なことになってくるのでそこまで踏み込みません。
ということでソートスクリプトの最初に、フィールドを記します。
変数を設定 [ $$_orderBy ; 値: Get ( アクティブフィールド名 ) ] if [ $$_order = "ASC" ] レコードをフィールド順でソート[ "降順" ] 変数を設定 [ $$_order ; "DESC" ] else レコードをフィールド順でソート[ "昇順" ] 変数を設定 [ $$_order ; "ASC" ] end if
こんな感じでどうでしょう。
見た目
さて見た目を整えます。さっきの絵をもう一度貼っておきましょう。
最下のフィールドをボタンみたいにデザインしたあとは、そのすぐ上にボタンバーを設置して矢印を表示させます。
ボタンバーは名前に計算式を使えるので、ラベル代わりにもよく使ってます。
名前欄の計算式は次のようにしました。
If ( $$_order = "ASC" ; "↑" ; If ( $$_order = "DESC" ; "↓" ; "" ) )
$$_order が ASC なら ↑、DESC なら ↓ それ以外なら空欄です。矢印じゃなく別の文字のほうがカッコいいかもしれませんがとりあえず。
で、このボタンバーはすべてのソートボタンで同じように表示されてしまうので、現在のソート対象以外では隠す必要があります。
隠す方法は、フィールドをもう一項追加して上にかぶせてしまいます。背景色などを下のフィールドと合わせておき、条件付き書式で調整します。
フィールドの「隠す」では上手くないです。なぜかというと、グローバル変数の値に応じて即時に反映させる必要があります。フィールドの「隠す」は変数の対応がリアルタイムでなく、再表示を必要としますから却下。条件付き書式でやります。
条件付き書式の計算式がこれまた雑ですいません。フィールド名のところにフルフィールド名が入るのかフィールド名だけなのか確認するのが面倒だったので PtternCount を使いました。
PatternCount ( GetFieldName ( Self ) ; $$__orderby )
ここで Self を使っているのがミソですね。このために、ソートさせるフィールドと同じフィールドを使いました。でないとフィールド名を指定しなければなりませんから。
(図ではJSONの文字も見えますがグローバル変数をどうするかって話でして、ここでは詳しく言いません。どうでもいい話です)
この計算式には若干の罠があります。GetFieldName には、関連テーブルのフィールドで正常に動かない可能性があります。個人的にはこれを正した GetFieldNameEx というカスタム関数を使っていますがここでは割愛します。もし関連テーブルのソートをする場合はGetFieldName ではない方法を模索するのがよいかもしれません。
ということで、このフィールドで現在のソート対象以外のフィールドではボタンバーを隠します。
ソートボタンラベルが完成しました。必要なだけ複製し、フィールドをソートするフィールドに変更します。最上位のテキスト部分にラベルを書いたらできあがり。
以上、その3の画期的な方法でした。Claris のビデオ、希にめっちゃ目から鱗なことがあります。
重要ポイント:関連テーブルでレコードがないとき
さて重要なポイントを一つ指摘しておきます。
関連テーブルを接続している場合、そのテーブルのフィールドにレコードがない場合はソートできません。この方法の、唯一にして最大の欠点です。
関連テーブルでは、レコードがなければフィールドをクリックしてもフィールド名をゲットできないし OnObjectEnter トリガのスクリプトも発動しません。何もできないんですね。
これを回避する方法は、リレーションシップでレコード作成許可を与えるしかありません。
許可を与えていれば、空のフィールドもフィールドとして認識してくれます。
レコード作成の許可を与える与えないは結構重要な設定だと思いますので、許可を与えないテーブルに対して、ソートのためだけに設定を変更するリスクを取れるか取れないか、そこんところが重要な判断になります。
関連テーブルかつレコードがない可能性がありかつレコード作成の許可を与えたくない場合のみ、他の方法を使うほかありません。その2の方法や、諦めて単独ボタン・単独スクリプトを用意するとかですね。
ということで、2023年12月、その3を追記しました。