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?

exeを作って終わりじゃなかった — Tauri製GUIアプリをMicrosoft Storeに出すまでの振り返り ー上

1
Posted at

Hello World! はじめまして

この投稿を読んでいただきありがとうございます。HelloweenHead's Depotという者です。Qiitaでの初の投稿で至らぬ点も多々あると思われ恐縮ですが、私の失敗点や学びが他の開発者の助けになるかと考えこのブログを書くことに致しました。何卒宜しくお願いします。

今回個人開発したTauri製の執筆アプリがMicrosoft Storeの認定に合格したため、これまでの振り返りの意味も込めてこのブログを書いています。

いくつかの章にまとめて、なるべく技術以外の心情なんかも書いています。。。(というか技術系の説明が浅いかもです。これから精進します。。。)

GitHub URL: TypeNap

序章:焦りに焦る12月

 大学2年の12月。大学の単位もぼちぼち取り終え、そろそろ就職や将来のことを漠然と考え始める時期。それは情報系の学生にとっても、当然例外ではなかった。

 そんな中、周りの技術力を持つ友人たちの話を盗み聞く(ダメだけど)。なんでも、「ポートフォリオ」なるものがあり、自分の作った作品をまとめておくものらしい(簡素な理解)。

 正直に言えば、この日まで「大学に入った!」という事実だけに浮かれ、遊び以外のことはほとんど何も考えていなかった。。。。典型的な駄目学生である。

 焦る。

 あれ、一応このままエンジニアとして社会に出るんだよな?
 これからどうするんだろう? 就活とか? 今のままで良いの? え、どうなの、おい?

 そんな感じで、受験期ぶりの悪寒と汗💦が頬を伝う。

 でも、そんな「ポートフォリオ」なんて話を聞いてしまったら、私も技術者の端くれとして何かソフトウェアを作りたい!!

 ――という、実に単純な発想に至った。(単純脳)


 それが、今回のプロジェクトの発端だった。

 では、何を作るのか。

 あれこれ考えた末、最初に出てきた答えはこうだった。

 CLIなんてちゃちなものじゃなく、GUIを作りたい! かっこいいから!!

 ……理由としては、非常に強い。(更なる単純脳)

 それからChatGPTやGeminiと相談する。私の単純な理想に対する彼らの答えは、だいたいこんな感じだった。

 「結論から言うね♡ それ、凄く良いアイデア!」

 単純な私は、さらに気分よく乗せられていった。

 しかし、ここで最大の問題があった。(迫真)

 当時の私の技術力が、あまりにも貧弱だったのである!!!!!(威風堂々)

 具体的には、授業で触ったCとJavaくらいしか使えず、しかもしばらく書いていなかったせいで、その感覚すらほとんど失っていた。

 一応、有利な点を挙げるなら、その年の秋に応用情報技術者試験には合格していた。

 ただし実態は、ペーパーテストはできるが、手を動かして何かを作ることはできない。豪華な抜け殻のような技術力である。

 そんなわけで、最初に作ると決めたのは、PythonとGUIライブラリのFletを使った簡単なメモ帳アプリだった。

 というのも、ゼミの研究を進めるにあたって近いうちにPythonを使うだろうと思っていたからだ(データサイエンス系なので)。

 そしてもう一つ。

 なんか簡単そうだったからである。

 この期に及んで、まだ舐め切っている。

3月:Flet製プロトタイプ完成、そしてTauriへ

初期のプロトタイプは、案外すんなり完成した。

 大体12月から3月中旬くらいまで。メモ帳として欲しかった機能は、この時点でほぼ一通り揃えることができた。

 主な機能としては、

ライトテーマ / ダークテーマの実装

「ストーリー」と呼ぶディレクトリの生成・削除

ストーリー内に配置する編集可能な .txt ファイルの生成・削除・リネーム

動的に追加・削除・並び替えのできるタブシステム

その他諸々……

 といった具合である。

 なんだ、お前やればできる子じゃん。

 ……と思うかもしれない。

 ただ、実際のところFletというフレームワークの標準機能を使えば、1つ目のテーマ切り替えは比較的簡単に実装できる。2、3についても、Python標準ライブラリの pathlib の扱いさえ覚えてしまえば、それほど難しい処理ではなかった。

 加えて、この実装にあたって僕は Antigravityという無料で使えるAI Agentをかなり酷使した。

 いやね、最低限、生成されたコードは理解していますし、ちゃんと自分で書いた部分もあるんですよ。。。。。(小さい声)

 でも、やっぱり生成AIは便利です。

 この頃はAI Agentをほとんど「お師匠様」にでも接するように扱っていて、

 「この処理はどういう仕組みなのでしょうか」
 「こちらの実装について教えていただけますか」

 などと敬語のプロンプトを打ちながら、和気あいあいと新しい技術を教えてもらっていた。

 ――後半への重大な伏線である。

Flet標準のTabsを使わず、タブシステムを自作する


 ここから少し技術的な話になる。

 このプロトタイプの中で最も実装に苦戦したのは、実は4つ目の

 「動的に追加・削除・並び替えのできるタブシステム」

 だった。

 FletにはタブUIを構築するための Tabs や Tab といったControlが用意されている。現在のFletでも Tabs、TabBar、TabBarView などを組み合わせる形でタブUIを構築できる。

 ところが、当時僕が作りたかったのは、単なる「ページを切り替えるためのタブ」ではない。

 VS Codeやブラウザのように、

 タブを動的に増やして、閉じて、さらにドラッグ&ドロップで好きな順番へ並び替えたい。

 というものだった。

 タブの追加・削除までは標準の仕組みを操作することで実現できたのだが、肝心のドラッグ&ドロップによる並び替えが、当時使っていたタブControlだけでは素直に実現できなかった。

 当時の僕は嫌な顔をしながら、

 「おいおい、Flet制作者もうちょっと頑張ってよぉ……」

 などと思っていた。

 もっとも、Flet自体がFlutterをベースとしたUIフレームワークであり、そもそも私が標準のタブUIに対して想定以上の仕事を要求していただけ、という見方もできる。

 そこで最終的に取った解決策が、

 「じゃあタブそのものを自分で作ればいいじゃん」

 であった。

 具体的には Row、GestureDetector、Draggable、DragTarget などを組み合わせ、タブUIそのものを独自に構築した。Fletの Draggable はドラッグ可能なControlを作るための仕組みで、対応する DragTarget と同じ group を指定することでドロップ処理を受け取れる。

 内部では、画面上に表示されるタブを tabswitch.controls、各タブに対応する本文を contentlist で管理している。

 両者は同じインデックスを共有する。

 たとえば、

tabswitch.controls[0]  <->  contentlist[0]
tabswitch.controls[1]  <->  contentlist[1]
tabswitch.controls[2]  <->  contentlist[2]

 という具合である。

 現在選択中のタブは tab_index に保持し、ユーザーがタブをクリックするとその値を書き換え、対応する contentlist の内容へ表示を切り替える。

 並び替えについては、各タブを Draggable と DragTarget で包むことで実装した。

 タブがドラッグされて別の位置へ移動すると、

tabswitch.controls の順序を変更

対応する contentlist も同じ順序へ変更

現在選択中の tab_index を補正

 という処理を行う。

 つまり、見た目のタブだけを移動するのではなく、その裏にある本文データも一緒に移動させるわけだ。

 タブを閉じる際も同様で、タブ側と本文側の両方から該当要素を削除し、必要であれば登録済みのリスナーも解除する。

 こうして、表示・選択・追加・削除・並び替えまでをまとめて管理する、独自のタブシステムが出来上がった。

 コアとなる部分をかなり簡略化すると、こんな感じである。

draggable = ft.Draggable(
    group="tabs",
    content=ft.GestureDetector(
        content=tab,
        on_tap=self.on_press,
    ),
    content_feedback=ghost_tab,
    on_drag_start=self.on_drag_start,
)

drag_tab = ft.DragTarget(
    group="tabs",
    on_accept=self.drag_accept,
    content=draggable,
)

self.tabswitch.controls.append(drag_tab)

 当然、本体はもっと長い。

 詳しい実装については GitHub .txtProject を参照してほしい。view/tabs.py にタブシステムの基幹部分があり、ファイルの生成・削除などについては utils ディレクトリ以下で処理している。

……Flet、ちょっと物足りなくない?

 ともあれ、こうして最初のプロトタイプは形になった。

 しかし、この頃から次第に一つの疑念が芽生え始める。

 自分のアプリをもっと豪華に作るには、Fletでは少し物足りないのでは?

 今回のタブシステムにしても、標準のタブUIでは足りず、自前で組み始めてしまった。

 そして何より――

 このFletの汎用的なUIデザインに飽きた!!!

 (威風堂々2回目)

 というわけで、僕は再びChatGPTとGeminiを召喚した。

 白雪姫に出てくる魔女が、

 「鏡よ鏡……」

 と問いかけるが如く、

 もっと自由にGUIを作れるフレームワークはないのか!!

 と尋ねる。

 すると例によって、

 「結論から言うね♡ それ……」

 (長いので割愛)

 そんなこんなで、本格的なデスクトップGUIアプリを作る候補として、主に2つの選択肢へ辿り着いた。

 Electronか、Tauriか。

 Electronなら、ペーパーテストだけはできる私の暗記辞書にも載っていた。有名どころではVisual Studio CodeやSlackなどでも採用されている、Web技術を使ってデスクトップアプリを作るためのフレームワークである。

 そして、もう一つが Tauri。

 こちらはフロントエンドにWeb技術を使いつつ、ネイティブ側の処理にRustを利用できることが大きな特徴だった。Electronとはアーキテクチャが異なり、OS側のWebViewを利用することで、比較的軽量なアプリケーションを構築しやすい。

 そして私の判断はこうだった。

 開発環境を一新して、Tauriを使おう。

 その理由は――

 軽量で高速ってええやん……。

 しかもRustて。なんかかっこええやん!!!

 (純粋脳筋)

 こうして3月。

 PythonとFletで作った最初のプロトタイプを置いて、私はTauriによる開発へ踏み込むことになった。

 なお、この時点で私はまだ、

 Reactが何なのかすら、よく分かっていない。

Reactって何? JavaScript? Javaとちゃうんか……?

 Tauriへ移行してからの数週間は、率直に言って地獄だった。

 そう、私はReactだのJavaScriptだの、いわゆるフロントエンドに関する知識がほとんど皆無だったのである。

 非常に恥ずかしい話だが、JavaScriptが本当にJavaとは全く別の言語なのだと確信したのもこの頃だった。それまでも「何か違うらしい」という認識くらいはあったのだが、文法だけでなく、言語としての思想や使われ方までまるで違うとは思っておらず、かなり面食らった。

 とはいえ、JavaScriptそのものの習得には、そこまで時間はかからなかった。

 直前までPythonを触っていたこともあり、動的型付けのスクリプト言語としての感覚は比較的馴染みやすかったからだ。もちろん、JavaScript特有の挙動や非同期処理などは別として、最初のうちはセミコロンや中括弧 {} に慣れ、よく使う標準メソッドを覚えていけば、最低限コードを書くことはできた。

 HTML/CSSについても、授業で基本的な書き方自体は触ったことがあったため、それほど大きな抵抗はなかった。

 問題は、その先である。

 Reactだった。

 TauriはReact専用というわけではなく、VueやSvelteなど別のフロントエンドを使うこともできる。

 しかし私は、

 「Reactってなんか聞いたことあるな」

 という、相変わらず軽率極まりない理由でReactを選んだ。

 そして案の定、苦しんだ。

 中でも特に印象に残っているのが useEffect() である。

 「値が変わったら何か処理をするもの」くらいの理解から入り、依存配列や実行タイミング、副作用という概念に何度も振り回された。

 正直に言えば、今でも厳密に一言で説明しろと言われると少し困る。


 Tauriへ移行した最初のミッションは単純だった。

 Flet版でできていたことを、Tauri版でもできるようにする。

 まずはこれだけである。

 Reactの基本的な書き方や状態管理のルールを覚えながら、ChatGPTやGeminiも酷使し、息も絶え絶えになりつつ移植を進めていった。

 タブシステムについては、Flet版のように単純なリストの添字だけで管理するのではなく、各タブに固有のIDを与え、状態管理ライブラリの zustand を使って管理する形へ変えた。

 ……のだが、これもまた初見ではかなり理解しづらかった。

 「状態を持つ」と言われても、最初の頃の私には、

 どこが状態で、誰がそれを持っていて、いつ更新されるのか

 という部分からして曖昧だったのである。


 そして、もう一つ困ったことがあった。

 Fletとは違い、Reactそのものがボタンやドロップダウン、スライダーといった完成済みのGUI部品を一式用意してくれるわけではない。

 もちろん、それこそが自由にデザインできる理由でもあるのだが、当時の私はそんな事情など知ったことではない。

 「えー! ボタンも自分で作るの!?」

 と普通に不満を漏らしていた。

 だが、文句を言っていてもUIは生えてこない。

 そこで私は、本体とは別にもう一つTauriプロジェクトを立ち上げ、まずは自分用のUIコンポーネントを作ることだけに集中することにした。

 ボタン、スライダー、ドロップダウンメニュー、各種入力UI……。

 一つ一つを見れば取るに足らない部品である。

 しかし、

 こいつらに一体感と遊び心がなければ、GUIとしては輝かない!!

 というのが当時の私の持論だった。

 アニメーションにはMotion系のライブラリも使いながら、挙動や見た目を揃えていった。

 結果として、このUIコンポーネント群の設計だけで、およそ1か月を使った。

 だが、これは無駄ではなかった。

 最初から最高にお洒落なアプリを作ると考えるのではなく、

 「そのための最高にお洒落なボタンを作る」

 という具合に問題を小さく切り分けたことで、Reactのコンポーネント設計やprops、state、イベント処理についての理解がかなり深まったのである。

 この頃の私は、

 「これが分割統治法!!!」

 などと得意げに語っていた。

 多分、少し意味は違う。


バックエンドRust:漠然と憧れる言語

 さて、フロントエンド側の問題は粗方片付いた。

 残る大きな問題は、もう1つの新言語――Rustとどう付き合うかである。

 Rustという言語に対して、私は以前から1つの強烈な偏見を持っていた。

 それは、

 Rust使ってる人って、なんかかっこいいいいい!!!!

 という、特に論拠のないものである。

 おそらくYouTubeかSNSのどこかで影響を受けたのだと思う。

 しかし実際に学び始めてみると、RustはCやC++、Javaなどと表面的に似ている部分こそあれど、かなり独特な設計思想を持った言語だった。

 有名な所有権だけではない。

 借用、ライフタイム、structimpltrait

 さらに Result<T, E>Option<T> といった型を中心に据えたエラー処理や、map_err()and_then() などを使った処理のつなぎ方。(最終的にコードがメソッドの数珠繋ぎみたいになってる。)

 これまで私が知っていた「クラスを作って継承して……」というJava的なオブジェクト指向の感覚だけでは、なかなか綺麗に整理できなかった。

 ここが鬼門だった。

 何なら、今でも分からないことは普通に多い。


 Tauriでは、Rust側の関数に #[tauri::command] を付けることで、フロントエンドから invoke() を使って呼び出せる。

 例えばRust側で、

#[tauri::command]
fn save_file(path: String, content: String) {
    // 保存処理
}

 のようなCommandを定義しておけば、フロントエンド側からは、

import { invoke } from "@tauri-apps/api/core";

await invoke("save_file", {
  path,
  content,
});

 のように呼び出すことができる。

 私はまず、ファイルの生成・書き込み・保存・削除といった処理からRust側へ移していった。

 GUI上の操作はReact。

 実際にOS側のファイルを触る処理はRust。

 少しずつではあるが、この辺りでようやく

 「ああ、Tauriってこうやって使うのか」

 という感覚が掴めてきた。


 ちなみにバックエンドを作る際にもAI Agentはかなり役に立った。

 ただし、ここではフロントエンドの頃と少し事情が違った。

 Rustは、初学者にとってはかなり読みにくい。

 そのためAIに大量のコードを書かせると、

 動いてはいる。しかし、私はこれをレビューできるほど理解しているのか?

 という問題が出てきた。

 理解できないコードが一つ積まれ、それを前提に次のコードが積まれ、さらにその上へまたコードが積まれる。

 こうなると、何のために存在するのか分からない処理が雪だるま式に増えていく危険があった。

 そこでRustについては、最初のうちはなるべく、

 まず自分で書く → AI Agentにレビューさせる

 という逆の使い方をするようにした。

 もちろん、遅くはなる。

 だが、その方が少なくとも、自分のコードが何をしているのかは把握できた。


 そんなことを続けているうちに、どうにかFlet版よりも少し個性の立ったメモ帳アプリが完成していた。

 最初は、

 「GUIを作りたい。かっこいいから」

 というだけだった。

 それがいつの間にか、

 Reactを書き、Rustを書き、自作UIコンポーネントを作り、ファイルシステムを触るアプリになっていた。

 少なくともこの頃には、最初に作ったFlet版とはだいぶ違うものになっていたと思う。

 ここで私は夢を見る。

 これを一般使いできるくらいにまで発展させられるんじゃね? んでもってアプリストアに出せたら最高じゃん!

 という具合に。

 そこからは実装方法だけでなく、そもそも何のためのアプリを作るのかについても考え直すようになった。

 そして出てきた結論が、

 小説執筆アプリにしよう。

 というものだった。

 発想としては単純で、世の中には既に小説を書くためのサービスや高機能なワープロがいくらでもある。ただ、実際に眺めてみると、それぞれ少しずつ私の欲しいものとは違っていた。

 例えば小説執筆専用のサービスは、登場人物、世界観、プロットなどを綺麗に整理できる一方で、アプリ側が「創作とはこう整理するものだ」と型を用意しすぎているようにも感じた。

 逆にWordのような汎用ワープロは自由ではあるが、今度はあまりにも「事務的な文書を作成する道具」であって、物語を書く場所として特別に整備されているわけではない。

 私が欲しかったのは、その中間だった。

 メモ帳のように気軽で、構造を押し付けず、それでいて使い込むほど創作のための道具として深くなっていくもの。

 作品も特定のサービスの中へ閉じ込めるのではなく、基本はローカルに置いて、自分のファイルとして扱えるようにしたい。

 要するに、

 「小説を書く方法」をアプリ側が決めるのではなく、書く人が自分なりの使い方を作れる執筆環境にしたかった。

 こうして、単なるメモ帳として始まったアプリは、少しずつ「物語を書くための場所」へ姿を変えていった。とはいえ、完成した今段階では飽くまでも小説執筆アプリとして何とか使える程度なので、この理想を叶えるには今後も改良を続けていこうと思う。

下へ続く。

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?