[5] Tauri 새 창 및 메뉴 버튼 만들기

2025. 5. 7. 15:04Tauri/Tutorial

기능

tauri 기능
tauri 기능

- open window 버튼을 누르면 새 창 띄우기
- 메뉴 버튼 만들기

프론트엔드

전체 코드

src/index.html

<html>
  <head></head>
  <div>
    <button onclick="openWindow()">Open Window</button>
    <div id="data"></div>
  </div>

  <script>
    const invoke = window.__TAURI__.core.invoke;
    const listen = window.__TAURI__.event.listen;

    async function openWindow() {
      await invoke("open_window");
    }

    function init() {
      listen("data", data => {
        document.querySelector("#data").innerHTML = data.payload;
      });
    }

    init();
    
  </script>
</html>

src/sub_window.html

<html>
  <head></head>
  <div>
    <input type="text" id="data" />
    <button onclick="sendData()">Send Data</button>
  </div>

  <script>
    const invoke = window.__TAURI__.core.invoke;
    
    async function sendData() {
        await invoke("send_data", {
            data : document.querySelector("#data").value
        });
    }
  </script>
</html>

프론트엔드 코드는 이제 베리 이지 하니까 넘어가겠다.
잘 모르겠으면 이전 글을 참고하시길!!

백엔드

전체 코드

src-tauri/src/lib.rs

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use tauri::{
    menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder},
    AppHandle, Manager, Emitter,
};


#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let open_menu = MenuItemBuilder::new("open")
                .id("open")
                .build(app)?;

            let save_menu = MenuItemBuilder::new("save")
                .id("save")
                .build(app)?;

            let file_sub_menu = SubmenuBuilder::new(app,"file")
                .items(&[&open_menu, &save_menu])
                .build()?;

            let menu = MenuBuilder::new(app)
                .items(&[&file_sub_menu])
                .build()?;

            let open_id = open_menu.id().clone();
            app.on_menu_event(move |_, event| {
                if *event.id() == open_id {
                    println!("open menu clicked");
                }
            });

            app.set_menu(menu)?;

            Ok(())
        })
        .invoke_handler(tauri::generate_handler![open_window, send_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

#[tauri::command]
fn open_window(app : AppHandle) {
    std::thread::spawn(move || {

        let option = app.get_webview_window("sub_window");

        if let Some(window) = option {
            let _ = window.show();
            let _ = window.set_focus();
        }
    });
}    

#[tauri::command]
fn send_data(app : AppHandle, data : String) {
    app.emit("data", data).unwrap();
}

src-tauri/tauri.conf.json의 일부

  "app": {
    "withGlobalTauri": true,
    "windows": [
      {
        "title": "rust_tauri",
        "width": 800,
        "height": 600,
        "resizable": true,
        "fullscreen": false
      },
      {
        "title": "rust_tauri",
        "label": "sub_window",
        "url": "sub_window.html",
        "visible": false,
        "width": 800,
        "height": 600,
        "resizable": true,
        "fullscreen": false
      }
    ],
    "security": {
      "csp": null
    }
  },

코드 분석

새 창 띄우기

프론트엔드의 open window 버튼과 invoke 된 rust 함수는 open_window()이다.

#[tauri::command]
fn open_window(app : AppHandle) {
    std::thread::spawn(move || {

        let option = app.get_webview_window("sub_window");

        if let Some(window) = option {
            let _ = window.show();
            let _ = window.set_focus();
        }
    });
}

thread를 분리하여 안정성을 높이고, app.get_webview_window 함수를 이용하여 새 창을 열었다.
새 창의 이름은 sub_window로, tauri.conf.json에 label과 url을 입력하여 연결해 주었다.
(다른 방법도 있으나 다른 글에서 다루도록 하겠다.)

결국 sub_window.html => sub_window(label) => get_webview_window("sub_window"); => open_window()의 흐름이다!

그럼 아래 코드는 어떤 의미일까?

if let Some(window) = option {
            let _ = window.show();
            let _ = window.set_focus();
}

 

 

이미 새 창이 열려있다면 그 창에 포커스 하는 것이다.
tauri.conf.json에서 visible이 false이므로 처음에는 새 창이 안 보이는데, 버튼을 누르면 보이는 것이다.
만약 if문이 없다면 새 창이 열린 상태로 버튼을 누를 때 백엔드에서 오류를 일으키게 된다.

메뉴 버튼 만들기

    tauri::Builder::default()
        .setup(|app| {
            let open_menu = MenuItemBuilder::new("open")
                .id("open")
                .build(app)?;

            let save_menu = MenuItemBuilder::new("save")
                .id("save")
                .build(app)?;

            let file_sub_menu = SubmenuBuilder::new(app,"file")
                .items(&[&open_menu, &save_menu])
                .build()?;

            let menu = MenuBuilder::new(app)
                .items(&[&file_sub_menu])
                .build()?;

            let open_id = open_menu.id().clone();
            app.on_menu_event(move |_, event| {
                if *event.id() == open_id {
                    println!("open menu clicked");
                }
            });

            app.set_menu(menu)?;

            Ok(())
        })

tauri::Builder에 메뉴를 추가하면 된다.
setup 내에 |app|을 통해 app을 클로저로 불러온다.

let open_menu = MenuItemBuilder::new("open")
    .id("open")
    .build(app)?;

let save_menu = MenuItemBuilder::new("save")
    .id("save")
    .build(app)?;

MenuItemBuilder::new() 함수를 통해 메뉴 항목을 만든다.
.build(app)을 통해 실제 MenuItem 객체로 빌드한다.
에러 전파를 위한 ?를 사용한다.

서브메뉴와 최상위 메뉴도 같다.

            let file_sub_menu = SubmenuBuilder::new(app,"file")
                .items(&[&open_menu, &save_menu])
                .build()?;

            let menu = MenuBuilder::new(app)
                .items(&[&file_sub_menu])
                .build()?;

여기서 .items는 조금 복잡한데, 소유권 이동의 영역이다.
배열 전체를 참조하기 위해 [ ] 앞에 &를 붙이고,
개별 객체를 참조하기 위해 내부에 &를 각각 붙인다.

┌───────────────┐   &   ┌───────────────────────────────┐
│ [ &file_sub_menu ]        │──►│  slice: &[ &dyn IsMenuItem ]                                 │
└───────────────┘        └───────────────────────────────┘
         │
         │ &         (trait object coercion)
        ▼
   ┌───────────────┐
   │ &Submenu                    │  ──►  &dyn IsMenuItem
   └───────────────┘

 

  • 내부 & : Submenu → &Submenu → &dyn IsMenuItem
  • 외부 & : [&dyn IsMenuItem] → &[&dyn IsMenuItem]

이런 느낌... 어렵다 ㅎㅎ...

사실 원래 코딩할 때 내부 &는 안 넣었는데, cargo한테 혼나고 넣었다.
컴파일러가 너무 성능이 좋다...

let open_id = open_menu.id().clone();   // "open"
app.on_menu_event(move |_, event| {
    if *event.id() == open_id {
        println!("open menu clicked");
    }
});

 

open 메뉴가 클릭되면 log 출력

open menu clicked 로그가 잘 출력된다.

            app.set_menu(menu)?;

            Ok(())

app.set_menu(menu)?;를 통해 빌드한 메뉴 버튼을 앱에 장착하고,

Ok(())를 통해 모든 초기화가 성공했음을 알린다!