Python tkinter GUI プログラミング Canvas move3

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

 少しあいて、一週間ぶりの更新ですね。お仕事で、ちょっと忙しくしていたのもあるのですけど、採用活動のページも作ったりしていたので、まあ、仕方ありませんね。ということで今日は、またしてもtkinterのCanvasについて書いていきたいと思います。Canvas上に描かれた要素の移動については、以前にも書いているのですが、今回は、移動するものをクラスとして扱ったらどうなるのか、ということでまとめてみました。

できあがりイメージ

キャンバス上の項目を移動できます

 このプログラムは実行すると、最初にふたつ箱が表示されます。何もないところをクリックすると赤い箱が追加されます。追加された箱はマウスでドラッグすることができます。これまでは、箱の移動についてはメインのクラスで実行していたのですが、今回別のクラスに動作を委譲している、というところが大きく異なる点です。

ソースコード

import tkinter as tk


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas move with class")
        self.geometry("1200x800")

        self.canvas = tk.Canvas(self, background="white")

        self.canvas.grid(row=0, column=0, sticky=tk.NSEW)
        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        self.canvas.bind("<Motion>", self.move)
        self.canvas.bind("<ButtonPress>", self.button_press)
        self.canvas.bind("<ButtonRelease>", self.button_release)

        self.movers = [Movable(self.canvas, 160, 160, "AliceBlue"),
                       Movable(self.canvas, 200, 200, "LightYellow")]

    def move(self, event):
        for m in self.movers:
            m.move(event)

    def button_press(self, event):
        event.consume = False
        for m in self.movers:
            m.button_press(event)
            if event.consume:
                return
        m = Movable(self.canvas, event.x, event.y, "Red")
        self.movers.append(m)
        m.button_press(event)

    def button_release(self, event):
        for m in self.movers:
            m.button_release(event)


class Movable():
    def __init__(self, canvas, x, y, color):
        self.canvas = c = canvas
        self.textid = c.create_text(x, y, text=color)
        self.rectid = c.create_rectangle(c.bbox(self.textid), fill=color)
        c.tag_lower(self.rectid, self.textid)
        self.start = None

    def move(self, event):
        if self.start is None:
            return
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        dx, dy = x - self.start[0], y - self.start[1]
        self.canvas.move(self.textid, dx, dy)
        self.canvas.move(self.rectid, dx, dy)
        self.start = x, y

    def button_press(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        b = self.canvas.bbox(self.rectid)
        if b[0] <= x <= b[2] and b[1] <= y <= b[3]:
            self.start = x, y
            event.consume = True

    def button_release(self, event):
        self.start = None


if __name__ == "__main__":
    app = App()
    app.mainloop()

説明

 初期化処理(__init__)の、6~8行目は、Tkの初期化処理、タイトルとサイズの設定をしています。10行目で背景が白色のCanvasを作成して、12~14行目でgridを使用して配置しています。そして、16~18行目でCanvasに対するイベントをメソッドに割り当てています。20行目は、初期値として、Movableクラスを二つ追加しています。

 イベントに割り当てたメソッドはそれぞれ追加したMovableクラスの同名のメソッドを実行するように記述してあります。button_pressだけちょっと変更していて、どのMovableクラスのインスタンスも対応する処理を実行しなかったとき、要するに、クリックしたのが箱じゃなかったとき、赤い箱を追加するようにしました。

 Movableクラスの初期化処理では、渡されたキャンバスのx,yに色の名前のテキストをcreate_text()メソッドを使って描画します。その後、そのテキストを囲む箱をcreate_rectangle()メソッドで描画します。このままだと、テキストの上に箱が描画されて字が見えないので、tag_lower()メソッドを呼び出すことで上下を入れ替えてテキストが表示されるようにしています。あと、移動開始用のポジションを保持するための変数を初期化しています。

 50~57行目もMovableクラスのmove()メソッドは、self.startの位置からeventが発生させた位置へ移動させます。self.startがセットされていない場合は特に動作しません。このmove()メソッドが動作するために、59~64行目のbutton_press()メソッドでは、自分自身が選択された場合に限り、self.startで開始位置をセットします。箱がクリックされたかどうかを判定するために、event.consumeを利用するようにしています。

 66~67行目はbutton_release()では、マウスボタンが離されたときは箱が動かないように、self.startにNoneをセットしています。

 もっといいやり方があるかも知れませんが、これで責任範囲を何とか分離することができました。他に、クリックされたら選択されたような枠の印をつけるとか、その枠を使って大きさを拡大縮小するとか、ダブルクリックしたときにメニューを出すとか、必要に応じてMovableクラスへ実装することができるようになりました。

まとめ

 キャンバス上の項目をひとつのクラスの中でまとめて一度に扱うことができるようになりました。いずれは、MVCパターンを検討してみたいと思います。

Python tkinter GUIプログラミング ログインダイアログ

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

 今回は、ログインダイアログを作ってみました。ログインダイアログは別ウィンドウなので、値の引き渡し方法をどうするのか、というのがポイントになります。

出来上がりイメージ

ログインダイアログ

ソースコード

import tkinter as tk
from tkinter.simpledialog import Dialog

class LoginDialog(Dialog):
    def __init__(self, root):
        super().__init__(root, title="Login")
        
    def body(self, master):
        self.username = tk.StringVar()
        self.password = tk.StringVar()
        userLabel = tk.Label(master, text="User:")
        userLabel.grid(row=0, column=0)
        username = tk.Entry(master, textvariable=self.username)
        username.grid(row=0, column=1)
        passwdLabel = tk.Label(master, text="Pass:")
        passwdLabel.grid(row=1, column=0)
        password = tk.Entry(master, show="*", textvariable=self.password)
        password.grid(row=1, column=1)
        self.information = tk.Label(master)
        self.information.grid(row=2,columnspan=2)
        
    def validate(self):
        self.information.configure(text="")
        if not self.username.get():
            self.information.configure(text="Input username")
            return False
        if not self.password.get():
            self.information.configure(text="Input password")
            return False
        return True

    def apply(self):
        self.result = self.username.get(), self.password.get()

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Login dialogue")
        self.geometry("400x100")
        
        show = tk.Button(self, text="login", command=self.show)
        close = tk.Button(self, text="close", command=self.destroy)
        show.grid(row=0, column=0, sticky=tk.EW, ipadx=20, padx=40, pady=20)
        close.grid(row=0, column=1, sticky=tk.EW, ipadx=20, padx=40, pady=20)
    
    def show(self):
        ret = LoginDialog(self)
        print(ret.result)

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

説明

 ログインダイアログは、tkinter.simpledialogのDialogを継承して作成しました。4~33行目です。ログインダイアログの中身は、body()メソッドをオーバーライドして実装します。ユーザー名とパスワードとメッセージ出力用のラベルを追加しました。もし、OKとCancel以外のボタンを使いたいときは、buttonbox()メソッドをオーバーライドしますが、今回はこれで充分でしょう。

 OKボタンを押したときに、値をチェックするには、validate()メソッドをオーバーライドします。今回は、22~30行目に記載があります。ユーザー名かパスワードが入力されていないとき、Falseを戻します。成功時はTrueを戻すようにしました。30行目のTrueを戻すことを忘れると、PythonはデフォルトでNoneを戻す約束ですので、OK押してもその後続処理のapply()は実行されませんので、注意が必要です。

 OKボタンを押したときの処理は、apply()メソッドをオーバーライドします。今回は、33行目で、ダイアログボックスに入力した値をself.resultへセットしています。self.resultは、初期化時にNoneをセットされていますので、Cancel押されたかどうか、この値を判定すればわかりますね。

 呼び出し方法は、47行目のようにします。戻り値の判定は48行目のret.resultをチェックすれば完璧です。Noneのときは、キャンセルされています。

まとめ

 ログインダイアログを作成することを通して、簡単なダイアログを作成するための基本を学びました。これで、いろいろなダイアログ、作成できそうですね。

Python tkinter GUIプログラミング 透明なキャンバス

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

 先日、透明なウィンドウをつくったところで、ひらめきました。透明なキャンバスのウィンドウをつくって、それを別のウィンドウに重ねるのは、どうでしょうか。と、いうことで試しにやってみました。ちょっと実用には遠いですが、アイディアとして記録しておきます。

出来上がりイメージ

透明なキャンバスを仮想的に実現しました

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Transparent Canvas Challenge!")
        self.geometry("500x300")

        self.top = tk.Toplevel()
        self.top.wm_attributes("-topmost", True)
        self.top.overrideredirect(True)
        self.top.geometry("500x300")
        self.top.forward = tk.Canvas(self.top, background="white")
        self.top.forward.pack(fill=tk.BOTH, expand=True)
        self.top.forward.create_oval(50,50,450,250,fill="lightblue")
        self.top.wm_attributes("-transparentcolor", "white")
        
        self.back = tk.Canvas(self, background="white")
        self.back.pack(fill=tk.BOTH, expand=True)
        self.back.create_rectangle(50,50,450,250)
        self.back.create_rectangle(100,100,400,200)
        self.bind('<Configure>', self.change)
        self.back.bind("<Unmap>", self.unmap)
        self.back.bind("<Map>", self.map)
        self.bind("<1>", self.draw1)
        self.bind("<3>", self.draw3)
        self.top.bind("<1>", self.draw1)
        self.top.bind("<3>", self.draw3)
        
    def draw1(self, event):
        self.top.forward.create_oval(event.x, event.y, event.x + 30, event.y + 30)
        
    def draw3(self, event):
        self.back.create_rectangle(event.x, event.y, event.x + 30, event.y + 30)

    def unmap(self, event):
        self.top.withdraw()

    def map(self, event):
        self.lift()
        self.top.wm_deiconify()
        self.top.attributes("-topmost", True)
        
    def change(self, event):
        x, y = self.back.winfo_rootx(), self.back.winfo_rooty()
        w, h = self.winfo_width(), self.winfo_height()
        self.top.geometry(f"{w}x{h}+{x}+{y}")
        
if __name__ == "__main__":
    application = Application()
    application.mainloop()

説明

 まずは、背景になるキャンバスをつくります。そして、前面に来る透明なキャンバスをつくるのですが、そのときに、Toplevelのウィンドウをつくって、このウィンドウ用のキャンバスとします。そして、Toplvelの方に透明となる設定を施す、というのがおおまかな流れです。

 9行目でToplevelのウィンドウをつくっています。10行目「self.top.wm_attributes(“-topmost”, True)」は、割とポイントになりますが、常に一番上のウィンドウとなるようにする設定です。これで、透明なキャンバスが常に上に表示されるようになります。

 11行目、「self.top.overrideredirect(True)」では、ウィンドウのタイトル部分を消去しています。これでアイコン化や閉じるボタンなども使えなくなります。

 13行目、14行目でキャンバスをつくって、配置しています。キャンバスの背景色は、「”white”(白)」にセットしていますが、これが16行目の「self.top.wm_attributes(“-transparentcolor”, “white”)」のところで効いてきます。ここで-transparentcolorは透明にする色で、「”white”(白)」を指定しています。こうすることでキャンバスの背景色の白が透明になります。

 23行目から、いくつかイベントをバインドしています。最初のイベントは<Configure>です。ウィンドウのサイズなどが変更されたときに発生します。ウィンドウサイズが変更されたときに、透明なキャンバスのサイズも同時に変更するために指定しました。<Unmap>と<Map>はウィンドウがアイコン化から戻されたときとアイコン化されたときに発生します。こちらもウィンドウを閉じたときに透明なキャンバスのウィンドウも閉じるために指定してあります。

 25~28行目は左クリックされたときと右クリックされたときにキャンバス上に丸と四角を描くためにセットしました。ちゃんと透明なキャンバスが上になっていることを確認するためにおまけで付けてみました。

 37行目の「self.top.withdraw()」は、ウィンドウを非表示にします。40行目の「self.lift()」は、ウィンドウを上に移動します。41行目「self.top.wm_deiconify()」でアイコン化から復帰し、42行目「self.top.attributes(“-topmost”, True)」で常に一番上のウィンドウとなるように再設定しています。

 44~47行目のchangeメソッドでは、透明なキャンバスのウィンドウのサイズを合わせています。

まとめ

 ウィンドウをふたつつくって、下のウィンドウにあわせて上のウィンドウが移動したりサイズ変更したり、ということで、透明なキャンバスを実現してみました。思ったとおりに動作させるのは、けっこう難しいですね。

 

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

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オプションの方は、フォルダ階層以外にもプログラム構成や、組織構造など、幅広く使えそうです。