Python tkinter GUIプログラミング 透明なウィンドウ

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

 透明なキャンバスを重ねるというアイディアを実現しようといろいろ調べまわりましたが、なかなか良いものが見つかりません。その中で見つけたちょっと面白い技を紹介します。透明なウィンドウです。

出来上がりイメージ

背景色が透明なウィンドウ

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Transparent window")
        self.geometry("500x300")
        self.config(bg="white")
        self.attributes("-transparentcolor", "white")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 ポイントはただ一つ、9行目の「self.attributes(“-transparentcolor”, “white”)」です。これで透明になる色をセットしています。この場合、白(”white”)ですね。8行目でbg(背景色)に白を設定しています。この色が透明になる、ということで画像のようなウィンドウが出来上がります。

まとめ

 背景を透明にすることで、変わった形のウィンドウを作ったり、特殊な効果を実現することができそうですね。ちなみに、Raspbianではうまく動作しない、と、言われていました。

Python プログラミング sqlite3を使ってみる

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

 Pythonからデータを保持するための方法のひとつ、sqlite3を利用してみます。sqlite3はPythonの標準ライブラリに含まれていますので、特別なインストールなど、不要です。それでいて、SQLが使えて、テーブルを作成してデータを簡単に保存したいというニーズに充分応えてくれます。

 ぼくは普段のおしごとでは、Oracleを専門に扱っているのですけど、他のデータベースも知っておいた方がよいでしょう、ということで、ちょっぴりチャレンジしてみました。データベースをつくって、テーブルをつくって、データを入れて、検索する、という一連の流れを実行してみました。

 ちなみに、sqlite3は、別のサーバプロセスを用意する必要はありません。その名のとおり軽量で、ディスク上のデータベースを提供してくれます。

ソースコード

import sqlite3

conn = sqlite3.connect('test.db')

curs = conn.cursor()

curs.execute("SELECT tbl_name FROM sqlite_master WHERE type = 'table'")
tables = [table[0] for table in curs.fetchall()]

if "BOOKS" in tables:
    curs.execute("DROP TABLE BOOKS")
curs.execute("CREATE TABLE BOOKS ( ID NUMBER PRIMARY KEY, TITLE VARCHAR(30), PUBLISHED DATE)")
curs.execute("INSERT INTO BOOKS (ID, TITLE, PUBLISHED) VALUES"
             " (1, '入門Python3', '2015-12-01')"
             ",(2, '実践Python3', '2015-12-01')"
             ",(3, 'Fluent Python', '2017-10-11')"
             ",(4, 'Effective Python', '2016-01-22')"
            )
conn.commit()
curs.execute("SELECT * FROM BOOKS")
rows = curs.fetchall()
print(rows)

説明

 1行目、ライブラリ名はsqlite3です。利用できるようにするため、importします。

 3行目、sqlite3のデータベースを使えるようにするためには、まずConnectionオブジェクトを作る必要があります。データは「test.db」ファイルに格納されます。「:memory:」という特殊な名前を指定すると、RAM上にデータベースが作れるそうです。ちなみに、ファイルが存在しなくてもエラーにならず、勝手にファイルが作成されます。

 5行目、データベースへの操作には、必ずカーソルが必要です。ということで、cursor()メソッドでカーソルを作成します。

 このデータベースの中に、「BOOKS」テーブルを作成しようと思ったのですけど、既にこの「BOOKS」テーブルが存在した場合はいったん削除してから作成しましょう。7行目で指定した「sqlite_master」テーブルには、このデータベースの中に入っているシステムテーブルで、作られたオブジェクトの情報が入力されています。

 7行目でSELECT文を定義して、8行目でデータを取り出しています。データはタプルで取り出されるので、リスト内包表記で一つ目の項目だけを取り出しています。10行目で比較するためです。1回目はテーブルが存在しないはずですので、12行目のテーブル削除は実行されず、12行目のCREATE TABLE文でテーブルをつくることから始めます。

 13行目のINSERT文でデータを投入しています。今回は、1行で4件データを作成しています。sqlite3はINSERT一文で複数件投入できるのですね、便利です。

 19行目、commit()です。これで、データが確定されました。

 20行目以降、SELECT文でデータを取得しています。

まとめ

 sqlie3、簡単ですね。ほとんど悩むことなく使えるようになりました。さすが標準モジュールです。

Python tkinter GUIプログラミング Canvas項目の前後切り替え

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

 EclipseのGraphical Editing Framework(GEF)には透明なキャンバスがあって、背面にConnection layer、前面にPrimary layerを配置することで、コネクションをうまくあつかう工夫をしています。tkinterでも同じことができないかといろいろと調べていたのですが、ちょっと難しそうです。

 レイヤーが変えられないので、項目を前後に移動して重なりを制御するしかありませんね。と、いうときに使えるのが、tag_raise()、tag_lower()です。

できあがりイメージ

初期画面
クリックで長方形が上にきます

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("600x400")
        self.title("canvas tag_raise")
        
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        
        self.r = self.canvas.create_rectangle(90,40,510,360,fill="lightblue")
        self.o = self.canvas.create_oval(30,30,560,370,fill="lightyellow")
        
        self.item = "r"
        
        self.canvas.bind("<ButtonPress>", self.clicked)
        
    def clicked(self, event):
        if self.item == "r":
            self.canvas.tag_raise(self.r)
            self.item = "o"
        else:
            self.canvas.tag_raise(self.o)
            self.item = "r"
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 12行目、13行目でcreate_rectangle()、create_oval()でキャンバス上に四角形と楕円を描画します。このときの戻り値はidで、キャンバス上に描画した項目を管理する番号です。17行目で、クリックしたときに実行するメソッドをバインドしています。

 21行目と24行目のtag_raise()で指定されたidの項目を上に移動させます。

まとめ

 tag_raise()とtag_lower()を使って、項目をz軸方向に移動させることができます。ただし、一番上と一番下しか指定できませんので、複数項目があって、重なる順番が必要な時は、工夫が必要になりそうですね。

Python tkinter GUIプログラミング フレームの切り替え

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

 今日は、キャンバスの切り替えのやり方です。透明なキャンバスを二枚重ねる方法はないかと調べていたのですが、見つからず、かわりにフレームを切り替える方法を見つけましたので、紹介します。

できあがりイメージ

 初期表示画面は以下のように三角形を描いてあります。

初期表示画面

 キャンバスをクリックすると次のように切り替わります。

キャンバスをクリックすると切り替わります。

ソースコード

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("test")
        self.frames = {}
        
        self.frames["top"] = top = tk.Frame(self,width=300,height=200)
        topcanvas = tk.Canvas(top)
        topcanvas.place(x=0, y=0)
        topcanvas.create_oval(30,30,270,170,outline="blue",fill="white")
        top.grid(row=0,column=0)

        self.frames["bottom"] = bottom = tk.Frame(self,width=300,height=200)
        bottomcanvas = tk.Canvas(bottom)
        bottomcanvas.place(x=0, y=0)
        bottomcanvas.create_polygon(150,30,30,170,270,170,fill="lightgrey")
        bottom.grid(row=0,column=0)

        self.bind("<ButtonPress>", self.clicked)
        self.frame = "top"

    def clicked(self, event):
        self.frames[self.frame].tkraise()
        if self.frame == "top":
            self.frame = "bottom"
        else:
            self.frame = "top"
        
if __name__ == "__main__":
    app = App()
    app.mainloop()

説明

 クリックしたら描かれたキャンバスが切り替わります。正確には、フレームが切り替わります。まずは、アプリケーション作成、いつものようにtkinterのTkを継承してクラスを開始しています。__init__メソッドでは、最初にsuper().__init__()と記述して、親クラスTkの__init__()初期化メソッドを呼び出しています。その後、タイトルをセットして、self.framesをディクショナリとして初期化しています。

9行目でフレームを作成、10行目でそのフレーム上にキャンバスを作成します。11行目でplaceを使って配置しています。12行目でキャンバスへ楕円を描画しています。13行目でフレームをgridを使って配置しています。作ったフレームはディクショナリにセットしています。

 15~19行目、同様にフレームとキャンバスを作成、配置しています。21行目、クリックされたときに呼び出すメソッドをセット、作ったフレームはディクショナリにセットしています。

 25行目、ここが実際のフレームの切り替えを実行している箇所です。ディクショナリに登録したフレームのtkraise()メソッドを呼び出して、フレームを最上位に移動しています。

 フレームをたくさん作って、タイマーでパカパカ切り替えたら、アニメーションが作れそうですね。

まとめ

 フレームの切り替え、やり方さえ知っていれば、簡単ですね。キャンバスを二枚重ねて表示する方法は、いまだ不明です。どなたか知っている方がいらっしゃれば、教えてください。

Pythonディクショナリ(辞書)

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

 今日は、Pythonのディクショナリを少し紹介します。ディクショナリはキーと値のペアを格納できるコレクションです。はじめてこのディクショナリを知った時は、便利すぎて衝撃でした。

作成方法と使い方

In [1]: d = { "A": "Apple", "B": "Boy", "C": "Color" }

In [2]: d
Out[2]: {'A': 'Apple', 'B': 'Boy', 'C': 'Color'}

 ディクショナリを作るには、上記のようにキー:値のペアをカンマで区切って並べ、波括弧「{}」で囲みます。それぞれの要素を取り出すには、キーを以下のように指定していきます。

In [3]: d["A"]
Out[3]: 'Apple'

In [4]: d["B"]
Out[4]: 'Boy'

In [5]: d["C"]
Out[5]: 'Color'

 キーだけを取り出す、値だけを取り出す、キーと値のペアを取り出すには以下のように記述します。

In [6]: d.keys()
Out[6]: dict_keys(['A', 'B', 'C'])

In [7]: d.values()
Out[7]: dict_values(['Apple', 'Boy', 'Color'])

In [8]: d.items()
Out[8]: dict_items([('A', 'Apple'), ('B', 'Boy'), ('C', 'Color')])

 ちなみに、存在しないキーを指定すると、エラーになります。

In [9]: d['Z']
Traceback (most recent call last):

  File "<ipython-input-168-b7ca268d4341>", line 1, in <module>
    d['Z']

KeyError: 'Z'

 このキーが含まれているかどうかは、inを使って確認することができます。

In [10]: 'A' in d
Out[10]: True

In [11]: 'Z' in d
Out[11]: False

 値を変更するには、以下のようにします。

In [12]: d['A'] = 'Air'

In [13]: d
Out[13]: {'A': 'Air', 'B': 'Boy', 'C': 'Color'}

 存在しないキーを使用すると、要素が追加されます。

In [14]: d['D'] = 'Dictionary'

In [15]: d
Out[15]: {'A': 'Air', 'B': 'Boy', 'C': 'Color', 'D': 'Dictionary'}

 要素を削除するには以下のようにします。

In [16]: del d['C']

In [17]: d
Out[17]: {'A': 'Air', 'B': 'Boy', 'D': 'Dictionary'}

 すべての値を削除するには、以下のようにします。

In [18]: d.clear()

In [19]: d
Out[19]: {}

 ここで、「{}」が出てまいりましたが。空のディクショナリを作成するときにも「{}」が使えます。なので、すべての値を削除するもう一つのやり方として、空辞書「{}」をディクショナリ名に代入することで実現できます。

 ほかのディクショナリの作成方法としては、dict()が使えます。

In [20]: dict()
Out[20]: {}

In [21]: dict(['ab','cd','ef')
Out[21]: {'a': 'b', 'c': 'd', 'e': 'f'}

In [22]: dict((['a','b'],['c','d'],['e','f']))
Out[22]: {'a': 'b', 'c': 'd', 'e': 'f'}

 この他にも、タプルやリストの組み合わせが利用可能です。

 値を取り出す他のやり方としては、get()があります。

In [23]: d.get('A')

In [24]: print(d.get('A'))
None

In [25]: d.get('A','a')
Out[25]: 'a'

 最初の例は、キー「’A’」の値を取り出していますが、存在しないため、Noneを戻しています。インタープリターでは何も表示されないため、print()を使って確認しています。その次の例は、値がなかった時のオプションとして「’a’」を指定しています。

まとめ

 ディクショナリは、とても便利ですね。連続で取り出すときの順番は保証されませんので、そこだけ注意が必要でしょうか。(順番を保証したい場合はOrderedDict()が利用できます。)

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の扱いに慣れるのには、ちょっと時間がかかりそうですね。またサンプル作ってみますね。

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のサブドメインにする方法とか、どうするのでしょうね。また機会があれば調べてみたいと思います。

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の方がなじみがありますよね。これで、いくつかの選択肢があっても大丈夫ですね。