はじめに
こんな経験、ありませんか?
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)を軸にした疎結合なシグナル/スロット設計にしています。MainWindow が ProcessRunner を QMap<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" "$@"
使い方
基本的な流れ
-
run.shを実行してランチャーを起動 -
.pyファイルをウィンドウにドラッグ&ドロップ(または「+ 新規アプリ追加」) - ダイアログで名前・venv・引数などを確認 → OK
- カード上の ▶ 起動 ボタンで実行
サンプルアプリ
初回起動時に「サンプルアプリ登録」ボタンからすぐ試せます。
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 は大歓迎です!