15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rust 2Advent Calendar 2020

Day 3

Rust で Windows プログラミング - 簡易画像ビューア編

Last updated at Posted at 2020-12-02

おはこんばんちは。この記事は Rust 2 Advent Calendar 2020 の3日目の記事です。Rust よりも Win32API の内容がメインです。すみません。

環境

toolchain: stable-x86_64-pc-windows-msvc
rustc 1.48.0 (7eac88abb 2020-11-16)
winapi 0.3.9

画像ビューアを作ろう

こんなものが出来ます

「Open」ボタンを押すとファイルピッカーダイアログが表示されます。画像ファイルを選択すると 640px x 480px 以下にリサイズされた画像が表示されるというもの。簡単のためにウィンドウのリサイズはできない仕様になっています。(写真は若桜鉄道の隼駅です)
2.jpg

CreateWindow

まずウィンドウを作ります。これは以前投稿した記事で説明しているので詳細は割愛します。

static mut BUF: Vec<u8> = Vec::new();

fn main() -> Result<()> {
    unsafe {
        let class_name = l("pinion_window_class");
        let wnd_class = WNDCLASSW {
            style: 0,
            lpfnWndProc: Some(window_proc),
            cbClsExtra: 0,
            cbWndExtra: 0,
            hInstance: ptr::null_mut(),
            hIcon: LoadIconW(ptr::null_mut(), IDI_APPLICATION),
            hCursor: LoadCursorW(ptr::null_mut(), IDI_APPLICATION),
            hbrBackground: GetSysColorBrush(COLOR_MENUBAR),
            lpszMenuName: ptr::null_mut(),
            lpszClassName: class_name.as_ptr(),
        };
        RegisterClassW(&wnd_class);

        let title = format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
        H_WINDOW = CreateWindowExW(
            0,
            class_name.as_ptr(),
            l(&title).as_ptr(),
            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            656,
            551,
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
        );
        ensure!(!H_WINDOW.is_null(), "CreateWindowExW failed.");

        // 無圧縮 Windows ビットマップデータを保存する領域を確保しておく
        // 640px x 480px x (RGB の 3 bytes) = 921,600 bytes
        BUF.reserve(640 * 480 * 3);

        ShowWindow(H_WINDOW, SW_SHOW);
        UpdateWindow(H_WINDOW);
        let mut msg = init::<MSG>();
        loop {
            if GetMessageW(&mut msg, ptr::null_mut(), 0, 0) == 0 {
                break;
            }
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    Ok(())
}

// NULL 終端文字を追加した UTF-16 文字列を返す
fn l(source: &str) -> Vec<u16> {
    source.encode_utf16().chain(Some(0)).collect()
}

ウィンドウプロシージャ

// ビットマップデータの長さを保持しておくグローバル変数
static mut DATA_LEN: usize = 0;

unsafe extern "system" fn window_proc(
    h_wnd: HWND,
    msg: UINT,
    w_param: WPARAM,
    l_param: LPARAM,
) -> LRESULT {
    match msg {
        // CreateWindow 関数が呼ばれたときに送られるメッセージ
        // ここで「Open」ボタンを作る
        WM_CREATE => create(h_wnd),

        // メニューとかボタンが押されたときに送られるメッセージ
        // 「Open」ボタンが押されたらファイルピッカーを開いて
        // 選択された画像をビットマップデータとしてメモリ上に保持する
        WM_COMMAND => command(h_wnd, w_param),

        // ウィンドウの描画が要求されたときに送られるメッセージ
        // ここでメモリ上のビットマップデータをウィンドウに表示する
        WM_PAINT => {
            if DATA_LEN > 0 {
                paint(h_wnd)
            } else {
                return DefWindowProcW(h_wnd, msg, w_param, l_param);
            }
        }

        // ウィンドウが破棄されるときに送られるメッセージ
        // 諸々の終了処理
        WM_DESTROY => {
            DeleteObject(H_FONT as *mut c_void);
            PostQuitMessage(0);
            Ok(())
        }
        _ => return DefWindowProcW(h_wnd, msg, w_param, l_param),
    }
    .map_err(msg_box)
    .ok();
    0
}

ファイルピッカーダイアログ

ファイル選択ダイアログを表示するにはGetOpenFileName関数を使用します。引数にはOPENFILENAME構造体へのポインタを渡します。

unsafe fn open_dialog(h_wnd: HWND) -> Result<String> {
    const MAX_PATH: u32 = 260;
    let mut buf = [0u16; MAX_PATH as usize];

    // 最後の \0 が必要なことに注意
    let filter = l("Image file\0*.jpg;*.png;*.gif;*.bmp\0");
    let title = l("Choose a image file");

    // zeroed 関数で構造体を 0 で初期化
    // 必要最低限のメンバだけ設定する
    let mut ofn = zeroed::<OPENFILENAMEW>();
    ofn.lStructSize = mem::size_of::<OPENFILENAMEW>() as u32;
    ofn.lpstrFilter = filter.as_ptr();
    ofn.lpstrTitle = title.as_ptr();
    ofn.lpstrFile = buf.as_mut_ptr();
    ofn.nMaxFile = MAX_PATH;
    ofn.Flags = OFN_FILEMUSTEXIST;
    ofn.hwndOwner = h_wnd;

    ensure!(GetOpenFileNameW(&mut ofn) != 0, "Cannot get file path.");

    // Windows の世界の UTF-16 を Rust の世界の UTF-8 に変換
    let slice = slice::from_raw_parts(ofn.lpstrFile, MAX_PATH as usize);
    Ok(decode(slice))
}

unsafe fn zeroed<T>() -> T {
    mem::MaybeUninit::<T>::zeroed().assume_init()
}

fn decode(source: &[u16]) -> String {
    decode_utf16(source.iter().take_while(|&n| n != &0).cloned())
        .map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
        .collect()
}

画像ファイルの読み込み

image crate で画像ファイルを読み込んで 640px x 480px 以下にリサイズします。メモリ上のバッファに無圧縮 Windows ビットマップデータとして保存します。各ピクセルの情報を BGR の順番で表すことに注意です( RGB ではない)。

あとビットマップデータは水平方向のバイト数が 4 の倍数である必要があります。4 の倍数になるように0x00のパディングを挿入します。例えば画像の横幅が 619px だった場合、水平方向のバイト数は 619px x BGR の 3bytes = 1857bytes となりますが、これは 4 の倍数ではありません。後ろに0x00を 3bytes 追加して 1860bytes にします。

static mut WIDTH: i32 = 0;
static mut HEIGHT: i32 = 0;

unsafe fn read_image(file_path: &str) -> Result<()> {
    let img = image::open(file_path)?;

    // 640px x 480px より大きければリサイズする
    let img = if img.width() > 640 || img.height() > 480 {
        let new_size = if img.width() as f32 / img.height() as f32 > 1.333 {
            640
        } else {
            if img.width() > img.height() {
                (480.0 / img.height() as f32 * img.width() as f32) as u32
            } else {
                480
            }
        };
        img.resize(new_size, new_size, imageops::Lanczos3)
    } else {
        img
    };
    // リサイズ後の幅、高さをグローバル変数に保存
    WIDTH = img.width() as i32;
    HEIGHT = img.height() as i32;

    // BGR としてデータ取り出す
    let bgr = img.into_bgr();
    ensure!(bgr.len() <= 640 * 480 * 3, "Invalid data length.");

    // 水平方向のバイト数が 4 の倍数でなければパディングを挿入し
    // BUF にビットマップデータをコピーする
    let remain = (3 * WIDTH as usize) % 4;
    if remain > 0 {
        let chunk_size = 3 * WIDTH as usize;
        let line_bytes_len = chunk_size + 4 - remain;
        DATA_LEN = line_bytes_len * HEIGHT as usize;
        let mut p = BUF.as_mut_ptr();
        bgr.chunks(chunk_size).for_each(|c| {
            ptr::copy_nonoverlapping(c.as_ptr(), p, chunk_size);
            p = p.add(line_bytes_len);
        });
    } else {
        DATA_LEN = (WIDTH * HEIGHT * 3) as usize;
        ptr::copy_nonoverlapping(bgr.as_ptr(), BUF.as_mut_ptr(), DATA_LEN);
    };

    // ウィンドウを再描画する
    // ウィンドウプロシージャに WM_PAINT メッセージが送られる
    let rc = RECT {
        top: 32,
        left: 0,
        right: 640,
        bottom: 512,
    };
    InvalidateRect(H_WINDOW, &rc, TRUE);

    // タイトルバーにファイルパスを表示する
    SetWindowTextW(H_WINDOW, l(file_path).as_ptr());
    Ok(())
}

ウィンドウに描画

最後にウィンドウへの描画処理です。Win32API 関数がいっぱい出てきますがよくわかってません(おい)。ここでの注意点はBITMAPINFOHEADER構造体のbiHeightメンバに負の値を渡しているところです。ビットマップデータはボトムアップ、つまり画像の左下が起点になっています。それに対して image crate はトップダウン、つまり画像の左上が起点になったデータを出力します。なのでbiHeightに正の値を渡すと上下反転した画像が表示されることになります。

unsafe fn paint(h_wnd: HWND) -> Result<()> {
    let mut ps = init::<PAINTSTRUCT>();
    let hdc = BeginPaint(h_wnd, &mut ps);

    // ビットマップ情報設定
    let mut bi = zeroed::<BITMAPINFO>();
    bi.bmiHeader = zeroed::<BITMAPINFOHEADER>();
    bi.bmiHeader.biSize = mem::size_of::<BITMAPINFOHEADER>() as u32;
    bi.bmiHeader.biWidth = WIDTH;
    bi.bmiHeader.biHeight = -HEIGHT; // ここ!
    bi.bmiHeader.biPlanes = 1;
    bi.bmiHeader.biBitCount = 24;
    bi.bmiHeader.biSizeImage = DATA_LEN as u32;
    bi.bmiHeader.biCompression = BI_RGB;

    // 指定したデバイスコンテキストと互換性のあるビットマップを作成???
    let h_bmp = CreateCompatibleBitmap(hdc, WIDTH, HEIGHT);

    // BUF のビットマップデータを h_bmp にセットする???
    SetDIBits(
        hdc,
        h_bmp,
        0,
        HEIGHT as u32,
        BUF.as_ptr() as *const c_void,
        &bi,
        DIB_RGB_COLORS,
    );

    // HBITMAP を HDC に変換???
    let h_mdc = CreateCompatibleDC(hdc);
    SelectObject(h_mdc, h_bmp as *mut c_void);

    // センタリングのためのパッディング
    let padding_left = (640 - WIDTH) / 2;
    let padding_top = (480 - HEIGHT) / 2;

    // デバイスコンテキストにビットマップデータを転送???
    BitBlt(
        hdc,
        padding_left,
        padding_top + 32,
        WIDTH,
        HEIGHT,
        h_mdc,
        0,
        0,
        SRCCOPY,
    );

    // 後始末
    DeleteDC(h_mdc);
    DeleteObject(h_bmp as *mut c_void);
    EndPaint(h_wnd, &ps);
    Ok(())
}

まとめ

  • Rust で Windows Native な簡易画像ビューアを作ったよ
  • Windows ビットマップの仕様を完全に理解したよ
  • なにかしら画像処理した結果をリアルタイムに表示するのに使えるかも
  • 写真だけじゃなくて、Rust で計算して生成した画像を表示するのもおもしろそう
  • unsafe なコードばっかりで Rust の安全性はどこへやら…
  • unsafe のスコープはできるだけ小さくした方がいいのかな?生ポインタはOptionで包んだ方がいいのかな?(参考)
  • 全体のソースコードはここ
15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?