Pythonバイナリデータの扱いかた

 今日も見に来てくださって、ありがとうございます。石川さんです。

 ここのところ、Tkinterのユニットテストの実現に向けていろいろと調査を進めているのですが、若干難航しております。。。

 ということで、本ブログの更新が滞っていたのですが、更新がないよりも、何か自分にとって当たり前のことであっても誰かの役に立つかもしれないということで、Pythonの標準モジュールの範囲内でのバイナリデータについて書いてみたいと思います。

バイト

 Pythonでは、8ビットの整数を扱うために、bytesとbytearrayを用意してくれています。扱えるのは、8ビットで表現できる符号なし数値の範囲で、0~255です。

In [1]: bytes(5)
Out[1]: b'\x00\x00\x00\x00\x00'

In [2]: bytearray(5)
Out[2]: bytearray(b'\x00\x00\x00\x00\x00')

 上記のように、数値を指定すると0x00で初期化された指定されたサイズ(今回は5バイト)のバイトとバイト列を生成してくれます。以下のようにすると、リストの値から生成可能です。

In [3]: l = [1,2,3,4,5]

In [4]: b = bytes(l)

In [5]: b
Out[5]: b'\x01\x02\x03\x04\x05'

In [6]: ba = bytearray(l)

In [7]: ba
Out[7]: bytearray(b'\x01\x02\x03\x04\x05')

 ちなみに、bytesはイミュータブルでbytearrayはミュータブルです。

In [8]: b[2] = 0
Traceback (most recent call last):

  File "<ipython-input-107-088d74b352b8>", line 1, in <module>
    b[2] = 0

TypeError: 'bytes' object does not support item assignment

In [9]: b
Out[9]: b'\x01\x02\x03\x04\x05'

In [10]: ba[2] = 0

In [11]: ba
Out[11]: bytearray(b'\x01\x02\x00\x04\x05')

 ごらんの通り、bytesは変更できません。

エンディアン

 いきなり「エンディアン」というタイトルを書いてしまいましたが、ちょうどぼくが新入社員のときにはじめてであった概念で、あまりにも衝撃的だったので、よく覚えています。ま、実際には衝撃を受けるひとはそんなにいないと思いますけど。(笑)

 当時C言語を使っていました。整数値を格納するshort型は2バイト、long型は4バイト、int型は、処理系によって、2バイトだったり4バイトだったりする、というところまではなるほどー、という感じだったのですけど、実際に数値が格納された結果、上下のバイトが入れ替わる、という話です。まったくの謎でした。

 例えば、先ほどの2バイト分について考えてみます。1バイト目に0x01、2バイト目に0x02が入力されているので、2バイトが一つの数値だと考えると、0x0102ということです。2進数に置き換えて計算するとこの数値は、(0000000100000010)2となり、
2**8+2**1 = 256+2 = 258になると思いますよね。それが、上下のバイトが入れ替わると、
(0000001000000001)2となり、2**9+2**0 = 512+1 = 513ということです。ね、意味がわかりませんよね。

 では、先ほど生成したbytesをstructモジュールを使って読み込んでみます。まずは、サンプルをご覧ください。structはC言語の構造体に似たデータを処理するのに便利に使えます。

In [12]: import struct 

In [13]: b[:2]
Out[13]: b'\x01\x02'

In [14]: struct.unpack('>H',b[:2])
Out[14]: (258,)

In [15]: struct.unpack('<H',b[:2])
Out[15]: (513,)

 ここで、'<H’、’>H’を指定していますが、最初の記号はエンディアン指定子ということで、「>」こちらがビッグエンディアン、「<」こちらがリトルエンディアンです。結果をご覧の通り、ビッグエンディアンは、直感的に正しい気がする方で、リトルエンディアンが上下のバイトが入れ替わるヤツですね。ちなみに二番目の記号「H」は2バイトの符号なし単整数を扱う書式指定子ということになります。

 実際にこのエンディアンという言葉、ぼくにはあんまりなじみがなくて、いつもビッグとリトル、どっちだったっけ?と、なってしまいます。重要なのは、時と場合によって入れ替わる可能性がある、ということを知っておくことと、バイトを扱うときにはどちらが採用されているのか、ということに気を付けなければいけない、ということですね。

structの書式文字列

 structの書式文字列はエンディアン指定子と書式指定子の二種類あり、使うときはエンディアン指定子の方を書式指定子より先に記述します。

指定子バイト順
<リトルインディアン
>ビッグエンディアン
エンディアン指定子
指定子意味バイト数
x1バイト読み飛ばし1
cchar 長さ1のバイト列1
bsigned char 符号付整数1
Bunsigned char 符号なし整数1
hshort 符号付整数2
Hunsigned short 符号なし整数2
iint 符号付整数4
Iunsigned int 符号なし整数4
llong 符号付整数4
Lunsigned long 符号なし整数4
qlong long 符号付整数8
Qunsigned long long 符号なし整数8
ffloat 単精度浮動小数点数4
ddouble 倍精度浮動小数点数8
書式指定子

 バイトをPythonデータに変換するときは、unpackを利用しますが、その逆のときは、packを利用します。

In [16]: struct.pack('>2L',500,12)
Out[16]: b'\x00\x00\x01\xf4\x00\x00\x00\x0c'

In [17]: struct.unpack('>2L',b'\x00\x00\x01\xf4\x00\x00\x00\x0c')
Out[17]: (500, 12)

 この書式指定子は、ビッグエンディアンで、2個の4バイトの符号なし整数を扱う、ということを意味しています。ちなみに、「x」の書式指定子は、指定されたバイト数分を読み飛ばす、ということを意味しています。

In [18]: struct.unpack('>2xH2xH',b'\x00\x00\x01\xf4\x00\x00\x00\x0c')
Out[18]: (500, 12)

 2バイト飛ばして2バイトの符号なし整数、2バイト飛ばして2バイトの符号なし整数、ということを意味しています。

まとめ

 バイトを扱うときは、エンディアンに気を付けて。簡単なバイトなら、structを使って、バイトとPythonデータを変換できます。

ORACLE SQL分析関数で受注を分割

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、ちょっとお仕事でどうしようかなぁ、というのをSQLで解決することにしましたので、その実験のまとめを記録しておきます。

解決したいこと

 外部から自社商品の注文明細データが送付されてくるのですが、一つの注文取引の中に、同じ商品の明細が含まれて送られてきます。こちら側のシステムでは、同じ商品を一つの受注単位で複数件登録できない仕様なので、その場合は二つ以上の受注単位に分割して登録する必要があります。

サンプルテーブル

サンプルテーブル

サンプルデータ

サンプルデータ

 解決したいことの具体例を示します。取引番号1のデータは、商品コードの重複はないので、そのまま登録可能です。取引番号2のデータについては、商品コード2と3が重複していますので、取引2-1と2-2に分割して登録する必要があります。取引番号3のデータは商品コード1が3重になっていますので取引を3-1、3-2、3-3と分割して登録する必要があります。

 この問題を解決するのにいろいろな方法がありますが、今回はORACLE分析関数のROW_NUMBER()を使いたいと思います。分析関数は、各行を取り出しつつ、全体に対する分析結果を同時に取得するための関数です。

解決方法

 以下のSQLを実行して、取引を分割するためのキー情報を作成しつつ、データを取り出します。このキー情報がこちら側の受注単位になります。

SELECT 取引番号||'-'||ROW_NUMBER() OVER (PARTITION BY 取引番号, 取引日, 商品コード ORDER BY 取引番号, 取引日, 商品コード) 取引
      ,取引番号
      ,取引日
      ,商品コード
      ,数量
  FROM 取引
 WHERE 取引日 = '20-05-27'
 ORDER BY 1;

実行結果

実行結果

 これで、取引を分割することができました。

スクリプト

今回使用したスクリプトです。

CREATE TABLE 取引 ( 取引番号 NUMBER, 取引日 DATE, 商品コード NUMBER, 数量 NUMBER);

INSERT INTO 取引 (
 SELECT 1, '20-05-27', 1, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 2, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 3, 1 FROM DUAL UNION ALL
 SELECT 1, '20-05-27', 4, 2 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 2, 1 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 2, 3 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 3, 4 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 4, 2 FROM DUAL UNION ALL
 SELECT 2, '20-05-27', 3, 3 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 2 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 1 FROM DUAL UNION ALL
 SELECT 3, '20-05-27', 1, 3 FROM DUAL
);

COMMIT;

SELECT 取引番号||'-'||ROW_NUMBER() OVER (PARTITION BY 取引番号, 取引日, 商品コード ORDER BY 取引番号, 取引日, 商品コード) BREAK
      ,取引番号
      ,取引日
      ,商品コード
      ,数量
  FROM 取引
 WHERE 取引日 = '20-05-27'
 ORDER BY 1
;

まとめ

 分析関数、いろんな用途に使えて便利ですので、ぜひ、使い方を覚えましょう。

Python tkinter GUIプログラミング テーブル情報取得

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、データベースオラクルへ接続、そのスキーマのテーブル情報を取得してTreeviewへ表示するプログラムにチャレンジしました。いや~、けっこう時間がかかりましたねぇ。Treeviewを思ったように操作するのが難しかったなぁ。

 Oracleのバージョンは18c Express Edition、PythonはAnaconda3です。

出来上がりイメージ

 今回のソースを実行して、「load table」ボタンをクリックすると、こんな感じのウィンドウになります。左側のテーブル一覧を選択すると右側に項目の一覧が表示されます。

できあがりイメージ

ソースコード

import cx_Oracle as db
import tkinter as tk
import tkinter.ttk as ttk

LOGIN_USER = "ユーザー名をここに入力"
LOGIN_PASSWORD = "パスワードをここに入力"
LOGIN_HOST = "localhost:1521/xepdb1"

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Table viewer sample for ORACLE XE 18c")

        self.conn = db.connect(LOGIN_USER, LOGIN_PASSWORD, LOGIN_HOST)
        self.cur = self.conn.cursor()
        
        self.style = ttk.Style(self)
        self.style.configure("Treeview", rowheight=30)
        
        columns = ("#1", "#2")
        self.table_list = ttk.Treeview(self, show="headings", columns=columns, selectmode="browse")
        self.table_list.heading("#1", text="TABLE_NAME")
        self.table_list.column("#1", width=125)
        self.table_list.heading("#2", text="COMMENTS")
        self.table_list.column("#2", width=300)
        self.table_list.grid(row=0, column=0, rowspan=2, sticky=tk.NSEW)
        self.scroll_bar = ttk.Scrollbar(self, command=self.table_list.yview)
        self.table_list.configure(yscroll=self.scroll_bar.set)
        self.scroll_bar.grid(row=0, column=1, rowspan=2, sticky=tk.NS)
        self.table_list.bind("<<TreeviewSelect>>", self.show_table)
        
        columns = ("#1","#2","#3")
        self.table_info = ttk.Treeview(self, show="headings", columns=columns)
        self.table_info.heading("#1", text="COLUMN_NAME")
        self.table_info.heading("#2", text="COMMENTS")
        self.table_info.heading("#3", text="DATA_TYPE")
        self.table_info.grid(row=0, column=2, sticky=tk.NSEW)
        
        self.load_button = ttk.Button(self, text="load table", command=self.load_table)
        self.load_button.grid(row=1, column=2)
        
        self.grid_rowconfigure(0, weight=1)

    def load_table(self):
        self.table_list.delete(*self.table_list.get_children())
        self.table_info.delete(*self.table_info.get_children())
        sql = ("SELECT TABLE_NAME, COMMENTS"
               "  FROM USER_TAB_COMMENTS"
               " WHERE TABLE_TYPE = 'TABLE'")
        results = self.cur.execute(sql)
        for row in results:
            self.table_list.insert("", tk.END, values=row)
        self.table_list.selection_set(self.table_list.get_children()[0])
    
    def show_table(self, event):
        sel = self.table_list.selection()
        table_name = self.table_list.item(sel[0])["values"][0]
        sql = ("SELECT T1.COLUMN_NAME, T1.COMMENTS,"
               "  CASE WHEN T2.DATA_TYPE IN ('TIMESTAMP(6)','DATE') THEN T2.DATA_TYPE"
               "       WHEN T2.DATA_TYPE IN ('CHAR','VARCHAR2') THEN T2.DATA_TYPE ||'('||T2.DATA_LENGTH||')'"
               "       WHEN T2.DATA_TYPE = 'NUMBER' THEN T2.DATA_TYPE || "
               "         CASE WHEN T2.DATA_PRECISION IS NULL AND T2.DATA_SCALE IS NULL THEN ''"
               "              WHEN T2.DATA_PRECISION IS NOT NULL AND NVL(T2.DATA_SCALE,0) = 0 THEN '('||T2.DATA_PRECISION||')'"
               "              WHEN T2.DATA_PRECISION IS NOT NULL AND T2.DATA_SCALE IS NOT NULL THEN '('||T2.DATA_PRECISION||'.'||T2.DATA_SCALE||')'"
               "              WHEN T2.DATA_PRECISION IS NULL THEN '(.'||T2.DATA_SCALE||')'"
               "              END"
               "       END"
               "  FROM USER_COL_COMMENTS T1, USER_TAB_COLUMNS T2"
               " WHERE T1.TABLE_NAME = T2.TABLE_NAME"
               "   AND T1.COLUMN_NAME = T2.COLUMN_NAME"
               "   AND T1.TABLE_NAME = :table_name")
        results = self.cur.execute(sql, table_name=table_name)
        self.table_info.delete(*self.table_info.get_children())
        for row in results:
            self.table_info.insert("", tk.END, values=row)
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

解説

 Oracleへの接続にはおなじみのcx_Oracleを利用しました。cx_Oracleのセットアップについては、こちらを参考にしてください。

 5~7行目の接続情報はご自分の環境にあわせて変更してください。Oracle18c Express Editionをデフォルトインストールしたぼくの環境では、このLOGIN_HOSTで接続できました。もしかしたらlocalhostをローカルPCのIPアドレスに変更する必要があるかも知れません。

 14、15行目の初期化処理で、Oracleへの接続とカーソル取得しています。21行目でTreeviewを作成しています。showオプションは”tree”と”heading”が指定できますが、組み合わせで以下のように変わってきます。

  • “tree” カラム#0を表示します。
  • “headings” ヘッダ行を表示します。
  • “tree headings” カラム#0とヘッダ行の両方を表示します。(デフォルト値)
  • “” カラム#0もヘッダ行も表示しない。

 selectmodeは、”extended”、”browse”、”none”が選択可能で、以下のような意味があります。

  • “extended” 複数アイテムが選択可能。(デフォルト値)
  • “browse” 単一のアイテムが選択可能。
  • “none” 選択状態は変わらない。

 ちなみに、このTreeviewの行間、ぼくの環境Windows10では低く設定されていて文字が切れてしまっていたので、17、18行目のtkk.Styleを使って”Treeview”のrowheightを30にセットしています。

 スクロールバーは27、28行目で作成してセットしています。イディオムだと思って覚えてしまいましょう。30行目でバインドしているのは、仮想イベントの<<TreeviewSelect>>です。セレクションが変更されたときに発生します。ここでは、show_tableをバインドしています。これで選択されたテーブルの情報を隣のTreeviewに表示しています。

 42行目のself.grid_rowconfigure(0, weight=1)ですが、ウィンドウのサイズを変更したときに、行がどれくらいの割合で広がるか、というのを設定しています。指定しない場合のweightは0にセットされています。これによりgridで指定された0行目がウィンドウのサイズ変更に伴って変更されます。ちなみに、ウィジェットにgridを適用するときにstickyを指定しない場合は大きさは変わらないので注意が必要です。

 45、46行目では、Treeviewの項目を削除しています。すべての項目を取得するのに、self.table_list.get_children()を利用しています。このメソッドはすべての項目をタプルで戻します。self.table_list.delete()の引数は、削除したい項目をすべて渡す必要があるため、先頭に「*」アスタリスクを付けて、タプルのアンパックをしています。タプルのままでは動作しませんので、注意が必要です。

 47~49行目はSQL文を定義しています。複数文字列をインデントを無視した形で改行するのに丸括弧「()」を使っています。丸括弧の中はインデント無視できるのですよね。ちなみにここで指定しているテーブルのUSER_TAB_COMMENTSは、オラクルのデータディクショナリで、テーブルに設定されているコメントを参照することができるビューになります。

 SQL文の実行は50行目です。51行目で結果セットを順番に取り出すforループを記述しています。データを取得して、ループして一行ずつ処理する、このパターンもよく使いますので、覚えておくと便利です。

 Treeviewへのデータの追加はinsert()メソッドです。valuesパラメータを使うことで複数項目を一気にセットすることができます。

 あと、53行目は、テーブルを取得したときに、1行目を選択させて、テーブル情報を右側に表示するために追加しました。1行目を選択するのに結構悩みましたよ~。selection_set()メソッドへ渡すパラメータは項目名なのですよね。0とか-1とかを指定してもエラーになります。そこで、ここでは、self.table_list.get_children()[0]を使って最初の項目を取得しています。この0を-1に変更すると最後の項目、任意の行の場合も数値を変更するだけで対応可能ですね。

 56行目、sel = self.table_list.selection()は、選択中のアイテム名を取得しています。取得結果は複数の場合もあるのでタプルで戻ってきます。ここでは一項目しか取得できない設定ですので、sel[0]と指定すれば、選択中の項目が利用可能になります。57行目のtable_name = self.table_list.item(sel[0])["values"][0]で、Treeviewにセットされた値を取得することができます。

まとめ

 いや~、時間かかったなぁ。あ、二つ目のSQLの説明を忘れてました。ま、そんなに難しくないし、見ればわかるか。Treeviewの扱いに慣れるのには、ちょっと時間がかかりそうですね。またサンプル作ってみますね。

EXCEL 絶対パスからファイル名のみ取り出す最速のやり方

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、お仕事で調査をしているときに、EXCELで絶対パスからファイル名だけを切り取るのをどうやったらいいのかを調べるのに、ちょっと時間がかかってしまったので、まとめておきます。

やりたいこと

 以下のように、エクセルに書かれた絶対パスからファイル名を取り出す。

絶対パスからファイル名のみを取り出す

 Pythonならすぐにこんな感じでできちゃうのですけどねぇ。

In [1]: from os.path import basename
   ...: basename(r"C:\work\tmframework\modebi-fw\TMD\MODEBI_FW_サンプルデータ.xlsx")
Out[1]: 'MODEBI_FW_サンプルデータ.xlsx'

 ま、エクセルですばやくどうするのか、とういことですから、きっとエクセルの関数を使うのがよいでしょうねぇ。で、調べて見つかったのが二つありました。

短めのやりかた

 最初に見つけたのは、このやり方です。

=TRIM(RIGHT(SUBSTITUTE(B2,"\",REPT(" ",100)),100))

 中身を説明すると、SUBSTITUTE関数で、区切り文字の「\」を空白100文字に置き換えて、右から100文字切り取って、そのあと空白を消す、という感じです。SUBSITUTE関数は、第一引数の文字列から、第二引数の文字列を探し出して、第三引数に置き換える関数です。REPT()関数は第一引数(この場合は半角スペース” “)を第二引数の個数分繰り返す関数で、100文字分の半角スペースを作っています。100文字を超えるファイル名の場合は使えないやり方ですが、Windowsではきっと問題ありませんね。ちょっとトリッキーなやり方だと思いますが、割と短く書くことができる、良いやり方だと思いました。ただ、「100」はあんまり好きじゃないのですよねぇ。

正統派(?)なやり方

 そして、もうちょっと探して見つけたのが次のやり方です。

=MID(B3,FIND("*",SUBSTITUTE(B3,"\","*",LEN(B3)-LEN(SUBSTITUTE(B3,"\",""))))+1,LEN(B3))

 こちらも説明すると、最後の区切り文字「\」を探して、「*」に置き換えて、「*」から最後までを切り出す、という感じですね。「*」に置き換えるのは上記と同様にSUBSTITUTE関数を利用していますが、秀逸なのは、「最後の」を決めるところですね。ここで「LEN(B3)-LEN(SUBSTITUTE(B3,”\”,””)))」とやっていますが、全体の文字数から、「\」を取り除いた文字列の文字数を引くことで、「\」の個数を求めています。いや、最初に思いついた人、天才かも。ただ、ちょっと長くて難しいのですよねぇ。

決定版

 で、関数を使ってファイル名を取り出すもっと美しい方法はないのかと、調べていたのですけど、最初に関数でどうやるのか、と、考えていたのが間違いでした。これって、実は、ぼくはこんなに難しいことができますよ~、という自慢でしかないのでした。

 と、いうことで、反省しました。これが恐らくいちばん簡単ではやい方法です。パスを選択して、「*\」で置換を使います。これで一発解決です。ものの数秒で解決しました。

絶対パスからファイル名のみを取り出す

 説明すると、検索する文字列に指定した「*」はワイルドカードと言って、0文字以上のすべての文字に一致する、という意味になります。なので、最初の例で言うと、まず「C:\」が置換されます。置換後の文字列は未入力なので、置換される、と言っても消去されます。次に、「work\」が消去され、「tmframework\」「modebi-fw\」「TMD\」と、次々に消去されて、「MODEBI_FW_サンプルデータ.xlsx」が残ります。

まとめ

 あまり技術力にこだわり過ぎないほうがよいかも。時には柔軟な発想でやりましょう。

在宅勤務4

 今日も見に来てくださって、ありがとうございます。石川さんです。

 在宅勤務もそろそろ終わりでしょうかねぇ、と、思っていましたが、ちょっと延長しそうな気配ですね。緊急事態宣言は解除されそうな雰囲気ですが、一斉に勤務開始、というふうにはならなさそうです。もうちょっと自宅の作業スペースの効率をよくしましょうね。と、いうことで、、、

 ぼくはふだんノートパソコンでお仕事しています。ノートパソコンは机の上なので、作業するのに姿勢がちょっとうつむき加減になって、ずっと作業を続けていると、首が痛くなるのですよね。で、書籍を積み上げて、その上にノートパソコンを置いて作業していました。少しの期間だから、と、思ってそのまま使っていましたが、そろそろちゃんとしようと思いまして、スタンドを用意しました。

ダンボール製ノートパソコンスタンド

 ジャジャーン、ちゃんとしました!(笑)

ダンボール製ノートPCスタンド
ダンボール製ノートPCスタンド

 はい、自作しました。市販のスタンドを買ってくる、というのでもよかったのですけど、探すのが億劫で、またネット通販だと時間がかかったり、届いたモノとイメージが違ったりするとやだなぁ、と、思いまして、自作することに決めました。と、言ってもダンボール工作ですけどね。ダンボールなら、イマイチでも捨てて作り直せばいいですし、エコですよね。

 ちょっと高めにして、姿勢よく作業できるようにしてみました。もしかしたら高すぎるかもしれませんが、しばらく使ってみて、ダメだったら、1センチずつ切って調節しようと思っています。

つくりかた

 つくりかた、知りたい人なんているか?という自問自答しながら、どうやってつくったか書いておきます。

 椅子に座って、これくらいの高さにしたい、というところへ持って行って、机からノートパソコンの距離を測りました。手前は20センチメートルですね。ぼくの場合、ノートパソコンを一番開いた状態にしたかったので、斜めに置くことになるので、後ろの方も高さを計測しました。こちらは30センチメートルです。そして、斜めにしたときのおおよその距離を測ると20センチメートルだったので、これで設計図を作ってみました。

手書きの設計図
手書きの設計図

 はい、手書きです。(笑)しかも、テキトーな上に、間のつなぎ部分の図面はありません。

 奥行きの20センチはちょっと不安だったので、5センチプラスしました。これを二枚つくります。あと、30×7の長方形の紙をつなぐ部分に用意して、つなぎにしました。切込みを台の下部へ前から20センチメートルのところと、つなぎ部分の左右から5センチメートルのところに4ミリの切れ込みをいれまして、組み立てて、終了です。

まとめ

 作業時間、およそ20分くらいでできましたので、首が痛くならないための投資としてはお安いものです。もっと早くやってればよかったですね。

GCPチュートリアル Hello World!アプリのデプロイ2

 今日も見に来てくださって、ありがとうございます。石川さんです。

 いや~、前回は、たいへんがんばりました。でも、とっても読みにくい記事になってしまったのでは、と反省いたしました。ホントのところは、まとめのあたりではもう、力尽きていたのですよねぇ。すみませんでした。

 と、いうことで、前回のホントのまとめをやりたいと思います。整理すると、以下のような感じですね。

GCPチュートリアル Hello World!アプリのデプロイ概要

GCPチュートリアル Hello World! デプロイ概要

 手順としては、プロジェクトを作成するか選んで、アプリの所属を決めます。そして、Cloud Shellと呼ばれるシェルの実行環境からコマンドラインでいろいろ実行していきます。最初に、用意されているサンプルアプリケーションをgithubリポジトリからクローンします。ま、コピーと同じですね。

 次に、試しにテスト実行してみました。そのあと、アプリを作成して、デプロイを実行。ここまででアプリが使えるようになるために必要な手順が出そろいました。アプリを作りたい人は、コピーしてきたリポジトリをどんどん追加修正していけば、オッケー、という感じでしょうか。

 チュートリアルの後半は、アプリの状態を確認することと、無効化すること、さらに、そのリソース管理としてプロジェクトをシャットダウン(削除)するところまで実行してみました。これでぼくも立派なプロジェクト管理者、アプリ作成者ですね。

そして、、、

 前回は力尽きて見逃していたのですけど、チュートリアルの最後に「まとめ」がありました。ステップ8/8ですね。

ステップ8/8前半

 丁寧にも、このチュートリアルを完了した人たちに向けて、次に何ができるか、というものも示してくれていました。

ステップ8/8後半

 そう、Google Cloud SDKをダウンロードしたら、ローカルで開発ができるようです。他にDjango(Pythonの別のWebフレームワーク)を利用したアプリを作成できるし、ウェブアプリのビルド、ということでこのチュートリアル以上のことも、学べそうです。

まとめ

 PythonのWebフレームワーク、FlaskやDjangoを使った開発がGCPで簡単に実現できるのですね。これがあれば、お客さまへ、いろいろな提案ができそうです。そういえば、サービスの名前を気にしていませんでしたが、ishikawasekkei.comのサブドメインにする方法とか、どうするのでしょうね。また機会があれば調べてみたいと思います。

GCPチュートリアル Hello World!アプリのデプロイ

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、何となくGoogle Cloud Platformコンソールを眺めていると、ふと、目に留まるものがありました。

GCPコンソール

 そうそう、Google Cloud Platformのスタートガイドにあった「Hello Worldアプリをデプロイ」というメニューです。先日「デプロイってなに?」と尋ねられたとき、ウェブサーバーにプログラムを配備して使えるようにすること、だけど、それでホントにあってるかなぁ、と、心配になったのでした。ここに、ありましたよ!デプロイが!!

 スタートガイドなので、初心者にも優しいはず。GCP、契約はしているんだけど、このWordPressのホームページ以外、ほとんど活用できていないのですよねぇ。会社を始めたときは、ぼくはGCPのプロフェッショナルになろう、と、思っていたのですけどねぇ。おぼれそうになって、しばらくお休みしていました。と、いうことで動かして見ました。

 クリックすると右側に概要が現れます。こんな感じです。

チュートリアルApp Engineのクイックスタート

 はい、では、右下の「開始」をクリックして始めたいと思います。でもその前に、チュートリアルの左にある「←」をクリックして見ます。

いろいろなチュートリアルメニュー

いろいろなチュートリアルがあるのがわかりました。今回は「App Engineを試す」ということでもう一度クリックして戻ります。

Hello Worldに利用する言語が選択できます。

 今回はPythonを選択します。いったん戻らないとGoが自動的に選択されてしまうので、違う言語でチュートリアルをやりたい方は、同じ手順でやってくださいね。上のPythonを選ぶとまた元の画面に戻ります。あと、環境は二つあって、上の「Python」は「スタンダード環境」下は「フレキシブル環境」と、異なります。違いについては、こちらに記載があります。自由度の高いシステムの場合は「フレキシブル環境」を選択する必要がありますが、今回は「スタンダード環境」で問題ありませんので、こちらを選択します。

 では、チュートリアルなので、そんなに難しく考えず、とにかく「開始」です!やってみて、初めてわかることって、けっこうあるんです。

プロジェクト設定

 お、プロジェクトを選ぶか、新しく作成するのですね。既存のプロジェクトがチュートリアルで汚れる(?)のはイヤなので、新しくプロジェクトを作りましょう。「新しいプロジェクトを作成」のところをクリックしてみます。

新しいプロジェクト作成画面

 なんと、新しいプロジェクト作成画面が開きました。お試しで作った時はプロジェクト名を変えて作成したのですけど、どうせ後で削除するプロジェクトなので、このまま「作成」をクリックします。

通知が表示されました。

 はい、通知が表示されて「My Project 60925」が作成されました。あ、前回と、、、前々回のプロジェクト作成履歴が表示されていますね。(笑)実は、二回も実験しています。慣れるまで、何回かやる必要があるかと思って、今回、三度目の正直ですね。

プロジェクト設定に新しいプロジェクトが選択されました

 プロジェクト設定の画面、新しいプロジェクトがセットされた状態になりました。「次へ」をクリックして進みましょう。

Cloud Shellを使用する

 次に、Cloud Shellを使う、ということですね。Cloud Shellとはいったい何なのか、全体を見渡すと、ありました。このアイコンですね。

Cloud Shellをアクティブにするボタン

 ボタンが見つけられましたので、クリックして見ます。何やらプロビジョニングしています、、、というのがしばらく表示された後、でました!

CLOUD SHELL

 なるほど、クラウド上のシェルだから、CLOUD SHELLなのですね。そのまんまやん。そして、次は、サンプルコードのクローンを作成して、「Hello World」コードに移動するのですが、このクローン作成、ちょっとだけ注意が必要です。

クローンを作成するコマンド

 よく見てください。なんと、続きがあるのです。

マウスをポイントすると、水平スクロールバーが登場、後半があることが判明

 ぼくは、「git clone https://github.com/GoogleCl」と入力してEnter、エラーになったくちです。いちばん右側のアイコンでコピーできますし、その左隣のアイコンでCloud Shellにコピペしてくれます。便利ですね。コピペして、実行しましょう。

実行結果

 はい、しばらくクルクルしてから終了しました。次はチュートリアルの指示に従ってディレクトリを移動します。クローンとして作ったフォルダへ移動します。ホームディレクトリの配下にできた「python-docs-samples/appengine/standard_python3/hello_world」へ移動します。こちらもボタンのコピペで実行できますね。(追記:2021年8月29日に実行して確認したところ、ディレクトリ名が画像と異なっていました。ま、コピペすれば大丈夫ですね。)

ディレクトリを切り替え

では、チュートリアルの「次へ」ボタンを押して進みます。ステップ3/8です。

ステップ3/8の前半

 デプロイの構成、ということでmain.pyの内容表示してみます。コメントがたくさんあって、必要なところは以下の部分だけでした。

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    """Return a friendly HTTP greeting."""
    return 'Hello World!'

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

 Flaskというのは、Python用のWebフレームワークです。このスクリプトはルートにアクセスされると単に挨拶として「Hello World!」を戻します。

ステップ3/8後半

 ここでは、構成ファイルを表示ということで中身を出力してみましたが、

runtime: python37

 というふうに、ランタイムにpython37を使う、ということがわかりました。「次へ」をクリックします。(追記:2021年8月29日時点で、こちらは「python39」になっていました。時代は進みますねぇ。ちなみにチュートリアルの記載は「python38」でした。更新漏れですね。(笑))

ステップ4/8前半

 次は、アプリのテストです。virtualenvを実行して仮想環境を作成して、それを有効にする、ということですね。コピペで実行します。

virtualenv実行結果
ステップ4/8後半

 次に、pipでFlaskに関連するモジュールをインストールしてmain.pyを実行します。

pip実行結果

(追記:2021年8月29日 pip実行時、pipのバージョンが古いですよ、と、WARNINGが出力されました。アップグレードの方法が記載されていましたが、ま、放置していても問題ありませんよね。)

main.py実行結果

 次に、ウェブでプレビューボタンをクリックします。ここにありました。

ウェブでプレビューボタン

クリックします。

ウェブでプレビューボタンをクリックしたところ

 ポート8080でプレビューを選択します。すると、

Hello World!

 やっと、たどり着きました。念願の「Hello World!」です。単に「Hello World!」の文字を出力しているだけです。ふ~。これでやっと半分終了ですね。Ctrl+Cキーでアプリーションのインスタンスが終了しました。「次へ」をクリックします。

ステップ5/8 App Engineへのデプロイ

 ほう、リージョン内に、アプリを作成して、それからデプロイですね。やっとでました、デプロイです。ちょっとここまでやってみましょう。

gcloud app create実行結果

########## 2021年8月29日 追記開始 ここから  ##########
「gcloud app create」を実行したところ、Cloud Shellの承認というポップアップが表示されました。途中で作業を中断して再開したせいかもしれません。

CloudShellの承認ポップアップ

「承認」をクリックしましたが、画面上にはエラーが出ていました。どうやらうまく承認はされなかったようです。

gcloud app create実行時に発生したエラー

 現在、アクティブなアカウントが選択されていません、ということのようですので、言われるがままに、「gcloud auth login」を実行してみました。以下のようにURLが示されました。その下に「Enter verification code:」と書いてあるので、URLで表示された先で認証コードが取得できるのでしょう。

gcloud auth loginを実行したところ

 と、いうことで示されたURL(https://accounts.google.com/o/oauth2/…)をクリックすると、ブラウザの別タブに「アカウントの選択」が表示されましたので、こちらのGCPで使用しているアカウントを選択しました。

アカウントの選択

すると、以下の通りリクエストの許可を求めてきました。

アカウントのアクセスをリクエスト

「許可」をクリックしたところ、以下のとおり、ログイン用のコードが表示された画面が出てきました。

ログイン用のコード

これをコピーして「Enter verification code:」のところへ貼り付けます。すると、ログインできた、というメッセージが表示されました。

verification codeを入力したところ

ここで改めて「gcloud app create」を実行したところ、上記の「gcloud app create実行結果 」の結果が表示されました。
########## 2021年8月29日 追記終了 ここまで  ##########

 おっと、「Please enter your numeric choice:」と言われています。あなたのApp Engineアプリケーションを置くリージョンを選んでください、ということで、まあ順当に[1]番でしょうねぇ。

リージョン選択後の画面

 リージョンを選択すると、10秒ほどクルクルして完了しました。「Success!」と言ってますね。次に「gcloud app deploy」を使ってあなたの最初のアプリをデプロイしてください、と言ってますね。チュートリアルの方はもうちょっとたくさん記述があって「gcloud app deploy app.yaml \ –project t-system-277912」と書いてありますね。チュートリアルの方を実行してみましょう。

gcloud app deploy app.yaml \ –project t-system-277912実行後

 おっと、続けますか?と、聞いてきています。もちろん続けますよ。「Y」を入力してエンター。

5分後、デプロイ完了です

 5分ほど経ったでしょうか、デプロイが完了いたしました!

 「アプリにアクセスする」をクリックして、見事、「Hello World!」が出力されました。

デプロイされた「Hello World!」アプリ

 「次へ」をクリックして6/8へ行きましょう!

ステップ6/8

 次は、アプリのステータスを表示する、ということですね。[App Engine]を選択するのですね。

App Engineを選択します

App Engineをクリックすると次の画面があらわれました。

App Engineのダッシュボード

 「次へ」をクリックします。

ステップ7/8

 設定ページに行って、アプリケーションを無効にすれば、課金されなくなるそうです。

設定画面

 「アプリケーションを無効にする」をクリックしてみます。

「アプリケーションを無効にする」をクリックしたところ

 おお、無効にしますか?と、聞いてきました。アプリIDを入力しないと無効にならないようです。間違って無効にしちゃった、という事故が起きにくい仕組みになっているようです。アプリIDを入力して、「無効にする」をクリックします。

アプリケーションを無効にしたところ

 次は、「プロジェクトを削除」ですね。[IAMと管理]を探します。

IAMと管理

 「IAMと管理」をクリックします。

IAMと管理をクリックしたところ

 次に、「リソースを管理」をクリックします。

リソースの管理画面

 今回作成した「My Project 60925」をチェックして「削除」をクリックします。

削除の確認画面

 おお、削除の確認画面ですね。ここでもプロジェクトIDを入力しないといけないようです。間違って違うプロジェクトを削除しにくい仕組みですね。それに30日間の猶予もあるのですね。プロジェクトIDを入力して「シャットダウン」をクリックします。削除なのに「シャットダウン」なのですねぇ。ちょっと不思議な感じがします。

シャットダウン後のメッセージ

 「OK」クリックしたところ、プロジェクトが消えました。

まとめ

 いや~、チュートリアルを順番にやっただけですが、長くなっちゃいましたね。ここまでお読みくださってありがとうございます。おつかれさまでした。

Python tkinter GUI プログラミング ドロップダウンボックス

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、バーコードを作るプログラムサンプルを作成しました。その時に、バーコードの種類を選べるようにドロップダウンボックスを用意して、選べるようにしようかな、と思いました。ところが調べてみるとドロップダウンの機能はOptionMenuとComboboxの二種類ありました。ぼくの思っていたドロップダウンはComboboxの方でしたが、ちょっと調べたものをまとめておきたいと思います。

出来上がりイメージ

 はい、今回のプログラムの出来上がりイメージはこんな感じです。上に、OptionMenu、下にComboboxを配置しました。選択肢は月の和名にしてみました。

ドロップダウンボックスのサンプル

ソースコード

 ソースコードは以下の通りです。

import tkinter as tk
import tkinter.ttk as ttk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ドロップダウンサンプル")
        
        months = ["睦月","如月","弥生",
                  "卯月","皐月","水無月",
                  "文月","葉月","長月",
                  "神無月","霜月","師走"]
        
        option_label = tk.Label(self, text="OptionMenu:")
        option_label.grid(row=0, column=0, padx=10, pady=10)

        self.ov = tk.StringVar()
        self.ov.set("弥生")
        self.o = tk.OptionMenu(self, self.ov, *months, command=self.optionmenu_selected)
        self.o.grid(row=0, column=1, padx=10, pady=10)
        
        combo_label = tk.Label(self, text="Combobox:")
        combo_label.grid(row=1, column=0, padx=10, pady=10)

        self.c = ttk.Combobox(self, values=months)
        self.c.grid(row=1, column=1, padx=10, pady=10)
        self.c.current(2)
        self.c.bind("<<ComboboxSelected>>", self.combobox_selected)
        
    def optionmenu_selected(self, event):
        print(event, self.ov.get())

    def combobox_selected(self, event):
        print(self.c.get())

if __name__ == '__main__':
    application = Application()
    application.mainloop()

プログラムの説明

 最初のポイントは19行目のOptionMenu()のインスタンスの作成方法ですね。第一引数のselfはよいとして、第二引数は、テキストの変数を指定します。第三引数以降に選択値を書いていきます。今回は、リストで定義しましたので、アスタリスク「*」を付けることでアンパックしています。commandで値が変更されたときの処理を指定することができます。18行目で初期値をセットしています。

 次のポイントは25行目のCombobox()のインスタンス作成のところでしょう。27行目のself.c.current(2)で、3個目の要素を初期値にしています。値が変更されたときのイベントを定義するために、28行目のself.c.bind("<<ComboboxSelected>>", self.combobox_selected)の中で疑似イベント<<ComboboxSelected>>を使って処理を指定しています。

 現在値の取得は、それぞれ31行目のself.ov.get()と34行目のself.c.get()というふうに記載します。

まとめ

 tkinterはもともとUNIXベースで作られていたのだと思います。OptionMenu()の選択肢は、UNIXユーザになじみのあるユーザーインターフェースですが、tkとttkの関係から、こちらの方が先に作られていたようです。Windowsユーザには、Comboboxの方がなじみがありますよね。これで、いくつかの選択肢があっても大丈夫ですね。

Python tkinter GUI Programing バーコード

 今日も見に来てくださって、ありがとうございます。石川さんです。

 先日、おしごとでバーコードを出力することがあって、ちょっと苦労しました。code128のバーコードを出力しなきゃいけなかったのですけど、このコード、スタートコードがあって、データ部分があって、チェックディジットのコードがあって、ストップコードがある、という風な構造になっていました。これだけ調べて出し方がわかるまでにも結構時間がかかりましたよ~。バーコード、難しいですねぇ。

 もっと簡単にできる方法を調査すべく、Python tkinterではどうやってバーコードを出すのかな、ということで調べました。まったくやり方がわからないので、Python barcodeでグーグル先生に聞いてみました。便利な世の中ですねぇ、すぐにPython-barcodeを使えばよいことがわかりました。

 出力結果を出すまで紆余曲折ありましたが、なんとか出力することができるようになりました。

できあがりイメージ

Barcodeを出力してみました

ソースコード

 ソースコードは以下の通りです。

import barcode
from barcode.writer import ImageWriter
from PIL import ImageTk
import tkinter as tk
import tkinter.filedialog as fd
import tkinter.messagebox as mb

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Tkinter barcode test")
        
        self.label = tk.Label(self, text="Input code128:")
        self.label.grid(row=0, column=0)
        self.barcode = tk.StringVar()
        self.barcode.set('Python tkiner')
        self.entry = tk.Entry(self, textvariable=self.barcode,width=50)
        self.entry.grid(row=0, column=1, columnspan=2)
        self.show_button = tk.Button(self, text="Show a barcode")
        self.show_button.bind("<Button-1>", self.create_image)
        self.show_button.grid(row=1,column=1)
        self.save_button = tk.Button(self, text="Save a barcode image")
        self.save_button.bind("<Button-1>", self.save_image)
        self.save_button.grid(row=1,column=0)
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.grid(row=2,columnspan=3,sticky=tk.NSEW)
        self.update()
        self.create_image(None)
        
    def create_image(self, event):
        data = self.barcode.get()
        if not data:
            return
        try:
            code128 = barcode.get('code128', data, writer=ImageWriter())
        except barcode.errors.IllegalCharacterError as e:
            mb.showerror("Error", e)
            return
        image = code128.render()
        photo = ImageTk.PhotoImage(image)

        self.canvas.delete(tk.ALL)
        self.canvas.create_image(self.canvas.winfo_width()//2,self.canvas.winfo_height()//2,image=photo)
        self.image = photo
        
    def save_image(self, event):
        data = self.barcode.get()
        if not data:
            return
        try:
            code128 = barcode.get('code128', data, writer=ImageWriter())
        except barcode.errors.IllegalCharacterError as e:
            mb.showerror("Error", e)
            self.update_idletasks()
            return
        filename = fd.asksaveasfilename()
        if filename:
            code128.save(filename)
        

if __name__ == '__main__':
    application = Application()
    application.mainloop()

詳細

 Python-barcordは標準モジュールではありませんので、インストールが必要でした。ぼくの環境はAnacondaですので、通常はcondaを使ってインストールするのですが、condaで管理しているところにはセットアップされていなかったので、インストラクションどおり、pipを利用しました。

pip install python-barcode

 インストールは、上記のコマンド一行、これだけです。簡単ですねぇ。一瞬で終了しました。

 インストールが完了しましたので、インストラクションどおりに動作するかどうか確認してみました。よくわからないことにチャレンジするときは、ひとつひとつ動作確認していきます。まずは、説明通り動作するか、書かれてあることを試してみます。そして、自分が実現したいことで利用可能なのかどうか、ちょっとずつ、試していくことになります。地道な作業です。

 今回は、ちょっとずつ試したことは割愛して、ソースコードの説明をしたいと思います。

 まずは、入力されたコードをもとに、バーコードを出力できるようにしたいと思います。バーコード出力先には、キャンバスを使うことにしました。ラベルやボタンでもイメージをセットできるようです。__init__では、これらのtkitnerのウィジェットを組み合わせ枠組みを組み立てています。今回の主ポイントではないので割愛します。

 バーコードのデータは35行目のcode128 = barcode.get('code128', data, writer=ImageWriter())で作成しています。第一引数の’code128’はcode128形式のバーコードを作ることを指定しています。dataは作成されるバーコードのもととなるデータで、これはEntry()で入力されたデータです。15行目で定義したStringVar()のインスタンスを17行目のEntry()インスタンス作成時のパラメータtextvariableへセットすることで、Entry()の内容が変わると自動的にStringVar()インスタンスの値も更新されるようになります。writerを指定しない場合は、svg形式のイメージができるようです。今回はtkinterで利用可能なpngへ変更するためにImageWriter()を指定しました。

 39行目のrender()で作成したイメージデータを出力しています。40行目のphoto = ImageTk.PhotoImage(image)この部分ですが、ImageTK.PhotoImage()は、tkinterで使えるようイメージ変換してくれます。

 で、ポイントは43行目、キャンバスの中央にイメージを作成しています。create_image()メソッドです。指定する座標は作成するイメージの中心点ということだったので、キャンバスの幅と高さを取得してその半分を座標にしました。やっとイメージがキャンバス上に表示されるようになりました。しかし!動かしてみるとわかりますが、これだけではイメージ出力されません。その次の44行目がミソです!イメージをクラスインスタンスに代入しています。これがないと、ローカル変数の値は、ガーベージコレクト、要するにメモリのお掃除対象になってしまって、処理が終わったら消えてしまうのです。最初はこれがなくて、バーコード表示されなかったのですよねぇ。

まとめ

 いや~、今回はがんばりました!バーコード作成は、需要があると思うのですけど、日本語の情報が少ないのですよね。誰かのお役に立つと、うれしいです。

Python tkinter GUI プログラミング Treeview

 今日も見に来てくださって、ありがとうございます。石川さんです。

 今日は、Treeviewについて調べてプログラミングしてみました。Treeviewには二種類の見せ方があって、headingstreeです。それぞれ上下に実装してみました。

できあがりイメージ

Treeviewの実装サンプル(showの二つのモード、上がheadings、下がtree)

ソースコード

import tkinter as tk
import tkinter.ttk as ttk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("ttk Treeview Widget example headings/tree")
        
        lf1 = tk.LabelFrame(self, text="headings")
        lf1.grid(row=0,column=0, padx=10,pady=10)
        headings = {"#1":"名前", "#2":"型", "#3":"サイズ"}
        self.treeview = ttk.Treeview(lf1, columns=headings.keys(), show="headings")
        self.treeview.pack(side=tk.LEFT, fill=tk.BOTH, expand=True,padx=10,pady=10)
        self.scroll = ttk.Scrollbar(lf1, command=self.treeview.yview)
        self.treeview.configure(yscroll=self.scroll.set)
        self.scroll.pack(side=tk.LEFT, fill=tk.BOTH, pady=10)
        
        for key, item in headings.items():
            self.treeview.heading(key, text=item)
            
        self.treeview.column("#1", width=500)
        
        self.treeview.insert("",tk.END, values=("head_information","dict","3"))
        self.treeview.insert("", tk.END, values=("tooltip_text","str","1"))
        
        lf2 = tk.LabelFrame(self, text="tree")
        lf2.grid(row=1,column=0, stick=tk.W, padx=10, pady=10)
        style = ttk.Style(lf2)
        style.configure('Treeview', rowheight=30)
        self.treeview2 = ttk.Treeview(lf2, show="tree")
        self.treeview2.heading("#0", text="tree view version", anchor=tk.W)
        self.treeview2.pack(padx=10, pady=10)
        parent1 = self.treeview2.insert("", tk.END,text="head_information")
        self.treeview2.insert(parent1, tk.END, text="dict")
        self.treeview2.insert(parent1, tk.END, text="3")
        parent2 = self.treeview2.insert("", tk.END, text="tooltip_text")
        self.treeview2.insert(parent2, tk.END, text="str")
        self.treeview2.insert(parent2, tk.END, text="1")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

詳細説明

 9行目と26行目でLabelFrame()を作成しています。それぞれheadings用と、tree用に準備しました。12行目でshow="headings"オプションをつけたTreeviewを作成しています。columnsオプションでカラムを指定しています。

 19行目のheadings()メソッドで、項目のヘッダ部分のテキストを設定しています。21行目でヘッダ項目の幅をセットしています。

 headingsを指定することで、このような表形式でデータを扱うことができます。

 30行目で、show="tree"オプションを付けたTreeviewを作成しています。フォルダ階層などのツリー形式のデータを表現することができます。

まとめ

 headingsオプションを使ったTreeviewは、データベーステーブルからデータを取得して一覧を作成するときなどに、使えそうですね。treeオプションの方は、フォルダ階層以外にもプログラム構成や、組織構造など、幅広く使えそうです。