Графические приложения на Rust для Windows 10

Первое приложение

Последнее обновление: 15.06.2021

В этой серии статей мы рассмотрим создание графических приложений на языке Rust для ОС Windows. В настоящее время есть ряд проектов и библиотек, которые позволяют создавать графические приложения. В данном случае мы будем воспользуемся проектом Rust for Windows, который официально развивается компанией Microsoft и который позволяет использовать любые Windows API (в том числе Win32 API или Windows 10 API). Официальный репозиторий проекта на github - https://github.com/microsoft/windows-rs.

Установка инструментария

Что нам потребуется для разработки графических приложений для Windows на языке Rust? Естественно у нас должен быть установлен сам компилятор Rust. Также необходио загузить либо Microsoft C++ Build Tools, либо Microsoft Visual Studio (например, можно выбрать бесплатный выруск Community).

Вне зависимости от того, какую именно из этих двух программ мы выбрали, при установке нам надо отметить следующие опции:

  • .NET desktop development / Разработка классических приложений .NET

  • Desktop development with C++ / Разработка классических приложений на C++

  • Universal Windows Platform development / Разработка классических приложений для универсальной платформы Windows

Rust for Windows

Создание проекта

Выберем для проекта какой-нибудь каталог для проекта и перейдем к нему в командной строке с помощью команды cd. Например, в моем случае для проект будет располагаться в папке C:\rust\windows.

Далее с помощью команды

cargo new hello

Создадим в этой папке новый проект с названием "hello".

Rust for Windows - создание проекта Cargo

В папке созданного проекта найдем файл Cargo.toml и откроем его. Добавим в него ссылку на библиотеку (а точнее crate) windows, которая и представляет проект Rust for Windows:

[package]
name = "hello"
version = "0.1.0"
authors = ["Eugene <metanit22@mail.ru>"]
edition = "2018"

[dependencies]
windows = "0.11.0"

[build-dependencies]
windows = "0.11.0"

Файл build.rs

Теперь определим в папке проекта новый файл - build.rs. Этот скрипт автоматически вызывается инфраструктурой Cargo непосредственно перед компиляцией приложения. Определим в этом файле следующее содержимое:

fn main() {
    windows::build! {
        Windows::ApplicationModel::Activation::*,
		Windows::UI::Xaml::Controls::TextBlock,
        Windows::UI::Xaml::*,
    };
}

Этот скрипт определяет те части Windows API, которые мы собираемся использовать в своем проекте. Они передаются в макрос windows::build!. Windows имеет множество разных API, но не все они нам нужны. И тем больше мы включим в проект различных API, тем медленнее будет происходить сборка приложения.

В итоге при выполнении этого файла макрос windows::build! создаст привязки к используемым компонентам Windows API. В частности, для создания и запуска приложения берем функциональность из модуля Windows::ApplicationModel::Activation и Windows::UI::Xaml. И так как в приложении мы будем использовать метку, которая будет выводить некоторый текст, то мы также подключаем соответствующий компонент (если точнее структуру) Windows::UI::Xaml::Controls::TextBlock.

И макрос windows::build!, используя метаданные этих компонентов, сгенерирует соответствующие типы Rust или привязки. Причем даже если мы указываем только конкретный тип, а не весь модуль, как в случае с Windows::UI::Xaml::Controls::TextBlock, то макрос может включать дополнительные типы, если они необходимы для функционирования этого типа.

Определение кода приложения

Теперь перейдем к файлу main.rs, который по умолчанию создается cargo в папке src и который по умолчанию имеет следующий код:

fn main() {
    println!("Hello, world!");
}

Изменим этот код на следующий:

#![windows_subsystem = "windows"]

mod bindings {
    windows::include_bindings!();
}

use bindings::*;
use windows::*;

use bindings::{
    Windows::ApplicationModel::Activation::*, 
	Windows::UI::Xaml::Controls::TextBlock,
    Windows::UI::Xaml::*,
};

#[implement(
    extend Windows::UI::Xaml::Application,
    override OnLaunched
)]
struct MyApp();

#[allow(non_snake_case)]
impl MyApp {
    fn OnLaunched(&self, _: &Option<LaunchActivatedEventArgs>) -> Result<()> {
        let window = Window::Current()?;
		
		let textBlock: TextBlock = TextBlock::new()?;
		textBlock.SetText("Hello Rust from Metanit.com")?;
		textBlock.SetFontSize(22.0)?;
		
        window.SetContent(textBlock)?;
        window.Activate()
    }
}


fn main() -> Result<()> {
    initialize_mta()?;
    Application::Start(ApplicationInitializationCallback::new(|_| {
        MyApp().new()?;
        Ok(())
    }))
}

Разбор кода

В начале идет атрибут

#![windows_subsystem = "windows"]

Он указывает на используемую подсистему. Подсистема в свою очередь определяет, как ОС будет выполнять приложение. В реальности для этого атрибута есть два возможных значения: windows и console. Значение "windows" указывает, что это будет графическое приложение, и позволяет скрыть консоль. (Без этого атрибута наряду с графическим окном мы увидим консоль приложения)

Далее определяется модуль bindings, который выполняет макрос include_bindings:

mod bindings {
    windows::include_bindings!();
}

Этот макрос добавляет сгенерированные привязки в текущий контекст. И через модуль bindings мы сможем обращаться к сгенерированным привязкам. (Если файлов с кодом много и во все надо добавлять определение данного модуля, то выполнение этого макроса можно вынести в отдельный файл)

Далее идут подключения модулей и их отдельной функциональности:

use bindings::*;
use windows::*;

use bindings::{
    Windows::ApplicationModel::Activation::*, 
	Windows::UI::Xaml::Controls::TextBlock,
    Windows::UI::Xaml::*,
};

Создание объекта приложения

Далее нам надо определить функционал приложения. Для представления приложения в библиотеке windows предназначена структура Windows::UI::Xaml::Application. Однако нам надо определить какой-то свой функционал приложения, чтобы оно имело то поведение и те компоненты, которые нам нужны. И для этого необходимо изменить поведение данной структуры. Но стоит учесть, что язык Rust не поддерживает наследование. То есть мы не можем просто так взять и унаследовать один тип от другого, переопределив его некоторые функции. Однако в библиотеке windows есть атрибут windows::implement, который позволяет применить для структур Rust функционал классов WinRT или любую комбинацию существующих интерфейсов COM и WinRT.

И в данном случае мы определяем структуру MyApp, которая и будет представлять приложение.

#[implement(
    extend Windows::UI::Xaml::Application,
    override OnLaunched
)]
struct MyApp();

Параметр extend атрибута implement указывает, какой тип будет расширять структура. (В данном случае Windows::UI::Xaml::Application).

А параметр override указывает, какой метод будет переопределять структура. Здесь это метод OnLaunched(), в котором мы можем задать визуальный интерфейс и логику приложения.

В реальности атрибут implement обернет структуру MyApp в объект COM/WinRT, который расширяет структуру Application, используя агрегацию.

Поскольку мы указали, что структура MyApp будет переопределять метод OnLaunched(), то нам его надо определить для MyApp. Этот метод запускается при старте приложения:

#[allow(non_snake_case)]
impl MyApp {
    fn OnLaunched(&self, _: &Option<LaunchActivatedEventArgs>) -> Result<()> {
        let window = Window::Current()?;
		
		let textBlock: TextBlock = TextBlock::new()?;
		textBlock.SetText("Hello Rust from Metanit.com")?;
		textBlock.SetFontSize(22.0)?;
		
        window.SetContent(textBlock)?;
        window.Activate()
    }
}

Чтобы указать компилятору, что мы не хотим использовать стиль кода snake_case, применяется атрибут #[allow(non_snake_case)]

В качестве второго параметра метод принимает объект _: &Option<LaunchActivatedEventArgs>, который предоставляет информацию о запуске приложения. Но поскольку нам этот параметр в данном случае не нужен, то в качестве его имени устанавливаем прочерк _.

В самом методе вначале получаем текущее окно приложения:

let window = Window::Current()?;

Далее создаем объект TextBlock, который представляет текстовую метку, с помощью метода new():

let textBlock: TextBlock = TextBlock::new()?;

С помощью метода SetText() устанавливаем текст метки:

textBlock.SetText("Hello Rust from Metanit.com")?;

А с помощью метода SetFontSize() устанавливаем высоту шрифта

textBlock.SetFontSize(22.0)?;

Далее устанавливаем данный объект TextBlock в качестве содержимого окна и активируем окно:

window.SetContent(textBlock)?;
window.Activate()

Функция main

После определения приложения нам надо запустить его в функции main:

fn main() -> Result<()> {
    initialize_mta()?;
    Application::Start(ApplicationInitializationCallback::new(|_| {
        MyApp().new()?;
        Ok(())
    }))
}

В начале идет вызов функции initialize_mta(), которая инициализирует COM.

Входной точкой в приложение является метод Application.Start(). В качестве параметра он принимает объект Windows::UI::Xaml::ApplicationInitializationCallback, который инициализирует приложение.

Для создания объекта ApplicationInitializationCallback применяется статическая функция ApplicationInitializationCallback::new(), которая в качестве параметра принимает объект FnMut(&Option<ApplicationInitializationCallbackParams>) -> Result<()>. То есть фактически мы можем передать в функцию ApplicationInitializationCallback::new мы можем передаеть функцию, которая принимает один параметр. В этой функции мы создаем объект структуры MyApp:

MyApp().new()

И после этого окно приложения отобразится на экране компьютера.

Создание файла манифеста

Поскольку мы использует API Windows 10, то просто скомпилировать exe и запустить его одним кликом не получится. Нам нужно определить файл манифеста приложения. Итак, в папке проекта создадим папку appx. В этой папке определим файл AppxManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  IgnorableNamespaces="uap">
  <Identity
    Name="Metanit.Hello"
    Publisher="CN=Metanit.com"
    Version="1.0.0.0" />
  <Properties>
    <DisplayName>Windows 10 App in Rust</DisplayName>
    <PublisherDisplayName>Metanit.com</PublisherDisplayName>
    <Logo>StoreLogo.png</Logo>
  </Properties>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
  </Dependencies>
  <Resources>
    <Resource Language="en-us" />
  </Resources>
  <Applications>
    <Application Id="App" Executable="hello.exe" EntryPoint="Hello.App">
      <uap:VisualElements DisplayName="Windows 10 App in Rust" Description="Description"
        Square150x150Logo="Square150x150Logo.png" Square44x44Logo="Square44x44Logo.png" BackgroundColor="transparent">
      </uap:VisualElements>
    </Application>
  </Applications>
</Package>

При локальном развертывании приложения можно оставить все эти данные, как они определены здесь. Но при желании можно и изменить. Вкратце рассмотрим его структуру.

Узел Identity

Задает глобально уникальные идентификаторы приложения и содержит следующие свойства:

  • Name: название пакета приложения. Представляет строку от 3 до 50 алфавитно-цифровых символов, точки и дефиса.

  • Publisher: создатель приложения. Должен начинаться с префикса "CN=". Может содержать от 1 до 8192 символов.

  • Version: версия приложения. Строка в формате "Major.Minor.Build.Revision"

Узел Properties

Описывает, как информация о приложении будет отображаться пользователю. Содержит следующие параметры:

  • DisplayName: отображаемое имя приложения

  • PublisherDisplayName: отображаемое имя создателя приложения

  • Logo: путь к логотипу приложения

Dependencies

Описывает зависимости приложения. Содержит элемент <TargetDeviceFamily>, который задает диапазон поддерживаемых устройств с помощью параметров:

  • Name: название семейства устройств, которые поддерживают приложение

  • MinVersion: минимальная версия устройства

  • MaxVersionTested: максимальная версия

Узел Resources

Определяет поддерживаемые языки с помощью вложеных элементов <Resource>. Первый элемент <Resource> задает язык приложения по умолчанию. Должен быть определен как минимум один язык.

Узел Applications

Определяет характеристики для каждого содержащегося в пакете приложения с помощью вложеных элементов <Application>.

Элемент <Application> имеет следующие атрибуты

  • Id: уникальный идентификатор приложения внутри пакета

  • Executable: имя файла приложения (файл exe или dll). Поскольку созданный через cargo проект называется "hello", то по умолчанию он будет компилироваться в файл "hello.exe"

  • EntryPoint: использует свойства Executable и Id

Также с помощью вложенного элемента uap:VisualElements элемент <Application> определяет визуальные настройки приложения. А именно:

  • DisplayName: отображаемое имя приложения - строка от 1 до 256 символов

  • Description: описание приложения - строка от 1 до 2048 символов

  • BackgroundColor: фоновый цвет в формате трехбайтного шестнадцатеричного значения, предваряемого символом "#", либо название цвета в виде строки.

  • Square150x150Logo: путь к логотипу величиной 150x150

  • Square44x44Logo: путь к логотипу величиной 44x144

Поскольку здесь необходимо как минимум три файла изображения, то можно их загрузить ниже. Файл StoreLogo.png:

Файл Square150x150Logo.png:

Файл Square44x44Logo.png:

Компиляция и установка приложения

Для компиляции и установки приложения в папку проекта добавим новый файл register.cmd со следующим содержимым:

cargo build
copy appx\* .\target\debug
cd .\target\debug
powershell -command "Add-AppxPackage -Register AppxManifest.xml"
cd ..\..\

Этот скрипт сначала компилирует приложение командой cargo build. Затем компирует содержимое из папки appx (то есть файл манифеста и изображения) в папку \target\debug, где будет располагаться скомилированный файл приложения. И затем выполняется команда "Add-AppxPackage", которая устанавливает приложения на локальном компьютере.

Таким образом, весь проект будет выглядеть следующим образом:

Графическое приложение Windows 10 на Rust

Теперь, когда все готово, перейдем в командой строке к папке проекта и запустим в ней файл register.cmd:

GUI Приложение Windows 10 на Rust

В итоге в меню Пуск в Windows мы сможем увидеть наше приложение, которое там называется так, как было указано в файле манифеста - Windows 10 App in Rust:

GUI App Windows 10 in Rust

Запустим его и увидим элемент TextBlock с надписью, которую мы определили в коде приложения:

Desktop App for Windows 10 in Rust
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850