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をセットしなきゃいけなくなってるし、一行が異様に長いところがあるし、自分で作ったプログラムなのですが、、、ちょっとわかりずらいなぁ。

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

Pythonプログラム 初期値の保存方法 Windowsスタイル

 今日も見に来てくださって、ありがとうございます。すっかり春めいてきましたね。

 Pythonでツールとかゲームを作りたいな、と、思ったときに必要になってくる設定ファイルって、どうしたらいいでしょう。ざっと考えただけでもいろんな選択肢があったのですが、ぼくの環境はWindows10ですので、Windowsスタイルの.iniファイルに保存して、読み書きするのがよさそうですね。ということで、標準モジュールのconfigparserを使ってみたいと思います。詳細はここに説明がありました。

読み込み方法

 まずは、試しにファイルを開いてみたいと思います。普段よく使っているサクラエディタのフォルダに.iniファイルがありましたので、そちらを見てみたいと思います。以下のスクリプトを実行してみます。

from configparser import ConfigParser

cfg = ConfigParser()
cfg.read("C:\Program Files (x86)\sakura\sakura.exe.ini")
for s in cfg.sections():
    print("["+s+"]")
    print(cfg.options(s))
    for o in cfg.options(s):
        print(o, cfg.get(s,o), cfg[s][o])

 Anaconda Promptからpythonコマンドを実行、その後スクリプトを実行しました。

実行結果

 1行目、configparseモジュールのConfigParserクラスをインポートしています。これで設定パーサーが使えるようになります。パーサーとは、構文解析を行うためのプログラムの総称です。今回のConfigParserは設定ファイルの構文を読み込んで利用できるようにしてくれます。

 3行目で、クラスのインスタンスを作成して、4行目で.iniファイルを読み込んでいます。

 セクションは、.iniファイルの中に[section]形式で指定された値です。5行目にあるように、cfg.sections()を実行することでセクション文字列のリストが取得できます。結果から、サクラエディタでは「Settings」というセクションが一つだけ設定されていることがわかります。

 セクションの中には、「=」で区切られたキーと値があります。6、7行目にあるように、cfg.options(s)とセクションを指定することでそのセクションのキー一覧が取得できます。注意点は、セクションは大文字小文字を区別するけど、オプションのキーは区別しない、ということでしょうか。

 値の取得方法は、いくつかありますが、簡単な方法として、二つ使ってみました。9行目の、cfg.get(s, o)とcfg[s][o]です。いずれもセクションとオプション(キー)を指定することで、値を取得できます。辞書のように使うことができるのでとっても便利ですね。

設定ファイルの作成、書き込み

 読み込みができたので、今度は書き込みです。test.iniファイルを新しく作成してみます。

cfg  = ConfigParser()
cfg.add_section("NewSection1")
cfg.set("NewSection1", "Option1", "Value1")
cfg.set("NewSection1", "Option2", "Value2")
cfg["OtherSection2"] = {"Option3":"Value3", "Option4":"Value4"}
with open("test.ini","w") as fp:
    cfg.write(fp)

 実行すると、test.iniファイルが出来上がりました。

[NewSection1]
option1 = Value1
option2 = Value2

[OtherSection2]
option3 = Value3
option4 = Value4

 サンプルとして二つの設定の仕方を試してみました。

1行目:まずはConfigParserのインスタンスを作成します。

2~4行目:add_sectionでセクションを追加して、追加したセクションにオプションをふたつセットしています。

5行目:辞書と同じ形式でセクションをセット、同時に別の辞書を使ってオプションを二つ追加しています。

6~7行目:最後にtest.iniファイルへ作成した値を保存しています。

おまけ

 これで、設定ファイルの読み込みと書き込みができるようになったのですが、これだと設定ファイル中のコメントが消えてしまうのですよね。まるっきり上書きですからね。ちょっと調べてみるとコメントを書くためのテクニックがありました。以下のスクリプトの通りです。

cfg = ConfigParser(allow_no_value=True)
cfg.add_section("SampleSection")
cfg.set("SampleSection","; これはセクションのコメントです。")
cfg.set("SampleSection","Option4", "Value4")

with open("test2.ini","w") as fp:
    fp.write("; これは全体のコメントのテストです。\n\n")
    cfg.write(fp)

 実行結果のtest2.iniファイルの内容です。

; これは全体のコメントのテストです。

[SampleSection]
; これはセクションのコメントです。
option4 = Value4

 ポイントは、1行目の「allow_no_value=True」をセットすることと、3行目のオプションの最初の文字を「;(セミコロン)」にしておく、ということですね。また全体のコメントを追加するためには、7行目のように、設定内容を書き込む直前にコメントを出力すればよいですね。

 他に、コメント文字は「#」、キーバリューの区切り文字に「:」も初期値として利用可能です。これらの値はConfigParserのインスタンスを作成するときに変更することができます。

まとめ

 設定ファイルの読み書きに、configparserが利用できます。コメント付きの設定ファイルの読み書きをしたいときには、ちょっとした工夫が必要です。

Pythonプログラム Oracleへの接続

 今日も見に来てくださって、ありがとうございます。今回は、pythonからOracleへ接続して、テーブルをつくったり、データをINSERT、SELECT、UPDATE、DELETEなどをやってみようと思います。

準備(まずはインストール)

 ぼくの現在の環境は、Windows10、Anaconda3(64bit)です。今回Oracleへ接続するためのモジュールに、cx_Oracleを使います。ちなみに、OracleはExpress Edition 18cです。Pythonでモジュールのインストールといえば、pipを使いますが、Anacondaの場合は、condaというコマンドを使うと整合性の取れたちょうど良いものを入れてくれるようなので、そちらを利用します。やり方がわからないときは、すぐにgoogle先生に聞いてみます。はい、インストールのやり方、ここにありましたね。ちなみに、cx_Oracleについては、丁寧なドキュメントがここにありました。(英語です。)

conda install -c anaconda cx_oracle

 コマンドが分かりましたので、手順を説明していきます。

まずは、Anacondaプロンプトの起動です。Windowsのメニューから以下の「Anaconda Prompt」を選択します。

Windows メニュー

すると、以下のようなプロンプト画面が表示されます。

Anaconda Prompt

 ここで、先ほどのコマンドを入力しましょう。おっと、condaの新しいバージョンが出ているといわれていました。

condaコマンドを走らせてアップデートしてください、ということなので、アップデートするのに、いったん中断しましょう。「Proceed ([y]/n)?」(続けますか?)と聞かれますのでnを入力、Enterキーを押して中断します。そして、今度は以下のcondaコマンドを更新するためのコマンドを入力して、結果を見ましょう。途中で「Proceed ([y]/n)?」(続けますか?)と聞かれますので、今度はyを入力してEnterを押して続けてください。

conda update -n base -c defaults conda

無事成功、ですね!
…と、思いましたが、正しくインストールできなかったようです。

失敗していました。

 ぱっと見た目はダウンロードして、解凍に成功していますので、バッチリできたねぇ、と、勘違いしても仕方ありませんよね。よく見ると「EnvironmentNotWritableError」という「環境が書き込みできないエラー」が発生しているようです。「The current user does not have write permissions to the target environment.」現在のユーザはターゲットの環境への書き込み権限を持っていません、ということですね。
 舞い戻って最初のコマンド入力のメッセージもよくよく眺めてみていたら、なんと、衝撃の事実が。あ、たいした事実ではありませんよ。(笑)

衝撃の事実

 そう、condaも一緒にアップデートされますよ、と記載があるじゃないですか。止めなくてもよかったのに。と、いうことで気を取り直して、書き込み権限のある状態でcondaを実行したいと思います。個別の権限設定があるかどうかは分かりませんが、こういう時はいつもAnaconda Promptを管理者権限で起動してからcondaコマンドを実行しています。

Anaconda Promptを管理者として実行

 Anaconda Promptを管理者として実行するために、Windowsメニューから「Anaconda Prompt」を右クリック、「その他」の「管理者として実行」を選択します。これで今度は成功するはず。ユーザーアカウント制御のダイアログボックスが表示された場合には、「このアプリがデバイスに変更を加えることを許可しますか?」の質問に対して「はい」を選択してください。これで今度は管理者としてAnaconda Promptが実行されました。

 左上に「管理者」と出力されていますね。
 では、気を取り直して再び先ほどのコマンドを実行します。

conda install -c anaconda cx_oracle

 今度こそ成功だね、と、思いましたが、なんと、またしてもエラーが発生。

またしてもエラー発生

 アクセスが拒否されました、ということですが、、、管理者権限で実行したのにねぇ。Qtのパッケージにアクセスするのに失敗しているようですね。ええと、もしかしたら、開発環境(IDE)のSpyderを使っているのが原因でしょうか。ぜんぜん気にしていませんでしたが、Spyderが起動中でした。Spyderも確かQtを利用していたと思いますので、おそらくこいつが掴んでいるためにアクセスが拒否されたのでしょうね。と、いうことでSpyderを終了してもう一度実行してみます。

今度はやっと成功しました。

 若干先ほどのパッケージと内容が変わっているのが気になりますが、なんとか成功したようです。

接続確認

 では、さっそく接続確認してみます。接続確認のスクリプトは以下の通りです。

import cx_Oracle

username = "ishikawa" # ユーザー名は適宜変更してください。
password = "********" # パスワードも適宜変更してください。
conn = cx_Oracle.connect(username, password, "127.0.0.1:1521/xepdb1")
print(conn.version)

 最初に必要なのは、import cx_Oracleとcx_Oracleモジュールをインポートすることです。接続には、モジュールで定義されているconnectメソッドを使います。接続後、versionが出力できれば、接続ができた、ということが確認できるでしょう。

接続成功!

はい、接続に成功したようです!

テーブル作成

 では、続けてテーブルを作成します。コネクションからcursor()を呼び出してカーソルを作成して、カーソルからexecute()を使ってSQLを実行します。

cur = conn.cursor()
cur.execute("create table poi( n number, v varchar2(20), c char(10), d date )")
テーブル作成成功!

 はい、成功したようです。念のため、SQL*Plusで確認してみました。あ、ちなみにSQL*Plusとは、Oracleのコマンドラインツールで、SQLを発行したり、結果をファイルへ出力したりできる、基本的なツールです。Oracleをインストールした環境にはたいていインストールされていますので、いろいろな現場へ行く人は使い方に習熟しておくとよいと思います。

テーブル作成結果をSQL*Plusで確認

 ちゃんと作成されてましたね。

データのINSERT、SELECT、UPDATE、DELETE

 次に、SQLのDMLを確認していきたいと思います。スクリプトは以下の通りです。

cur.execute("insert into poi values ( 1, 'abc', 'def', sysdate )")
for row in cur.execute("select * from poi"):
    print(row)

cur.execute("update poi set n = 2 where n = 1")
conn.commit()
cur.execute("delete from poi where n = 2")
conn.commit()

 まずINSERTを実行して、内容をSELECTしてみます。そして、UPDATEしてから内容をコミットします。

INSERT、SELECT、UPDATE、COMMITの実行結果

 ごらんの通り、SELECTの結果はタプルのシーケンスとして戻されるのですね。日付はdatetimeモジュールのdatetimeで戻されるのですね。なるほど。
 ちゃんと更新されているかどうか、SQL*Plusからも確認してみます。

INSERT、UPDATE、COMMITの結果をSQL*Plusから確認

 ちゃんとNが2に更新されていますね。続いてDELETEを実行して、コミットします。

DELETEとCOMMITの実行

再度、SQL*Plusから確認してみます。

DELETEとCOMMITの実行結果をSQL*Plusから確認

 はい、ちゃんと削除されていました。

まとめ

 今回、cx_Oracleを使ってオラクルへ接続して、簡単なSQLを発行してみました。とりあえずはこれだけできるようになっていれば、簡単なアプリケーションは実現できそうですね。でも、エラー発生時のハンドリングとか、ストアドプログラムの呼び出しとか、変数をバインドしたりとか、ちょっと考えただけでもまだまだやらないといけないこと、たくさんありますね。また機会があれば、書いていきたいと思います。

 今回の注意点としては、インストールはcondaコマンドを管理者権限で実行する。このとき余計なプログラムは終了させておく。インストールのメッセージが英語だからと出力を適当に流さない、ということぐらいでしょうかねぇ。

PythonでGUIプログラミング キー入力を受け付ける

 今日も見に来て下さって、ありがとうございます。地道ながらに更新を続けたせいか、コロナウィルスで引きこもり中の余裕のある人たちのおかげか、週間のユーザ数が三桁に達するようになってまいりました。以前書いた「Pythonでプログラミング キー入力を受け付ける」のアクセス数がなぜか多いので、調子にのってGUI版を書いてみることにしました。

出来上がりイメージ

 出来上がりイメージです。ウィンドウの中にキャンバスをつくって、その中に黄色い丸と青い丸を書きました。マウスを動かすと、丸がついて回ります。そして、矢印キーを押すと、キーを押した方向へ丸が移動します。という、単純なものです。

出来上がりソースコード

とりあえず、動かしてみたいんじゃ、という忙しい方のために、まずはソースを貼っておきます。

import tkinter as tk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Moving in Canvas")
        self.pos = (0,0)
        self.pressed = {}
        self.canvas = tk.Canvas(width=600, height=400, background="white")
        self.canvas.pack()
        self.item = self.canvas.create_oval(10, 10, 40, 40, fill="yellow", tag="t")
        self.inner_item = self.canvas.create_oval(20, 20, 30, 30, fill="blue", tag="t")
        self.canvas.bind("<Motion>",self.move_by_mouse)
        self.bind("<KeyPress>",self.key_pressed)
        self.bind("<KeyRelease>",self.key_released)
        self.move_by_key()

    def move_by_mouse(self, event):
        if self.pos == (0,0):
            x0, y0, x1, y1 = self.canvas.coords(self.item)
            px = x0 + (x1 - x0) // 2
            py = y0 + (y1 - y0) // 2
            dx = event.x - px
            dy = event.y - py
            self.canvas.move("t", dx, dy)
            self.pos = (event.x, event.y)
            return
        dx = event.x - self.pos[0]
        dy = event.y - self.pos[1]
        self.pos = (event.x, event.y)
        self.canvas.move("t", dx, dy)
    
    def key_pressed(self, event):
        self.pressed[event.keysym] = True
        self.pos = (0,0)
    
    def key_released(self, event):
        self.pressed.pop(event.keysym, None)
    
    def move_by_key(self):
        dx, dy = 0, 0
        m = 5
        if "Up" in self.pressed:
            dy -= m
        if "Down" in self.pressed:
            dy += m
        if "Left" in self.pressed:
            dx -= m
        if "Right" in self.pressed:
            dx += m
        x0, y0, x1, y1 = self.canvas.coords(self.item)
        px = x0 + (x1 - x0) // 2 + dx
        py = y0 + (y1 - y0) // 2 + dy
        if 0 <= px <= self.canvas.winfo_width() and 0 <= py <= self.canvas.winfo_height():
            self.canvas.move("t", dx, dy)
        
        self.after(10, self.move_by_key)

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

内容説明

 まずは、1行目、「import tkinter as tk」で、tkinterモジュールをインポートします。「as tk」と指定してあるのは、グローバルな名前空間が汚されないようにするためです。チュートリアルでありがちな、「from tkinter import *」はダメな例なのでまねしないようにしましょう。

 次に、3行目、「class App(tk.Tk):」GUIアプリケーションのトップレベルは、必ずtk.Tkになりますので、これを継承してアプリケーションを作成します。

 4、5行目「def __init__(self):」では、アプリケーションの初期化を行います。まずは、親のメソッドを呼び出すため、「super().__init__()」をコールしています。ここまでは、もうお約束ですので完全に記憶しましょう。

 6行目「self.title("Moving in Canvas")」ウィンドウのタイトルをセットしています。tkinterでは特に何も意識せず日本語も使えます。

 7、8行目は、あとで使う変数を初期化しています。

 9~12行目で、Canvasをつくって、その上に丸を書いています。ポイントは、この丸を書くときに「tag="t"」とタグをセットしているところでしょうか。あとで出てきますが、キャンバス上のモノは、IDかタグで動かしたり属性を変更したりすることができます。IDだと一つしか動かせませんので、今回は二つの丸を動かすために、タグを指定しました。

 13~15行目は、イベントにバインドしています。どういうことかというと、例えばこの「self.canvas.bind("<Motion>",self.move_by_mouse)」の場合だと、self.canvas<Motion>(マウスポインタがキャンバスで動いた!)というイベントが発生したときには「self.move_by_mouse」を呼び出してね、と、割り当てている、ということになります。日本語のニュアンスだと結び付けている、というのが適当でしょうか。
 ここでは、3つのイベントをそれぞれ割り当てています。

  • <Motion> マウスが動いた → self.move_by_mouse
  • <KeyPress> キーが押された → self.key_pressed
  • <KeyRelease> キーが離された → self.key_released

 マウスによる移動と、キー入力による移動はそれぞれ独立しています。まずはマウスによる移動の方から説明します。

マウスによる移動

 マウスが動くたびに、<Motion>イベントが発生します。14行目でバインドしたので、マウスの動きに合わせてself.move_by_mouseが呼び出されます。マウスに合わせて動作させるのは、これだけで充分です。ポイントは、self.canvas.moveで指定できるのは、指定したタグを移動するオフセット値になることです。現在位置からx軸に+10、y軸にー20といった風に設定することになります。eventで取得できるのが左上のコーナーを0,0との基準にしてプラスに増えていく座標になっています。このため、最初の動き出しの時だけは、動かずに動作開始点を保持するだけにして、次のイベントが発生したときに、前の位置から5,3動く、といった指定になるようにしました。

 ちなみに、x0, y0, x1, y1 = self.canvas.coords(self.item)は、self.itemの左上の原点からの座標を取得するメソッドです。self.itemを矩形で切り取って左上の座標と右下の座標を同時に取得しています。self.itemの中心座標を計算するのに、px = x0 + (x1 - x0) // 2py = y0 + (y1 - y0) // 2としています。その後、dx = event.x - pxdy = event.y - pyにて、中心点からマウスカーソルの座標までのそれぞれの移動距離を計算しています。その後、self.canvas.move("t", dx, dy)として、マウスカーソルまでself.itemを移動します。

キーによる移動

 キーが押されるたびに、<KeyPress>イベントが発生します。押したキーを話すたびに、<KeyRelease>イベントが発生します。イベントにはそれぞれ、key_pressedkey_releasedがバインドされていました。このため、キーが押されると、8行目で初期化されたディクショナリ「self.pressed = {}」の中に、押されたキーのシンボルがTrueとして登録されます。例えば、右キーを押すと、ディクショナリの中身は{'Right': True}という風になります。キーは同時に押すこともできますので、例えば上と右キーを同時に入力するとディクショナリの中身は{'Up': True, 'Right': True}のようになります。ソースコードを見ればわかると思いますが、キーのシンボルはevent.keysymで取得しています。

 初期化の説明の時にはさらりと飛ばしましたが、初期化(__init__(self))の最後の行、16行目で、self.move_by_key()を呼び出しています。このmove_by_keyは呼び出されると、最後にself.after(10, self.move_by_key)を呼び出すことで、自分自身を10ミリ秒後に呼び出すことで、無限ループを開始します。このループにて、キー入力を処理しています。

 具体的には、キー入力で上下左右の移動距離(ここでは5)をセットして、キャンバスのmoveメソッドを呼び出すself.canvas.move("t", dx, dy)ことでアイテムを動かしています。その直前のif文は、画面の外へはみ出して移動しないように制御しています。

まとめ

 上記のキー入力制御のアプローチは、個別にイベントをバインドするやりかたよりも好ましいと思います。個別にバインドした場合は、キー入力しない限りイベントが発生しないので、位置を飛ばして移動するような移動には使えますが、よりスムーズな移動には今回のようなイベントループで制御する必要があります。

 これでtkinterを使うときにキー入力やマウスによるイベントの制御はバッチリですね!

PowerBuilderでダウンロードフォルダを取得

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

珍しく、PowerBuilderの記事を書いてみることにしました。今のおしごとでは、PowerBuilder8.0.3というかなーり古い開発ツールを使っていますので、ニーズはほとんどないと思いますけど、自分用メモ、といういことでご容赦ください。

今回、Webからダウンロードしたファイルを読み込んで処理をする必要があって、ファイルの初期値をどうしようかなぁ、と、思ったのが発端です。とりあえず、ダウンロードフォルダにファイルが入るだろうから、そのフォルダを示しておけば大丈夫でしょう、ということでフォルダ名の取得方法を調べました。

こちらに紹介がありましたが、とうぜんPowerBuilderでの使い方ではありませんね。VBScriptでは、以下のように入力すればよいということでした。

MsgBox CreateObject("Shell.Application").Namespace("shell:Downloads").Self.Path

これを手掛かりに作ったのが以下のスクリプトです。

OLEObject lole_shell
lole_shell = CREATE OLEObject
lole_shell.ConnectToNewObject('Shell.Application')
OLEObject lpo
lpo = lole_shell.namespace("shell:Downloads")
if not isNull(lpo) then
    string ls_path
    ls_path = lpo.Self.path
    if directoryExists(ls_path) then
        sle_nm_file.text = ls_path + "\"
    else
        sle_nm_file.text = ""
    end if
else
    sle_nm_file.text = ""
end if
DESTROY lole_shell

ポイントは、lole_shell.namespace("shell:Downloads")を取得して、値がセットされたかどうかをチェックしているところです。ぼくの開発環境は古くて値がセットされず、エラーになっちゃうのですよね。でも、作ったプログラムをWin10環境で動かせば、値が取得できる、というトリックです。とりあえずユーザーが利用できればいいのですけど、ぼくが開発環境でその画面を開いたら異常終了、というのはいただけませんからねぇ。