В этой серии статей мы рассмотрим создание графических приложений на языке 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
Выберем для проекта какой-нибудь каталог для проекта и перейдем к нему в командной строке с помощью команды cd. Например, в моем
случае для проект будет располагаться в папке C:\rust\windows
.
Далее с помощью команды
cargo new hello
Создадим в этой папке новый проект с названием "hello".
В папке созданного проекта найдем файл 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. Этот скрипт автоматически вызывается инфраструктурой 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:
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>
При локальном развертывании приложения можно оставить все эти данные, как они определены здесь. Но при желании можно и изменить. Вкратце рассмотрим его структуру.
Задает глобально уникальные идентификаторы приложения и содержит следующие свойства:
Name: название пакета приложения. Представляет строку от 3 до 50 алфавитно-цифровых символов, точки и дефиса.
Publisher: создатель приложения. Должен начинаться с префикса "CN="
. Может содержать от 1 до 8192 символов.
Version: версия приложения. Строка в формате "Major.Minor.Build.Revision"
Описывает, как информация о приложении будет отображаться пользователю. Содержит следующие параметры:
DisplayName: отображаемое имя приложения
PublisherDisplayName: отображаемое имя создателя приложения
Logo: путь к логотипу приложения
Описывает зависимости приложения. Содержит элемент <TargetDeviceFamily>
, который задает диапазон поддерживаемых устройств с помощью параметров:
Name: название семейства устройств, которые поддерживают приложение
MinVersion: минимальная версия устройства
MaxVersionTested: максимальная версия
Определяет поддерживаемые языки с помощью вложеных элементов <Resource>
. Первый элемент <Resource>
задает язык приложения по умолчанию. Должен быть определен как минимум один язык.
Определяет характеристики для каждого содержащегося в пакете приложения с помощью вложеных элементов <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",
которая устанавливает приложения на локальном компьютере.
Таким образом, весь проект будет выглядеть следующим образом:
Теперь, когда все готово, перейдем в командой строке к папке проекта и запустим в ней файл register.cmd:
В итоге в меню Пуск в Windows мы сможем увидеть наше приложение, которое там называется так, как было указано в файле манифеста - Windows 10 App in Rust
:
Запустим его и увидим элемент TextBlock с надписью, которую мы определили в коде приложения: