Python tkinter GUIプログラミング Canvas の項目リサイズ

 こんにちは。すっかり秋になりましたね。石川さんです。久しぶりの投稿になりました。

 今回は、tkinterのCanvasに四角形を描いて、それの大きさをマウスで変更する、というのにチャレンジしました。

出来上がりイメージ

出来上がりイメージ

 今回使用するtkinterのオブジェクトはCanvasだけです。四角形はcreate_rectangleで描いて、マウスが上に来たら、それっぽいポインターに変わります。その上でドラッグすると、サイズが変更される、というシンプルなプログラムです。

ソースコード

import tkinter as tk


class App(tk.Tk):
    MARGIN = 5

    def __init__(self):
        super().__init__()
        self.geometry("800x700")
        self.title("Canvas item resize example")

        self.canvas = c = tk.Canvas(self, background="white")
        c.pack(fill=tk.BOTH, expand=True)

        x1, y1, x2, y2 = 100, 100, 400, 500
        self.rect = c.create_rectangle(x1, y1, x2, y2, width=App.MARGIN)
        c.bind("<ButtonPress>", self.press)
        c.bind("<ButtonRelease>", self.release)
        c.bind("<Motion>", self.move)
        self.coords = None
        self.position = None

    def get_cursor_position(self, x, y):
        left, right = x - App.MARGIN * 2, x + App.MARGIN * 2
        top, bottom = y - App.MARGIN * 2, y + App.MARGIN * 2
        x1, y1, x2, y2 = self.canvas.coords(self.rect)
        cursors = [
            ["ul_angle",          "sb_v_double_arrow", "ur_angle"],
            ["sb_h_double_arrow", "",                  "sb_h_double_arrow"],
            ["ll_angle",          "sb_v_double_arrow", "lr_angle"]
        ]
        if bottom < y1 or y2 < top:
            return "", None
        elif top <= y1 <= bottom:
            row = 0
        elif top <= y2 <= bottom:
            row = 2
        else:
            row = 1
        if right < x1 or x2 < left:
            return "", None
        elif left <= x1 <= right:
            column = 0
        elif left <= x2 <= right:
            column = 2
        else:
            column = 1
        return cursors[row][column], (row, column)

    def press(self, event):
        self.coords = self.canvas.coords(self.rect)

    def move(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        if self.coords and self.position:
            if self.position[1] == 0:  # Left
                self.coords[0] = x
            if self.position[0] == 0:  # Top
                self.coords[1] = y
            if self.position[1] == 2:  # Right
                self.coords[2] = x
            if self.position[0] == 2:  # Bottom
                self.coords[3] = y
            self.canvas.coords(self.rect, *self.coords)

        cursor, self.position = self.get_cursor_position(x, y)
        self.configure(cursor=cursor)

    def release(self, event):
        if self.coords:
            self.coords = None


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

解説

 9行目でウィンドウのサイズ、10行目でタイトルをセットしています。12、13行目、Canvasをつくって配置しています。15、16行目のcreate_rectangle()で四角形を描いています。17~19行目ではbindを使って、<ButtonPress>マウスボタンが押されたとき、<ButtonRelease>マウスボタンが離されたとき、<Motion>マウスが動いたとき、に、それぞれ処理をあてがっています。21、22行は後の処理で利用するときにエラーとならないよう変数を初期化しています。

 23~48行目のメソッド、get_cursor_positionでは、マウスの現在位置からカーソルの種類と位置を戻しています。ポイントは27行目のcursorsリストを配列のように使っているところです。3×3の行列をイメージして、6種類のカーソルを場所によって特定するようにしています。ちなみに26行目のcoordsメソッドは、self.rectの左上の座標と右上の座標を順番に返します。

 50、51行目のメソッド、pressはマウスボタンが押されたときに呼び出される処理です。ここでインスタンス変数のcoordsへ値をセットしています。この値がセットされていることで、ボタンが押されている、ということが他の処理からでもわかるようになっています。ちなみに69~71行目のreleaseはマウスボタンが離されたときに呼び出される処理で、この値(coords)をNoneにセットすることでマウスボタンは押されていない、ということがわかります。

 53~67行目のmoveメソッドが主な処理部分ですね。前半部分はサイズを変更する処理で、後半部分はカーソルの形状を変更する処理です。54行目のcanvasxとcanvasyは、座標系の変更をしています。eventから取得したx、yは、Canvas上のx、yと異なり、画面系の座標なのでcanvasx、canvasyで変換して使う必要があります。55行目の判定は、self.coordsはマウスボタンが押されているときに限るための条件、self.positionはカーソルが近くにある時に限るための条件になっています。<Motion>イベントに対する処理なので、実行される条件をきちんと絞り込む必要がありますね。
 64行目、再びcoordsが登場するのですが、26行目とは違い、第二引数があります。前回は座標を取得するだけでしたが、この第二引数が指定された場合には、項目の座標を第二引数のとおりに変更する、という点が異なります。この一文を指定することで、マウスポインタの移動にあわせて四角形を変形させています。
 66、67行目はカーソルが角っこに移動したときにカーソルの形状を変更するためにここへ入れました。また、ここでself.positionをセットすることで、四角形から離れた場所からのドラッグでリサイズできるようになっています。

まとめ

 いろいろと試行錯誤していましたので条件判定が割合スッキリとできたのではないでしょうか。今回は簡単にするために、四角形はクラス化せずにひとつだけ用意しておくようにしました。実際の現場だったりするとひとつだけ用意すればよい、ということはほとんどないと思いますので、クラス化もやってみたいですね。

在宅勤務ー5 Windows10 Pythonでディスプレイの解像度を変更する

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

 ここのところ、暑さが少し和らいで、少しだけですが秋の気配を感じ始めましたね。
さて、今日は仕事に便利な裏技を紹介したいと思います。「ディスプレイの解像度」と「テキスト、アプリ、その他の項目のサイズを変更する」の値をワンアクションで変更できるようにする方法です。

ディスプレイの設定

 ちなみに、この設定は、画面上の何もないところを右クリックして表示されるコンテキストメニューの、「ディスプレイの設定」を選択することで表示できます。

ディスプレイ設定

やり方

 ぼくは、ここのところPythonにハマっていますので、Pythonを使ってできないかなぁ、ということで調べてみました。以下のPythonスクリプトで変更可能です。ぼくはデスクトップにこのスクリプト置いて、ダブルクリックして実行しています。あ、拡張子は「.py」ファイルにして、この拡張子のファイルをPython.exeで実行するように定義しました。

''' ディスプレイの解像度と倍率を変更します '''
import win32con
import win32api
import subprocess
import time
import pyautogui

proc = subprocess.Popen('DpiScaling') # ディスプレイの設定を開いておきます。
time.sleep(3)
proc.wait()

DEVICE = win32api.EnumDisplayDevices(None, 0)
DEVMODE = win32api.EnumDisplaySettings(DEVICE.DeviceName, win32con.ENUM_CURRENT_SETTINGS)

if DEVMODE.PelsWidth == 2160:
    DEVMODE.PelsWidth = 1920
    DEVMODE.PelsHeight = 1080
else:
    DEVMODE.PelsWidth = 2160
    DEVMODE.PelsHeight = 1440

DEVMODE.Fields = win32con.DM_PELSWIDTH | win32con.DM_PELSHEIGHT
win32api.ChangeDisplaySettings(DEVMODE, 0)


window = pyautogui.getWindowsWithTitle('設定')[0]
if window:
    window.activate()
    if DEVMODE.PelsWidth == 1920:
        pyautogui.typewrite(['tab','tab','up','up'])
    else:
        pyautogui.typewrite(['tab','tab','down','down'])
    pyautogui.hotkey('alt', 'f4')

ソースコードの説明

 今回の設定変更ですが、ノートパソコンからリモートデスクトップ接続で他のパソコンにログインしたときに、解像度が一致していないと挙動がおかしくなることがあって、その対応としてはリモート接続先のパソコンの解像度にあわせてからログインするとうまくいく、ということだったのだけど、何度も設定を切り替えるのが面倒になってきたので、スクリプトを用意して、ダブルクリック一回だけで切り替わらないかなぁ、というのが事の発端です。ちなみにぼくはWindows10のノートパソコン(Let’s Note)を使っています。

テキスト、アプリ、その他の項目サイズを変更するディスプレイの解像度
通常150%(推奨)2160 × 1440(推奨)
リモート時100%1920 × 1080
ディスプレイ設定の切り替え

 このほかにもプロジェクターで発表するときや、Zoomなどのウェブ会議なんかで利用できるかも、と、思っています。

 まずは、「ディスプレイの解像度」の方ですが、こちらはGoogle先生に聞いて、すぐに解決できました。win32apiChangeDisplaySettingsを利用します。

 12行目の「DEVICE = win32api.EnumDisplayDevices(None, 0)」で現在のディスプレイ設定の名称などを取得しています。この0のところを1、2と増やしていって他のデバイスも取得できるらしいので、ちょっと試してみたところ、ぼくの環境では「\\.\DISPLAY1」「\\.\DISPLAY2」「\\.\DISPLAY3」と三つのDEVICE.DeviceNameが取得できました。モニターを複数つないだ時に増えるのかと思っていたのですが、違いました。DeviceIDは同じだったので、同じモニターの異なる設定を持っているのかも知れませんが、とりあえず深追いはやめました。どなたか知っているひとがいたら、教えてください♪

 13行目の「DEVMODE = win32api.EnumDisplaySettings(DEVICE.DeviceName, win32con.ENUM_CURRENT_SETTINGS)」で現在のディスプレイ設定の詳細な設定値を取得しています。ちなみに「\\.\DISPLAY2」「\\.\DISPLAY3」を指定しても以下のようなエラーが発生するだけで設定値は取得できませんでした。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
pywintypes.error: (123, 'EnumDisplaySettings', 'ファイル名、ディレクトリ名、またはボリューム ラベルの構文が間違っています。')
>

 次に15行目のPelsWidthのチェックですが、現在値が2160のとき切り替えして、そうじゃないときに元の値に戻すようにするために記載してあります。ここは皆さんの事情と環境に応じて書き換える必要がある個所です。22行目の「DEVMODE.Fields = win32con.DM_PELSWIDTH | win32con.DM_PELSHEIGHT」を指定することでその次の23行目の「win32api.ChangeDisplaySettings(DEVMODE, 0)」呼び出しで、指定してPelsWidthPelsHightの値を反映してくれるようになります。

 26行目からは、「テキスト、アプリ、その他の項目のサイズを変更する」の変更です。最初のうちは、上記のChangeDisplaySettingsでこちらも設定できるのでしょうね、と思い込んでしまって、ずいぶんと遠回りしてしまいました。Scaleという項目があるので、これでできそうだと思ったのですよねぇ。いろいろ調べて回った結果、最終的に、pyautoguiで実現することにしました。pyautoguiはGUIで操作することをプログラムから再現するためのモジュールですので、実行中はマウスを触ったりキー入力したりしないようにお願いしますね。途中でクリックしたりすると、正しく動作しなくなりますので。まずは「設定」というタイトルのウィンドウを取得していますが、これは、8行目の「subprocess.Popen('DpiScaling')」で事前に実行しておいた、ディプレイ設定の画面を取得しています。

 設定ウィンドウが取得できた場合に、そのウィンドウをアクティブにして、タブキーを二回押して項目移動して、↑または↓の矢印キー2回で設定を変更しています。終わったら「ALT+F4」で設定画面を終了するようにキー入力しています。もしかしたらこのキー入力は環境によって異なるかも知れませんので、それぞれ変更して利用するようにお願いします。ちなみに、このpyautoguiを使って「ディスプレイの解像度」まで変更できないのか、というツッコミもありそうですけど、できますね。ただ、現在値から設定する値を判断するためにwin32api.EnumDisplaySettingsを使う必要があったので、どうせ使うなら設定変更まで、ということでこのようになっています。

 ちなみに、最初はこちらの「テキスト、アプリ、その他の項目のサイズを変更する」の設定を、ウィンドウズのレジストリを変更して対応する、winregというモジュールを使うやり方を調べて実現できたので、そちらで説明を書こうとしていました。しかし、説明を書きながら確認していくと、思った動きと反対の動きになっていたり、レジストリの値が思ったように変更されなかったり、ということがあったので、pyautoguiを使うように変更しました。ホントは完全にプログラムから実行したかったのですけどねぇ。さすがにレジストリの変更を無責任におすすめできませんよね。でもせっかく調べたので、参考までに、winregを使ったやり方を掲載しておきます。後半部分が異なります。SCALEは、0が推奨値で、正の値は拡大した値、負の値は縮小した値です。今回、-2が推奨値から二つ縮小した値の100%を表しているのですが、実際には4294967294を設定しています。これは、16進数の0xFFFFFFFEを符号なし整数値で表現した値になります。

''' ディスプレイの解像度と倍率を変更します '''
import winreg
import win32con
import win32api

DEVICE = win32api.EnumDisplayDevices(None, 0)
DEVMODE = win32api.EnumDisplaySettings(DEVICE.DeviceName, win32con.ENUM_CURRENT_SETTINGS)

if DEVMODE.PelsWidth == 2160:
    DEVMODE.PelsWidth = 1920
    DEVMODE.PelsHeight = 1080
else:
    DEVMODE.PelsWidth = 2160
    DEVMODE.PelsHeight = 1440

DEVMODE.Fields = win32con.DM_PELSWIDTH | win32con.DM_PELSHEIGHT
win32api.ChangeDisplaySettings(DEVMODE, 0)


REG_PATH = r"Control Panel\Desktop\PerMonitorSettings"
REG_PATH += r"\※ここはそれぞれの値をレジストリで調べて変更してください※"
KEY = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_WRITE)
if DEVMODE.PelsWidth == 1920:
    SCALE = 0
else:
    SCALE = 4294967294  # -2
winreg.SetValueEx(KEY, "DpiValue", 0, winreg.REG_DWORD, SCALE)
winreg.CloseKey(KEY)

まとめ

 最終的にはpyautoguiがあれば、何でも自動化できそうですね。

追記 2021年1月7日

 上記のスクリプト、ときどきうまくいかないことがあって、しばらく理由がわからなかったのですけど、やっとわかりました!

解像度変更時の通知

 そう!解像度を変更したときにこの通知が画面右下に登場して、どうやらフォーカスを奪っていたのが原因だったようです。スクリプトでは、解像度を変更(→通知が表示)→倍率を変更、というふうに実行していたので、通知が表示するタイミングによって、倍率が変更できないことがあった、ということでした。それに気づいたので、先に倍率を変更してから解像度を変更することにしました。スクリプトで言うと、解像度を変更する部分(最初のスクリプトの23行目)「win32api.ChangeDisplaySettings(DEVMODE, 0)」をスクリプトの一番最後に移動するだけです。

 これでスッキリしました!!!

Python Tkinter GUIプログラミング Text tag

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

 気が付けば、1ヶ月以上も更新していませんでした。採用活動とか、夏休みとか、もろもろあって、忙しかった、かも、と、言い訳してみましたが、よくよく考えてみると、Amazonプライムで海外ドラマの「Elementary」をずっと見ていたのが原因です。すみません。言い訳はよくないですね。

 さて、気を取り直して。ここのところ取り組んでいたtkinterの中にちょくちょく登場してくるタグ(tag)の考え方でちょっと気になったことを調べるためにプログラムを書いてみました。今回はTextで使われるタグについて、です。タグは通常、Textの文字列に色を付けたりフォントを変えたりするのにつかいます。気になりポイントは、同じ文字列にタグをセットしたとき、どちらが有効になるのか、ということです。

 急いでいる方のために結論を言いますと、タグは設定した順に積まれていき、最後の設定が有効になります。前に設定したものを優先するためには、積んだ順を入れ替えれば大丈夫です。

できあがりイメージ

タグの優先順位を変更するプログラム

 プログラムを実行すると、上部のテキストボックスに、赤、緑、青の文字列が初期値として表示された画面が表示されます。「Set Color」ボタンを押すことで、真ん中のラジオボタンで選択した色を、選択した文字列の文字色(foreground)か背景色(background)に、タグと色を付けます。

 下部の一覧は、タグ一覧です。行を選択して、「↑」、「↓」でタグを上下に移動、「DEL」ボタンでタグを削除できます。このボタンでタグを上下することで優先順位が変わった時の挙動が確認できます。行の内訳は、左側がタグ名で、括弧の中が[開始 – 終了]を表します。中の数字は、「行.列」を表していて、行は1オリジン、列は0オリジンの数値がセットされています。

 タグ一覧の中にある「sel」は、特殊なタグで、自動的に作成されます。現在選択されている範囲を表すタグです。

ソースコード

import tkinter as tk
from tkinter import colorchooser


class Application(tk.Tk):
    ''' Textタグの優先順位を確認します。
    タグをセットするためのTextの作成。
    色をセットするためのフレームの作成。
    タグの内容を表示するためのリストボックスを作成。
    リストボックスの名前を移動したり削除したりするためのフレームを作成。
    '''
    def __init__(self):
        super().__init__()
        self.title("Tag Priority")

        self.text = tk.Text(self)
        self.text.pack()
        self.text.bind("<KeyRelease>", self.refresh_list)
        self.text.bind("<ButtonRelease>", self.refresh_list)

        frame = tk.Frame(self)
        self.color = tk.StringVar(value="red")
        radio_red = tk.Radiobutton(frame, text="Red",
                                   variable=self.color, value="red")
        radio_red.pack(side=tk.LEFT)
        radio_green = tk.Radiobutton(frame, text="Green",
                                     variable=self.color, value="green")
        radio_green.pack(side=tk.LEFT)
        radio_blue = tk.Radiobutton(frame, text="Blue",
                                    variable=self.color, value="blue")
        radio_blue.pack(side=tk.LEFT)
        radio_color = tk.Radiobutton(frame, text="Color",
                                     variable=self.color, value="color",
                                     command=self.set_palet_color)
        radio_color.pack(side=tk.LEFT)

        self.color_palette = tk.Canvas(frame, width=20, height=20,
                                       background="white")
        self.color_palette.pack(side=tk.LEFT)
        self.color_palette.bind("<Button-1>", self.set_palet_color, add=True)

        self.option = tk.StringVar(self)
        self.option.set("foreground")
        combo = tk.OptionMenu(frame, self.option,
                              *{"foreground", "background"})
        combo.pack(side=tk.LEFT)

        button = tk.Button(frame, text="Set Color",
                           command=self.set_color)
        button.pack(side=tk.RIGHT)
        frame.pack()

        self.list = tk.Listbox(self)
        self.list.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        updown_frame = tk.Frame(self)
        up_button = tk.Button(updown_frame, text="↑", command=self.tag_up)
        up_button.pack()
        del_button = tk.Button(updown_frame, text="DEL", command=self.tag_del)
        del_button.pack()
        down_button = tk.Button(updown_frame, text="↓", command=self.tag_down)
        down_button.pack()
        updown_frame.pack(side=tk.LEFT)

        # テキストの初期値
        self.text.insert(1.0, "*****Red******\n*****Green****\n*****Blue*****")
        self.text.tag_add("red", 1.0, "1.end")
        self.text.tag_config("red", foreground="red")
        self.text.tag_add("green", 2.0, "2.end")
        self.text.tag_config("green", foreground="green")
        self.text.tag_add("blue", 3.0, "3.end")
        self.text.tag_config("blue", foreground="blue")

        self.refresh_list()

    def set_color(self):
        ''' 選択されたTextの文字列に色を付けます。 '''
        tag_range = self.text.tag_ranges('sel')
        if not tag_range:
            return
        option = self.option.get()
        color = self.color.get()
        if color == "color":
            color = self.color_palette.cget("background")
        tagname = "t_"+option+"_"+color+"_"+str(len(self.text.tag_names()))
        self.text.tag_add(tagname, *tag_range)
        self.text.tag_config(tagname, {option: color})
        self.refresh_list()
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))

    def refresh_list(self, event=None):
        ''' リストを更新します。 '''
        # pylint: disable=unused-argument
        self.list.delete(0, tk.END)
        for i, tag in enumerate(reversed(self.text.tag_names())):
            tag_range = self.text.tag_ranges(tag)
            if tag_range:
                self.list.insert(i, tag + f"[{tag_range[0]} - {tag_range[1]}]")
            else:
                self.list.insert(i, tag + "[未選択]")

    def tag_up(self):
        ''' タグを一行上に移動します。'''
        self.tag_move(-1)

    def tag_down(self):
        ''' タグを一行下に移動します。'''
        self.tag_move(1)

    def tag_move(self, offset):
        ''' タグを移動します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)
        self.list.insert(active_position + offset, active_value)
        self.list.activate(active_position + offset)
        self.list.see(active_position + offset)

        active_tag = active_value.split("[")[0]
        above_or_below_tag = self.list.get(active_position).split("[")[0]
        if offset == 1:
            self.text.tag_lower(active_tag, above_or_below_tag)
        else:
            self.text.tag_raise(active_tag, above_or_below_tag)
        print('*'*20)
        for tag_name in reversed(self.text.tag_names()):
            print(tag_name, self.text.tag_ranges(tag_name))
        print('*'*20)

    def set_palet_color(self, event=None):
        ''' パレットの色をセットするためのダイアログを呼び出し、取得した色をセットします。 '''
        # pylint: disable=unused-argument
        initial_color = self.color_palette.cget("background")
        color = colorchooser.askcolor(color=initial_color)
        if color:
            print(color)
            self.color_palette.configure(background=color[1])
        self.color.set("color")

    def tag_del(self):
        ''' リストで選択されたタグを削除します。'''
        active_position = self.list.index(tk.ACTIVE)
        active_value = self.list.get(tk.ACTIVE)
        self.list.delete(active_position)

        active_tag = active_value.split("[")[0]
        self.text.tag_delete(active_tag)


def main():
    ''' メインプログラム(呼び出し用) '''
    main_application = Application()
    main_application.mainloop()


if __name__ == "__main__":
    main()

 今回は、ソースコードにpylintで静的コード分析を実施してみました。意外といろいろとうるさくて、クラスドキュメンテーション文字列がない、メソッドドキュメンテーション文字列がない、変数名、メソッド名のルールがおかしい、使用していない引数がある、カンマの後ろに空白がない、コロンの後ろに空白がない、空行が足りない、等々、いろいろとご指摘いただきました。

 リファクタリングの提案もしてくれていて、クラスのアトリビュートが多すぎるので、見直した方がよいですよ、と、言われていました。

ソースコードの説明

 12行目、__init__(self)でこのアプリケーションの全体を定義しています。16~19行目でTextを作成、定義しています。イベントの<KeyRelease><ButtonRelease>が発生したときに、一覧を更新するように定義しています。何らかのキー入力が終了したときか、マウスボタンクリックが終わった時に、refresh_list()を呼び出します。

 21~51行目、ラジオボタン、カラーパレットのキャンバス、オプションメニュー、「Set color」ボタンを作っています。特に34行目のラジオボタン「Color」が押されたときはカラーパレットを表示するように「command=self.set_palet_color」を定義しています。ここの部分は、もし40行目のbindと同じようにマウスクリックに定義してしまうと、ラジオボタンが選択できなくなってしまいますので、注意が必要です。

 53、54行目、リストボックスを作成、56~63行目で、「↑」「DEL」「↓」のボタンを作成しています。

 65~72行目でTextの初期値をタグ付きの色をセットして、74行目でリストボックスのリフレッシュしています。

 76~90行は、set_color()メソッドで、選択されたテキストの範囲にタグを定義しつつ色をセットしています。現在選択されている範囲を取得するのに、「tag_range = self.text.tag_ranges('sel')」を使用できます。’sel’は「tk.SEL」として定義されていましたので、こちらを利用した方がよかったかもしれません。tag_ranges()で範囲が取得できないとタグをセットするときにエラーになりますので、チェックしています。
 89行目、self.text.tag_names()でタグ名の一覧が取得できますが、作成した順番に格納されていくので、スタックされているイメージに合いません。なので、組み込み関数のreversed()を使って逆順リストを出力するようにしました。

 103~129行目まで、タグが移動できるように定義しています。ポイントは123行目と125行目のself.text.tag_lower(active_tag, above_or_below_tag)self.text.tag_raise(active_tag, above_or_below_tag)です。これらのメソッドは、二つ目の引数がないと一番上か一番下までタグが移動してしまうので、設定しました。上下ボタンでひとつずつ移動してほしかったので。二つ目の引数で指定したタグの下、または上に移動します。

 131~139行目はカラーを選択してもらうためのカラーパレットを表示する機能です。ラジオボタンの「color」を選択するか、色をクリックすると以下のようなウィンドウで色を選択できるようにしています。単にcolor = colorchooser.askcolor(color=initial_color)と、呼び出すだけで使えます。あ、colorchooserは、先頭の方でfrom tkinter import colorchooserとインポートしてあります。

色の設定ダイアログ

 141~148行目まで、タグを削除しています。リストからは、self.list.delete(active_position)を使って削除して、タグの一覧からはself.text.tag_delete(active_tag)でタグを取り除きます。

 151~154行目は、main()関数を定義しました。これまでは、main_application = Application()の行を、if __name__ == "__main__":の下へ記述していましたが、それだとpylintに叱られてしまいまして。なんと、コンスタント値の変数名は、UPPER_CASEの形式で宣言することが望ましいとのことです。Pythonにはコンスタント値という考え方がないので、モジュールレベルの変数はすべてコンスタント値でしょ、というふうに考えられているようです。main()関数の中に記述することでモジュールレベルの変数になることを避ける、というのが一般的な解決方法のようです。

まとめ

 今回はTextで利用されているタグを調べました。タグを使うとテキストの文字色や、背景色、フォントの設定などを変更することができます。複数のタグを使用した場合の優先順位は最後に定義されたタグの方が優先されます。あとからtag_raise()メソッド、tag_lower()メソッドを使って優先順位を変更することも可能です。

Python プログラミング matplotlib subplotの出力順を変更したい

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

 さて、今回は、matplotlibのsubplotとタイトルにありますが、メインはsubplotではありませんね。subplotの出力順を変更したい、というだけの、標準的なプログラミングのお話です。

やりたいこと

 通常、subplotを使うと、複数のグラフを並べて出力することができます。こんな具合です。こちらのサイトを参考に出力してみました。

subplotサンプル

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

import numpy as np
import matplotlib.pyplot as plt

x = np.random.rand(10)
y = np.random.rand(10)
z = np.sqrt(x**2 + y**2)
verts = np.array([[-1, -1], [1, -1], [1, 1], [-1, -1]])
markers = (">", (5, 0), verts, (5, 1), '+', (5, 2))

for i, m in enumerate(markers, 1):
    plt.subplot(3, 2, i)
    plt.scatter(x, y, s=80, c=z, marker=m)

plt.show()

 11行目の、plt.subplot(3, 2, i)でサブプロットを作成しています。最初の二つのパラメータは行列を表しています。今回は、3行2列を表します。iはインデックス値で、1から順番に以下のとおり、左から右、上から下へ、3行2列を以下のような順番で出力していきます。

subplotデフォルトの出力順

 これを以下のように出力したい、という要望です。

subplot出力順をこのように変更したい

 subplotのオプションか何かで簡単に変更できないかと思って調べてみましたが、そのようなオプションは見つけられませんでした。かわりにsubplotsを使って行列の配列を取得する方法があったので、これならば、今回のような工夫は必要なさそうです。ま、でも、今回は頭の体操ですね。もっと簡単な方法があれば、どなたか教えてください。

 1~6のループを割り算したり、余りを求めたり、numpy.transpose()が使えるかも、と、検討してみたけど、これは行列が同じ数のときしか使えないとか、いろいろと実験してみました。その結果、今回は、インデックスを[1, 3, 5, 2, 4, 6]というふうに出力すれば望みどおりに出力できることがわかりました。ここまでくればもうできたも同然ですね。コーディングしてみました。

import numpy as np
import matplotlib.pyplot as plt

x = np.random.rand(10)
y = np.random.rand(10)
z = np.sqrt(x**2 + y**2)
verts = np.array([[-1, -1], [1, -1], [1, 1], [-1, -1]])
markers = (">", (5, 0), verts, (5, 1), '+', (5, 2))


def index_generator(row, col):
    for c in range(col):
        for i in range(c + 1, row * col + 1, col):
            yield i


index = index_generator(3, 2)

for i, m in zip(index, markers):
    plt.subplot(3, 2, i)
    plt.scatter(x, y, s=80, c=z, marker=m)

plt.show()

 ポイントは11~14行目、知ったかぶりをして、ジェネレータを作ってみました。パラメータは行、列です。関数内では、列数分繰り返しますが、1から始めて列数ごとに数字を取り出していきます。これにより、3行2列を指定した場合、列の1回目のループで、1、3、5、を戻し、列の2回目のループで、2、4、6を取り出しています。

 14行目の「yield i」を使うことで関数がジェネレータになります。ジェネレータは、次の要素が取り出されるまで、値を生成しないのでメモリが節約できる、という特徴があります。

 17行目でindexを定義してインデックスの生成にはこちらを利用します。ということで、19行目のforループもenumerateをやめて、zipに変更しました。

まとめ

 割り算や余りを使って計算でできるのでは、と、簡単そうに思いましたが、実は、ちょっとひねりが必要だったという、よい問題でした。

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もなかなかやりますねぇ。