ساخت برنامه های رابط کاربری گرافیکی متقابل پلت فرم در Rust با استفاده از egui
Summarize this content to 400 words in Persian Lang
نوشته ماریو زوپان✏️
از زمان ظهور Electron، که منجر به کمی رنسانس برنامه های دسکتاپ متقابل پلت فرم با استفاده از فناوری وب شد، محدودیت های این رویکرد آشکار شد. زمان آهسته راه اندازی، استفاده از منابع بسیار بالا، و محدودیت های فناوری وب برای ساخت برنامه های رابط کاربری گرافیکی با وفاداری بالا، همچنان کاربران بسیاری از برنامه های کاربردی مبتنی بر الکترون را تا به امروز آزار می دهد.
Electron ساخت اپلیکیشنهای دسکتاپ چند پلتفرمی را بسیار قابل دسترستر از رویکردهای قبلی خود کرد، اما مطمئناً پاسخ همه چیز نبود. مشکل ساخت برنامه های کاربردی چند پلتفرمی غنی، بسیار کارآمد و قوی هنوز حل نشده است و وعده “یک بار بسازید، هر جا اجرا کنید” هنوز در هیچ یک از فناوری های موجود در حال حاضر آشکار نشده است.
با این حال، با علاقهمندی جدید به برنامههای دسکتاپ متقابل پلتفرم و بهبود پلتفرم وب در امتداد خطوط WebGL/WebGPU، و همچنین WebAssembly، پروژههای جالب زیادی به وجود آمدند که تلاش میکنند گامهای بعدی را در تلاش برای ساختن بردارند. پایه و اساس برنامه های کاربردی رابط کاربری گرافیکی کراس پلتفرم.
یکی از این پروژه ها که با استفاده از Rust ساخته شده است، egui است. در این مقاله، خواهیم دید که چگونه میتوانیم یک برنامه گرافیکی ساده و چند پلتفرمی با آن بسازیم.
ما قصد داریم یک برنامه مدیریت حیوانات خانگی بسیار ساده بسازیم که به ما امکان می دهد حیوانات خانگی را اضافه و حذف کنیم و لیست و نمای جزئیات را به ما نشان می دهد.
برای اینکه کل موضوع جالب تر شود، از یک استفاده می کنیم sqlite پایگاه داده برای ذخیره سازی داده ها، و هر زمان که صفحه جزئیات حیوان خانگی باز شود، تصویری تصادفی از یک گربه یا سگ دریافت می کنیم.
اما ابتدا، بیایید کمی بیشتر در مورد egui و دلیل جالب بودن آن بیاموزیم.
egui / eframe
Egui یک کتابخانه/چارچوب رابط کاربری گرافیکی مبتنی بر Rust و حالت فوری است که بر سهولت استفاده، پاسخگویی و قابلیت حمل تمرکز دارد.
اما صبر کنید، “حالت فوری” به چه معناست؟ خوب، در دنیای رابطهای کاربری گرافیکی، دو راه کلی برای ساخت برنامهها وجود دارد – حالت حفظ شده و حالت فوری، با حالت حفظ شده که بیشتر مواردی را که احتمالاً در مورد چارچوبهای رابط کاربری گرافیکی (مانند QT یا DOM) میدانید پوشش میدهد.
حالت فوری، که برای اولین بار در این ویدیو توسط کیسی موراتوری در سال 2005 ذکر شد، با سادهسازی رویکرد تعامل، رویکرد متفاوتی نسبت به رابط کاربری حالت حفظ شده دارد.
بهجای افزودن کنترلکنندههای پاسخ به تماس به عناصر، مانند دکمهها، در حالی که عنصر در برنامه رابط کاربری گرافیکی «حفظ» میشود و فقط بر اساس درخواست تغییر میکند، در حالت فوری، هر عنصر روی صفحه در هر فریم نمایش داده میشود، بنابراین هرگونه تعاملی انجام میشود. فوری
این تفاوت برخی معاوضه ها را به همراه دارد. به عنوان مثال، چیدمان پیچیده در رابط کاربری گرافیکی حالت فوری بسیار سخت تر است، زیرا اطلاعات زیادی در مورد هر فریم ندارید. راهحلهایی برای این کار وجود دارد، اما در رابط کاربری گرافیکی حالت حفظ شده آسانتر است.
به طور کلی، ساختن و استدلال در مورد رابطهای کاربری گرافیکی حالت فوری بسیار سادهتر است، زیرا تعامل همزمانتر است، که مشکل وضعیت برنامه و ناهمگام بودن آنچه را که روی صفحه نمایش داده میشود، برطرف میکند. این یک مشکل اصلی در رابط کاربری گرافیکی حالت حفظ شده است.
شناخته شده ترین چارچوب رابط کاربری گرافیکی حالت فوری، که egui نیز از آن الهام گرفته شده، Dear imgui است. مخزن egui همچنین دارای یک بخش در مورد معاوضه ها در رابطه با رابط کاربری گرافیکی حالت فوری است که من قطعاً توصیه می کنم آن را بررسی کنید.
سفارشی سازی و دسترسی
یک جنبه جالب از egui آن است egui در واقع فقط یک کتابخانه برای ساخت یک رابط کاربری گرافیکی است، نه یک چارچوب. اما چارچوبی هم به نام وجود دارد eframe، که محیط اطراف را برای ساخت یک برنامه واقعی اضافه می کند.
کتابخانه، egui را می توان در چارچوب های مختلف، مانند موتورهای بازی، ادغام کرد، که قابلیت استفاده مجدد و ماژولار بودن خوبی را ایجاد می کند.
یکی از جنبه های مهم هر کتابخانه یا چارچوب رابط کاربری گرافیکی، امکان سفارشی سازی است egui شما را در آنجا تحت پوشش قرار داده است.
میتوانید در صفحه نمایشی فوقالعاده آنها مشاهده کنید، که نه تنها تمام ویجتهای موجود و برخی از برنامههای نمایشی عالی را نشان میدهد، بلکه ابزارهای اشکال زدایی و گزینههای سفارشیسازی را نیز نشان میدهد، بنابراین قبل از غواصی عمیقتر با آن بازی کنید.
Egui همچنین دارای اسناد عالی و نمونه های بسیار زیادی است که برای آموزش و نشان دادن قابلیت های کتابخانه ساخته شده اند: یکی دیگر از جنبههای جالب egui این است که AccessKit را یکپارچه میکند، یک چارچوب بین پلتفرمی برای دسترسی.
اگرچه AccessKit، در زمان نگارش، پیادهسازی کاملی برای وب ندارد، به نظر میرسد یک پروژه بسیار امیدوارکننده برای آسانتر کردن پیادهسازی ویژگیهای دسترسی برای هر پلتفرم باشد.
کراس پلتفرم و وب
همانطور که در ابتدا ذکر شد، هدف از egui این است که یک کتابخانه برنامه رابط کاربری گرافیکی بین پلتفرمی باشد. با استفاده از eframe_template، می توان برنامه هایی برای لینوکس، مک، ویندوز، اندروید و وب با استفاده از WebGL/WebGPU و WASM ساخت.
پشتیبانی از پلتفرم های مختلف هنوز در مرحله بلوغ است، اما برای دسکتاپ از قبل به طور پایدار کار می کند. با توجه به رویکردی که به وب میرسد، محدودیتهایی وجود دارد، بنابراین egui مطمئناً جایگزینی برای ساخت هیچ و همه برنامههای کاربردی وب نیست، اما سعی نمیکند خود را به این شکل باشد یا تبلیغ کند.
با این حال، وقتی به برنامههای آزمایشی در صفحه نمایشی نگاه میکنید، کاملاً چشمگیر است که از قبل با استفاده از egui حتی در محدودترین محیط آن ممکن است.
در مثالی که در این مقاله آمده است، ما یک برنامه دسکتاپ اول می سازیم که نمی توانیم آن را در WASM کامپایل کنیم زیرا از sqlite به عنوان ذخیره سازی داده استفاده می کنیم.
با این حال، با چند تغییر وابسته به پلت فرم و eframe_template، میتوانیم در هنگام ساختن برای WASM از ذخیرهسازی دادههای متفاوتی استفاده کنیم، و میتوانیم آن را برای وب نیز کار کنیم، اما این موضوع خارج از محدوده این پست است.
بیایید شروع به ساخت برنامه خود کنیم!
راه اندازی
برای ادامه، تنها چیزی که نیاز دارید، نصب اخیر Rust است که 1.79 آخرین مورد در زمان نگارش این مقاله است.
ابتدا یک پروژه Rust جدید ایجاد کنید:
cargo new rust-egui-example
cd rust-egui-example
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در مرحله بعد، فایل Cargo.toml را ویرایش کنید و وابستگی های مورد نیاز خود را اضافه کنید:
[dependencies] sqlite = “0.36.0”anyhow = “1.0.82”
eframe = { version = “0.28.0”, features = [“wgpu”] }
env_logger = “0.11.3”
log = “0.4.21”
ehttp = { version = “0.5.0”, features = [“json”] }
serde = { version = “1.0.203”, features = [“derive”] }
egui_extras = { version = “0.28.0”, features = [“all_loaders”] }
image = { version = “0.25”, features = [“jpeg”, “png”] }
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
را اضافه می کنیم egui و eframe وابستگی ها و همچنین egui_extras، که به ما امکان می دهد از عملکرد بارگذاری تصویر همراه با image جعبه
ما هم اضافه می کنیم ehttp برای واکشی تصاویر حیوانات تصادفی و serde برای سریال سازی و سریال زدایی علاوه بر این، ما نیاز داریم sqlite برای ذخیره سازی داده های ما، و ما اضافه می کنیم anyhow برای رسیدگی آسان تر خطا و env_logger و log برای قابلیت های ورود به سیستم
و با آن، ما می توانیم شروع به ساخت برنامه خود کنیم!
مدل داده
ابتدا مدل داده های اساسی خود را تعریف می کنیم. ما در حال ساخت یک برنامه مدیریت حیوانات خانگی هستیم، بنابراین به یک برنامه نیاز داریم Pet نوع ما هم تصمیم گرفتیم استفاده کنیم sqlite برای ذخیره سازی داده، بنابراین برای کار با مدل داده خود با استفاده از SQL به تنظیماتی نیاز داریم:
#[derive(Debug, PartialEq, Clone)] struct PetKind(String);
#[derive(Debug, PartialEq, Clone)]
struct Pet {
id: i64,
name: String,
age: i64,
kind: PetKind,
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این Pet نوع بسیار ساده است و شامل یک id، name، age و kind. البته، ما میتوانیم بسیاری از ویژگیهای جالبتر را اضافه کنیم، اما آن را ساده نگه میداریم.
مرحله بعدی ایجاد جدول در ما است sqlite پایگاه داده:
CREATE TABLE IF NOT EXISTS pets (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL,
kind TEXT NOT NULL
);
INSERT INTO pets (name, age, kind) VALUES (‘minka’, 9, ‘cat’);
INSERT INTO pets (name, age, kind) VALUES (‘nala’, 7, ‘dog’);
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این به سادگی آینه ماست Pet نوع همچنین می توانیم پرس و جوهایی را که برای عملیات مورد نیاز خود استفاده خواهیم کرد، تعریف کنیم.
یک حیوان خانگی را از پایگاه داده با شناسه آن واکشی کنید
همه حیوانات خانگی را از پایگاه داده واکشی کنید
یک حیوان خانگی را در پایگاه داده قرار دهید
یک حیوان خانگی را از پایگاه داده حذف کنید
const GET_PET_BY_ID: &str = “SELECT id, name, age, kind FROM pets where id = ?”;
const DELETE_PET_BY_ID: &str = “DELETE FROM pets where id = ?”;
const INSERT_PET: &str =
“INSERT INTO pets (name, age, kind) VALUES (?, ?, ?) RETURNING id, name, age, kind”;
const GET_PETS: &str = “SELECT id, name, age, kind FROM pets”;
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اکنون، تنها کاری که باید انجام دهیم این است که یک اتصال به an راه اندازی کنیم sqlite پایگاه داده در برنامه ما و پرس و جو را در اسکریپت اولیه ما در بالا هنگام راه اندازی اجرا کنید.
در این مورد، ما از یک استفاده می کنیم in-memory نسخه از sqlite، که تست را آسان تر می کند. این بدان معنی است که هر بار که برنامه شروع می شود پایگاه داده بازنشانی می شود، که برای آزمایش خوب است، اما البته اگر بخواهیم نسخه تولیدی این برنامه را بسازیم، باید تغییر کند:
fn load_init_sql() -> std::io::Result {
fs::read_to_string(“./init.sql”)
}
fn main() -> Result()> {
env_logger::init();
…
let init_query = load_init_sql().expect(“can load init query”);
let db_con = sqlite::open(“:memory:”).expect(“can create sqlite db”);
db_con
.execute(init_query)
.expect(“can initialize sqlite db”);
…
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما اسکریپت اولیه سازی پایگاه داده را بارگذاری می کنیم، یک اتصال به حافظه باز می کنیم sqlite پایگاه داده، و اسکریپت را اجرا کنید، پایگاه داده ما را مقداردهی اولیه کنید.
سپس، اجازه دهید توابع کمکی را برای عملیات واکشی و ذخیره سازی داده های فوق الذکر تعریف کنیم. هر یک از آنها را خواهد گرفت db_con اما در داخل یک Arc>. دلیل آن این است که sqlite::Connection به خودی خود نمی تواند به طور ایمن در سراسر رشته ها به اشتراک گذاشته شود، بنابراین ما باید آن را قفل کنیم و مطمئن شویم که همه ارجاعات به آن به حساب می آیند.
چرا ما حتی می خواهیم آن را در سراسر رشته ها به اشتراک بگذاریم؟ آیا نمیتوانیم فقط از اتصال استفاده کنیم و هر زمان که نیاز داشتیم در رشته اصلی کوئریها را به صورت همزمان اجرا کنیم؟
ما می توانستیم، اما از آنجایی که قرار است این یک برنامه رابط کاربری گرافیکی بسیار تعاملی باشد، مسدود کردن رشته اصلی ایده خوبی نیست. اگر کاربر روی دکمهای کلیک میکرد، تا زمانی که تمام درخواستهایی که با فشار دادن دکمه فعال میشوند، UI پاسخگو نمیشود.
این رفتاری نیست که ما به دنبال آن هستیم، و بنابراین میخواهیم یک سیستم انتقال رویداد ساده بسازیم، که به رشته اصلی اجازه میدهد رویدادها را به یک رشته پسزمینه ارسال کند، که سپس کار واکشی دادهها را به صورت ناهمزمان انجام میدهد و به اصلی اطلاع میدهد. زمانی که داده ها در دسترس هستند، رشته کنید.
بیایید به کمک کننده های عملیات داده نگاه کنیم. ما با قرار دادن حیوانات خانگی جدید در پایگاه داده شروع می کنیم:
fn insert_pet_to_db(db_con: ArcMutex>, pet: Pet) -> Result {
let con = db_con
.lock()
.map_err(|_| anyhow!(“error while locking db connection”))?;
let mut stmt = con.prepare(INSERT_PET)?;
stmt.bind((1, pet.name.as_str()))?;
stmt.bind((2, pet.age))?;
stmt.bind((3, pet.kind.0.as_str()))?;
if stmt.next()? == sqlite::State::Row {
let id = stmt.read::i64, _>(0)?;
let name = stmt.read::String, _>(1)?;
let age = stmt.read::i64, _>(2)?;
let kind = stmt.read::String, _>(3)?;
return Ok(Pet {
id,
name,
age,
kind: PetKind(kind),
});
}
Err(anyhow!(“error while inserting pet”))
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ابتدا باید یک قفل برای اتصال پایگاه داده مشترک خود داشته باشیم، بنابراین مطمئن هستیم که تنها ما از آن استفاده می کنیم. سپس، یک عبارت آماده شده با پرس و جو تعریف شده در بالا ایجاد می کنیم و مقادیر ورودی را به همتایان مربوطه خود در پرس و جو متصل می کنیم.
سپس، کوئری را اجرا می کنیم و یک پرس و جو جدید ایجاد می کنیم Pet از ردیف برگشتی اگر عملیات ناموفق باشد، خطا را منتشر می کنیم.
بیایید در ادامه به حذف حیوان خانگی نگاه کنیم:
fn delete_pet_from_db(db_con: ArcMutex>, pet_id: i64) -> Result()> {
let con = db_con
.lock()
.map_err(|_| anyhow!(“error while locking db connection”))?;
let mut stmt = con.prepare(DELETE_PET_BY_ID)?;
stmt.bind((1, pet_id))?;
if stmt.next()? == sqlite::State::Done {
Ok(())
} else {
Err(anyhow!(“error while deleting pet with id {}”, pet_id))
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
حذف حیوان خانگی بسیار ساده است. ما یک قفل برای اتصال پایگاه داده خود به دست می آوریم و به سادگی کوئری حذف را اجرا می کنیم و هر گونه خطای رخ داده را مدیریت می کنیم:
fn get_pet_from_db(db_con: ArcMutex>, pet_id: i64) -> Resultoption> {
let con = db_con
.lock()
.map_err(|_| anyhow!(“error while locking db connection”))?;
let mut stmt = con.prepare(GET_PET_BY_ID)?;
stmt.bind((1, pet_id))?;
if stmt.next()? == sqlite::State::Row {
let id = stmt.read::i64, _>(0)?;
let name = stmt.read::String, _>(1)?;
let age = stmt.read::i64, _>(2)?;
let kind = stmt.read::String, _>(3)?;
return Ok(Some(Pet {
id,
name,
age,
kind: PetKind(kind),
}));
}
Ok(None)
}
fn get_pets_from_db(db_con: ArcMutex>) -> ResultVec> {
let con = db_con
.lock()
.map_err(|_| anyhow!(“error while locking db connection”))?;
let mut pets: Vec = vec![];
let mut stmt = con.prepare(GET_PETS)?;
for row in stmt.iter() {
let row = row?;
let id = row.read::i64, _>(0);
let name = row.read::str, _>(1);
let age = row.read::i64, _>(2);
let kind = row.read::str, _>(3);
pets.push(Pet {
id,
name: name.to_owned(),
age,
kind: PetKind(kind.to_owned()),
});
}
Ok(pets)
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
واکشی یک حیوان خانگی از پایگاه داده و واکشی همه حیوانات خانگی کاملاً مشابه هستند. در هر دو مورد، ما دوباره یک قفل برای اتصال پایگاه داده می گیریم، دستور را با استفاده از پرس و جو آماده می کنیم، پرس و جو را اجرا می کنیم و داده های خروجی خود را از داده های ردیف های برگشتی می سازیم.
این برای مدل داده ما و دسترسی به داده است!
در بالا ذکر کردیم که میخواهیم تصاویر تصادفی را برای گربهها و سگها واکشی کنیم تا برنامه کمی زندهتر به نظر برسد، بنابراین بیایید آن قسمت را در ادامه پیادهسازی کنیم.
واکشی تصاویر تصادفی برای گربه ها و سگ ها
برای واکشی تصاویر سگ، از https://dog.ceo/api/breeds/image/random و برای گربه ها از https://api.thecatapi.com/v1/images/search استفاده می کنیم.
هر دوی این APIها به ما اجازه میدهند چند پرسوجو را رایگان انجام دهیم، که برای آزمایش خوب است. اگر چند درخواست آزمایشی انجام دهیم، میتوانیم ببینیم که دادههای برگشتی چگونه به نظر میرسند، و میتوانیم یک مدل داده برای پاسخهای JSON هر دو API ایجاد کنیم:
#[derive(Debug, Deserialize)]
struct CatJSON {
#[serde(alias = “0”)]
item: CatJSONInner,
}
#[derive(Debug, Deserialize)]
struct CatJSONInner {
url: String,
}
#[derive(Debug, Deserialize)]
struct DogJSON {
message: String,
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
استفاده می کنیم serde::Deserialize تا بتوانیم JSON برگردانده شده را به انواع خود غیر سریالی کنیم. مرحله بعدی استفاده از ehttp برای ایجاد یک درخواست HTTP به API های مربوطه، تجزیه پاسخ و مدیریت URL هایی که دریافت می کنیم:
fn fetch_pet_image(ctx: egui::Context, pet_kind: PetKind, sender: Sender) {
let url = if pet_kind.0 == “dog” {
“https://dog.ceo/api/breeds/image/random”
} else {
“https://api.thecatapi.com/v1/images/search”
};
ehttp::fetch(
ehttp::Request::get(url),
move |result: ehttp::Result| {
if let Ok(result) = result {
let image_url = if pet_kind.0 == “dog” {
if let Ok(json) = result.json::() {
Some(json.message)
} else {
None
}
} else if let Ok(json) = result.json::() {
Some(json.item.url)
} else {
None
};
let _ = sender.send(Event::SetPetImage(image_url));
ctx.request_repaint();
}
},
);
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اگر به امضای تابع نگاه کنیم، می بینیم egui::Context، PetKind، و Sender. اینها چیست و ما برای چه به آنها نیاز داریم؟
ما نیاز داریم egui::Context برای درخواست رنگ آمیزی مجدد رابط کاربری گرافیکی، پس از اتمام واکشی داده های خود. این معادل این است که به رشته رندر اطلاع دهیم که داده های جدیدی داریم و برای نمایش آن داده ها باید دوباره رندر شود. در غیر این صورت، اگر هیچ تعاملی اتفاق نیفتاد، ممکن است رابط کاربری ثابت بماند تا عمر باتری حفظ شود و دادههای جدید را نشان ندهد.
این PetKind صرفاً برای بررسی این است که آیا ما یک تصویر برای یک گربه یا یک سگ واکشی می کنیم Sender قسمت ارسال الف است std::sync::mpsc کانال، که به ما امکان میدهد دادهها را به رشته رندر اصلی ارسال کنیم، که سپس میتوان آنها را از آنجا دریافت و مدیریت کرد.
در بخش بعدی نگاهی به نحوه عملکرد این مکانیسم مدیریت رویداد خواهیم داشت.
رسیدگی به رویداد
همانطور که در بالا ذکر شد، ما به راهی برای واکشی ناهمزمان داده ها بدون مسدود کردن رشته رندر اصلی نیاز داریم. با این حال، ما همچنین به راهی برای برقراری ارتباط بین این رشته پسزمینه و موضوع اصلی نیاز داریم. برای این منظور از دو مورد استفاده می کنیم std::sync::mpsc کانالها – یکی برای ارسال رویدادها به رشته پسزمینه و دریافت آنها در آنجا، و دیگری برای ارسال مجدد دادهها به رشته اصلی و دریافت دادهها در آنجا:
fn main() -> Result()> {
…
let (background_event_sender, background_event_receiver) = channel::();
let (event_sender, event_receiver) = channel::();
…
std::thread::spawn(move || {
while let Ok(event) = background_event_receiver.recv() {
let sender = event_sender.clone();
handle_events(event, sender);
}
});
…
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
در main تابع، ما دو کانال را ایجاد می کنیم، هر کدام با آنها Sender و Receiver به پایان می رسد.
سپس، ما یک رشته جدید ایجاد می کنیم – رشته پس زمینه ما – و در آن، منتظر رویدادهایی در background_event_receiver و برای هر رویداد، رویداد را مدیریت کنید و داده ها را با استفاده از آن به رشته اصلی ارسال کنید event_sender، Sender بخشی از کانال تاپیک اصلی
در مرحله بعد، ما را تعریف می کنیم Event type، که یک enum است که شامل تمام انواع رویدادهای مختلف است که در برنامه به آن نیاز داریم:
enum Event {
SetPets(Vec),
GetPetImage(egui::Context, PetKind),
SetPetImage(Option),
GetPetFromDB(egui::Context, ArcMutex>, i64),
SetSelectedPet(Option),
InsertPetToDB(egui::Context, ArcMutex>, Pet),
DeletePetFromDB(egui::Context, ArcMutex>, i64),
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
SetPets – لیست جدیدی از حیوانات خانگی واکشی شده را در AppState برنامه تنظیم می کند
GetPetImage – یک تصویر تصادفی جدید برای یک نوع معین از حیوان خانگی دریافت می کند
SetPetImage – تصویر حیوان خانگی واکشی شده را در AppState تنظیم می کند
GetPetFromDB – یک حیوان خانگی با شناسه داده شده (i64) را از پایگاه داده واکشی می کند
SetSelectedPet — حیوان خانگی انتخاب شده را در AppState برنامه تنظیم می کند
InsertPetToDB – یک حیوان خانگی جدید به پایگاه داده اضافه می کند
DeletePetFromDB – یک حیوان خانگی را از پایگاه داده حذف می کند
سپس، ما آن را اجرا می کنیم handle_events تابعی که در داخل رشته پس زمینه فراخوانی می شود:
fn handle_events(event: Event, sender: Sender) {
match event {
Event::GetPetImage(ctx, pet_kind) => {
fetch_pet_image(ctx, pet_kind, sender);
}
Event::GetPetFromDB(ctx, db_con, pet_id) => {
if let Ok(Some(pet)) = get_pet_from_db(db_con, pet_id) {
let _ = sender.send(Event::SetSelectedPet(Some(pet)));
ctx.request_repaint();
}
}
Event::DeletePetFromDB(ctx, db_con, pet_id) => {
if delete_pet_from_db(db_con.clone(), pet_id).is_ok() {
if let Ok(pets) = get_pets_from_db(db_con) {
let _ = sender.send(Event::SetPets(pets));
ctx.request_repaint();
}
}
}
Event::InsertPetToDB(ctx, db_con, pet) => {
if let Ok(new_pet) = insert_pet_to_db(db_con.clone(), pet) {
if let Ok(pets) = get_pets_from_db(db_con) {
let _ = sender.send(Event::SetPets(pets));
let _ = sender.send(Event::SetSelectedPet(Some(new_pet)));
ctx.request_repaint();
}
}
}
_ => (),
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما با رویداد ورودی مطابقت می دهیم، داده ها را واکشی می کنیم، داده ها را با استفاده از آن ارسال می کنیم Sender، و تماس بگیرید ctx.request_repaint() برای راه اندازی به روز رسانی جدید در موضوع رندر.
در مورد حذف یک حیوان خانگی، لیست جدیدی از حیوانات خانگی را از پایگاه داده دریافت می کنیم، بنابراین تغییر در لیست نمایش داده شده منعکس می شود. برای قرار دادن حیوان خانگی جدید نیز همین کار را انجام می دهیم و آن را نیز تنظیم می کنیم selected_pet در AppState به حیوان خانگی تازه ایجاد شده، از این رو در هنگام ایجاد به آن پیمایش کنید.
این نیمی از منطق مدیریت رویداد است، منطقی که در رشته پسزمینه است. اما باید یک همتا در رشته رندر وجود داشته باشد که کانال را برای داده های ارسال شده از رشته پس زمینه بررسی می کند، که سپس به AppState اعمال می شود، که به نوبه خود منجر به به روز رسانی رابط کاربری گرافیکی می شود.
این قسمت در handle_gui_events در داخل PetApp نوع، که در بخش بعدی و آخر با آن آشنا خواهیم شد:
fn handle_gui_events(&mut self) {
while let Ok(event) = self.event_receiver.try_recv() {
match event {
Event::SetPetImage(pet_image) => {
self.app_state.pet_image = pet_image;
}
Event::SetSelectedPet(pet) => self.app_state.selected_pet = pet,
Event::SetPets(pets) => {
if let Some(ref selected_pet) = self.app_state.selected_pet {
if !pets.iter().any(|p| p.id == selected_pet.id) {
self.app_state.selected_pet = None;
}
}
self.app_state.pets = pets;
}
_ => (),
};
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
دوباره، ما روی همه رویدادهای دریافتی تکرار می کنیم و با آنها مطابقت می کنیم. برای هر رویداد، ما AppState را به روز می کنیم. جالب ترین پیاده سازی در است SetPets، که در آن، اگر حیوان خانگی انتخاب شده قبلی دیگر بخشی از لیست به روز شده نباشد، حیوان خانگی انتخاب شده را بازنشانی می کنیم. این موضوع مرتبط است زیرا اگر حیوان خانگی را حذف کنیم و این کار را انجام ندهیم، نمای جزئیات همچنان حیوان خانگی حذف شده را نشان می دهد.
خوب، اکنون مدل داده، منطق واکشی تصویر و مکانیسم مدیریت رویداد را داریم، و در نهایت میتوانیم همه چیز را سیمکشی کنیم و رابط کاربری گرافیکی خود را بسازیم.
ساخت رابط کاربری گرافیکی
ساختار رابط کاربری به گونه ای است که در سمت چپ پنل لیستی وجود دارد که دکمه افزودن حیوان خانگی جدید و همچنین فرمی برای افزودن حیوانات خانگی جدید را نگه می دارد. با کلیک بر روی یک حیوان خانگی در لیست، جزئیات آن حیوان خانگی و یک تصویر تصادفی برای حیوان خانگی در پنل جزئیات در سمت راست UI نشان داده می شود.
در پایان، باید تقریباً شبیه به این باشد:
بیایید با انواع داده هایی که نیاز داریم شروع کنیم:
struct PetApp {
app_state: AppState,
background_event_sender: Sender,
event_receiver: Receiver,
db_con: ArcMutex>,
}
#[derive(Debug, Clone)]
struct AppState {
selected_pet: Option,
pets: Vec,
pet_image: Option,
add_form: AddForm,
}
#[derive(Debug, Clone)]
struct AddForm {
show: bool,
name: String,
age: String,
kind: String,
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
را تعریف می کنیم PetApp، که ساختاری است که تمام داده های برنامه را در خود نگه می دارد. نگه می دارد AppState، که نمایشی از وضعیت فعلی برنامه است، به عنوان مثال، کدام حیوانات خانگی فهرست شده اند، کدام یک انتخاب شده است و غیره، و همچنین AddForm حالت، که نشان دهنده وضعیت فعلی فرم برای اضافه کردن یک حیوان خانگی جدید است.
PetApp همچنین شامل اتصال پایگاه داده مشترک، انتهای فرستنده برای رشته پسزمینه، و همچنین انتهای گیرنده برای کانال رویداد برای رشته رندر است، بنابراین میتوانیم مکانیزم مدیریت رویداد را که در بالا پیادهسازی کردیم، اجرا کنیم.
در مرحله بعد، ما را اجرا می کنیم PetApp::new روش، بنابراین ما می توانیم برنامه خود را مقداردهی اولیه کنیم:
impl PetApp {
fn new(
background_event_sender: Sender,
event_receiver: Receiver,
db_con: sqlite::Connection,
) -> ResultBox> {
let db_con = Arc::new(Mutex::new(db_con));
let pets = get_pets_from_db(db_con.clone())?;
Ok(Box::new(Self {
app_state: AppState {
selected_pet: None,
pets,
pet_image: None,
add_form: AddForm {
show: false,
name: String::default(),
age: String::default(),
kind: String::default(),
},
},
background_event_sender,
event_receiver,
db_con,
}))
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ورودی را بسته بندی می کنیم sqlite::Connection داخل یک Arc> بنابراین می توانیم با خیال راحت آن را در سراسر رشته ها به اشتراک بگذاریم و لیست اولیه حیوانات خانگی را واکشی کنیم و آن را در AppState تنظیم کنیم. بقیه حالت به سادگی به یک پیش فرض ایمن مقداردهی اولیه می شود.
برمی گردیم a Result> از آنجایی که آن چیزی است eframe انتظار می رود هنگام ایجاد یک برنامه بومی جدید، که در ادامه به بررسی آن خواهیم پرداخت main:
fn main() -> Result()> {
…
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([640.0, 480.0]),
..Default::default()
};
…
eframe::run_native(
“PetApp”,
options,
Box::new(|context| {
egui_extras::install_image_loaders(&context.egui_ctx);
Ok(PetApp::new(
background_event_sender,
event_receiver,
db_con,
)?)
}),
)
.map_err(|e| anyhow!(“eframe error: {}”, e))
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما برخی از گزینه های برنامه مانند اندازه پنجره را تعریف می کنیم و سپس استفاده می کنیم eframe::run_native برای مقداردهی اولیه برنامه رابط کاربری گرافیکی.
یکی از مواردی که در اینجا باید به آن اشاره کرد، لودرهای egui_extras هستند که در حین ایجاد برنامه نصب میکنیم. اینها کمکهایی هستند که بارگذاری پویا تصاویر را از روی دیسک یا از یک URL درون آسان میکنند egui. این عملکرد را می توان سفارشی کرد، اما پیش فرض کاملاً مفید است، نشانگر بارگذاری را در حین واکشی تصویر و سپس نمایش آن را نشان می دهد.
از آنجایی که ما فقط می توانیم از انواعی استفاده کنیم که the را پیاده سازی کنند eframe::App صفت درون eframe::run_native، ما باید این ویژگی را اجرا کنیم.
اجرا فقط شامل update تابعی که دارای امضای زیر است:
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame)
طول می کشد Context و الف Frame، که ما به آن نیاز نخواهیم داشت. اما احتمالاً می توانید آن را به خاطر بسپارید Context، زیرا این همان چیزی است که قبلاً برای شروع رنگ آمیزی مجدد در موضوع رندر استفاده کردیم. بنابراین هر زمان که تماس می گیریم context.request_repaint()، این update تابع دوباره فراخوانی می شود:
impl eframe::App for PetApp {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
self.handle_gui_events();
egui::CentralPanel::default().show(ctx, |ui| {
egui::SidePanel::left(“left panel”)
.resizable(false)
.default_width(200.0)
.show_inside(ui, |ui| {
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
اجرای آن را آغاز می کنیم update با تماس با ما handle_gui_events روشی که در بالا پیاده سازی کردیم. این قسمت دریافت کننده کانال را برای رویدادها بررسی می کند و آنها را اعمال می کند، که تضمین می کند که یک AppState به روز خواهیم داشت که وارد مرحله رندر ما می شود.
همانطور که در بالا ذکر شد، با توجه به اینکه egui یک چارچوب رابط کاربری گرافیکی حالت فوری است، هر چیزی که در اینجا تعریف می کنیم در هر فریم رندر می شود. بنابراین اگر به دلیل ارسال یک رویداد، تغییری در اینجا در AppState ایجاد کنیم، بلافاصله در رابط کاربری نمایش داده میشود بدون اینکه نیازی به دستکاری و بهروزرسانی هر مؤلفهای داشته باشیم.
سپس، کلی را تعریف می کنیم CentralPanel و در داخل آن با رندر کردن پنل لیست سمت چپ شروع کنید، که آن را روی عرض تنظیم می کنیم 200.0 و قابل تغییر اندازه نیست.
در این پانل، ما را نشان خواهیم داد Pets عنوان، جداکننده، Add new Pet دکمه و، اگر دکمه فشار داده شد، فرم اضافه کردن یک حیوان خانگی جدید. در زیر آن، یک جداکننده دیگر و لیست حیوانات خانگی:
ui.vertical_centered(|ui| {
ui.heading(“Pets”);
ui.separator();
if ui.button(“Add new Pet”).clicked() {
self.app_state.add_form.show = !self.app_state.add_form.show;
}
if self.app_state.add_form.show {
ui.separator();
ui.vertical_centered(|ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(“name:”);
ui.label(“age”);
ui.label(“kind”);
});
ui.end_row();
ui.vertical(|ui| {
ui.text_edit_singleline(&mut self.app_state.add_form.name);
ui.text_edit_singleline(&mut self.app_state.add_form.age);
ui.text_edit_singleline(&mut self.app_state.add_form.kind);
});
});
if ui.button(“Submit”).clicked() {
let add_form = &mut self.app_state.add_form;
let age = add_form.age.parse::().unwrap_or(0);
let kind = match add_form.kind.as_str() {
“cat” => PetKind(String::from(“cat”)),
_ => PetKind(String::from(“dog”)),
};
let name = add_form.name.to_owned();
if !name.is_empty() && age > 0 {
let _ = self.background_event_sender.send(
Event::InsertPetToDB(
ctx.clone(),
self.db_con.clone(),
Pet {
id: -1,
name,
age,
kind: kind.clone(),
},
),
);
let _ = self
.background_event_sender
.send(Event::GetPetImage(ctx.clone(), kind));
add_form.name = String::default();
add_form.age = String::default();
add_form.kind = String::default();
}
}
});
}
ui.separator();
self.app_state.pets.iter().for_each(|pet| {
if ui
.selectable_value(
&mut self.app_state.selected_pet,
Some(pet.to_owned()),
pet.name.clone(),
)
.changed()
{
let _ = self.background_event_sender.send(Event::GetPetFromDB(
ctx.clone(),
self.db_con.clone(),
pet.id,
));
let _ = self
.background_event_sender
.send(Event::GetPetImage(ctx.clone(), pet.kind.clone()));
}
});
});
});
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
روشی که ما تعاملات را در رابطهای کاربری گرافیکی حالت فوری مدیریت میکنیم این است که عناصری مانند دکمهها حالت خود را برمیگردانند. به عنوان مثال، اگر روی دکمه در یک قاب کلیک شود، clicked() متد true را برمی گرداند. این بسیار متفاوت از رابطهای کاربری گرافیکی حالت حفظ شده است، جایی که ما یک رویداد شنونده را متصل میکنیم، که سپس به صورت ناهمزمان فراخوانی میشود.
برای تغییرات مبتنی بر حالت، ما میتوانیم به سادگی از if استفاده کنیم، مانند نشان دادن فرم برای افزودن حیوانات خانگی تنها در صورتی که پرچم AppState برای نشان دادن آن درست باشد و حتی آن را در غیر این صورت رندر نکنیم.
فرم رندر نسبتا ساده است و پس از کلیک کردن Submit، ما برخی از اعتبار سنجی بسیار اساسی را انجام می دهیم. اگر دادهها خوب باشند، رویدادی را راهاندازی میکنیم تا موضوع پسزمینه را برای افزودن یک حیوان خانگی جدید به پایگاه داده مطلع کنیم.
در یک پیادهسازی واقعی، ما البته در اینجا مقداری مدیریت خطا را اضافه میکنیم، به عنوان مثال، اگر کاربر یک مقدار نامعتبر برای سن وارد کند، یک پیام خطا با فرمت زیبا را به کاربر نشان میدهد.
در زیر آن، ما به سادگی لیست حیوانات خانگی را تکرار می کنیم و یک فیلد انتخابی برای آن ارائه می کنیم. اگر روی یکی از حیوانات خانگی کلیک شود، رسیدگی به آن از طریق انجام می شود changed() روش در selectable_value، که به ما این اطلاعات را می دهد که مقدار تغییر کرده است و به ما امکان می دهد رویدادی را در رشته پس زمینه برای واکشی داده ها، به روز رسانی AppState و واکشی یک تصویر جدید راه اندازی کنیم.
نوشتن UI نیز در داخل بسیار بصری است egui. ما می توانیم عناصر ساختاری مختلف را به طور دلخواه در تودرتو قرار دهیم و در صورت نیاز عناصر افقی و عمودی را اضافه کنیم. egui دارای طیف گسترده ای از ویجت های مختلف است. اما همانطور که در بالا ذکر شد، ساخت ویجت های کاملاً سفارشی با استفاده از API رندر آنها نیز امکان پذیر است.
با وجود پنل لیست ما، اکنون می توانیم خود را پیاده سازی کنیم details پانل. این بخش کمی ساده تر است زیرا آنقدر تعاملی نیست.
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading(“Details”);
if let Some(pet) = &self.app_state.selected_pet {
ui.vertical(|ui| {
ui.horizontal(|ui| {
if ui.button(“Delete”).clicked() {
let _ =
self.background_event_sender.send(Event::DeletePetFromDB(
ctx.clone(),
self.db_con.clone(),
pet.id,
));
}
});
ui.separator();
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label(“id:”);
ui.label(“name:”);
ui.label(“age”);
ui.label(“kind”);
});
ui.end_row();
ui.vertical(|ui| {
ui.label(pet.id.to_string());
ui.label(&pet.name);
ui.label(pet.age.to_string());
ui.label(&pet.kind.0);
});
});
ui.separator();
if let Some(ref pet_image) = self.app_state.pet_image {
ui.add(egui::Image::from_uri(pet_image).max_width(200.0));
}
});
});
} else {
ui.label(“No pet selected.”);
}
});
});
});
}
}
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
ما رندر می کنیم Details عنوان و delete دکمه اگر دکمه کلیک شود، رویداد حذف را به رشته پس زمینه ارسال می کنیم. سپس به سادگی جزئیات حیوان خانگی را رندر می کنیم و در نهایت استفاده می کنیم egui::image::from_uri، که از موارد ذکر شده در بالا استفاده می کند egui_extras برای واکشی تصویر از URL داده شده، نشانگر بارگیری را در این فاصله نشان می دهد.
اگر حیوان خانگی انتخاب نشده باشد، به سادگی یک برچسب نشان می دهیم.
این برای ساختن رابط کاربری است. خیلی ساده، درست است؟
می توان ساخت یک برنامه پیچیده و تودرتو با این موارد اولیه را تصور کرد، بخش های مختلف را در ماژول های مختلف ساختار داد و آن را در لبه های تعامل در جریان کاربر کنار هم قرار داد.
این روش رندر باعث می شود egui نسیمی برای کار و شروع.
بیایید ببینیم که آیا آنچه ما پیاده سازی کردیم واقعاً کار می کند یا خیر.
تست کردن
ما می توانیم برنامه را با استفاده از آن اجرا کنیم RUST_LOG=info cargo run، که یک رابط کاربری گرافیکی به شکل زیر باز می کند:
ما می توانیم یکی از حیوانات خانگی را در حالت اولیه انتخاب کنیم و جزئیات آن در سمت راست نشان داده می شود. همچنین یک نشانگر بارگیری کوتاه را مشاهده خواهید کرد که در آن تصویر تصادفی یک گربه پس از مدت کوتاهی ظاهر می شود. این رفتار پیشفرض عملکرد بارگیری تصویر در بالا است egui_extras:
در حالت اولیه که ما یک تصویر تصادفی سگ را از سایت دیگری دریافت می کنیم، همین کار برای سگ انجام می شود:
در مرحله بعد، میتوانیم سعی کنیم با استفاده از دکمه، یک حیوان خانگی جدید را به لیست اضافه کنیم، و شکلی که با کلیک در پانل پیمایش در سمت چپ نشان میدهد:
و یکبار زدیم Submit، فرم بازنشانی می شود و یک حیوان خانگی جدید به لیست اضافه می شود و به آدرس زیر هدایت می شود:
خوب است، کار می کند! می توانستیم استفاده کنیم Context::set_style همانطور که در ابتدا ذکر شد برای شخصی سازی کامل برنامه از نظر فونت ها، رنگ ها و موارد مشابه، اما به نظر من، حتی سبک پیش فرض نیز بسیار زیبا به نظر می رسد، به خصوص زمانی که ما فقط قصد داریم یک برنامه نمونه بسازیم. کد کامل این مثال را می توانید در GitHub پیدا کنید.
نتیجه گیری
در این مقاله، ما نگاهی به نحوه ساخت برنامههای رابط کاربری گرافیکی با حالت فوری کراس پلتفرم با استفاده از egui و eframe در Rust انداختیم.
اگرچه egui یک کتابخانه نسبتاً جدید برای ایجاد رابط کاربری گرافیکی است، اکوسیستم اطراف آن عالی است و خیلی سریع در حال رشد است. شروع به کار به دلیل اسناد خوب و برنامههای آزمایشی فراوان که میتوانید کد منبع را بررسی کنید و از آنها بیاموزید، بسیار آسان است.
در این مرحله، اگر بخواهم یک برنامه رابط کاربری گرافیکی با استفاده از Rust بسازم، egui اولین انتخاب من برای انجام این کار خواهد بود.
LogRocket: دید کامل در صفحات وب برای برنامه های Rust
اشکال زدایی برنامه های Rust می تواند دشوار باشد، به خصوص زمانی که کاربران مشکلاتی را تجربه می کنند که بازتولید آن ها دشوار است. اگر علاقه مند به نظارت و ردیابی عملکرد برنامه های Rust خود، نمایش خودکار خطاها، و پیگیری درخواست های شبکه و زمان بارگذاری کند هستید، LogRocket را امتحان کنید.
LogRocket مانند یک DVR برای برنامه های وب و تلفن همراه است که به معنای واقعی کلمه هر چیزی را که در برنامه Rust شما اتفاق می افتد ضبط می کند. به جای حدس زدن چرایی مشکلات، می توانید در مورد وضعیتی که برنامه شما در هنگام بروز مشکل در آن قرار داشت، جمع آوری کرده و گزارش دهید. LogRocket همچنین عملکرد برنامه شما را نظارت می کند، معیارهایی مانند بار CPU مشتری، استفاده از حافظه مشتری و موارد دیگر را گزارش می دهد.
نحوه اشکال زدایی برنامه های Rust خود را مدرن کنید – نظارت را به صورت رایگان شروع کنید.
نوشته ماریو زوپان✏️
از زمان ظهور Electron، که منجر به کمی رنسانس برنامه های دسکتاپ متقابل پلت فرم با استفاده از فناوری وب شد، محدودیت های این رویکرد آشکار شد. زمان آهسته راه اندازی، استفاده از منابع بسیار بالا، و محدودیت های فناوری وب برای ساخت برنامه های رابط کاربری گرافیکی با وفاداری بالا، همچنان کاربران بسیاری از برنامه های کاربردی مبتنی بر الکترون را تا به امروز آزار می دهد.
Electron ساخت اپلیکیشنهای دسکتاپ چند پلتفرمی را بسیار قابل دسترستر از رویکردهای قبلی خود کرد، اما مطمئناً پاسخ همه چیز نبود. مشکل ساخت برنامه های کاربردی چند پلتفرمی غنی، بسیار کارآمد و قوی هنوز حل نشده است و وعده “یک بار بسازید، هر جا اجرا کنید” هنوز در هیچ یک از فناوری های موجود در حال حاضر آشکار نشده است.
با این حال، با علاقهمندی جدید به برنامههای دسکتاپ متقابل پلتفرم و بهبود پلتفرم وب در امتداد خطوط WebGL/WebGPU، و همچنین WebAssembly، پروژههای جالب زیادی به وجود آمدند که تلاش میکنند گامهای بعدی را در تلاش برای ساختن بردارند. پایه و اساس برنامه های کاربردی رابط کاربری گرافیکی کراس پلتفرم.
یکی از این پروژه ها که با استفاده از Rust ساخته شده است، egui است. در این مقاله، خواهیم دید که چگونه میتوانیم یک برنامه گرافیکی ساده و چند پلتفرمی با آن بسازیم.
ما قصد داریم یک برنامه مدیریت حیوانات خانگی بسیار ساده بسازیم که به ما امکان می دهد حیوانات خانگی را اضافه و حذف کنیم و لیست و نمای جزئیات را به ما نشان می دهد.
برای اینکه کل موضوع جالب تر شود، از یک استفاده می کنیم sqlite
پایگاه داده برای ذخیره سازی داده ها، و هر زمان که صفحه جزئیات حیوان خانگی باز شود، تصویری تصادفی از یک گربه یا سگ دریافت می کنیم.
اما ابتدا، بیایید کمی بیشتر در مورد egui و دلیل جالب بودن آن بیاموزیم.
egui / eframe
Egui یک کتابخانه/چارچوب رابط کاربری گرافیکی مبتنی بر Rust و حالت فوری است که بر سهولت استفاده، پاسخگویی و قابلیت حمل تمرکز دارد.
اما صبر کنید، “حالت فوری” به چه معناست؟ خوب، در دنیای رابطهای کاربری گرافیکی، دو راه کلی برای ساخت برنامهها وجود دارد – حالت حفظ شده و حالت فوری، با حالت حفظ شده که بیشتر مواردی را که احتمالاً در مورد چارچوبهای رابط کاربری گرافیکی (مانند QT یا DOM) میدانید پوشش میدهد.
حالت فوری، که برای اولین بار در این ویدیو توسط کیسی موراتوری در سال 2005 ذکر شد، با سادهسازی رویکرد تعامل، رویکرد متفاوتی نسبت به رابط کاربری حالت حفظ شده دارد.
بهجای افزودن کنترلکنندههای پاسخ به تماس به عناصر، مانند دکمهها، در حالی که عنصر در برنامه رابط کاربری گرافیکی «حفظ» میشود و فقط بر اساس درخواست تغییر میکند، در حالت فوری، هر عنصر روی صفحه در هر فریم نمایش داده میشود، بنابراین هرگونه تعاملی انجام میشود. فوری
این تفاوت برخی معاوضه ها را به همراه دارد. به عنوان مثال، چیدمان پیچیده در رابط کاربری گرافیکی حالت فوری بسیار سخت تر است، زیرا اطلاعات زیادی در مورد هر فریم ندارید. راهحلهایی برای این کار وجود دارد، اما در رابط کاربری گرافیکی حالت حفظ شده آسانتر است.
به طور کلی، ساختن و استدلال در مورد رابطهای کاربری گرافیکی حالت فوری بسیار سادهتر است، زیرا تعامل همزمانتر است، که مشکل وضعیت برنامه و ناهمگام بودن آنچه را که روی صفحه نمایش داده میشود، برطرف میکند. این یک مشکل اصلی در رابط کاربری گرافیکی حالت حفظ شده است.
شناخته شده ترین چارچوب رابط کاربری گرافیکی حالت فوری، که egui نیز از آن الهام گرفته شده، Dear imgui است. مخزن egui همچنین دارای یک بخش در مورد معاوضه ها در رابطه با رابط کاربری گرافیکی حالت فوری است که من قطعاً توصیه می کنم آن را بررسی کنید.
سفارشی سازی و دسترسی
یک جنبه جالب از egui
آن است egui
در واقع فقط یک کتابخانه برای ساخت یک رابط کاربری گرافیکی است، نه یک چارچوب. اما چارچوبی هم به نام وجود دارد eframe
، که محیط اطراف را برای ساخت یک برنامه واقعی اضافه می کند.
کتابخانه، egui
را می توان در چارچوب های مختلف، مانند موتورهای بازی، ادغام کرد، که قابلیت استفاده مجدد و ماژولار بودن خوبی را ایجاد می کند.
یکی از جنبه های مهم هر کتابخانه یا چارچوب رابط کاربری گرافیکی، امکان سفارشی سازی است egui
شما را در آنجا تحت پوشش قرار داده است.
میتوانید در صفحه نمایشی فوقالعاده آنها مشاهده کنید، که نه تنها تمام ویجتهای موجود و برخی از برنامههای نمایشی عالی را نشان میدهد، بلکه ابزارهای اشکال زدایی و گزینههای سفارشیسازی را نیز نشان میدهد، بنابراین قبل از غواصی عمیقتر با آن بازی کنید.
Egui همچنین دارای اسناد عالی و نمونه های بسیار زیادی است که برای آموزش و نشان دادن قابلیت های کتابخانه ساخته شده اند: یکی دیگر از جنبههای جالب egui این است که AccessKit را یکپارچه میکند، یک چارچوب بین پلتفرمی برای دسترسی.
اگرچه AccessKit، در زمان نگارش، پیادهسازی کاملی برای وب ندارد، به نظر میرسد یک پروژه بسیار امیدوارکننده برای آسانتر کردن پیادهسازی ویژگیهای دسترسی برای هر پلتفرم باشد.
کراس پلتفرم و وب
همانطور که در ابتدا ذکر شد، هدف از egui
این است که یک کتابخانه برنامه رابط کاربری گرافیکی بین پلتفرمی باشد. با استفاده از eframe_template، می توان برنامه هایی برای لینوکس، مک، ویندوز، اندروید و وب با استفاده از WebGL/WebGPU و WASM ساخت.
پشتیبانی از پلتفرم های مختلف هنوز در مرحله بلوغ است، اما برای دسکتاپ از قبل به طور پایدار کار می کند. با توجه به رویکردی که به وب میرسد، محدودیتهایی وجود دارد، بنابراین egui مطمئناً جایگزینی برای ساخت هیچ و همه برنامههای کاربردی وب نیست، اما سعی نمیکند خود را به این شکل باشد یا تبلیغ کند.
با این حال، وقتی به برنامههای آزمایشی در صفحه نمایشی نگاه میکنید، کاملاً چشمگیر است که از قبل با استفاده از egui حتی در محدودترین محیط آن ممکن است.
در مثالی که در این مقاله آمده است، ما یک برنامه دسکتاپ اول می سازیم که نمی توانیم آن را در WASM کامپایل کنیم زیرا از sqlite به عنوان ذخیره سازی داده استفاده می کنیم.
با این حال، با چند تغییر وابسته به پلت فرم و eframe_template
، میتوانیم در هنگام ساختن برای WASM از ذخیرهسازی دادههای متفاوتی استفاده کنیم، و میتوانیم آن را برای وب نیز کار کنیم، اما این موضوع خارج از محدوده این پست است.
بیایید شروع به ساخت برنامه خود کنیم!
راه اندازی
برای ادامه، تنها چیزی که نیاز دارید، نصب اخیر Rust است که 1.79 آخرین مورد در زمان نگارش این مقاله است.
ابتدا یک پروژه Rust جدید ایجاد کنید:
cargo new rust-egui-example
cd rust-egui-example
در مرحله بعد، فایل Cargo.toml را ویرایش کنید و وابستگی های مورد نیاز خود را اضافه کنید:
[dependencies]
sqlite = "0.36.0"
anyhow = "1.0.82"
eframe = { version = "0.28.0", features = ["wgpu"] }
env_logger = "0.11.3"
log = "0.4.21"
ehttp = { version = "0.5.0", features = ["json"] }
serde = { version = "1.0.203", features = ["derive"] }
egui_extras = { version = "0.28.0", features = ["all_loaders"] }
image = { version = "0.25", features = ["jpeg", "png"] }
را اضافه می کنیم egui
و eframe
وابستگی ها و همچنین egui_extras
، که به ما امکان می دهد از عملکرد بارگذاری تصویر همراه با image
جعبه
ما هم اضافه می کنیم ehttp
برای واکشی تصاویر حیوانات تصادفی و serde
برای سریال سازی و سریال زدایی علاوه بر این، ما نیاز داریم sqlite
برای ذخیره سازی داده های ما، و ما اضافه می کنیم anyhow
برای رسیدگی آسان تر خطا و env_logger
و log
برای قابلیت های ورود به سیستم
و با آن، ما می توانیم شروع به ساخت برنامه خود کنیم!
مدل داده
ابتدا مدل داده های اساسی خود را تعریف می کنیم. ما در حال ساخت یک برنامه مدیریت حیوانات خانگی هستیم، بنابراین به یک برنامه نیاز داریم Pet
نوع ما هم تصمیم گرفتیم استفاده کنیم sqlite
برای ذخیره سازی داده، بنابراین برای کار با مدل داده خود با استفاده از SQL به تنظیماتی نیاز داریم:
#[derive(Debug, PartialEq, Clone)]
struct PetKind(String);
#[derive(Debug, PartialEq, Clone)]
struct Pet {
id: i64,
name: String,
age: i64,
kind: PetKind,
}
این Pet
نوع بسیار ساده است و شامل یک id
، name
، age
و kind
. البته، ما میتوانیم بسیاری از ویژگیهای جالبتر را اضافه کنیم، اما آن را ساده نگه میداریم.
مرحله بعدی ایجاد جدول در ما است sqlite
پایگاه داده:
CREATE TABLE IF NOT EXISTS pets (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL,
kind TEXT NOT NULL
);
INSERT INTO pets (name, age, kind) VALUES ('minka', 9, 'cat');
INSERT INTO pets (name, age, kind) VALUES ('nala', 7, 'dog');
این به سادگی آینه ماست Pet
نوع همچنین می توانیم پرس و جوهایی را که برای عملیات مورد نیاز خود استفاده خواهیم کرد، تعریف کنیم.
- یک حیوان خانگی را از پایگاه داده با شناسه آن واکشی کنید
- همه حیوانات خانگی را از پایگاه داده واکشی کنید
- یک حیوان خانگی را در پایگاه داده قرار دهید
- یک حیوان خانگی را از پایگاه داده حذف کنید
const GET_PET_BY_ID: &str = "SELECT id, name, age, kind FROM pets where id = ?";
const DELETE_PET_BY_ID: &str = "DELETE FROM pets where id = ?";
const INSERT_PET: &str =
"INSERT INTO pets (name, age, kind) VALUES (?, ?, ?) RETURNING id, name, age, kind";
const GET_PETS: &str = "SELECT id, name, age, kind FROM pets";
اکنون، تنها کاری که باید انجام دهیم این است که یک اتصال به an راه اندازی کنیم sqlite
پایگاه داده در برنامه ما و پرس و جو را در اسکریپت اولیه ما در بالا هنگام راه اندازی اجرا کنید.
در این مورد، ما از یک استفاده می کنیم in-memory
نسخه از sqlite
، که تست را آسان تر می کند. این بدان معنی است که هر بار که برنامه شروع می شود پایگاه داده بازنشانی می شود، که برای آزمایش خوب است، اما البته اگر بخواهیم نسخه تولیدی این برنامه را بسازیم، باید تغییر کند:
fn load_init_sql() -> std::io::Result {
fs::read_to_string("./init.sql")
}
fn main() -> Result()> {
env_logger::init();
...
let init_query = load_init_sql().expect("can load init query");
let db_con = sqlite::open(":memory:").expect("can create sqlite db");
db_con
.execute(init_query)
.expect("can initialize sqlite db");
...
}
ما اسکریپت اولیه سازی پایگاه داده را بارگذاری می کنیم، یک اتصال به حافظه باز می کنیم sqlite
پایگاه داده، و اسکریپت را اجرا کنید، پایگاه داده ما را مقداردهی اولیه کنید.
سپس، اجازه دهید توابع کمکی را برای عملیات واکشی و ذخیره سازی داده های فوق الذکر تعریف کنیم. هر یک از آنها را خواهد گرفت db_con
اما در داخل یک Arc
. دلیل آن این است که sqlite::Connection
به خودی خود نمی تواند به طور ایمن در سراسر رشته ها به اشتراک گذاشته شود، بنابراین ما باید آن را قفل کنیم و مطمئن شویم که همه ارجاعات به آن به حساب می آیند.
چرا ما حتی می خواهیم آن را در سراسر رشته ها به اشتراک بگذاریم؟ آیا نمیتوانیم فقط از اتصال استفاده کنیم و هر زمان که نیاز داشتیم در رشته اصلی کوئریها را به صورت همزمان اجرا کنیم؟
ما می توانستیم، اما از آنجایی که قرار است این یک برنامه رابط کاربری گرافیکی بسیار تعاملی باشد، مسدود کردن رشته اصلی ایده خوبی نیست. اگر کاربر روی دکمهای کلیک میکرد، تا زمانی که تمام درخواستهایی که با فشار دادن دکمه فعال میشوند، UI پاسخگو نمیشود.
این رفتاری نیست که ما به دنبال آن هستیم، و بنابراین میخواهیم یک سیستم انتقال رویداد ساده بسازیم، که به رشته اصلی اجازه میدهد رویدادها را به یک رشته پسزمینه ارسال کند، که سپس کار واکشی دادهها را به صورت ناهمزمان انجام میدهد و به اصلی اطلاع میدهد. زمانی که داده ها در دسترس هستند، رشته کنید.
بیایید به کمک کننده های عملیات داده نگاه کنیم. ما با قرار دادن حیوانات خانگی جدید در پایگاه داده شروع می کنیم:
fn insert_pet_to_db(db_con: ArcMutex>, pet: Pet) -> Result {
let con = db_con
.lock()
.map_err(|_| anyhow!("error while locking db connection"))?;
let mut stmt = con.prepare(INSERT_PET)?;
stmt.bind((1, pet.name.as_str()))?;
stmt.bind((2, pet.age))?;
stmt.bind((3, pet.kind.0.as_str()))?;
if stmt.next()? == sqlite::State::Row {
let id = stmt.read::i64, _>(0)?;
let name = stmt.read::String, _>(1)?;
let age = stmt.read::i64, _>(2)?;
let kind = stmt.read::String, _>(3)?;
return Ok(Pet {
id,
name,
age,
kind: PetKind(kind),
});
}
Err(anyhow!("error while inserting pet"))
}
ابتدا باید یک قفل برای اتصال پایگاه داده مشترک خود داشته باشیم، بنابراین مطمئن هستیم که تنها ما از آن استفاده می کنیم. سپس، یک عبارت آماده شده با پرس و جو تعریف شده در بالا ایجاد می کنیم و مقادیر ورودی را به همتایان مربوطه خود در پرس و جو متصل می کنیم.
سپس، کوئری را اجرا می کنیم و یک پرس و جو جدید ایجاد می کنیم Pet
از ردیف برگشتی اگر عملیات ناموفق باشد، خطا را منتشر می کنیم.
بیایید در ادامه به حذف حیوان خانگی نگاه کنیم:
fn delete_pet_from_db(db_con: ArcMutex>, pet_id: i64) -> Result()> {
let con = db_con
.lock()
.map_err(|_| anyhow!("error while locking db connection"))?;
let mut stmt = con.prepare(DELETE_PET_BY_ID)?;
stmt.bind((1, pet_id))?;
if stmt.next()? == sqlite::State::Done {
Ok(())
} else {
Err(anyhow!("error while deleting pet with id {}", pet_id))
}
}
حذف حیوان خانگی بسیار ساده است. ما یک قفل برای اتصال پایگاه داده خود به دست می آوریم و به سادگی کوئری حذف را اجرا می کنیم و هر گونه خطای رخ داده را مدیریت می کنیم:
fn get_pet_from_db(db_con: ArcMutex>, pet_id: i64) -> Resultoption> {
let con = db_con
.lock()
.map_err(|_| anyhow!("error while locking db connection"))?;
let mut stmt = con.prepare(GET_PET_BY_ID)?;
stmt.bind((1, pet_id))?;
if stmt.next()? == sqlite::State::Row {
let id = stmt.read::i64, _>(0)?;
let name = stmt.read::String, _>(1)?;
let age = stmt.read::i64, _>(2)?;
let kind = stmt.read::String, _>(3)?;
return Ok(Some(Pet {
id,
name,
age,
kind: PetKind(kind),
}));
}
Ok(None)
}
fn get_pets_from_db(db_con: ArcMutex>) -> ResultVec> {
let con = db_con
.lock()
.map_err(|_| anyhow!("error while locking db connection"))?;
let mut pets: Vec = vec![];
let mut stmt = con.prepare(GET_PETS)?;
for row in stmt.iter() {
let row = row?;
let id = row.read::i64, _>(0);
let name = row.read::str, _>(1);
let age = row.read::i64, _>(2);
let kind = row.read::str, _>(3);
pets.push(Pet {
id,
name: name.to_owned(),
age,
kind: PetKind(kind.to_owned()),
});
}
Ok(pets)
}
واکشی یک حیوان خانگی از پایگاه داده و واکشی همه حیوانات خانگی کاملاً مشابه هستند. در هر دو مورد، ما دوباره یک قفل برای اتصال پایگاه داده می گیریم، دستور را با استفاده از پرس و جو آماده می کنیم، پرس و جو را اجرا می کنیم و داده های خروجی خود را از داده های ردیف های برگشتی می سازیم.
این برای مدل داده ما و دسترسی به داده است!
در بالا ذکر کردیم که میخواهیم تصاویر تصادفی را برای گربهها و سگها واکشی کنیم تا برنامه کمی زندهتر به نظر برسد، بنابراین بیایید آن قسمت را در ادامه پیادهسازی کنیم.
واکشی تصاویر تصادفی برای گربه ها و سگ ها
برای واکشی تصاویر سگ، از https://dog.ceo/api/breeds/image/random و برای گربه ها از https://api.thecatapi.com/v1/images/search استفاده می کنیم.
هر دوی این APIها به ما اجازه میدهند چند پرسوجو را رایگان انجام دهیم، که برای آزمایش خوب است. اگر چند درخواست آزمایشی انجام دهیم، میتوانیم ببینیم که دادههای برگشتی چگونه به نظر میرسند، و میتوانیم یک مدل داده برای پاسخهای JSON هر دو API ایجاد کنیم:
#[derive(Debug, Deserialize)]
struct CatJSON {
#[serde(alias = "0")]
item: CatJSONInner,
}
#[derive(Debug, Deserialize)]
struct CatJSONInner {
url: String,
}
#[derive(Debug, Deserialize)]
struct DogJSON {
message: String,
}
استفاده می کنیم serde::Deserialize
تا بتوانیم JSON برگردانده شده را به انواع خود غیر سریالی کنیم. مرحله بعدی استفاده از ehttp
برای ایجاد یک درخواست HTTP به API های مربوطه، تجزیه پاسخ و مدیریت URL هایی که دریافت می کنیم:
fn fetch_pet_image(ctx: egui::Context, pet_kind: PetKind, sender: Sender) {
let url = if pet_kind.0 == "dog" {
"https://dog.ceo/api/breeds/image/random"
} else {
"https://api.thecatapi.com/v1/images/search"
};
ehttp::fetch(
ehttp::Request::get(url),
move |result: ehttp::Result| {
if let Ok(result) = result {
let image_url = if pet_kind.0 == "dog" {
if let Ok(json) = result.json::() {
Some(json.message)
} else {
None
}
} else if let Ok(json) = result.json::() {
Some(json.item.url)
} else {
None
};
let _ = sender.send(Event::SetPetImage(image_url));
ctx.request_repaint();
}
},
);
}
اگر به امضای تابع نگاه کنیم، می بینیم egui::Context
، PetKind
، و Sender
. اینها چیست و ما برای چه به آنها نیاز داریم؟
ما نیاز داریم egui::Context
برای درخواست رنگ آمیزی مجدد رابط کاربری گرافیکی، پس از اتمام واکشی داده های خود. این معادل این است که به رشته رندر اطلاع دهیم که داده های جدیدی داریم و برای نمایش آن داده ها باید دوباره رندر شود. در غیر این صورت، اگر هیچ تعاملی اتفاق نیفتاد، ممکن است رابط کاربری ثابت بماند تا عمر باتری حفظ شود و دادههای جدید را نشان ندهد.
این PetKind
صرفاً برای بررسی این است که آیا ما یک تصویر برای یک گربه یا یک سگ واکشی می کنیم Sender
قسمت ارسال الف است std::sync::mpsc
کانال، که به ما امکان میدهد دادهها را به رشته رندر اصلی ارسال کنیم، که سپس میتوان آنها را از آنجا دریافت و مدیریت کرد.
در بخش بعدی نگاهی به نحوه عملکرد این مکانیسم مدیریت رویداد خواهیم داشت.
رسیدگی به رویداد
همانطور که در بالا ذکر شد، ما به راهی برای واکشی ناهمزمان داده ها بدون مسدود کردن رشته رندر اصلی نیاز داریم. با این حال، ما همچنین به راهی برای برقراری ارتباط بین این رشته پسزمینه و موضوع اصلی نیاز داریم. برای این منظور از دو مورد استفاده می کنیم std::sync::mpsc
کانالها – یکی برای ارسال رویدادها به رشته پسزمینه و دریافت آنها در آنجا، و دیگری برای ارسال مجدد دادهها به رشته اصلی و دریافت دادهها در آنجا:
fn main() -> Result()> {
...
let (background_event_sender, background_event_receiver) = channel::();
let (event_sender, event_receiver) = channel::();
...
std::thread::spawn(move || {
while let Ok(event) = background_event_receiver.recv() {
let sender = event_sender.clone();
handle_events(event, sender);
}
});
...
}
در main
تابع، ما دو کانال را ایجاد می کنیم، هر کدام با آنها Sender
و Receiver
به پایان می رسد.
سپس، ما یک رشته جدید ایجاد می کنیم – رشته پس زمینه ما – و در آن، منتظر رویدادهایی در background_event_receiver
و برای هر رویداد، رویداد را مدیریت کنید و داده ها را با استفاده از آن به رشته اصلی ارسال کنید event_sender
، Sender
بخشی از کانال تاپیک اصلی
در مرحله بعد، ما را تعریف می کنیم Event
type، که یک enum است که شامل تمام انواع رویدادهای مختلف است که در برنامه به آن نیاز داریم:
enum Event {
SetPets(Vec),
GetPetImage(egui::Context, PetKind),
SetPetImage(Option),
GetPetFromDB(egui::Context, ArcMutex>, i64),
SetSelectedPet(Option),
InsertPetToDB(egui::Context, ArcMutex>, Pet),
DeletePetFromDB(egui::Context, ArcMutex>, i64),
}
- SetPets – لیست جدیدی از حیوانات خانگی واکشی شده را در AppState برنامه تنظیم می کند
- GetPetImage – یک تصویر تصادفی جدید برای یک نوع معین از حیوان خانگی دریافت می کند
- SetPetImage – تصویر حیوان خانگی واکشی شده را در AppState تنظیم می کند
- GetPetFromDB – یک حیوان خانگی با شناسه داده شده (i64) را از پایگاه داده واکشی می کند
- SetSelectedPet — حیوان خانگی انتخاب شده را در AppState برنامه تنظیم می کند
- InsertPetToDB – یک حیوان خانگی جدید به پایگاه داده اضافه می کند
- DeletePetFromDB – یک حیوان خانگی را از پایگاه داده حذف می کند
سپس، ما آن را اجرا می کنیم handle_events
تابعی که در داخل رشته پس زمینه فراخوانی می شود:
fn handle_events(event: Event, sender: Sender) {
match event {
Event::GetPetImage(ctx, pet_kind) => {
fetch_pet_image(ctx, pet_kind, sender);
}
Event::GetPetFromDB(ctx, db_con, pet_id) => {
if let Ok(Some(pet)) = get_pet_from_db(db_con, pet_id) {
let _ = sender.send(Event::SetSelectedPet(Some(pet)));
ctx.request_repaint();
}
}
Event::DeletePetFromDB(ctx, db_con, pet_id) => {
if delete_pet_from_db(db_con.clone(), pet_id).is_ok() {
if let Ok(pets) = get_pets_from_db(db_con) {
let _ = sender.send(Event::SetPets(pets));
ctx.request_repaint();
}
}
}
Event::InsertPetToDB(ctx, db_con, pet) => {
if let Ok(new_pet) = insert_pet_to_db(db_con.clone(), pet) {
if let Ok(pets) = get_pets_from_db(db_con) {
let _ = sender.send(Event::SetPets(pets));
let _ = sender.send(Event::SetSelectedPet(Some(new_pet)));
ctx.request_repaint();
}
}
}
_ => (),
}
}
ما با رویداد ورودی مطابقت می دهیم، داده ها را واکشی می کنیم، داده ها را با استفاده از آن ارسال می کنیم Sender
، و تماس بگیرید ctx.request_repaint()
برای راه اندازی به روز رسانی جدید در موضوع رندر.
در مورد حذف یک حیوان خانگی، لیست جدیدی از حیوانات خانگی را از پایگاه داده دریافت می کنیم، بنابراین تغییر در لیست نمایش داده شده منعکس می شود. برای قرار دادن حیوان خانگی جدید نیز همین کار را انجام می دهیم و آن را نیز تنظیم می کنیم selected_pet
در AppState به حیوان خانگی تازه ایجاد شده، از این رو در هنگام ایجاد به آن پیمایش کنید.
این نیمی از منطق مدیریت رویداد است، منطقی که در رشته پسزمینه است. اما باید یک همتا در رشته رندر وجود داشته باشد که کانال را برای داده های ارسال شده از رشته پس زمینه بررسی می کند، که سپس به AppState اعمال می شود، که به نوبه خود منجر به به روز رسانی رابط کاربری گرافیکی می شود.
این قسمت در handle_gui_events
در داخل PetApp
نوع، که در بخش بعدی و آخر با آن آشنا خواهیم شد:
fn handle_gui_events(&mut self) {
while let Ok(event) = self.event_receiver.try_recv() {
match event {
Event::SetPetImage(pet_image) => {
self.app_state.pet_image = pet_image;
}
Event::SetSelectedPet(pet) => self.app_state.selected_pet = pet,
Event::SetPets(pets) => {
if let Some(ref selected_pet) = self.app_state.selected_pet {
if !pets.iter().any(|p| p.id == selected_pet.id) {
self.app_state.selected_pet = None;
}
}
self.app_state.pets = pets;
}
_ => (),
};
}
}
دوباره، ما روی همه رویدادهای دریافتی تکرار می کنیم و با آنها مطابقت می کنیم. برای هر رویداد، ما AppState را به روز می کنیم. جالب ترین پیاده سازی در است SetPets
، که در آن، اگر حیوان خانگی انتخاب شده قبلی دیگر بخشی از لیست به روز شده نباشد، حیوان خانگی انتخاب شده را بازنشانی می کنیم. این موضوع مرتبط است زیرا اگر حیوان خانگی را حذف کنیم و این کار را انجام ندهیم، نمای جزئیات همچنان حیوان خانگی حذف شده را نشان می دهد.
خوب، اکنون مدل داده، منطق واکشی تصویر و مکانیسم مدیریت رویداد را داریم، و در نهایت میتوانیم همه چیز را سیمکشی کنیم و رابط کاربری گرافیکی خود را بسازیم.
ساخت رابط کاربری گرافیکی
ساختار رابط کاربری به گونه ای است که در سمت چپ پنل لیستی وجود دارد که دکمه افزودن حیوان خانگی جدید و همچنین فرمی برای افزودن حیوانات خانگی جدید را نگه می دارد. با کلیک بر روی یک حیوان خانگی در لیست، جزئیات آن حیوان خانگی و یک تصویر تصادفی برای حیوان خانگی در پنل جزئیات در سمت راست UI نشان داده می شود.
در پایان، باید تقریباً شبیه به این باشد:
بیایید با انواع داده هایی که نیاز داریم شروع کنیم:
struct PetApp {
app_state: AppState,
background_event_sender: Sender,
event_receiver: Receiver,
db_con: ArcMutex>,
}
#[derive(Debug, Clone)]
struct AppState {
selected_pet: Option,
pets: Vec,
pet_image: Option,
add_form: AddForm,
}
#[derive(Debug, Clone)]
struct AddForm {
show: bool,
name: String,
age: String,
kind: String,
}
را تعریف می کنیم PetApp
، که ساختاری است که تمام داده های برنامه را در خود نگه می دارد. نگه می دارد AppState
، که نمایشی از وضعیت فعلی برنامه است، به عنوان مثال، کدام حیوانات خانگی فهرست شده اند، کدام یک انتخاب شده است و غیره، و همچنین AddForm
حالت، که نشان دهنده وضعیت فعلی فرم برای اضافه کردن یک حیوان خانگی جدید است.
PetApp
همچنین شامل اتصال پایگاه داده مشترک، انتهای فرستنده برای رشته پسزمینه، و همچنین انتهای گیرنده برای کانال رویداد برای رشته رندر است، بنابراین میتوانیم مکانیزم مدیریت رویداد را که در بالا پیادهسازی کردیم، اجرا کنیم.
در مرحله بعد، ما را اجرا می کنیم PetApp::new
روش، بنابراین ما می توانیم برنامه خود را مقداردهی اولیه کنیم:
impl PetApp {
fn new(
background_event_sender: Sender,
event_receiver: Receiver,
db_con: sqlite::Connection,
) -> ResultBox> {
let db_con = Arc::new(Mutex::new(db_con));
let pets = get_pets_from_db(db_con.clone())?;
Ok(Box::new(Self {
app_state: AppState {
selected_pet: None,
pets,
pet_image: None,
add_form: AddForm {
show: false,
name: String::default(),
age: String::default(),
kind: String::default(),
},
},
background_event_sender,
event_receiver,
db_con,
}))
}
}
ورودی را بسته بندی می کنیم sqlite::Connection
داخل یک Arc
بنابراین می توانیم با خیال راحت آن را در سراسر رشته ها به اشتراک بگذاریم و لیست اولیه حیوانات خانگی را واکشی کنیم و آن را در AppState تنظیم کنیم. بقیه حالت به سادگی به یک پیش فرض ایمن مقداردهی اولیه می شود.
برمی گردیم a Result
از آنجایی که آن چیزی است eframe
انتظار می رود هنگام ایجاد یک برنامه بومی جدید، که در ادامه به بررسی آن خواهیم پرداخت main
:
fn main() -> Result()> {
...
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_always_on_top()
.with_inner_size([640.0, 480.0]),
..Default::default()
};
...
eframe::run_native(
"PetApp",
options,
Box::new(|context| {
egui_extras::install_image_loaders(&context.egui_ctx);
Ok(PetApp::new(
background_event_sender,
event_receiver,
db_con,
)?)
}),
)
.map_err(|e| anyhow!("eframe error: {}", e))
}
ما برخی از گزینه های برنامه مانند اندازه پنجره را تعریف می کنیم و سپس استفاده می کنیم eframe::run_native
برای مقداردهی اولیه برنامه رابط کاربری گرافیکی.
یکی از مواردی که در اینجا باید به آن اشاره کرد، لودرهای egui_extras هستند که در حین ایجاد برنامه نصب میکنیم. اینها کمکهایی هستند که بارگذاری پویا تصاویر را از روی دیسک یا از یک URL درون آسان میکنند egui
. این عملکرد را می توان سفارشی کرد، اما پیش فرض کاملاً مفید است، نشانگر بارگذاری را در حین واکشی تصویر و سپس نمایش آن را نشان می دهد.
از آنجایی که ما فقط می توانیم از انواعی استفاده کنیم که the را پیاده سازی کنند eframe::App
صفت درون eframe::run_native
، ما باید این ویژگی را اجرا کنیم.
اجرا فقط شامل update
تابعی که دارای امضای زیر است:
-
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame)
طول می کشد Context
و الف Frame
، که ما به آن نیاز نخواهیم داشت. اما احتمالاً می توانید آن را به خاطر بسپارید Context
، زیرا این همان چیزی است که قبلاً برای شروع رنگ آمیزی مجدد در موضوع رندر استفاده کردیم. بنابراین هر زمان که تماس می گیریم context.request_repaint()
، این update
تابع دوباره فراخوانی می شود:
impl eframe::App for PetApp {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
self.handle_gui_events();
egui::CentralPanel::default().show(ctx, |ui| {
egui::SidePanel::left("left panel")
.resizable(false)
.default_width(200.0)
.show_inside(ui, |ui| {
اجرای آن را آغاز می کنیم update
با تماس با ما handle_gui_events
روشی که در بالا پیاده سازی کردیم. این قسمت دریافت کننده کانال را برای رویدادها بررسی می کند و آنها را اعمال می کند، که تضمین می کند که یک AppState به روز خواهیم داشت که وارد مرحله رندر ما می شود.
همانطور که در بالا ذکر شد، با توجه به اینکه egui یک چارچوب رابط کاربری گرافیکی حالت فوری است، هر چیزی که در اینجا تعریف می کنیم در هر فریم رندر می شود. بنابراین اگر به دلیل ارسال یک رویداد، تغییری در اینجا در AppState ایجاد کنیم، بلافاصله در رابط کاربری نمایش داده میشود بدون اینکه نیازی به دستکاری و بهروزرسانی هر مؤلفهای داشته باشیم.
سپس، کلی را تعریف می کنیم CentralPanel
و در داخل آن با رندر کردن پنل لیست سمت چپ شروع کنید، که آن را روی عرض تنظیم می کنیم 200.0
و قابل تغییر اندازه نیست.
در این پانل، ما را نشان خواهیم داد Pets
عنوان، جداکننده، Add new Pet
دکمه و، اگر دکمه فشار داده شد، فرم اضافه کردن یک حیوان خانگی جدید. در زیر آن، یک جداکننده دیگر و لیست حیوانات خانگی:
ui.vertical_centered(|ui| {
ui.heading("Pets");
ui.separator();
if ui.button("Add new Pet").clicked() {
self.app_state.add_form.show = !self.app_state.add_form.show;
}
if self.app_state.add_form.show {
ui.separator();
ui.vertical_centered(|ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label("name:");
ui.label("age");
ui.label("kind");
});
ui.end_row();
ui.vertical(|ui| {
ui.text_edit_singleline(&mut self.app_state.add_form.name);
ui.text_edit_singleline(&mut self.app_state.add_form.age);
ui.text_edit_singleline(&mut self.app_state.add_form.kind);
});
});
if ui.button("Submit").clicked() {
let add_form = &mut self.app_state.add_form;
let age = add_form.age.parse::().unwrap_or(0);
let kind = match add_form.kind.as_str() {
"cat" => PetKind(String::from("cat")),
_ => PetKind(String::from("dog")),
};
let name = add_form.name.to_owned();
if !name.is_empty() && age > 0 {
let _ = self.background_event_sender.send(
Event::InsertPetToDB(
ctx.clone(),
self.db_con.clone(),
Pet {
id: -1,
name,
age,
kind: kind.clone(),
},
),
);
let _ = self
.background_event_sender
.send(Event::GetPetImage(ctx.clone(), kind));
add_form.name = String::default();
add_form.age = String::default();
add_form.kind = String::default();
}
}
});
}
ui.separator();
self.app_state.pets.iter().for_each(|pet| {
if ui
.selectable_value(
&mut self.app_state.selected_pet,
Some(pet.to_owned()),
pet.name.clone(),
)
.changed()
{
let _ = self.background_event_sender.send(Event::GetPetFromDB(
ctx.clone(),
self.db_con.clone(),
pet.id,
));
let _ = self
.background_event_sender
.send(Event::GetPetImage(ctx.clone(), pet.kind.clone()));
}
});
});
});
روشی که ما تعاملات را در رابطهای کاربری گرافیکی حالت فوری مدیریت میکنیم این است که عناصری مانند دکمهها حالت خود را برمیگردانند. به عنوان مثال، اگر روی دکمه در یک قاب کلیک شود، clicked()
متد true را برمی گرداند. این بسیار متفاوت از رابطهای کاربری گرافیکی حالت حفظ شده است، جایی که ما یک رویداد شنونده را متصل میکنیم، که سپس به صورت ناهمزمان فراخوانی میشود.
برای تغییرات مبتنی بر حالت، ما میتوانیم به سادگی از if استفاده کنیم، مانند نشان دادن فرم برای افزودن حیوانات خانگی تنها در صورتی که پرچم AppState برای نشان دادن آن درست باشد و حتی آن را در غیر این صورت رندر نکنیم.
فرم رندر نسبتا ساده است و پس از کلیک کردن Submit
، ما برخی از اعتبار سنجی بسیار اساسی را انجام می دهیم. اگر دادهها خوب باشند، رویدادی را راهاندازی میکنیم تا موضوع پسزمینه را برای افزودن یک حیوان خانگی جدید به پایگاه داده مطلع کنیم.
در یک پیادهسازی واقعی، ما البته در اینجا مقداری مدیریت خطا را اضافه میکنیم، به عنوان مثال، اگر کاربر یک مقدار نامعتبر برای سن وارد کند، یک پیام خطا با فرمت زیبا را به کاربر نشان میدهد.
در زیر آن، ما به سادگی لیست حیوانات خانگی را تکرار می کنیم و یک فیلد انتخابی برای آن ارائه می کنیم. اگر روی یکی از حیوانات خانگی کلیک شود، رسیدگی به آن از طریق انجام می شود changed()
روش در selectable_value
، که به ما این اطلاعات را می دهد که مقدار تغییر کرده است و به ما امکان می دهد رویدادی را در رشته پس زمینه برای واکشی داده ها، به روز رسانی AppState و واکشی یک تصویر جدید راه اندازی کنیم.
نوشتن UI نیز در داخل بسیار بصری است egui
. ما می توانیم عناصر ساختاری مختلف را به طور دلخواه در تودرتو قرار دهیم و در صورت نیاز عناصر افقی و عمودی را اضافه کنیم. egui
دارای طیف گسترده ای از ویجت های مختلف است. اما همانطور که در بالا ذکر شد، ساخت ویجت های کاملاً سفارشی با استفاده از API رندر آنها نیز امکان پذیر است.
با وجود پنل لیست ما، اکنون می توانیم خود را پیاده سازی کنیم details
پانل. این بخش کمی ساده تر است زیرا آنقدر تعاملی نیست.
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.vertical_centered(|ui| {
ui.heading("Details");
if let Some(pet) = &self.app_state.selected_pet {
ui.vertical(|ui| {
ui.horizontal(|ui| {
if ui.button("Delete").clicked() {
let _ =
self.background_event_sender.send(Event::DeletePetFromDB(
ctx.clone(),
self.db_con.clone(),
pet.id,
));
}
});
ui.separator();
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.label("id:");
ui.label("name:");
ui.label("age");
ui.label("kind");
});
ui.end_row();
ui.vertical(|ui| {
ui.label(pet.id.to_string());
ui.label(&pet.name);
ui.label(pet.age.to_string());
ui.label(&pet.kind.0);
});
});
ui.separator();
if let Some(ref pet_image) = self.app_state.pet_image {
ui.add(egui::Image::from_uri(pet_image).max_width(200.0));
}
});
});
} else {
ui.label("No pet selected.");
}
});
});
});
}
}
ما رندر می کنیم Details
عنوان و delete
دکمه اگر دکمه کلیک شود، رویداد حذف را به رشته پس زمینه ارسال می کنیم. سپس به سادگی جزئیات حیوان خانگی را رندر می کنیم و در نهایت استفاده می کنیم egui::image::from_uri
، که از موارد ذکر شده در بالا استفاده می کند egui_extras
برای واکشی تصویر از URL داده شده، نشانگر بارگیری را در این فاصله نشان می دهد.
اگر حیوان خانگی انتخاب نشده باشد، به سادگی یک برچسب نشان می دهیم.
این برای ساختن رابط کاربری است. خیلی ساده، درست است؟
می توان ساخت یک برنامه پیچیده و تودرتو با این موارد اولیه را تصور کرد، بخش های مختلف را در ماژول های مختلف ساختار داد و آن را در لبه های تعامل در جریان کاربر کنار هم قرار داد.
این روش رندر باعث می شود egui
نسیمی برای کار و شروع.
بیایید ببینیم که آیا آنچه ما پیاده سازی کردیم واقعاً کار می کند یا خیر.
تست کردن
ما می توانیم برنامه را با استفاده از آن اجرا کنیم RUST_LOG=info cargo run
، که یک رابط کاربری گرافیکی به شکل زیر باز می کند:
ما می توانیم یکی از حیوانات خانگی را در حالت اولیه انتخاب کنیم و جزئیات آن در سمت راست نشان داده می شود. همچنین یک نشانگر بارگیری کوتاه را مشاهده خواهید کرد که در آن تصویر تصادفی یک گربه پس از مدت کوتاهی ظاهر می شود. این رفتار پیشفرض عملکرد بارگیری تصویر در بالا است egui_extras
:
در حالت اولیه که ما یک تصویر تصادفی سگ را از سایت دیگری دریافت می کنیم، همین کار برای سگ انجام می شود:
در مرحله بعد، میتوانیم سعی کنیم با استفاده از دکمه، یک حیوان خانگی جدید را به لیست اضافه کنیم، و شکلی که با کلیک در پانل پیمایش در سمت چپ نشان میدهد:
و یکبار زدیم Submit
، فرم بازنشانی می شود و یک حیوان خانگی جدید به لیست اضافه می شود و به آدرس زیر هدایت می شود:
خوب است، کار می کند! می توانستیم استفاده کنیم Context::set_style
همانطور که در ابتدا ذکر شد برای شخصی سازی کامل برنامه از نظر فونت ها، رنگ ها و موارد مشابه، اما به نظر من، حتی سبک پیش فرض نیز بسیار زیبا به نظر می رسد، به خصوص زمانی که ما فقط قصد داریم یک برنامه نمونه بسازیم. کد کامل این مثال را می توانید در GitHub پیدا کنید.
نتیجه گیری
در این مقاله، ما نگاهی به نحوه ساخت برنامههای رابط کاربری گرافیکی با حالت فوری کراس پلتفرم با استفاده از egui و eframe در Rust انداختیم.
اگرچه egui یک کتابخانه نسبتاً جدید برای ایجاد رابط کاربری گرافیکی است، اکوسیستم اطراف آن عالی است و خیلی سریع در حال رشد است. شروع به کار به دلیل اسناد خوب و برنامههای آزمایشی فراوان که میتوانید کد منبع را بررسی کنید و از آنها بیاموزید، بسیار آسان است.
در این مرحله، اگر بخواهم یک برنامه رابط کاربری گرافیکی با استفاده از Rust بسازم، egui اولین انتخاب من برای انجام این کار خواهد بود.
LogRocket: دید کامل در صفحات وب برای برنامه های Rust
اشکال زدایی برنامه های Rust می تواند دشوار باشد، به خصوص زمانی که کاربران مشکلاتی را تجربه می کنند که بازتولید آن ها دشوار است. اگر علاقه مند به نظارت و ردیابی عملکرد برنامه های Rust خود، نمایش خودکار خطاها، و پیگیری درخواست های شبکه و زمان بارگذاری کند هستید، LogRocket را امتحان کنید.
LogRocket مانند یک DVR برای برنامه های وب و تلفن همراه است که به معنای واقعی کلمه هر چیزی را که در برنامه Rust شما اتفاق می افتد ضبط می کند. به جای حدس زدن چرایی مشکلات، می توانید در مورد وضعیتی که برنامه شما در هنگام بروز مشکل در آن قرار داشت، جمع آوری کرده و گزارش دهید. LogRocket همچنین عملکرد برنامه شما را نظارت می کند، معیارهایی مانند بار CPU مشتری، استفاده از حافظه مشتری و موارد دیگر را گزارش می دهد.
نحوه اشکال زدایی برنامه های Rust خود را مدرن کنید – نظارت را به صورت رایگان شروع کنید.