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

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

WordPress予約公開の仕方

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

 ブログの文章、何度も書いていると、少しずつですが、だんだんと文章を早くなってきた気がします。やっぱり継続は大事ですね。ずっと書くのが苦手だったのですよね。ずいぶん前にたまたま余裕があって一日に記事を二つ書いたことがあったのですけど、下書きのままにしておいて、翌日公開するというのもとてもおっくうなので、その時はすぐに公開しちゃいました。何日分も前もって書いておくのはぼくにはムリだなぁ、と、思っていましたのです。

 でもそれが、簡単に予約しておくことができることがわかりました。そんな機能があるとは夢にも思っていなかったので、昨日まで知らなかったのでした。

 そう、いつもは、記事を書き終わったら、編集画面の右上にあるボタンの「公開する」を押して、公開していました。そう、このボタンです。

「公開する」ボタン

 書いている途中で中断する時は、「下書きとして保存」 を、途中で仕上がりを確認するときには、「プレビュー」を押して確認する、それくらいしか使ってなかったのですよねぇ。

 とりあえず、「公開する」を押してみます。すると、次のように「公開してもよいですか?」と、聞いてきます。

「公開する」ボタンを押したところ

 で、いつもは、この「公開:今すぐ」のところをクリックして、カレンダーが出てくるので、時刻だけ00:00に修正してました。日にちは確認したことがなかったのでした。いつも過去の日付が出てくるので、このカレンダーは作ったタイミングで決まるのかなぁ、というくらいには思っていたのですけど。で、時刻を修正すると、過去の日付の00:00に設定されることになるので、今すぐに公開されるのですよね。当たり前ですよね。

いつも変更するのは時刻だけでした

 昨日は、なぜか日にちも変えてみよう、って、思って変更したのですよね。ちょっとやってみます。未来の日付、3月24日を選択しました。

「予約投稿」になった!

 すると、ごらんください!「公開」ボタンだったところが、なんと「予約投稿」に替わったのです。いや~、予約投稿なんてできたのですね!しかも、こんなに簡単に♪
 偶然とは言え、自分の知らない機能を発見して、ちょっとうれしい今日この頃です。この記事はこのままにして今日の夜に公開します!
 どんどん書いて、どんどん予約投稿、、、できるといいなぁ。(笑)

追記

 …と、いうことで、朝起きて確認してみましたが、なんと、まだ公開されていません。そういえば何となく気になっていました。日付と時刻、なんかずれてるような気がしてたのですよねぇ。試しに新規追加して作成された日時を確認してみます。

 おお、現在2020年3月24日の6時48分なのに、23日の12時48分の日時で作成されてしまいました。これは、、、完全にずれてますね。

 設定を確認してみたところ、ありました。「タイムゾーン」ですね。

タイムゾーンの設定

 同じタイムゾーンの都市またはUTC、、、ということで、ありました「アジア」から「東京」が選べるようです。選択して保存しました。再度、この文章を保存、あ、ボタンは「予約投稿」になってますが、、、これでどうでしょう。。。

予約投稿の失敗

 ははは、予約投稿の失敗だそうです。まあ、予約してた時間よりタイムゾーンが早くなりましたので、そりゃそうか、という感じで仕方ありませんね。でも、失敗もちゃんと考慮されてあって、WordPressってすごいですね!

 気を取り直してこの後の7時10分に予約投稿してみます。今度はどうかな。はい、今度はちゃんと予約済みになっていました!

 そして、無事、公開されてますね。ホッ。

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

WordPressリンク切れの原因

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

今日は、WordPressの話題です。WordPressを使い始めたころ、「石川さん、リンクが切れてますよ」と、リンク切れを教えてもらいました。フツーに文章を書いて、そのまま公開すると、そんなことが起きます。この文章もおそらくそうなると思いますので、ちょっと途中ですが、この状態で公開してみます。

 投稿の編集画面の右上の「公開する」ボタンを押すと、「公開しました。」というメッセージが出てきますので、「投稿を表示」をクリックします。

はい、でました。こんな感じです。

 サイトのトップページに行くと、記事はちゃんと見えているので、しばらく謎だったのですよね。いろいろと調べてみてわかった原因は、これでした。

URLをご覧ください。日本語になっています。

 ここのURLに日本語(マルチバイト文字)がセットされるとなぜか表示されない、ということがわかりました。どこでこれを修正するかというと、こちらです。(他に、投稿一覧から「クイック編集」を選択して「URLスラッグ」を修正することもできます。

文書-パーマリンクーURLスラッグ

ここを英数字と半角記号だけに修正すれば、正しくリンクをたどっていくことができるようになります。修正すると「投稿を表示」のURLが変更されて、正しく修正されたことを確認できます。では、修正してみましょう。

wordpressーcause-of-missing-linkに修正

 さて、どうでしょうか。うまくいくと思いますか?「更新」ボタンを押してから「投稿を表示」の部分をクリックしてみます。

またしても、失敗!今度は、なぜ?

 ぼくは、これでずいぶんと悩みました。と言っても30分くらいですけど。他のはこれでうまいこといったのに、どうしてできないのでしょうか。。。

 正解は、ハイフンが半角英数字のハイフンではなくて、マルチバイト文字になっていたからでした。ここの「wordpressーcause-of-missing-link」ひとつめのハイフンと、ふたつめからよっつめのハイフンとは、微妙に長さが違いますよね。わかりますか?

 そう、自分で入力したのですけど、日本語を削除してアルファベットを入力するときに、ひとつめだけハイフンの代わりに半角カタカナの長音の記号を入れてしまっていた、ということが原因でした。フォントが違えばすぐにわかるのでしょうけど、ブラウザだとほとんどわかりませんね。誰かのお役にたてるとうれしいです。