customizing-tauri-windows
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTauri Window Customization
Tauri 窗口自定义
Covers window customization in Tauri v2: custom titlebars, transparent windows, and window menus.
涵盖Tauri v2中的窗口自定义内容:自定义标题栏、透明窗口和窗口菜单。
Configuration Methods
配置方式
- tauri.conf.json - Static configuration at build time
- JavaScript Window API - Runtime modifications from frontend
- Rust Window struct - Runtime modifications from backend
- tauri.conf.json - 构建时的静态配置
- JavaScript Window API - 前端运行时修改
- Rust Window 结构体 - 后端运行时修改
Window Configuration (tauri.conf.json)
窗口配置(tauri.conf.json)
json
{
"app": {
"windows": [{
"title": "My App",
"width": 800,
"height": 600,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"center": true
}]
}
}json
{
"app": {
"windows": [{
"title": "My App",
"width": 800,
"height": 600,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"center": true
}]
}
}Custom Titlebar Implementation
自定义标题栏实现
Step 1: Disable Decorations
步骤1:禁用默认装饰
json
{ "app": { "windows": [{ "decorations": false }] } }json
{ "app": { "windows": [{ "decorations": false }] } }Step 2: Configure Permissions (src-tauri/capabilities/default.json)
步骤2:配置权限(src-tauri/capabilities/default.json)
json
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize"
]
}json
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:window:default",
"core:window:allow-start-dragging",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize"
]
}Step 3: HTML Structure
步骤3:HTML结构
html
<div class="titlebar">
<div class="titlebar-drag" data-tauri-drag-region>
<span class="title">My Application</span>
</div>
<div class="titlebar-controls">
<button id="titlebar-minimize">-</button>
<button id="titlebar-maximize">[]</button>
<button id="titlebar-close">x</button>
</div>
</div>
<main class="content"><!-- App content --></main>html
<div class="titlebar">
<div class="titlebar-drag" data-tauri-drag-region>
<span class="title">My Application</span>
</div>
<div class="titlebar-controls">
<button id="titlebar-minimize">-</button>
<button id="titlebar-maximize">[]</button>
<button id="titlebar-close">x</button>
</div>
</div>
<main class="content"><!-- App content --></main>Step 4: CSS Styling
步骤4:CSS样式
css
.titlebar {
height: 30px;
background: #329ea3;
position: fixed;
top: 0;
left: 0;
right: 0;
display: grid;
grid-template-columns: 1fr auto;
user-select: none;
}
.titlebar-drag {
display: flex;
align-items: center;
padding-left: 12px;
}
.titlebar-controls { display: flex; }
.titlebar-controls button {
width: 46px;
height: 30px;
border: none;
background: transparent;
color: white;
cursor: pointer;
}
.titlebar-controls button:hover { background: rgba(255,255,255,0.1); }
.titlebar-controls button#titlebar-close:hover { background: #e81123; }
.content { margin-top: 30px; padding: 16px; }css
.titlebar {
height: 30px;
background: #329ea3;
position: fixed;
top: 0;
left: 0;
right: 0;
display: grid;
grid-template-columns: 1fr auto;
user-select: none;
}
.titlebar-drag {
display: flex;
align-items: center;
padding-left: 12px;
}
.titlebar-controls { display: flex; }
.titlebar-controls button {
width: 46px;
height: 30px;
border: none;
background: transparent;
color: white;
cursor: pointer;
}
.titlebar-controls button:hover { background: rgba(255,255,255,0.1); }
.titlebar-controls button#titlebar-close:hover { background: #e81123; }
.content { margin-top: 30px; padding: 16px; }Step 5: JavaScript Controls
步骤5:JavaScript控制逻辑
typescript
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
document.getElementById('titlebar-minimize')
?.addEventListener('click', () => appWindow.minimize());
document.getElementById('titlebar-maximize')
?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('titlebar-close')
?.addEventListener('click', () => appWindow.close());typescript
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
document.getElementById('titlebar-minimize')
?.addEventListener('click', () => appWindow.minimize());
document.getElementById('titlebar-maximize')
?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('titlebar-close')
?.addEventListener('click', () => appWindow.close());Drag Region Behavior
拖拽区域行为
The attribute applies only to its element, not children. This preserves button interactivity. Add the attribute to each draggable child if needed.
data-tauri-drag-regiondata-tauri-drag-regionManual Drag with Double-Click Maximize
支持双击最大化的手动拖拽
typescript
document.getElementById('titlebar')?.addEventListener('mousedown', (e) => {
if (e.buttons === 1 && e.target === e.currentTarget) {
e.detail === 2 ? appWindow.toggleMaximize() : appWindow.startDragging();
}
});typescript
document.getElementById('titlebar')?.addEventListener('mousedown', (e) => {
if (e.buttons === 1 && e.target === e.currentTarget) {
e.detail === 2 ? appWindow.toggleMaximize() : appWindow.startDragging();
}
});macOS Transparent Titlebar
macOS 透明标题栏
Cargo.toml
Cargo.toml配置
toml
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.26"toml
[target."cfg(target_os = \"macos\")".dependencies]
cocoa = "0.26"Rust Implementation
Rust 实现
rust
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("Transparent Titlebar Window")
.inner_size(800.0, 600.0);
#[cfg(target_os = "macos")]
let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent);
let window = win_builder.build().unwrap();
#[cfg(target_os = "macos")]
{
use cocoa::appkit::{NSColor, NSWindow};
use cocoa::base::{id, nil};
let ns_window = window.ns_window().unwrap() as id;
unsafe {
let bg_color = NSColor::colorWithRed_green_blue_alpha_(
nil, 50.0/255.0, 158.0/255.0, 163.5/255.0, 1.0
);
ns_window.setBackgroundColor_(bg_color);
}
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application")
}Note: Custom titlebars on macOS lose native features like window snapping. Transparent titlebar preserves these.
rust
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.title("Transparent Titlebar Window")
.inner_size(800.0, 600.0);
#[cfg(target_os = "macos")]
let win_builder = win_builder.title_bar_style(TitleBarStyle::Transparent);
let window = win_builder.build().unwrap();
#[cfg(target_os = "macos")]
{
use cocoa::appkit::{NSColor, NSWindow};
use cocoa::base::{id, nil};
let ns_window = window.ns_window().unwrap() as id;
unsafe {
let bg_color = NSColor::colorWithRed_green_blue_alpha_(
nil, 50.0/255.0, 158.0/255.0, 163.5/255.0, 1.0
);
ns_window.setBackgroundColor_(bg_color);
}
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application")
}注意:macOS上的自定义标题栏会失去窗口吸附等原生特性,而透明标题栏则会保留这些特性。
Window Menus
窗口菜单
Menu Item Types
菜单项类型
| Type | Description |
|---|---|
| Text | Basic labeled menu option |
| Check | Toggleable entry with checked state |
| Separator | Visual divider between sections |
| Icon | Entry with custom icon (Tauri 2.8.0+) |
| 类型 | 描述 |
|---|---|
| 文本 | 基础带标签的菜单项 |
| 复选 | 可切换选中状态的菜单项 |
| 分隔符 | 菜单项之间的视觉分隔线 |
| 图标 | 带自定义图标的菜单项(Tauri 2.8.0+) |
Creating Menus (JavaScript/TypeScript)
创建菜单(JavaScript/TypeScript)
typescript
import { Menu, MenuItem, Submenu, PredefinedMenuItem, CheckMenuItem } from '@tauri-apps/api/menu';
const fileSubmenu = await Submenu.new({
text: 'File',
items: [
await MenuItem.new({
id: 'new', text: 'New', accelerator: 'CmdOrCtrl+N',
action: () => console.log('New')
}),
await MenuItem.new({
id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O',
action: () => console.log('Open')
}),
await MenuItem.new({
id: 'save', text: 'Save', accelerator: 'CmdOrCtrl+S',
action: () => console.log('Save')
}),
{ type: 'Separator' },
await MenuItem.new({
id: 'quit', text: 'Quit', accelerator: 'CmdOrCtrl+Q',
action: () => console.log('Quit')
})
]
});
const editSubmenu = await Submenu.new({
text: 'Edit',
items: [
await PredefinedMenuItem.new({ item: 'Undo' }),
await PredefinedMenuItem.new({ item: 'Redo' }),
await PredefinedMenuItem.new({ item: 'Separator' }),
await PredefinedMenuItem.new({ item: 'Cut' }),
await PredefinedMenuItem.new({ item: 'Copy' }),
await PredefinedMenuItem.new({ item: 'Paste' })
]
});
const viewSubmenu = await Submenu.new({
text: 'View',
items: [
await CheckMenuItem.new({
id: 'sidebar', text: 'Show Sidebar', checked: true,
action: async (item) => console.log('Sidebar:', await item.isChecked())
})
]
});
const menu = await Menu.new({ items: [fileSubmenu, editSubmenu, viewSubmenu] });
await menu.setAsAppMenu();typescript
import { Menu, MenuItem, Submenu, PredefinedMenuItem, CheckMenuItem } from '@tauri-apps/api/menu';
const fileSubmenu = await Submenu.new({
text: 'File',
items: [
await MenuItem.new({
id: 'new', text: 'New', accelerator: 'CmdOrCtrl+N',
action: () => console.log('New')
}),
await MenuItem.new({
id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O',
action: () => console.log('Open')
}),
await MenuItem.new({
id: 'save', text: 'Save', accelerator: 'CmdOrCtrl+S',
action: () => console.log('Save')
}),
{ type: 'Separator' },
await MenuItem.new({
id: 'quit', text: 'Quit', accelerator: 'CmdOrCtrl+Q',
action: () => console.log('Quit')
})
]
});
const editSubmenu = await Submenu.new({
text: 'Edit',
items: [
await PredefinedMenuItem.new({ item: 'Undo' }),
await PredefinedMenuItem.new({ item: 'Redo' }),
await PredefinedMenuItem.new({ item: 'Separator' }),
await PredefinedMenuItem.new({ item: 'Cut' }),
await PredefinedMenuItem.new({ item: 'Copy' }),
await PredefinedMenuItem.new({ item: 'Paste' })
]
});
const viewSubmenu = await Submenu.new({
text: 'View',
items: [
await CheckMenuItem.new({
id: 'sidebar', text: 'Show Sidebar', checked: true,
action: async (item) => console.log('Sidebar:', await item.isChecked())
})
]
});
const menu = await Menu.new({ items: [fileSubmenu, editSubmenu, viewSubmenu] });
await menu.setAsAppMenu();Creating Menus (Rust)
创建菜单(Rust)
rust
use tauri::menu::{MenuBuilder, SubmenuBuilder};
let file_menu = SubmenuBuilder::new(app, "File")
.text("new", "New")
.text("open", "Open")
.text("save", "Save")
.separator()
.text("quit", "Quit")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.build()?;
let menu = MenuBuilder::new(app)
.items(&[&file_menu, &edit_menu])
.build()?;
app.set_menu(menu)?;macOS Note: All menu items must be grouped under submenus. Top-level items are ignored.
rust
use tauri::menu::{MenuBuilder, SubmenuBuilder};
let file_menu = SubmenuBuilder::new(app, "File")
.text("new", "New")
.text("open", "Open")
.text("save", "Save")
.separator()
.text("quit", "Quit")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo()
.redo()
.separator()
.cut()
.copy()
.paste()
.build()?;
let menu = MenuBuilder::new(app)
.items(&[&file_menu, &edit_menu])
.build()?;
app.set_menu(menu)?;macOS 注意事项:所有菜单项必须分组到子菜单下,顶级菜单项会被忽略。
Handling Menu Events (Rust)
处理菜单事件(Rust)
rust
app.on_menu_event(|_app_handle, event| {
match event.id().0.as_str() {
"new" => println!("New file"),
"open" => println!("Open file"),
"save" => println!("Save file"),
"quit" => std::process::exit(0),
_ => {}
}
});rust
app.on_menu_event(|_app_handle, event| {
match event.id().0.as_str() {
"new" => println!("New file"),
"open" => println!("Open file"),
"save" => println!("Save file"),
"quit" => std::process::exit(0),
_ => {}
}
});Dynamic Menu Updates
动态更新菜单
JavaScript:
typescript
const statusItem = await menu.get('status');
if (statusItem) await statusItem.setText('Status: Ready');Rust:
rust
menu.get("status").unwrap().as_menuitem_unchecked().set_text("Status: Ready")?;JavaScript:
typescript
const statusItem = await menu.get('status');
if (statusItem) await statusItem.setText('Status: Ready');Rust:
rust
menu.get("status").unwrap().as_menuitem_unchecked().set_text("Status: Ready")?;Keyboard Shortcuts (Accelerators)
键盘快捷键(快捷键字符串)
| Shortcut | Accelerator String |
|---|---|
| Ctrl+S / Cmd+S | |
| Ctrl+Shift+S | |
| Alt+F4 | |
| F11 | |
| 快捷键 | 快捷键字符串 |
|---|---|
| Ctrl+S / Cmd+S | |
| Ctrl+Shift+S | |
| Alt+F4 | |
| F11 | |
Complete Example
完整示例
main.rs
main.rs
rust
use tauri::menu::{MenuBuilder, SubmenuBuilder};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let file_menu = SubmenuBuilder::new(app, "File")
.text("new", "New")
.text("open", "Open")
.separator()
.text("quit", "Quit")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo().redo().separator().cut().copy().paste()
.build()?;
let menu = MenuBuilder::new(app)
.items(&[&file_menu, &edit_menu])
.build()?;
app.set_menu(menu)?;
Ok(())
})
.on_menu_event(|_app, event| {
match event.id().0.as_str() {
"quit" => std::process::exit(0),
id => println!("Menu event: {}", id),
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application")
}rust
use tauri::menu::{MenuBuilder, SubmenuBuilder};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let file_menu = SubmenuBuilder::new(app, "File")
.text("new", "New")
.text("open", "Open")
.separator()
.text("quit", "Quit")
.build()?;
let edit_menu = SubmenuBuilder::new(app, "Edit")
.undo().redo().separator().cut().copy().paste()
.build()?;
let menu = MenuBuilder::new(app)
.items(&[&file_menu, &edit_menu])
.build()?;
app.set_menu(menu)?;
Ok(())
})
.on_menu_event(|_app, event| {
match event.id().0.as_str() {
"quit" => std::process::exit(0),
id => println!("Menu event: {}", id),
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application")
}React Component
React 组件
tsx
import { useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
function App() {
useEffect(() => {
const appWindow = getCurrentWindow();
const minimize = () => appWindow.minimize();
const maximize = () => appWindow.toggleMaximize();
const close = () => appWindow.close();
document.getElementById('titlebar-minimize')?.addEventListener('click', minimize);
document.getElementById('titlebar-maximize')?.addEventListener('click', maximize);
document.getElementById('titlebar-close')?.addEventListener('click', close);
return () => {
document.getElementById('titlebar-minimize')?.removeEventListener('click', minimize);
document.getElementById('titlebar-maximize')?.removeEventListener('click', maximize);
document.getElementById('titlebar-close')?.removeEventListener('click', close);
};
}, []);
return (
<>
<div className="titlebar">
<div className="titlebar-drag" data-tauri-drag-region>
<span>My Tauri App</span>
</div>
<div className="titlebar-controls">
<button id="titlebar-minimize">-</button>
<button id="titlebar-maximize">[]</button>
<button id="titlebar-close">x</button>
</div>
</div>
<main className="content">
<h1>Welcome to Tauri</h1>
</main>
</>
);
}
export default App;tsx
import { useEffect } from 'react';
import { getCurrentWindow } from '@tauri-apps/api/window';
function App() {
useEffect(() => {
const appWindow = getCurrentWindow();
const minimize = () => appWindow.minimize();
const maximize = () => appWindow.toggleMaximize();
const close = () => appWindow.close();
document.getElementById('titlebar-minimize')?.addEventListener('click', minimize);
document.getElementById('titlebar-maximize')?.addEventListener('click', maximize);
document.getElementById('titlebar-close')?.addEventListener('click', close);
return () => {
document.getElementById('titlebar-minimize')?.removeEventListener('click', minimize);
document.getElementById('titlebar-maximize')?.removeEventListener('click', maximize);
document.getElementById('titlebar-close')?.removeEventListener('click', close);
};
}, []);
return (
<>
<div className="titlebar">
<div className="titlebar-drag" data-tauri-drag-region>
<span>My Tauri App</span>
</div>
<div className="titlebar-controls">
<button id="titlebar-minimize">-</button>
<button id="titlebar-maximize">[]</button>
<button id="titlebar-close">x</button>
</div>
</div>
<main className="content">
<h1>Welcome to Tauri</h1>
</main>
</>
);
}
export default App;