Python tkinter GUIプログラミング Entry概要

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

 Tkinterで業務アプリケーション作るときに絶対避けて通れない、Entryについてちょっとまとめたいと思います。Entryは一行だけデータを入力できるフィールドを作るためのウィジェットです。データが入力できるだけだよねぇ、と、思っていたのですが、意外と便利な機能が用意されているようです。

実行イメージ

 普通にEntryを何の飾りもなく使うとこんな感じになります。

Entry実行イメージ

ソースコード

 もっともシンプルなEntryの使い方は、以下の通りです。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Entry Test")
        self.entry = tk.Entry()
        self.entry.pack()
    
if __name__ == "__main__":
    app = App()
    app.mainloop()

使い方

 普通にテキストを入力することができます。

abcdefgを入力してみました。

 入力されたテキストを取得するには、self.entry.get()のように、getメソッドで入力されたテキストを取得することができます。

 入力時のチェックは、validate、validatecommandで実装可能です。例えば入力文字数を6文字に限定するにはスクリプトを以下のように書き換えます。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Entry Test")
        vc = (self.register(self.validate_command), "%P")
        self.entry = tk.Entry(validate="key", validatecommand=vc)
        self.entry.pack()
        
    def validate_command(self, string):
        return len(string) <= 6
                
if __name__ == "__main__":
    app = App()
    app.mainloop()

 7行目でコマンドを登録して8行目のEntry定義時にvalidatevalidatecommandをセットしています。validateには、ここにあるように、”key”以外にも、”none”、”focusin”、”focusout”、”focus”、”all”が指定できるようです。ちなみに”focus”は、”focusin”と”focusout”の両方で、”all”は、”key”と”focusin”、”focusout”と同じだそうです。指定することで、いずれかのトリガーでコマンドが実行される、ということになります。

 ”%P”を指定しているところは、以下の指定が可能でそれぞれの意味を記載しておきます。

コード渡される値
%d1は挿入、0は削除、その他のイベントは-1
%i挿入または削除される文字列のインデックス
%P変更後に想定されるフィールドの値
%s編集前のエントリの現在値
%S挿入または削除されるテキスト文字列
%v現在設定されている検証のタイプ(focusin、focusout、key、forced)
%Vコールバックした検証のタイプ(focusin、focusout、key、forced)
%Wウィジェットの名前(entry!)
指定できるコードの種類

まとめ

 今回紹介した、validateとvalidatecommandを利用することで簡単な入力制限を実現できそうですね。

Tkinter GUIプログラミング レイアウトマネージャpack

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

 Tkinterのレイアウトマネージャは三種類あるのですけど、その中のpackがちょっとだけよくわからなかったので、いろいろと動作確認をしてみましたのでまとめます。お役に立てればうれしいです。

 packというくらいなので、箱詰めするんだよねぇ、というところまではイメージ通りなのですけど、箱詰めの方法がよくわからんなぁ、ということで、packのソースファイルまでさかのぼって確認してみました。コメントには、以下のようになってました。

Pack a widget in the parent widget. Use as options:
after=widget - pack it after you have packed widget
anchor=NSEW (or subset) - position widget according to given direction
before=widget - pack it before you will pack widget
expand=bool - expand widget if parent size grows
fill=NONE or X or Y or BOTH - fill widget if widget grows
in=master - use master to contain this widget
in_=master - see 'in' option description
ipadx=amount - add internal padding in x direction
ipady=amount - add internal padding in y direction
padx=amount - add padding in x direction
pady=amount - add padding in y direction
side=TOP or BOTTOM or LEFT or RIGHT -  where to add this widget.

 このsideパラメータで位置を決めていますので、組み合わせでどうなるか、ちょっとやってみました。

ソースコード

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

import tkinter as tk
import itertools as it

SIDES = [tk.TOP,tk.BOTTOM,tk.LEFT,tk.RIGHT]
COLORS = ["red","green","blue","yellow","gray"]

class Test(tk.Toplevel):
    def __init__(self, master, i, sides):
        super().__init__(master)
        self.title(str(i))
        self.geometry("140x120")
        for i, l in enumerate(sides):
            label = tk.Label(self,text=l, bg=COLORS[i])
            label.pack(side=l)
        
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Pack test")

        for i, s in enumerate(it.combinations_with_replacement(SIDES,len(COLORS))):
            print(i,s)
            t = Test(self, i, s)
            t.geometry("+"+str(10 + (i%10)*140)+"+"+str(10 + (i//10)*130))

        self.geometry("300x30+900+700")
        
if __name__ == "__main__":
    app = App()
    app.mainloop()

実行結果

 上のソースコードの実行結果です。

実行結果

解説

 どこへウィジェットを加えるかを決めるパラメータの、sideですが、TOPBOTTOMLEFTRIGHTの四種類の位置をセットすることができます。この組み合わせでどうなるか確認したかったので、itertoolscombinations_with_replacementを使って組み合わせを実現することにしました。このcombinations_with_replacementは、指定された長さのタプル列を重複ありのソートされた順で戻してくれます。

 このスクリプトでは、クラスを二つ定義しています。一つは、Tkを継承した本体のクラスで、もう一つは新たなウィンドウを作成するためのToplevelを継承したTestクラスです。このTestクラスでsideをいろいろと設定してみて試してみました。

 順番は、赤、緑、青、黄色、グレーの順で追加されています。afterbeforeを指定すれば順番が入れ替わるようですが、今回は追加した順番になっています。面白いな、と、思ったのは、RIGHTBOTTOMを指定した順番が右から順番、下から順番という風になっているところです。何となく、上から順番、左から順番と思っていたので、おやっ、と、思いました。

まとめ

 要素が多くないときや、単純な並びの時は、packが便利に使えそうですね。

Python GUI プログラミングを教えています「その3」

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

 Python GUIプログラミング教室、先日はウィンドウを出して、サイズを変更して、背景色を変更するところまでやりました。次は、どうしようかなぁ、と、思っていましたが、やっぱりゲームなら、キャンバスが使えないとダメでしょう。と、いうことで、Canvasを作ってみましょう。

 とりあえず、ウィンドウを出すテンプレートとして、以下のスクリプトを覚えてもらいました。

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
# ここに色々な設定を書いていきます。
        
        self.mainloop()

if __name__ == "__main__":
    Application()

 サンプルとして、キャンバスを作ってみましょう。

        self.canvas = tk.Canvas()
        self.canvas.pack()

 追加しましたが、大きさが若干変化したくらいで、あんまり変わりませんね。次に線を引いてみましょう。

        self.canvas.create_line(10,10,200,200)
線が引けました

 キャンバスは、左上に原点の(0,0)があって、x軸が右、y軸が下に行くほど数値が増えていく構造です。なので上記の一行は、左上の(10,10)の座標から右下の(200,200)へ線を引く、という命令になります。ここまで教えて、さあ、自分でも好きな線を引いてみて、と、促したところ、下の二行を追加してくれました。

        self.canvas.create_line(100,100,200,200)
        self.canvas.create_line(65,65,150,150)

 当然ながら、実行しても線は増えませんね。追加した線は、もともと引いてあった線の上を引き直しているだけですからねぇ。

 次に、fill=”white”とか”red”で色が付けられることと、create_rectangleで四角形、create_ovalで楕円が書けることを教えてあげて、遊んでもらいました。最終的に何かができた、と、言っていたのですが、出来上がりは、こんな感じでした!

日の丸!

 そして、そこで終わらずに、さらに欲張って、キーボードを押されたイベントで押されたキーを出力するスクリプトを追加しました。

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        self.canvas = tk.Canvas()
        self.canvas.pack()
        self.canvas.create_rectangle(10,50,200,160,fill="white")
        self.canvas.create_oval(80,80,130,130,fill="red")

        self.bind("<KeyPress>",self.print_key)
        
        self.mainloop()

    def print_key(self, event):
        print(event.keysym)
        
if __name__ == "__main__":
    Application()

12行目のbind"<KeyPress>"イベントに、16行目のself.print_keyメソッドを割り当てています。つまり、何かのキーが押されたよ、というイベントが発生したら、self.print_keyメソッドが実行されるようになります。print_keyの中で、event.keysymprintしています。押されたキーの内容によって、event.keysymの内容が変化します。適当にキーを押すと、出力結果は以下のようになりました。

space
a
b
c
Return
BackSpace
F1
F2
Down
Left
Up
Right
1
2
3
4
Shift_L
exclam

 この結果から、矢印キーが押されたときに何かさせようとすると、「Down」「Left」「Up」「Right」を判定すればうまくいきそうです。

 さあ、次は、、、と、思っていたら、もう疲れた~、と、またしても逃げられてしまいました。ねぇ、ホントはプログラミングしたくないんじゃないの?
 む~、それか、教え方がよくないのかなぁ。。。

Python tkinter 事前定義の色名 Windows編

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

 今回は、tkinterの事前定義の色名にはどんなものがあるのか気になったので作りました。もちろんTcl/Tk本家のホームページのこちらを参考にさせていただきました。実行すると、以下のようなウィンドウが出力されます。

tkinter Windowsでの事前定義の色名

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

import tkinter as tk

COLORS = [
"system3dDarkShadow", "systemHighlight", "system3dLight", "systemHighlightText", "systemActiveBorder", "systemInactiveBorder", "systemActiveCaption", "systemInactiveCaption", "systemAppWorkspace", "systemInactiveCaptionText",
"systemBackground", "systemInfoBackground", "systemButtonFace", "systemInfoText", "systemButtonHighlight", "systemMenu", "systemButtonShadow", "systemMenuText", "systemButtonText", "systemScrollbar",
"systemCaptionText", "systemWindow", "systemDisabledText", "systemWindowFrame", "systemGrayText", "systemWindowText",
] 

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        for i, c in enumerate(COLORS):
            foreground = "SystemButtonText"
            if self.winfo_rgb(c) <= (10,10,10):
                foreground = "white"
            label = tk.Label(self, text=c, background=c, foreground=foreground)
            label.bind("<Enter>", self.show_color_info)
            label.grid(row=i//5, column=i%5, sticky=(tk.W+tk.E), padx=1, pady=1)
            
    def show_color_info(self, event):
        color = event.widget.cget("text")
        rgb = event.widget.winfo_rgb(color)
        rgbstring = "#%02X%02X%02X"%(rgb[0]//256,rgb[1]//256,rgb[2]//256)
        self.title("You are pointing ["+color+"] and background = "+rgbstring+"]")

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

 COLORSの内容以外、前回とほとんど変わりませんね。数が減ったので、幅の指定をなくしたのと、色が暗いときには白い字で色名が出力されるようにしました。Mac用の色名も多数あったのでスクリプトを作ったのですけど、ぼくはMacを持っていなくて実行できなかったので、お蔵入りとなりました。

Python tkinter 事前定義の色について

 今日も見に来てくださってありがとうございます。こんな時期なのに、今日はなんと雪が降りましたね。

 先日、tkinterの事前定義の色ってどんなのがあるのかなぁ、と、思って本家のホームページまでたどり着きましたが、どんな色かわからないので、一覧を作ってみようと思いました。なんと、OS依存しない名称は760色、多いなぁ。20×38でちょっと作ってみよう。

 出来上がりイメージは以下の通りです。

上記の画面を出力するためのスクリプトは以下の通りです。

import tkinter as tk

COLORS=[
"alice blue", "AliceBlue", "antique white", "AntiqueWhite", "AntiqueWhite1", "AntiqueWhite2", "AntiqueWhite3", "AntiqueWhite4", "aqua", "aquamarine",
"aquamarine1", "aquamarine2", "aquamarine3", "aquamarine4", "azure", "azure1", "azure2", "azure3", "azure4", "beige",
"bisque", "bisque1", "bisque2", "bisque3", "bisque4", "black", "blanched almond", "BlanchedAlmond", "blue", "blue violet",
"blue1", "blue2", "blue3", "blue4", "BlueViolet", "brown", "brown1", "brown2", "brown3", "brown4",
"burlywood", "burlywood1", "burlywood2", "burlywood3", "burlywood4", "cadet blue", "CadetBlue", "CadetBlue1", "CadetBlue2", "CadetBlue3",
"CadetBlue4", "chartreuse", "chartreuse1", "chartreuse2", "chartreuse3", "chartreuse4", "chocolate", "chocolate1", "chocolate2", "chocolate3",
"chocolate4", "coral", "coral1", "coral2", "coral3", "coral4", "cornflower blue", "CornflowerBlue", "cornsilk", "cornsilk1",
"cornsilk2", "cornsilk3", "cornsilk4", "crimson", "cyan", "cyan1", "cyan2", "cyan3", "cyan4", "dark blue",
"dark cyan", "dark goldenrod", "dark gray", "dark green", "dark grey", "dark khaki", "dark magenta", "dark olive green", "dark orange", "dark orchid",
"dark red", "dark salmon", "dark sea green", "dark slate blue", "dark slate gray", "dark slate grey", "dark turquoise", "dark violet", "DarkBlue", "DarkCyan",
"DarkGoldenrod", "DarkGoldenrod1", "DarkGoldenrod2", "DarkGoldenrod3", "DarkGoldenrod4", "DarkGray", "DarkGreen", "DarkGrey", "DarkKhaki", "DarkMagenta",
"DarkOliveGreen", "DarkOliveGreen1", "DarkOliveGreen2", "DarkOliveGreen3", "DarkOliveGreen4", "DarkOrange", "DarkOrange1", "DarkOrange2", "DarkOrange3", "DarkOrange4",
"DarkOrchid", "DarkOrchid1", "DarkOrchid2", "DarkOrchid3", "DarkOrchid4", "DarkRed", "DarkSalmon", "DarkSeaGreen", "DarkSeaGreen1", "DarkSeaGreen2",
"DarkSeaGreen3", "DarkSeaGreen4", "DarkSlateBlue", "DarkSlateGray", "DarkSlateGray1", "DarkSlateGray2", "DarkSlateGray3", "DarkSlateGray4", "DarkSlateGrey", "DarkTurquoise",
"DarkViolet", "deep pink", "deep sky blue", "DeepPink", "DeepPink1", "DeepPink2", "DeepPink3", "DeepPink4", "DeepSkyBlue", "DeepSkyBlue1",
"DeepSkyBlue2", "DeepSkyBlue3", "DeepSkyBlue4", "dim gray", "dim grey", "DimGray", "DimGrey", "dodger blue", "DodgerBlue", "DodgerBlue1",
"DodgerBlue2", "DodgerBlue3", "DodgerBlue4", "firebrick", "firebrick1", "firebrick2", "firebrick3", "firebrick4", "floral white", "FloralWhite",
"forest green", "ForestGreen", "fuchsia", "gainsboro", "ghost white", "GhostWhite", "gold", "gold1", "gold2", "gold3",
"gold4", "goldenrod", "goldenrod1", "goldenrod2", "goldenrod3", "goldenrod4", "gray", "gray0", "gray1", "gray2",
"gray3", "gray4", "gray5", "gray6", "gray7", "gray8", "gray9", "gray10", "gray11", "gray12",
"gray13", "gray14", "gray15", "gray16", "gray17", "gray18", "gray19", "gray20", "gray21", "gray22",
"gray23", "gray24", "gray25", "gray26", "gray27", "gray28", "gray29", "gray30", "gray31", "gray32",
"gray33", "gray34", "gray35", "gray36", "gray37", "gray38", "gray39", "gray40", "gray41", "gray42",
"gray43", "gray44", "gray45", "gray46", "gray47", "gray48", "gray49", "gray50", "gray51", "gray52",
"gray53", "gray54", "gray55", "gray56", "gray57", "gray58", "gray59", "gray60", "gray61", "gray62",
"gray63", "gray64", "gray65", "gray66", "gray67", "gray68", "gray69", "gray70", "gray71", "gray72",
"gray73", "gray74", "gray75", "gray76", "gray77", "gray78", "gray79", "gray80", "gray81", "gray82",
"gray83", "gray84", "gray85", "gray86", "gray87", "gray88", "gray89", "gray90", "gray91", "gray92",
"gray93", "gray94", "gray95", "gray96", "gray97", "gray98", "gray99", "gray100", "green", "green yellow",
"green1", "green2", "green3", "green4", "GreenYellow", "grey", "grey0", "grey1", "grey2", "grey3",
"grey4", "grey5", "grey6", "grey7", "grey8", "grey9", "grey10", "grey11", "grey12", "grey13",
"grey14", "grey15", "grey16", "grey17", "grey18", "grey19", "grey20", "grey21", "grey22", "grey23",
"grey24", "grey25", "grey26", "grey27", "grey28", "grey29", "grey30", "grey31", "grey32", "grey33",
"grey34", "grey35", "grey36", "grey37", "grey38", "grey39", "grey40", "grey41", "grey42", "grey43",
"grey44", "grey45", "grey46", "grey47", "grey48", "grey49", "grey50", "grey51", "grey52", "grey53",
"grey54", "grey55", "grey56", "grey57", "grey58", "grey59", "grey60", "grey61", "grey62", "grey63",
"grey64", "grey65", "grey66", "grey67", "grey68", "grey69", "grey70", "grey71", "grey72", "grey73",
"grey74", "grey75", "grey76", "grey77", "grey78", "grey79", "grey80", "grey81", "grey82", "grey83",
"grey84", "grey85", "grey86", "grey87", "grey88", "grey89", "grey90", "grey91", "grey92", "grey93",
"grey94", "grey95", "grey96", "grey97", "grey98", "grey99", "grey100", "honeydew", "honeydew1", "honeydew2",
"honeydew3", "honeydew4", "hot pink", "HotPink", "HotPink1", "HotPink2", "HotPink3", "HotPink4", "indian red", "IndianRed",
"IndianRed1", "IndianRed2", "IndianRed3", "IndianRed4", "indigo", "ivory", "ivory1", "ivory2", "ivory3", "ivory4",
"khaki", "khaki1", "khaki2", "khaki3", "khaki4", "lavender", "lavender blush", "LavenderBlush", "LavenderBlush1", "LavenderBlush2",
"LavenderBlush3", "LavenderBlush4", "lawn green", "LawnGreen", "lemon chiffon", "LemonChiffon", "LemonChiffon1", "LemonChiffon2", "LemonChiffon3", "LemonChiffon4",
"light blue", "light coral", "light cyan", "light goldenrod", "light goldenrod yellow", "light gray", "light green", "light grey", "light pink", "light salmon",
"light sea green", "light sky blue", "light slate blue", "light slate gray", "light slate grey", "light steel blue", "light yellow", "LightBlue", "LightBlue1", "LightBlue2",
"LightBlue3", "LightBlue4", "LightCoral", "LightCyan", "LightCyan1", "LightCyan2", "LightCyan3", "LightCyan4", "LightGoldenrod", "LightGoldenrod1",
"LightGoldenrod2", "LightGoldenrod3", "LightGoldenrod4", "LightGoldenrodYellow", "LightGray", "LightGreen", "LightGrey", "LightPink", "LightPink1", "LightPink2",
"LightPink3", "LightPink4", "LightSalmon", "LightSalmon1", "LightSalmon2", "LightSalmon3", "LightSalmon4", "LightSeaGreen", "LightSkyBlue", "LightSkyBlue1",
"LightSkyBlue2", "LightSkyBlue3", "LightSkyBlue4", "LightSlateBlue", "LightSlateGray", "LightSlateGrey", "LightSteelBlue", "LightSteelBlue1", "LightSteelBlue2", "LightSteelBlue3",
"LightSteelBlue4", "LightYellow", "LightYellow1", "LightYellow2", "LightYellow3", "LightYellow4", "lime", "lime green", "LimeGreen", "linen",
"magenta", "magenta1", "magenta2", "magenta3", "magenta4", "maroon", "maroon1", "maroon2", "maroon3", "maroon4",
"medium aquamarine", "medium blue", "medium orchid", "medium purple", "medium sea green", "medium slate blue", "medium spring green", "medium turquoise", "medium violet red", "MediumAquamarine",
"MediumBlue", "MediumOrchid", "MediumOrchid1", "MediumOrchid2", "MediumOrchid3", "MediumOrchid4", "MediumPurple", "MediumPurple1", "MediumPurple2", "MediumPurple3",
"MediumPurple4", "MediumSeaGreen", "MediumSlateBlue", "MediumSpringGreen", "MediumTurquoise", "MediumVioletRed", "midnight blue", "MidnightBlue", "mint cream", "MintCream",
"misty rose", "MistyRose", "MistyRose1", "MistyRose2", "MistyRose3", "MistyRose4", "moccasin", "navajo white", "NavajoWhite", "NavajoWhite1",
"NavajoWhite2", "NavajoWhite3", "NavajoWhite4", "navy", "navy blue", "NavyBlue", "old lace", "OldLace", "olive", "olive drab",
"OliveDrab", "OliveDrab1", "OliveDrab2", "OliveDrab3", "OliveDrab4", "orange", "orange red", "orange1", "orange2", "orange3",
"orange4", "OrangeRed", "OrangeRed1", "OrangeRed2", "OrangeRed3", "OrangeRed4", "orchid", "orchid1", "orchid2", "orchid3",
"orchid4", "pale goldenrod", "pale green", "pale turquoise", "pale violet red", "PaleGoldenrod", "PaleGreen", "PaleGreen1", "PaleGreen2", "PaleGreen3",
"PaleGreen4", "PaleTurquoise", "PaleTurquoise1", "PaleTurquoise2", "PaleTurquoise3", "PaleTurquoise4", "PaleVioletRed", "PaleVioletRed1", "PaleVioletRed2", "PaleVioletRed3",
"PaleVioletRed4", "papaya whip", "PapayaWhip", "peach puff", "PeachPuff", "PeachPuff1", "PeachPuff2", "PeachPuff3", "PeachPuff4", "peru",
"pink", "pink1", "pink2", "pink3", "pink4", "plum", "plum1", "plum2", "plum3", "plum4",
"powder blue", "PowderBlue", "purple", "purple1", "purple2", "purple3", "purple4", "red", "red1", "red2",
"red3", "red4", "rosy brown", "RosyBrown", "RosyBrown1", "RosyBrown2", "RosyBrown3", "RosyBrown4", "royal blue", "RoyalBlue",
"RoyalBlue1", "RoyalBlue2", "RoyalBlue3", "RoyalBlue4", "saddle brown", "SaddleBrown", "salmon", "salmon1", "salmon2", "salmon3",
"salmon4", "sandy brown", "SandyBrown", "sea green", "SeaGreen", "SeaGreen1", "SeaGreen2", "SeaGreen3", "SeaGreen4", "seashell",
"seashell1", "seashell2", "seashell3", "seashell4", "sienna", "sienna1", "sienna2", "sienna3", "sienna4", "silver",
"sky blue", "SkyBlue", "SkyBlue1", "SkyBlue2", "SkyBlue3", "SkyBlue4", "slate blue", "slate gray", "slate grey", "SlateBlue",
"SlateBlue1", "SlateBlue2", "SlateBlue3", "SlateBlue4", "SlateGray", "SlateGray1", "SlateGray2", "SlateGray3", "SlateGray4", "SlateGrey",
"snow", "snow1", "snow2", "snow3", "snow4", "spring green", "SpringGreen", "SpringGreen1", "SpringGreen2", "SpringGreen3",
"SpringGreen4", "steel blue", "SteelBlue", "SteelBlue1", "SteelBlue2", "SteelBlue3", "SteelBlue4", "tan", "tan1", "tan2",
"tan3", "tan4", "teal", "thistle", "thistle1", "thistle2", "thistle3", "thistle4", "tomato", "tomato1",
"tomato2", "tomato3", "tomato4", "turquoise", "turquoise1", "turquoise2", "turquoise3", "turquoise4", "violet", "violet red",
"VioletRed", "VioletRed1", "VioletRed2", "VioletRed3", "VioletRed4", "wheat", "wheat1", "wheat2", "wheat3", "wheat4",
"white", "white smoke", "WhiteSmoke", "yellow", "yellow green", "yellow1", "yellow2", "yellow3", "yellow4", "YellowGreen",
]

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        for i, c in enumerate(COLORS):
            label=tk.Label(self,text=c,background=c,width=8)
            label.bind("<Enter>",self.show_color_name)
            label.grid(row=i//20,column=i%20)

    def show_color_name(self,event):
        color = event.widget.cget("text")
        rgb = event.widget.winfo_rgb(color)
        rgbstring = "#%02X%02X%02X"%(rgb[0]//256, rgb[1]//256, rgb[2]//256)
        self.title("You are pointing ["+color+"] and background color = ["+rgbstring+"].")

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

 前半部分は、COLORSの定義です。Tcl/Tkの本家のホームページにあった色定義をコピペして作りました。ただ、実行時に2件、エラーが出ました。

TclError: unknown color name "agua"
TclError: unknown color name "crymson"

 きっと単なるtypoですね。以下の通り修正しました。

  • "agua" → "aqua"
  • "crymson" → "crimson"

 本家に修正依頼を出せればよいのでしょうけど、、、どこへ連絡すればよいのか分かりませんねぇ。と、いうことでこのtypoは放置するしかなさそうですね。

 プログラムの方は、ラベルをつくって、マウスポインタがラベルに入ったら(<Enter>イベントで)、ラベルに指定された色名と、RGB形式に変更した値をタイトルとして出力するように作りました。

 いつも気になるのが、mainloop()を書く位置なんだけど、スクリプトとして実行された後に書くか、__init__の最後で書くか、どっちがいいのかなぁ。たぶんどっちでもいいんだろうけど、メインアプリケーションとして実行されるなら、__init__の最後に書いておくのが正解なんじゃないかなぁ、と、思う今日この頃です。

Python GUI プログラミングを教えています「その2」

 今日も見に来てくださってありがとうございます。みなさん、新型コロナウィルスで自粛中でしょうか。

 前回のプログラミングを教えるの続きです。ちなみに息子に使わせている環境ですが、ぼくの環境は触らせたくなかったので、しばらく使っていなかったRaspberryPI3を使うことにしました。フツーにRaspbianが入っているのですけど、プログラミング教育を意識しているのか、メニューの中に「プログラミング」があって、その中からSpyder3を使うことにしました。

 あ、ちなみに、前回の内容の感想を聞いたら、「難しい」と、言っていました。うん、まあ簡単じゃないよねぇ。でもprint("Hello World!")じゃつまんないでしょ。いきなり難しいことをやらせすぎ、と、言われることもありましたが、ぼくは習うより慣れろ、が正解じゃないかなぁ、と、思っています。

 と、いうことで、次回は、ウィンドウのサイズを変えてみたり、背景の色を変えてみたりしましょう。まずは、ウィンドウのサイズを変えてみます。タイトルをセットしている次の行に追加して実行みましょう。

        self.geometry("300x100")

 これで、大きさが変わります。自由に変更してみて、というと、どこまで大きくなるんだろうかと、”10000×10000″をセットしてました。どうなるのかとハラハラしてみていましたが、スクリーンより大きいウィンドウにはならないようで、ホッとしました。

 背景色は、以下のとおりにセットすると「赤」になります。

        self.configure(background="#FF0000")

 この"#FF0000"は色の三原色で、RGBのRed、Green、Blueの順番にそれぞれ2桁の16進数で指定します。ここで16進数の説明が必要になりますね。このように三原色で指定する以外では、”red”、”green”、”blue”、”gray”など、事前に定義された色名があります。詳しくはTcl/Tkの本家のホームページに記載があります。

 Pythonでは、16進数への変換は簡単です。例えばAliceBlueとして定義されていた色は、それぞれ240、248、255ですが、”#{:02X}{:02X}{:02X}”.format(240,248,255)と指定するだけで16進数に変換されます。こんな感じです。やさしい青色ですねぇ。

        self.configure(background="#{:02X}{:02X}{:02X}".format(240,248,255))
#違う書き方もありました
        self.configure(background="#%02X%02X%02X"%(240,248,255))

 コンピュータで、どうして16進数が使われているのかという話を始めたら、「疲れた~」と、言って逃げられちゃいました。はい、16進数は余分でしたね。

 次は何やろうかなぁ。

Python GUI プログラミングを教えています「その1」

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

 中学生の息子にこれからプログラミングを教えようと思います。彼は「ミニゲームをつくりたいんだよね。」と言っていましたので、最終的には自分でミニゲームができるくらいのレベル感に仕上げましょう。ちょうどtkinterの勉強をしていたので、これを使って教えようかと。さあ、どうなることでしょうね。

 プログラミングを教えるということについてちょっと考えてみました。最初のハードルとして、プログラミングってどういうことか、ということを教えるのが異様に難しいと思います。次に環境について理解してもらうこと。これも難しいです。なので、まずは実際にどうすればどうなるを体験してもらって、それに慣れてもらおうと思います。そして、そのあとからナニをやっていたのか、プログラミングとはどういうことなのか、環境にはどんなものがあるのか、ということを教えていこうかな、と、思います。

 導入としては、やっぱりおなじみの「Hello World!」ですよねぇ。と、いうことで、まずは、ウィンドウを表示するプログラムを作ってもらいます。環境はこちらで準備しましょう。コードを入力するところかやってもらって、すぐに実行する、ということを体験してもらおう。まずは以下のスクリプトからのスタートです。これを実行してみたら、どんな反応がありますかねぇ。。。

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Hello World!")
        self.mainloop()

if __name__ == "__main__":
    app = Application()

 こんな感じです。

Tkinter版のHello World!

 はい、ちょっと難しいかなぁ、とも思いましたが、無事実行できました。「日本語でもできるの?」と聞いてきたので、「Hello World!のところを日本語で書き換えればいいよ」と教えました。すると、なんと、「こんにちは みつのり」に書き換えたスクリプトを今のスクリプトの下に書き始めて実行してしまいました。当然、ウィンドウが2回表示されました。2回目のウィンドウが下の通りです。

        self.title("こんにちは みつのり")
こんにちは みつのり

 なかなか、自由な感じですね。

 次は、何をしようかなぁ。

Raspberry PI 3 Bluetoothマウスを認識させる

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

 久しぶりにRaspberry PI3を使ってみようと電源を入れてみました。ずいぶんと放置していたので、以前使っていたマウスがなくなって、Bluetoothマウスしか手元になかったのですよねぇ。Bluetoothを使ってペアリングする画面を開くためにマウスがないとクリックできない、というジレンマに陥りました。ということで、ターミナルからBluetoothマウスを認識させる方法を調べて動作させたので、記録しておきます。

 やったことは、以下の通り。まずはここのページを参考にさせていただきました。

  • メニュー → アクセサリ → LXTerminalを起動
  • 「sudo bluetoothctl」コマンド実行
  • [bluetooth]プロンプトが表示されるので、「agent on」を実行
  • さらに「scan on」を実行
  • 一覧に使おうと思っていた「ELECOM Laser Mouse」が登場
  • 「pair xx:xx:xx:xx:xx」を実行 (xxのところは表示されたデバイスのIDです)
  • ダイアログ画面でなにやら2度ほど聞いてきたので「OK」を押した

 これで、本来は完了のはずなのですけど、なぜか動作しなかったのですよねぇ。で、さらにやったことが以下の2つのコマンド実行後、再起動です。

  • sudo apt-get update
  • sudo apt-get dist-upgrade

 2つ目のアップデート、めっちゃ時間がかかりましたが、なんとか終わって再起動したら、マウスが使えるようになりました。ちょっと感動しました!

Tkinterでシュルテ・テーブルをつくる リファクタリング編

 今日も見に来てくださってありがとうございます。まだ、朝は肌寒いですねぇ。

 さて、予言通り、先日のスクリプトをリファクタリングしたいと思います。ポイントは、まず、Frameを継承しているところですね。次は、コンスタント値を定義しているところ、そして、大枠とテキストをリフレッシュしているところを分離するところでしょうか。ちょっとやってみたいと思います。

Frameの変更

 まずは、単純にFrameをTkに置き換えてみます。そして、実行。

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 73, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    super().__init__(master)
  File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk

(base) C:\work\tkinter_example>

 思った通り、エラーがでましたが、、、まずは、Tkの初期化、__init__()のところですね。アプリケーションレベルの初期化になりますので、masterをパラメータで受け取る必要はありません。master関連を削除していきたいと思います。そうすると、タイトルの設定も、self.master.title("Schulte Table")ではなくて、self.title("Schulte Table")と変更する必要がありますね。あと、self.pack()となっている部分もFrameではなくなったので、削除します。あとは、importからもFrameを削除しましょう。これだけでも__init__()が随分とスッキリしましたね。

修正前の__init__()

    def __init__(self, master=None):
        if master == None:
            master = Tk()
        super().__init__(master)
        master.title("Schulte Table")
        self.master = master
        self.pack()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

修正後の__init__()

    def __init__(self):
        super().__init__()
        self.title("Schulte Table")
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

 実行してみたところ、またしてもエラーが発生しました。

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 73, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    super().__init__(master)
  File "C:\ProgramData\Anaconda3\lib\tkinter\__init__.py", line 2023, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
TypeError: create() argument 1 must be str or None, not Tk

(base) C:\work\tkinter_example>python SchulteTable2.py
Traceback (most recent call last):
  File "SchulteTable2.py", line 69, in <module>
    shulteTable = SchulteTable()
  File "SchulteTable2.py", line 22, in __init__
    self.create_widgets()
  File "SchulteTable2.py", line 38, in create_widgets
    command=self.master.destroy)
AttributeError: 'NoneType' object has no attribute 'destroy'

(base) C:\work\tkinter_example>

 ええと、destroyという属性はありませんよ、と言われていますね。destroyTkの属性でしたね。Frameを使っていたから、その親のmasterから呼び出していたのでした。なので、ここのself.master.destroyは、self.destroyに変更します。再度実行してみますと、画面はバッチリ表示されましたが、クリックするとエラーがでました。

(base) C:\work\tkinter_example>python SchulteTable2.py
Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "SchulteTable2.py", line 43, in redraw_text
    messagebox.showinfo("結果",f"かかった時間は、{self.elapse_time:0.2f}秒です。")
  File "C:\Users\me-ishikawa\AppData\Local\Continuum\anaconda3\lib\tkinter\__init__.py", line 2101, in __getattr__
    return getattr(self.tk, attr)
AttributeError: '_tkinter.tkapp' object has no attribute 'elapse_time'

 ほお、elapse_timeがありません、とおっしゃいますか。なるほど、self.textが存在しないので、redraw_textがうまく動作していないようです。なぜ、text__init__()で初期化しなかったのでしょうね。ちょっと謎ですが、過去のことは追求してもわかりません。self.textに変更して__init__()の中へ移動して初期化するよう変更します。

 初期化の問題は解決できたようですが、挙動がおかしくなりました。クリックしてスタートするのは問題なさそうです。ただスタートしたあとに再度クリックしたら停止するはずなのですけど、クリックする位置によっては、停止した後すぐにまたスタートしてしまいます。イベントのバインドに問題があるようです。挙動から考えると、Tkを継承したトップレベルでバインドして、さらにその子供のウィジェットにもバインドしたときに、イベントに対する処理を二度実行してしまっているようです。Frameの場合はそんなことなかったのに、不思議ですね。今回は、トップレベルのイベントで充分ですので、個々のウィジェットのバインドは削除しましょう。

 そして、classのすぐ下に定義してある変数を何とか整理したいです。名前空間がモジュール内にあるので、クラス定義の外に出してしまいましょう。そうすると、「self.」を書かなくてよくなります。それでもまだ横に長いのと、わかりやすさのために値をいったん変数に入れて、後で見たときに少し確認しやすくしましょう。あと、一回も参照されていない、self.line1への代入はやめてしまいましょう。

 いろいろと改善した結果が以下のソースです。

from tkinter import Tk, Button, Canvas, Label, messagebox
from random import sample
from time import time

MARGIN = 10
DISTANCE = 50
WIDTH = 5
HEIGHT = 5
FSIZE = 25

class SchulteTable(Tk):
    def __init__(self):
        super().__init__()
        self.title("Schulte Table")
        self.text = list()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

    def create_widgets(self):
        self.info = Label(self, text="Tap anywhere to start", font=("",14))
        self.info.pack(side="top")
        width = MARGIN*2+WIDTH*DISTANCE
        height = MARGIN*2+HEIGHT*DISTANCE
        self.canvas = Canvas(self,width=width, height=height)
        self.canvas.pack(side="top")
        for i in range(WIDTH+1): #縦線
            x0, y0 = MARGIN + i * DISTANCE, MARGIN
            x1, y1 = MARGIN + i * DISTANCE, MARGIN + DISTANCE * HEIGHT
            self.canvas.create_line(x0,y0,x1,y1)
        for i in range(HEIGHT+1): #横線
            x0, y0 = MARGIN, MARGIN + i * DISTANCE
            x1, y1 = MARGIN + WIDTH * DISTANCE, MARGIN + i * DISTANCE
            self.canvas.create_line(x0,y0,x1,y1)

        self.quit = Button(self, text="QUIT", fg="red", command=self.destroy)
        self.quit.pack(side="bottom")

    def redraw_text(self, event):
        if not len(self.text):
            self.info["text"] = "Tap anywhere to STOP"
            self.time = time()
            self.after(100, self.time_update)
        else:
            for t in self.text:
                self.canvas.delete(t)
            self.text = list()
            message = f"かかった時間は、{self.elapse_time:0.2f}秒です。"
            messagebox.showinfo("結果",message)
            self.info["text"] = "Tap anywhere to start"
            return
        counter = 0
        answers = sample(range(1,WIDTH*HEIGHT+1),WIDTH*HEIGHT)
        for x in range(WIDTH):
            for y in range(HEIGHT): # 数字のテキスト描画
                x0 = MARGIN + x * DISTANCE + DISTANCE / 2
                y0 = MARGIN + y * DISTANCE + DISTANCE / 2
                ans = str(answers[counter])
                text = self.canvas.create_text(x0,y0,text=ans,font=("",FSIZE))
                self.text.append(text)
                counter+=1

    def time_update(self):
        if not len(self.text):
            return
        self.elapse_time = time() - self.time
        self.info["text"] = f"Tap anywhere to STOP:{self.elapse_time:0.2f}"
        if len(self.text):
            self.after(10, self.time_update)

if __name__ == '__main__':
    shulteTable = SchulteTable()
    shulteTable.mainloop()

 どうでしょう、少しは分かりやすくなったでしょう。もしかして、単なる自己満足でしょうか。。。

Tkinterでシュルテ・テーブルをつくる

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

 みなさん、シュルテ・テーブルというものをご存知でしょうか。先日読んだ本に、脳のトレーニングとして紹介されていました。少し前にこれをtkinterで作ったので、紹介したいと思います。

できあがりイメージ

 シュルテ・テーブルは、以下のような表です。これで、周辺視野、注意力、セルフコントロール、集中力などが鍛えられるということです。表の中心に視線を固定して、周辺視野だけを使って、1~25を順番に探すだけです。しばらく続けれていれば、12秒~15秒ほどで25まで数えられるようになるそうです。そして、これだけで、観察力が高まるそうですよ。

シュルテ・テーブル

ソースコード

 ソースコードです。

from tkinter import Tk, Button, Canvas, Frame, Label, messagebox
from random import sample
from time import time

class SchulteTable(Frame):
    MARGIN = 10
    DISTANCE = 50
    WIDTH = 5
    HEIGHT = 5
    FONT_SIZE = 25
    text = list()
    def __init__(self, master=None):
        if master == None:
            master = Tk()
        super().__init__(master)
        master.title("Schulte Table")
        self.master = master
        self.pack()
        self.create_widgets()
        self.bind("<Button-1>", self.redraw_text)

    def create_widgets(self):
        self.info = Label(self, text="Tap anywhere to start", font=("",14))
        self.info.pack(side="top")
        self.info.bind("<Button-1>", self.redraw_text)
        self.canvas = Canvas(self,width=self.MARGIN*2+self.WIDTH*self.DISTANCE, height=self.MARGIN*2+self.HEIGHT*self.DISTANCE)
        for i in range(self.WIDTH+1):
            self.line1 = self.canvas.create_line(self.MARGIN+i*self.DISTANCE,self.MARGIN,self.MARGIN+i*self.DISTANCE,self.MARGIN+self.DISTANCE*self.HEIGHT)
        for i in range(self.HEIGHT+1):
            self.line1 = self.canvas.create_line(self.MARGIN,self.MARGIN+i*self.DISTANCE,self.MARGIN+self.WIDTH*self.DISTANCE,self.MARGIN+i*self.DISTANCE)
        self.canvas.pack(side="top")
        self.canvas.bind("<Button-1>", self.redraw_text)

        self.quit = Button(self, text="QUIT", fg="red",
                              command=self.master.destroy)
        self.quit.pack(side="bottom")

    def redraw_text(self, event):
        if not len(self.text):
            self.info["text"] = "Tap anywhere to STOP"
            self.time = time()
            self.after(100, self.time_update)
        else:
            for t in self.text:
                self.canvas.delete(t)
            self.text = list()
            messagebox.showinfo("結果",f"かかった時間は、{self.elapse_time:0.2f}秒です。")
            self.info["text"] = "Tap anywhere to start"
            return
        counter = 0
        answer = sample(range(1,self.WIDTH*self.HEIGHT+1),self.WIDTH*self.HEIGHT)
        for x in range(self.WIDTH):
            for y in range(self.HEIGHT):
                self.text.append(self.canvas.create_text(self.MARGIN+x*self.DISTANCE+self.DISTANCE/2,self.MARGIN+y*self.DISTANCE+self.DISTANCE/2,text=str(answer[counter]),font=("",self.FONT_SIZE)))
                counter+=1

    def time_update(self):
        if not len(self.text):
            return
        self.elapse_time = time() - self.time
        self.info["text"] = f"Tap anywhere to STOP:{self.elapse_time:0.2f}"
        if len(self.text):
            self.after(10, self.time_update)

if __name__ == '__main__':
    shulteTable = SchulteTable()
    shulteTable.mainloop()

 このシュルテ・テーブル、ちょっと昔に作ったのですが、いけてませんね。なぜかFrameを継承しているし、そのせいでmasterをセットしなきゃいけなくなってるし、一行が異様に長いところがあるし、自分で作ったプログラムなのですが、、、ちょっとわかりずらいなぁ。

 とりあえず、動いているので、公開しようと思います。見れば見るほど修正したくなってきましたので、次回、リファクタリングします!