
2025/12/17 0:41
Writing a blatant Telegram clone using Qt, QML and Rust. And C++
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
著者は、UIにQMLを使用し、コアロジックにはRustを採用したTelegram風メッセンジャー「Provoke」を構築しました。これにより高速ビルドとライブリロード機能が実現されました。
cxx-qtからqmetaobject-rsへ切り替えることでコンパイル時間が短縮され、カスタム HotReload ウォッチャーはファイル変更時にアプリを再起動します。ビルドシステムはCMakeとRustの cmake クレートを利用し、手作業で構築したC++モジュールをコンパイルし、qmlRegisterSingletonType を通じて QML へ ProvokeTools として公開します。
QML のシステムトレイアイコン(
Qt.labs.platform から)は、ShaderEffectSource 経由で QML イメージを描画し、動的バッジカウントを表示します。UI コンポーネントは Telegram の外観を模倣しており、折りたたみ可能なサイドバー、カスタムスプリッター、アニメーション付き絵文字リアクションポップアップ、およびテール付きメッセージバブルが NumberAnimation で同期されます。
著者は VS Code における QML 言語サーバーの設定や Qt の実験的モジュールへの対応といった課題を指摘しています。今後の作業ではアニメーションの洗練とコンポーネントの磨きを目指し、Provoke を「Telegram とほぼ区別できない」レベルに仕上げることを計画しています。これにより、Rust がモダンなチャットアプリを動かすことが可能であり、QML/C++ を活用してリッチなインターフェースを実現できることを示しています。
本文
このプロジェクトは数日間の楽しい試作でしたが、結局今は置いておくことにしました。なぜなら、すでに取り組んでいた別の仕事を継続したいからです。以下では私の進捗状況をご紹介します。
ネタバレ注意: ここまでしか進められませんでした。
意見まとめ
10年前にQt、とくにQMLを使ったときのことが今でも鮮明に覚えています。デザインを思い浮かべてすぐに実装できる点が魅力的でした。
それゆえ、Webアプリを作るためにまだベータ版だったSvelteを選びました。QMLに最も近い感覚があったからです。
Rustは本当に素晴らしい言語です。2012年にコンパイル時にポインタ使用を検証し、実行時のオーバーヘッドを増やさない例を見たときから好きになりました。
私は計画とは裏腹にフルタイムでFull‑Stack Web開発者となってしまいましたが、それでも UI を作ることや難しい問題を解決することは好きです。本当に「ネイティブ」なアプリを作りたいと考えています。
Telegram はその UI の洗練さからお気に入りです。「Saved Messages」で10年分のチャットを閲覧できます。
Android 上で動く Matrix クライアント Element も悪くありませんが、デスクトップ版は Telegram に比べて喜びに欠けます。Element X は Rust の
matrix-rust-sdk ライブラリを利用しているので、将来的には役立つかもしれません。
プロジェクト:QML + Rust で作る Telegram クローン
Day One – Hour Zero
Rust アプリの UI に QML を使う試みとして、以下を調査しました:
- qmetaobject‑rs
- cxx‑qt
どちらも選択肢ですが、
cxx-qt は「公式」らしさがありますがコード生成によりビルドが遅くなります。まず cxx‑qt でシンプルな「ウィンドウを開く」プログラムを書きました。それにワクしたのでコミットしました。
ホットリロード
cxx-qt が高価な再コンパイルを避けられるよう試行錯誤。VS Code はターミナルと違う cargo check を実行し、毎回キャッシュが消えてしまいました。そこで
qmetaobject‑rs に切り替え、ビルドは速く、QML も即座に動きました。ただしホットリロードを欲しかったので以下のような構成を試しました。
Rust 側実装例(簡略化)
let (tx, rx) = std::sync::mpsc::channel(); let mut watcher = notify::recommended_watcher(tx).unwrap(); watcher.configure(notify::Config::default().with_compare_contents(true)).unwrap(); watcher.watch(Path::new(qml_folder), notify::RecursiveMode::Recursive).unwrap(); thread_dirty_state.clone(); std::thread::spawn(move || { while let Ok(change) = rx.recv() { if let notify::EventKind::Modify(_) = change.kind { thread_dirty_state.store(true, std::sync::atomic::Ordering::SeqCst); } } });
イベントループ側:
loop { hot_reload_state.store(false, Ordering::SeqCst); let mut engine = QmlEngine::new(); engine.set_property("HotReload".into(), hot_reload.pinned().into()); engine.load_file(format!("{qml_folder}main.qml").into()); engine.exec(); if !hot_reload_state.load(Ordering::SeqCst) { break; } }
QML の
HotReload オブジェクトがクリーンアップしてループを再起動します。また、VS Code で QML Language Server を稼働させるのに少し手間はかかったものの、Qt Creator は QML 編集には依然として優秀です。
Hour Five – Telegram クローン作り始め
スプリッター
Telegram のサイドバーは折りたたみモード(65 px)と通常モード(260 px)の二つがあり、右側の最小幅は 380 px。組み込みスプリッターに機能不足を感じ、独自実装しました。
- マウスカーソルを正しく表示
- カスタムサイズロジックをサポート
アクセシビリティも念頭に置いてプロトタイピングしました。
UI 実験
を使って Material Design の「伸びる円」アニメーションを再実装OpacityMask- 送信者別に尾アイコン付きのチャットバブル作成
- Telegram に似た絵文字リアクションポップアップ(行列、スクロールビュー拡張、検索ボックス)
- メッセージ選択と数値アニメーションを駆使した華麗なトランジション
property real expandness: expand ? 1 : 0 Behavior on expandness { NumberAnimation { duration: 500; easing.type: Easing.InOutQuad } } opacity: 1 - expandness height: lerp(0, topSectionContent.height, expandness) radius: lerp(barHeight / 2, 5, expandness)
システムトレイ
のQt.labs.platform
を実験的に使用SystemTrayIcon- メッセージカウントバッジは
で描画した画像を取得し、ShaderEffectSource
に設定icon.source - アプリアイコンが他と被らないよう独自メガホンアイコンを作成
Rust + C++ の統合
Qt の機能を qmetaobject‑rs で露出できないものにアクセスしたいので、最小限の C++ ラッパーを追加しました。
CMakeLists.txt
cmake_minimum_required(VERSION 3.16) project(provokecpp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt6 COMPONENTS Core Gui Qml REQUIRED) qt_add_library(provokecpp STATIC provokecpp.cpp provokecpp.hpp) target_link_libraries(provokecpp Qt6::Core Qt6::Gui Qt6::Qml)
C++ ソース (provokecpp.cpp)
QObject* provoke_tools_singleton_provider(QQmlEngine*, QJSEngine*) { return new ProvokeTools(); } extern "C" void register_provoke_qml_types() { qmlRegisterSingletonType<ProvokeTools>("provoke", 1, 0, "ProvokeTools", provoke_tools_singleton_provider); qmlRegisterType<OverrideMouseCursor>("provoke", 1, 0, "OverrideMouseCursor"); }
Rust ビルドスクリプト (build.rs)
fn main() { let dest = cmake::Config::new("cpp") .build_target("all") .build(); println!("cargo::rerun-if-changed=cpp/CMakeLists.txt"); println!("cargo::rerun-if-changed=cpp/provokecpp.cpp"); println!("cargo::rerun-if-changed=cpp/provokecpp.hpp"); println!("cargo::rustc-link-search=native={}/build", dest.display()); println!("cargo::rustc-link-lib=static=provokecpp"); }
Rust 側使用例
extern "C" { fn register_provoke_qml_types(); } fn main() { unsafe { register_provoke_qml_types(); } // ... アプリの残り }
この構成ならビルドはキャッシュのおかげで即時に完了し、Rust が制御を保ちながら C++ Qt タイプを公開できます。
結果
プロトタイプは Telegram に驚くほど似ており、スプリットビュー、チャットバブル、絵文字リアクション、システムトレイアイコン付きバッジ、滑らかなアニメーションが実装済みです。実際のアプリではなくても、まるで本物を見ているような錯覚にびっくりしています。
リポジトリ: (リンク)
次なるステップと疑問
- これが人気メッセンジャーになるか?
- 継続開発するのか、それともゲームエンジンや CAD といった別プロジェクトに戻るのか?
- 今後もブログ記事を投稿するか?
次回の App Dev By That Guy! をお楽しみに!