【Python中級】cursesの基礎と設計

Python中級

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つ持つ
  • その画面の renderhandle_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
│                                              │
│                                              │
│                                              │
└─────────────────────────────────────┘
タイトルとURLをコピーしました