1.可以用iced框架,star比較多,而且在快速發展中,源碼跨平台;
2.在main方法文件最上面加上:#![windows_subsystem = "windows"],這樣Windows平台運行編譯好的gui程序時就不會彈出控制台框;
對於Mac OS,Linux可以自己寫個圖標配置(Linux是.desktop文件)就可以直接雙擊圖標來打開程序而非必須先開一個控制台,所以這兩個平台沒有這個全局宏。
3.iced使用示例:根據官網示例寫一個加減的按鈕和顯示:
Cargo.toml:(這個配置兼容性不好,換成下面代碼塊里的)
[package] name = "demo-iced" version = "0.1.0" authors = ["silentdoer <1010993610@qq.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] iced = { git = "https://github.com/hecrj/iced", features = ["async-std", "debug"] } # debug考慮不要
iced_web = "0.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1" directories = "2" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Window", "Storage"] } wasm-timer = "0.2" [package.metadata.deb] assets = [ ["target/release/todos", "usr/bin/iced-todos", "755"], ["iced-todos.desktop", "usr/share/applications/", "644"], ]
上面的Cargo.toml配置有幾個問題,第一是必需是vs2017或以上,第二就是debug模式編譯的程序運行有問題,第三個就是需要編譯的模塊很多,第四個是win10編譯出的release程序(debug是win10都運行有問題)win10可以用,但是win7運行失敗(經過測試win7編譯出的win10可以運行,但是win10編譯出的在win7上仍然運行失敗(2017,2015,gnu三個都失敗了,等會再換2013和2012【2012直接編譯失敗,哪怕是OpenGL的】的C++如果還失敗那只能在win7上編譯Windows全平台了【或者測試下Ubuntu跨平台編譯的gui程序是否可以同時在win10和win7上運行】));(當然也有好處,上面的配置用的GUI后端是DX或vulkan,而接下來的配置用的是OpenGL效率沒上面的高【注意,不需要安裝OpenGL,win10和win7里都有】)
【這種方式編譯出來的程序也有問題,就是它放到一個沒有安裝過C++環境的系統里運行不了。。。坑啊,目前沒有比較好的解決方案,暫時還是用第一種算了,然后win10和win7都以release編譯一下(剛才又查了下說也可能是顯卡驅動的原因。。)】
[package] name = "demo-iced" version = "0.1.0" authors = ["silentdoer <1010993610@qq.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] iced = { git = "https://github.com/hecrj/iced", default-features = false, features = ["async-std", "debug", "glow"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] async-std = "1" directories = "2" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["Window", "Storage"] } wasm-timer = "0.2" [package.metadata.deb] assets = [ ["target/release/todos", "usr/bin/iced-todos", "755"], ["iced-todos.desktop", "usr/share/applications/", "644"], ]
在項目根目錄添加fonts目錄,里面將Windows的simkai.ttf字體復制進去
main.rs:
#![windows_subsystem = "windows"] use iced::{ button, scrollable, text_input, Align, Application, Button, Checkbox, Column, Command, Container, Element, Font, HorizontalAlignment, Length, Row, Scrollable, Settings, Text, TextInput, }; use serde::{Deserialize, Serialize}; pub fn main() { Todos::run(Settings { default_font: Some(include_bytes!("../fonts/simkai.ttf")), ..Settings::default() }) } #[derive(Debug)] enum Todos { Loading, Loaded(State), } #[derive(Debug, Default)] struct State { scroll: scrollable::State, input: text_input::State, input_value: String, filter: Filter, tasks: Vec<Task>, controls: Controls, dirty: bool, saving: bool, } #[derive(Debug, Clone)] enum Message { Loaded(Result<SavedState, LoadError>), Saved(Result<(), SaveError>), InputChanged(String), CreateTask, FilterChanged(Filter), TaskMessage(usize, TaskMessage), } impl Application for Todos { type Executor = iced::executor::Default; type Message = Message; type Flags = (); fn new(_flags: ()) -> (Todos, Command<Message>) { ( Todos::Loading, Command::perform(SavedState::load(), Message::Loaded), ) } fn title(&self) -> String { let dirty = match self { Todos::Loading => false, Todos::Loaded(state) => state.dirty, }; format!("Todos{} - Iced", if dirty { "*" } else { "" }) } fn update(&mut self, message: Message) -> Command<Message> { match self { Todos::Loading => { match message { Message::Loaded(Ok(state)) => { *self = Todos::Loaded(State { input_value: state.input_value, filter: state.filter, tasks: state.tasks, ..State::default() }); } Message::Loaded(Err(_)) => { *self = Todos::Loaded(State::default()); } _ => {} } Command::none() } Todos::Loaded(state) => { let mut saved = false; match message { Message::InputChanged(value) => { state.input_value = value; } Message::CreateTask => { if !state.input_value.is_empty() { state .tasks .push(Task::new(state.input_value.clone())); state.input_value.clear(); } } Message::FilterChanged(filter) => { state.filter = filter; } Message::TaskMessage(i, TaskMessage::Delete) => { state.tasks.remove(i); } Message::TaskMessage(i, task_message) => { if let Some(task) = state.tasks.get_mut(i) { task.update(task_message); } } Message::Saved(_) => { state.saving = false; saved = true; } _ => {} } if !saved { state.dirty = true; } if state.dirty && !state.saving { state.dirty = false; state.saving = true; Command::perform( SavedState { input_value: state.input_value.clone(), filter: state.filter, tasks: state.tasks.clone(), } .save(), Message::Saved, ) } else { Command::none() } } } } fn view(&mut self) -> Element<Message> { match self { Todos::Loading => loading_message(), Todos::Loaded(State { scroll, input, input_value, filter, tasks, controls, .. }) => { let title = Text::new("todos") .width(Length::Fill) .size(100) .color([0.5, 0.5, 0.5]) .horizontal_alignment(HorizontalAlignment::Center); let input = TextInput::new( input, "What needs to be done?", input_value, Message::InputChanged, ) .padding(15) .size(30) .on_submit(Message::CreateTask); let controls = controls.view(&tasks, *filter); let filtered_tasks = tasks.iter().filter(|task| filter.matches(task)); let tasks: Element<_> = if filtered_tasks.count() > 0 { tasks .iter_mut() .enumerate() .filter(|(_, task)| filter.matches(task)) .fold(Column::new().spacing(20), |column, (i, task)| { column.push(task.view().map(move |message| { Message::TaskMessage(i, message) })) }) .into() } else { empty_message(match filter { Filter::All => "You have not created a task yet...", Filter::Active => "All your tasks are done! :D", Filter::Completed => { "You have not completed a task yet..." } }) }; let content = Column::new() .max_width(800) .spacing(20) .push(title) .push(input) .push(controls) .push(tasks); Scrollable::new(scroll) .padding(40) .push( Container::new(content).width(Length::Fill).center_x(), ) .into() } } } } #[derive(Debug, Clone, Serialize, Deserialize)] struct Task { description: String, completed: bool, #[serde(skip)] state: TaskState, } #[derive(Debug, Clone)] pub enum TaskState { Idle { edit_button: button::State, }, Editing { text_input: text_input::State, delete_button: button::State, }, } impl Default for TaskState { fn default() -> Self { TaskState::Idle { edit_button: button::State::new(), } } } #[derive(Debug, Clone)] pub enum TaskMessage { Completed(bool), Edit, DescriptionEdited(String), FinishEdition, Delete, } impl Task { fn new(description: String) -> Self { Task { description, completed: false, state: TaskState::Idle { edit_button: button::State::new(), }, } } fn update(&mut self, message: TaskMessage) { match message { TaskMessage::Completed(completed) => { self.completed = completed; } TaskMessage::Edit => { self.state = TaskState::Editing { text_input: text_input::State::focused(), delete_button: button::State::new(), }; } TaskMessage::DescriptionEdited(new_description) => { self.description = new_description; } TaskMessage::FinishEdition => { if !self.description.is_empty() { self.state = TaskState::Idle { edit_button: button::State::new(), } } } TaskMessage::Delete => {} } } fn view(&mut self) -> Element<TaskMessage> { match &mut self.state { // 重要 TaskState::Idle { edit_button } => { let checkbox = Checkbox::new( self.completed, &format!("{}{}", self.description, "***").to_string(), TaskMessage::Completed, ) .width(Length::Fill); Row::new() .spacing(20) .align_items(Align::Center) // 空閑狀態時,左側一個checkbox,右側的button樣式是Icon .push(checkbox) .push( // edit_button是state數據,edit_icon()返回的是一個Unicode字符作為顯示字符(瀏覽器上不一致應該是fonts的原因) Button::new(edit_button, edit_icon()) .on_press(TaskMessage::Edit) .padding(10) .style(style::Button::Icon), ) .into() } // 當變為Editing狀態時的view變化(要變成紅色Delete) TaskState::Editing { text_input, delete_button, } => { let text_input = TextInput::new( text_input, "Describe your task...", &self.description, TaskMessage::DescriptionEdited, ) .on_submit(TaskMessage::FinishEdition) .padding(10); Row::new() .spacing(20) .align_items(Align::Center) // editing狀態下左側沒有了checkbox .push(text_input) .push( // delete_button是state數據 Button::new( delete_button, Row::new() .spacing(10) .push(delete_icon()) .push(Text::new("Delete")), ) .on_press(TaskMessage::Delete) .padding(10) // 可以看到Editing狀態時的button的樣式是這個 .style(style::Button::Destructive), ) .into() } } } } #[derive(Debug, Default, Clone)] pub struct Controls { all_button: button::State, active_button: button::State, completed_button: button::State, } impl Controls { fn view(&mut self, tasks: &[Task], current_filter: Filter) -> Row<Message> { let Controls { all_button, active_button, completed_button, } = self; let tasks_left = tasks.iter().filter(|task| !task.completed).count(); let filter_button = |state, label, filter, current_filter| { let label = Text::new(label).size(16); let button = Button::new(state, label).style(style::Button::Filter { selected: filter == current_filter, }); button.on_press(Message::FilterChanged(filter)).padding(8) }; Row::new() .spacing(20) .align_items(Align::Center) .push( Text::new(&format!( "{} {} left", tasks_left, if tasks_left == 1 { "task" } else { "tasks" } )) .width(Length::Fill) .size(16), ) .push( Row::new() .width(Length::Shrink) .spacing(10) .push(filter_button( all_button, "All", Filter::All, current_filter, )) .push(filter_button( active_button, "Active", Filter::Active, current_filter, )) .push(filter_button( completed_button, "Completed", Filter::Completed, current_filter, )), ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Filter { All, Active, Completed, } impl Default for Filter { fn default() -> Self { Filter::All } } impl Filter { fn matches(&self, task: &Task) -> bool { match self { Filter::All => true, Filter::Active => !task.completed, Filter::Completed => task.completed, } } } fn loading_message() -> Element<'static, Message> { Container::new( Text::new("Loading...") .horizontal_alignment(HorizontalAlignment::Center) .size(50), ) .width(Length::Fill) .height(Length::Fill) .center_y() .into() } fn empty_message(message: &str) -> Element<'static, Message> { Container::new( Text::new(message) .width(Length::Fill) .size(25) .horizontal_alignment(HorizontalAlignment::Center) .color([0.7, 0.7, 0.7]), ) .width(Length::Fill) .height(Length::Units(200)) .center_y() .into() } // Fonts const ICONS: Font = Font::External { name: "Icons", bytes: include_bytes!("../fonts/simkai.ttf"), }; fn icon(unicode: char) -> Text { Text::new(&unicode.to_string()) .font(ICONS) .width(Length::Units(20)) .horizontal_alignment(HorizontalAlignment::Center) .size(20) }
// 這個字符在web和原生應用中的顯示比較一致〇 fn edit_icon() -> Text { icon('□') } // '\u{F1F8}'是Unicode字符〇 fn delete_icon() -> Text { icon('◇') } // Persistence #[derive(Debug, Clone, Serialize, Deserialize)] struct SavedState { input_value: String, filter: Filter, tasks: Vec<Task>, } #[derive(Debug, Clone)] enum LoadError { FileError, FormatError, } #[derive(Debug, Clone)] enum SaveError { DirectoryError, FileError, WriteError, FormatError, }
// 這個應用可以編譯為Web應用,但是這里保存數據在web和普通應用間有點區別,所以這里通過這個宏來描述web編譯時用哪個實現,非web應用時用哪種實現 #[cfg(not(target_arch = "wasm32"))] impl SavedState { fn path() -> std::path::PathBuf { let mut path = if let Some(project_dirs) = directories::ProjectDirs::from("rs", "Iced", "Todos") { project_dirs.data_dir().into() } else { std::env::current_dir().unwrap_or(std::path::PathBuf::new()) }; path.push("todos.json"); path } async fn load() -> Result<SavedState, LoadError> { use async_std::prelude::*; let mut contents = String::new(); let mut file = async_std::fs::File::open(Self::path()) .await .map_err(|_| LoadError::FileError)?; file.read_to_string(&mut contents) .await .map_err(|_| LoadError::FileError)?; serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) } async fn save(self) -> Result<(), SaveError> { use async_std::prelude::*; let json = serde_json::to_string_pretty(&self) .map_err(|_| SaveError::FormatError)?; let path = Self::path(); if let Some(dir) = path.parent() { async_std::fs::create_dir_all(dir) .await .map_err(|_| SaveError::DirectoryError)?; } { let mut file = async_std::fs::File::create(path) .await .map_err(|_| SaveError::FileError)?; file.write_all(json.as_bytes()) .await .map_err(|_| SaveError::WriteError)?; } // This is a simple way to save at most once every couple seconds async_std::task::sleep(std::time::Duration::from_secs(2)).await; Ok(()) } } #[cfg(target_arch = "wasm32")] impl SavedState { fn storage() -> Option<web_sys::Storage> { let window = web_sys::window()?; window.local_storage().ok()? } async fn load() -> Result<SavedState, LoadError> { let storage = Self::storage().ok_or(LoadError::FileError)?; let contents = storage .get_item("state") .map_err(|_| LoadError::FileError)? .ok_or(LoadError::FileError)?; serde_json::from_str(&contents).map_err(|_| LoadError::FormatError) } async fn save(self) -> Result<(), SaveError> { let storage = Self::storage().ok_or(SaveError::FileError)?; let json = serde_json::to_string_pretty(&self) .map_err(|_| SaveError::FormatError)?; storage .set_item("state", &json) .map_err(|_| SaveError::WriteError)?; let _ = wasm_timer::Delay::new(std::time::Duration::from_secs(2)).await; Ok(()) } } // 設置樣式,重點關注 mod style { use iced::{button, Background, Color, Vector}; pub enum Button { Filter { selected: bool }, Icon, Destructive, } impl button::StyleSheet for Button { fn active(&self) -> button::Style { match self { Button::Filter { selected } => { if *selected { button::Style { background: Some(Background::Color( Color::from_rgb(0.2, 0.2, 0.7), )), border_radius: 10, text_color: Color::WHITE, ..button::Style::default() } } else { button::Style::default() } }, Button::Icon => button::Style { text_color: Color::from_rgb(0.5, 0.5, 0.5), ..button::Style::default() }, Button::Destructive => button::Style { // 紅色,0.8*255要這么對應CSS里的配置 background: Some(Background::Color(Color::from_rgb( 0.8, 0.2, 0.2, ))), border_radius: 5, text_color: Color::WHITE, shadow_offset: Vector::new(1.0, 1.0), ..button::Style::default() }, } } fn hovered(&self) -> button::Style { let active = self.active(); button::Style { text_color: match self { Button::Icon => Color::from_rgb(0.2, 0.2, 0.7), Button::Filter { selected } if !selected => { Color::from_rgb(0.2, 0.2, 0.7) } _ => active.text_color, }, shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0), ..active } } } }
4.Windows10配置gui開發環境,待寫;
5.Ubuntu20.04開發gui開發環境;
5.1.build時如果出現failed to run custom build command for `x11 v2.18.2`需要安裝pkg-config和org-dev兩個依賴(用apt即可安裝)