コンテンツにスキップ

pywebviewでマイクロブログを作る

[質問]

pywebviewに興味があります。そのサンプルとして、個人で使うメモというかmicroblogを作ってください。画面の上部にフォームがあり、本文を入れて投稿すると記事が追加されます。画面の下部に全記事がタイムラインとして表示されます。単語検索で見つかった記事のみ表示するフィルタ機能と、記事ごとに削除できるボタンも付けてください。

[回答] Grok

pywebview のサンプルとして、個人用マイクロブログ(シンプルメモアプリ)を作りました!

このアプリは:

  • 上部に投稿フォーム(本文を入力して「投稿する」ボタン)
  • 下部にタイムライン(最新順で表示)
  • 検索欄で単語検索 → リアルタイムで該当記事のみフィルタ
  • 各記事に「削除」ボタン

を搭載しています。
データは自動で microblog_posts.json に保存されるので、次回起動時も消えません。

実行方法

  1. pip install pywebview(初回のみ)
  2. 下記のコードを microblog.py として保存
  3. python microblog.py で起動
import webview
import json
import os
from datetime import datetime

class Api:
    def __init__(self):
        self.data_file = 'microblog_posts.json'
        self.posts = self._load_posts()

    def _load_posts(self):
        if os.path.exists(self.data_file):
            try:
                with open(self.data_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception:
                return []
        return []

    def _save_posts(self):
        with open(self.data_file, 'w', encoding='utf-8') as f:
            json.dump(self.posts, f, ensure_ascii=False, indent=2)

    def add_post(self, content):
        content = content.strip()
        if not content:
            return None
        post_id = max((p['id'] for p in self.posts), default=0) + 1
        post = {
            'id': post_id,
            'content': content,
            'timestamp': datetime.now().isoformat()
        }
        self.posts.append(post)
        self._save_posts()
        return post

    def get_posts(self, search_term=''):
        filtered = self.posts
        if search_term and search_term.strip():
            term = search_term.strip().lower()
            filtered = [p for p in self.posts if term in p['content'].lower()]
        # 新しい順にソート
        filtered.sort(key=lambda p: p['timestamp'], reverse=True)
        return filtered

    def delete_post(self, post_id):
        self.posts = [p for p in self.posts if p['id'] != post_id]
        self._save_posts()
        return True


if __name__ == '__main__':
    api = Api()

    html = """
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>個人マイクロブログ</title>
        <style>
            body {
                font-family: system-ui, -apple-system, sans-serif;
                margin: 0;
                padding: 20px;
                background: #f5f5f5;
            }
            .container {
                max-width: 800px;
                margin: auto;
                background: white;
                border-radius: 12px;
                box-shadow: 0 4px 20px rgba(0,0,0,0.1);
                overflow: hidden;
            }
            header {
                background: #007bff;
                color: white;
                padding: 20px;
                text-align: center;
            }
            .form-area {
                padding: 20px;
                border-bottom: 1px solid #eee;
            }
            textarea {
                width: 100%;
                height: 120px;
                padding: 12px;
                border: 2px solid #ddd;
                border-radius: 8px;
                font-size: 16px;
                box-sizing: border-box;
                resize: vertical;
            }
            button {
                margin-top: 10px;
                padding: 12px 24px;
                background: #007bff;
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 16px;
                cursor: pointer;
            }
            button:hover { background: #0056b3; }
            .search-area {
                padding: 15px 20px;
                background: #f8f9fa;
            }
            #search {
                width: 100%;
                padding: 12px;
                font-size: 16px;
                border: 2px solid #ddd;
                border-radius: 8px;
                box-sizing: border-box;
            }
            #timeline {
                padding: 20px;
            }
            .post {
                padding: 15px;
                margin-bottom: 15px;
                border: 1px solid #eee;
                border-radius: 8px;
                background: #fafafa;
                position: relative;
            }
            .post p {
                margin: 0 0 10px 0;
                white-space: pre-wrap;
                font-size: 16px;
                line-height: 1.5;
            }
            .meta {
                font-size: 14px;
                color: #666;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .delete-btn {
                background: #dc3545;
                color: white;
                padding: 6px 12px;
                border: none;
                border-radius: 6px;
                cursor: pointer;
                font-size: 14px;
            }
            .delete-btn:hover { background: #c82333; }
            .empty {
                text-align: center;
                color: #999;
                padding: 40px 20px;
                font-size: 18px;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <header>
                <h1>📝 個人マイクロブログ</h1>
            </header>

            <div class="form-area">
                <textarea id="content" placeholder="今日の出来事、アイデア、メモを自由に書いてください..."></textarea>
                <button onclick="postNew()">投稿する</button>
            </div>

            <div class="search-area">
                <input type="text" id="search" placeholder="単語で検索(リアルタイム)" oninput="filterPosts()">
            </div>

            <div id="timeline"></div>
        </div>

        <script>
            let currentFilter = '';

            async function postNew() {
                const textarea = document.getElementById('content');
                const content = textarea.value.trim();
                if (!content) {
                    alert('内容を入力してください');
                    return;
                }
                try {
                    await pywebview.api.add_post(content);
                    textarea.value = '';
                    refreshTimeline();
                } catch (e) {
                    console.error(e);
                    alert('投稿に失敗しました');
                }
            }

            async function deletePost(id) {
                if (!confirm('この記事を削除しますか?')) return;
                try {
                    await pywebview.api.delete_post(id);
                    refreshTimeline();
                } catch (e) {
                    console.error(e);
                }
            }

            function filterPosts() {
                currentFilter = document.getElementById('search').value;
                refreshTimeline();
            }

            async function refreshTimeline() {
                try {
                    const posts = await pywebview.api.get_posts(currentFilter);
                    renderTimeline(posts);
                } catch (e) {
                    console.error(e);
                }
            }

            function renderTimeline(posts) {
                const timeline = document.getElementById('timeline');
                if (posts.length === 0) {
                    timeline.innerHTML = '<div class="empty">記事がありません</div>';
                    return;
                }
                let html = '';
                for (let post of posts) {
                    const time = new Date(post.timestamp).toLocaleString('ja-JP', {
                        year: 'numeric',
                        month: '2-digit',
                        day: '2-digit',
                        hour: '2-digit',
                        minute: '2-digit'
                    });
                    html += `
                        <div class="post">
                            <p>${post.content.replace(/\\n/g, '<br>')}</p>
                            <div class="meta">
                                <span>${time}</span>
                                <button class="delete-btn" onclick="deletePost(${post.id})">削除</button>
                            </div>
                        </div>
                    `;
                }
                timeline.innerHTML = html;
            }

            // 起動時に読み込み
            window.onload = refreshTimeline;
        </script>
    </body>
    </html>
    """

    window = webview.create_window(
        title='個人マイクロブログ',
        html=html,
        js_api=api,
        width=900,
        height=700,
        resizable=True,
        min_size=(600, 500)
    )
    webview.start()

これで動きます!
見た目や機能をカスタマイズしたい場合(例:ダークモード追加、編集機能、タグなど)は気軽に教えてください。すぐに拡張版をお作りします♪

楽しんでください!