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スクリプトを毎回CLIで起動する面倒を、Qt C++ 製 GUI ランチャーで解消した

1
Posted at

はじめに

こんな経験、ありませんか?

cd ~/projects/my_tool
source .venv/bin/activate
python main.py

毎回これを打つのが地味に面倒くさい。

特に「ターミナル操作に慣れていない友人や同僚に自分の Python ツールを使ってもらいたいのに、起動方法を教えるのが大変」という状況に何度も直面しました。

そこで Qt C++ で GUI ランチャーを自作しました。.py ファイルをドラッグ&ドロップするだけで登録でき、以降はボタン一つで起動できるアプリです。

作ったもの

Python App Launcher — Ubuntu 向け GUI Python スクリプト起動管理ツール

主な機能

機能 説明
ドラッグ&ドロップ登録 .py / .pyw をウィンドウに投げ込むだけ
venv 自動検出 スクリプト横の venv / .venv を自動認識
独立起動モード ランチャーを閉じても Python プロセスが生き続ける
リアルタイムログ stdout/stderr を色分けしてリアルタイム表示
Ubuntu アプリ登録 ワンクリックで .desktop ファイルを生成、Super キーから検索可能
設定の自動保存 登録内容を JSON で永続化、次回起動時に復元

なぜ Qt C++ で作ったのか

最初は Python + Tkinter や PyQt で作ることを考えましたが、以下の理由で Qt C++ を選びました。

  • Python ランタイム不要 — ランチャー自体が Python に依存していたら本末転倒
  • 軽量・高速 — ネイティブバイナリなのでインストール不要、起動が速い
  • ネイティブ UI — Linux デスクトップとの親和性が高い

実装の解説

1. アーキテクチャ概要

MainWindow
├── AppItem (登録アプリのデータモデル)
├── ProcessRunner (QProcess ラッパー)
├── LogViewer (リアルタイムログ表示)
├── AddAppDialog (アプリ追加ダイアログ)
└── StyleHelper (スタイルシート管理)

appId(UUID)を軸にした疎結合なシグナル/スロット設計にしています。MainWindowProcessRunnerQMap<QString, ProcessRunner*> で管理し、プロセスの状態変化は stateChanged シグナルで伝達されます。

2. venv 自動検出

// appitem.cpp
QString AppItem::autoDetectInterpreter(const QString& scriptPath) {
    if (!scriptPath.isEmpty()) {
        QFileInfo scriptInfo(scriptPath);
        QDir dir = scriptInfo.dir();

        // スクリプトと同じディレクトリの venv を探す
        QString venvPath = dir.filePath("venv/bin/python");
        if (QFileInfo::exists(venvPath)) return venvPath;

        QString dotVenvPath = dir.filePath(".venv/bin/python");
        if (QFileInfo::exists(dotVenvPath)) return dotVenvPath;

        // 親ディレクトリの venv も探す
        dir.cdUp();
        venvPath = dir.filePath("venv/bin/python");
        if (QFileInfo::exists(venvPath)) return venvPath;
        dotVenvPath = dir.filePath(".venv/bin/python");
        if (QFileInfo::exists(dotVenvPath)) return dotVenvPath;
    }

    // フォールバック: システム python3
    if (QFileInfo::exists("/usr/bin/python3")) return "/usr/bin/python3";
    return "python3";
}

スクリプトのあるディレクトリとその親ディレクトリを自動検索します。モノレポ構成など、一段上に venv があるケースにも対応しています。

3. 独立起動(デタッチ)モード

GUI アプリを「独立起動」すると、ランチャーを閉じてもプロセスが生き続けます。これが地味に重要な機能です。

// processrunner.cpp(一部抜粋)
QString shellCmd = QString(
    "export DISPLAY=\"%1\"; "
    "export WAYLAND_DISPLAY=\"%2\"; "
    "export XAUTHORITY=\"%3\"; "
    "export PYTHONUNBUFFERED=1; "
    "exec setsid nohup '%4' %5 > '%6' 2>&1 < /dev/null"
).arg(displayEnv, waylandEnv, xauthEnv, program, formattedArgs.join(" "), logPath);

qint64 pid = 0;
QProcess::startDetached("/bin/sh", {"-c", shellCmd}, workDir, &pid);

ポイントは3つ:

  • setsid — 新しいセッションを作成し、親プロセスの終了から切り離す
  • DISPLAY / WAYLAND_DISPLAY / XAUTHORITY の引き継ぎ — デタッチ後も GUI ウィンドウが表示できるよう環境変数を明示的にセット
  • PYTHONUNBUFFERED=1 — stdout をリアルタイムにログへ流すため

4. Ubuntu アプリ一覧への登録/解除

ボタン一つで ~/.local/share/applications/python-launcher.desktop を生成します。

QTextStream out(&file);
out << "[Desktop Entry]\n";
out << "Version=1.0\n";
out << "Type=Application\n";
out << "Name=Python Launcher\n";
out << "Exec=\"" << execPath << "\" %f\n";
out << "Terminal=false\n";
out << "Categories=Development;Utility;\n";

// 実行権限を付与
file.setPermissions(
    QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner |
    QFileDevice::ReadGroup | QFileDevice::ExeGroup |
    QFileDevice::ReadOther | QFileDevice::ExeOther
);

登録済みかどうかはファイルの存在で判定し、ボタンのラベルを「🖥️ アプリ一覧に登録」↔「🗑️ アプリ一覧から解除」と自動切り替えします。

5. リアルタイムログモニター

stdout / stderr を色分けして表示します。ログはアプリごとに QMap で管理し、「全アプリ一括表示」と「個別表示」を切り替えられます。

// logviewer.cpp
QString color = isError
    ? "#f92672"  // 赤: stderr
    : (text.startsWith("[システム]") ? "#66d9ef" : "#a6e22e");  // シアン: システムメッセージ、緑: stdout

6. スタイルの一元管理

QSS(Qt スタイルシート)を StyleHelper クラスに集約しています。

// stylehelper.h
class StyleHelper {
public:
    static QString getAppStyleSheet();
    static QString getCardStyleSheet(bool isRunning);
};

アプリカードの実行中/停止中の状態に応じてボーダーカラーを切り替えるなど、UI のフィードバックをスタイルで表現しています。


ビルド・インストール方法

必要パッケージ (Ubuntu/Debian)

sudo apt update
sudo apt install -y build-essential cmake qtbase5-dev

ビルド & 起動

git clone https://github.com/amekusa03/PythonLauncher.git
cd PythonLauncher
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
./PythonLauncher

または run.sh をダブルクリックするだけで、未ビルドの場合は自動ビルドから起動まで実行します。

#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "$DIR"
if [ ! -f "$DIR/build/PythonLauncher" ]; then
    echo "ビルド済みバイナリが見つかりません。ビルドを実行します..."
    mkdir -p build && cd build && cmake .. && make -j$(nproc) && cd ..
fi
exec "$DIR/build/PythonLauncher" "$@"

使い方

基本的な流れ

  1. run.sh を実行してランチャーを起動
  2. .py ファイルをウィンドウにドラッグ&ドロップ(または「+ 新規アプリ追加」)
  3. ダイアログで名前・venv・引数などを確認 → OK
  4. カード上の ▶ 起動 ボタンで実行

サンプルアプリ

初回起動時に「サンプルアプリ登録」ボタンからすぐ試せます。

Tkinter GUI サンプル:

#!/usr/bin/env python3
import tkinter as tk
from tkinter import messagebox

def main():
    root = tk.Tk()
    root.title("サンプル GUI Python アプリ")
    # ... 独立起動後もウィンドウが表示され続けることを確認できる
    root.mainloop()

if __name__ == "__main__":
    main()

バックグラウンド CLI サンプル:

#!/usr/bin/env python3
import time, sys

def main():
    count = 1
    while True:
        print(f"[{count}] バックグラウンド定期処理を実行中...", flush=True)
        if count % 5 == 0:
            print(f"[NOTICE] チェックポイント {count}", file=sys.stderr, flush=True)
        time.sleep(2)
        count += 1

if __name__ == "__main__":
    main()

まとめ

課題 解決方法
毎回 cd してコマンド打つのが面倒 ドラッグ&ドロップで登録、ボタン一つで起動
venv の有効化を忘れる スクリプト横の venv を自動検出
GUI アプリ起動後もターミナルを残しておく必要がある 独立起動モードで親プロセスから切り離し
ターミナルを開かないとログが見えない リアルタイム GUI ログモニター
非エンジニアに使ってもらいにくい Ubuntu アプリ一覧に登録できる

元 Windows ユーザーや、コマンドライン操作が苦手な方に Python ツールを配布する際に役立てていただければ幸いです。


フィードバックや PR は大歓迎です!

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?