Tkinterでペイントもどきを作る

tkinterのcanvasで遊んでいる内に思いついたので
ちょっとづつ思いつくまま作っていきます

# マウスで簡単な絵が描けるモノを作る
まずはwindowsのペイントにある鉛筆を作ります
コードはこんな感じ

```python:PaintModoki.py
# -*- coding:utf-8 -*-
import tkinter

class PaintModoki:
    def __init__(self):
        # 操作中の図形のID
        self.curr_id = -1
        
        # メインウィンドウ作成
        root = tkinter.Tk()
        root.title("無題")

        # 画像表示用キャンバス作成
        self.canvas = tkinter.Canvas(root, bg="white")
        self.canvas.pack(expand=True, fill=tkinter.BOTH)
        # キーバインド
        self.canvas.bind("<ButtonPress-1>", self.on_key_left)
        self.canvas.bind("<B1-Motion>", self.dragging)

        root.mainloop()

    # マウス左ボタン押下
    def on_key_left(self, event):
        # 直線描画
        self.curr_id = self.canvas.create_line(event.x, event.y, event.x, event.y,
            fill = "black",width = 1)
 
   # ドラッグ中
    def dragging(self, event):
        points = self.canvas.coords(self.curr_id)
        points.extend([event.x,event.y])
        self.canvas.coords(self.curr_id, points)

if __name__ == '__main__':
    PaintModoki()
```

コードの説明
メイン画面一杯にcanvas一個だけ作成しマウスのマウス左ボタン押したときとドラッグ中のキーバインドを作成します。
紐付け先の関数で、左ボタン押下時に直線を作成しそのIDをインスタンス変数self.curr_idに格納します
ドラッグする度にその図形を変形して行くとcanvasに絵が描けます
![ペイントもどき](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2532786/04306301-7efc-194f-ecb9-18e8a354bb05.png "ペイントもどき")

***

30行程度でこんなモノが作れるのは良いですね。実際に動かして触ってみると色々機能を追加したくなるのでそれを作っていきます
まずは色の変更と線の太さを変更できるようにしましょう

# 線の太さと色の変更
色の変更はcolorchooserというダイアログがあるのでこれを使いましょう
ただ毎回colorchooserで色を選ぶのは面倒なので、よく使う色をパレットに置いておき1クリックで変更できるようにしましょうか。ここら辺はペイントのデザインをパクります
線の太さはEntryで入力させても良いのですがせっかくなので他で利用方法の思いつかないScaleを使ってみます

colorchooserの使い方
```python:colorchooser.py
import tkinter.colorchooser

color = tkinter.colorchooser.askcolor()   #colorchooser呼び出し
# color :((160, 160, 160), '#a0a0a0')
if color[1] is not None:    # キャンセル時は(None, None)が返る
    print(color[1]) # '#a0a0a0'

```
返り値は((R,G,B), "#RRGGBB")というタプルで返ってきます。#RRGGBB形式の方が都合が良いので
color[1]のみ使います。またキャンセルボタンを押したときは(None,None)が返るのでNoneを比較して
キャンセルの時は何もしないようにしないといけないです


カラーパレットはcanvasに矩形を描き、クリックされた矩形の色を取得するようにします
```python:colorpalette.py
# 現在の色表示及び色変更ボタン
color_canvas = tkinter.Canvas(frame_a, width=w, height=h, bg="white", bd =1)
# 黒
color_canvas.create_rectangle(3, 3, 22, 22, outline="white", activeoutline="skyblue1", fill="black")
# 赤
color_canvas.create_rectangle(23, 3, 52, 22, outline="white", activeoutline="skyblue1", fill="red")
# 他の色も似たように作成する

color_canvas.pack(side=tkinter.LEFT)
# キーバインド　マウス左クリック
color_canvas.bind("<ButtonPress-1>", self.select_color)

# カラーパレット押下時
def select_color(self, event):
    fill = ""
    # クリックした地点の図形を取得し、その色をself.colorに格納する
    for obj in color_canvas.find_overlapping(event.x,event.y,event.x,event.y):
        fill = color_canvas.itemcget(obj, "fill")
        break
    if fill != "":
        self.color= fill

```
文字で書くと簡単ですがコードを書こうとすると一色ずつ矩形を作成あたりが結構面倒です。

最後にScaleです。from_にだけアンダーバーが付く以外は特に書くことないですが
from_からtoまでスライドバーを動かすとvariableに入れた値がリニアに変化するモノです
太さの上限をいくつにするか迷ったのですが、適当に30にしてあります。もっと太い線を描きたい方はこの値を100とかにあげてください
```python:PaintModoki.py
# スケールの作成
self.thick = tkinter.IntVar()
slidebar = tkinter.Scale(frame_a,variable=self.thick,orient=tkinter.HORIZONTAL,length=100,from_=1,to=30,label="太さ")
slidebar.pack(side=tkinter.LEFT)

```

最後にコード全文を載せます

<details><summary>修正前コード</summary>

```python:PaintModoki.py
# -*- coding:utf-8 -*-
import tkinter
import tkinter.colorchooser

class PaintModoki:
    def __init__(self):
        
        self.curr_id = -1       # 操作中の図形のID
        self.thick = 1          # 線の太さ
        self.color = "black"            # 現在の色
        self.curr_color_canvas = None   # 現在の色を表示するキャンバス
        self.color_canvas = None        # 色パレットキャンバス
        
        # メインウィンドウ作成
        root = tkinter.Tk()
        root.title("無題")
        
        # 制御部フレーム作成
        frame_a = tkinter.Frame(root)
        
        # 現在の色表示及び色変更ボタン
        self.curr_color_canvas = tkinter.Canvas(frame_a, width=20, height=30, bg="white", bd =1)
        self.curr_color_canvas.pack(side=tkinter.LEFT)
        self.curr_color_canvas.create_rectangle(3, 3, 22, 32,outline="white",fill=self.color, tag="curr_color")
        self.curr_color_canvas.bind("<ButtonPress-1>", self.click_color_btn)
        
        # 色パレット作成
        col_cnt = 5 # 1行の色数
        xs = 3      # 開始座標 x方向
        ys = 3      # 開始座標 y方向
        dx = 1      # 色同士の間隔 x方向
        dy = 1      # 色同士の間隔 y方向
        len_x = 19  # 各色パレットの大きさ x方向
        len_y = 19  # 各色パレットの大きさ y方向
        # パレットに利用する色一覧
        colors = ("black","blue","red","yellow","green","gray","light blue","pink","lime","yellowgreen")
        # パレットとcanvasの大きさを計算する
        w = col_cnt * (len_x + dx)
        q,r = divmod(len(colors) , col_cnt)
        if r > 0 :
            q +=1
        h = q * (len_y + dy)
        # 色パレット用canvas
        self.color_canvas = tkinter.Canvas(frame_a, width=w, height=h, bg="white", bd =1)
        self.color_canvas.pack(side=tkinter.LEFT)
        for n,color in enumerate (colors):
            # パレットの大きさ、座標計算
            q,r = divmod(n, col_cnt)
            x1 = xs + (dx+len_x)*r
            y1 = ys + (dy+len_y)*q
            x2 = x1 + len_x
            y2 = y1 + len_y
            self.color_canvas.create_rectangle(x1, y1, x2, y2,outline="white",activeoutline="skyblue1",fill=color)
        # カラーパレットキーバインド
        self.color_canvas.bind("<ButtonPress-1>", self.select_color)
        
        
        # スケールの作成 線の太さ用
        self.thick = tkinter.IntVar()
        slidebar = tkinter.Scale(frame_a,variable=self.thick,orient=tkinter.HORIZONTAL,length=100,from_=1,to=30,label="太さ")
        slidebar.pack(side=tkinter.LEFT)
        
        frame_a.pack(fill=tkinter.X)
        
        # 画像表示用キャンバス作成
        self.canvas = tkinter.Canvas(root, bg="white")
        self.canvas.pack(expand=True, fill=tkinter.BOTH)
        
        # キーバインド
        self.canvas.bind("<ButtonPress-1>", self.on_key_left)
        self.canvas.bind("<B1-Motion>", self.dragging)

        root.mainloop()
    
    def on_key_left(self, event):
        # 直線描画
        self.curr_id = self.canvas.create_line(event.x, event.y, event.x, event.y,
            fill = self.color ,width = self.thick.get())
    
    def dragging(self, event):
        points = self.canvas.coords(self.curr_id)
        points.extend([event.x,event.y])
        self.canvas.coords(self.curr_id, points)
    
    # カラーパレット押下時
    def select_color(self, event):
        fill = ""
        # クリックした地点の図形を取得し、その色をself.colorに格納する
        for obj in self.color_canvas.find_overlapping(event.x,event.y,event.x,event.y):
            fill = self.color_canvas.itemcget(obj, "fill")
            break
        if fill != "":
            self.color= fill
            self.curr_color_canvas.itemconfig("curr_color", fill=self.color)
    
    # 色の変更ダイアログ表示
    def click_color_btn(self,event):
        color = tkinter.colorchooser.askcolor()   #colorchooser呼び出し
        if color[1] is not None:
            self.color = color[1]
            self.curr_color_canvas.itemconfig("curr_color", fill=self.color)
    
    
if __name__ == '__main__':
    PaintModoki()

```
</details>

```python:PaintModoki.py
# -*- coding:utf-8 -*-
import tkinter
import tkinter.colorchooser

class PaintModoki:
    def __init__(self):
        self.curr_id = -1       # 操作中の図形のID
        self.thick = None       # 線の太さ
        self.color = "black"            # 現在の色
        self.curr_color_canvas = None   # 現在の色を表示するキャンバス
        self.color_canvas = None        # 色パレットキャンバス
        self.canvas = None              # 描画用キャンバス
    
    def createwindow(self):
        # メインウィンドウ作成
        root = tkinter.Tk()
        root.title("無題")
        
        # 制御部フレーム作成
        frame_a = tkinter.Frame(root)
        
        # 現在の色表示及び色変更ボタン
        self.curr_color_canvas = tkinter.Canvas(frame_a, width=20, height=30, bg="white", bd =1)
        self.curr_color_canvas.pack(side=tkinter.LEFT)
        self.curr_color_canvas.create_rectangle(3, 3, 22, 32,outline="white",fill=self.color, tag="curr_color")
        self.curr_color_canvas.bind("<ButtonPress-1>", self.click_color_btn)
        
        # 色パレット作成
        col_cnt = 5 # 1行の色数
        xs = 3      # 開始座標 x方向
        ys = 3      # 開始座標 y方向
        dx = 1      # 色同士の間隔 x方向
        dy = 1      # 色同士の間隔 y方向
        len_x = 19  # 各色パレットの大きさ x方向
        len_y = 19  # 各色パレットの大きさ y方向
        # パレットに利用する色一覧
        colors = ("black","blue","red","yellow","green","gray","light blue","pink","lime","yellowgreen")
        # パレットとcanvasの大きさを計算する
        w = col_cnt * (len_x + dx)
        q,r = divmod(len(colors) , col_cnt)
        if r > 0 :
            q +=1
        h = q * (len_y + dy)
        # 色パレット用canvas
        self.color_canvas = tkinter.Canvas(frame_a, width=w, height=h, bg="white", bd =1)
        self.color_canvas.pack(side=tkinter.LEFT)
        for n,color in enumerate (colors):
            # パレットの大きさ、座標計算
            q,r = divmod(n, col_cnt)
            x1 = xs + (dx+len_x)*r
            y1 = ys + (dy+len_y)*q
            x2 = x1 + len_x
            y2 = y1 + len_y
            self.color_canvas.create_rectangle(x1, y1, x2, y2,outline="white",activeoutline="skyblue1",fill=color)
        # カラーパレットキーバインド
        self.color_canvas.bind("<ButtonPress-1>", self.select_color)
        
        
        # スケールの作成 線の太さ用
        self.thick = tkinter.IntVar()
        slidebar = tkinter.Scale(frame_a,variable=self.thick,orient=tkinter.HORIZONTAL,length=100,from_=1,to=30,label="太さ")
        slidebar.pack(side=tkinter.LEFT)
        
        frame_a.pack(fill=tkinter.X)
        
        # 画像表示用キャンバス作成
        self.canvas = tkinter.Canvas(root, bg="white")
        self.canvas.pack(expand=True, fill=tkinter.BOTH)
        
        # キーバインド
        self.canvas.bind("<ButtonPress-1>", self.on_key_left)
        self.canvas.bind("<B1-Motion>", self.dragging)
        
        root.mainloop()
    
    def on_key_left(self, event):
        # 直線描画
        self.curr_id = self.canvas.create_line(event.x, event.y, event.x, event.y,
            fill = self.color ,width = self.thick.get())
    
    def dragging(self, event):
        points = self.canvas.coords(self.curr_id)
        points.extend([event.x,event.y])
        self.canvas.coords(self.curr_id, points)
    
    # カラーパレット押下時
    def select_color(self, event):
        fill = ""
        # クリックした地点の図形を取得し、その色をself.colorに格納する
        for obj in self.color_canvas.find_overlapping(event.x,event.y,event.x,event.y):
            fill = self.color_canvas.itemcget(obj, "fill")
            break
        if fill != "":
            self.color= fill
            self.curr_color_canvas.itemconfig("curr_color", fill=self.color)
    
    # 色の変更ダイアログ表示
    def click_color_btn(self,event):
        color = tkinter.colorchooser.askcolor()   #colorchooser呼び出し
        if color[1] is not None:
            self.color = color[1]
            self.curr_color_canvas.itemconfig("curr_color", fill=self.color)
    
    
if __name__ == '__main__':
    paintmodoki = PaintModoki()
    paintmodoki.createwindow()

```

![色と太さ変更](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2532786/187250b2-fc7a-8365-43d4-2f2aed90233f.png "色と太さ変更")


2022/03/21 20:39 最終コードを一部修正しました
 変更箇所：コンストラクタでの処理をインスタンス変数の初期化のみに変更