Pythonスキルの習得

【 Python 】かっこいいGUI wxPythonの使い方入門 その3 – レイアウト( Panel , Sizer )の紹介 –

【 Python 】かっこいいGUI wxPythonの使い方入門 その3 - レイアウト( Panel , Sizer )の紹介 -

本記事は、

  • Pythonのスキル、初心者以上の方

を対象にした内容になっております。


本記事を含む、一連の記事を読むことで、(全7回)

  • かっこよくて
  • おしゃれな

「簡単なGUIアプリ」を作成できるようになります。

例えば、wxPythonで、画像表示アプリを作ってみました。

上記みたいな、見た目(Macの場合)となります。

本記事は、下記の続編となります。

前回までの記事を読んでいなくても、内容が理解できるよう、構成しています。

【 Python 】 かっこいいGUI wxPythonの使い方入門 その1
【 Python 】 かっこいいGUI wxPythonの使い方入門 その1Pythonの、かっこいいGUIである、wxPythonの使い方を紹介した記事です。...
【 Python 】 かっこいいGUI wxPythonの使い方入門 その2 - ウィジェットの紹介 -
【 Python 】 かっこいいGUI wxPythonの使い方入門 その2 - ウィジェットの紹介 -Pythonの、かっこいいGUIである、wxPythonの使い方を紹介した記事です。ウィジェットの紹介とコードによる配置方法を紹介しました。...

本記事のゴール設定

wxPythonの

  • 標準的な、GUI構造の理解と、
  • GUI部分のコードの書き方

を理解する事になっています。

wxPythonにおける、GUI構造

以下のGUI構造が、標準です。

この段階で、意味が分からなくても、大丈夫です。

順番に、解説していきます。

<GUI 構造>
  1. Frame」というトップウインドウを、まず作る
  2. その子供に、「Panel」と呼ばれる部品を配置する。
  3. その「Panel」のレイアウト部品として、「Sizer」と呼ばれる部品を設定。(そのSizerの中に、別のSizerの設置も可能)
  4. 「Sizer」に、ボタン等のウィジェット(コントロール部品)を追加する。

という構造をとります。

厳密な親子関係は以下の通りです。

(よく分からない場合、本記事を最後まで読んだ後、ご覧になると理解できると思います。)

「Panel」の概念把握と、コード紹介

Panelとは

Panelは、その子供に設定された「コントロール部品」間をタブで移動できる、つまり部品をグループにまとめる役割があります。

次回以降の解説となりますが、ボタンを押した時の挙動等(イベント処理)にも、影響があります。

Panelの解説(英語版ですいません。日本語の解説が見つかりませんでした。)

wx.Panel widgets enable tabbing between Widgets on Windows. So if you want to be able to tab through the widgets in a form you have created, you are required to have a panel as their parent.

解説本「Creating GUI Applications with wxPython」 P10

また、Frameの子供に設定されたGUI部品は、ウインドウ全体に広げられます。

前回の記事では、Frame直下にウィジェットを設置すると、画面いっぱいに広がっていました。

例えば、コンボボックスを設置して、「▼をクリック」した状態が、下記画面になります。

ここに、「Frame」の子供に「Panel」を設置し、その「Panel」の子供に「コンボボックス」を設置した例を見てみます。

コンボボックスの直下に選択肢が現れ、期待した通りの見た目になっています。

(参考)前回紹介した「ラジオボタン」の場合、以下のようになります。

Panel のコード

以下のコードが、wxPythonの、基本的なコード(GUI部分)になります。

  1. wx.Panelを継承した、Panelクラスを作成
  2. wx.Frameを継承した、Frameクラスを作成
  3. Frameクラスから、Panelクラスをインスタンス化。

(下記コード中の、クラス名は、自由に変更して下さい。 また、見やすさ優先で、PEP8を無視した記載になっています。 ご了承ください。)

import wx

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(self, parent)

        ######################################
        #  ここにSizer, ボタン等のウィジェットを記載
        #  (本記事にて、後ほど、解説)
        ######################################

    ######################################################
    #  ここにボタンを押下等のイベント処理を、メソッドで記載することが多い
    #  (別記事にて、解説します)
    ######################################################

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, id=-1, title='wxPython')
        panel = MyPanel(self)

        ########################################################
        #  必要ならば、メニューバー、ステータスバー、ツールバーを作成する
        #  (別記事にて、解説します)
        ########################################################

        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

「Sizer」の概念把握

「Sizer」とは

前回の記事で紹介した、下記問題が解決できる、レイアウト部品です。

  • 各ウィジェットが重なって表示される
  • ウインドウサイズを大きくした場合(マウスのドラッグ等にて)、画面が再レイアウトしてくれない

Sizerを使うと、

  • 各ウィジェットが、重ならないようになる
  • ウインドウサイズを変更した時、再レイアウトしてくれる

ようになります。

Sizerにも、さまざまな種類があります。

次の章で、ご紹介します。

「Sizer」の種類

Sizerには、下記の種類があります。

  • BoxSizer
  • StaticBoxSizer
  • GridSizer
  • FlexGridSizer
  • WrapSizer
  • GridBugSizer(本記事では、省略。)

順番に、各特徴を見ていきます。

BoxSizer

ウインドウの中に、

  • ウィジェットを、長方形状に、並べていく

レイアウト部品です。

<イメージ絵>

下絵は、BoxSizerの中に、ウィジェットを3つ設置し、縦方向に重ねたものです。

ウインドウ(Frame)が大きくなると、

  • 「ウィジェット」も比例して、大きくなっています。
<上記イメージ図の、GUI構造>

BoxSizerの特徴:

  • ウインドウ全体に渡って、ウィジェットを縦に並べる。
  • ウィジェットを横に並べる事も可能。
  • 下絵の通り、BoxSizerの中に設置したウィジェットは、ウインドウの大きさに合わせて、サイズ比を保ったまま、大きさが変わる。(大きさを変えない設定も可能)

StaticBoxSizer

既に紹介した、BoxSizserに

  • 外枠をつけて、タイトル名を表示する事ができる

レイアウト部品です。

下記の、イメージ図を見て頂いた方が、理解が早いと思います。

<イメージ絵>
<上記イメージ図の、GUI構造>

StaticBoxSizerの特徴:

  • ウイジェットを視覚的に、グループ化する時に使用。
  • 他の特徴は、BoxSizerと同じです。

GridSizer

ウインドウの中に、

  • ウィジェットを、格子状(グリッド状)に並べていく

レイアウト部品です。

<イメージ絵>

下絵は、

  • 縦2 ✖️ 横2で設定したGridSizerを使用して、
  • ウィジェットを4つ、設置したイメージになります。
<上記イメージ図の、GUI構造>

GridSizerの特徴:

  • 縦、横の分割数を、任意で設定できる
  • ウインドウ拡大・縮小時の挙動は、BoxSizerの解説と同じ

FlexGridSizer

既に紹介したGridSizerは

  • 全グリッドのサイズが同じ

であるのに対し

  • 設定した行、もしくは列のみ(もしくは両方)、サイズ変更できる

レイアウト部品です。

<イメージ図>

2列目のみ、変更可能に設定した場合の、挙動です。

<上記イメージ図の、GUI構造>

FlexGridSizerの特徴:

  • ウインドウサイズ変更時に、サイズを変更したい行、列が設定できる。(正確には、「○行目、△列目を可変にする」と設定する。)
  • 他は、GridSizerの特徴と同じです。

WrapSizer

ウインドウに

  • スペースがある限り、ウィジェットを一列に並べる
  • スペースがなくなったら、ウィジェットの並べ替えをする

という、レイアウト部品です。

下記イメージ図を見て頂いた方が、理解が早いと思います。

<イメージ図>
<上記イメージ図の、GUI構造>

WrapSizerの特徴:

  • HTML5でいう、「レスポンシブWEBデザイン」みたいな挙動をする。(ウィンドウサイズにより、再レイアウトされる。)
  • 他の機能は、他Sizerと同じです。

各「Sizer」のコード紹介

紹介する順番は以下の通りです。(前章と同じ順番です。)

  • BoxSizer
  • StaticBoxSizer
  • GridSizer
  • FlexGridSizer
  • WrapSizer

Sizerを使ったコード手順(全Sizer共通)

  1. 「ウィジェット」を作る(ボタン、テキスト等)
  2. 「Sizer」を作る
  3. 作った「Sizer」に、作った「ウィジェット」をAddしていく
  4. Panelの規定Sizerに、作った「Sizer」を指定する

上記の「作る」という表現は、「インスタンス化」を意味しています。

以下の章では、

  • 既に紹介した下記コードの、7行目〜10行目該当するコード

順番に、ご紹介します。

import wx

class MyPanel(wx.Panel):
    def __init__(self, parent):
        super().__init__(self, parent)

        ######################################
        #  ここにSizer, ボタン等のウィジェットを記載
        #  (本記事にて、解説)
        ######################################

    ######################################################
    #  ここにボタンを押下等のイベント処理を、メソッドで記載することが多い
    #  (別途、解説します)
    ######################################################

class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(None, id=-1, title='wxPython')
        panel = MyPanel(self)

        ########################################################
        #  必要ならば、メニューバー、ステータスバー、ツールバーを作成する
        #  (別途、解説します)
        ########################################################

        self.Show()

if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

BoxSizer のコード

下記構造の、コードをご紹介します。

BoxSizerは、wx.BoxSizer( )にて指定します。

引数として

  • wx.VERTICAL: ウィジェットを縦に並べる
  • wx.HORIZONTAL: ウィジェットを横に並べる

を指定します。

コードは、わかりやすいように、パラメータ引数を使って記載しています。

このBoxSizerに、

  • Add( )でウィジェットを追加していき、
  • 最後に、Panelの規定Sizerに、作ったSizerを指定します。
        # ウィジェットを作る
        text1 = wx.TextCtrl(self, id=-1, value='aa')
        # 分かり易さのため、背景を青に設定
        text1.SetBackgroundColour('blue')
        # ウィジェットを作る
        text2 = wx.TextCtrl(self, id=-1, value='aa')
        # 色の指定は、下記方法でもOK
        text2.SetBackgroundColour('#ff00ff')

        # Sizerを作る
        sizer = wx.BoxSizer(orient=wx.VERTICAL) # 縦に並べる
        # 各ウィジェットをSizerにAdd していく
        sizer.Add(text1, 1) 
        sizer.Add(text2, 1)
        # Panleの規定Sizerに指定
        self.SetSizer(sizer) 

<実行結果>

◆ wx.BoxSizer(wx.HORIZONTAL) を指定した場合

各ウィジェットの、サイズ比を変えたい

上記のコードは、ウィジェットのサイズ比が1:1でしたが、2:1に変えたコードがこちらです。

13行目と14行目の

  • sizer.Add(text, 1) の

第2引数の「1」がサイズを表しています。

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text1.SetBackgroundColour('blue')
        text2 = wx.TextCtrl(self, id=-1, value='aa')
        text2.SetBackgroundColour('#ff00ff')

        sizer = wx.BoxSizer(orient=wx.VERTICAL)
        # 大きさを 2 にする
        sizer.Add(text1, 2)
        # 大きさを 1 にする 
        sizer.Add(text2, 1)
        self.SetSizer(sizer)

これで、サイズ比が 2 : 1 で保たれます。

<実行結果>

StaticBoxSizer のコード

下記構造の、コードをご紹介します。

StaticBoxSizerは、wx.StaticBoxSizer( )にて指定します。

引数として

  • 第1引数: wx.VERTICAL or wx.HORIZONTAL
  • 第2引数: 親を指定
  • 第3引数: グループのタイトル

を指定します。

コードは、わかりやすいように、パラメータ引数を使って記載しています。

        button_1 = wx.Button(self, -1, 'ボタン1')
        button_2 = wx.Button(self, -1, 'ボタン2')
        button_3 = wx.Button(self, -1, 'ボタン3')

        sizer = wx.StaticBoxSizer(orient=wx.HORIZONTAL, parent=self, title='タイトル')
        # 上記1行は、下記、2行でもOK
        # nm = wx.StaticBox(self, -1, 'タイトル')
        # sizer = wx.StaticBoxSizer(box=nm, orient=wx.HORIZONTAL)

        sizer.Add(button_1)
        sizer.Add(button_2)
        sizer.Add(button_3)
        self.SetSizer(sizer)

<実行結果>

GridSizer のコード

下記構造の、コードをご紹介します。

GridSizerは、wx.GridSizer( )にて指定します。

引数として

  • 第1引数: 縦を何分割するか
  • 第2引数: 横を何分割するか
  • 第3引数: 各ウィジェットの隙間(タプルにて、ピクセル指定)

を指定します。

コードは、わかりやすいように、パラメータ引数を使って記載しています。

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text1.SetBackgroundColour('blue')
        text2 = wx.TextCtrl(self, id=-1, value='bb')
        text2.SetBackgroundColour('#ff00ff')
        text3 = wx.TextCtrl(self, id=-1, value='cc')
        text3.SetBackgroundColour('red')
        text4 = wx.TextCtrl(self, id=-1, value='dd')
        text4.SetBackgroundColour('pink')

        sizer = wx.GridSizer(rows=2, cols=2, gap=(0, 0))

        sizer.Add(text1)
        sizer.Add(text2)
        sizer.Add(text3)
        sizer.Add(text4)
        self.SetSizer(sizer)

<実行結果>

FlexGridSizer のコード

下記構造の、コードをご紹介します。

FlexGridSizerは、wx.FlexGridSizer( )にて指定します。

引数にの指定は、GridSizerと同じです。

  • 第1引数: 縦を何分割するか
  • 第2引数: 横を何分割するか
  • 第3引数: 各ウィジェットの隙間(タプルにて、ピクセル指定)

コードは、わかりやすいように、パラメータ引数を使って記載しています。

見た目優先で、本コード(Sizer.Add( ) のメソッド)の引数に、wx.EXPAND を指定しています。

詳細は、後ほど、解説しています。

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text2 = wx.TextCtrl(self, id=-1, value='bb')
        text3 = wx.TextCtrl(self, id=-1, value='cc')
        text4 = wx.TextCtrl(self, id=-1, value='dd')
        text5 = wx.TextCtrl(self, id=-1, value='ee')
        text6 = wx.TextCtrl(self, id=-1, value='rr')

        sizer = wx.FlexGridSizer(rows=3, cols=2, gap=(10, 10))

        sizer.Add(text1, 1, wx.EXPAND)
        sizer.Add(text2, 1, wx.EXPAND)
        sizer.Add(text3, 1, wx.EXPAND)
        sizer.Add(text4, 1, wx.EXPAND)
        sizer.Add(text5, 1, wx.EXPAND)
        sizer.Add(text6, 1, wx.EXPAND)
        # サイズ変更できる行を指定 (0から開始)
        sizer.AddGrowableRow(0) # 1行目
        # サイズ変更できる列を指定 (0から開始)
        sizer.AddGrowableCol(1) # 2列目

        self.SetSizer(sizer)

<実行結果>

WrapGridSizer のコード

下記構造の、コードをご紹介します。

WrapSizerは、wx.WrapSizer( )にて指定します。

引数として

  • 第1引数: wx.VERTICAL → ウィジェットを縦に並べるのを優先。wx.HORIZONTAL → ウィジェットを横に並べるのを優先。
  • 第2引数: ウィジェット配置時に、スペースを残すかどうか
    wx.WRAPSIZER_DEFAULT_FLAGS → 残す
    wx.REMOVE_LEADING_SPACES → 残さない

を指定します。

第2引数の挙動ですが、文章だけでは、よく分からないと思うので、実行結果をご参照下さい

コードは、わかりやすいように、パラメータ引数を使って記載しています。

見た目優先で、本コード(Sizer.Add( ) のメソッド)の引数に、wx.EXPAND を指定しています。

詳細は、後ほど、解説しています。

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text2 = wx.TextCtrl(self, id=-1, value='bb')
        text3 = wx.TextCtrl(self, id=-1, value='cc')
        text4 = wx.TextCtrl(self, id=-1, value='dd')
        text5 = wx.TextCtrl(self, id=-1, value='ee')
        text6 = wx.TextCtrl(self, id=-1, value='rr')

        sizer = wx.WrapSizer(orient=wx.HORIZONTAL, flags=wx.WRAPSIZER_DEFAULT_FLAGS)

        sizer.Add(text1, 1, wx.EXPAND)
        sizer.Add(text2, 1, wx.EXPAND)
        sizer.Add(text3, 1, wx.EXPAND)
        sizer.Add(text4, 1, wx.EXPAND)
        sizer.Add(text5, 1, wx.EXPAND)
        sizer.Add(text6, 1, wx.EXPAND)

        self.SetSizer(sizer)

<実行結果>

◆ wx.WrapSizerのflags引数:wx.WRAPSIZER_DEFAULT_FLAGS の場合

テキストコントロールとウインドウ枠との間に、空白ができています

◆ wx.WrapSizerのflags引数:wx.REMOVE_LEADING_SPACES の場合

テキストコントロールとウインドウ枠との間の、空白が埋まっています

各Sizerの詳細設定

ここでは、BoxSizer を例として、ご紹介します。

ただし、全てのSizerに、応用できる内容になっています。

sizer.Add( )の引数 詳細解説

sizer.Add( )の引数、詳細に見てみます。

  • 第1引数:Sizerに追加するウィジェット(コントロール)を指定
  • 第2引数:proportionというキーワード引数名になります。コントロールの大きさを指定。0 の場合、最小サイズが設定されます。
  • 第3引数:flagというキーワード引数名になります。詳細は、この後の記事を参考にして下さい。
  • 第4引数:borderというキーワード引数名になります。詳細は、この後の記事を参考にして下さい。

キーワード引数を省略せずに記載すると

  • sizer.Add(<ウィジェット名>, proportion=<XXX>, flag=<XXX>, border=<XXX>) になります。 (※ 例 sizer.Add(text, proportion=1, flag=wx.LEFT, border=1)

になります。

各ウィジェットを左寄せ、中央寄せ、右寄せにしたい

sizer.Add( )に第3引数を追加します。

wx.BoxSizer(VERTICAL)の場合、指定できるもの:

  • 左寄せ → sizer.Add(text, 1, wx.ALIGN_LEFT)
  • 中央寄せ → sizer.Add(text, 1, wx.ALIGN_CENTER)
  • 右寄せ → sizer.Add(text, 1, wx.ALIGN_LEFT)

wx.BoxSizer(HORIZONTAL)の場合、指定できるもの:

  • 上寄せ → sizer.Add(text, 1, wx.ALIGN_TOP)
  • 中央寄せ → sizer.Add(text, 1, wx.ALIGN_CENTER)
  • 下寄せ → sizer.Add(text, 1, wx.ALIGN_BOTTOM)

(上記メソッドの、第1、第2引数は、仮で表記しています。)

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text1.SetBackgroundColour('blue')
        text2 = wx.TextCtrl(self, id=-1, value='aa')
        text2.SetBackgroundColour('#ff00ff')

        sizer = wx.BoxSizer(wx.VERTICAL)
        # 中央寄せ
        sizer.Add(text1, 2, wx.ALIGN_CENTER)
        # 右寄せ
        sizer.Add(text2, 1, wx.ALIGN_RIGHT)
        self.SetSizer(sizer)

<実行結果>

各ウィジェットで、ウインドウとの空白を埋めたい

sizer.Add( )に第3引数を追加します。

  • sizer.Add(text, 1, wx.EXPAND)

(上記メソッドの、第1、第2引数は、仮で表記しています。)

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text1.SetBackgroundColour('blue')
        text2 = wx.TextCtrl(self, id=-1, value='aa')
        text2.SetBackgroundColour('#ff00ff')

        sizer = wx.BoxSizer(wx.VERTICAL) 
        # wx.EXPAND を追加
        sizer.Add(text1, 2, wx.EXPAND)
        sizer.Add(text2, 1, wx.EXPAND)
        self.SetSizer(sizer)

<実行結果>

◆ (参考)GridSizerの場合

指定したサイズで、マージンを作りたい

ウィジェットの境界に、指定したサイズで、余白を作りたい場合です。

sizer.Add( )の、第3、第4引数で設定します。

下記メソッドの場合、上下左右に、10pxの余白が作られます。

  • sizer.Add(text, 1, wx.ALL, 10)

上下左右、単独で設定したい場合は、以下の通りです。

  • 上に余白 → sizer.Add(text, 1, wx.TOP, 10)
  • 下に余白 → sizer.Add(text, 1, wx.BOTTOM, 10)
  • 左に余白 → sizer.Add(text, 1, wx.LEFT, 10)
  • 右に余白 → sizer.Add(text, 1, wx.RIGHT, 10)

上記を組み合わせる事もできます。( | を使用します。複数使用OK)

  • 上と左と右のみ → sizer.Add(text, 1, wx.TOP | wx.LEFT | wx.RIGHT, 10)

(上記メソッドの、第1、第2引数は、仮で表記しています。)

        text1 = wx.TextCtrl(self, id=-1, value='aa')
        text1.SetBackgroundColour('blue')
        text2 = wx.TextCtrl(self, id=-1, value='aa')
        text2.SetBackgroundColour('#ff00ff')

        sizer = wx.BoxSizer(wx.VERTICAL) 
        # wx.EXPAND と 同時に使用
        # 上下左右に10px、余白
        sizer.Add(text1, 2, wx.EXPAND | wx.ALL, 10)
        # 上、左右に10px、余白
        sizer.Add(text2, 1, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, 10)
        self.SetSizer(sizer)

<実行結果>

ウィジェットとウインドウとの間に、10pxの空白ができています。

(text1とtext2の間は、合計20pxの空白が、発生。)

番外編 Sizerの中にSizerを入れる

Sizerの中に、Sizerを入れる事も可能です。

例えば、GridSizerの中に、BoxSizerを設置する事が可能です。

<例えば、下記構造とか・・>

上記構造の場合、コードは以下の通りです。

        text_box_sizer = wx.TextCtrl(self, id=-1, value='BoxSizer下')
        text_grid1 = wx.TextCtrl(self, id=-1, value='GridSizer下_1')
        text_grid2 = wx.TextCtrl(self, id=-1, value='GridSizer下_2')
        text_grid3 = wx.TextCtrl(self, id=-1, value='GridSizer下_3')
        text_grid4 = wx.TextCtrl(self, id=-1, value='GridSizer下_4')

        # Sizerをインスタンス化
        sizer = wx.BoxSizer(wx.VERTICAL)
        grid_sizer = wx.GridSizer(rows=2, cols=2, gap=(0, 0))

        # GridSizerにAddしていく
        grid_sizer.Add(text_grid1, 1, wx.EXPAND)
        grid_sizer.Add(text_grid2, 1, wx.EXPAND)
        grid_sizer.Add(text_grid3, 1, wx.EXPAND)
        grid_sizer.Add(text_grid4, 1, wx.EXPAND)

        # BoxSizerにAddしていく
        sizer.Add(text_box_sizer, 1, wx.EXPAND)
        sizer.Add(grid_sizer, 1, wx.EXPAND)

        self.SetSizer(sizer)

<実行結果>

次回予告

第2回と3回(本記事)で、画面の見た目を、ご紹介してきました。

次回記事では、ボタン押下等の処理、つまりイベント発生時の処理方法をご紹介します。

次回記事は、ステータスバー、メニューバーの紹介をしました。

画面の「見た目」を、まとめて紹介した方が、わかりやすいと思ったからです。

イベント処理のご紹介は、次回以降にしました。

リンクを貼り付けます。

【 Python 】かっこいいGUI wxPythonの使い方入門 その4 – ステータスバー 、メニューバー の紹介 –
【 Python 】かっこいいGUI wxPythonの使い方入門 その4 – ステータスバー 、メニューバー の紹介 –Pythonの、かっこいいGUIである、wxPythonの使い方を紹介した記事です。wxPythonにおいて、ステータスバー、メニューバーの設置方法を、Pythonによるコードにて紹介した記事です。...

本記事は、少々長くなってしまいました。

次回の記事は、できるだけ短くなるよう、努めます。

最後まで読んで頂き、ありがとうございました。

また、お会いしましょう。