Rust でプラットフォーム native な Window を作成する


はじめに

Rust で native な Window を作成する3種です。


Windows

microsoft が提供している windows クレートを利用することで、伝統的な Windows API の作法でプログラミングできます。

[dependencies.windows]
version = "0.45.0"
features = [
    "Win32_Foundation",
    "Win32_Graphics_Gdi",
    "Win32_System_LibraryLoader",
    "Win32_UI_WindowsAndMessaging",
]

昔ながらの Win32 で CreateWindowEx するのと同様なプログラミングモデルとなります。

use windows::{
    core::*,
    Win32::Foundation::*,
    Win32::Graphics::Gdi::{PAINTSTRUCT, BeginPaint, EndPaint, TextOutA},
    Win32::System::LibraryLoader::GetModuleHandleA,
    Win32::UI::WindowsAndMessaging::*,
};

fn main() -> Result<()> {
    unsafe {
        let instance = GetModuleHandleA(None)?;
        let window_class = s!("window");

        let wc = WNDCLASSA {
            hCursor: LoadCursorW(None, IDC_ARROW)?,
            hInstance: instance,
            lpszClassName: window_class,

            style: CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: Some(wndproc),
            ..Default::default()
        };

        let atom = RegisterClassA(&wc);

        CreateWindowExA(
            WINDOW_EX_STYLE::default(),
            window_class,
            s!("hello world window"),
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            None,
            None,
            instance,
            None,
        );

        let mut message = MSG::default();

        while GetMessageA(&mut message, HWND(0), 0, 0).into() {
            DispatchMessageA(&message);
        }

        Ok(())
    }
}

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message {
            WM_PAINT => {
                let mut ps = PAINTSTRUCT::default();
                let hdc = BeginPaint(window, &mut ps);
                TextOutA(hdc, 10, 10, "Hello World".as_bytes());
                EndPaint(window, &mut ps);
                LRESULT(0)
            }
            WM_DESTROY => {
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}

以下のような Window が表示できます。


macOS(cocoa)

Firefox のブラウザエンジンである Servo のサブプロジェクトで管理されている cocoa クレートが利用できます。

[dependencies]
cocoa = "0.24.1"

NSWindow で Window 作成の流れになります。

use cocoa::base::{ selector, nil, NO, id,
};
use cocoa::foundation::{
    NSAutoreleasePool, NSRect, NSPoint, NSSize, NSString,
};
use cocoa::appkit::{
    NSApp, NSWindow, NSApplication,
    NSWindowStyleMask, NSMenu, NSMenuItem,
    NSApplicationActivationPolicyRegular,
    NSBackingStoreBuffered,
};

fn main() {

    unsafe {
        let _pool = NSAutoreleasePool::new(nil);
        let app = NSApp();
        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
        add_menu(&app);

        let window = NSWindow::alloc(nil)
            .initWithContentRect_styleMask_backing_defer_(
                NSRect::new(NSPoint::new(0., 0.), NSSize::new(500., 300.)),
                NSWindowStyleMask::NSTitledWindowMask |
                NSWindowStyleMask::NSClosableWindowMask |
                NSWindowStyleMask::NSResizableWindowMask |
                NSWindowStyleMask::NSMiniaturizableWindowMask |
                NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask,
                NSBackingStoreBuffered,
                NO
            ).autorelease();

        window.cascadeTopLeftFromPoint_(NSPoint::new(20., 20.));
        window.center();

        let title = NSString::alloc(nil).init_str("Hello World!");
        window.setTitle_(title);
        window.makeKeyAndOrderFront_(nil);

        app.run();
    }
}

unsafe fn add_menu(app: &id) {
    // create Menu Bar
    let menubar = NSMenu::new(nil).autorelease();
    let app_menu_item = NSMenuItem::new(nil).autorelease();
    menubar.addItem_(app_menu_item);
    app.setMainMenu_(menubar);

    // create Application menu
    let app_menu = NSMenu::new(nil).autorelease();
    let quit_title = NSString::alloc(nil).init_str("Quit");
    let quit_action = selector("terminate:");
    let quit_key = NSString::alloc(nil).init_str("q");
    let quit_item = NSMenuItem::alloc(nil)
        .initWithTitle_action_keyEquivalent_(quit_title, quit_action, quit_key)
        .autorelease();
    app_menu.addItem_(quit_item);
    app_menu_item.setSubmenu_(app_menu);
}

以下のような Window が表示できます。


winit

winit クレートを使えば、前述のようなプラットフォームの違いを意識せずにプログラミングできます。

winit クレートを定義すれば、

[dependencies]
winit = "0.2"

以下のようにしてウインドウが生成できます。

use winit::{
    event::{ Event, WindowEvent },
    event_loop::{ ControlFlow, EventLoop },
    window::WindowBuilder,
};

fn main() {
    
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
        .with_inner_size(winit::dpi::LogicalSize::new(500, 300))
        .with_title("winit")
        .build(&event_loop).unwrap();

    event_loop.run(move |event, _, control_flow| {
        *control_flow = ControlFlow::Wait;

        match event {
            Event::WindowEvent {
                event: WindowEvent::CloseRequested,
                window_id,
            } if window_id == window.id() => *control_flow = ControlFlow::Exit,
            _ => (),
        }
    });
}

なお、winit はウインドウの生成とイベントループの管理しか提供しないので、ウインドウ内への描画は raw_window_handleraw_display_handle などのハンドルを介したり、他のクレートと連携して実現する必要があります。

以下のような Window が表示できます。


まとめ

Rust で native な Window を作成する方法を紹介しました。

Rust エコシステムでは、未だ GUIライブラリの土壌は発展途上です。

WebAssembly を土台に、WebGPU の方向から発展するのが主流になっていくものと思われます。