IContextMenu_Impl::InvokeCommand не вызывается при нажатии пункта меню (windows-rs) (интерфейс COM IContextMenu)

Вопрос или проблема

Мы относительно новички в разработке оболочке расширений и в данный момент работаем над реализацией интерфейсов IContextMenu и IShellExtInit. Пока что мы успешно реализовали метод QueryContextMenu, который добавляет элементы в контекстное меню. Однако у нас возникают проблемы с следующим:

  • Метод InvokeCommand не срабатывает при клике на элемент меню.
  • Метод GetCommandString не вызывается при наведении на элемент меню.

Учитывая наш ограниченный опыт с расширениями оболочки, мы подозреваем, что могли упустить какой-то важный шаг или деталь. Мы будем очень благодарны за любые советы или помощь в решении этих проблем.

Манифест crate

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
windows-core = "0.58.0"
# winreg = "0.52.0"

[dependencies.windows]
version = "0.58.0"
features = [
    "Win32_Foundation",
    "Win32_System_SystemServices",
    "Win32_UI_WindowsAndMessaging",
    "Win32_System_Threading",
    "Win32_System_Com",
    "UI",
    "UI_Shell",
    "Win32_UI_Shell",
    "Win32_UI_WindowsAndMessaging",
    "implement",
    "Win32_Graphics_Gdi",
    "Win32_Storage_FileSystem",
    "Win32_System_Diagnostics_Debug",
    "Win32_System_LibraryLoader",
    "Win32_System_ProcessStatus",
    "Win32_UI_Shell_PropertiesSystem",
    "Win32_System_Registry",
    "Win32_UI_Shell_Common"
]

Код crate

use std::ffi::c_void;
use windows::Win32::Foundation::*;
use windows::Win32::System::Threading::GetCurrentProcessId;
use windows::{core::*, Win32::UI::WindowsAndMessaging::MessageBoxA};

use windows::Win32::UI::WindowsAndMessaging::*;
use windows::Win32::{
    Foundation::BOOL,
    System::Com::{IClassFactory, IClassFactory_Impl},
    UI::WindowsAndMessaging::{
        InsertMenuItemW, HMENU, MENUITEMINFOW, MFT_STRING, MIIM_FTYPE, MIIM_STATE, MIIM_STRING,
    },
};
use windows::Win32::{System::Registry::HKEY, UI::Shell::Common::ITEMIDLIST};
use windows::{
    core::*,
    Win32::System::Com::IDataObject,
    Win32::UI::Shell::{
        IContextMenu, IContextMenu_Impl, IShellExtInit, IShellExtInit_Impl, CMINVOKECOMMANDINFO,
    },
};

// Константы для идентификаторов элементов меню
const IDM_ONLINE: u32 = 0;
const IDM_OFFLINE: u32 = 1;
const IDM_SYNC: u32 = 2;

#[no_mangle]
#[allow(non_snake_case, unused_variables)]
pub extern "system" fn DllMain(dll_module: HINSTANCE, call_reason: u32, _: *mut ()) -> bool {
    println!("DllMain");

    // match call_reason {
    //     DLL_PROCESS_ATTACH => notify("attach"),
    //     DLL_PROCESS_DETACH => notify("detach"),
    //     some => {
    //         // notify(&format!("DllMain called"));
    //     }
    // }

    true
}

#[no_mangle]
unsafe extern "system" fn DllGetClassObject(
    rclsid: *const GUID,
    riid: *const GUID,
    ppv: *mut *mut c_void,
) -> HRESULT {
    notify("get class object");

    match *rclsid {
        // OVERLAY_CLSID => IClassFactory::from(WatchedOverlayFactory).query(riid, ppv),
        _ => IClassFactory::from(CustomContextMenuFactory).query(riid, ppv), // _ => IContextMenu::from(CustomContextMenuFactory).query(iid, interface)
        // PROPERTY_STORE_CLSID => IClassFactory::from(WatchedPropertyStoreFactory).query(riid, ppv),
        // _ => CLASS_E_CLASSNOTAVAILABLE,
    }
}

#[no_mangle]
unsafe extern "system" fn DllCanUnloadNow() -> HRESULT {
    // notify("DllCanUnloadNow");
    S_OK
}

#[no_mangle]
pub extern "system" fn DllRegisterServer() -> HRESULT {
    // Реализуйте логику регистрации здесь
    S_OK
}

#[no_mangle]
pub extern "system" fn DllUnregisterServer() -> HRESULT {
    // Реализуйте логику отмены регистрации здесь
    S_OK
}

fn notify(msg: &str) {
    unsafe {
        let pid = GetCurrentProcessId();

        MessageBoxA(
            HWND::default(),
            PCSTR(msg.as_ptr()),
            s!("hello.dll"),
            Default::default(),
        );
    };
}

#[implement(IContextMenu, IShellExtInit)]
struct CustomContextMenu;

impl IContextMenu_Impl for CustomContextMenu_Impl {
    fn QueryContextMenu(
        &self,
        hmenu: HMENU,
        index_menu: u32,
        id_cmd_first: u32,
        id_cmd_last: u32,
        u_flags: u32,
    ) -> Result<()> {
        notify(&format!(
            "query context menu: id: first:{}, last:{}",
            id_cmd_first, id_cmd_last
        ));

        unsafe {
            let menu_item = MENUITEMINFOW {
                cbSize: std::mem::size_of::() as u32,
                fMask: MIIM_FTYPE | MIIM_ID | MIIM_STRING | MIIM_STATE,
                fType: MFT_STRING,
                dwTypeData: PWSTR(to_pcwstr("online").as_mut_ptr()),
                cch: 2 * "online".len() as u32,
                wID: index_menu,
                fState: MFS_ENABLED,
                ..Default::default()
            };
            InsertMenuItemW(hmenu, index_menu, true, &menu_item).unwrap();
            // AppendMenuW(hmenu, MF_STRING, index_menu as usize, w!("Sync"));
        }

        Ok(())
    }

    fn InvokeCommand(&self, lpici: *const CMINVOKECOMMANDINFO) -> Result<()> {
        notify("invoke command");

        unsafe {
            let val = (*lpici).lpVerb;
            let cmd_id = val.as_ptr() as u32;

            match cmd_id {
                IDM_ONLINE => {
                    MessageBoxW(None, w!("Online selected"), w!("Context Menu"), MB_OK);
                }
                IDM_OFFLINE => {
                    MessageBoxW(None, w!("Offline selected"), w!("Context Menu"), MB_OK);
                }
                IDM_SYNC => {
                    MessageBoxW(None, w!("Sync selected"), w!("Context Menu"), MB_OK);
                }
                _ => {}
            }
        }
        Ok(())
    }

    fn GetCommandString(
        &self,
        idcmd: usize,
        uflags: u32,
        reserved: *const u32,
        pszname: PSTR,
        cchmax: u32,
    ) -> Result<()> {
        notify("get command string");
        Ok(())
        // Err(Error::from(E_NOTIMPL))
    }
}

impl IShellExtInit_Impl for CustomContextMenu_Impl {
    fn Initialize(
        &self,
        _pidl_folder: *const ITEMIDLIST, // что-то вроде NULL/ParentFolder
        idata_obj: Option<&IDataObject>, // элемент по которому мы кликнули
        _hkey_prog_id: HKEY, // это HKEY элемента, чтобы вы могли получить больше деталей о нем из реестра
    ) -> Result<()> {
        Ok(())
    }
}

#[implement(IClassFactory)]
pub struct CustomContextMenuFactory;

impl IClassFactory_Impl for CustomContextMenuFactory_Impl {
    fn CreateInstance(
        &self,
        _punkouter: Option<&IUnknown>,
        riid: *const windows_core::GUID,
        ppvobject: *mut *mut ::core::ffi::c_void,
    ) -> Result<()> {
        // notify("creating instance");

        let obj_1: IContextMenu = CustomContextMenu.into();
        let obj = IInspectable::from(CustomContextMenu);

        unsafe {
            obj_1.query(riid, ppvobject).unwrap();
            obj.query(riid, ppvobject).ok()
        }
    }

    fn LockServer(&self, _flock: BOOL) -> windows_core::Result<()> {
        Ok(())
    }
}

pub fn to_pcwstr(s: &str) -> Vec {
    s.encode_utf16().chain(std::iter::once(0)).collect()
}

Мы пытались разместить отладочные уведомления (попапы с сообщением) в обеих функциях InvokeCommand и GetCommandString в первой строке. Но мы заметили, что эти функции вообще не вызываются.

Ответ или решение

Ваша проблема с вызовом метода IContextMenu_Impl::InvokeCommand, который не вызывается при клике на элемент контекстного меню, а также с тем, что метод GetCommandString не срабатывает при наведении, может быть связана с несколькими ключевыми аспектами, которые мы рассмотрим далее.

1. Проверка вызова QueryContextMenu

Первым делом необходимо удостовериться, что ваш метод QueryContextMenu правильно добавляет элементы в контекстное меню, и что идентификаторы команд (ID) уникальны и соответствуют ожидаемым значениям в методе InvokeCommand. Вы добавили элемент с помощью InsertMenuItemW и задали ID равным index_menu. Убедитесь, что значения IDM_ONLINE, IDM_OFFLINE, и IDM_SYNC корректно сопоставляются с ID, которые вы объявили.

Ошибки в сопоставлении ID могут привести к тому, что InvokeCommand будет вызван с неправильным значением, что приведет к тому, что условие match не будет выполнено.

2. Правильная реализация InvokeCommand

Ваша реализация метода InvokeCommand предполагает, что вы правильно извлекаете lpVerb из CMINVOKECOMMANDINFO. Однако, вероятнее всего, lpVerb содержит строку, а не идентификатор команды. Вам нужно будет преобразовать строку, на которую указывает lpVerb, в численный идентификатор, который вы используете для управления логикой ответа. Попробуйте изменить подход к получению идентификатора:

fn InvokeCommand(&self, lpici: *const CMINVOKECOMMANDINFO) -> Result<()> {
    notify("invoke command");

    let verb = unsafe { CStr::from_ptr((*lpici).lpVerb as *const c_char) }.to_string_lossy();
    match verb.as_ref() {
        "online" => {
            MessageBoxW(None, w!("Online selected"), w!("Context Menu"), MB_OK);
        }
        "offline" => {
            MessageBoxW(None, w!("Offline selected"), w!("Context Menu"), MB_OK);
        }
        "sync" => {
            MessageBoxW(None, w!("Sync selected"), w!("Context Menu"), MB_OK);
        }
        _ => {}
    }
    Ok(())
}

3. Проверка регистрации и устройства

Не забывайте, что для правильной работы вашего COM-объекта необходимо, чтобы он был корректно зарегистрирован. Убедитесь, что ваш DllRegisterServer и DllUnregisterServer реализованы и корректно вызываются. Например, нужно убедиться, что в реестре Windows создана запись для вашего контекстного меню, и что все необходимые параметрические значения для CLSID указаны правильно.

4. Определение свойств GetCommandString

Метод GetCommandString, согласно документации, вызывается для предоставления дополнительной информации о команде (например, описание или подсказки). Вам также следует убедиться, что вы реализуете его правильно.

fn GetCommandString(
    &self,
    idcmd: usize,
    uflags: u32,
    reserved: *const u32,
    pszname: PSTR,
    cchmax: u32,
) -> Result<()> {
    notify("get command string");
    if uflags == GCS_HELPTEXT {
        let help_text = to_pcwstr("This will set your status to online.");
        unsafe {
            pszname.copy_from_nonoverlapping(help_text.as_ptr() as *const i8, help_text.len().min(cchmax as usize));
        }
    }
    Ok(())
}

5. Использование инструментов для отладки

Если вышеупомянутые проверки не сработают, воспользуйтесь инструментами для отладки, такими как Sysinternals Suite, чтобы отследить вызовы и исключения, возникающие в вашем коде. Так вы сможете более точно определить точку, где возникает проблема.

Заключение

Убедитесь, что ваш код и регистрация жестко следуют спецификациям COM и хорошо обрабатывают различные условия. Применяйте предложенные изменения и контрольные проверки, чтобы диагностировать и устранить текущие проблемы. Если вам понадобится дальнейшая помощь, всегда полезно обратиться к сообществу разработчиков Windows или форумам по решению вопросов, связанным с COM и расширениями оболочки.

Оцените материал
Добавить комментарий

Капча загружается...