1. curses (テキストUIライブラリ)
画面を専有する「TUI(テキストUI)」を作るための低レベルライブラリ。
(1)何をするためのもの?
- ターミナル画面(黒い画面)を丸ごと“キャンバス”として使うためのライブラリ
- print ではなく
- 画面座標指定で文字を置く
- 画面を一括再描画する
- キーボード入力を1文字ずつ取得などを行う
- ターミナル画面で見た目のいいUIを作れる(色を変えたり、枠線を引いたり)
- Windowsではそのまま動かないため注意
(2)基本の書き方パターン
import curses
def main(stdscr): #画面オブジェクト(stdscr)を取得※standard screenの略
# 初期設定
curses.curs_set(0) # 通常のターミナルのカーソルを非表示
stdscr.nodelay(False) # getch() をブロッキングして入力待ち状態とする
stdscr.keypad(True) # 矢印キーなどをgetch()で取得できるようにする
while True: #画面描画ループ
stdscr.clear() #描画画面を何もない状態にする
stdscr.addstr(0, 0, "Hello curses") #指定座標(行,列)に文字列を表示する
stdscr.addstr(1, 0, "q で終了") #printと違い座標で表示できる
#↑正確には表示座標と文字列をバッファに入れるだけで、まだ表示されない
#↓ここでたまった文字列を一気に表示(描画)させる
stdscr.refresh()
key = stdscr.getch() #ユーザーのキー入力を取得
#stdscr.nodelayやkeypadの設定がここで生かされる
if key == ord('q'):
break
if __name__ == "__main__":
curses.wrapper(main) #cursesを初期化して、キー入力を即座に取得する特殊モードへ
#ここがエントリポイント。main(stdscr)へ画面を渡す
#wrapperがあることで正常終了できる。無いとクラッシュするかも
ポイント:
- curses.wrapper(main) が例外処理+終了処理をよしなにやってくれる
- stdscr が「標準の画面オブジェクト」
- addstr(y, x, text) で位置指定して文字表示
- getch() でキー入力
(3)curses での設計の考え方
「状態」と「描画」を分ける
#アプリの状態をまとめるクラスを作成(カーソル位置(行番号)と表示アイテムリスト)
class AppState:
def __init__(self):
self.cursor = 0
self.items = ["A", "B", "C"]
#画面描画を担当する関数。
def render(stdscr, state: AppState): #引数に画面オブジェクトとAppStateオブジェクト
stdscr.clear() #画面クリア
for i, item in enumerate(state.items):
marker = ">" if i == state.cursor else " "
stdscr.addstr(i, 0, f"{marker} {item}") #i行0列目に作成した文字列を表示
stdscr.refresh() #描画反映
#キーの状態を更新する関数。この状態とはcursorの位置をどこに移動するか
def handle_key(state: AppState, key: int): #引数にAppStateとKey(キー番号)
if key == curses.KEY_UP: #↑ボタンを押したか?
state.cursor = max(0, state.cursor - 1)#カーソル位置が0未満にならないように
#maxで指定。下記も同じ。
elif key == curses.KEY_DOWN: #↓ボタンを押したか?
state.cursor = min(len(state.items)-1, state.cursor + 1)
- 状態(State):カーソル位置・選択中メニュー・スクロール位置など
- 描画(Render):状態を見て画面に描く
- 入力処理(Handle):キー入力で状態を更新
この 3 つを分けると、あとで画面を増やしたりキー操作を変えたりしやすいです。
画面(Screen / View)ごとにクラスを分割する
tui/
app.py # アプリの入口&メインループ(画面管理)
screens/
base.py # Screen の共通インターフェース
menu.py # メニュー画面(リスト選択)
detail.py # 詳細画面(選択した項目の内容表示)
BaseScreen を作って、MenuScreen, DetailScreen などを継承させるパターンが使いやすいです。
<実例>
1. app.py:メインループ & 画面管理
やること:
- curses 初期化(
curses.wrapper) - 「今どの画面か」を1つ持つ
- その画面の
renderとhandle_keyを呼ぶ - 画面遷移(戻る/進む)を司る
# tui/app.py
import curses from screens.menu import MenuScreen #メインメニューをインポート
def main(stdscr):
curses.curs_set(0) #初期化
stdscr.keypad(True)
screen = MenuScreen() # 最初はメニュー画面
while True:
screen.render(stdscr)
key = stdscr.getch()
# 画面側に「次の画面」を聞く
next_screen = screen.handle_key(key)
if next_screen is None:
# None を返したら終了、とする
break
elif next_screen is not screen:
# 別の画面オブジェクトを返してきたら画面遷移
screen = next_screen
if __name__ == "__main__":
curses.wrapper(main)
2. screens/base.py:画面クラスの共通基底
各画面は「描画」と「キー処理」を持つ、という共通インターフェースにする
# tui/screens/base.py
from abc import ABC, abstractmethod
import curses
class BaseScreen(ABC):
@abstractmethod
def render(self, stdscr: "curses.window") -> None:
"""画面描画"""
@abstractmethod
def handle_key(self, key: int):
"""
キー入力処理。
- 自分のままなら self を返す
- 別画面に遷移するなら新しい Screen を返す
- 終了したいなら None を返す
"""
3. screens/menu.py:メニュー画面
AppState的に「カーソル位置」と「項目リスト」を持つ- Enter で詳細画面へ
- q で終了(None を返す)
# tui/screens/menu.py
import curses
from .base import BaseScreen
from .detail import DetailScreen
class MenuScreen(BaseScreen):
def __init__(self):
self.cursor = 0
self.items = ["Item A", "Item B", "Item C"]
def render(self, stdscr):
stdscr.clear()
stdscr.addstr(0, 0, "メニュー (↑↓で移動, Enterで詳細, qで終了)")
for i, item in enumerate(self.items, start=2):
marker = ">" if i-2 == self.cursor else " "
stdscr.addstr(i, 0, f"{marker} {item}")
stdscr.refresh()
def handle_key(self, key: int):
if key == curses.KEY_UP:
self.cursor = max(0, self.cursor - 1)
elif key == curses.KEY_DOWN:
self.cursor = min(len(self.items)-1, self.cursor + 1)
elif key in (curses.KEY_ENTER, 10, 13):
# 選択された項目を渡して詳細画面へ
selected = self.items[self.cursor]
return DetailScreen(selected)
elif key == ord('q'):
return None # アプリ終了
return self # 画面はそのまま
4. screens/detail.py:詳細画面
- コンストラクタで「どの項目か」を受け取る
- 内容を表示して、b で戻る(MenuScreen に戻る)想定
# tui/screens/detail.py
import curses
from .base import BaseScreen
from .menu import MenuScreen # 戻る用
class DetailScreen(BaseScreen):
def __init__(self, item: str):
self.item = item
def render(self, stdscr):
stdscr.clear()
stdscr.addstr(0, 0, f"詳細画面: {self.item}")
stdscr.addstr(2, 0, "b: 戻る, q: 終了")
stdscr.refresh()
def handle_key(self, key: int):
if key == ord('b'):
return MenuScreen() # シンプルに毎回作り直し
elif key == ord('q'):
return None
return self
5.まとめ
screens/は「画面ごとのクラス」が入る場所BaseScreenで 画面の型 を決めるapp.pyは「今の画面を回すだけのお世話係」Screen.render():表示担当Screen.handle_key():状態変更&画面遷移担当
この形にしておくと、画面を追加したくなったときは:
screens/list.py追加class ListScreen(BaseScreen): ...- 必要な画面から
return ListScreen()とするだけ
でどんどん増やせます。
2.おまけ
実例により表示される画面イメージを以下に置いておきます。
MenuScreen(screens/menu.py)
┌─────────────────────────────────────┐
│メニュー (↑↓で移動, Enterで詳細, qで終了) │ ← 行0
│ │ ← 行1(空行)
│> Item A │ ← 行2(cursor = 0 のとき)
│ Item B │ ← 行3
│ Item C │ ← 行4
│ │
│ │
│ (ターミナル画面) │
└─────────────────────────────────────┘
↓ キーでカーソルを動かすと:
┌─────────────────────────────────────┐
│メニュー (↑↓で移動, Enterで詳細, qで終了) │
│ │
│ Item A │
│> Item B │ ← cursor = 1
│ Item C │
│ │
└─────────────────────────────────────┘
DetailScreen(screens/detail.py)
┌─────────────────────────────────────┐
│詳細画面: Item B │ ← 行0
│ │ ← 行1(空行)
│b: 戻る, q: 終了 │ ← 行2
│ │
│ │
│ │
└─────────────────────────────────────┘

