ウィンドウをドラッグ移動できる領域を作る

data-tauri-drag-region 属性と権限 core:window:allow-start-dragging で、自作タイトルバーをドラッグで動かせるようにする。ボタンの除外と startDragging() の使い方も示す。

ウィンドウ 対象: Tauri 2.x 更新日: 読了目安: 約6分 win-006
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 属性方式(推奨)
  4. startDragging() を JS から呼ぶ方式
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

OS 標準のタイトルバーを消して独自ヘッダーを描くと、そのままではウィンドウを掴んで動かせません。Tauri では HTML 要素に data-tauri-drag-region 属性を付けるだけで、その要素上でのマウス押下 + 移動が OS ネイティブのウィンドウ移動になります。JS からタイミングを制御したいときは getCurrentWindow().startDragging() を直接呼べます。ここでは decorations: false の自作タイトルバーを題材に、ボタンをドラッグ対象から外す方法とダブルクリック最大化の挙動まで扱います。

前提条件

プラグインは不要です。core:window:allow-start-dragging は core:window:default に含まれていないので src-tauri/capabilities/default.json に追加します。最小化・最大化・閉じるボタンも自作するなら、それらの権限も入れます。

{
  "permissions": [
    "core:default",
    "core:window:allow-start-dragging",
    "core:window:allow-minimize",
    "core:window:allow-toggle-maximize",
    "core:window:allow-close"
  ]
}

標準タイトルバーは tauri.conf.json の app.windows[] で消します。

{
  "app": {
    "windows": [
      { "label": "main", "decorations": false, "width": 900, "height": 600 }
    ]
  }
}

1. フロントエンドから実装する (TypeScript)

属性方式(推奨)

data-tauri-drag-region は属性を付けた要素そのものにしか効き、子要素には継承されません。タイトル文字を <span> に入れるとその上では掴めなくなる一方、ボタンには属性を付けないだけで「ボタンはクリック、それ以外はドラッグ」と分離できます。

<header data-tauri-drag-region class="titlebar">
  <span data-tauri-drag-region class="title">My App</span>
  <div class="controls">
    <button id="min">&#x2013;</button>
    <button id="max">&#x25A1;</button>
    <button id="close">&#x2715;</button>
  </div>
</header>
.titlebar {
  height: 32px; display: flex; align-items: center; justify-content: space-between;
  padding-left: 12px; background: #2b2b2b; color: #eee;
  user-select: none;   /* ドラッグ中の文字選択を防ぐ */
}
.controls button { width: 46px; height: 32px; border: 0; background: transparent; color: inherit; }
.controls button:hover { background: #444; }
import { getCurrentWindow } from '@tauri-apps/api/window';

const win = getCurrentWindow();
document.getElementById('min')!.addEventListener('click', () => win.minimize());
document.getElementById('max')!.addEventListener('click', () => win.toggleMaximize());
document.getElementById('close')!.addEventListener('click', () => win.close());

属性を付けた領域をダブルクリックすると最大化がトグルします。core:window:default に含まれる core:window:allow-internal-toggle-maximize が使われるので追加権限は不要です。resizable: false のウィンドウでは何も起きません。

startDragging() を JS から呼ぶ方式

属性が使えない要素(Canvas や、フレームワークが属性を書き換えるコンポーネント)や「Alt を押しながらならどこでもドラッグ」のような条件付きには、pointerdown で startDragging() を呼びます。左ボタン判定や修飾キー判定は自前で行います。

import { getCurrentWindow } from '@tauri-apps/api/window';

const win = getCurrentWindow();
document.getElementById('board')!.addEventListener('pointerdown', async (e) => {
  if (e.button !== 0 || !e.altKey) return;   // Alt + 左ボタンのときだけ
  e.preventDefault();
  if (e.detail === 2) await win.toggleMaximize();   // ダブルクリック相当
  else await win.startDragging();
});

startDragging() はドラッグを開始するだけで、以降の移動と離す操作は OS が処理します。

2. バックエンドから実装する (Rust)

Rust 側には Window::start_dragging() があります。マウス押下は Webview 内で起きるので実用上は JS から呼びますが、コマンド経由にすると ACL の権限追加なしで動かせます。

#[tauri::command]
fn begin_drag(window: tauri::Window) -> Result<(), String> {
    window.start_dragging().map_err(|e| e.to_string())
}
// invoke_handler に begin_drag を登録
import { invoke } from '@tauri-apps/api/core';

const el = document.querySelector<HTMLElement>('.titlebar')!; // 上の HTML のタイトルバー
el.addEventListener('pointerdown', (e) => { if (e.button === 0) invoke('begin_drag'); });

動作確認

npm run tauri dev で起動すると、タイトルバーのないウィンドウが出ます。ヘッダーの暗い帯を掴んで動かせること、3 つのボタンが最小化・最大化・閉じるとして働くこと、帯のダブルクリックで最大化がトグルすることを確認してください。span.title から属性を外すと、文字の上だけ掴めなくなるのが分かります。

よくあるエラーと対処法

  • 動かず、コンソールに window.start_dragging not allowed. Permissions associated with this command: core:window:allow-start-dragging という趣旨のエラー: 権限未追加です。追加後は tauri dev を再起動します(capabilities はホットリロードされません)。
  • タイトル文字やアイコンの上だけドラッグできない: 子要素に継承されないためです。掴ませたい子要素すべてに data-tauri-drag-region を付けます。
  • ボタンをクリックするとウィンドウが動く: ボタン側にも属性が付いているか、React などの {...props} 展開で属性が伝播しています。ボタンとその祖先(div.controls まで)から属性を外します。
  • Electron の -webkit-app-region: drag が効かない: Tauri はこの CSS を解釈しません。属性方式に置き換えます。
  • startDragging() を click で呼んでいる: click はボタンを離した後に発火します。pointerdown / mousedown で呼びます。

OS ごとの違いと注意点

  • macOS: decorations: false にすると信号機ボタンも消えます。信号機を残したいなら decorations: true のまま "titleBarStyle": "Overlay" と "hiddenTitle": true にし、ヘッダー左端に約 70px の余白を取って重なりを避けます。
  • Windows: 自作ボタンでは Windows 11 のスナップレイアウト(最大化ボタンのホバーメニュー)は出ません。影と角丸は shadow 設定(win-014)に依存します。
  • Linux: X11 では属性方式・startDragging() とも動作します。Wayland ではコンポジターが移動を担当するため環境で挙動が異なります。
  • 属性を付けた領域内の <input> は、属性を直接付けない限り通常どおり入力できます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する