11-Flask

Flask(Part.18)| 【画像のアップロードと利用(実装編)】

python| まとめ | 現役エンジニア&プログラミングスクール講師「python」のまとめページです。pythonに関して抑えておきたい知識や文法やにについて記事をまとめています。まとめページの下部には「おすすめの学習書籍」「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中...

目標

  • 画像のアップロードを行うプログラムを実装できる。
  • アップロードした画像を利用するプログラムを実装できる。

Flaskで画像を利用する方法

アプリケーションのプログラム追加と修正

商品モデルクラスの修正(models.py)

models.pyファイルの商品モデルクラスに画像のURLを保存するカラム用の属性を追加します。

全体のプログラムは記事の最後に掲載しています。

# 商品モデル(テーブル)の定義
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Integer, nullable=False)
    category_id = db.Column(
        db.Integer, db.ForeignKey("category.id"), nullable=True
    )  # カテゴリID
    image_url = db.Column(db.String(255), nullable=True)  # 画像のURLを保存するカラム

    def __repr__(self):
        return f"<Product {self.id} - {self.name} - {self.price}>"

商品フォームクラスの修正(forms.py)

forms.pyファイルで、FileFieldとFileAllowedが利用できるように追加のライブラリをインポートします。

from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed

forms.pyファイルの商品追加フォームクラスで商品画像用の項目を追記します。

class AddProductForm(FlaskForm):
    product_name = StringField("商品名", validators=[DataRequired()])
    product_price = IntegerField(
        "価格",
        validators=[
            DataRequired(),
            NumberRange(min=1, message="価格は1以上である必要があります"),
        ],
    )
    category = SelectField("カテゴリー", coerce=int)  # カテゴリー選択フィールド
    product_image = FileField(
        "商品画像",
        validators=[
            FileAllowed(
                ["jpg", "png", "jpeg", "gif"], "画像ファイルのみ許可されています"
            )
        ],
    )

app.pyの修正

app.pyの編集後の内容です。必要なライブラリの追加インポートと、商品の追加・編集のルートに画像用の項目を追記します。

import os
from itertools import groupby

from flask import Flask, flash, redirect, render_template, request, url_for
from werkzeug.utils import secure_filename

from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product, db  # models.pyからインポート

次は画像ファイルのアップロード用のフォルダの準備を行っています。この記述はimport文のすぐ後、アプリケーションの初期化「app = Flask(__name__)」の前に記述します。

# パスの設定
UPLOAD_FOLDER = os.path.join(os.getcwd(), "apps", "productsapp", "static", "images")
# ディレクトリが存在しない場合に作成
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

画像の形式やサイズの設定を行います。。

# アップロードの設定
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER  # 画像保存先ディレクトリ
app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif"}  # 許可する画像拡張子
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 最大アップロードサイズ(16MB)

アップロードされたファイルが許可される拡張子のものかをチェックする関数を定義します。この記述は全てのルートよりも前に記述します。

# アップロードが許可されている拡張子かをチェック
def allowed_file(filename):
    return (
        "." in filename
        and filename.rsplit(".", 1)[ 1 ].lower() in app.config["ALLOWED_EXTENSIONS"]
    )

ルート:商品追加ページの編集

# ルート: 商品追加ページ
@app.route("/product/add", methods=["GET", "POST"])
def add_product():
    form = AddProductForm()
    form.set_category_choices()

    if form.validate_on_submit():
        # フォームのデータを取得
        name = form.product_name.data
        price = form.product_price.data
        category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files.get("product_image")
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)  # ファイル名を安全に処理
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
                image.save(image_path)  # 画像を保存

                # 画像のパスをデータベースに保存
                image_url = f"images/{filename}"  # アップロードされた画像のURL
            else:
                image_url = "images/noimage.png"
        else:
            image_url = "images/noimage.png"

        # 商品の追加
        new_product = Product(
            name=name, price=price, category_id=category_id, image_url=image_url
        )
        db.session.add(new_product)
        db.session.commit()

        return redirect(url_for("product_list"))

    return render_template("product_form.html", form=form, title="商品追加フォーム")

ルート:商品編集ページの編集

# ルート: 商品を編集
@app.route("/product/edit/<int:product_id>", methods=["GET", "POST"])
def edit_product(product_id):
    product = Product.query.get_or_404(product_id)
    form = AddProductForm(obj=product)
    form.set_category_choices()

    if form.validate_on_submit():
        product.name = form.product_name.data
        product.price = form.product_price.data
        product.category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files["product_image"]
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)

                # もしすでに画像があれば削除
                if product.image_url and product.image_url != "images/noimage.png":
                    old_image_path = os.path.join(
                        app.config["UPLOAD_FOLDER"], os.path.basename(product.image_url)
                    )
                    if os.path.exists(old_image_path):
                        os.remove(old_image_path)

                # 新しい画像を保存
                image.save(image_path)
                product.image_url = f"images/{filename}"  # 新しい画像URLを保存

        db.session.commit()
        flash("商品情報を更新しました", "success")
        return redirect(url_for("product_list"))

    return render_template(
        "product_form.html", form=form, title="商品編集", product=product
    )

product_form.htmlの編集

formタブのリクエストに「add_product」もしくは「edit_product」が選択できるようにします。このようにすると、追加の時には「add_product」、編集の時は「edit_product」が選択されるようになります。

<form method="POST" action="{{ url_for('edit_product', product_id=product.id) if product else url_for('add_product') }}"
    enctype="multipart/form-data">

product_form.htmlに次の項目を追加します。

<label for="product_image">商品画像</label>
<!-- 既存の画像がある場合は表示 -->
{% if product.image_url %}
    <div>
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="現在の画像" style="width: 100px; height: 100px;">
        <p>※新しい画像を選択すると上書きされます</p>
    </div>
{% endif %}
<label for="product_image" class="file-upload-label">画像を選択</label>
<input type="file" id="product_image" name="product_image" accept="image/*">

product_form.htmlのformの閉じタグの下にJavaScriptを追記します。このプログラムは選択したファイルを表示するプログラムです。

<script>
    document.getElementById("product_image").addEventListener("change", function () {
        const fileName = this.files.length > 0 ? this.files[0].name : "画像を選択";
        document.querySelector(".file-upload-label").textContent = fileName;
    });
</script>

products.htmlの編集

products.htmlで商品の名前と価格を表示する部分よりも前に次の内容を入力します。

       <!-- 商品画像の表示 -->
        {% if product.image_url %}
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="{{ product.name }}"
            style="width: 50px; height: 50px; margin-right: 10px; border-right: 2px;">
        {% endif %}

スタイルシートの編集

画像を選択するボタンにスタイルを適用します。

/* ファイル選択ボタンを隠す */
input[type="file"] {
    display: none;
}

/* カスタムアップロードボタンのスタイル */
.file-upload-label {
    display: inline-block;
    padding: 10px 20px;
    background-color: #f2491f;
    color: white;
    text-align: center;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* ホバー時のスタイル */
.file-upload-label:hover {
    background-color: #45a049;
}

古いデータベースの削除とアプリケーションの稼働確認

instanceディレクトリの中に、products.dbファイルを削除してサーバーを再起動します。

サーバーを起動している場合はターミナルで「Ctrl + C」を押下し、サーバーを停止してからproducts.dbファイルを削除します。

instanceディレクトリの中に、products.dbファイルを右クリックして、「Delete」をクリックします。

ブラウザで http://127.0.0.1:5000/manage_productsにアクセスします。

商品管理ページが表示されます。

この時、instanceディレクトリに新しくproducts.dbが作成され、同時にapps/productsapp/staticディレクトリにimagesディレクトリが作成されます。

カテゴリー追加で「ソフトドリンク」「アルコール」を追加します。

「商品管理ページ」をクリックし、続けて「商品追加」をクリックします。

次の商品を追加します。

商品名「りんごジュース」価格「250」カテゴリー「ソフトドリンク」画像「noimage.png」

続けて、次のふたつを追加します。

商品名「オレンジジュース」価格「250」カテゴリー「ソフトドリンク」画像は選択無し

商品名「グレープジュース」価格「250」カテゴリー「ソフトドリンク」画像は選択無し

画像が「noimage.png」で表示されます。続けて、リンゴジュースの画像を変更します。

りんごジュースの編集ボタンをクリックして、商品編集ページから、商品名「りんごジュース」価格「250」カテゴリー「ソフトドリンク」画像「apple_juice.png」で商品を登録しなおします。

りんごジュースの画像が変更されます。

今回は以上になります。

models.py
from flask_sqlalchemy import SQLAlchemy

# SQLAlchemyインスタンスを共通で使えるように
db = SQLAlchemy()


# カテゴリーモデル
class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False, unique=True)
    products = db.relationship(
        "Product", backref="category", lazy=True
    )  # 商品とのリレーション


# 商品モデル(テーブル)の定義
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Integer, nullable=False)
    category_id = db.Column(
        db.Integer, db.ForeignKey("category.id"), nullable=True
    )  # カテゴリID
    image_url = db.Column(db.String(255), nullable=True)  # 画像のURLを保存するカラム

    def __repr__(self):
        return f"<Product {self.id} - {self.name} - {self.price}>"
forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed
from wtforms import FileField, IntegerField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired, NumberRange

from apps.productsapp.models import Category


class CategoryForm(FlaskForm):
    name = StringField("カテゴリー名", validators=[DataRequired()])
    submit = SubmitField("カテゴリーを追加")


class AddProductForm(FlaskForm):
    product_name = StringField("商品名", validators=[DataRequired()])
    product_price = IntegerField(
        "価格",
        validators=[
            DataRequired(),
            NumberRange(min=1, message="価格は1以上である必要があります"),
        ],
    )
    category = SelectField("カテゴリー", coerce=int)  # カテゴリー選択フィールド
    product_image = FileField(
        "商品画像",
        validators=[
            FileAllowed(
                ["jpg", "png", "jpeg", "gif"], "画像ファイルのみ許可されています"
            )
        ],
    )

    def set_category_choices(self):
        self.category.choices = [
            (c.id, c.name) for c in Category.query.all()
        ]  # DBから取得
app.py
import os
from itertools import groupby

from flask import Flask, flash, redirect, render_template, request, url_for
from werkzeug.utils import secure_filename

from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product, db  # models.pyからインポート

UPLOAD_FOLDER = os.path.join(os.getcwd(), "apps", "productsapp", "static", "images")
# ディレクトリが存在しない場合に作成
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)

# アプリケーションの初期化
app = Flask(__name__)
app.secret_key = "your_secret_key"

# アップロードの設定
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER  # 画像保存先ディレクトリ
app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif"}  # 許可する画像拡張子
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 最大アップロードサイズ(16MB)

# SQLiteの設定
app.config["SQLALCHEMY_DATABASE_URI"] = (
    "sqlite:///products.db"  # SQLiteデータベースファイル
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False  # 不要な変更追跡を無効化

# SQLAlchemyのインスタンスを初期化
db.init_app(app)


# アプリケーションコンテキスト内でテーブル作成
with app.app_context():
    db.create_all()


# アップロードが許可されている拡張子かをチェック
def allowed_file(filename):
    return (
        "." in filename
        and filename.rsplit(".", 1).lower() in app.config["ALLOWED_EXTENSIONS"]
    )


# ルート: トップページ
@app.route("/")
def index():
    return "Top page"


# カテゴリー一覧を表示
@app.route("/categories")
def category_list():
    categories = Category.query.all()
    return render_template(
        "categories.html", categories=categories, title="カテゴリー一覧"
    )


# カテゴリーを追加
@app.route("/category/add", methods=["GET", "POST"])
def add_category():
    form = CategoryForm()
    if form.validate_on_submit():
        new_category = Category(name=form.name.data)
        db.session.add(new_category)
        db.session.commit()
        flash("カテゴリーを追加しました", "success")
        return redirect(url_for("category_list"))
    return render_template(
        "category_form.html", form=form, title="カテゴリー追加フォーム"
    )


# カテゴリーを編集
@app.route("/category/edit/<int:category_id>", methods=["GET", "POST"])
def edit_category(category_id):
    category = Category.query.get_or_404(category_id)
    form = CategoryForm(obj=category)

    if form.validate_on_submit():
        category.name = form.name.data
        db.session.commit()
        flash("カテゴリーを更新しました", "success")
        return redirect(url_for("category_list"))

    return render_template("category_form.html", form=form, title="カテゴリー編集")


# カテゴリーを削除
@app.route("/category/delete/<int:category_id>", methods=["POST"])
def delete_category(category_id):
    category = Category.query.get_or_404(category_id)

    # もしカテゴリーに紐づいた商品があればエラーを出す
    if category.products:
        flash("このカテゴリーには商品が登録されているため削除できません", "danger")
        return redirect(url_for("category_list"))

    db.session.delete(category)
    db.session.commit()
    flash("カテゴリーを削除しました", "success")
    return redirect(url_for("category_list"))


# ルート: 商品一覧ページ
@app.route("/products")
def product_list():
    products = Product.query.all()  # SQLiteから商品データを取得
    # 商品をカテゴリーごとにグループ化
    grouped_products = {}
    for key, group in groupby(
        sorted(products, key=lambda p: p.category.name if p.category else "なし"),
        key=lambda p: p.category.name if p.category else "なし",
    ):
        grouped_products[key] = list(group)

    return render_template(
        "products.html", grouped_products=grouped_products, title="商品一覧"
    )


# ルート: 商品追加ページ
@app.route("/product/add", methods=["GET", "POST"])
def add_product():
    form = AddProductForm()
    form.set_category_choices()

    if form.validate_on_submit():
        # フォームのデータを取得
        name = form.product_name.data
        price = form.product_price.data
        category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files.get("product_image")
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)  # ファイル名を安全に処理
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)
                image.save(image_path)  # 画像を保存

                # 画像のパスをデータベースに保存
                image_url = f"images/{filename}"  # アップロードされた画像のURL
            else:
                image_url = "images/noimage.png"
        else:
            image_url = "images/noimage.png"

        # 商品の追加
        new_product = Product(
            name=name, price=price, category_id=category_id, image_url=image_url
        )
        db.session.add(new_product)
        db.session.commit()

        return redirect(url_for("product_list"))

    return render_template("product_form.html", form=form, title="商品追加フォーム")


# ルート: 商品を編集
@app.route("/product/edit/<int:product_id>", methods=["GET", "POST"])
def edit_product(product_id):
    product = Product.query.get_or_404(product_id)
    form = AddProductForm(obj=product)
    form.set_category_choices()

    if form.validate_on_submit():
        product.name = form.product_name.data
        product.price = form.product_price.data
        product.category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files["product_image"]
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)
                image_path = os.path.join(app.config["UPLOAD_FOLDER"], filename)

                # もしすでに画像があれば削除
                if product.image_url and product.image_url != "images/noimage.png":
                    old_image_path = os.path.join(
                        app.config["UPLOAD_FOLDER"], os.path.basename(product.image_url)
                    )
                    if os.path.exists(old_image_path):
                        os.remove(old_image_path)

                # 新しい画像を保存
                image.save(image_path)
                product.image_url = f"images/{filename}"  # 新しい画像URLを保存

        db.session.commit()
        flash("商品情報を更新しました", "success")
        return redirect(url_for("product_list"))

    return render_template(
        "product_form.html", form=form, title="商品編集", product=product
    )


# ルート: 商品を削除
@app.route("/product/delete/<int:product_id>", methods=["POST"])
def delete_product(product_id):
    product = Product.query.get_or_404(product_id)
    db.session.delete(product)
    db.session.commit()
    flash("商品を削除しました", "success")
    return redirect(url_for("product_list"))


# ルート: 商品管理ページ
@app.route("/manage_products")
def manage_products():
    return render_template("manage_products.html", title="商品管理MENU")


if __name__ == "__main__":
    app.run(debug=True)
product_form.html
{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('product_list') }}" class="button-link">商品一覧に戻る</a></li>
</ul>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>

<form method="POST" action="{{ url_for('edit_product', product_id=product.id) if product else url_for('add_product') }}"
    enctype="multipart/form-data">

    {{ form.hidden_tag() }} <!-- CSRFトークンを含めるため -->

    <label for="product_name">商品名</label>
    {{ form.product_name() }} <!-- 商品名入力フィールド -->
    {% if form.product_name.errors %}
    <ul>
        {% for error in form.product_name.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="product_price">価格</label>
    {{ form.product_price() }} <!-- 価格入力フィールド -->
    {% if form.product_price.errors %}
    <ul>
        {% for error in form.product_price.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="category">カテゴリー</label>
    {{ form.category() }}
    {% if form.category.errors %}
    <ul>
        {% for error in form.category.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="product_image">商品画像</label>
    <!-- 既存の画像がある場合は表示 -->
    {% if product and product.image_url %}
    <div>
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="現在の画像" style="width: 100px; height: 100px;">
        <p>※新しい画像を選択すると上書きされます</p>
    </div>
    {% endif %}
    <label for="product_image" class="file-upload-label">画像を選択</label>
    <input type="file" id="product_image" name="product_image" accept="image/*">

    <input type="submit" value="商品を登録する">
</form>
<script>
    document.getElementById("product_image").addEventListener("change", function () {
        const fileName = this.files.length > 0 ? this.files[0].name : "画像を選択";
        document.querySelector(".file-upload-label").textContent = fileName;
    });
</script>

{% endblock %}
products.html
{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('add_product') }}" class="button-link">商品を追加する</a></li>
</ul>
{% endblock %}

{% block content %}
<h1>{{ title }}</h1>
{% for category, products in grouped_products.items() %}
<h2>{{ category }}</h2>
<ul class="product-list">
    {% for product in products %}
    <li class="product-item">
        <!-- 商品画像の表示 -->
        {% if product.image_url %}
        <img src="{{ url_for('static', filename=product.image_url) }}" alt="{{ product.name }}"
            style="width: 50px; height: 50px; margin-right: 10px; border-right: 2px;">
        {% endif %}

        <span class="product-name">{{ product.name }} : ¥{{ "{:,}".format(product.price) }}</span>

        <div class="button-group">
            <form action="{{ url_for('edit_product', product_id=product.id) }}" method="GET">
                <button type="submit" class="edit-button">編集</button>
            </form>
            <form action="{{ url_for('delete_product', product_id=product.id) }}" method="POST">
                <button type="submit" class="delete-button" onclick="return confirm('本当に削除しますか?')">削除</button>
            </form>
        </div>
    </li>
    {% endfor %}
</ul>
{% endfor %}
{% endblock %}
style.css
body {
    font-family: Arial, sans-serif;
    margin: 20px;
    padding: 20px;
    background-color: #f8f8f8;
}

header {
    margin: 0;
    padding: 10px 0;
}

main {
    margin: 0;
    padding: 20px;
}

footer {
    background-color: #aaa;
    color: white;
    text-align: center;
    padding: 10px 0;
    margin-top: 20px;
}

h1 {
    color: #333;
}

/* ボタン部分のリストを横並びにする */
header ul,
main ul {
    padding: 0;
}

header ul:first-of-type {
    display: flex;
    /* 横並び */
    justify-content: flex-start;
    /* 左寄せ */
}

header ul:first-of-type li {
    margin-right: 10px;
    /* ボタン間に余白 */
}

/* リストアイテムの基本スタイル */
header li {
    list-style-type: none;
    /* デフォルトのリストスタイルを削除 */
}

/* カテゴリーごとの商品リストのスタイル */
ul.product-list {
    list-style-type: none;
    padding: 0;
}

ul.product-list li {
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* ボタンリンクのスタイル */
.button-link {
    display: block;
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    text-decoration: none;
    border-radius: 5px;
    font-size: 16px;
    transition: background-color 0.3s ease;
}

/* ボタンにホバーしたときのスタイル */
.button-link:hover {
    background-color: #45a049;
}

/* 商品リストのスタイル */
h2 {
    margin-top: 30px;
    /* 商品リストの見出しに余白 */
}

ul:nth-of-type(2) {
    list-style-type: none;
    /* 商品リストのマーカーを削除 */
    padding: 0;
}

ul:nth-of-type(2) li {
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* フォーム全体のスタイル */
form {
    background-color: #fff;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    margin-top: 30px;
}

/* フォーム内のラベルのスタイル */
form label {
    font-size: 16px;
    color: #333;
    display: block;
    margin-bottom: 8px;
}

/* フォーム内の入力フィールドのスタイル */
form input[type="text"],
form input[type="number"],
form input[type="submit"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 16px;
    box-sizing: border-box;
    /* パディングを含めてサイズ調整 */
}

/* フォーム内のボタン(送信ボタン) */
form input[type="submit"] {
    background-color: #4CAF50;
    color: white;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* 送信ボタンにホバーしたときのスタイル */
form input[type="submit"]:hover,
form button:hover {
    background-color: #45a049;
}

/* ファイル選択ボタンを隠す */
input[type="file"] {
    display: none;
}

/* カスタムアップロードボタンのスタイル */
.file-upload-label {
    display: inline-block;
    padding: 10px 20px;
    background-color: #f2491f;
    color: white;
    text-align: center;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* ホバー時のスタイル */
.file-upload-label:hover {
    background-color: #45a049;
}

/* カテゴリーの選択ボックス(select) */
form select {
    width: 100%;
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 16px;
    box-sizing: border-box;
    transition: border-color 0.3s ease;
}

/* フォーカス時のスタイル */
form select:focus {
    border-color: #4CAF50;
    outline: none;
    box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
}


/* リストアイテムを横並びにする */
.category-item,
.product-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* カテゴリー名、商品名のスタイル */
.category-name,
.product-name {
    font-size: 16px;
    flex-grow: 1;
    /* 左側に余白を作り、ボタンを右寄せ */
}

/* ボタンを右側に配置 */
.button-group {
    display: flex;
    gap: 10px;
    /* ボタン間のスペース */
}

/* ボタンを囲む form のスタイルをリセット */
.button-group form {
    display: inline-block;
    margin: 0;
    padding: 0;
}

/* 編集・削除ボタンの共通スタイル */
.button-group button {
    display: inline-block;
    padding: 8px 16px;
    font-size: 14px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
    color: white;
    width: 80px;
    /* 幅を統一 */
    text-align: center;
}

/* 編集ボタンのスタイル */
.button-group .edit-button {
    background-color: #2196F3;
    /* ブルー */
}

.button-group .edit-button:hover {
    background-color: #1976D2;
    /* ホバー時の色 */
}

/* 削除ボタンのスタイル */
.button-group .delete-button {
    background-color: #F44336;
    /* レッド */
}

.button-group .delete-button:hover {
    background-color: #D32F2F;
    /* ホバー時の色 */
}
「python」おすすめ書籍 ベスト3 | 現役エンジニア&プログラミングスクール講師「python」の学習でお勧めしたい書籍をご紹介しています。お勧めする理由としては、考え方、イメージなどを適切に捉えていること、「生のpython」に焦点をあてて解説をしている書籍であることなどが理由です。勿論、この他にも良い書籍はありますが、特に質の高かったものを選んで記事にしています。ページの下部には「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中。...

ブックマークのすすめ

「ほわほわぶろぐ」を常に検索するのが面倒だという方はブックマークをお勧めします。ブックマークの設定は別記事にて掲載しています。

「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】について解説している記事です。削除方法も掲載しています。...
【パソコン選び】失敗しないための重要ポイント | 現役エンジニア&プログラミングスクール講師【パソコン選び】失敗しないための重要ポイントについての記事です。パソコンのタイプと購入時に検討すべき点・家電量販店で見かけるCPUの見方・購入者が必要とするメモリ容量・HDDとSSDについて・ディスプレイの種類・バッテリーの持ち時間や保証・Officeソフト・ウィルス対策ソフトについて書いています。...
RELATED POST
11-Flask

Flask(Part.28)| 【注文管理アプリケーションのプログラミング(4)l(3)の解説】

2025年3月21日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site
11-Flask

Flask(Part.42)| 【消費税額管理アプリケーションのプログラミング(1)管理者側】

2025年4月4日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site