mod_avatar_part.py•16.7 kB
from PySide6.QtWidgets import (
QApplication, QWidget, QLabel, QComboBox, QListWidget, QListWidgetItem,
QVBoxLayout, QHBoxLayout, QScrollArea, QCheckBox
)
from PySide6.QtWidgets import QHBoxLayout, QLabel, QRadioButton, QButtonGroup
from PySide6.QtCore import Qt
import sys
import random
import logging
# ロガーの設定
logger = logging.getLogger(__name__)
class AvatarPartWidget(QWidget):
def __init__(self, part_name, image_files, config=None):
"""
Args:
part_name: パーツ名
image_files: 画像ファイルのリスト
config: 初期設定辞書(Noneならデフォルト値)
"""
super().__init__()
self.part_name = part_name
self.image_files = image_files
# デフォルト値で初期化
self._init_default_values()
# 設定が渡されていれば適用
if config:
self.load_config(config)
# GUI構築(設定を反映した状態で)
self._setup_gui()
def _init_default_values(self):
"""デフォルト値で初期化"""
if len(self.image_files) > 0:
self.base_image = self.image_files[0]
else:
self.base_image = None
self.current_image = self.base_image
self.selected_files = []
self.update_idx = 0
self.interval = 3
self.anime_type = "固定"
self.is_enabled = True # パーツの有効状態
self.random_wait_tick = random.choice([10, 20, 30, 40, 50])
self.random_wait_idx = 0
self.random_anime_idx = 0
self.loop_anime_idx = 0
self.oneshot_idx = 0
#
# save/load confg
#
def save_config(self):
"""
現在の設定を辞書形式で返す
Returns:
dict: 設定辞書
"""
config = {
"part_name": self.part_name,
"base_image": self.base_image,
"selected_files": self.selected_files.copy(),
"interval": self.interval,
"anime_type": self.anime_type,
"is_enabled": self.is_enabled
}
logger.info(f"save_config [{self.part_name}]: {config}")
return config
def load_config(self, config):
"""
設定辞書から値を読み込む
Args:
config: save_config()で保存した辞書
"""
logger.info(f"load_config [{self.part_name}]: {config}")
if "base_image" in config:
self.base_image = config["base_image"]
self.current_image = self.base_image
if "selected_files" in config:
self.selected_files = config["selected_files"].copy()
if "interval" in config:
self.interval = config["interval"]
if "anime_type" in config:
self.anime_type = config["anime_type"]
if "is_enabled" in config:
self.is_enabled = config["is_enabled"]
# カウンタ類をリセット
self.update_idx = 0
self.loop_anime_idx = 0
self.random_anime_idx = 0
self.random_wait_idx = 0
self.random_wait_tick = random.choice([10, 20, 30, 40, 50])
# GUIが既に構築されていれば、GUIにも反映
if hasattr(self, 'combo_base'):
self._apply_config_to_gui()
#
# GUI
#
def _setup_gui(self):
main_layout = QVBoxLayout(self)
# 1段目: パーツ名
part_name_layout = QHBoxLayout()
part_name_layout.addWidget(QLabel("パーツ名:"))
part_name_layout.addWidget(QLabel(self.part_name))
part_name_layout.addStretch(1)
main_layout.addLayout(part_name_layout)
# 2段目: 有効/無効チェックボックス
enabled_layout = QHBoxLayout()
enabled_layout.addWidget(QLabel("有効/無効:"))
self.check_enabled = QCheckBox()
self.check_enabled.setChecked(True)
self.check_enabled.stateChanged.connect(self._on_enabled_changed)
enabled_layout.addWidget(self.check_enabled)
enabled_layout.addStretch(1)
main_layout.addLayout(enabled_layout)
# 3段目: ベース画像
base_layout = QHBoxLayout()
base_layout.addWidget(QLabel("ベース画像:"))
self.combo_base = QComboBox()
self.combo_base.addItems(self.image_files)
self.combo_base.currentTextChanged.connect(self._update_base_image)
base_layout.addWidget(self.combo_base)
base_layout.addStretch(1)
main_layout.addLayout(base_layout)
# 4段目: アニメ画像
anim_layout = QHBoxLayout()
anim_layout.addWidget(QLabel("アニメ画像:"), alignment=Qt.AlignTop)
self.list_anim = QListWidget()
self.list_anim.setSelectionMode(QListWidget.MultiSelection)
for f in self.image_files:
self.list_anim.addItem(QListWidgetItem(f))
self.list_anim.itemSelectionChanged.connect(self._update_selected_files)
self.list_anim.setMaximumHeight(100)
anim_layout.addWidget(self.list_anim)
main_layout.addLayout(anim_layout)
# 5段目: interval
interval_layout = QHBoxLayout()
interval_layout.addWidget(QLabel("インターバル:"))
self.combo_interval = QComboBox()
self.combo_interval.addItems(["1", "2", "4", "8", "10","12", "14", "16", "18", "20", "22", "24", "26", "28", "30"])
self.combo_interval.setCurrentIndex(2)
self.combo_interval.currentTextChanged.connect(self._update_interval)
interval_layout.addWidget(self.combo_interval)
interval_layout.addStretch(1)
main_layout.addLayout(interval_layout)
# 6段目: アニメーションタイプ
anim_type_layout = QHBoxLayout()
anim_type_layout.addWidget(QLabel("アニメタイプ:"))
self.radio_fixed = QRadioButton("固定")
self.radio_loop = QRadioButton("ループ")
self.radio_random_a = QRadioButton("ランダムA")
self.radio_random_b = QRadioButton("ランダムB")
self.radio_oneshot = QRadioButton("ワンショット")
self.anim_type_group = QButtonGroup(self)
self.anim_type_group.addButton(self.radio_fixed)
self.anim_type_group.addButton(self.radio_loop)
self.anim_type_group.addButton(self.radio_random_a)
self.anim_type_group.addButton(self.radio_random_b)
self.anim_type_group.addButton(self.radio_oneshot)
self.anim_type_group.buttonClicked.connect(self._on_anim_type_changed)
self.radio_fixed.setChecked(True)
anim_type_layout.addWidget(self.radio_fixed)
anim_type_layout.addWidget(self.radio_loop)
anim_type_layout.addWidget(self.radio_random_a)
anim_type_layout.addWidget(self.radio_random_b)
anim_type_layout.addWidget(self.radio_oneshot)
anim_type_layout.addStretch(1)
main_layout.addLayout(anim_type_layout)
# 最後に設定をGUIに反映
self._apply_config_to_gui()
def _apply_config_to_gui(self):
"""設定値をGUIウィジェットに反映"""
logger.info(f"_apply_config_to_gui [{self.part_name}]")
logger.info(f" selected_files: {self.selected_files}")
# 有効/無効のチェックボックス
self.check_enabled.blockSignals(True)
self.check_enabled.setChecked(self.is_enabled)
self.check_enabled.blockSignals(False)
# ベース画像のコンボボックス
if self.base_image:
index = self.combo_base.findText(self.base_image)
if index >= 0:
self.combo_base.setCurrentIndex(index)
# アニメ画像のリスト選択
# シグナルを一時的にブロックして、余計なイベント発火を防ぐ
self.list_anim.blockSignals(True)
# まず全選択解除
for i in range(self.list_anim.count()):
self.list_anim.item(i).setSelected(False)
# selected_filesに含まれるものだけ選択
for i in range(self.list_anim.count()):
item = self.list_anim.item(i)
if item.text() in self.selected_files:
item.setSelected(True)
logger.info(f" 選択: {item.text()}")
# シグナルのブロックを解除
self.list_anim.blockSignals(False)
# インターバル
index = self.combo_interval.findText(str(self.interval))
if index >= 0:
self.combo_interval.setCurrentIndex(index)
# アニメタイプのラジオボタン
if self.anime_type == "固定":
self.radio_fixed.setChecked(True)
elif self.anime_type == "ループ":
self.radio_loop.setChecked(True)
elif self.anime_type == "ランダムA":
self.radio_random_a.setChecked(True)
elif self.anime_type == "ランダムB":
self.radio_random_b.setChecked(True)
elif self.anime_type == "ワンショット":
self.radio_oneshot.setChecked(True)
#
# gui handlers
#
def _on_enabled_changed(self, state):
"""有効/無効チェックボックスの状態変化ハンドラ"""
self.is_enabled = bool(state)
logger.info(f"is_enabled [{self.part_name}]: {self.is_enabled}")
def _update_selected_files(self):
self.selected_files = [item.text() for item in self.list_anim.selectedItems()]
logger.info(f"selected_files: {self.selected_files}")
def _update_base_image(self, text):
self.base_image = text
logger.info(f"base_image: {self.base_image}")
def _update_interval(self, text):
self.interval = int(text)
logger.info(f"interval: {self.interval}")
def _on_anim_type_changed(self, button):
self.anime_type = button.text()
logger.info(f"選択されたアニメーションタイプ: {self.anime_type}")
# 共通のカウンタをリセット
self.update_idx = 0
self.loop_anime_idx = 0
self.random_anime_idx = 0
self.random_wait_idx = 0
self.random_wait_tick = random.choice([10, 20, 30, 40, 50])
self.start_oneshot()
logger.info(f"random_wait_tick : {self.random_wait_tick}")
#
# animation
#
def start_oneshot(self):
"""外部からoneshotアニメを開始するトリガー"""
if not self.is_enabled:
return
if len(self.selected_files) > 0:
logger.info(f"{self.part_name}: start_oneshot")
self.oneshot_idx = 1
def update(self):
if not self.is_enabled:
return None
if len(self.image_files) == 0:
return None
if self.update_idx < self.interval:
self.update_idx = self.update_idx + 1
return self.current_image
self.update_idx = 0
if self.anime_type == "固定":
self.current_image = self.base_image
if self.anime_type == "ループ":
self._update_loop()
if self.anime_type == "ランダムA":
self._update_random_a()
if self.anime_type == "ランダムB":
self._update_random_b()
if self.anime_type == "ワンショット":
self._update_oneshot()
return self.current_image
def _update_loop(self):
self.current_image = self.selected_files[self.loop_anime_idx]
self.loop_anime_idx = (self.loop_anime_idx + 1) % len(self.selected_files)
def _update_random_a(self):
"""ランダムA: base画像をランダム時間表示 → アニメ画像をワンショット再生"""
if self.random_anime_idx == 0:
# 待機モード: base画像を表示
self.current_image = self.base_image
# カウントアップ
self.random_wait_idx += 1
# ランダム待機時間に達したらアニメ開始
if self.random_wait_idx >= self.random_wait_tick:
self.random_wait_idx = 0
# 次のランダム待機時間を設定
self.random_wait_tick = random.choice([10, 20, 30, 40, 50])
logger.info(f"random_wait_tick : {self.random_wait_tick}")
# アニメ画像が選択されていればアニメ開始
if len(self.selected_files) > 0:
self.random_anime_idx = 1 # アニメ開始フラグ
else:
# アニメ再生モード
idx = self.random_anime_idx - 1
if idx < len(self.selected_files):
self.current_image = self.selected_files[idx]
self.random_anime_idx += 1
else:
# アニメ終了: 待機モードに戻る
self.random_anime_idx = 0
self.current_image = self.base_image
def _update_random_b(self):
"""ランダムB: selected_filesからランダムに1つ選んで、ランダムな時間表示"""
# カウンタをインクリメント
self.random_wait_idx += 1
# まだ待機時間に達していない場合は、現在の画像をそのまま返す
if self.random_wait_idx < self.random_wait_tick:
return
# 待機時間に達したら、次の画像に切り替え
self.random_wait_idx = 0
# 次のランダムな待機時間を設定
self.random_wait_tick = random.choice([10, 20, 30, 40, 50])
logger.info(f"random_wait_tick : {self.random_wait_tick}")
# ランダムに画像を選択
if len(self.selected_files) > 0:
self.current_image = random.choice(self.selected_files)
else:
# フォールバック
self.current_image = self.base_image
def _update_oneshot(self):
"""ワンショット: selected_filesを順番に表示→base_imageに戻る"""
if self.oneshot_idx == 0:
# 待機モード: base画像を表示
self.current_image = self.base_image
elif self.oneshot_idx > 0:
# アニメ再生モード
idx = self.oneshot_idx - 1
if idx < len(self.selected_files):
self.current_image = self.selected_files[idx]
self.oneshot_idx += 1
else:
# アニメ終了: 待機モードに戻る
self.oneshot_idx = 0
self.current_image = self.base_image
if __name__ == "__main__":
app = QApplication(sys.argv)
# テスト1: デフォルトで生成
print("=== テスト1: デフォルト生成 ===")
part_widget1 = AvatarPartWidget("目", ["01.png", "02.png", "03.png"])
# テスト2: 設定を保存
print("\n=== テスト2: 設定を保存 ===")
config = part_widget1.save_config()
print(f"保存された設定: {config}")
# テスト3: 設定を変更
print("\n=== テスト3: 設定を変更 ===")
part_widget1.anime_type = "ランダムA"
part_widget1.selected_files = ["01.png", "02.png"]
part_widget1.interval = 5
# テスト4: 変更した設定を保存
print("\n=== テスト4: 変更後の設定を保存 ===")
config2 = part_widget1.save_config()
print(f"変更後の設定: {config2}")
# テスト5: 設定付きで新規生成
print("\n=== テスト5: 設定付きで生成 ===")
preset_config = {
"part_name": "口",
"base_image": "02.png",
"selected_files": ["01.png", "03.png"],
"interval": 2,
"anime_type": "ループ",
"is_enabled": True
}
part_widget2 = AvatarPartWidget("口", ["01.png", "02.png", "03.png"], config=preset_config)
part_widget1.show()
part_widget2.show()
sys.exit(app.exec())