概要
単純な文字列置換アプリケーションをTkinterで作成します。
最終的な外観や操作は以下のような感じになります。
前提
実行環境
本ページで記載しているコードはWindows10, python3.7で実行できることを確認しております。
必要なpythonモジュール
デフォルトでインストールされているモジュールのみを使用します。
ソースコードについて
GitHubで公開しております。
https://github.com/The-town/ReplaceString
記事内のコードは若干手を加えているため、GitHub上のコードと差異があります。
それでは、はじめましょう。
STEP1 GUIオブジェクトの設定
import tkinter.scrolledtext as scrolled_text
import tkinter as tk
class Frame(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self["width"] = 100
self["height"] = 100
self["bg"] = "white"
class InputReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 50
self["height"] = 20
class RuleReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 25
self["height"] = 20
class OutputReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 50
self["height"] = 20
class Button(tk.Button):
def __init__(self, master=None):
tk.Button.__init__(self, master)
self["height"] = 1
self["width"] = 10
class ReplaceButton(Button):
def __init__(self, master=None):
Button.__init__(self, master)
self["text"] = "置換"
class CheckButton(tk.Checkbutton):
def __init__(self, master=None):
tk.Checkbutton.__init__(self, master)
self["bg"] = "white"
self["onvalue"] = "yes"
self["offvalue"] = "no"
class StringTypeCheckButton(CheckButton):
def __init__(self, master=None):
CheckButton.__init__(self, master)
self["text"] = "改行文字列\\nを使い、1行の文字列として置換する"
ここでは、基本的なウィジェット(Widget)を定義しています。
各ウィジェットがGUIのどこで使用されるかについては、以下の画像で表されます。
tk.Frameやtk.ButtonなどのTkinterオブジェクトを、自分が作ったClassオブジェクトで継承しています。
これには以下のような理由があります。
- Tkinterオブジェクト側の仕様を 自分のClassオブジェクトで覆う ことで、Tkinter側の仕様変更に備える
- 各ウィジェットに共通する背景色などを設定し統一化を図る
STEP2 GUIオブジェクトの配置
import tkinter.scrolledtext as scrolled_text
import tkinter as tk
class Main:
def __init__(self) -> None:
root: tk.Tk = tk.Tk()
root.title("文字列置換")
text_frame: Frame = Frame(root)
text_frame.grid(column=0, row=1)
self.input_replace_text: InputReplaceText = InputReplaceText(master=text_frame)
self.input_replace_text.grid(column=0, row=0)
self.rule_replace_text: RuleReplaceText = RuleReplaceText(master=text_frame)
self.rule_replace_text.grid(column=1, row=0)
self.output_replace_text: OutputReplaceText = OutputReplaceText(master=text_frame)
self.output_replace_text.grid(column=2, row=0)
function_frame: Frame = Frame(root)
function_frame.grid(column=0, row=0)
replace_button: ReplaceButton = ReplaceButton(function_frame)
replace_button.grid(column=0, row=1)
self.string_type_check_button: StringTypeCheckButton = StringTypeCheckButton(function_frame)
self.string_type_check_button.grid(column=0, row=0)
root.mainloop()
class Frame
...
ここではMainクラスを追加して、各ウィジェットを配置しています。
注目したいのは、各ウィジェットのmasterです。整理すると以下の表のとおりになります。
ウィジェット | master |
---|---|
text_frame | root |
input_replace_text | text_frame |
rule_replace_text | text_frame |
output_replace_text | text_frame |
function_frame | root |
replace_button | function_frame |
string_type_check_button | function_frame |
ここでmasterとなっているウィジェットは3つあります。
rootとtext_frameとfunction_frameです。
rootはTkinterを実行した際に出力されるウィンドウ全体のことを表します。
そのため、必ずrootがあらゆるウィジェットの最終的なmasterになります。
このコードでは、それ以外に2つのFrameウィジェットをmasterとして間に挟んでいます。
先程お見せした画像へFrameウィジェットの情報を加えてみます。
このように、rootと各ウィジェット(例えばButtonやText)の間に、Frameウィジェットを挟む目的は、各ウィジェットを配置しやすくするためです。
また、後で変更しやすくするためでもあります。
過去の自分の記事から引用します。
「root」つまり画面のオブジェクト上に直接「Label」オブジェクトや「Entry」オブジェクトを配置しない理由は様々でしょう。このコードでそのようにしない理由は後々の配置換えを容易にするためです。
直接「root」上にオブジェクトを乗せ、「grid()」で配置する方法はオブジェクト数が少なければ非常に有効です。
ただし、オブジェクトの数が多くなると相対的な位置関係は複雑になります。また、あるオブジェクトAをあるオブジェクトBのとなりに配置したくなった場合、オブジェクト数が多いとほとんどすべてのオブジェクトの配置を見直す必要が出て来ます。
これは面倒なので、関連するオブジェクトを「Frame」オブジェクト上にまとめてしまい、「root」の上に乗るオブジェクトは「Frame」オブジェクトのみとするのです。このようにすることで、後々オブジェクトを追加したい場合はそのオブジェクトを配置するのに適切な「Frame」オブジェクトを見つけ、その「Frame」オブジェクト上に乗っているオブジェクトのみ配置を変えればよいのです。
この考え方は私たちが普段から何気なく行っていることなのです。WindowsでもLinuxでもファイルを作成した後は、適切なフォルダへと配置するでしょう。あらゆるファイルを一つのフォルダへ放り込んでいる人は少ないのではないでしょうか。
STEP3 置換関数の作成
class Main:
...
def execute_replace_rule(self, event=None) -> None:
input_text: str = self.input_replace_text.get(1.0, tk.END)[:-1]
rule_replace_text: str = self.rule_replace_text.get(1.0, tk.END)[:-1]
strings_before_change, strings_after_change = analyze_replace_rules(rule_replace_text)
self.output_replace_text.delete(1.0, tk.END)
output_text = change_string(input_text, tuple(strings_before_change), tuple(strings_after_change))
self.output_replace_text.insert(tk.END, output_text)
ここではinput_replace_textに入力された文字列を、rule_replace_textに入力された規則に従って変換し、output_replace_textへ出力するという処理を行っています。
入力された置換規則を解析するanalyze_replace_rules関数、入力された文字列を置換するchange_string関数は後ほど解説します。
途中で**self.output_replace_text.delete(1.0, tk.END)**をしているのは、出力された文字列を消すためです。
以前に置換した文字列を削除した上で出力することで、何度も置換ボタンを押した際に同一の挙動をするようにしています。
# deleteなしの場合
1回目の置換:
置換後の文字列です。
2回目の置換:
置換後の文字列です。
置換後の文字列です。
...
# deleteありの場合
1回目の置換:
置換後の文字列です。
2回目の置換:
置換後の文字列です。
...
analyze_replace_rules
from typing import Tuple, List
def analyze_replace_rules(rule_replace_text: str) -> Tuple[List, List]:
"""
入力された置換ルールを解析して、置換前の文字列と置換後の文字列を分ける関数
Parameters
----------
rule_replace_text: str
置換ルール
Returns
-------
strings_before_change, strings_after_change: tuple
置換前の文字列と置換後の文字列のリスト
"""
split_string: str = " -> "
strings_before_change: List[str] = []
strings_after_change: List[str] = []
for rule in rule_replace_text.split("\n"):
if split_string in rule:
strings_before_change.append(rule.split(split_string)[0])
strings_after_change.append(rule.split(split_string)[1])
return strings_before_change, strings_after_change
置換ルールは置換前の文字列と置換後の文字列、そして->
という区切り文字列で構成されます。
hogehoge -> hugahuga
hogehogeという文字列をhugahugaという文字列へ置換
また、置換ルールは複数記入することが可能です。
そのため、置換前の文字列を改行文字列\n
で各行に分けた後、->
で置換前と置換後の文字列に分割します。
change_string
import re
from typing import Tuple, List
def analyze_replace_rules(rule_replace_text: str) -> Tuple[List, List]:
...
def change_string(text: str, strings_before_change: Tuple[str], strings_after_change: Tuple[str]) -> str:
"""
文章中にある、複数の指定した文字列を、複数の指定した文字列へ変換する関数
Parameters
----------
text: str
置換対象の文章
strings_before_change: Tuple[str]
置換する文字列のタプル
strings_after_change: Tuple[str]
置換後の文字列のタプル
Returns
--------
text: str
置換後の文章
"""
for i in range(len(strings_after_change)):
re_obj_change_string: re.Pattern = re.compile(rf"{strings_before_change[i]}")
text = re.sub(re_obj_change_string, strings_after_change[i], text)
return text
正規表現に対応した置換としたいためreモジュールを使用します。
STEP4 置換関数とボタンの紐付け
class Main:
def __init__(self) -> None:
...
replace_button: ReplaceButton = ReplaceButton(function_frame)
replace_button.grid(column=0, row=1)
replace_button["command"] = self.execute_replace_rule # 追加
...
replace_buttonのcommand属性へ、置換関数execute_replace_ruleを設定します。
これでボタンが押された際に置換関数が実行されます。
STEP5 実行
以下のmain.pyよりここまでのコードを実行してみます。
from gui_component import Main
if __name__ == '__main__':
gui_main: Main = Main()
フォルダ構成は以下のようになっているかと思います。
- main.py
- gui_component.py
- replace_rule.py
置換ボタンを押せば文字列の置換を行います。
【追加STEP】STEP6 見た目を整える
import tkinter.scrolledtext as scrolled_text
import tkinter as tk
from replaceRule import analyze_replace_rules, change_string, change_string_list
class Main:
def __init__(self) -> None:
root.configure(background='white')
...
class Frame(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.grid(column=0, row=0)
self["width"] = 100
self["height"] = 100
self["padx"] = 10
self["pady"] = 10
self["bg"] = "white"
class InputReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 50
self["height"] = 20
self["font"] = ("メイリオ", 12)
class RuleReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 25
self["height"] = 20
self["font"] = ("メイリオ", 12)
class OutputReplaceText(scrolled_text.ScrolledText):
def __init__(self, master=None):
scrolled_text.ScrolledText.__init__(self, master)
self["width"] = 50
self["height"] = 20
self["font"] = ("メイリオ", 12)
class Button(tk.Button):
def __init__(self, master=None):
tk.Button.__init__(self, master)
self["height"] = 1
self["width"] = 10
class ReplaceButton(Button):
def __init__(self, master=None):
Button.__init__(self, master)
self["text"] = "置換"
self["font"] = ("メイリオ", 15)
class CheckButton(tk.Checkbutton):
def __init__(self, master=None):
tk.Checkbutton.__init__(self, master)
self["bg"] = "white"
self["text"] = "test"
self["font"] = ("メイリオ", 15)
class StringTypeCheckButton(CheckButton):
def __init__(self, master=None):
CheckButton.__init__(self, master)
self["text"] = "改行文字列\\nを使い、1行の文字列として置換する"
背景色、フォントを整えました。
ここは自由にカスタマイズしましょう。
【追加STEP】STEP7 チェックボタンを使う
class Main:
def execute_replace_rule(self, event=None) -> None:
...
output_text: str = ""
if self.string_type_check_button.state.get() == "one_line":
output_text = change_string(input_text, tuple(strings_before_change), tuple(strings_after_change))
elif self.string_type_check_button.state.get() == "many_lines":
output_text = change_string_list(tuple(input_text.split("\n")), tuple(strings_before_change),
tuple(strings_after_change))
self.output_replace_text.insert(tk.END, output_text)
class CheckButton(tk.Checkbutton):
def __init__(self, master=None):
tk.Checkbutton.__init__(self, master)
self["bg"] = "white"
self["text"] = "test"
self["font"] = ("メイリオ", 15)
self.state: tk.StringVar = tk.StringVar()
self.state.set("no")
self["variable"] = self.state
self["onvalue"] = "yes"
self["offvalue"] = "no"
class StringTypeCheckButton(CheckButton):
def __init__(self, master=None):
CheckButton.__init__(self, master)
self["text"] = "改行文字列\\nを使い、1行の文字列として置換する"
self.state.set("many_lines")
self["onvalue"] = "one_line"
self["offvalue"] = "many_lines"
def change_string_list(texts: Tuple[str, ...], strings_before_change: Tuple[str, ...],
strings_after_change: Tuple[str, ...]) -> str:
"""
リストとして受け取った文章中にある、複数の指定した文字列を、複数の指定した文字列へ変換する関数
正規表現の「^」や「$」をリストの要素ごとに実行するために作成した。
例;
texts = [123, abc, 145c]
^1 -> a
[a23, abc, a45c]
Parameters
----------
texts: Tuple[str, ...]
置換対象の文章
strings_before_change: Tuple[str, ...]
置換する文字列のタプル
strings_after_change: Tuple[str, ...]
置換後の文字列のタプル
Returns
--------
text: str
置換後の文章
"""
tmp_texts: List[str] = list(texts)
for i in range(len(strings_after_change)):
re_obj_change_string: re.Pattern = re.compile(rf"{strings_before_change[i]}")
tmp_texts = [re.sub(re_obj_change_string, strings_after_change[i], text) for text in tmp_texts]
return "\n".join(tmp_texts)
チェックボタンによって処理を切り替えられるようにします。
StringTypeCheckButtonのonvalue・offvalueは、それぞれチェックボタンにチェックが入っているときと入っていないときの状態を表す文字列です。
状態を取り出すために**string_type_check_button.state.get()**を使います。