こんにちは、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アプリを作ろうとしている方の助けになれば幸いです。
コメント