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?

Ubuntuでデスクトップアプリ作成手順

Last updated at Posted at 2025-08-09

pythonで作り直し

windowsFormアプリケーションで作成した下記のDesktopマスコットをUbuntuで動くようにする為にPythonで作り直しました。

このプログラムはデスクトップをカービィがふわふわと自由に飛び回っているのを眺めて癒されるアプリの作成になります。

C#のベースになったFormのコード

using System;
using System.Drawing;
using System.Media;
using System.Windows.Forms;
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
using System.Xml.Linq;
using System.Threading.Tasks;
using System.Linq;
using System.IO;
using System.Text;
using System.Collections.Generic;

namespace DesktopMascot_Share_kirby {
    public partial class Form1 : Form {
        private Image[][] frames;
        private int currentFrame = 0;
        private int direction = 1; // 1=右, -1=左
        private int speedX = 2;
        private double waveCounter = 0;
        private int centerY;
        private int waveAmplitude = 20; // 浮き沈み幅を大きく
        private SoundPlayer clickSound;
        private ClipboardHistoryManager clipboardHistory;

        // ドラッグ用
        private bool mouseDown = false;
        private Point mouseOffset;


        public Form1() {
            InitializeComponent();
            this.FormBorderStyle = FormBorderStyle.None;
            this.TopMost = true;
            this.BackColor = Color.Magenta;
            this.TransparencyKey = Color.Magenta;
            this.StartPosition = FormStartPosition.Manual;

            // 初期位置
            this.Location = new Point(100, 300);
            centerY = this.Top;

            // フレーム画像(右 / 左)
            frames = new Image[][] {
                new Image[] {
                    Properties.Resources.base1,
                    Properties.Resources.base2,
                    Properties.Resources.base3
                },
                new Image[] {
                    Properties.Resources.base1_left,
                    Properties.Resources.base2_left,
                    Properties.Resources.base3_left
                }
            };

            // 音声
            clickSound = new SoundPlayer(Properties.Resources.kirby_1);

            // タイマー設定
            timer1.Interval = 100;
            timer1.Tick += timer1_Tick;
            timer1.Start();

            this.BackgroundImage = frames[0][0];
            this.BackgroundImageLayout = ImageLayout.Stretch;

            // イベント登録
            this.MouseClick += Form1_MouseClick;
            this.MouseDown += Form1_MouseDown;
            this.MouseMove += Form1_MouseMove;
            this.MouseUp += Form1_MouseUp;
            // ... 既存の初期化のあとに追加
            
            clipboardHistory = new ClipboardHistoryManager();

            this.contextMenuStrip1.Items.Add("ファイルのパスを作成", null, Get_File_Paths);
            this.contextMenuStrip1.Items.Add("コピー履歴を見る", null, ShowClipboardHistory);


            // 1週間以上過去のクリップボードデータを削除
            CleanupOldClipboardData();

            // 非同期でデータを取得
            _ = InitializeFirebaseDataAsync();
        }

        private async Task InitializeFirebaseDataAsync() {
            try {
                await LoadData();
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"Error in InitializeFirebaseDataAsync: {ex.Message}");
                System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}");
                MessageBox.Show($"データの初期化中にエラーが発生しました: {ex.Message}", "エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private async Task LoadData() {
            try {
                
                 // 設定の処理
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"Error loading data: {ex.Message}");
                MessageBox.Show($"データの読み込み中にエラーが発生しました: {ex.Message}", "エラー",
                    MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        // 1週間以上過去のクリップボードデータを削除
        private void CleanupOldClipboardData() {
            try {
                var database = new SQLite_Database();
                database.DeleteOldClipboardData();
                System.Diagnostics.Debug.WriteLine("古いクリップボードデータの削除が完了しました。");
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"古いクリップボードデータの削除中にエラーが発生しました: {ex.Message}");
                // エラーが発生してもアプリの起動は継続する
            }
        }

        private async void Get_File_Paths(object sender, EventArgs e) {
            using (FolderBrowserDialog folderDialog = new FolderBrowserDialog()) {
                folderDialog.Description = "ファイル一覧を作成するフォルダを選択してください";
                folderDialog.ShowNewFolderButton = true;

                if (folderDialog.ShowDialog() == DialogResult.OK) {
                    try {
                        string selectedPath = folderDialog.SelectedPath;
                        var fileList = new List<string>();

                        // ヘッダー情報を追加
                        fileList.Add($"フォルダ一覧: {selectedPath}");
                        fileList.Add($"作成日時: {DateTime.Now:yyyy/MM/dd HH:mm:ss}");
                        fileList.Add(new string('-', 80));
                        fileList.Add("");

                        // ディレクトリとファイルを再帰的に取得
                        await Task.Run(() => {
                            ProcessDirectory(selectedPath, fileList, 0);
                        });

                        // 保存ダイアログを表示
                        using (SaveFileDialog saveDialog = new SaveFileDialog()) {
                            saveDialog.Filter = "テキストファイル (*.txt)|*.txt";
                            saveDialog.Title = "ファイル一覧を保存";
                            saveDialog.FileName = $"ファイル一覧_{DateTime.Now:yyyyMMdd_HHmmss}.txt";
                            saveDialog.DefaultExt = "txt";
                            saveDialog.AddExtension = true;

                            if (saveDialog.ShowDialog() == DialogResult.OK) {
                                // ファイルに保存(非同期処理を使用)
                                await Task.Run(() => {
                                    File.WriteAllLines(saveDialog.FileName, fileList, Encoding.UTF8);
                                });
                                MessageBox.Show("ファイル一覧を保存しました。", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
                            }
                        }
                    }
                    catch (Exception ex) {
                        MessageBox.Show($"エラーが発生しました:{ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    }
                }
            }
        }

        private void ProcessDirectory(string path, List<string> fileList, int level) {
            string currentIndent = new string(' ', level * 2);
            try {
                // 現在のディレクトリ内のファイルを取得
                var files = Directory.GetFiles(path);
                foreach (var file in files.OrderBy(f => f)) {
                    var fileInfo = new FileInfo(file);
                    string fileSize = FormatFileSize(fileInfo.Length);
                    string fileDate = fileInfo.LastWriteTime.ToString("yyyy/MM/dd HH:mm:ss");
                    fileList.Add($"{currentIndent}📄 {Path.GetFileName(file)} ({fileSize}) - {fileDate}");
                }

                // サブディレクトリを取得して処理
                var directories = Directory.GetDirectories(path);
                foreach (var dir in directories.OrderBy(d => d)) {
                    var dirInfo = new DirectoryInfo(dir);
                    string dirDate = dirInfo.LastWriteTime.ToString("yyyy/MM/dd HH:mm:ss");
                    fileList.Add($"{currentIndent}📁 {Path.GetFileName(dir)} - {dirDate}");
                    ProcessDirectory(dir, fileList, level + 1);
                }
            }
            catch (UnauthorizedAccessException) {
                fileList.Add($"{currentIndent}⚠️ アクセスが拒否されたフォルダ: {Path.GetFileName(path)}");
            }
            catch (Exception ex) {
                fileList.Add($"{currentIndent}⚠️ エラー: {Path.GetFileName(path)} - {ex.Message}");
            }
        }

        private string FormatFileSize(long bytes) {
            string[] sizes = { "B", "KB", "MB", "GB", "TB" };
            int order = 0;
            double size = bytes;
            
            while (size >= 1024 && order < sizes.Length - 1) {
                order++;
                size /= 1024;
            }

            return $"{size:0.##} {sizes[order]}";
        }



        private void ShowClipboardHistory(object sender, EventArgs e) {
            ClipboardHistoryForm historyForm = new ClipboardHistoryForm(clipboardHistory);
            historyForm.Show();
        }



        private void timer1_Tick(object sender, EventArgs e) {
            // アニメーション
            currentFrame = (currentFrame + 1) % frames[0].Length;
            this.BackgroundImage = frames[direction == 1 ? 0 : 1][currentFrame];

            // 自動移動
            if (!mouseDown) {
                this.Left += speedX * direction;

                // 画面端で反転
                var screenBounds = Screen.PrimaryScreen.WorkingArea;
                if (this.Right >= screenBounds.Right || this.Left <= screenBounds.Left) {
                    direction *= -1;
                }

                // 浮き沈み(波の幅を大きく)
                waveCounter += 0.2;
                int waveOffset = (int)(Math.Sin(waveCounter) * waveAmplitude);
                this.Top = centerY + waveOffset;
            } else {
                // ドラッグ中は中心Yを更新
                centerY = this.Top;
            }
        }

        private void Form1_MouseClick(object sender, MouseEventArgs e) {
            clickSound.Play();
        }

        private void Form1_MouseDown(object sender, MouseEventArgs e) {
            mouseDown = true;
            mouseOffset = new Point(e.X, e.Y);
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e) {
            if (mouseDown) {
                this.Left = Cursor.Position.X - mouseOffset.X;
                this.Top = Cursor.Position.Y - mouseOffset.Y;
            }
        }

        private void Form1_MouseUp(object sender, MouseEventArgs e) {
            mouseDown = false;
        }

        private void 終了ToolStripMenuItem_Click(object sender, EventArgs e) {
            this.Close();
        }

        private void Form1_Load(object sender, EventArgs e) {
            // アプリケーションの設定を読み込む
            Properties.Settings.Default.Reload();

            //try {
            //    string google_ai_api_key = Properties.Settings.Default.googel_ai_apikey;//【設定した名前が"Name"】
            //   // Program.sql_flg = flg;
            //} catch (Exception) {

            //}
        }

        private void Form1_FormClosed(object sender,System.Windows.Forms.FormClosedEventArgs e) {
            try {
               // Properties.Settings.Default.sql_flg = Program.sql_flg;
            } catch (Exception) {

            }

            // アプリケーションの設定を保存する
            Properties.Settings.Default.Save();
        }

        private void タスク管理ToolStripMenuItem_Click(object sender, EventArgs e) {

        }
    }
}

PySide6 マスコット — オフライン .deb パッケージ化ガイド(wheels 同梱 & venv 自動作成)

このドキュメントでは、改良版 main_qt.py を使った PySide6 マスコットアプリ
オフラインインストール可能な .deb パッケージ として作成する方法を説明します。

特徴

  • PySide6 の wheel ファイルを同梱(インストール時にインターネット不要)
  • postinst スクリプトで 自動的に venv を作成し、同梱 wheel からインストール
  • デスクトップエントリ(アプリメニューに表示)と ランチャーをインストール
  • (任意)エンドユーザー向けに GUI インストーラー を提供
    Install-MyApp.desktop + インストール用スクリプト)

前提条件

  • ソースファイル: /home/yu/python_project/pyside/main_qt.py は仮想環境で起動するように作成しています
  • ソースファイル: /home/yu/python_project/ で仮想環境を作成コマンドで
cd /home/yu/python_project/
python3 -m venv venv
source venv/bin/activate # 仮想環境を起動する
  • ソースファイル: /home/yu/python_project/pyside/main_qt.py は以下のパッケージをインストールしています
    必要なパッケージ
pip install PySide6
pip install pyinstaller
  • ソースファイル: /home/yu/python_project/pyside/main_qt.py(改良済みバージョン)
  • リソースディレクトリ: /home/yu/python_project/pyside/Resources/
    (例: base*.png, kirby_1.wav など)
  • ビルドルート: /home/yu/debbuild/myapp/
  • パッケージ内インストール先: /usr/share/myapp/
  • 同梱する wheels: x86_64 Linux & Python 3.9+ ABI 向け(cp39-abi3、Python 3.10 でも動作)

ビルド前にPythonが動くかテストしたい場合

作成例

  • /home/yu/python_project/PyTkinter_mini/main_qt.pyを仮想環境で起動
  • /home/yu/python_project/.venv/bin/python に仮想環境のファイルがある

/home/ユーザ名/デスクトップ/myapp.desktop

[Desktop Entry]
Type=Application
Name=PyQt Mascot
# Waylandで不安定な場合は QT_QPA_PLATFORM=xcb を付ける(下の行は付けた例)
Exec=bash -lc 'QT_QPA_PLATFORM=xcb /home/yu/python_project/.venv/bin/python "/home/yu/python_project/PyTkinter_mini/main_qt.py" >>"/home/yu/python_project/PyTkinter_mini/qt_output.log" 2>&1'
Path=/home/yu/python_project/PyTkinter_mini
Icon=utilities-terminal
Terminal=false

ショートカットの作成ポイント

「仮想環境で確実に起動」させるときの要点をサクッとまとめます。

押さえるポイント(実務版)
venv の Python を“絶対パス”で指定
activate は不要・使わないのが安定。

/home/yu/python_project/.venv/bin/python ここの部分が仮想環境の設定がある場所から起動することを指定している
そして /home/yu/python_project/PyTkinter_mini/main_qt.py が実行するファイル
Exec=で実行する場所とファイルを指定している

# main_qt.py
# pip install PySide6
from PySide6.QtWidgets import (
    QApplication, QLabel, QMenu, QFileDialog, QMessageBox
)
from PySide6.QtGui import QPixmap, QAction
from PySide6.QtCore import Qt, QTimer, QPoint, Signal, QObject, QThread
from PySide6.QtGui import QGuiApplication
from PySide6.QtMultimedia import QSoundEffect
from PySide6.QtCore import QUrl
# 先頭の import 群に追加
from PySide6.QtGui import QTransform

import sys, os, glob, math, time

# ---------- ユーティリティ ----------
def resource_path(*relative):
    """開発時/pyinstaller双方で Resources を見つける"""
    if hasattr(sys, "_MEIPASS"):
        base = sys._MEIPASS
    else:
        base = os.path.abspath(os.path.dirname(__file__))
    return os.path.join(base, "Resources", *relative)


# ---------- バックグラウンドでディレクトリ走査 ----------
class WalkerWorker(QObject):
    finished = Signal(list)   # 完成した文字列リストをメインに返す
    error = Signal(str)

    def __init__(self, root_dir: str):
        super().__init__()
        self.root_dir = root_dir

    def run(self):
        try:
            d = self.root_dir
            now = time.strftime("%Y/%m/%d %H:%M:%S")
            file_list = [f"フォルダ一覧: {d}", f"作成日時: {now}", "-"*80, ""]
            for root, dirs, files in os.walk(d):
                rel = os.path.relpath(root, d)
                depth = 0 if rel == "." else rel.count(os.sep) + 1
                indent = "  " * depth

                if depth > 0:
                    try:
                        st = os.stat(root)
                        ts = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(st.st_mtime))
                        file_list.append(f"{indent}📁 {os.path.basename(root)} - {ts}")
                    except Exception as ex:
                        file_list.append(f"{indent}⚠️ {os.path.basename(root)} - {ex}")

                for f in sorted(files):
                    p = os.path.join(root, f)
                    try:
                        st = os.stat(p)
                        ts = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(st.st_mtime))
                        size = WalkerWorker.format_size(st.st_size)
                        file_list.append(f"{indent}📄 {f} ({size}) - {ts}")
                    except Exception as ex:
                        file_list.append(f"{indent}⚠️ {f} - {ex}")

            self.finished.emit(file_list)
        except Exception as e:
            self.error.emit(str(e))

    @staticmethod
    def format_size(n: int) -> str:
        units = ["B","KB","MB","GB","TB"]
        i = 0
        f = float(n)
        while f >= 1024 and i < len(units)-1:
            f /= 1024
            i += 1
        return f"{f:.2f} {units[i]}"


# ---------- マスコット本体(ラベルを窓として使用) ----------
class Mascot(QLabel):
    def __init__(self):
        super().__init__()

        # 透過・最前面・枠なし
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
        self.setAttribute(Qt.WA_TranslucentBackground)

        # アニメ関連パラメータ(好みに合わせて変更)
        self.interval_ms = 100    # タイマー間隔
        self.wave_amp    = 30     # 浮き沈みの幅
        self.wave_step   = 0.20   # 浮き沈みの速さ
        self.speed       = 2      # 横移動の速さ(ピクセル/tick)

        # 画像読み込み(右/左フレーム数チェック)
        self.frames_right, self.frames_left = self.load_frames_or_raise()
        self.frames = [self.frames_right, self.frames_left]  # 0=右,1=左

        # 初期状態
        self.dir = 0     # 0=右, 1=左
        self.idx = 0
        self.wave = 0.0
        self.center_y = 300
        self.move(100, self.center_y)

        # 初回表示
        self.setPixmap(self.frames[self.dir][self.idx])
        self.resize(self.pixmap().size())

        # 右クリックメニュー
        self.menu = QMenu(self)
        act_list = QAction("ファイルのパスを作成", self)
        act_quit = QAction("終了", self)
        act_list.triggered.connect(self.make_file_list)
        act_quit.triggered.connect(self.close)
        self.menu.addAction(act_list)
        self.menu.addSeparator()
        self.menu.addAction(act_quit)

        # アニメーションタイマー
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.loop)
        self.timer.start(self.interval_ms)

        # 画面幅(作業領域)取得
        self.screen_rect = QGuiApplication.primaryScreen().availableGeometry()

        # ドラッグ用
        self.drag_offset = QPoint()

        # ▼ クリック音の準備
        self.sound = QSoundEffect(self)
        wav_path = resource_path("kirby_1.wav")
        if os.path.exists(wav_path):
            self.sound.setSource(QUrl.fromLocalFile(wav_path))
            self.sound.setVolume(0.5)  # 0.0〜1.0
        else:
            self.sound = None  # 無ければ無音

    # ---------- クリック(左と右クリック) ----------
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            # 音を鳴らす
            if self.sound is not None:
                if self.sound.isPlaying():
                    self.sound.stop()
                self.sound.play()
            # ドラッグ準備
            self.drag_offset = event.globalPosition().toPoint() - self.pos()
            self.center_y = self.y()
        elif event.button() == Qt.RightButton:
            # 右クリック時はメニュー
            self.menu.exec(event.globalPos())


    # ---------- 画像読み込み ----------
    def load_frames_or_raise(self):
        candidates = sorted(glob.glob(resource_path("base*.png")))
        right_paths = [p for p in candidates if "_left" not in os.path.basename(p)]
        left_paths  = [p for p in candidates if "_left" in  os.path.basename(p)]

        if not right_paths:
            raise RuntimeError("Resources に base*.png が見つかりません。")

        right = [QPixmap(p) for p in right_paths]
        if left_paths:
            left = [QPixmap(p) for p in sorted(left_paths)]
            # 左右の枚数が一致しなければエラー(起動時に検知)
            if len(right) != len(left):
                raise RuntimeError(f"左右のフレーム枚数が一致しません: 右={len(right)}枚, 左={len(left)}")
        else:
            # 左向きが全く無い場合は右をミラーして同数生成
            left = [pixmap.transformed(QTransform().scale(-1, 1), Qt.SmoothTransformation)
                    for pixmap in right]

        return right, left

    # ---------- アニメーション ----------
    def loop(self):
        # フレーム更新(向きごとの枚数で回すのが安全)
        cur_list = self.frames[self.dir]
        if not cur_list:
            return
        self.idx = (self.idx + 1) % len(cur_list)
        self.setPixmap(cur_list[self.idx])

        # 自動移動&端で反転
        x = self.x() + (self.speed if self.dir == 0 else -self.speed)
        if x + self.width() >= self.screen_rect.right() or x <= self.screen_rect.left():
            self.dir = 1 - self.dir

        # 浮き沈み
        self.wave += self.wave_step
        y = self.center_y + int(math.sin(self.wave) * self.wave_amp)
        self.move(x, y)


    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            new_pos = event.globalPosition().toPoint() - self.drag_offset
            self.move(new_pos)

    # ---------- ファイル一覧 ----------
    def make_file_list(self):
        directory = QFileDialog.getExistingDirectory(self, "ファイル一覧を作成するフォルダを選択してください")
        if not directory:
            return

        # バックグラウンドで走査
        self.worker = WalkerWorker(directory)
        self.thread = QThread(self)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self._on_walk_finished)
        self.worker.error.connect(self._on_walk_error)
        # 後始末
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.error.connect(self.thread.quit)
        self.worker.error.connect(self.worker.deleteLater)

        self.thread.start()

    def _on_walk_finished(self, file_list):
        # 保存ダイアログはメインスレッドで
        fn, _ = QFileDialog.getSaveFileName(
            self,
            "ファイル一覧を保存",
            f"ファイル一覧_{time.strftime('%Y%m%d_%H%M%S')}.txt",
            "テキストファイル (*.txt)"
        )
        if not fn:
            return
        try:
            with open(fn, "w", encoding="utf-8") as f:
                f.write("\n".join(file_list))
            QMessageBox.information(self, "成功", "ファイル一覧を保存しました。")
        except Exception as e:
            QMessageBox.critical(self, "エラー", str(e))

    def _on_walk_error(self, msg):
        QMessageBox.critical(self, "エラー", msg)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Mascot()
    w.show()
    sys.exit(app.exec())

0) ビルド用ディレクトリの準備

1) アプリとリソースをパッケージツリーにコピー

cp /home/yu/python_project/pyside/main_qt.py ~/debbuild/myapp/usr/share/myapp/
cp -r /home/yu/python_project/pyside/Resources ~/debbuild/myapp/usr/share/myapp/

ソースファイル

debbuild/myapp/usr/share/myapp/main_qt.py

mkdir -p ~/debbuild/myapp/DEBIAN
mkdir -p ~/debbuild/myapp/usr/share/myapp
mkdir -p ~/debbuild/myapp/usr/bin
mkdir -p ~/debbuild/myapp/usr/share/applications

ビルドするフォルダに必要なパッケージをインストール

# オンライン環境で一式取得(バージョン固定)
mkdir -p ~/pyside6_wheels && cd ~/pyside6_wheels
pip download PySide6==6.9.1
pip download pip setuptools wheel

# パッケージに同梱
mkdir -p ~/debbuild/myapp/usr/share/myapp/wheels
cp ./*.whl ~/debbuild/myapp/usr/share/myapp/wheels/
ls -1 ~/debbuild/myapp/usr/share/myapp/wheels | sed 's/^/- /'

2) オフライン用 PySide6 インストールのための wheel を同梱

wheel のダウンロード方法(オンラインPC)

# 一時ディレクトリを作成
mkdir ~/pyside6_wheels
cd ~/pyside6_wheels

# pip download で PySide6 関連の wheel を取得(例: 最新版)
pip download PySide6

wheel をパッケージツリーにコピー(オフライン対応)
ダウンロードした .whl ファイルを、~/debbuild/myapp/usr/share/myapp/wheels/ にコピーします。

mkdir -p ~/debbuild/myapp/usr/share/myapp/wheels
# 例: 事前にダウンロードした PySide6 の wheel をコピー
cp PySide6*.whl ~/debbuild/myapp/usr/share/myapp/wheels/

3) ランチャースクリプト /usr/bin/myapp を作成

cat > ~/debbuild/myapp/usr/bin/myapp << 'EOF'
#!/bin/bash
cd /usr/share/myapp
/usr/share/myapp/.venv/bin/python main_qt.py
EOF
chmod 755 ~/debbuild/myapp/usr/bin/myapp

4) デスクトップエントリ作成(アプリのアイコンの設定なども)

nano ~/debbuild/myapp/usr/share/applications/myapp.desktop

Wayland について

ウィンドウ位置をアプリから勝手に変えるのは禁止(勝手にカーソル位置や画面外に飛ばす悪意あるアプリを防ぐため)。
そのため、マスコットアプリは意図通り動かない(動いてても同じ場所に固定表示される)。

  1. 回避策
    WaylandでもX11互換モードで動かすには、
    .desktop ファイルの Exec= 行を修正して 強制的に XWayland を使わせる ことができます。

  2. 実際の対処案
    開発中は Python から動作確認(現状OK)
    配布用には .desktop の Exec に QT_QPA_PLATFORM=xcb を付与
    ユーザーが Wayland でもマスコットを自由に動かせるようにする

内容

[Desktop Entry]
Name=MyApp Mascot
Exec=env QT_QPA_PLATFORM=xcb myapp
Icon=myapp
Type=Application
Categories=Utility;

アイコンの配置 処理手順

mkdir -p ~/debbuild/myapp/usr/share/icons/hicolor/128x128/apps
cp /home/yu/python_project/pyside/Resources/base1.png \
   ~/debbuild/myapp/usr/share/icons/hicolor/128x128/apps/myapp.png

5) control ファイル(依存関係とメタデータ)

nano ~/debbuild/myapp/DEBIAN/control
Package: myapp
Version: 1.0-offline2
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Yu <you@example.com>
Depends: python3, python3-venv, python3-pip, libgl1, libxkbcommon-x11-0, libxcb-cursor0, libxcb-xinerama0, libasound2
Description: PySide6 mascot (offline wheels, auto-venv)
 Installs a virtualenv and pip-installs PySide6 from bundled wheels (no internet).
 Includes WAV click sound via QSoundEffect.

6) postinst — wheels 同梱 & venv 自動作成

nano ~/debbuild/myapp/DEBIAN/postinst
# ~/debbuild/myapp/DEBIAN/postinst
#!/bin/sh
set -e
APPDIR="/usr/share/myapp"
VENV="$APPDIR/.venv"
WHEELSDIR="$APPDIR/wheels"
LOG="/var/log/myapp_postinst.log"

echo "[myapp] postinst start" | tee -a "$LOG"

# 必須 wheel が揃っているか確認
need="shiboken6 PySide6_Essentials PySide6_Addons PySide6"
for pkg in $need; do
  if ! ls "$WHEELSDIR"/${pkg}-*.whl >/dev/null 2>&1; then
    echo "[myapp] ERROR: $pkg の wheel が見つかりません ($WHEELSDIR)" | tee -a "$LOG"
    exit 1
  fi
done

# venv 作成
[ -d "$VENV" ] || python3 -m venv "$VENV"

# pip 系を先に更新(同梱があればオフラインでOK)
if ls "$WHEELSDIR"/pip-*.whl >/dev/null 2>&1; then
  "$VENV/bin/python" -m pip install --no-index --find-links="$WHEELSDIR" pip setuptools wheel >>"$LOG" 2>&1 || true
fi

# 明示順でインストール(--no-index でネット遮断)
"$VENV/bin/python" -m pip install --no-index --find-links="$WHEELSDIR" \
  shiboken6 PySide6_Essentials PySide6_Addons PySide6 >>"$LOG" 2>&1

# ---- ここから追加:アイコン/デスクトップDB更新 ----
# アイコンキャッシュ更新(hicolor テーマに myapp.png を入れている前提)
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
  gtk-update-icon-cache -f /usr/share/icons/hicolor || true
fi

# .desktop データベース更新
if command -v update-desktop-database >/dev/null 2>&1; then
  update-desktop-database -q || true
fi
# ---- 追加ここまで ----

echo "[myapp] postinst done" | tee -a "$LOG"
exit 0

保存後に権限を付与

chmod 755 ~/debbuild/myapp/DEBIAN/postinst

prermを作成する

cat > ~/debbuild/myapp/DEBIAN/prerm << 'EOF'
#!/bin/sh
# prerm - uninstall hook for myapp
# 役割: パッケージ削除の直前に、パッケージ外生成物(venvやログ)を片付ける
set -e

APPDIR="/usr/share/myapp"
VENV="$APPDIR/.venv"
LOG="/var/log/myapp_postinst.log"

case "$1" in
  remove|purge)
    # venv の削除(存在すれば)
    if [ -d "$VENV" ]; then
      echo "Removing MyApp virtual environment: $VENV"
      rm -rf "$VENV"
    fi

    # 任意:postinst ログの削除(purge のときだけ)
    if [ "$1" = "purge" ] && [ -f "$LOG" ]; then
      rm -f "$LOG" || true
    fi
    ;;

  upgrade|deconfigure|failed-upgrade)
    # アップグレード時は何もしない
    ;;

  *)
    # 想定外の引数でも失敗しない
    :
    ;;
esac

exit 0
EOF

chmod 755 ~/debbuild/myapp/DEBIAN/prerm

7) パーミッション設定 & ビルド

cd ~/debbuild
dpkg-deb --build --root-owner-group myapp
sudo apt install ./myapp.deb

配布フォルダ作成

mkdir -p ~/配布

8)README.txtの作成

以下のコマンドを実行

cat > ~/配布/README.txt << 'EOF'
MyApp オフラインインストーラー
--------------------------------
1. install_myapp.sh を右クリックして「プロパティ」→「アクセス権」→「実行を許可」にチェック
2. install_myapp.sh をダブルクリック
3. 「端末で実行」または自動でGUIパスワード入力が表示されます
4. インストール完了後、アプリケーションメニューから MyApp を起動できます

【補足】
- .deb をダブルクリックで開くには『ソフトウェア』(gnome-software) が必要です。
- 本インストーラはオンライン時に gnome-software の自動導入を試みます。
- オフライン環境や導入に失敗した場合でも、install_myapp.sh からのインストールは可能です。
EOF

9) install_myapp.sh(インストール実行スクリプト)の作成

以下で一括実行

cat > ~/配布/install_myapp.sh << 'EOF'
#!/bin/bash
# MyApp オフラインインストーラ(GUI パスワード: pkexec / 通知: zenity)
# 追加: gnome-software が無ければオンライン時に自動導入を試みる(任意)

set -e
APPDIR="$(cd "$(dirname "$0")" && pwd)"
DEB="$APPDIR/myapp.deb"

# --- 補助関数 ---
has_cmd() { command -v "$1" >/dev/null 2>&1; }
has_pkg() { dpkg -s "$1" >/dev/null 2>&1; }
gui_info()  { has_cmd zenity && zenity --info  --title="Kirby" --text="$1" || echo "$1"; }
gui_error() { has_cmd zenity && zenity --error --title="Kirby" --text="$1" || echo "$1" >&2; }

# --- 前提チェック: pkexec / zenity を推奨 ---
if ! has_cmd pkexec; then
  echo "pkexec が見つかりません(GUI 認証が使えません)。policykit-1 の導入が必要です。"
  echo "sudo 権限があるなら:  sudo apt install -y policykit-1"
  exit 1
fi

# zenity が無い場合は導入を試みる(失敗したらテキスト通知で継続)
if ! has_cmd zenity; then
  pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt-get update || true; apt-get install -y zenity || true" || true
fi

# --- 任意: gnome-software を自動導入(ダブルクリックで .deb を開きたい人向け) ---
# オフライン環境では当然失敗するので、エラーは握りつぶして続行。
if ! has_pkg gnome-software; then
  gui_info "『ソフトウェア』(gnome-software) が未インストールのため、導入を試みます(オンライン時のみ)。"
  pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt-get update || true; apt-get install -y gnome-software || true" || true
fi

# --- 本体インストール(オフライン可) ---
if [ ! -f "$DEB" ]; then
  gui_error "myapp.deb が見つかりません。配布フォルダ内で実行してください。"
  exit 1
fi

if pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt install -y '$DEB'"; then
  gui_info "MyApp のインストールが完了しました!"
else
  gui_error "インストールに失敗しました。ログを確認してください。"
  exit 1
fi
EOF
chmod +x ~/配布/install_myapp.sh

※ install_myapp.sh の中身は以下の範囲

#!/bin/bash
# MyApp オフラインインストーラ(GUI パスワード: pkexec / 通知: zenity)
# 追加: gnome-software が無ければオンライン時に自動導入を試みる(任意)

set -e
APPDIR="$(cd "$(dirname "$0")" && pwd)"
DEB="$APPDIR/myapp.deb"

# --- 補助関数 ---
has_cmd() { command -v "$1" >/dev/null 2>&1; }
has_pkg() { dpkg -s "$1" >/dev/null 2>&1; }
gui_info()  { has_cmd zenity && zenity --info  --title="Kirby" --text="$1" || echo "$1"; }
gui_error() { has_cmd zenity && zenity --error --title="Kirby" --text="$1" || echo "$1" >&2; }

# --- 前提チェック: pkexec / zenity を推奨 ---
if ! has_cmd pkexec; then
  echo "pkexec が見つかりません(GUI 認証が使えません)。policykit-1 の導入が必要です。"
  echo "sudo 権限があるなら:  sudo apt install -y policykit-1"
  exit 1
fi

# zenity が無い場合は導入を試みる(失敗したらテキスト通知で継続)
if ! has_cmd zenity; then
  pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt-get update || true; apt-get install -y zenity || true" || true
fi

# --- 任意: gnome-software を自動導入(ダブルクリックで .deb を開きたい人向け) ---
# オフライン環境では当然失敗するので、エラーは握りつぶして続行。
if ! has_pkg gnome-software; then
  gui_info "『ソフトウェア』(gnome-software) が未インストールのため、導入を試みます(オンライン時のみ)。"
  pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt-get update || true; apt-get install -y gnome-software || true" || true
fi

# --- 本体インストール(オフライン可) ---
if [ ! -f "$DEB" ]; then
  gui_error "myapp.deb が見つかりません。配布フォルダ内で実行してください。"
  exit 1
fi

if pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY bash -lc "apt install -y '$DEB'"; then
  gui_info "MyApp のインストールが完了しました!"
else
  gui_error "インストールに失敗しました。ログを確認してください。"
  exit 1
fi

保存後に実行権限を追加

chmod +x ~/install_myapp.sh

10) ユーザーフレンドリーなオフラインインストーラー zip を作成

mkdir -p ~/配布
cp ~/debbuild/myapp.deb ~/配布/
cp ~/README.txt ~/配布/
cp ~/install_myapp.sh ~/配布/
cd ~
zip -r myapp_offline.zip 配布

12) 最後にデスクトップに移動させます

mv ~/myapp_offline.zip ~/デスクトップ/

13) 注意点 & トラブルシューティング

  • Python バージョンが 3.9 以上であることを確認
  • 他の環境で試す場合は sudo apt remove myapp で削除してから再インストール

14) 簡易チェックリスト(Excel形式QA向け)

項目 確認
.deb が生成されている
wheels が同梱されている
venv が postinst で作られる
デスクトップメニューから起動できる
オフライン環境で動作確認済み

アプリをアンインストールする場合

以下のコマンドを実行

sudo apt remove myapp
sudo apt autoremove

インストールし直す前に行うコマンド

# パッケージ本体を削除(設定も消すなら purge)
sudo apt remove myapp
# または
sudo apt purge myapp

# 依存の不要分を整理(任意)
sudo apt autoremove

sudo rm -rf /usr/share/myapp
rm -f ~/.config/autostart/myapp.desktop
rm -f ~/デスクトップ/myapp.desktop

マスコットが座標から動かない原因との関係

Xorg
self.move(x, y) のようなウィンドウ移動APIはそのまま通るので、デスクトップマスコットのような「自由に動くウィンドウ」は正常動作しやすい。

Wayland
ウィンドウ位置をアプリから勝手に変えるのは禁止(勝手にカーソル位置や画面外に飛ばす悪意あるアプリを防ぐため)。
そのため、マスコットアプリは意図通り動かない(動いてても同じ場所に固定表示される)。

  1. どっちを使えばいい?
    普通のユーザー → Wayland推奨(セキュリティと描画のなめらかさが良い)

ウィンドウを動かす系アプリを開発・利用する人 → Xorg(Ubuntuのログイン画面で「Ubuntu on Xorg」を選択)

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?