├── .gitattributes ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── icon.png ├── images ├── screen1.png └── screen2.png └── src ├── app.rs ├── dialog.rs ├── main.rs └── tray_icon.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | /.history 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | # MSVC Windows builds of rustc generate these, which store debugging information 15 | *.pdb 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "slint_multi_window" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | slint = "1.2.2" 8 | tray-icon = "0.9.0" 9 | tao = "0.22.2" 10 | image = { version = "0.24.7", default-features = false, features = ["png"]} 11 | anyhow = "1" 12 | ipc-channel = "0.17.0" 13 | serde = { version = "1.0", features = ["derive"] } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jia Ye 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slint多窗口 + Tray Icon + 窗口间通信 2 | 3 | ![截图1](images/screen1.png "截图1") 4 | 5 | ![截图2](images/screen2.png "截图2") 6 | 7 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planet0104/slint_multi_window/9a1a7179a1af216a2566016987a2c1d980d0cd76/icon.png -------------------------------------------------------------------------------- /images/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planet0104/slint_multi_window/9a1a7179a1af216a2566016987a2c1d980d0cd76/images/screen1.png -------------------------------------------------------------------------------- /images/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planet0104/slint_multi_window/9a1a7179a1af216a2566016987a2c1d980d0cd76/images/screen2.png -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use ipc_channel::ipc::IpcOneShotServer; 5 | use serde::{Serialize, Deserialize}; 6 | use crate::open_dialog; 7 | 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub enum IpcMessage{ 10 | LabelMessage(String) 11 | } 12 | 13 | slint::slint!{ 14 | import { Button , HorizontalBox, VerticalBox} from "std-widgets.slint"; 15 | export component App inherits Window { 16 | title: "主窗口"; 17 | width: 320px; 18 | height: 240px; 19 | icon: @image-url("icon.png"); 20 | 21 | callback open-dialog(); 22 | in-out property label-text: "hello world!"; 23 | in-out property ipc-server-name: ""; 24 | 25 | VerticalBox { 26 | Button { 27 | text: "打开对话框"; 28 | width: 100px; 29 | height: 40px; 30 | clicked => { 31 | open-dialog() 32 | } 33 | } 34 | Text { 35 | text: label-text; 36 | color: green; 37 | } 38 | } 39 | } 40 | } 41 | 42 | pub fn main() -> Result<()>{ 43 | let app = App::new()?; 44 | 45 | //接收其他窗口传递的消息 46 | start_ipc_server(&app)?; 47 | 48 | let app_clone = app.as_weak(); 49 | app.on_open_dialog(move ||{ 50 | let app = app_clone.unwrap(); 51 | let pos = app.window().position(); 52 | let _ = open_dialog(app.get_ipc_server_name().to_string(), pos.x, pos.y); 53 | }); 54 | 55 | app.run()?; 56 | Ok(()) 57 | } 58 | 59 | fn start_ipc_server(app: &App) -> Result<()>{ 60 | let (server, name):(IpcOneShotServer, String) = IpcOneShotServer::new()?; 61 | app.set_ipc_server_name(name.into()); 62 | let app_clone = app.as_weak(); 63 | std::thread::spawn(move ||{ 64 | let (rx, mut data) = server.accept().unwrap(); 65 | loop{ 66 | match &data { 67 | IpcMessage::LabelMessage(label_text) => { 68 | let label_text = label_text.to_string(); 69 | let app_clone = app_clone.clone(); 70 | slint::invoke_from_event_loop(move || app_clone.unwrap().set_label_text(label_text.into())).unwrap(); 71 | }, 72 | } 73 | if let Ok(d) = rx.try_recv(){ 74 | data = d; 75 | } 76 | std::thread::sleep(Duration::from_millis(50)); 77 | } 78 | }); 79 | Ok(()) 80 | } -------------------------------------------------------------------------------- /src/dialog.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use ipc_channel::ipc::IpcSender; 3 | use slint::PhysicalPosition; 4 | 5 | use crate::app::IpcMessage; 6 | 7 | slint::slint!{ 8 | import { Button , HorizontalBox, VerticalBox} from "std-widgets.slint"; 9 | export component Dialog inherits Window { 10 | title: "对话框"; 11 | width: 300px; 12 | height: 200px; 13 | icon: @image-url("icon.png"); 14 | 15 | callback close-dialog(); 16 | 17 | VerticalBox { 18 | Text { 19 | text: "对话框"; 20 | color: green; 21 | } 22 | Button { 23 | text: "返回数据并关闭"; 24 | width: 120px; 25 | height: 40px; 26 | clicked => { 27 | close-dialog() 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | pub fn main(ipc_server_name:String, x: i32, y:i32) -> Result<()>{ 35 | let dialog = Dialog::new()?; 36 | 37 | dialog.window().set_position(PhysicalPosition::new(x, y)); 38 | 39 | let dialog_handle = dialog.as_weak(); 40 | dialog.on_close_dialog(move ||{ 41 | let server_name = ipc_server_name.clone(); 42 | //给主窗口返回数据 43 | let tx: IpcSender = IpcSender::connect(server_name).unwrap(); 44 | tx.send(IpcMessage::LabelMessage("数据更新成功!".to_string())).unwrap(); 45 | dialog_handle.unwrap().hide().unwrap(); 46 | }); 47 | 48 | dialog.run()?; 49 | Ok(()) 50 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | use std::process::Command; 4 | use anyhow::Result; 5 | 6 | mod app; 7 | mod dialog; 8 | mod tray_icon; 9 | 10 | pub const ARGS_APP:&str = "app"; 11 | pub const ARGS_DIALOG:&str = "dialog"; 12 | 13 | fn main() -> Result<()> { 14 | let args: Vec = std::env::args().collect(); 15 | if args.len() > 1{ 16 | let arg1 = args[1].to_lowercase(); 17 | if arg1.starts_with(ARGS_APP) { 18 | //打开App 19 | return app::main(); 20 | }else if arg1.starts_with(ARGS_DIALOG){ 21 | let ipc_server_name = args[4].to_string(); 22 | //打开对话框 23 | return dialog::main(ipc_server_name, args[2].parse()?, args[3].parse()?); 24 | } 25 | } 26 | 27 | //打开app 28 | open_app(); 29 | 30 | //打开图标 31 | tray_icon::main() 32 | } 33 | 34 | pub fn open_app(){ 35 | let _ = start_process(vec![ARGS_APP.to_string()]); 36 | } 37 | 38 | pub fn open_dialog(ipc_server_name: String, x: i32, y:i32){ 39 | let _ = start_process(vec![ARGS_DIALOG.to_string(), format!("{x}"), format!("{y}"), ipc_server_name]); 40 | } 41 | 42 | fn start_process(command_args: Vec) -> Result<()>{ 43 | // 获取当前可执行文件的路径 44 | let current_exe = std::env::current_exe()?; 45 | 46 | // 启动新进程并传递命令行参数 47 | Command::new(current_exe) 48 | .args(&command_args) 49 | .spawn()?; 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /src/tray_icon.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use tao::event_loop::{EventLoopBuilder, ControlFlow}; 4 | use tray_icon::{menu::{Menu, MenuItem, MenuEvent}, TrayIconBuilder, Icon, TrayIconEvent, ClickType}; 5 | use anyhow::Result; 6 | 7 | use crate::open_app; 8 | 9 | const ICON: &[u8] = include_bytes!("../icon.png"); 10 | 11 | pub fn main() -> Result<()>{ 12 | let icon = load_icon()?; 13 | 14 | let event_loop = EventLoopBuilder::new().build(); 15 | 16 | let menu = Menu::new(); 17 | menu.append(&MenuItem::new("打开", true, None))?; 18 | menu.append(&MenuItem::new("退出", true, None))?; 19 | 20 | let _tray_icon = Some( 21 | TrayIconBuilder::new() 22 | .with_menu(Box::new(menu)) 23 | .with_tooltip("多窗口") 24 | .with_icon(icon) 25 | .build()?, 26 | ); 27 | 28 | let menu_channel = MenuEvent::receiver(); 29 | let tray_channel = TrayIconEvent::receiver(); 30 | 31 | let event_loop_proxy = event_loop.create_proxy(); 32 | std::thread::spawn(move || { 33 | loop { 34 | event_loop_proxy.send_event(()).ok(); 35 | std::thread::sleep(Duration::from_millis(50)); 36 | } 37 | }); 38 | 39 | event_loop.run(move |_event, _, control_flow| { 40 | *control_flow = ControlFlow::Wait; 41 | 42 | if let Ok(MenuEvent { id }) = menu_channel.try_recv() { 43 | if id.0 == "1001"{ 44 | //打开app 45 | open_app(); 46 | }else{ 47 | //退出 48 | *control_flow = ControlFlow::Exit; 49 | } 50 | } 51 | 52 | if let Ok(TrayIconEvent {click_type, id: _, x: _, y: _, icon_rect: _ }) = tray_channel.try_recv(){ 53 | if let ClickType::Left = click_type{ 54 | //打开app 55 | open_app(); 56 | } 57 | } 58 | }); 59 | } 60 | 61 | fn load_icon() -> Result{ 62 | let (icon_rgba, icon_width, icon_height) = { 63 | let image = image::load_from_memory_with_format(ICON, image::ImageFormat::Png)?.into_rgba8(); 64 | let (width, height) = image.dimensions(); 65 | let rgba = image.into_raw(); 66 | (rgba, width, height) 67 | }; 68 | Ok(tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height)?) 69 | } --------------------------------------------------------------------------------