1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python×CustomTkinterで病院で使う患者の体重管理アプリ作ってみた

1
Posted at

はじめに

 こんにちは。
 私は2026年6月中旬からBizCodeXというプログラミングスクールでPythonの学習を行っています。普段は看護師をしていて、仕事の傍ら副業を目指して日々学習を進めていくなかで、アウトプットをかねて「体重管理アプリ」を作ってみました。
 私の勤務先では、患者さんの体重をノートで手書き管理しています。しかし、月が変わるたびに患者名を新しいページへ書き写す必要があり、毎月同じ作業を繰り返していました。
 そこで、現在使っているノートをそのままアプリにしたようなものが作ってみたいと考えPythonとCustomTkinterを使って作成してみることにしました。

現在使っているノートの内容

まず、現在使用しているノートの内容をご紹介します。
・患者さんの名前
・測定期間(1週間ごと,2週間ごと,1ヶ月ごと)
・日付
・体重
実際のイメージ画像がこちらです。

※患者名などの個人情報は架空のデータに置き換えています
私の病院では3Fと4Fに病床があり、階数ごとに患者さんの名前を分けて書いています。

コードを書き始める前に

 アウトプットもアプリを作るのもはじめてだったので、何からすればいいかわからなかったのですが、とりあえず作りたいものをイメージできるように紙にイメージ図を書くことから始めました。
紙に書いたイメージ図を写真に撮り、chatGPTにデジタル化してもらったのがこちらです。

これを頭に思い浮かべながらアプリ作成に取り掛かりました。

作成したアプリの概要

 作成したアプリでは、現在使用しているノートをできるだけそのまま再現することを目標にしました。
主な機能は以下のとおりです。
・患者名の登録・編集・削除
・体重と測定日の登録
・データのCSV保存・読み込み
・グラフによる体重推移の表示(←現在使用しているノートにはない内容ですが、あると便利だと思い実装しました。)

実際に出来上がったアプリの紹介

(スタート画面)

最初の画面では、新規の患者さんの名前と測定頻度を入力する形にしました。
「💾保存する」を押すとCSVファイルに保存され、「📖一覧を見る」のボタンで入力した内容を見ることができます。

(「💾保存する」ボタンを押した後の画面)

(「📖一覧を見る」ボタンを押した後の画面)

 それぞれの項目を手入力で編集できるようにし、ボタンで更新できるようにしました。(CSVのデータも更新されます。)
削除もボタンひとつで1行削除できる形にしました。また、グラフボタンで患者さんそれぞれの体重データをグラフで見ることができます。
 本当は、階数ごとに患者を分けたかったのですが実装できませんでした。
また、月ごとに一覧を表示するようにしたかったのですが出来ず、今後の課題にしたいと思いました。

(「グラフ」ボタンを押した後の画面)

使用した技術

・Python
・CustomTkinter
・CSV
・matplotlib
・datetime

実際のソースコードとポイント

ここでは実際に書いたコードとそのポイントを書いていきます。
(※ソースコード全文は記事の最後に掲載します。)

  1. 画面の切り替え
#画面の初期化
def clear_screen():
    for widget in app.winfo_children(): #部品(ボタンなど)を全部消す
        widget.destroy()

今回のアプリでは画面を切り替えることが多いため、画面の部品(ボタンなど)を全部消す関数を作成しました。

def show_list_screen():
    
    #画面リセット
    clear_screen()

    #画面をつくる
    frame = ctk.CTkFrame(app, fg_color='#F8FAF8')
    frame.pack(expand=True, fill='both')

このように画面を切り替えるごとにclear_screen()を呼んで画面をリセットし、新たにまた画面を作っています。


2.CSVファイルの読み込み

#ファイルを読み込む
def load_data():
    try:
        with open(CSV_FILE, 'r', encoding='utf-8') as file: #csvファイルを読み込む
            reader = csv.reader(file)
            data=list(reader) #情報をdataにリストで保存
            return data[1:]  #一番上の見出しを除くdataを返す
    except FileNotFoundError: #ファイルがない場合からのリストを返す
        return []

これは、CSVファイルを読み込む関数で、ポイントはreturn data[1:]としたところです。CSVファイルでは一番上の行は見出しであるため、データを読み込むときに患者データだけを読み込むよう1行目からの値を返すようにしました。(↓実際のCSVファイル)
スクリーンショット 2026-07-07 20.55.32.png


3.空欄の際にtextを返す

if patient_name == '':
        error_label.configure(text='患者名を入力してください') #'患者名'が空欄の場合textを返す
        return
#患者名無記入だったら表示
    error_label = ctk.CTkLabel(
        card,
        text='', #save_data()のconfigureで表示
        text_color='red'
    )
    error_label.pack(pady=5)
if data == []: #データがない場合
        no_data_label = ctk.CTkLabel(frame, text='まだデータがありません')
        no_data_label.pack(pady=20)
        return

データが空欄のままボタンを押したときにtextが表示されるようコードを書きました。
(↓実際の画面)



4. 見出しをつくる
#表を作る
    #見出しリスト
    headers = ['患者名', '測定頻度', '1W日付', '1W' ,'2W日付','2W', '3W日付','3W','4W日付', '4W', 'メモ','更新', '削除', 'グラフ']

    #表の土台をつくる
    table_frame = ctk.CTkFrame(frame, fg_color='white')
    table_frame.pack(pady=10)

    #見出しをつくる
    for col, header in enumerate(headers):#見出しに番号をつけながら取り出す
        label = ctk.CTkLabel(
            table_frame,
            text=header,
            width=85,
            font=('Arial', 12, 'bold'),
            fg_color='#CDEFFF',
            text_color='black',
        )
        label.grid(row=0, column=col, padx=2, pady=2)#1番上に左から順に表示する

書いてて混乱しましたが、headerに番号をつけながら1つずつ取り出し、row=0(つまり一番上の行)に順番に並ぶようにしています。


5.入力欄をつくり順番にデータを入れる

 # dataから1行ずつ取り出し、1から番号を付けながら処理する
 for row_index, row in enumerate(data, start=1):
        #常に11列にする
        while len(row)<11:
            row.append('')

# 11個の入力欄を作り、順番にデータを表示する
        for col_index in range(11):
            entry = ctk.CTkEntry(
                table_frame,
                width=70,
                height=30,
                text_color='black',
                fg_color='white',
                justify='center'
                    )
            entry.insert(0, row[col_index])#entryに取り出したデータを入れていく
            entry.grid(row=row_index, column=col_index , padx=2, pady=2)#entryのデータを表に表示

スクリーンショット 2026-07-07 20.55.32.png
 外側のfor文ではデータを1行ずつ番号をつけながら取り出しています。
内側のfor文では入力欄を11個作成しているのと、入力欄に外側のfor文で取得したデータを入れています。
while文では常に11列にデータが揃うようにしています。CSVによって列数が足りないことがあるため、11列になるまで空文字を追加しています。

苦労したポイント

まず最初に、コードがまったく思い浮かばないことに苦労しました。今まで殆どインプットしかしておらず、コードを自分で1から書くということをしていなかったため、非常に戸惑いました。詰まった部分ではChatGPTに相談しながら実装を進めました。その過程でコードの意味を理解することの重要性を実感しました。また、作っている途中に追加で実装したいものが出来たときにコードを修正するのがとても大変でした。自分が書いたコードを十分理解してないと書き直すのが難しいということを学びました。

まとめ(学んだこと)

 今回、初めてのアウトプットで病院で使う体重管理アプリを作成してみました。実際に使えるようになるにはまだまだ実装したいものがあるため、まだ使えませんが講義で習ったPythonの基礎や関数などをたくさん使ったため学びになったと思います。今後は、講義で習ったスクレイピングやpandas、excelの自動操作などを使用したツールを作ってみたいと思っています。


最後まで読んでいただき、ありがとうございました!

ソースコード全文

details
import customtkinter as ctk
from datetime import date
import csv
import os
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
plt.rcParams["font.family"] = "Hiragino Sans"
#=================================================================================================================

# アプリ設定
ctk.set_appearance_mode('Light')
app = ctk.CTk()
app.title('🩺 体重ノート')
app.geometry('1600x900')
app.configure(fg_color='#F8FAF8')


#csvファイル
CSV_FILE = 'weight.csv'


#画面の初期化
def clear_screen():
    for widget in app.winfo_children(): #部品(ボタンなど)を全部消す
        widget.destroy()


#ファイルを読み込む
def load_data():
    try:
        with open(CSV_FILE, 'r', encoding='utf-8') as file: #csvファイルを読み込む
            reader = csv.reader(file)
            data=list(reader) #情報をdataにリストで保存
            return data[1:]  #一番上の見出しを除くdataを返す
    except FileNotFoundError: #ファイルがない場合からのリストを返す
        return []


#保存する
def save_data():
    patient_name = name_entry.get() #'患者名'を取り出す
    check = check_menu.get() #'測定頻度'を取り出す

    if patient_name == '':
        error_label.configure(text='患者名を入力してください') #'患者名'が空欄の場合textを返す
        return
    
    file_exists = os.path.exists(CSV_FILE) #OSにファイルがあるか調べる

    with open(CSV_FILE, 'a', newline='', encoding='utf-8') as file: #csvファイルに書き足す
        writer = csv.writer(file)

        if not file_exists: #ファイルがなかったら(=False)
            writer.writerow(['患者名', '測定頻度','1W日付', '1W' ,'2W日付','2W', '3W日付','3W','4W日付', '4W', 'メモ']) #見出しのみ書く

        writer.writerow([ patient_name, check,'', '', '', '','','','','','']) #インプットした情報を書き足す

    show_success_screen()


#保存完了画面
def show_success_screen():
    
    #画面をリセット
    clear_screen()
    
    #画面を作る
    frame = ctk.CTkFrame(app, fg_color='#F8FAF8')
    frame.pack(expand=True, fill='both')

    label = ctk.CTkLabel(
        frame,
        text='保存しました!',
        font=('Arial', 26, 'bold')
    )
    label.pack(pady=100)

    ok_button = ctk.CTkButton(
        frame,
        text='OK',
        command=show_input_screen #初期画面に戻る
    )
    ok_button.pack(pady=20)



#入力画面(初期画面)
def show_input_screen():
    global name_entry, check_menu, error_label #これらの関数を他でも使えるようにする

    #画面をリセット
    clear_screen()

    #画面を作る
    frame = ctk.CTkFrame(app, fg_color='#F7F7F5')
    frame.pack(expand=True, fill='both')

    card=ctk.CTkFrame(
        frame,
        width=650,
        height=600,
        corner_radius=20,
        fg_color="white"
    )
    card.pack(expand=True)
    card.pack_propagate(False)

    #タイトル
    title_label = ctk.CTkLabel(
        card,
        text='🩺 体重ノート',
        font=('Arial', 32, 'bold')
    )
    title_label.pack(pady=(20, 5))

    #線
    line=ctk.CTkFrame(
        card,
        height=2,
        fg_color='#BFC9CA'
    )
    line.pack(fill='x',padx=30,pady=10)

    #サブタイトル
    sub_title=ctk.CTkLabel(
        card,
        text='新規患者登録',
        font=('Arial',24,'bold')

    )
    sub_title.pack(pady=(20,30))

    #患者名入力欄をつくる
    name_label = ctk.CTkLabel(card, text='患者名',font=('Arial',14))
    name_label.pack(pady=(5, 5))

    name_entry = ctk.CTkEntry(card, width=300)
    name_entry.pack()

    #測定頻度セレクトボタンをつくる
    check_label = ctk.CTkLabel(card, text='測定頻度',font=('Arial',14))
    check_label.pack(pady=(15, 5))

    check_menu = ctk.CTkOptionMenu(
        card,
        values=['1週間', '2週間', '1ヶ月']
    )
    check_menu.pack()

    #患者名無記入だったら表示
    error_label = ctk.CTkLabel(
        card,
        text='', #save_data()のconfigureで表示
        text_color='red'
    )
    error_label.pack(pady=5)

    #保存ボタン
    save_button = ctk.CTkButton(
        card,
        text='💾保存する',
        width=220,
        height=45,
        fg_color='#7Bc67B',
        hover_color='#68B568',
        font=('Arial',18,'bold'),
        command=save_data
    )
    save_button.pack(pady=(25, 10))

    #一覧ボタン
    list_button = ctk.CTkButton(
        card,
        text='📖一覧を見る',
        width=220,
        height=45,
        font=('Arial',18,'bold'),
        command=show_list_screen
    )
    list_button.pack(pady=10)

    #info文表示
    info=ctk.CTkLabel(
        card,
        text='患者を登録してください\n\n登録後は一覧画面から\n体重・日付を入力できます',
        font=('Arial',13),
        text_color='gray'
    )
    info.pack(pady=(20,20))


#一覧表示
def show_list_screen():
    
    #画面リセット
    clear_screen()

    #画面をつくる
    frame = ctk.CTkFrame(app, fg_color='#F8FAF8')
    frame.pack(expand=True, fill='both')

    #タイトル
    title_label = ctk.CTkLabel(
        frame,
        text='患者一覧',
        font=('Arial', 26, 'bold')
    )
    title_label.pack(pady=20)

    #戻るボタン
    back_button = ctk.CTkButton(
        frame,
        text='戻る',
        command=show_input_screen
    )
    back_button.pack(pady=10)

    #ファイルを読み込む
    data = load_data()

    if data == []: #データがない場合
        no_data_label = ctk.CTkLabel(frame, text='まだデータがありません')
        no_data_label.pack(pady=20)
        return

 #表を作る
    #見出しリスト
    headers = ['患者名', '測定頻度', '1W日付', '1W' ,'2W日付','2W', '3W日付','3W','4W日付', '4W', 'メモ','更新', '削除', 'グラフ']

    #表の土台をつくる
    table_frame = ctk.CTkFrame(frame, fg_color='white')
    table_frame.pack(pady=10)

    #見出しをつくる
    for col, header in enumerate(headers):#見出しに番号をつけながら取り出す
        label = ctk.CTkLabel(
            table_frame,
            text=header,
            width=85,
            font=('Arial', 12, 'bold'),
            fg_color='#CDEFFF',
            text_color='black',
        )
        label.grid(row=0, column=col, padx=2, pady=2)#1番上に左から順に表示する

    # dataから1行ずつ取り出し、1から番号を付けながら処理する
    for row_index, row in enumerate(data, start=1):
        #常に11列にする
        while len(row)<11:
            row.append('')

        #入力欄のリストをつくる
        entries = []

        # 11個の入力欄を作り、順番にデータを表示する
        for col_index in range(11):
            entry = ctk.CTkEntry(
                table_frame,
                width=70,
                height=30,
                text_color='black',
                fg_color='white',
                justify='center'
                    )
            entry.insert(0, row[col_index])#entryに取り出したデータを入れていく
            entry.grid(row=row_index, column=col_index , padx=2, pady=2)#entryのデータを表に表示
            entries.append(entry)#入力欄をリストに入れる
            
        #更新ボタンをつくる
        save_button = ctk.CTkButton(
            table_frame,
            text='更新',
            width=50,      # ↓start:1で始まっているため0に戻す
            command=lambda i=row_index -1, e=entries: update_row_data(i, e)
                        )
        save_button.grid(row=row_index, column=11, padx=2, pady=2)

        #削除ボタンをつくる
        delete_button = ctk.CTkButton(
            table_frame,
            text='削除',
            width=50,
            fg_color='#D96C6C',
            hover_color='#C85A5A',
            command=lambda i=row_index - 1: delete_data(i)
                )
        delete_button.grid(row=row_index, column=12, padx=2, pady=2)

        #グラフボタンをつくる
        graph_button = ctk.CTkButton(
            table_frame,
            text='グラフ',
            width=55,
            command=lambda i=row_index - 1: show_graph_screen(i)
            )
        graph_button.grid(row=row_index, column=13, padx=2, pady=2)


#データを更新する
def update_row_data(index, entries):
 
    #ファイルを読み込む
    data = load_data()

    # 入力欄の値を順番に取得して、CSVデータに反映する
    for i in range(11):
        data[index][i]=entries[i].get() #入力欄に入ってる値をdataに入れる

    with open(CSV_FILE, 'w', newline='', encoding='utf-8') as file:#csvデータを書き直す
        writer = csv.writer(file)
        writer.writerow(['患者名', '測定頻度', '1W日付', '1W' ,'2W日付','2W', '3W日付','3W','4W日付', '4W', 'メモ'])#見出しを書く

        writer.writerows(data)#データを書く

    show_list_screen()


#データを削除する
def delete_data(index):
    
    #ファイルを読み込む
    data = load_data()

    del data[index]#データを削除する

    with open(CSV_FILE, 'w', newline='', encoding='utf-8') as file:#ファイルを書き直す
        writer = csv.writer(file)

        writer.writerow([
            '患者名','測定頻度','1W日付','1W','2W日付','2W','3W日付','3W','4W日付','4W','メモ'])#見出しを書く

        writer.writerows(data)#残りのデータを書く

    show_list_screen()

#グラフを作成する
def show_graph_screen(index):

    #画面をリセットする
    clear_screen()

    #ファイルを読み込む
    data = load_data()

    #データから1行とりだす
    row = data[index]

    #それぞれがどこの列に当てはまるか
    patient_name = row[0]
    dates = [row[2], row[4], row[6], row[8]]
    weights = [row[3], row[5], row[7], row[9]]

    #日付と体重を入れるリスト(空)をつくる
    graph_dates = []
    graph_weights = []

    #体重と日付をセットにする
    for d, w in zip(dates, weights):
        if d != '' and w != '': #日付を体重が空欄じゃない時
            try:
                graph_weights.append(float(w))#float型で体重を追加する
                graph_dates.append(d)#日付を追加する
            except ValueError:
                pass #エラーを無視して進める

    #グラフの土台をつくる
    frame = ctk.CTkFrame(app, fg_color='#F8FAF8')
    frame.pack(expand=True, fill='both')

    #タイトル
    title_label = ctk.CTkLabel(
        frame,
        text=f'{patient_name}さんの体重推移',
        font=('Arial', 26, 'bold')
    )
    title_label.pack(pady=20)

    #体重が空欄の場合
    if graph_weights == []:
        no_data_label = ctk.CTkLabel(
            frame,
            text='グラフにできる体重データがありません',
            font=('Arial', 16)
        )
        no_data_label.pack(pady=40)
    
    #グラフを書く
    else:
        fig, ax = plt.subplots(figsize=(10, 6))
        ax.plot(graph_dates, graph_weights, marker='o')

        ax.set_title(f'{patient_name}さんの体重推移', fontsize=18)
        ax.set_xlabel('測定日', fontsize=14)
        ax.set_ylabel('体重(kg)', fontsize=14)
        ax.grid(True)

        for x, y in zip(graph_dates, graph_weights):
            ax.text(
            x,
            y + 0.1,#少し上に表示
            f'{y}kg',
            ha='center',
            fontsize=10
        )
        #グラフを表示する
        canvas = FigureCanvasTkAgg(fig, master=frame)#グラフをtkinterで表示できるようにする
        canvas.draw()#グラフを描く
        canvas.get_tk_widget().pack(pady=20)#グラフを画面に配置する

    back_button = ctk.CTkButton(
        frame,
        text='一覧に戻る',
        command=show_list_screen
    )
    back_button.pack(pady=20)


# 起動
show_input_screen()
app.mainloop()
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?