当サイトには広告・プロモーションが含まれています。

PySimpleGUIで入力補助機能(自動インデント・ペア補完)の実装

この記事で分かること
  • Pythonの「PySimpleGUI」ライブラリで「テキストフィールド」に「ショートカットキー」を設定したい人に向けた記事です。
  • テキストフィールドには、デフォルトだと入力・編集の補助機能がほとんどありません。
  • ここでは「テキストエディタ風」に「自動インデント」や「カッコのペア補完入力」を実装します。
    そうです。pysimpleguiでなぜかテキストエディタを作りたくなった人向けの内容です。
  • また、その他ショートカットキーの作成方法を紹介します。
目次

実装した入力機能

  • インデント関連
    • Enterキー 関連・・・改行時の自動インデント。コロンがあればさらにインデントを付ける。
    • Tabキー関連・・・タブ文字をスペース4つに変更。選択があれば複数行をインデント。またShift修飾でインデントを下げる。
  • ペア入力の補完
    • カッコ([],<>,{},())
    • クオート(ダブルクオート,シングルクオート)
      のペア補完する。状況により片側だけの入力を判断する。
  • Undo/redo
  • おまけ・ショートカットキー(ここではCtrl + Enterをアサイン)

自動インデントの動作

コロンのある改行時に自動インデント。またタブキーで複数行に対応したインデントの挿入、Shift+タブキーでインデントの削除をデモンストレーション。

ペア入力の動作

カッコ入力時に “[” の入力で ”]” が補完されます。ただし “]” が行内に多くあるときは補完しません。また””で囲われているときも補完しません。

おまけ・サイズ変更可能な2つのテキストフィールドをもつウィンドウ

ただのテキストフィールドのウィンドウだとつまらなかったので、2つのテキストフィールドをもつウィンドウにしました。各テキストフィールドとウィンドウ全体のサイズを変更できます。

「pysimplegui」のテキストフィールドにショートカットを設定

Pythonコード

import PySimpleGUI as sg
import tkinter as tk
import re
#----------------------------------------------------

def redo(event,  text):
    try:
        text.edit_redo()
    except:
        pass

# オートインデント
# コロンがある場合はインデントを増やす
def auto_indent(event):
    text = event.widget
    prev = text.get("insert -1c","insert") 
    line = text.get("insert linestart",  "insert")
    match = re.match(r'^(\s+)',  line)
    whitespace = match.group(0) if match else ""

    if prev == ":":
        text.insert("insert",  f"\n{whitespace}    ") # add 4spaces
        return "break"
    else:
        text.insert("insert",  f"\n{whitespace}")
        return "break"

# 複数行に対応したスペース4つの挿入
def tab(event, text):
    try:
        start_line = int(text.index("sel.first")[0])
        last_line = int(text.index("sel.last")[0])
        for x in range(start_line,  last_line+1):
            text.insert(f"{x}.0",  "    ")
        return 'break'

    except tk.TclError:
        text.insert("insert", "    ")
        return 'break'

# 複数行に対応したスペース4つの削除
def Shift_tab(event, text):
    try:
        start_line = int(text.index("sel.first")[0])
        last_line = int(text.index("sel.last")[0])
        for x in range(start_line,  last_line+1):
            if text.get(f"{x}.0", f"{x}.4") == "    ":
                text.delete(f"{x}.0", f"{x}.4")
        return 'break'

    except tk.TclError:
        if text.get("insert-4c", 'insert') == "    ":
            text.delete("insert-4c", 'insert')
        return 'break'

# カッコの補完入力
def complement_pair(text, string1, string2):
    prev = text.get("insert -1c","insert") 
    next = text.get("insert","insert +1c")
    line = text.get("insert linestart",  "insert lineend")
    n1 = line.count(string1)
    n2 = line.count(string2)

    # 選択がある場合は囲う
    if text.tag_ranges('sel'):
        text.insert("sel.first", string1)
        text.insert("sel.last", string2)
    else:
        # クォートがある場合はそのまま入力する
        if (next == "\"" and prev == "\"") or (next == "\'" and prev == "\'"):
            text.insert("insert", string1)

        # 後ろのカッコの方が多い場合はそのまま入力する
        elif n1 < n2:
            text.insert("insert", string1)

        # ペアで入力する
        else:
            text.insert("insert", string1)
            text.mark_gravity(tk.INSERT,tk.LEFT)
            text.insert("insert", string2)
            text.mark_gravity(tk.INSERT,tk.RIGHT)

def square_brackets(event, text):
    complement_pair(text, "[", "]")
    return 'break'

def round_brackets(event, text):
    complement_pair(text, "(", ")")
    return 'break'

def curly_brackets(event, text):
    complement_pair(text, "{", "}") 
    return 'break'

def angle_brackets(event, text):
    complement_pair(text, "<", ">") 
    return 'break'

def double_quotes(event, text):
    complement_pair(text, "\"", "\"")
    return 'break'
    
def single_quotes(event, text):
    complement_pair(text, "\'", "\'") 
    return 'break'
#----------------------------------------------------

def make_script_window():
    #テキストウィンドウ2つ
    script_t1 = sg.Column([[sg.ML("#Script window1\n", key="script_window1", size=(70, 20))]])
    script_t2 = sg.Column([[sg.ML("#Script window2\n", key="script_window2", size=(70, 10))]])

    # 上下のウィンドウの割合を変更可能にする
    layout_script = [
        [sg.Pane([script_t1 , script_t2], key="script_pane")]
    ]
    #ウィンドウサイズを変更可能にする
    window = sg.Window('Script window',  layout_script,  finalize=True,  return_keyboard_events=True,
        resizable=True,  margins=(0, 0)
    )

    #テキストウィンドウとその器も変更可能にする
    window["script_pane"].expand(expand_x=True,  expand_y=True)
    window["script_window1"].expand(expand_x=True,  expand_y=True)
    window["script_window2"].expand(expand_x=True,  expand_y=True)

    #キーのバインド
    for i in ["script_window1", "script_window2"]:
        text1 = window[i].Widget
        text1.configure(undo=True) # Undo機能追加
        text1.bind("<Control-Key-Y>",  lambda event,  text1=text1: redo(event,  text1)) #Redo機能追加
        text1.bind("<Tab>",  lambda event,  text1=text1: tab(event,  text1))
        text1.bind("<Shift-Tab>",  lambda event,  text1=text1: Shift_tab(event,  text1))
        # eventを登録するときはwindowにつける。
        window[i].bind("<Control-Return>", "__Control_return")
        text1.bind("<Return>",  auto_indent)
        text1.bind("<Key-[>",  lambda event,  text1=text1: square_brackets(event,  text1))
        text1.bind("<Key-(>",  lambda event,  text1=text1: round_brackets(event,  text1))
        text1.bind("<Key-{>",  lambda event,  text1=text1: curly_brackets(event,  text1))
        text1.bind("<Key-<>",  lambda event,  text1=text1: angle_brackets(event,  text1))
        text1.bind("<Key-\">",  lambda event,  text1=text1: double_quotes(event,  text1))
        text1.bind("<Key-\'>",  lambda event,  text1=text1: single_quotes(event,  text1))
    return window

window =  make_script_window()

while True:     # Event Loop
    event, values = window.read()

    if event == sg.WIN_CLOSED:
        break

    # ctrol + enterの処理例
    if event in ("script_window1__Control_return"):
        print("ctrl enter pressed.")
window.close()

ショートカットキーの実装パターン1:キー入力で関数を呼ぶ(text.bind())

元の機能を残さずにキーの動作を変更したい場合があります。

例えば「Tabキー」は本来タブ文字(\t)1つを挿入しますが、エディタの動作は全く違います!Python向けにはタブ文字でなく4つのスペースの挿入や、複数行の選択時にはそれらのインデントに対応するように動作を変更されています。

このようにキーの機能を元の機能を残さずに変える場合には、bindする関数定義の最後にreturn ”break”を付ければOKです。
関数の動作の後に本来のキー動作をするのを、breakで止めます。

text = window[どこかのテキストフィールド].Widget
text.bind("<なにかキー>",  function)
def function(event):
    return "break"

ショートカットキーの実装パターン2:キー入力をイベントで受ける(window.bind()、キーボード操作)

特定のキーの入力をeventで受ける場合は、bind()の2つめの引数を指定します。

ここでは”__Control_return”を加えることで通常のevent “script_window1″に修飾が加わり、 “script_window1__Control_return” というeventを発生できます。

for i in ["script_window1", "script_window2"]:
    window[i].bind("<Control-Return>", "__Control_return")
while True:     # Event Loop
    event, values = window.read()

    if event == sg.WIN_CLOSED:
        break

    # ctrol + enterの処理例
    if event in ("script_window1__Control_return"):
        print("ctrl enter pressed.")

if event in (複数の条件)の形式にしておけば、すでにウィジェットで実装している機能(たとえばボタンを押す)について、ショートカットキーを設定できます。

解説

自動インデント関係

前提として、インデントはすべてタブ文字ではなくスペース4つで実装します。

改行Enter

pythonコードの入力にはインデントが重要です。そのために改行するときは、その行のインデントを次の行にも行います。

ただし、関数定義などでコロン:がある場合は必ずインデントを増やすので、その際はスペース4つ ” ” を加えています。

タブTab

行の選択があるかでtry/exeptの分岐させています。

選択がない場合はスペース4つをそのまま挿入し、選択がある場合は複数行の行頭にスペース4つを挿入します。

Shift + Tab

こちらはインデントを下げる場合です。タブと同様に選択の有無で分岐しますが、スペース4つがあれば、それを削除することでインデントを下げます。

ペア入力補完

complement_pair(text, string1, string2)という関数を定義しています。ペアを補完したい文字であるカッコ((),{},[],<>)とクオート(”,’)について、さらに個別の関数を定義し、それらをキーバインドしています。

一般的なエディタの動作を真似しようとすると、意外と複雑でした。
・選択がある場合はそれを囲む。
・クオートに囲まれている場合は補完しない。
・後ろのペアの方が行内の数が多い場合も補完しない。
・残りは補完する
という動作になっています。

まとめ

PySimpleGUI(またはtkinter)向けに、テキストフィールドをpythonエディタ風に機能を補完する実装を作成しました。

誰に需要があるのでしょうか。刺さった人がいたら教えてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次