0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Tkinter CookBook ~文字列置換アプリケーション~

Last updated at Posted at 2022-01-10

概要

単純な文字列置換アプリケーションをTkinterで作成します。
最終的な外観や操作は以下のような感じになります。

replace_string.gif

前提

実行環境

本ページで記載しているコードはWindows10, python3.7で実行できることを確認しております。

必要なpythonモジュール

デフォルトでインストールされているモジュールのみを使用します。

ソースコードについて

GitHubで公開しております。
https://github.com/The-town/ReplaceString

記事内のコードは若干手を加えているため、GitHub上のコードと差異があります。

それでは、はじめましょう。

STEP1 GUIオブジェクトの設定

gui_component.py

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のどこで使用されるかについては、以下の画像で表されます。

Class名と場所の関係.png

tk.Frametk.ButtonなどのTkinterオブジェクトを、自分が作ったClassオブジェクトで継承しています。
これには以下のような理由があります。

  • Tkinterオブジェクト側の仕様を 自分のClassオブジェクトで覆う ことで、Tkinter側の仕様変更に備える
  • 各ウィジェットに共通する背景色などを設定し統一化を図る

STEP2 GUIオブジェクトの配置

gui_component.py

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つあります。
roottext_framefunction_frameです。

rootはTkinterを実行した際に出力されるウィンドウ全体のことを表します。
そのため、必ずrootがあらゆるウィジェットの最終的なmasterになります。

このコードでは、それ以外に2つのFrameウィジェットをmasterとして間に挟んでいます。

先程お見せした画像へFrameウィジェットの情報を加えてみます。

Frame.png

このように、rootと各ウィジェット(例えばButtonやText)の間に、Frameウィジェットを挟む目的は、各ウィジェットを配置しやすくするためです。
また、後で変更しやすくするためでもあります。

過去の自分の記事から引用します。

「root」つまり画面のオブジェクト上に直接「Label」オブジェクトや「Entry」オブジェクトを配置しない理由は様々でしょう。このコードでそのようにしない理由は後々の配置換えを容易にするためです。

直接「root」上にオブジェクトを乗せ、「grid()」で配置する方法はオブジェクト数が少なければ非常に有効です。
ただし、オブジェクトの数が多くなると相対的な位置関係は複雑になります。また、あるオブジェクトAをあるオブジェクトBのとなりに配置したくなった場合、オブジェクト数が多いとほとんどすべてのオブジェクトの配置を見直す必要が出て来ます。

これは面倒なので、関連するオブジェクトを「Frame」オブジェクト上にまとめてしまい、「root」の上に乗るオブジェクトは「Frame」オブジェクトのみとするのです。このようにすることで、後々オブジェクトを追加したい場合はそのオブジェクトを配置するのに適切な「Frame」オブジェクトを見つけ、その「Frame」オブジェクト上に乗っているオブジェクトのみ配置を変えればよいのです。

この考え方は私たちが普段から何気なく行っていることなのです。WindowsでもLinuxでもファイルを作成した後は、適切なフォルダへと配置するでしょう。あらゆるファイルを一つのフォルダへ放り込んでいる人は少ないのではないでしょうか。

Tkinter CookBook 1. 新しい「Frame」オブジェクトを作成

STEP3 置換関数の作成

gui_component.py
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

replace_rule.py

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

replace_rule.py

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モジュールを使用します。

参考:re --- 正規表現操作

STEP4 置換関数とボタンの紐付け

gui_component.py
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_buttoncommand属性へ、置換関数execute_replace_ruleを設定します。
これでボタンが押された際に置換関数が実行されます。

STEP5 実行

以下のmain.pyよりここまでのコードを実行してみます。

main.py
from gui_component import Main


if __name__ == '__main__':
    gui_main: Main = Main()

フォルダ構成は以下のようになっているかと思います。

  • main.py
  • gui_component.py
  • replace_rule.py

replaceString_STEP5.png

置換ボタンを押せば文字列の置換を行います。

【追加STEP】STEP6 見た目を整える

gui_component.py
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行の文字列として置換する"

背景色、フォントを整えました。
ここは自由にカスタマイズしましょう。

replaceString_STEP6

【追加STEP】STEP7 チェックボタンを使う

gui_component.py

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"
replaceRule.py
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)

チェックボタンによって処理を切り替えられるようにします。

StringTypeCheckButtononvalueoffvalueは、それぞれチェックボタンにチェックが入っているときと入っていないときの状態を表す文字列です。

状態を取り出すために**string_type_check_button.state.get()**を使います。

参考:30. ttk.Checkbutton

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?