Update 2023.11.19 2017.05.05

Python デザインパターン サンプルコード Command
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

Java サンプルより機能が豊富なお絵かきソフトができました。

左押ドラッグで線を描き,右押ドラッグで消しゴムになります。描く線の色は,RGB各256階調のフルカラーであり,線の太さ1~15ピクセルから選べます。

でも,Command パターンではありません。実は,Java サンプルも Command パターンとして完成していません。undo, redo 機能がないのです。

お絵かきソフトのオブジェクトの扱いが Command パターンと相性が悪いような気がします。

ダウンロードできるソースコードには,お絵かきソフトの他に Java サンプルの骨格をそのまま残してありますので,誰かいじってみてください。


きちんと動作するCommandパターンはこのWebサイトの別記事を参照してください。 https://yamakatsusan.web.fc2.com/pythonpattern123.html
『Mark Summerfield『実践 Python 3』のデザインパターン』

◆◆Command パターンの使われる場面◆◆

要は,いわゆる undo(取り消し)や redo(再実行)をする方法が Command パターンである。ポイントは,undo(取り消し)や redo(再実行)の対象をオブジェクトにすることである。


(2023-11-19)Python3.11で動作確認済み


◆◆Command パターンとは◆◆

GoFによれば,Command パターンの目的は, 「要求をオブジェクトとしてカプセル化することによって,異なる要求や,要求からなるキューやログにより,クライアントをパラメータ化する。また,取り消し可能なオペレーションをサポートする。」

GoFによれば,Command パターンの別名は Action,Transaction です。

GoFによれば,次のような場合に Command パターンを使用することができる。
・先にあげた MenuItem オブジェクトのように,実行する動作によりオブジェクトをパラメータ化したい場合。手続き型言語では,そのようなパラメータ化をコールバック関数を使って表現する。すなわち,コールバック関数を,呼び出してほしいところに登録しておく,という形になる。Command パターンでは,そのようなコールバック関数の代わりにオブジェクトを使う。
(以下省略)

GoFによれば,Command パターンの関連するパターンは次のようなものである。
Composite パターン:MacroCommand クラスを実装するために Composite パターンを使うことができる。
Memento パターン:command がその実行結果を取り消すことができるように,状態を保存しておくことができる。
Prototype パターン:command を履歴リストに入れる前にコピーする場合,コピー元のオブジェクトは prototype として振る舞っている。

◆◆ tkinter の解説と実装◆◆

tkinter に限らず GUI は,1つのパーツが1つのクラスになっていて,インスタンス化して使います。tkinter はモジュールです。

上の図で,一番外の枠は tkinter.Frameです。絵を描いてある部分が tkinter.Canvasです。import の方法によっては,モジュール tkinter を省略したり,略して書いたりすることができます(以下の説明ではモジュールを Tk と略す)。

このサンプルでは,Frame のサブクラスを使っていますので,メインルーチンでインスタンス化して pack() しています。この2つは必ず対ペアで使います。mainloop() は,tkinter の起動であり 1回しか動作しません。1回しか動作させないのでコンストラクタにおいてもかまいません。

Frame のサブクラスを使わなければ,上の3行をメインルーチンに置かなくてもかまいません。このような場合は,一番外の枠は window = Tk.Tk()とすることが多いです(インスタンス名は任意)。これは pack()しません。Tk()の次には Tk.Frame を使うことが多いです。これは pack()します。

各パーツは上から順に配置していきます。サンプルでは,Tk.Canvas もパーツと同じ扱いです。その他にパーツには,Tk.Label, Tk.Button, Tk.Entry, Tk.MssageBox などがあります。

少し特殊なものとして,サンプルにもある Tk.Scale やプルダウンメニューなどもあります。みな pack()することで配置されます。pack()の代わりに grid(row=, column=)もあります。これは場所を指定できます。

円や四角形やいろいろな図形を描くクラスがたくさんありますが,Webを参考にしてください。

自由な線を引くには,create_line を使います。この場合はイベントドリブンというテクニックを使います。マウスの左ボタンを押すとかマウスを動かすとか(合わせてドラッグという)するときイベントドリブンが発生し起動するメソッドを指定するのが bind です。

イベントドリブンの起動時に座標が勝手に付いてきますのでその書き方はソースコードを参考にしてください。引数にはない座標を取り扱いを間違えると上手く動作しません。

このサンプルでは,自由な線を描く create_line を Tk.Canvas のサブクラスに置いています。これだけ別のクラスにすることでインスタンス化してオブジェクトを独立させようと目論んだのですが,一筆が1つのオブジェクトにならなくて上手く操作できませんでした。

盤上にある押しボタンをクリックされたときもイベントドリブンが発生し起動するメソッドを指定するのが command です。パラメータの詳細はWebを参考にしてください。

以上でポイントはすべて説明しました。ポイントを押さえると tkinter はそんなに難しくありません。インスタンスを意識的に使うと上手く行きます。パラメータの詳細はWebを参考にしてください。

◆◆ソースコード◆◆

このWebページの左上隅からダウンロードできます。

ソースファイルは1つです。
・Command.py;tkinter を使ったお絵かきソフト

【Command.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from abc import ABCMeta, abstractmethod
import tkinter as Tk

class Command(metaclass=ABCMeta):

    def execute():
        pass

class MacroCommand(Command):
    # 命令の集合
    # 実行
    def execute():
        pass
    # 追加
    def append(cmd):
        pass
    # 最後の命令を削除
    def undo():
        pass
    # 全部削除
    def clear():
        pass

class DrawCommand(Command):
    def DrawCommand(drawable, position):
        self.drawable = drawable
        self.position = position
    # 実行
    def execute():
        drawable.draw(position.x, position.y)

class Drawable(metaclass=ABCMeta):

    def draw(x, y):
        pass

class DrawCanvas(Tk.Canvas, Drawable):
    draw_canvas = None
    def __init__(self, x0, y0, x1, y1, **key):
        self.id = self.draw_canvas.create_line(x0, y0, x1, y1, **key)
        self.draw_canvas.bind('<ButtonPress-3>', self.mouse_right_click)
        self.draw_canvas.bind('<B3-Motion>', self.mouse_drag)
    # 消しゴム
    def mouse_right_click(self, event):
        self.sx = event.x
        self.sy = event.y
    def mouse_drag(self, event):
        self.draw_canvas.create_line(self.sx, self.sy, event.x, event.y,
            fill='white', width=10)
        self.sx = event.x
        self.sy = event.y

class MainFrame(Tk.Frame):
    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.master.title("Command Pattern Sample")
        # ウィンドウはメインルーチンで作られている
        #window = Tk.Tk()
        # クリアボタン
        self.clear_button = Tk.Button(
            self, text = "clear", command=self.clear_click)
        self.clear_button.pack(side = Tk.TOP)
        # キャンバス(別のクラス)
        self.canvas = Tk.Canvas(
            self, bg = "white", width = 500, height = 500)
        self.canvas.pack()
        # お絵かきのためのマウス操作のイベントドリブン
        self.canvas.bind('<ButtonPress-1>', self.mouse_left_click)
        self.canvas.bind('<B1-Motion>', self.mouse_drag)
        # 調合した色のサンプル
        self.label = Tk.Label(self, text='Selected', fg='#ffffff', width=15)
        self.label.pack()
        # 色の調合のためのスケール
        self.R_scale = Tk.Scale(
            self, label='Red', orient=Tk.HORIZONTAL, from_=0, to=255,
            length=300, resolution=1, command=self.palette)
        self.R_scale.set(255)
        self.R_scale.pack()
        self.G_scale = Tk.Scale(
            self, label='Green', orient=Tk.HORIZONTAL, from_=0, to=255,
            length=300, resolution=1, command=self.palette)
        self.G_scale.pack()
        self.B_scale = Tk.Scale(
            self, label='Blue', orient=Tk.HORIZONTAL, from_=0, to=255,
            length=300, resolution=1, command=self.palette)
        self.B_scale.pack()
        # 線の太さを選ぶ
        self.line_width = Tk.Scale(
            self, label='Line Width', from_=1, to=15, orient=Tk.HORIZONTAL)
        self.line_width.set(5)
        self.line_width.pack()
    # コンストラクタ終
    # 左ボタンクリック
    def mouse_left_click(self, event):
        self.sx = event.x
        self.sy = event.y
    # マウスドラッグ
    def mouse_drag(self, event):
        DrawCanvas.draw_canvas=self.canvas
        DrawCanvas(self.sx, self.sy, event.x, event.y,
            fill=self.select_color, width=self.line_width.get())
        self.sx = event.x
        self.sy = event.y
    # 色の調合
    def palette(self, event):
        self.select_color = '#%02x%02x%02x' % (
            self.R_scale.get(), self.G_scale.get(), self.B_scale.get())
        self.label.configure(bg=self.select_color)
    def clear_click(self):
        self.canvas.delete("all")

if __name__ == '__main__':
    app = MainFrame()
    app.pack()
    app.mainloop()

以上

トップページに戻る
inserted by FC2 system