M.Hiroi's Home Page

Lightweigth Language

新・お気楽 Python/Tkinter 入門

[ PrevPage | Python | NextPage ]

テキストウィジェット

いよいよ巨大なウィジェットである「テキストウィジェット」を説明します。エントリーウィジェットがラインエディタとするならば、テキストウィジェットはスクリーンエディタに相当し、柔軟で高度なテキスト編集を行うことができます。

●テキストウィジェットの生成

テキストウィジェットは Text() で生成します。テキストウィジェットには標準動作が用意されていて、それだけでテキスト編集が可能になっています。マウスの操作は、左クリックでカーソル位置の変更、ドラッグで範囲の選択、ダブルクリックで単語の選択が行えます。また、トリプルクリックで行の選択、ドラッグで文字列の選択ができます。

テキストウィジェットの場合、オプション width と height は桁数と行数を表します。使用するフォントによってウィンドウのサイズが変わることに注意してください。また、オプション state に disabled を設定すると、テキストの変更を禁止することができます。これはキーボートからの入力だけでなく、プログラムによる挿入や削除も禁止されるので、必要なテキストデータをウィジェットに挿入してから、state を disabled に設定してください。

それから、オプション wrap で行の折り畳みを設定することができます。none を指定すると折り畳みは行われません。char は文字の切れ目で、word は単語の切れ目で折り畳みます。

●位置の指定

テキストウィジェットはリストボックスと同様に、スクロールバーと組み合わせて表示範囲を変更することができます。垂直スクロールバー付きのテキストウィジェットはモジュール tkinter.scrolledText を使うと簡単です。このほかにも、文字列の挿入、削除、検索といった、テキストエディタとして必要なメソッドが多数用意されています。詳細は Tkinter のマニュアルを参照してください。

また、多くのメソッドで位置の指定が必要になります。基本的な指定方法を下表に示します。

表 : 位置の基本指定
N.MN 行の M 文字目
@x,yテキスト内の (x,y) の位置にある文字
endテキスト末尾
マーク名その名前のマークをつけた位置
タグ名.firstその名前のタグの最初の位置
タグ名.lastその名前のタグの最後の位置

マークとタグについてはあとで詳しく説明します。テキストウィジェットでは、行は 1 から数えますが、文字は 0 から数えるので注意してください。この基本指定に加えて、次に示す相対指定を組み合わせることができます。

表 : 位置の相対指定
+Nchars, -Ncharsそこから N 文字先、手前
+Nlines, -Nlinesそこから N 行先、手前
linestart, lineendその行の先頭、末尾
wordstart, wordendその単語の先頭、末尾

●テキストファイルを表示する

テキストウィジェットは多機能なので、ほかのウィジェットに比べて使いこなすのはちょっと難しいと思います。ですが、テキストを表示するだけならば、とても簡単にプログラムすることができます。まず最初に、テキストファイルを表示するプログラムを作ってみましょう。次のリストを見てください。

リスト : テキストファイルを表示する

import tkinter as tk
import tkinter.filedialog as fd
import tkinter.scrolledtext as st
import sys, os.path

# MainWindow
root = tk.Tk()
root.option_add('*font', ('Noto Sans Mono CJK JP', 10))
root.title('Text Viewer')

# Global
path_name = os.getcwd()

# Text
t0 = st.ScrolledText()
t0.pack()

# File Select
def load_file():
    global path_name
    filename = fd.askopenfilename(filetypes = [('Text Files', ('.txt', '.py'))],
                                  initialdir = path_name)
    if filename != "":
        path_name = os.path.dirname(filename)
        fi = open(filename)
        t0.delete('1.0', 'end')
        for x in fi:
            t0.insert('end', x)
        fi.close()
        t0.mark_set(tk.INSERT, '1.0')
        t0.focus_set()

# Menu
m0 = tk.Menu(root);
root.configure(menu = m0);

m1 = tk.Menu(m0, tearoff = 0)
m1.add_command(label = 'Open', under = 0, command = load_file)
m1.add_separator
m1.add_command(label = 'Exit', under = 0, command = sys.exit)
m0.add_cascade(label = 'File', under = 0, menu = m1 )

root.mainloop()

メニューの設定とファイルの選択は イメージとファイルの選択 で作成した画像ローダーと同じです。テキストウィジェットは ScrolledText() で生成します。これで垂直スクロールバーが付いたテキストウィジェットを生成することができます。

ファイルの読み込みは関数 load_file() で行います。askopenfilename() でファイル選択ダイアログを表示してファイル名を取得します。テキストウィジェットにデータを挿入するメソッドが insert() で、削除するメソッドが delete() です。まず、表示しているテキストを delete() で削除します。1.0 は 1 行目の 0 文字、つまりテキストの先頭を表します。

次に、open() でファイルをリードオープンし、1 行ずつデータを入力していきます。insert() の位置指定は end なので、データはテキストウィジェットの最後に追加されます。最後に close() でファイルを閉じて、focus_set() でフォーカスを設定します。

テキストウィジェット テキストウィジェット

ファイルの表示 ファイルの表示

●行番号の挿入と削除

テキストを表示するだけでは面白くないので、今度は行番号を表示してみましょう。メニューに次の項目を追加します。

m1.add_checkbutton(label = 'Number', under = 0, variable = num_flag, command = change_number)

Number がチェックされていれば行番号を表示し、そうでなければ行番号を表示しません。チェックボタンの値はグローバル変数 num_flag に格納し、行番号の処理は関数 change_number() で行います。

リスト : 行番号の挿入と削除

def change_number():
    line = int(float(t0.index('end')))
    if num_flag.get():
        for x in range(1, line):
            t0.insert('{}.0'.format(x), '{:6d}:'.format(x))
    else:
        for x in range(1, line):
            t0.delete('{}.0'.format(x), '{}.7'.format(x))

最初に行数を index() メソッドで求めます。index() はテキストの位置を line.char の形式の文字列で返します。end を指定することで最終行の次の行を求めることができます。たとえば、ファイルの行数が 55 行であれば、index('end') は '56.0' を返します。int(float('56.0')) で整数値に変換すればファイルの行数を求めることができます。

行番号は行の先頭に挿入することで表示します。変数 x は行番号を表し、挿入する文字列を format で作成しています。この場合、先頭に空白を含めて 7 文字挿入することになります。行番号の削除は行の先頭から 7 文字削除するだけです。テキストウィジェットのメソッドで範囲指定を指定する場合、終了位置の文字は範囲に含まれません。ご注意くださいませ。これで 0 から 6 文字目までの 7 文字が削除されます。

最後に、関数 load_file() を修正します。ファイルを読み込んだあとで num_flag が真であれば、change_number() を呼び出して行番号を挿入します。これでプログラムは完成です。

行番号の表示 行番号の表示

●プログラムリスト

リスト : 行番号の表示

import tkinter as tk
import tkinter.filedialog as fd
import tkinter.scrolledtext as st
import sys, os.path

# MainWindow
root = tk.Tk()
root.option_add('*font', ('Noto Sans Mono CJK JP', 10))
root.title('Text Viewer')

# Global
path_name = os.getcwd()

# Text
t0 = st.ScrolledText()
t0.pack()

#
num_flag = tk.BooleanVar()
num_flag.set(False)

# Change Number
def change_number():
    line = int(float(t0.index('end')))
    if num_flag.get():
        for x in range(1, line):
            t0.insert('{}.0'.format(x), '{:6d}:'.format(x))
    else:
        for x in range(1, line):
            t0.delete('{}.0'.format(x), '{}.7'.format(x))

# File Select
def load_file():
    global path_name
    filename = fd.askopenfilename(filetypes = [('Text Files', ('.txt', '.py'))],
                                  initialdir = path_name)
    if filename != "":
        path_name = os.path.dirname(filename)
        fi = open(filename)
        t0.delete('1.0', 'end')
        for x in fi:
            t0.insert('end', x)
        fi.close()
        if num_flag.get(): change_number()
        t0.mark_set(tk.INSERT, '1.0')
        t0.focus_set()

# Menu
m0 = tk.Menu(root);
root.configure(menu = m0);

m1 = tk.Menu(m0, tearoff = 0)
m1.add_command(label = 'Open', under = 0, command = load_file)
m1.add_checkbutton(label = 'Number', under = 0, variable = num_flag, command = change_number)
m1.add_separator
m1.add_command(label = 'Exit', under = 0, command = sys.exit)
m0.add_cascade(label = 'File', under = 0, menu = m1 )

root.mainloop()

テキストウィジェット : マークとタグ

テキストウィジェットの最大の特徴は、特定の位置をマークしたり、特定の文字列にタグをつけ、フォントや色といった属性の変更やバインディングの設定が可能なことです。また、キャンバスウィジェットと同様に、テキストの中にウィジェットを表示することもできます。

●マーク

それではマークから説明しましょう。マークはテキストの位置を表す名前のことです。マークは文字自体につけられるのではなく、文字と文字の間に設定されます。このため、マークで指定した位置に文字列を挿入する場合はとても便利です。また、テキストを操作するメソッドで、位置の指定にマークを使うこともできます。

マークを操作するおもなメソッドを表に示します。

表 : マーク操作用のおもなメソッド
mark_set(markname, index)マークの設定
mark_unset(*markname)マークの削除
mark_names()定義されているすべてのマークを返す
mark_gravity(markname, left_or_right)マークのつき方を left と right で指定
mark_next(index)index より後ろにあるマークを返す
mark_previous(index)index より前にあるマークを返す

マークの設定は mark_set() メソッドで行います。マークは指定した位置の文字とその前の文字の間に設定されます。たとえば '1.3' と指定すると、1 行目の 2 文字目と 3 文字目の間にマークが設定されます。文字は 0 から数えることに注意してください。たとえば、テキストウィジェットの 1 行目に abcdefg が書き込まれている状態で、次のように first という名前のマークを設定します。

t0.mark_set(first, '1.3')

変数 t0 はテキストウィジェットのオブジェクトです。これで、マーク first は 1 行目の 2 文字目 (c) と 3 文字目 (d) の間に設定されます。この状態で 1 行目の先頭文字 a を削除すると文字 d は 2 文字目になるので、first の位置は 1.3 ではなく 1.2 に変わります。また、行頭に文字 A を挿入すれば d は 4 文字目になるので、first は 1.3 から 1.4 に変わります。このように指定した文字 d が移動すれば、その文字とともにマークも移動するわけです。

マークの位置に文字列を挿入する場合、マークは挿入した文字列の左右どちらかにつきます。mark_gravity() メソッドは、文字列を挿入したときのマークのつき方を指定します。left であれば挿入した文字列の左側に、right であれば右側にマークが設定されます。たとえば、first の位置に文字列 1234 を挿入するには、次のように行います。

t0.insert(first, '1234')

first の位置が c と d の間であれば、文字列は abc1234defg となります。このとき、マークが挿入した文字列の左側につく場合は c と 1 の間にマークが設定されます。逆に、右側につく場合は 4 と d の間に設定されます。デフォルトの設定は right なので、ここでもう一度 first に文字列 5678 を挿入すると、文字列は abc12345678defg となります。

それから、特別なマークとしてカーソル位置を表す insert と、マウスカーソルが指す文字位置を表す current があります。たとえば、カーソルの位置に文字列を挿入したい場合は、次のように行います。

t0.insert(insert, 'string')

これで指定した string がカーソル位置に挿入されます。

●タグ

テキストウィジェットのタグはキャンバスウィジェットのタグと同様に、指定した文字列に名前(タグ名)をつける機能です。そして、タグごとにフォントや色などの表示属性やバインディングを設定することができます。この機能により、テキストウィジェットは単なるテキスト編集だけではなく、ある単語をクリックしたら別のテキストを表示する、といったハイパーテキストを構成することができます。

Python/Tkinter では、タグを操作するメソッドが多数用意されています。タグの設定、削除、検索といった基本的な機能のほかに、オプションやバインディングの設定を行うことができます。フォント、色、アンダーラインなどの表示属性はオプションで設定します。おもなメソッドを表に示します。

表 : タグ操作用のおもなメソッド
tag_add(tagname, index1, index2)指定した範囲に対して、タグ tagname を設定
tag_delete(*tagname)タグの削除
tag_names(index)index の位置にある文字と関連するすべてのタグを返す
tag_cget(tagname, option)タグ tagname のオプションの値を返す
tag_configure(tagname, option, value)タグ tagname のオプションを設定する
tag_bind(tagname, event, callback)タグ tagname にバインドを設定する

このほかにも、いろいろなメソッドやオプションが用意されています。詳細は Tkinter のマニュアルを参照してもらうことにして、さっそく簡単な例題を示します。前回作成したテキストを表示するプログラムで、行番号を赤く表示してみましょう。行番号を表す文字列にタグ LINENUM を指定し、色をオプションで設定します。

オプションは tag_configure() で設定します。タグ LINENUM の設定は次のようになります。
t0.tag_configure('LINENUM', foreground = 'red')

文字の色は今まで使ってきたウィジェットと同じく foreground で指定します。また、background で背景色も指定することができます。このオプションが指定されていると、borderwidth でふちの幅を、relief で形状を指定することができます。

行番号にタグを設定することはとても簡単です。insert() メソッドでタグを指定するだけです。

t0.insert(index, string, tagname, ...)

タグを指定すると、挿入した文字列にそのタグが設定されます。したがって、前回作成した change_number() を次のように修正するだけです。

リスト : 行番号の挿入と削除

def change_number():
    line = int(float(t0.index('end')))
    if num_flag.get():
        for x in range(1, line):
            t0.insert('{}.0'.format(x), '{:6d}:'.format(x), 'LINENUM')
    else:
        for x in range(1, line):
            t0.delete('{}.0'.format(x), '{}.7'.format(x))

このように、挿入する文字列の後ろにタグ名 LINENUM を指定します。これで行番号が赤く表示されます。

テキストの表示 テキストを表示する

行番号を赤く表示 行番号を赤く表示する


テキストウィジェット : ウィジェットの表示

●埋め込みウィンドウ

テキストウィジェットはキャンバスウィジェットと同様に、テキストだけではなくほかのウィジェットも表示することができます。これを「埋め込みウィンドウ」といいます。埋め込みウィンドウ用のメソッドを次に示します。

window_create() メソッド で使用するオプションを表に示します。

表 : windowCreate メソッドのオプション
windowウィジェット名
createウィジェットを生成するコマンドを指定(window を指定しない場合のみ有効)
align上下方向の揃え指定 (baseline, top, bottom, center)
stretch上下方向の引き延ばし
padx, padyスペースの指定

●プログラムの改造

それでは、テキストにイメージを挿入できるように、前回作成したプログラムを改造してみましょう。ラベルにイメージを貼り付けて、そのラベルをテキストウィジェットに埋め込みます。画像ファイルはメニュー Image で選択し、カーソル位置にその画像を挿入します。

メニュー Image の設定は簡単です。次の 1 行を追加するだけです。

m1.add_command(label = 'Image', under = 0, command = insert_image)

ラベル (画像) を挿入する関数 insert_image() は次のようになります。

リスト : ラベル (画像) の挿入

def insert_image():
    global path_name_image
    filename = askopenfilename(filetypes = [('Image Files', ('.gif', '.ppm')),
                                            ('GIF Files', '.gif'),
                                            ('PPM Files', '.ppm')],
                               initialdir = path_name_image)
    if filename != "":
        if filename in image_buff:
            image_data = image_buff[filename]
        else:
            image_data = PhotoImage(file = filename)
            image_buff[filename] = image_data
        path_name_image = os.path.dirname(filename)
        label = Label(root, image = image_data, relief = 'raised', borderwidth = 4)
        t0.window_create('insert', window = label, align = 'baseline')

askopenfilename() でファイル選択ダイアログを表示し、画像ファイル名を取得します。PhotoImage() で生成したイメージ image_data は、ファイル名をキーにしてディクショナリ image_buff に格納します。同一のファイル名が image_buff にある場合は、そのイメージを再利用します。

あとは、image_data を Label() でラベルに貼り付けます。そして、そのラベルを window_create() メソッドでテキストウィジェットに表示します。表示位置はマーク insert で指定できるので簡単です。

ウィジェットを挿入したあとで、テキストを編集することはもちろん可能です。ウィジェットの前に文字を挿入すればウィジェットは後ろへ移動しますし、文字を削除すれば前の方へ移動します。挿入したウィジェットは、文字と同じ操作で削除することができます。

テキストウィジェット テキストを表示

画像挿入 Tcl/Tk Logo を挿入

テキスト編集 テキストの編集

●埋め込み画像

ちなみに、テキストウィジェットに画像を表示するだけならば、画像専用のメソッドを使った方が簡単です。これを 埋め込み画像 といいます。埋め込み画像用のメソッドを次に示します。

image_create() メソッド で使用するオプションを表に示します。

表 : image_create メソッドのオプション
image表示する画像
name画像を参照するための名前を付ける
align上下方向の揃え指定(baseline, top, bottom, center)
padx, padyスペースの指定

画像を挿入する関数 insert_image() は、次のようになります。

リスト : 画像の挿入

def insert_image():
    global path_name_image
    filename = fd.askopenfilename(filetypes = [('Image Files', ('.gif', '.ppm')),
                                               ('GIF Files', '.gif'),
                                               ('PPM Files', '.ppm')],
                                  initialdir = path_name_image)
    if filename != "":
        if filename in image_buff:
            image_data = image_buff[filename]
        else:
            image_data = tk.PhotoImage(file = filename)
            image_buff[filename] = image_data
        path_name_image = os.path.dirname(filename)
        t0.image_create(tk.INSERT, image = image_data, align = 'baseline')

PhotoImage() で生成したイメージを image_create() メソッドでテキストウィジェットに直接表示します。このように、ラベルウィジェットを生成しない分だけプログラムは簡単になります。

画像挿入 Tcl/Tk Logo を挿入

●プログラムリスト

リスト : 画像の挿入

import tkinter as tk
import tkinter.filedialog as fd
import tkinter.scrolledtext as st
import sys, os.path

# MainWindow
root = tk.Tk()
root.option_add('*font', ('Noto Sans Mono CJK JP', 10))
root.title('Text Viewer')

# Global
path_name = os.getcwd()
path_name_image = os.getcwd()
image_buff = {}

# Text
t0 = st.ScrolledText()
t0.tag_configure('LINENUM', foreground = 'red')
t0.pack()

#
num_flag = tk.BooleanVar()
num_flag.set(False)

# Change Number
def change_number():
    line = int(float(t0.index('end')))
    if num_flag.get():
        for x in range(1, line):
            t0.insert('{}.0'.format(x), '{:6d}:'.format(x), 'LINENUM')
    else:
        for x in range(1, line):
            t0.delete('{}.0'.format(x), '{}.7'.format(x))

# File Select
def load_file():
    global path_name
    filename = fd.askopenfilename(filetypes = [('Text Files', ('.txt', '.py'))],
                                  initialdir = path_name)
    if filename != "":
        path_name = os.path.dirname(filename)
        fi = open(filename)
        t0.delete('1.0', 'end')
        for x in fi:
            t0.insert('end', x)
        fi.close()
        if num_flag.get(): change_number()
        t0.mark_set(tk.INSERT, '1.0')
        t0.focus_set()

# ラベルの挿入
def insert_image():
    global path_name_image
    filename = fd.askopenfilename(filetypes = [('Image Files', ('.gif', '.ppm')),
                                               ('GIF Files', '.gif'),
                                               ('PPM Files', '.ppm')],
                                  initialdir = path_name_image)
    if filename != "":
        if filename in image_buff:
            image_data = image_buff[filename]
        else:
            image_data = tk.PhotoImage(file = filename)
            image_buff[filename] = image_data
        path_name_image = os.path.dirname(filename)
        t0.image_create(tk.INSERT, image = image_data, align = 'baseline')

# Menu
m0 = tk.Menu(root);
root.configure(menu = m0);

m1 = tk.Menu(m0, tearoff = 0)
m1.add_command(label = 'Open', under = 0, command = load_file)
m1.add_checkbutton(label = 'Number', under = 0, variable = num_flag, command = change_number)
m1.add_command(label = 'Image', under = 0, command = insert_image)
m1.add_separator
m1.add_command(label = 'Exit', under = 0, command = sys.exit)
m0.add_cascade(label = 'File', under = 0, menu = m1 )

root.mainloop()

初版 2006 年 3 月 5 日
改訂 2023 年 1 月 3 日

Copyright (C) 2006-2023 Makoto Hiroi
All rights reserved.

[ PrevPage | Python | NextPage ]