1
1

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でデスクトップアプリ作成手順2

Last updated at Posted at 2025-08-10

pythonで作り直し

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

下のリンクのプログラムはデスクトップをカービィがふわふわと自由に飛び回っているのを眺めて癒されるアプリの作成になります。
https://qiita.com/studyhiminato1107/items/a88359901f85e1539cae

今回のプログラムも似たようなものですがうさまるのデスクトップトップマスコットです

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;
using Microsoft.VisualBasic;//参照の追加が必要
using Desktop_usemaru_share.Models;

namespace Desktop_usemaru_share
{

    #region ----------------- enum うさまるのモード ----------------------------
    public enum Usamaru_Mode
    {
        Stop,
        Patoka,
        Walk,
        HiyokoGyu,
        PopCone,
        Cry,
        Sleep,
        Cheerleader,
        Amaenbo,
        Kanpe,
        Yorokobi
    }
    #endregion ----------------- enum うさまるのモード 末尾 ----------------------------

    public partial class Form1 : Form
    {

        #region ----------------- フィールド ----------------------------
        //private SoundPlayer Sound_Item;
        private ClipboardHistoryManager clipboardHistory;
        // ドラッグ用
        private bool mouseDown = false;
        private Point mouseOffset;
        private bool Fixed_Flg = false;
        private string Fixed_moji = "固定モードON";
        private const int Fixed_num = 4;

        //移動するフラグ
        private Usamaru_Mode usamaru_mode = Usamaru_Mode.Stop;

        // パトカーモード用の変数
        private int direction = 1; // 1=右, -1=左
        private int speedX = 5; // パトカーは速めに
        private int centerY;
        private double waveCounter = 0;
        private int waveAmplitude = 10; // パトカーは浮き沈みを小さめに

        private int Animation_Frame = 1; // アニメーション用のフレームカウンタ-
        private int Return_Anime_Counter = 0;

        private int physicalTimerCounter = 0; // 体力減少用のカウンター
        private int foodTimerCounter = 0; // 満腹度減少用のカウンター
        private int sleepTimerCounter = 0;//睡眠カウンター
        private Point AmaenboCounter;

        private Usamaru_Mode Before_Mode;//履歴モード

        // 例:なでた回数を数える
        private int strokeCount = 0;

        private System.Windows.Forms.Label label_Kanpe = new System.Windows.Forms.Label();
        private ContextMenuStrip contextMenu_Kanpe =new ContextMenuStrip();
        private int fixedMenuItemsCount = 0; // 固定メニューアイテムの数を記録

        //アラート
        private List<Alert> _upcoming = new List<Alert>();
        private Timer _alertTimer;
        // 同じ分に同じアラートを二重発火させないためのキー
        private HashSet<string> _firedKeysThisMinute = new HashSet<string>();
        private SQLite_Database _db;
        #endregion ----------------- フィールド 末尾 ----------------------------

        #region ----------------- 初期化 ----------------------------
        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;

            // PictureBox1に画像を設定
            pictureBox1.Image = Properties.Resources.main;
            pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;

            // タイマー設定
            timer1.Interval = 50; // パトカーは速めに更新
            timer1.Enabled = true;

            // 体力タイマーの設定
            timer_Physical.Interval = 1000; // 1秒間隔
            timer_Physical.Enabled = true;

            // イベント登録
            this.MouseDown += Form1_MouseDown;
            this.MouseMove += Form1_MouseMove;
            this.MouseUp += Form1_MouseUp;

            clipboardHistory = new ClipboardHistoryManager();

            // 固定のメニューアイテムを追加
            AddFixedMenuItems();
            
            // アクションメニューを追加
            AddActionMenuItems();

            // 登録済みのエクスプローラとURLアイテムをメニューに追加
            AddRegisteredItemsToMenu();

            label_Kanpe.ContextMenuStrip = contextMenu_Kanpe;
            contextMenu_Kanpe.Items.Add("テキスト変更", null, Kanpe_Text_Change);

            progressBar_Food.Style = ProgressBarStyle.Continuous;
            progressBar_Physical.Style = ProgressBarStyle.Continuous;

            // 1週間以上過去のクリップボードデータを削除
            CleanupOldClipboardData();
            // 非同期でデータを取得
            _ = InitializeFirebaseDataAsync();

            _db = new SQLite_Database(); // ★ここで必ず生成
        }

        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);
            }
        }

        /// <summary>
        /// 固定のメニューアイテムを追加
        /// </summary>
        private void AddFixedMenuItems() {
            this.contextMenuStrip1.Items.Add("終了", null, 終了ToolStripMenuItem_Click);
            this.contextMenuStrip1.Items.Add("ファイルのパスを作成", null, Get_File_Paths);
            this.contextMenuStrip1.Items.Add("コピー履歴を見る", null, ShowClipboardHistory);
            this.contextMenuStrip1.Items.Add("エクスプローラ起動", null, Show_Explorer_FM);
            this.contextMenuStrip1.Items.Add(Fixed_moji, null, Fixed_mode_change);
            this.contextMenuStrip1.Items.Add("アラート設定", null, ShowAlertForm);
            this.contextMenuStrip1.Items.Add(new ToolStripSeparator());
            
            // 固定アイテムの数を記録(4つのメニューアイテム + 1つのセパレーター + 1つの終了メニュー)
            fixedMenuItemsCount = 6;
        }

        /// <summary>
        /// アクションメニューを追加
        /// </summary>
        private void AddActionMenuItems() {
            // アクションサブメニューを作成
            var actionMenuItem = new ToolStripMenuItem("アクション");
            
            // アクションサブメニューに項目を追加
            actionMenuItem.DropDownItems.Add("寝る", null, 寝るToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("パトカー", null, パトカーToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("ストップ", null, ストップToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("ひよこ", null, ひよこToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("ポップコーン", null, ポップコーンToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("応援", null, 応援ToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("カンペ", null, カンペToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("甘えんぼモード", null, 甘えんぼモードToolStripMenuItem_Click);
            actionMenuItem.DropDownItems.Add("体力の表示ON/OFF", null, 体力の表示ToolStripMenuItem_Click);
            
            // アクションメニューをコンテキストメニューに追加
            this.contextMenuStrip1.Items.Add(actionMenuItem);
        }

        /// <summary>
        /// データベースから登録済みのエクスプローラとURLアイテムを取得してコンテキストメニューに追加
        /// </summary>
        private void AddRegisteredItemsToMenu() {
            try {
                var database = new SQLite_Database();
                
                // 登録済みのエクスプローラアイテムを取得
                var explorerEntries = database.GetExplorerEntries()
                    .Where(e => e.Registration == 1)
                    .OrderBy(e => e.ItemName)
                    .ToList();
                
                // 登録済みのURLアイテムを取得
                var urlEntries = database.GetURLEntries()
                    .Where(u => u.Registration == 1)
                    .OrderBy(u => u.ItemName)
                    .ToList();
                
                // エクスプローラアイテムをメニューに追加
                if (explorerEntries.Any()) {
                    this.contextMenuStrip1.Items.Add(new ToolStripSeparator());
                    var explorerMenuItem = new ToolStripMenuItem("エクスプローラ");
                    
                    foreach (var entry in explorerEntries) {
                        var item = new ToolStripMenuItem(entry.ItemName ?? "無名のフォルダ", null, (s, e) => OpenExplorer(entry.Text));
                        item.ToolTipText = entry.Text;
                        explorerMenuItem.DropDownItems.Add(item);
                    }
                    
                    this.contextMenuStrip1.Items.Add(explorerMenuItem);
                }
                
                // URLアイテムをメニューに追加
                if (urlEntries.Any()) {
                    if (!explorerEntries.Any()) {
                        this.contextMenuStrip1.Items.Add(new ToolStripSeparator());
                    }
                    var urlMenuItem = new ToolStripMenuItem("URL");
                    
                    foreach (var entry in urlEntries) {
                        var item = new ToolStripMenuItem(entry.ItemName ?? "無名のURL", null, (s, e) => OpenURL(entry.Text));
                        item.ToolTipText = entry.Text;
                        urlMenuItem.DropDownItems.Add(item);
                    }
                    
                    this.contextMenuStrip1.Items.Add(urlMenuItem);
                }
                
                System.Diagnostics.Debug.WriteLine($"登録済みアイテムをメニューに追加しました。エクスプローラ: {explorerEntries.Count}件, URL: {urlEntries.Count}件");
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"登録済みアイテムのメニュー追加中にエラーが発生しました: {ex.Message}");
            }
        }

        /// <summary>
        /// URLをブラウザで開く
        /// </summary>
        /// <param name="url">開くURL</param>
        private void OpenURL(string url) {
            try {
                if (string.IsNullOrWhiteSpace(url)) {
                    MessageBox.Show("URLが無効です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                
                // URLが有効かチェック
                if (!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) {
                    MessageBox.Show("無効なURLです。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                
                // デフォルトブラウザでURLを開く
                System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo {
                    FileName = url,
                    UseShellExecute = true
                });
                
                System.Diagnostics.Debug.WriteLine($"URLを開きました: {url}");
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"URLを開く際にエラーが発生しました: {ex.Message}");
                MessageBox.Show($"URLを開く際にエラーが発生しました: {ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        /// <summary>
        /// エクスプローラでフォルダまたはファイルを開く
        /// </summary>
        /// <param name="path">開くパス</param>
        private void OpenExplorer(string path) {
            try {
                if (string.IsNullOrWhiteSpace(path)) {
                    MessageBox.Show("パスが無効です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                
                // パスが存在するかチェック
                if (!System.IO.Directory.Exists(path) && !System.IO.File.Exists(path)) {
                    MessageBox.Show("指定されたパスが存在しません。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                
                // エクスプローラで開く
                if (System.IO.Directory.Exists(path)) {
                    // フォルダの場合
                    ExplorerLauncher.OpenFolder(path);
                } else {
                    // ファイルの場合、ファイルを選択状態でエクスプローラを開く
                    ExplorerLauncher.OpenAndSelectFile(path);
                }
                
                System.Diagnostics.Debug.WriteLine($"エクスプローラで開きました: {path}");
            } catch (Exception ex) {
                System.Diagnostics.Debug.WriteLine($"エクスプローラを開く際にエラーが発生しました: {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}");
                // エラーが発生してもアプリの起動は継続する
            }
        }
        #endregion ----------------- 初期化 末尾 ----------------------------

        #region ----------------- 便利機能 ----------------------------
        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);
            }
        }

        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();
        }
        #endregion ----------------- 便利機能 末尾 ----------------------------

        #region ----------------- マウス ----------------------------
        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 && (usamaru_mode != Usamaru_Mode.Amaenbo))
            {
                this.Left = Cursor.Position.X - mouseOffset.X;
                this.Top = Cursor.Position.Y - mouseOffset.Y;
            }

            if (usamaru_mode == Usamaru_Mode.Amaenbo)
            {
                if (this.ClientRectangle.Contains(e.Location))
                {
                    if(AmaenboCounter != e.Location) {
                        AmaenboCounter = e.Location;
                        strokeCount++;
                        if (strokeCount > Properties.Settings.Default.Amaenbo) // 例:100回以上MouseMove検出でモード変更
                        {
                            Yorokobi_Change();
                        }
                    }

                }
            }
        }

        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
            mouseDown = false;
        }
        #endregion ------------------- マウス末尾 ---------------------------------

        #region ----------------- タイマー処理 ----------------------------
            #region ---- 各モードアクション用タイマー -----
            private void timer1_Tick(object sender, EventArgs e)
            {
                switch (usamaru_mode)
                {
                    case Usamaru_Mode.Patoka:// 自動移動
                        PatoCar_Action();
                        break;

                    case Usamaru_Mode.Sleep:
                        //  睡眠
                        Sleep_Action();
                        break;

                }

            }
        #endregion ---- 各モード アクション用タイマー 末尾 -----

            #region ---- 減少系タイマー  -----
        private void timer_Physical_Tick(object sender, EventArgs e) {
            //System.Diagnostics.Debug.WriteLine($"減少タイマー");
            #region ---- 体力減少 -----
            if (progressBar_Physical.Value > 0 && (usamaru_mode != Usamaru_Mode.Sleep)) {
                //System.Diagnostics.Debug.WriteLine($"体力0以上");
                if (physicalTimerCounter >= 60) {
                    //System.Diagnostics.Debug.WriteLine($"体力減少");
                    progressBar_Physical.Value--;
                    physicalTimerCounter = 0;
                } else {
                    //System.Diagnostics.Debug.WriteLine($"体力カウンター上昇"+physicalTimerCounter.ToString());
                    physicalTimerCounter++;
                }
            } else if ((usamaru_mode != Usamaru_Mode.Sleep) && (usamaru_mode != Usamaru_Mode.PopCone)) {
                Sleep_Change();
            } else {
               // System.Diagnostics.Debug.WriteLine($"体力 例外");
            }
            #endregion ---- 体力減少 末尾 -----
        }
        #endregion ---- 減少系タイマー 末尾 -----

        #endregion ----------------- タイマー処理末尾 ----------------------------

        #region ----------------- クリックイベント ----------------------------
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            switch (usamaru_mode)
            {
                case Usamaru_Mode.Patoka:
                    SoundList.Sound_Patoka();
                    break;

                case Usamaru_Mode.Sleep:
                    SoundList.Sound_Sleep();
                    break;
            }
        }
        #endregion ----------------- クリックイベント末尾 ----------------------------

        #region ---- 各モードアクション -----
        private void PatoCar_Action() {
            if (!mouseDown) {
                this.Left += speedX * direction;

                // 画面端で反転
                var screenBounds = Screen.PrimaryScreen.WorkingArea;
                if (this.Right >= screenBounds.Right || this.Left <= screenBounds.Left) {
                    direction *= -1;
                    // 画像を切り替え
                    pictureBox1.Image = (direction == 1) ? Properties.Resources.Patoka_R : Properties.Resources.Patoka_L;
                }

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

        private void Sleep_Action()
        {
            if (!mouseDown)
            {
                if (progressBar_Physical.Value < 100)
                {

                    if (Animation_Frame < 17)
                    {
                        // フレームを更新
                        Animation_Frame++;
                        // 画像を切り替え
                        string resourceName = $"Sleep_{Animation_Frame}";
                        pictureBox1.Image = (Image)Properties.Resources.ResourceManager.GetObject(resourceName);
                    }

                    if (sleepTimerCounter < 10)
                    {
                        sleepTimerCounter++;
                        progressBar_Physical.Value++;
                    }
                    else
                    {
                        sleepTimerCounter = 0;
                    }

                }
                else
                {
                    Eat_Sleep_After_Change();
                }
            }
        }


        #endregion ---- 各モード アクション 末尾 -----


    }
}

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

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

特徴

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

前提条件

  • ソースファイル: /home/kali/python_project/PySide6/Usamaru.py は仮想環境で起動するように作成しています
  • ソースファイル: /home/kali/python_project/ で仮想環境を作成コマンドで
cd /home/kali/python_project/
python3 -m venv venv
source venv/bin/activate # 仮想環境を起動する
  • ソースファイル: /home/kali/python_project/PySide6/Usamaru.py は以下のパッケージをインストールしています
    必要なパッケージ
pip install PySide6
pip install pyinstaller
  • ソースファイル: /home/kali/python_project/PySide6/Usamaru.py(改良済みバージョン)
  • リソースディレクトリ: /home/kali/python_project/PySide6/Resources

ソースコード

Usamaru.py
import sys, os, time, math, glob, webbrowser, subprocess
from enum import Enum
from pathlib import Path

from PySide6.QtCore import Qt, QTimer, QPoint, QSize, QUrl
from PySide6.QtGui import QPixmap, QAction, QGuiApplication, QMovie
from PySide6.QtWidgets import (
    QApplication, QLabel, QMenu, QFileDialog, QMessageBox, QProgressBar
)
from PySide6.QtMultimedia import QSoundEffect

# ------------------------------------------------------------
# 便利: Resources フォルダを解決(PyInstaller/開発時 両対応)
# ------------------------------------------------------------
def resource_path(*relative):
    if hasattr(sys, "_MEIPASS"):
        base = sys._MEIPASS
    else:
        base = os.path.abspath(os.path.dirname(__file__))
    return os.path.join(base, "Resources", *relative)

class UsamaruMode(Enum):
    Stop = 0
    Patoka = 1
    Sleep = 2

class MascotWindow(QLabel):
    def __init__(self):
        super().__init__()

        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
        self.setAttribute(Qt.WA_TranslucentBackground)

        self.mode = UsamaruMode.Patoka  # デフォルトをパトカー
        self.mouse_down = False
        self._press_pos = None
        self.drag_offset = QPoint()
        self.direction = 1
        self.speedX = 5
        self.centerY = 300
        self.wave = 0.0
        self.wave_amp = 10
        self.sleep_frame = 1
        self.sleep_max_frame = 17

        self.screen_rect = QGuiApplication.primaryScreen().availableGeometry()

        self.pix_main = self.load_or_placeholder("main.png")
        self.movie_patoka_r = self.load_movie_or_none("Patoka_R.gif")
        self.movie_patoka_l = self.load_movie_or_none("Patoka_L.gif")
        self.sleep_frames = self.load_sleep_frames()

        self.patoka_sound = self.load_sound("Police_Car-Siren1.wav")

        self.move(100, self.centerY)

        self.physical = QProgressBar(self)
        self.physical.setRange(0, 100)
        self.physical.setValue(80)
        self.physical.setStyleSheet("QProgressBar{background:rgba(0,0,0,80); color:white; border:1px solid rgba(255,255,255,120);} QProgressBar::chunk{background:rgba(0,180,0,200);}")

        self.to_patoka()           # ← ここに移動(physical作成後)

        self.timer = QTimer(self)
        self.timer.setInterval(50)
        self.timer.timeout.connect(self.tick)
        self.timer.start()

        self.ph_timer = QTimer(self)
        self.ph_timer.setInterval(1000)
        self.ph_timer.timeout.connect(self.decay_physical)
        self.ph_timer.start()

        self.menu = QMenu(self)
        self.add_fixed_menu_items(self.menu)
        self.add_action_submenu(self.menu)
        self.reposition_progress()

    def load_or_placeholder(self, name: str) -> QPixmap:
        p = resource_path(name)
        if os.path.exists(p):
            pm = QPixmap(p)
            if not pm.isNull():
                return pm
        ph = QPixmap(QSize(160, 160))
        ph.fill(Qt.transparent)
        return ph

    def load_sleep_frames(self):
        frames = []
        for i in range(1, self.sleep_max_frame + 1):
            fname = f"Sleep_{i}.png"
            frames.append(self.load_or_placeholder(fname))
        return frames

    def load_movie_or_none(self, name: str):
        p = resource_path(name)
        if os.path.exists(p):
            mv = QMovie(p)
            if mv.isValid():
                return mv
        return None

    def load_sound(self, name: str):
        path = resource_path(name)
        if os.path.exists(path):
            snd = QSoundEffect(self)  # 親を渡す
            snd.setSource(QUrl.fromLocalFile(path))
            return snd
        return None


    def add_fixed_menu_items(self, menu: QMenu):
        menu.addAction("終了", self.close)
        menu.addAction("ファイルのパスを作成", self.make_file_list)
        menu.addAction("体力の表示ON/OFF", self.toggle_physical)
        menu.addSeparator()

    def add_action_submenu(self, menu: QMenu):
        act_menu = menu.addMenu("アクション")
        act_menu.addAction("寝る", self.to_sleep)
        act_menu.addAction("パトカー", self.to_patoka)

    def contextMenuEvent(self, e):
        self.menu.exec(e.globalPos())

    def mousePressEvent(self, e):
        if e.button() == Qt.LeftButton:
            self.mouse_down = True
            self.drag_offset = e.globalPosition().toPoint() - self.pos()
            self._press_pos = e.position().toPoint()
            self.centerY = self.y()

    def mouseReleaseEvent(self, e):
        if e.button() == Qt.LeftButton:
            # 単クリック判定(ドラッグ距離が小さい)
            if self._press_pos is not None:
                if (e.position().toPoint() - self._press_pos).manhattanLength() < 6:
                    if self.mode == UsamaruMode.Patoka and self.patoka_sound:
                        self.patoka_sound.setLoopCount(1)
                        self.patoka_sound.setVolume(0.9)  # 0.0〜1.0
                        self.patoka_sound.play()
            self._press_pos = None
        self.mouse_down = False


    def mouseMoveEvent(self, e):
        if self.mouse_down:
            new_pos = e.globalPosition().toPoint() - self.drag_offset
            self.move(new_pos)

    def to_patoka(self):
        self.mode = UsamaruMode.Patoka
        mv = self.movie_patoka_r if self.direction == 1 else self.movie_patoka_l
        if mv is not None:
            self.setMovie(mv)
            mv.start()
            self.current_movie = mv
            sz = mv.currentImage().size()
            if not sz.isEmpty():
                self.resize(sz)
        else:
            self.setPixmap(self.pix_main)
            self.resize(self.pixmap().size())
        self.reposition_progress()

    def to_sleep(self):
        self.mode = UsamaruMode.Sleep
        self.sleep_frame = 1
        if hasattr(self, 'current_movie') and self.current_movie:
            self.current_movie.stop()
        self.setPixmap(self.sleep_frames[0])
        self.resize(self.pixmap().size())
        self.reposition_progress()

    def tick(self):
        if self.mode == UsamaruMode.Patoka:
            self.patoka_action()
        elif self.mode == UsamaruMode.Sleep:
            self.sleep_action()

    def patoka_action(self):
        if not self.mouse_down:
            x = self.x() + self.speedX * self.direction
            if x + self.width() >= self.screen_rect.right() or x <= self.screen_rect.left():
                self.direction *= -1
                mv = self.movie_patoka_r if self.direction == 1 else self.movie_patoka_l
                # ★追加: 以前のムービーを止める
                if hasattr(self, 'current_movie') and self.current_movie and self.current_movie is not mv:
                    self.current_movie.stop()
                    self.setMovie(mv)
                    mv.frameChanged.connect(lambda _=None, m=mv: (not m.currentImage().isNull()) and self.resize(m.currentImage().size()))
                    mv.start()

                    self.current_movie = mv
            self.wave += 0.2
            y = self.centerY + int(math.sin(self.wave) * self.wave_amp)
            self.move(x, y)
        else:
            self.centerY = self.y()

    def sleep_action(self):
        if not self.mouse_down:
            if self.sleep_frame < self.sleep_max_frame:
                self.sleep_frame += 1
                self.setPixmap(self.sleep_frames[self.sleep_frame - 1])
            if self.physical.value() < 100:
                self.physical.setValue(self.physical.value() + 1)
            else:
                self.to_patoka()

    def decay_physical(self):
        if self.mode != UsamaruMode.Sleep:
            v = self.physical.value()
            if v > 0:
                self.physical.setValue(v - 1)
            else:
                self.to_sleep()

    def reposition_progress(self):
        self.physical.setFixedWidth(max(120, self.width()))
        self.physical.move(0, self.height() + 6)

    def toggle_physical(self):
        self.physical.setVisible(not self.physical.isVisible())

    def make_file_list(self):
        directory = QFileDialog.getExistingDirectory(self, "ファイル一覧を作成するフォルダを選択してください")
        if not directory:
            return
        try:
            file_list = []
            file_list.append(f"フォルダ一覧: {directory}")
            file_list.append(f"作成日時: {time.strftime('%Y/%m/%d %H:%M:%S')}")
            file_list.append('-'*80)
            file_list.append("")
            for root, dirs, files in os.walk(directory):
                rel = os.path.relpath(root, directory)
                depth = 0 if rel == "." else rel.count(os.sep) + 1
                indent = "  " * depth
                if depth > 0:
                    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}")
                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 = self.format_size(st.st_size)
                        file_list.append(f"{indent}📄 {f} ({size}) - {ts}")
                    except Exception as ex:
                        file_list.append(f"{indent}⚠️ {f} - {ex}")
            fn, _ = QFileDialog.getSaveFileName(self, "ファイル一覧を保存", f"ファイル一覧_{time.strftime('%Y%m%d_%H%M%S')}.txt", "テキストファイル (*.txt)")
            if not fn:
                return
            with open(fn, 'w', encoding='utf-8') as fp:
                fp.write("\n".join(file_list))
            QMessageBox.information(self, "成功", "ファイル一覧を保存しました。")
        except Exception as e:
            QMessageBox.critical(self, "エラー", 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]}"

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

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

作成例

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

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

[Desktop Entry]
Type=Application
Name=Usamaru Qt
Comment=Launch Usamaru (PySide6) in venv
Exec=/usr/bin/env bash -lc 'exec /home/kali/python_project/venv/bin/python /home/kali/python_project/PySide6/Usamaru.py >> "$HOME/usamaru.log" 2>&1'
Path=/home/kali/python_project/PySide6
Icon=/home/kali/python_project/PySide6/Resources/Patoka.png
Terminal=false
Categories=Utility;

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

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

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

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

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

ソースファイル

/home/kali/debbuild/usamaru/usr/share/usamaru/main_qt.py

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

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

cp /home/kali/python_project/PySide6/Usamaru.py ~/debbuild/usamaru/usr/share/usamaru/
cp -r /home/kali/python_project/PySide6/Resources ~/debbuild/usamaru/usr/share/usamaru/

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

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

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

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

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

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

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

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

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

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

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

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

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

Wayland について

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

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

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

内容

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

※ビルド手順の 「3) ランチャースクリプト /usr/bin/usamaru を作成」 のところで作られているファイル名と以下は一致が必要

Exec=env QT_QPA_PLATFORM=xcb usamaru

アイコンの配置 処理手順

mkdir -p ~/debbuild/usamaru/usr/share/icons/hicolor/128x128/apps
cp /home/kali/python_project/PySide6/Resources/Patoka.png \
   ~/debbuild/usamaru/usr/share/icons/hicolor/128x128/apps/usamaru.png

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

nano ~/debbuild/usamaru/DEBIAN/control
Package: usamaru
Version: 1.0-offline2
Section: utils
Priority: optional
Architecture: amd64
Maintainer: your <your@gmail.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.

ビルドしたアプリの名前はどこの設定なのか

ビルドしたアプリの名前は、
この .deb パッケージの中では DEBIAN/control ファイルの Package: と Name=(.desktop ファイル) で決まります。

具体的には次の2か所です。

DEBIAN/control の Package: フィールド

Package: usamaru

これはパッケージ名として apt/dpkg で認識される名前です。
インストールやアンインストールで使う名前(例: sudo apt remove usamaru)になります。

usr/share/applications/usamaru.desktop の Name= フィールド

Name=Usamaru Mascot
これはアプリケーションメニューやランチャーに表示される名前です。
ユーザーが目にする名称はこちらです。

つまり、

システム的なパッケージ名 → control ファイルの Package:

メニューやランチャーの表示名 → .desktop ファイルの Name=

という役割分担になっています。
両方を変えると、アプリの見た目の名前と内部パッケージ名の両方を変更できます。

他のアプリとどっちが被るとまずい?

「まずい(不具合や衝突する)被り方」は、次の2種類があります。

  1. control ファイルの Package: 名が被る
    これは dpkg / apt でのパッケージ名 なので、同じ名前が既に存在すると、

インストール時に「置き換え」扱いされて既存アプリが消える

依存関係が壊れる

例:
Package: usamaru が既に別のアプリで使われていたら、sudo apt install ./usamaru.deb するとそれを上書きします。

💡 対策
ユニークな名前にする(例: Package: usamaru-yu や Package: mascot-kirby)
Debian系パッケージでは英小文字+数字+ハイフンが無難。

  1. .desktop ファイルの Name= や Icon= が被る
    Name= が被る
    → アプリメニュー上で区別しづらくなる(衝突してもシステム的には動くが混乱する)。

Icon= が被る
→ 同じ名前のアイコンファイルが /usr/share/icons/hicolor/... にあると、別アプリのアイコンが上書きされる可能性あり。

💡 対策

Name= はユーザーが混乱しないユニークな表記にする(例: 「Kirby Mascot」など)。

Icon= 名もアプリ専用のファイル名にする(例: Icon=kirby_mascot)。

まとめ
絶対に被ってはいけないのは Package: 名(被るとシステム的に上書きされる)。

混乱を避けるためにユニークにすべきなのは Name= と Icon=(表示や見た目の問題)。

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

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

echo "[usamaru] 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 "[usamaru] 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 テーマに usamaru.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 "[usamaru] postinst done" | tee -a "$LOG"
exit 0

保存後に権限を付与

chmod 755 ~/debbuild/usamaru/DEBIAN/postinst

prermを作成する

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

APPDIR="/usr/share/usamaru"
VENV="$APPDIR/.venv"
LOG="/var/log/usamaru_postinst.log"

case "$1" in
  remove|purge)
    # venv の削除(存在すれば)
    if [ -d "$VENV" ]; then
      echo "Removing usamaru 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/usamaru/DEBIAN/prerm

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

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

配布フォルダ作成

mkdir -p ~/配布

8)README.txtの作成

以下のコマンドを実行

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

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

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

以下で一括実行

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

set -e
APPDIR="$(cd "$(dirname "$0")" && pwd)"
DEB="$APPDIR/usamaru.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 "usamaru.deb が見つかりません。配布フォルダ内で実行してください。"
  exit 1
fi

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

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

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

set -e
APPDIR="$(cd "$(dirname "$0")" && pwd)"
DEB="$APPDIR/usamaru.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 "usamaru.deb が見つかりません。配布フォルダ内で実行してください。"
  exit 1
fi

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

保存後に実行権限を追加

chmod +x ~/配布/install_usamaru.sh

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

cp ~/debbuild/usamaru.deb ~/配布/
cd ~
zip -r usamaru_offline.zip 配布

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

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

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

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

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

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

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

以下のコマンドを実行

sudo apt remove usamaru
sudo apt autoremove

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

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

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

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

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

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

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

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

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

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?