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

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

 しつこくCanvasの移動についてやってみました。前回移動するためのクラスを作ったときに、移動のメソッドをクラス側に作ったけど、呼び出しはメインクラスの方から呼び出していて、それがちょっと気に入らなかったのですが、解決できたので、紹介します。

できあがりイメージ

 できあがりイメージは前回と同じです。機能的にはほとんど同じですが、イベントの処理がクラスに分離できたところが異なります。ま、見た目はまったく同じなのですけど。

ソースコード

import tkinter as tk


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

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

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

        self.canvas.bind("<Button-1>", self.button1)

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

    def button1(self, event):
        item = self.canvas.find_overlapping(event.x, event.y, event.x, event.y)
        if item:
            return
        m = Movable(self.canvas, event.x, event.y, "Red")
        self.canvas.movers.append(m)

class Movable():
    BOXID = 0
    def __init__(self, canvas, x, y, color):
        self.canvas = c = canvas
        global BOXID
        BOXID += 1
        self.tag = t = "MOVABLE-" + str(BOXID)
        self.t_id = c.create_text(x, y, text=color, tag=t)
        self.r_id = c.create_rectangle(c.bbox(self.t_id), fill=color, tag=t)
        c.tag_lower(self.r_id, self.t_id)
        self.start = None
        c.tag_bind(t, "<Button-1>", self.button1)
        c.tag_bind(t, "<Motion>", self.move)
        c.tag_bind(t, "<ButtonRelease>", self.button_release)
        c.tag_bind(t, "<Button-3>", self.remove)

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

    def button1(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        self.start = x, y

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

    def remove(self, event):
        c = self.canvas
        c.tag_unbind(self.tag, "")
        c.delete(self.tag)
        c.movers.remove(self)


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

説明

 最初の違いは、イベントをバインドしている16行目です。クリックしたときのイベントですが、これは、Canvasをクリックしたイベントを処理するためのものです。クリックしたところに箱がなければ、新しく箱を作ります。箱を作るための処理は、21~26行目です。以前は、この位置に、move()とbutton_release()がありましたが、今回はありません。これらは全部、クラスの方で処理するようになりました。

 29行目に追加した「BOXID」ですが、Canvas上の項目をまとめるためのタグに連番をつけるためだけのものです。Movableクラスのインスタンスが作られるごとにひとつずつ増えていきます。32行目、globalキーワードを指定することで、BOXIDが外部で宣言されたグローバル変数だということを宣言しています。33行目で1を足して連番が増えるようにして、34行目でtagを「MOVABLE-連番」となるように作成しています。35行目、36行目のテキストと四角を作成しているところでtagを指定しています。

 39~42行目でtag_bind()を使って、このクラスのオブジェクトで発生したイベントとメソッドをバインドしています。ここでのポイントはtag_bind()を使ってタグごとにイベントを割り当てているところです。前回は、キャンバスごとに割り当てていたので、オブジェクトに割り当てるたびに前回のイベント割り当てが上書きされてしまい、うまく動作しなかったのでした。調べた結果、キャンバスごとでも「add=”+”」オプションをつけてバインドすることでイベント割り当てが上書きされないということが分かったのですが、tag_bind()だとタグごとの割り当てになるので、こちらの方がしっくりきますね。

 バインドされたメソッドの方ですが、イベントはそれぞれのタグごとに発生するので、クラス内でこのオブジェクトが選択されたかどうか、という判定は不要になりました。

 あと、今回は右クリックで箱を消去するようにしてみました。

まとめ

 いかがでしょう、前回よりスッキリしたと思いませんか?
いや~、また自画自賛かなぁ。(笑)

Pythonプログラミング リストの逆順

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

 今日は、ちょっと簡単に、リストを逆順にする方法です。

やり方

In [1]: l = list(range(10))

In [2]: l
Out[2]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [3]: l[-1::-1]
Out[3]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [4]: l[::-1]
Out[4]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [5]: list(reversed(l))
Out[5]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

In [6]: l.reverse()

In [7]: l
Out[7]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

解説

 1行目でリストを作成しています。3行目、4行目で内容を確認しています。0~9のリストが出力されています。6行目、7行目で逆順のリストを生成しています。9行目、10行目も、12行目、13行目も同様に逆順のリストを生成しています。

 6行目、7行目の記述ですが、[start:stop:step]は、スライスといいます。start、stopを省略すると最初と最後を指定したことになります。例のようにstartに-1、stepに-1を指定することで逆順に取り出すことができます。9行目、10行目はstartを省略しています。

15行目のreverse()は、12行目のreversed()とは異なり、リスト(l)自身の内容が入れ替わります。スライスやreversed()は、逆順のリストのコピーを戻しています。

まとめ

 ポイントはリスト自身が逆順になってしまうのか、逆順のリストのコピーを戻すのか、という点ですね。スライスのやり方については、記憶してしまいましょう。

Python プログラミング 1から10までの合計を出す

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

 先日、とある勉強会で、Pythonを使って1から10までの合計を出す、ということをやっていたのですが、自分でも解答を作ってみました。宿題は10個考えてくる、ということだったので、ぼくも10個、考えてみました。

 あとで、実行時間を検証するために関数にしています。

解答と結果1

In [1]: def a1(N=10):
   ...:     return 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
   ...: print("解答1:", a1())
解答1: 55

解説1

 10個作らなきゃ、ということで真っ先に思いついたのがこれです。単純に足し算で足した結果を戻しています。1から10までの合計を算出する、という点においては合格ですが、関数としては、汎用性がなくてイマイチですね。合計が50までとか、10000まで、と変化した場合には使えません。そういう意味で、模範解答には遠いですね。

解答と結果2

In [2]: def a2(N=10):
   ...:     s = 0
   ...:     for i in range(1, N+1):
   ...:         s += i
   ...:     return s
   ...: print("解答2:", a2())
解答2: 55

解説2

 次に思いついたのが、これですね。たぶん、いちばんオーソドックスなやつでは、と、思います。forループを使って繰り返し実行しますが、等差数列が必要なときは、組み込み関数のrange()を使用します。パラメータを二つ指定していますが、ひとつめの1は、初期値で1から始める、という意味です。ふたつめのN+1は、N+1のひとつ前の数字まで繰り返す、という意味です。これで、1から始めて11のひとつまえの10まで繰り返すことができます。

 forループの構文の最後に指定するものは、イテラブルオブジェクトといって、反復可能なオブジェクトを指定します。具体的には、リストや文字列などはイテラブルオブジェクトで、繰り返し扱うための要素を複数持っているオブジェクトのことです。forループで指定されると、その要素をひとつずつ処理することができます。

 ここで使用されている組み込み関数のrange()は、パラメータで指定された数のシーケンスで、イテラブルオブジェクトになります。もっと簡単に言うと、次の整数の値を順番に返してくれるオブジェクト、ということです。一般に、forループにおいて特定の回数の繰り返し処理に使われます。文章よりも実際の戻り値を確認してもらった方が早いと思いますので、まずは実行結果をご覧ください。

In [1]: range(3).__iter__()
Out[1]: <range_iterator at 0x26faa394f50>

In [2]: i = range(3).__iter__()

In [3]: i.__next__()
Out[3]: 0

In [4]: i.__next__()
Out[4]: 1

In [5]: i.__next__()
Out[5]: 2

In [6]: i.__next__()
Traceback (most recent call last):

  File "<ipython-input-6-aa5506447029>", line 1, in <module>
    i.__next__()

StopIteration


In [7]: 

 まずは、forループの最後の要素はイテラブルオブジェクトですので、__iter__()が呼び出されて、イテレータが取得されます。Out[1]から「range(3).__iter__()」の戻り値がrange_iteratorオブジェクトということがわかります。イテレータは__next__()を呼び出すことで次の要素を戻すという約束のあるオブジェクトです。ここで__next__()を呼び出すと、順番に、0、1、2を戻していますね。In [6]の、最後3を戻すかというところで、StopIterationの例外を発生します。forループはイテレータがStopIterationが発生するまで繰り返される、というお約束になっています。このようにして、要素の数分の繰り返し処理が実行されています。

 ちなみに、__iter__()や__next__()などの特殊メソッドの部分は以下のように書き換えることもできます。

In [7]: i = iter(range(3))

In [8]: next(i)
Out[8]: 0

In [9]: next(i)
Out[9]: 1

In [10]: next(i)
Out[10]: 2

In [11]: next(i)
Traceback (most recent call last):

  File "<ipython-input-11-a883b34d6d8a>", line 1, in <module>
    next(i)

StopIteration

In [12]: 

 この特殊メソッドについて理解が進むと、Pythonでのプログラミングがより効率的にできるようになって、もっと楽しくなってくるのでは、と思います。

解答と結果3

def a3(N=10):
    s = 0
    for i in range(N):
        s += (i + 1)
    return s
print("解答3:", a3())
解答3: 55

解説3

 解答2の類似で、0から始めて9までの10回繰り返す、というループで合計値を変えるパターンですね。そんなに違いはありませんがすべての繰り返しで足し算をしている分、遅いかもしれません。

解答と結果4

In [4]: def a4(N=10):
   ...:     s = i = 0 
   ...:     while i < N:
   ...:         i += 1
   ...:         s += i
   ...:     return s
   ...: print("解答4:", a4())
解答4: 55

解説4

 Pythonで使える繰り返しもうひとつの繰り返し構文にwhileがあります。二行目で変数、sとiにそれぞれ0を代入して初期化しています。whileループの右側に条件が書いてありますが、この条件がTrue、真のあいだ、このループは繰り返されます。条件を書き間違えると無限にループしますので、注意が必要です。
 4行目の「i += 1」は、iに1を加えた値をまたiに代入する、という意味で、iが1から始まってひとつずつ増えていくことになります。5行目の「s += i」は、sにiを加えた値を代入していきますので、この行が合計部分の処理をしていると言っていいでしょう。

解答と結果5

In [5]: def a5(N=10):
   ...:     s = 0
   ...:     while N > 0:
   ...:         s += N
   ...:         N -= 1
   ...:     return s
   ...: print("解答5:", a5())
解答5: 55

解説5

 解説4は1から10までという順番で加えていきましたが、今回は、10から1までという順番で加算するところが違いますね。5行目の「N -= 1」で1ずつ減算しているところがポイントでしょうか。

解答と結果6

In [6]: def a6(N=10):
   ...:     s = 0
   ...:     for i in range(N,0,-1):
   ...:         s += i
   ...:     return s
   ...: print("解答6:", a6())
解答6: 55

解説6

 解説5をやって、range()の場合の逆順の加算を忘れていたことに気づきました。range()の第三引数に-1を指定することで、1ずつ減算していくことが可能です。まあ、a2、a3と、たいして変わりませんね。range()の使い方が異なるだけです。

解答と結果7

In [7]: def a7(N=10):
   ...:     if N == 1:
   ...:         return 1
   ...:     return N + a7(N - 1)
   ...: print("解答7:", a7())
解答7: 55

解説7

 これは、再帰呼び出しを利用した解答です。再帰呼び出しとは、関数の中で、自分自身の関数を呼び出すことをいいます。これにより繰り返し処理が可能になるプログラミング上のテクニックです。ポイントは4行目で、Nと自分自身を「N – 1」のパラメータで呼び出したときの戻り値を加算しているところです。呼び出された関数は、「N – 1」と自分自身を「(N – 1) – 1」で呼び出した値を戻して、と、次々と呼び出され、2行目の条件、Nが1になるまで再帰的に呼び出しが繰り返され、Nから1までの合計が求まります。

 このテクニックの若干の問題は、関数呼び出しを何度も繰り返すところです。関数が呼び出されると呼び出した関数はスタックというところへ積み上げられます。呼び出された関数の処理が終了するまではそこで待機、というイメージですね。これのどこに問題があるのか、というと、このスタックの上限が整数値と比べると割と小さめに設定されている点です。

 1000回では問題なく実行できましたが、10000回を指定するとエラーになってしまいました。

  File "<ipython-input-4-124a61628543>", line 4, in a7
    return N + a7(N - 1)

  File "<ipython-input-4-124a61628543>", line 4, in a7
    return N + a7(N - 1)

  File "<ipython-input-4-124a61628543>", line 2, in a7
    if N == 1:

RecursionError: maximum recursion depth exceeded in comparison

 RecursionErrorということで、再帰エラーですね。調べてみると、こちらで現在値を確認できるようです。

In [8]: import sys

In [9]: sys.getrecursionlimit()
Out[9]: 3000

 現在の値は3000でしたね。sys.setrecursionlimit()を使うことでセットもできるようですが、長くなりますので、今回はそこまで検証しませんね。

 ちなみに、return文のところへif文を記述すると、関数内は以下の一行で記述できました。ちょっぴりだけスマートな感じがしますね。

    return N + a7(N - 1) if N > 1 else 1

解答と結果8

def a8(N=10):
    return N * (N + 1) // 2
print("解答8:", a8())
解答8: 55

解説8

 これは、等差数列の合計を求める公式です。いつも公式が覚えられないので、以下の画像のような長方形の面積を考えてから、それの半分、というふうにして公式を導き出しています。下の場合、(1+10)×10が長方形の面積になって、その半分が求める合計ですね。これからN×(N+1)÷2の公式を導き出しています。

等差数列の合計を求める

 ぼくの予想では、この式を使って答えを出力するのが一番効率がよいのでは、というふうに思いました。他の処理の計算量はO(N)(Nに比例する)なのですが、この式の計算量はNの大きさによらないので、O(1)(定数)となります。

解答と結果9

In [9]: def a9(N=10):
   ...:     return sum(range(N+1))
   ...: print("解答9:", a9())
解答9: 55

解説9

 実は、宿題になる前に思いついたのがこのやり方です。Pythonだと、今回やりたいことは1行で完成します。はい、アルゴリズムの勉強にはなりませんね。

解答と結果10

In [10]: import numpy as np
    ...: def a10(N=10):
    ...:     return np.sum(np.arange(N+1))
    ...: print("解答10:", a10())
解答10: 55

解説10

 こちらもひとつ前のa9()と同じですが、numpyのライブラリを利用しています。大量データを扱うライブラリが豊富なnumpyですが、こちらの処理速度はいかがでしょうか。

まとめ

 さて、最後になりますが、ここで紹介したロジックは、どれを使うのが正解なのでしょうか。どれを使っても結果はでるし、今回のロジックはどれも時間がかかるものではありませんので、いずれも正解です。結論から言うと、思いついたものですばやく実装する、もしくは、後で見たときにわかりやすい実装にする、というので問題ないでしょうね。あ、a1は、ダメですよ。

 でも、プロとしてどのように実装するのが正解なのか、という指針を持っておきたい、という声が聞こえてきそうですね。指針のひとつとして、処理速度を計測しておきたいと思います。IPythonには%timeitというマジックコマンドがあって、いい感じに実行速度を測ってくれます。

In [11]: for f in [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]:
    ...:     print(f.__name__, "=", f(), ":", end="")
    ...:     %timeit f()
a1 = 55 :67.2 ns ± 3.61 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a2 = 55 :753 ns ± 51.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a3 = 55 :842 ns ± 56.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a4 = 55 :799 ns ± 145 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a5 = 55 :801 ns ± 109 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a6 = 55 :699 ns ± 43 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a7 = 55 :1.23 µs ± 33.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a8 = 55 :131 ns ± 4.65 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a9 = 55 :509 ns ± 19.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
a10 = 55 :3.48 µs ± 34.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

 a1は参考値ですが、いちばん速いですね。1から10までの計算ではそんなに差が出ませんが、再帰処理のa7とnumpyの処理がちょっと遅いようです。ちょっと1から1000までの計算で測定しなおしてみます。

In [12]: for f in [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]:
    ...:     print(f.__name__, "=", f(1000), ":", end="")
    ...:     %timeit f(1000)
a1 = 55 :78.2 ns ± 4.14 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a2 = 500500 :51 µs ± 3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a3 = 500500 :78.4 µs ± 4.88 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a4 = 500500 :85.7 µs ± 6.29 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a5 = 500500 :104 µs ± 13 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a6 = 500500 :51.7 µs ± 3.34 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a7 = 500500 :177 µs ± 26.8 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
a8 = 500500 :188 ns ± 15.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
a9 = 500500 :16.4 µs ± 1.3 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
a10 = 500500 :4.76 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

 やはり最初の見立て通り、a8の公式を使った処理が最速で、Nの大きさによらないパフォーマンスですね。そして、a7の再帰処理、遅いですね。そしてnumpyを使ったa10は、10回と1000回でそんなに差が出ないという面白い結果になりました。大量データを扱うための工夫がありそうですね。あと、a2とa3は予想通り、a3の方が若干遅めですね。

 ごらんの通り、遅いと言ってもすべて1ミリ秒以内で完了しているので、今回のケースでは、ほとんど誤差、と考えても差し支えないでしょうね。
(1秒=1,000ミリ秒=1,000,000マイクロ秒=1,000,000,000ナノ秒)

 1から10までの合計を出すだけでも、いろいろなやり方があって、面白いですね!

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

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

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

できあがりイメージ

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

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

ソースコード

import tkinter as tk


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

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

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

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

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

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

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

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


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

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

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

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


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

説明

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

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

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

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

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

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

まとめ

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

Python プログラミング Enum

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

 今回は、Pythonプログラムでコンスタント値を扱うのはどうするのかなぁ、と調べていて発見した、Enumを紹介したいと思います。

ソースコード

import enum


class PaperSize(tuple, enum.Enum):
    # Paper size
    A0 = (841, 1189)
    A1 = (594, 841)
    A2 = (420, 594)
    A3 = (297, 420)
    A4 = (210, 297)
    A5 = (148, 210)

    def __str__(self):
        return self.name

 このコードを例えばconstants.pyのようなファイルに保存することで、モジュールが作成できます。利用するためには、

from constants import *

 のようにすればよいでしょう。
※追記:この、インポートで*を使うのはよく見かけますが、バッドプラクティスだそうです。名前空間を汚染するから、というのがその理由です。なるほど。きちんと名前を指定しましょう。

 さて、標準モジュールにあったenum.Enumですが、ちゃんとドキュメントがありました。読んでもイマイチわからないので(笑)、とりあえず使い方ですね。

In [69]: for paper in PaperSize:
    ...:     print(paper)
    ...:     
A0
A1
A2
A3
A4
A5

 このように、イテレータとして使うことができます。

In [70]: PaperSize.A0
Out[70]: <PaperSize.A0: (841, 1189)>

In [71]: PaperSize.A0[0], PaperSize.A0[1]
Out[71]: (841, 1189)

 また、A0、A1といった、それぞれの要素を直接参照することができます。

In [72]: for paper in PaperSize:
    ...:     print(paper.name, paper.value)
    ...:     
A0 (841, 1189)
A1 (594, 841)
A2 (420, 594)
A3 (297, 420)
A4 (210, 297)
A5 (148, 210)

 このように「name」「value」といった属性が利用できます。

まとめ

 このような、複数の値を保持する必要がある場合は、enumモジュールのEnumを利用できるかも知れませんね。

Pythonプログラミング Spyder設定について

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

 今日は、Python Anacondaで提供されているSpyderというIDEの設定についてです。ちょうど先ほどまで勉強会をしていたのですけど、動きが遅い、というクレームがあったのですよねぇ。確かに遅い、と、思いましたが、カーソルの表示はちゃんとしていたので、わざと遅くしているんじゃないかなぁ、と、いうことで調べてみました。

何が遅いのかというと

 動画をご覧ください。メソッドや変数名などの名称をクリックすると、同じ名称のものをハイライトしてくれるのですが、ハイライトされるまでの時間がかかりすぎです。うん、確かに遅いよね。

ハイライトされるまでの時間

 クリックされてカーソルが表示されるのは一瞬ですね。ただ、そのあとの薄い黄色に反転するまでに、1.5秒ほどでしょうか、かかっていますね。

設定

 設定を調べたら、ありましたよ。

ツール(T)-設定(F)

 「ツール(T)」の「設定(F)」を選択します。

「エディタ」の「表示」プロパティ

 開いた「設定」ウィンドウの「エディタ」を選択すると、ありました。「表示」の「変更後に構文を強調するまでの時間」が「1500ms」に設定されていました。はい、1.5秒ほどですね。

 この値を300msほどに設定しなおすと、サクサク表示されました。

まとめ

 変更後に構文を強調するまでの時間まで設定可能とは、Spyderもなかなかやりますねぇ。

Python プログラミング __name__ってなに?

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

 お友だちから、__name__のことがよく分からない、と、質問をいただいたので、実際にどうなっているのか、実行結果を確認しつつ、説明したいと思います。

はじめに

 まず初めに、dir()という組み込み関数がありまして、コマンドインタプリターで実行すると、現在のローカルスコープにある名前のリストを返してくれます。

In [1]: print(dir())
['In', 'Out', '_', '__', '___', '__builtin__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', '_dh', '_i', '_i1', '_ih', '_ii', '_iii', '_oh', 'exit', 'get_ipython', 'quit']

 出力結果の1行目の右端にありますね。こんな感じで、「__name__」という名前がPythonインタープリタの中に事前定義されています。さて、こちらがなにか、ちょっと調べてみます。組み込み関数のtype()を使うとオブジェクトの型を返してくれます。まずはtype()で型を調べます。

In [2]: type(__name__)
Out[2]: str

 なるほど、ここで、「__name__」が文字列の変数であることがわかりました。中身を見てみましょう。

In [3]: print(__name__)
__main__

 変数「__name__」の中身は「”__main__”」ということがわかりました。

ファイルをつくって確認します

 コマンドインタープリタで確認してみると、セットされた値は「__main__」でした。この値は、「__main__」以外に設定されることはあるのでしょうか?利用されている箇所で知っているのはファイルの中でした。と、いうことで、以下のように「mynameis.py」ファイルを作ってみました。

"""" __name__ にどんな値がセットされているか、確認します。 """

print("My name is " + __name__)

 単純に、値をprint()するだけの記述を含むファイルです。
さて、実行してみます。

In [4]: %run mynameis.py
My name is __main__

 フツーに実行すると、やはり「__main__」がセットされています。ちなみに%runはIPythonのマジックコマンドで、指定されたファイルを直接実行します。コマンドプロンプトだと、「python mynameis.py」と実行するのと同じ意味になります。
さて、次にこの「mnameis.py」ファイルがモジュールだということにして、importしてみます。

In [5]: import mynameis
My name is mynameis

 importすると、mynameis.pyの内容が読み込まれ、すべて上から順番に実行されます。ただし、「__name__」には、このようにモジュール名がセットされることになっています。モジュールをつくるときは、この違いを利用して、

if __name__ == "__main__":

というふうにモジュール内にif文を記載することで、importから実行されたときと、%runなどで直接実行されたときのふるまいを変更することができます。

まとめ

 モジュールを作るとき、importで呼び出して利用するだけでなく、直接実行することができるようにするために、「__name__」を利用することができます。

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

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

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

出来上がりイメージ

ログインダイアログ

ソースコード

import tkinter as tk
from tkinter.simpledialog import Dialog

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

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

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

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

説明

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

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

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

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

まとめ

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

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

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

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

出来上がりイメージ

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

ソースコード

import tkinter as tk

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

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

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

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

説明

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

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

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

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

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

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

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

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

まとめ

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

 

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

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

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

出来上がりイメージ

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

ソースコード

import tkinter as tk

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

説明

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

まとめ

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