授業でPygameを触り、グループワーク時にタイピングゲームを作ったので、その時に面倒だったテキスト入力の備忘録。
Pygame 1.9.5(SDL2.0.0)以降のテキスト入力
Pygame1.9.5以降、新たなイベントが加わり、テキスト入力が比較的楽になったようです(多分)。とはいえ、Pygameの日本語のドキュメントにそれがまだ載ってない上に、公式のテキスト入力のサンプル(この記事の執筆時の最新コミット)も分かりづらかったので、改めて書くことにしました。テキスト入力系で追加されたのは、TEXTINPUTとTEXTEDITINGの2つです。TEXTINPUTは直接入力モード時に受け取るイベント、TEXTEDITINGがIMEを使っている時に受け取るイベントらしいです。また、SDLのバグの関係で、IMEの予測変換は使えないので今回はそれを扱っていません。
プログラム
ライブラリ化したものを上げておきました。ご活用ください。
以下がテキストの処理をするロジック(text.py)
from typing import List
class Text:
"""
PygameのINPUT、EDITINGイベントで使うクラス
カーソル操作や文字列処理に使う
"""
def __init__(self) -> None:
self.text = ["|"] # 入力されたテキストを格納していく変数
self.editing: List[str] = [] # 全角の文字編集中(変換前)の文字を格納するための変数
self.is_editing = False # 編集中文字列の有無(全角入力時に使用)
self.cursor_pos = 0 # 文字入力のカーソル(パイプ|)の位置
def __str__(self) -> str:
"""self.textリストを文字列にして返す"""
return "".join(self.text)
def edit(self, text: str, editing_cursor_pos: int) -> str:
"""
edit(編集中)であるときに呼ばれるメソッド
全角かつ漢字変換前の確定していないときに呼ばれる
"""
if text: # テキストがあるなら
self.is_editing = True
for x in text:
self.editing.append(x) # 編集中の文字列をリストに格納していく
self.editing.insert(editing_cursor_pos, "|") # カーソル位置にカーソルを追加
disp = "[" + "".join(self.editing) + "]"
else:
self.is_editing = False # テキストが空の時はFalse
disp = "|"
self.editing = [] # 次のeditで使うために空にする
# self.cursorを読み飛ばして結合する
return (
format(self)[0 : self.cursor_pos]
+ disp
+ format(self)[self.cursor_pos + 1 :]
)
def input(self, text: str) -> str:
"""半角文字が打たれたとき、もしくは全角で変換が確定したときに呼ばれるメソッド"""
self.is_editing = False # 編集中ではなくなったのでFalseにする
for x in text:
self.text.insert(self.cursor_pos, x) # カーソル位置にテキストを追加
# 現在のカーソル位置にテキストを追加したので、カーソル位置を後ろにずらす
self.cursor_pos += 1
return format(self)
def delete_left_of_cursor(self) -> str:
"""カーソルの左の文字を削除するためのメソッド"""
# カーソル位置が0であるとき
if self.cursor_pos == 0:
return format(self)
self.text.pop(self.cursor_pos - 1) # カーソル位置の一個前(左)を消す
self.cursor_pos -= 1 # カーソル位置を前にずらす
return format(self)
def delete_right_of_cursor(self) -> str:
"""カーソルの右の文字を削除するためのメソッド"""
# カーソル位置より後ろに文字がないとき
if len(self.text[self.cursor_pos+1:]) == 0:
return format(self)
self.text.pop(self.cursor_pos + 1) # カーソル位置の一個後(右)を消す
return format(self)
def enter(self) -> str:
"""入力文字が確定したときに呼ばれるメソッド"""
# カーソルを読み飛ばす
entered = (
format(self)[0 : self.cursor_pos] + format(self)[self.cursor_pos + 1 :]
)
self.text = ["|"] # 次回の入力で使うためにself.textを空にする
self.cursor_pos = 0 # self.text[0] == "|"となる
return entered
def move_cursor_left(self) -> str:
"""inputされた文字のカーソル(パイプ|)の位置を左に動かすメソッド"""
if self.cursor_pos > 0:
# カーソル位置をカーソル位置の前の文字と交換する
self.text[self.cursor_pos], self.text[self.cursor_pos - 1] = (
self.text[self.cursor_pos - 1],
self.text[self.cursor_pos],
)
self.cursor_pos -= 1 # カーソルが1つ前に行ったのでデクリメント
return format(self)
def move_cursor_right(self) -> str:
"""inputされた文字のカーソル(パイプ|)の位置を右に動かすメソッド"""
if len(self.text) - 1 > self.cursor_pos:
# カーソル位置をカーソル位置の後ろの文字と交換する
self.text[self.cursor_pos], self.text[self.cursor_pos + 1] = (
self.text[self.cursor_pos + 1],
self.text[self.cursor_pos],
)
self.cursor_pos += 1 # カーソルが1つ後ろに行ったのでインクリメント
return format(self)
次に、プログラム本体(text_input.py)ですが、日本語が扱えるフォントを用意しておく必要があります(Windowsならコピペでも動くかもしれません)。追記をご参照ください。
↓プログラム本体
import sys
import pygame
from pygame.locals import *
from text import Text
def draw_text(text: str) -> None:
"""
入力文字を描画するための関数
"""
text_surface = font.render(text, True, (0, 0, 0))
screen.fill((112, 225, 112))
# テキストに応じて上下左右中央揃えにする
center_w = (800 / 2) - (text_surface.get_width() / 2)
center_h = (600 / 2) - (text_surface.get_height() / 2)
screen.blit(text_surface, (center_w, center_h))
pygame.display.update()
def event_loop():
# テキスト入力時のキーとそれに対応するイベント
event_trigger = {
K_BACKSPACE: text.delete_left_of_cursor,
K_DELETE: text.delete_right_of_cursor,
K_LEFT: text.move_cursor_left,
K_RIGHT: text.move_cursor_right,
K_RETURN: text.enter,
}
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit(0)
# キーダウンかつ、全角のテキスト編集中でない
elif event.type == KEYDOWN and not text.is_editing:
if event.key in event_trigger.keys():
input_text = event_trigger[event.key]()
# 入力の確定
if event.unicode in ("\r", "") and event.key == K_RETURN:
print(input_text) # 確定した文字列を表示
draw_text(format(text)) # テキストボックスに"|"を表示
input_text = format(text) # "|"に戻す
break
elif event.type == TEXTEDITING: # 全角入力
input_text = text.edit(event.text, event.start)
elif event.type == TEXTINPUT: # 半角入力、もしくは全角入力時にenterを押したとき
input_text = text.input(event.text)
# 描画しなおす必要があるとき
if event.type in [KEYDOWN, TEXTEDITING, TEXTINPUT]:
draw_text(input_text)
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((800, 600))
font = pygame.font.SysFont("yumincho", 30)
text = Text() # テキスト処理のロジックTextクラスをインスタンス化
pygame.key.start_text_input() # input, editingイベントをキャッチするようにする
draw_text(format(text)) # 起動時にカーソルを表示するようにする
event_loop()
実行結果
テキストを入力し、エンターを押すと入力を確定します。確定すると、コンソール画面に確定した文字が表示される単純な作りにしています。また、日本語入力モード中では一般的に使われるアンダーラインではなく、[]を使って日本語入力モード中であることを示しています。アンダーライン版の動作は追記に載っています。
追記
フォントについて
pygameの初期化をした後
font = pygame.font.SysFont("yumincho", 30)
としていますが、環境によってはyuminchoが無く、文字化けしますので注意ください。(そもそもフォントがない状態は文字化けとは言いませんが…) おすすめはIPAexフォントなど適当な日本語フォントを自身の環境に落とし、
font = pygame.font.Font("落としたフォントまでのパス", フォントサイズ)
とするのがいいでしょう。
一般的な日本語入力
今回は日本語入力中は括弧[]でそれを表していましたが、freetypeを使って少しコードを弄ると普通のアンダーラインを使った入力ができるようになります。
TEXTEDITINGの文字数について
SDLの実装上、TEXTEDITINGが受け取れる(保持できる)サイズが決まっており、現在のSDLのドキュメントを見ると
char[32] text the null-terminated editing text in UTF-8 encoding
と書かれています。
char(1バイト) × 32であり、NULL終端文字を除いて、使えるのは31バイトであると考えられます。UTF-8の日本語は3バイトですので、TEXTEDITING(編集中の文字)が扱える日本語はせいぜい10文字が限界だと思います。
これは簡単なプログラムで確かめることができます。 適当に全角入力で「あいうえおかきくけこさしすせそ」と打っても編集中の文字列は「あいうえおかきくけこ」までしか表示されないかと思います。エンターキーを押して入力を確定させた後もTEXTINPUTで受け取るのは「あいうえおかきくけこ」となっていると思います。この仕様で困ることは個人的にはありませんでしたが、10文字程度入力したのちに変換等をする人は注意が必要になります。
import sys
import pygame
from pygame.locals import *
if __name__ == "__main__":
pygame.init()
pygame.display.set_mode((50, 50))
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit(0)
elif event.type == TEXTEDITING and len(event.text) > 0:
print("編集中の文字: {}".format(event.text))
elif event.type == TEXTINPUT and len(event.text) > 0:
print("入力された文字: {}".format(event.text))
機能追加✨・バグ修正🐛
2021/05/02
- 🐛 カーソルの位置が先頭であるのにも関わらず、その後ろの文字を消そうとしていた部分を修正
2021/05/03
- ✨ デリートキーによる文字削除をする機能を追加