【Python GUI開発奮闘記】PySide6でコの字鋼材の応力計算アプリを作った話(KeyErrorの罠)

ALL

こんにちは、Tech Samuraiです!
今回は、少し専門的ですが「コの字型鋼材の応力計算」をテーマに、Pythonで便利なデスクトップアプリを開発したプロセスをご紹介します。Pythonに標準で入っているtkinterで手軽に作り始めることも考えましたが、今回はより本格的でモダンなGUIライブラリ**`PySide6`**を選択しました。

この記事では、アプリの設計思想から、実装のポイント、そして開発中に私が実際にハマってしまった**「落とし穴(KeyError)」**とその解決策までを、詳しく解説していきます。プログラミング学習の良い題材になると思いますので、ぜひ最後までお付き合いください!


STEP 1: 何を作るか? – 設計者のための計算ツール

機械設計では、部品が力に耐えられるかを確認するために「応力計算」が欠かせません。「コの字型断面」(チャンネル材)はよく使われる材料ですが、形状が少し複雑で、断面性能(強さの指標)の計算が手作業だと面倒です。

そこで、以下の情報を入力すれば、必要な計算を自動で行ってくれるツールを作ることにしました。

  • 断面の寸法: 高さ(H), 幅(B), ウェブ厚(tw), フランジ厚(tf)
  • 荷重条件: 梁の長さ(L), かかる力(F), 支持条件(片持ち梁など)

このツールがあれば、面倒な計算から解放され、より本質的な設計業務に集中できるはずです。


STEP 2: PySide6による本格的なアプリ開発

「どうせ作るなら、見た目も機能も本格的なものを」と考え、今回はプロの現場でも使われる**`PySide6`**を選択しました。`PySide6`は、非常に高機能でモダンな見た目のアプリが作れる、Qtというフレームワークの公式Pythonバインディングです。

ウィンドウサイズを変更してもレイアウトが崩れないようにQGridLayoutを使ったり、ボタンがクリックされたときの動作を.clicked.connect(...)で指定したりと、本格的なGUIアプリの骨格を組んでいきました。


STEP 3: トラブルシュート – `KeyError: ‘H’` の謎を解け!

見た目も機能もリッチになり、満足のいくアプリが完成!…と思いきや、ここで思わぬエラーに遭遇します。完成したアプリで「計算実行」ボタンを押すと、**`KeyError: ‘H’`**というエラーで停止してしまいました。

これは「辞書データの中に’H’という名前(キー)のデータが見つかりません」という意味です。入力欄から’H’の値を取得しているはずなのに、なぜ…?

原因調査

コードをよく見直すと、入力された値をプログラム内部で保持する辞書(self.inputs)を作成する部分に問題がありました。

問題のコード:

# GUI部品を作るとき
input_fields = {"高さ H (mm):": "100", ...} # 表示用のテキストをキーにしていた

# データを内部で保持するとき
for text, entry in input_fields.items():
    # "高さ H (mm):" を空白で区切って先頭を取得 -> "高さ" というキーが作られていた
    key = text.split(" ")[0]
    self.inputs[key] = entry # つまり self.inputs['高さ'] = ... となっていた

このコードは、画面に表示するラベル名(例: “高さ H (mm):”)から、プログラムが内部で使うキー(’H’)を自動で作り出そうとしていました。しかし、文字列を分割する処理がうまくいかず、キーが期待した**`’H’`**ではなく、日本語の**`’高さ’`**になってしまっていたのです。

その結果、後の計算処理で`’H’`というキーで値を取り出そうとしても、「そんな名前のデータは登録されていませんよ!」とPythonに怒られていたわけです。

解決策

この問題の解決策は、「プログラム内部で使う名前(キー)」と「ユーザーの画面に見せる名前(ラベルテキスト)」を、明確に分離して管理することです。

修正後のコード:

# キー('H'), 表示テキスト, 初期値をセットで定義
input_fields = [
    ('H', "高さ H (mm):", "100"),
    ('B', "幅 B (mm):", "50"),
    # ...
]

# 正しいキーで辞書に登録
for key, text, default_val in input_fields:
    # ...
    # self.inputs['H'] = ... と正しく登録される
    self.inputs[key] = line_edit

このように修正することで、プログラムは`’H’`というキーで確実に入力値にアクセスできるようになり、エラーは無事解消されました!


完成版:応力計算ツールの全コード

上記の修正を反映した、最終的なツールの全コードはこちらです。

import sys
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QLabel, QLineEdit, QPushButton,
    QComboBox, QVBoxLayout, QHBoxLayout, QGridLayout, QMessageBox, QGroupBox
)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont

# --- 計算ロジック部分 (変更なし) ---

def calculate_section_properties(H, B, tw, tf):
    """コの字型断面の断面性能を計算する関数"""
    if H <= 0 or B <= 0 or tw <= 0 or tf <= 0 or H <= 2 * tf or B <= tw:
        return None
    try:
        A = (H * tw) + 2 * ((B - tw) * tf)
        web_area_moment = (H * tw) * (tw / 2)
        flange_area_moment = 2 * ((B - tw) * tf) * (tw + (B - tw) / 2)
        ex = (web_area_moment + flange_area_moment) / A
        Ix = (B * H**3) / 12 - ((B - tw) * (H - 2 * tf)**3) / 12
        Iy1 = (H * tw**3) / 12 + (H * tw) * (ex - tw / 2)**2
        Iy2_single = (tf * (B - tw)**3) / 12 + ((B - tw) * tf) * ((tw + (B - tw) / 2) - ex)**2
        Iy2 = 2 * Iy2_single
        Iy = Iy1 + Iy2
        Zx = Ix / (H / 2)
        Zy1 = Iy / ex
        Zy2 = Iy / (B - ex)
        return {
            "断面積 A (mm^2)": A, "図心 ex (mm)": ex, "断面二次モーメント Ix (mm^4)": Ix,
            "断面二次モーメント Iy (mm^4)": Iy, "断面係数 Zx (mm^3)": Zx,
            "断面係数 Zy1 (背面側) (mm^3)": Zy1, "断面係数 Zy2 (開口側) (mm^3)": Zy2,
        }
    except (ValueError, ZeroDivisionError):
        return None

def calculate_stress(F, L, Z, condition):
    """最大曲げモーメントと最大曲げ応力を計算する関数"""
    M = 0
    if condition == "片持ち梁 (先端荷重)":
        M = F * L
    elif condition == "両端支持梁 (中央荷重)":
        M = (F * L) / 4
    sigma = M / Z if Z and Z != 0 else float('inf')
    return M, sigma

# --- GUI部分 (PySide6) ---

class StressCalculatorApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("コの字型断面 応力計算ツール (PySide6版)")
        self.setGeometry(100, 100, 600, 600)

        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        # --- 入力グループ ---
        input_group = QGroupBox("入力情報")
        input_layout = QGridLayout()
        input_group.setLayout(input_layout)
        
        self.inputs = {}
        
        input_fields = [
            ('H', "高さ H (mm):", "100"),
            ('B', "幅 B (mm):", "50"),
            ('tw', "ウェブ厚 tw (mm):", "5"),
            ('tf', "フランジ厚 tf (mm):", "7"),
            ('L', "梁の長さ L (mm):", "1000"),
            ('F', "荷重 F (N):", "500")
        ]

        for i, (key, text, default_val) in enumerate(input_fields):
            label = QLabel(text)
            line_edit = QLineEdit(default_val)
            input_layout.addWidget(label, i, 0)
            input_layout.addWidget(line_edit, i, 1)
            self.inputs[key] = line_edit

        self.condition_combo = QComboBox()
        self.condition_combo.addItems(["片持ち梁 (先端荷重)", "両端支持梁 (中央荷重)"])
        # 'i' はループの最後の値なので、次の行に配置するために i + 1 を使う
        input_layout.addWidget(QLabel("支持条件:"), i + 1, 0)
        input_layout.addWidget(self.condition_combo, i + 1, 1)

        main_layout.addWidget(input_group)

        # --- 計算ボタン ---
        calc_button = QPushButton("計算実行")
        calc_button.clicked.connect(self.perform_calculation)
        main_layout.addWidget(calc_button)

        # --- 結果表示グループ (変更なし) ---
        result_group = QGroupBox("計算結果")
        result_layout = QGridLayout()
        result_group.setLayout(result_layout)

        self.result_labels = {}
        result_keys = [
            "断面積 A (mm^2)", "図心 ex (mm)", "断面二次モーメント Ix (mm^4)",
            "断面二次モーメント Iy (mm^4)", "断面係数 Zx (mm^3)",
            "断面係数 Zy1 (背面側) (mm^3)", "断面係数 Zy2 (開口側) (mm^3)",
            "最大曲げモーメント M (N・mm)", "最大曲げ応力 σ (MPa or N/mm^2)"
        ]
        
        row = 0
        for key in result_keys:
            if "モーメント" in key and row > 0:
                line = QWidget()
                line.setFixedHeight(1)
                line.setStyleSheet("background-color: #c0c0c0;")
                result_layout.addWidget(line, row, 0, 1, 2)
                row += 1

            key_label = QLabel(f"{key}:")
            val_label = QLabel("-")
            result_layout.addWidget(key_label, row, 0)
            result_layout.addWidget(val_label, row, 1)
            self.result_labels[key] = val_label
            row += 1

        main_layout.addWidget(result_group)
        main_layout.addStretch(1)

    def perform_calculation(self):
        """ 計算を実行し、結果をGUIに表示する """
        try:
            params = {key: float(entry.text()) for key, entry in self.inputs.items()}
            
            section_props = calculate_section_properties(params['H'], params['B'], params['tw'], params['tf'])
            
            if section_props is None:
                self.show_error("入力された寸法が不正です。\n(例: H > 2*tf, B > tw)")
                return

            for key, value in section_props.items():
                self.result_labels[key].setText(f"{value:.2f}")

            condition = self.condition_combo.currentText()
            M, sigma = calculate_stress(params['F'], params['L'], section_props["断面係数 Zx (mm^3)"], condition)

            self.result_labels["最大曲げモーメント M (N・mm)"].setText(f"{M:.2f}")
            self.result_labels["最大曲げ応力 σ (MPa or N/mm^2)"].setText(f"{sigma:.2f}")

        except ValueError:
            self.show_error("すべての入力フィールドに有効な数値を入力してください。")
        except Exception as e:
            self.show_error(f"エラーが発生しました: {e}")

    def show_error(self, message):
        """ エラーメッセージをポップアップで表示 """
        msg_box = QMessageBox()
        msg_box.setIcon(QMessageBox.Icon.Critical)
        msg_box.setText(message)
        msg_box.setWindowTitle("エラー")
        msg_box.exec()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = StressCalculatorApp()
    window.show()
    sys.exit(app.exec())

まとめ

今回のアプリ開発の旅を振り返ってみましょう。

  • アイデアの実現: まずは解決したい課題を明確にし、そのためのツールを設計する。
  • 品質の向上: `PySide6`のような高機能ライブラリを使い、本格的で使いやすいUIを構築する。
  • デバッグの重要性: エラーは成長のチャンス!原因を冷静に分析し、より堅牢なコードに修正する。

特に今回の`KeyError`は、プログラムの内部ロジックとユーザーが見る画面表示を安易に結びつけてしまったことが原因でした。これは多くの初学者が陥りやすい罠かもしれません。この経験が、これからPythonでGUIアプリを作ろうとしている方の助けになれば幸いです。

コメント

タイトルとURLをコピーしました