Python GUIプログラミング Canvas Text入力

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

 TkinterCanvasでテキストを入力することができるかどうか、ということで調べてみました。先日、Canvasのメソッドのcreate_text()でテキストを描画することはできたのですが、いろいろと調査した結果、このテキストを編集することができる、ということがわかりましたので、まとめます。

実行イメージ

キャンバス上のテキストを編集可能にしました

 マウスクリックでテキストを選択、矢印キーでカーソル位置を移動、HomeキーやEndキーが使えます。Enterキーで確定、Escキーで取り消し、タブキーで次の項目へ移動、シフトキーと移動キーで部分選択が可能です。

ソースコード

import tkinter as tk

class Application(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Canvas text edit")
        self.geometry("400x280")
        self.canvas = tk.Canvas(self, background="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.tag_bind("editable","<Button-1>", self.clicked)
        self.canvas.tag_bind("editable","<Key>", self.do_key)
        
        self.focused_item = None
        self.focused_item_text = None
        
        self.canvas.create_text(20,80, anchor=tk.NW, text="このテキストは修正可能です。", tags=("editable",))
        self.canvas.create_text(20, 120, anchor=tk.NW, text="このテキストも修正可能です。", tags=("editable",))
        self.canvas.create_text(20, 160, anchor=tk.NW, text="このテキストも修正可能です。", tags=("editable",))

    def clicked(self,event):
        if self.canvas.type(tk.CURRENT) == "text":
            prev_item = self.focused_item
            self.canvas.focus_set() 
            self.canvas.focus(tk.CURRENT)
            x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
            self.focused_item = self.canvas.find_overlapping(x, y, x, y)
            if prev_item == self.focused_item:
                x = self.canvas.canvasx(event.x)
                y = self.canvas.canvasy(event.y)
                if event.state&1:
                    index_current = self.canvas.index(tk.CURRENT, tk.INSERT)
                    index_selected = self.canvas.index(tk.CURRENT, "@%d,%d" % (x, y))
                    if index_current <= index_selected:
                        self.canvas.select_from(tk.CURRENT, index_current)
                        self.canvas.select_to(tk.CURRENT, index_selected)
                    else:
                        self.canvas.select_from(tk.CURRENT, index_selected)
                        self.canvas.select_to(tk.CURRENT, index_current - 1)
                else:
                    self.canvas.icursor(self.focused_item, "@%d,%d" % (x, y))
                    self.canvas.select_clear()
            else:
                self.canvas.select_from(tk.CURRENT, 0)
                self.canvas.select_to(tk.CURRENT, tk.END)
            if self.focused_item:
                self.focused_item_text = self.canvas.itemcget(self.focused_item,"text")
            else:
                self.focused_item_text = None

    def do_key(self, event):
        if event.keycode == 229: # IME入力中
            return
        item = self.canvas.focus()
        if item:
            current_index = self.canvas.index(item,tk.INSERT)
            if event.keysym == 'Right':
                new_index = current_index + 1
            elif event.keysym == 'Left':
                new_index = current_index - 1
            elif event.keysym == 'End':
                new_index = self.canvas.index(item,tk.END)
            elif event.keysym == 'Home':
                new_index = self.canvas.index(item,0)
            elif event.keysym == 'BackSpace':
                selection = self.canvas.select_item()
                if selection:
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                else:
                    if current_index > 0:
                        self.canvas.dchars(item, current_index - 1)
                return
            elif event.keysym == 'Delete':
                selection = self.canvas.select_item()
                if selection:
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                else:
                    self.canvas.dchars(item, current_index)
                return
            elif event.keysym == 'Tab':
                items = self.canvas.find_withtag("editable")
                if items:
                    index = items.index(item)
                    if index + 1 == len(items):
                        next_item = items[0]
                    else:
                        next_item = items[index + 1]
                    self.canvas.focus(next_item)
                    self.canvas.select_from(next_item,0)
                    self.canvas.select_to(next_item,tk.END)
                return
            elif event.keysym == 'Return':
                self.canvas.select_clear()
                self.canvas.focus("")
                self.focused_item = None
                self.focused_item_text = None
                return
            elif event.keysym == 'Escape':
                self.canvas.itemconfig(item, text=self.focused_item_text)
                self.canvas.select_clear()
                self.canvas.focus("")
                self.focused_item = None
                self.focused_item_text = None
                return
            elif event.keycode in (16, 17): # Shift, Ctrl
                return

            if event.char >= ' ':
                selection = self.canvas.select_item()
                if selection:
                    new_index = self.canvas.index(item, tk.SEL_FIRST) + len(event.char)
                    self.canvas.dchars(item, tk.SEL_FIRST, tk.SEL_LAST)
                    self.canvas.select_clear()
                else:
                    new_index = current_index + len(event.char)
                self.canvas.insert(item, tk.INSERT, event.char)

            if event.state&1: # Shift Key
                selection = self.canvas.select_item()
                if selection:
                    if self.canvas.index(item,tk.SEL_LAST) >= new_index:
                        self.canvas.select_from(item, new_index)
                        self.canvas.select_to(item, tk.SEL_LAST)
                    else:
                        self.canvas.select_from(item, tk.SEL_FIRST)
                        self.canvas.select_to(item, new_index - 1)
                    self.canvas.icursor(item, new_index)
                else:
                    self.canvas.select_from(item, current_index)
                    if current_index < new_index:
                        self.canvas.select_to(item, new_index - 1)
                    else:
                        self.canvas.select_to(item, new_index)
                    self.canvas.icursor(item, new_index)
            else:
                self.canvas.icursor(item, new_index)
                self.canvas.select_clear()
                

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

 ソースコード、めっちゃ長くなってしまいました。もっと短くしたいですよねぇ。

解説

 いやー、今回のやりたいこと、割と基本的なことばかりなので、もっと簡単に実装できるのかと思っていました。それが、意外や意外、時間もかかったし、ソースコードも長くなって、ブログで紹介するのを躊躇するほどのボリュームになってしまいました。(あと、実は、バグも残ってるんですよ。ここだけの話。全部さっき気づきましたが、シフトキー押しながら大文字とか記号を入力すると、なぜか選択されちゃったり、他にもCtrl+Vで張り付けられなかったり、ファンクションキーを押すとUnboundLocalErrorがraiseしたりとか、入力をキャンセルしようとして選択されていないキャンバス上のどこかをクリックしても反応しなかったりと、いろいろ足りないことがありましたので、ブログの更新を優先することにして、そのあたりの実装は、今回はあきらめました。ちょっと整理が必要ですね。)

 今回の一つ目のポイントは11、12行目のtag_bind()です。Canvas上の要素は、連番で識別される、idか、ユーザーが定義したタグという概念で管理することができるようです。このタグにイベントをバインドする(割り当てる)ことができるのがこのtag_bind()です。idだと一つの要素にのみ影響することができますが、タグを使うことで複数の要素に対して同時に影響を及ぼすことが可能になります。今回はここで、editable(編集可能)というタグを定義しました。

 16~18行目では、create_text()実行するときにこのタグをセットしています。これでクリックされたときのイベント<Button-1>と、キー入力されたときのイベント<Key>に対する処理が割り当てられます。

 20行目からは、クリックされたときのイベントの処理になります。ソースコード上でうまく表現できていないのですけど、最初のテキストを選択したとき、と、選択されたテキストのカーソル位置を変更するとき、の、二種類の処理が混ざっています。いろいろ編集された後にEscapeキーを押されて元に戻すためのデータもこのタイミングで保持するようにしています。クリックしたときと前にクリックしたときの項目が同じならカーソル位置の変更、そうでないなら最初のテキストの選択、という風に判断しています。

 50行目から、キー入力されたときのイベントの処理です。このメソッド、長すぎますね!何とかせんかーい、と、言いたくなります。自分にですけど。いろいろと継ぎ足し継ぎ足しして作っていったのが、よくわかります。51行目の「if event.keycode == 229:」ですが、なんと、IMEで日本語入力中、つまり返還前のキー入力は、このkeycodeが229としてイベントが発生してきました。IMEの入力に対する処理は不要なので、無視するように変更しました。ドキュメントのどこかに書いてあればよいのですけど、見つけられませんでした。でも、これでよいですよね?どなたか詳しい方、教えてください♪

 それ以降は、おおよそ何をやっているかというと、カーソルの移動に対応しております。キー入力で右に行ったり左に行ったりするのも、自動では対応されていないので、自分でコーディングする必要があるのですね。もちろん、HomeキーやEndキー、Backspaceキー、Deleteキーの動きも、自分でコーディングする必要がありました。それぞれの移動時にShiftキーが押されていた時は選択したいのですけど、それも当然自分でコーディングする必要がありました。Entryウィジェットなどは、ちゃんと処理されているのですが、Canvas上の項目については定義されていません。これは、面倒ですが、、、動作を自由にカスタマイズができる、ということですね!

 91行目、Returnキーを入力したときに、確定、97行目、Escapeキーを入力したときに入力をキャンセルするように実装してみました。そして、104行目は、Shiftキー、Ctrlキーが単独で押されたときのイベントですね。後続の処理をしないようにしました。

 ちなみに、シフトキーが押された状態かどうか、というのは、117行目のif event.state&1:で判定しています。ステータスの1ビット目がセットされてくるようです。これもドキュメントに書かれているところが見つけられなかったのですよねぇ。

まとめ

 Canvascreate_text()メソッドで、テキスト入力項目を作る、というのはちょっとハードルが高い、ということがわかりました。もうちょっと整理して、共通部品の作り方を検討した方がよさそうです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。