doc drawn up: 2012-04-16 .. 2022-12-23

Python

Hello, ルパン!?

サンプルプログラムの定番は Hello, world! ですが、今回 Python に手を染める理由は CUI プログラムではなく Pygame を使った GUI アプリケーションを作成することにあるため、ちょっとした GUI アプリケーションのサンプルプログラムを作成することにします。

そこでサンプルプログラムとして思い付いたのが、ルパン三世のタイトルみたいなものを表示するジョークソフトです。Vector にも Windows 3.1 版(Windows 2000 でも動きます)が公開されているもので、知る人ぞ知る、ちょっとしたお遊びアプリケーションです。元祖は X68000 版のようですが、色々な人がクローンを作成したようで、Macintosh 版も見たことがあります。

今回はそれを Python + Pygame で作ってしまおうというわけです。Pygame さえインストールすれば、Windows だけでなく、macOS や Linux でも動作可能です。

Lupin the 3rd のソース

作成したプログラムのソースは以下のようになりました:


#!/usr/bin/env python

# lupin3rd.py v2.0 ©2012-2022 M.Hata All rights reserved.

# python -m PyInstaller --noconsole --onefile --icon=lupin3rd.ico lupin3rd.py
# で Windows EXE 化

# python 3.10.9 / pygame 2.1.2 で動作確認(3.11.x は pygame 2.1.2 が未対応)
import pygame
from pygame.locals import *

import sys # sys.exit() で使っている
import random # 乱数処理のために使う

# 設定値
SCREEN_WIDTH  = 640
SCREEN_HEIGHT = 480
FONT_FILE = 'ipaexm.ttf'
TEXT_FILE = 'lupin3rd.txt'
SOUND_FILE_1 = 'lupin3rd_1.wav'
SOUND_FILE_2 = 'lupin3rd_2.wav'

# ヘッドラインのテキストに関しては、OOP 的なクラスとしてまとめてみた
class Headline:
    def __init__(self, lines: [str]):
        headlines = []
        for line in lines:
            headlines.append(line.rstrip())
        self.texts = tuple(headlines) # 改行除去の上、タプル化してフィールドメンバーとして格納

        self.lottery() # 最初のランダム選択処理を行う

    # テキストから表示する行をランダムに選ぶ
    def lottery(self) -> None:
        number = random.randint(0, len(self.texts) - 1)
        self.text = self.texts[number] # ランダム選択したものをフィールドメンバーとして格納

        # その選んだテキストの文字数と画面サイズから、文字のサイズを決定する
        self.chars = len(self.text)
        self.font_size = int(SCREEN_WIDTH / self.chars)

# キーイベントを判別するシンプルな関数サブルーチン
def checkKeyEvent() -> int:
    for event in pygame.event.get():
        if (event.type == QUIT) or (event.type == KEYDOWN and event.key == K_ESCAPE):
            # ウィンドウを閉じるか、ESC キーが押された場合 ⇒ 終了
            return 0
        elif event.type == KEYDOWN and event.key == K_SPACE:
            # SPACE キーが押された場合 ⇒ 新しいアニメーションを開始
            pygame.event.clear()
            return 1
        elif event.type == KEYDOWN and event.key == K_TAB:
            # TAB キーが押された場合 ⇒ 全画面モード
            pygame.event.clear()
            return 2
        elif event.type == KEYDOWN:
            # ESC、SPACE、TAB 以外の何らかのキーが押された場合 ⇒ ウィンドウモード
            pygame.event.clear()
            return 3

    # キーイベント無し
    return -1



# ファイルからのテキスト読み込み
datafile = open(TEXT_FILE, 'r', encoding = 'utf-8')
lines = datafile.readlines()
datafile.close()

headline = Headline(lines)

# Pygameライブラリの初期化
pygame.init()

# フォントの設定(font_large は不変だが、font_small は今後の処理で適宜変動することに留意)
font_large = pygame.font.Font(FONT_FILE, SCREEN_HEIGHT)
font_small = pygame.font.Font(FONT_FILE, headline.font_size)

typing_sound = pygame.mixer.Sound(SOUND_FILE_1)
pygame.mixer.music.load(SOUND_FILE_2)

clock = pygame.time.Clock()

# ウィンドウの設定
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), FULLSCREEN)
pygame.display.set_caption('ルパン3世')
pygame.mouse.set_visible(False)

screen.fill((0, 0, 0))

# 各フラグとカウンター変数の初期化
frame = 0
charIndex = 0
animationOnGoing = True

# 以下、Windows アプリケーションとしてのイベント待ち無限ループ
while True:
    pygame.event.pump()

    if animationOnGoing:
        if frame % 15 == 0: # 15 フレーム(0.25 秒)毎に 1 ステップ進める
            if charIndex < headline.chars:
                # 効果音と共に文字を 1 文字ずつ表示する
                text = font_large.render(headline.text[charIndex], True, (255, 255, 255))
                screen.fill((0, 0, 0))
                screen.blit(text, ((SCREEN_WIDTH - SCREEN_HEIGHT) / 2, 0))
                typing_sound.play()
                charIndex += 1
            elif charIndex == headline.chars:
                # テキスト全体を表示する
                text = font_small.render(headline.text, True, (255, 255, 255))
                screen.fill((0, 0, 0))
                screen.blit(text, (0, SCREEN_HEIGHT / 2 - headline.font_size / 2))
                charIndex += 1
            elif charIndex == headline.chars + 1:
                # 音楽を再生する
                pygame.mixer.music.play()
                animationOnGoing = False

        frame += 1
    else:
        match checkKeyEvent():
            case 0: # 終了
                pygame.quit()
                sys.exit(0)
            case 1: # 新しいアニメーションを開始
                headline.lottery()
                font_small = pygame.font.Font(FONT_FILE, headline.font_size)

                screen.fill((0, 0, 0))

                # 各フラグとカウンター変数の初期化
                frame = 0
                charIndex = 0
                animationOnGoing = True
            case 2: # 全画面モードに
                screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), FULLSCREEN)
                text = font_small.render(headline.text, True, (255, 255, 255))
                screen.blit(text, (0, SCREEN_HEIGHT / 2 - headline.font_size / 2))
                pygame.mouse.set_visible(False)
            case 3: # ウィンドウモードに(全画面モード解除)
                screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
                text = font_small.render(headline.text, True, (255, 255, 255))
                screen.blit(text, (0, SCREEN_HEIGHT / 2 - headline.font_size / 2))
                pygame.mouse.set_visible(True)

    clock.tick(60) # 60fps のペースでこの while ループを処理する
    pygame.display.update()

プログラミングに慣れている人であれば、ソースのコメントだけでも、十分に理解できるのではないかと思います。いくつか要点を述べておくと:

コーディング作業

コーディング作業は、IDLE で Python プログラムのソースファイルを編集しつつ(Edit with IDLE)、逐次 IDLE の「Run Module」で実行させてチェックしつつ、再び IDLE の画面に戻って手を加えていくという形で進めていくことになります。エラーなどの諸情報は Python Shell の画面に報告されます。また、変数の値などをチェックするために print 命令で Python Shell に出力して調べたりできます。この IDLE のようなものが標準で備わっているという点が、実は Python の見逃せない美点だと思います。

PyInstaller による Windows .exe ファイル化

配布時に、Python をわざわざインストールさせずに済むように、.exe ファイル化してみた。作業も Windows 上で行う必要があるが、PyInstaller を使えばよい。


pyinstaller --onefile lupin3rd.py

基本的にはこんな感じで使えるが今回の lupin3rd はコンソールアプリではないので、実際にはさらに noconsole オプションを付け、 icon オプションも指定してみた。

Windows 用のコンソールアプリで、エスケープシーケンスを有効にしたい場合

ついでの話として、もし、PyInstaller を使って Windows .exe 化する時、元が Mac や Linux で開発したコンソールアプリであった場合、エスケープシーケンスを使ったやや凝った出力にする場合も考えられるが、この場合は、Python ではなく Windows の問題として、コマンドプロンプトがデフォルトでは、エスケープシーケンスに対応していないという問題がある(WSL だ何だと言っても、所詮はポーズなのか?)。これについては、調べてみると、簡単にできることがわかった。

レジストリー(HKEY_CURRENT_USER\Console)に VirtualTerminalLevel = 1 を追加するだけ。

次の内容のテキストファイルを例えば VirtualTerminalLevel.reg として保存したものを配布し、レジストリーエディターから統合する形で使ってもらうなりすればよい:


Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Console]
"VirtualTerminalLevel"=dword:00000001

ダウンロード

[lupin3rd-2.0.zip]

その他の話題


Perl との比較でハマりやすかった点

foreach の違い
PythonPerlJava

#!/usr/bin/env python

strings = ['a', 'b', 'c']

for string in strings:
    print(string)

for string in strings:
    string += '+'

for string in strings:
    print(string)

#!/usr/bin/env perl -w
use warnings;
use v5.12;

my @strings = ('a', 'b', 'c');

for my $string (@strings) {
    say $string;
}

for my $string (@strings) {
    $string .= '+';
}

for my $string (@strings) {
    say $string;
}

List<String> strings =
  new ArrayList<>(
    Arrays.asList("a", "b", "c")
);

for (String string: strings) {
    System.out.println(string);
}

for (String string: strings) {
    string += "+";
}

for (String string: strings) {
    System.out.println(string);
}

a
b
c
a
b
c

a
b
c
a+
b+
c+

a
b
c
a
b
c

Perl の foreach は(参照渡しされるため)便利だったのだが、同じ使い方が Python ではできない(参照渡しされない)ので、そこは注意を要する。Perl の foreach 的な使い方をしたいのであれば、Python では普通にカウンター変数 i を使った従来型の for ループにするしかない。

というか、Perl の foreach が過剰に便利すぎただけで、Python だけでなく、Java でも foreach は参照渡しされない。Perl のように foreach で direct modification できるのは、他には Perl の派生である PHP や Raku だけのようだ。


<Programming>