gabdube / native-windows-gui

A light windows GUI toolkit for rust
https://gabdube.github.io/native-windows-gui/
MIT License
1.96k stars 127 forks source link

Updating MenuItem text at runtime #275

Open Marko19907 opened 1 year ago

Marko19907 commented 1 year ago

Hi, I'm new to using the native-windows-gui (NWG) Rust library and I'm also new to Rust in general. I apologize if this is a basic question. I'm currently working on a system tray app and I was wondering if there is a way to update the text of a MenuItem at runtime?

Lej77 commented 3 months ago

I wrote a helper function to update a menu item's text using the windows crate:

use nwg::ControlHandle;

/// Copied from [`native_windows_gui::win32::base_helper::to_utf16`].
fn to_utf16(s: &str) -> Vec<u16> {
    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    OsStr::new(s)
        .encode_wide()
        .chain(core::iter::once(0u16))
        .collect()
}

/**
    Return the index of a children menu/menuitem in a parent menu.
    Panic if the menu is not found in the parent.

    Adapted from [`native_windows_gui::win32::menu::menu_index_in_parent`] (in the private `win32` module).
*/
pub fn menu_index_in_parent(menu_handle: ControlHandle) -> Option<u32> {
    if menu_handle.blank() {
        return None;
    }
    let (parent, menu) = menu_handle.hmenu()?;

    // Safety: we check the same preconditions as the nwg crate does when it
    // calls this function on a menu.
    use windows::Win32::UI::WindowsAndMessaging::{GetMenuItemCount, GetSubMenu, HMENU};

    let parent = HMENU(parent as isize);
    let children_count = unsafe { GetMenuItemCount(parent) };
    let mut sub_menu;

    for i in 0..children_count {
        sub_menu = unsafe { GetSubMenu(parent, i) };
        if sub_menu.0 == 0 {
            continue;
        } else if sub_menu.0 == (menu as isize) {
            return Some(i as u32);
        }
    }

    None
}

/// Update the text of a submenu or menu item.
pub fn set_menu_item_text(handle: ControlHandle, text: &str) {
    if handle.blank() {
        panic!("Unbound handle");
    }
    enum MenuItemInfo {
        Position(u32),
        Id(u32),
    }
    let (parent, item_info) = match handle {
        ControlHandle::Menu(parent, _) => {
            // Safety: the handles inside ControlHandle is valid, according to
            // https://gabdube.github.io/native-windows-gui/native-windows-docs/extern_wrapping.html
            // constructing new ControlHandle instances should be considered
            // unsafe.
            if let Some(index) = menu_index_in_parent(handle) {
                (parent, MenuItemInfo::Position(index))
            } else {
                return;
            }
        }
        ControlHandle::MenuItem(parent, id) => (parent, MenuItemInfo::Id(id)),
        _ => return,
    };

    use windows::{
        core::PWSTR,
        Win32::UI::WindowsAndMessaging::{SetMenuItemInfoW, HMENU, MENUITEMINFOW, MIIM_STRING},
    };

    // The code below was inspired by `nwg::win32::menu::enable_menuitem`
    // and: https://stackoverflow.com/questions/25139819/change-text-of-an-menu-item

    let use_position = matches!(item_info, MenuItemInfo::Position(_));
    let value = match item_info {
        MenuItemInfo::Position(p) => p,
        MenuItemInfo::Id(id) => id,
    };

    let text = to_utf16(text);

    let mut info = MENUITEMINFOW::default();
    info.cbSize = core::mem::size_of_val(&info) as u32;
    info.fMask = MIIM_STRING;
    info.dwTypeData = PWSTR(text.as_ptr().cast_mut());

    let _ = unsafe { SetMenuItemInfoW(HMENU(parent as _), value, use_position, &info) };
}

/// Remove a submenu from its parent. Note that this is not done automatically
/// when a menu is dropped.
pub fn remove_menu(menu: &nwg::Menu) {
    if menu.handle.blank() {
        return;
    }
    let Some((parent, _)) = menu.handle.hmenu() else {
        return;
    };

    let Some(index) = menu_index_in_parent(menu.handle) else {
        return;
    };

    use windows::Win32::UI::WindowsAndMessaging::{RemoveMenu, HMENU, MF_BYPOSITION};

    let _ = unsafe { RemoveMenu(HMENU(parent as isize), index, MF_BYPOSITION) };
}