суббота, 15 августа 2009 г.

Как управлять IContextMenu, часть 11 - компоновка расширителей меню - компоновка

Это перевод How to host an IContextMenu, part 11 - Composite extensions - composition. Автор: Реймонд Чен.

Окей, теперь когда у нас есть два контекстных меню, которые мы хотели бы соединить (а именно: "настоящее" от пространства имён оболочки и наше "фальшивое", которое содержит дополнительные команды), мы можем слить и вместе с помощью составного обработчика контекстных меню.

Ядром составного обработчика будет соединение нескольких контекстных меню в один обработчик контекстного меню, используя смещения идентификаторов меню для направления команд.

Всё остальное - это просто тупая печать кода.

type
TContextMenuInfo = record
pcm: IContextMenu;
cids: UINT;
end;
PContextMenuInfo = ^TContextMenuInfo;

TCompositeContextMenu = class(TInterfacedObject, IInterface, IContextMenu, IContextMenu2, IContextMenu3)
private
m_rgcmi: array of TContextMenuInfo;
m_ccmi: UINT;
function Initialize(rgpcm: array of IContextMenu): HRESULT;
function ReduceOrdinal(var pidCmd: UINT_PTR; out ppcmi: PContextMenuInfo): HRESULT;
protected
// *** IContextMenu ***
function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult; stdcall;
function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
function GetCommandString(idCmd: UINT_PTR; uType: UINT; pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult; stdcall;

// *** IContextMenu2 ***
function HandleMenuMsg(uMsg: UINT;
wParam: WPARAM; lParam: LPARAM): HResult; stdcall;

// *** IContextMenu3 ***
function HandleMenuMsg2(uMsg: UINT; wParam: WPARAM; lParam: LPARAM;
out plResult: LRESULT): HResult; stdcall;
end;

Локальная запись TContextMenuInfo содержит информацию о каждом контекстном меню, которое является частью нашего композитного меню. Нам понадобиться указатель на само контекстное меню, а также количество использованных его обработчиком IContextMenu.QueryContextMenu идентификаторов. Мы увидим почему, когда мы закончим реализовывать этот класс.

function TCompositeContextMenu.Initialize(rgpcm: array of IContextMenu): HRESULT;
var
pcmi: PContextMenuInfo;
icmi: Integer;
begin
SetLength(m_rgcmi, Length(rgpcm));

m_ccmi := Length(rgpcm);
for icmi := 0 to m_ccmi - 1 do
begin
pcmi := @(m_rgcmi[icmi]);
pcmi^.pcm := rgpcm[icmi];
pcmi^.cids := 0;
end;

Result := S_OK;
end;

Наша функция инициализации размещает несколько записей TContextMenuInfo и копирует переданные ей IContextMenu (со скрытым вызовом AddRef - спасибо Delphi) для учёта (заметьте, что поле m_ccmi не устанавливается до тех пор, пока мы не будем уверены, что выделение памяти было успешным).

Деструктор потом выполнит эи действия в обратном порядке (в Delphi можно даже его не писать - и интерфейсы и динамические масивы относятся к авто-финализируемым типам).

Снова мы опускаем реализацию скучных интерфейсов COM (спасибо Delphi) и переходим к первому интересному методу: IContextMenu.QueryContextMenu:

function TCompositeContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast, uFlags: UINT): HResult;
var
cids: UINT;
icmi: UINT;
pcmi: PCONTEXTMENUINFO;
begin
cids := 0;

for icmi := 0 to m_ccmi - 1 do
begin
pcmi := @(m_rgcmi[icmi]);
Result := pcmi.pcm.QueryContextMenu(Menu, indexMenu, idCmdFirst, idCmdLast, uFlags);
if SUCCEEDED(Result) then
begin
pcmi.cids := Result;
cids := cids + pcmi.cids;
idCmdFirst := idCmdFirst + pcmi.cids;
end;
end;

Result := MAKE_HRESULT(SEVERITY_SUCCESS, 0, cids);
end;

Мы просим каждое сохранённый обработчик контекстных меню добавить свои команды в контекстное меню. Тут вы видите несколько причин для именно такого возвращаемого значения IContextMenu.QueryContextMenu method. Если вы скажете вызывающему (т.е. контейнеру), сколько идентификаторов меню вы использовали, то контейнер будет знать, солько их ещё осталось для остальных обработчиков. Контейнер потом вернёт полное количество всех идентификаторов меню, использованных контекстными меню.

Ещё одну причину для такого возвращаемого значения IContextMenu.QueryContextMenu можно увидеть в следующем вспомогательном методе:

function TCompositeContextMenu.ReduceOrdinal(var pidCmd: UINT_PTR; out ppcmi: PCONTEXTMENUINFO): HRESULT;
var
icmi: UINT;
pcmi: PContextMenuInfo;
begin
for icmi := 0 to m_ccmi - 1 do
begin
pcmi := @(m_rgcmi[icmi]);
if pidCmd < pcmi.cids then
begin
ppcmi := pcmi;
Result := S_OK;
Exit;
end;
pidCmd := pidCmd - pcmi.cids;
end;
Result := E_INVALIDARG;
end;

Этот метод берёт смещение меню и пытается сообразить: какому обработчику оно принадлежит, используя возвращённое значение от IContextMenu.QueryContextMenu для решения вопроса по разделению пространства идентификаторов. Параметр pidCmd передаётся по ссылке (in/out). На входе он содержит смещение меню для составного контекстного меню; на выходе это смещение для конкретного обработчика контекстных меню, сам обработчик возвращается в параметре ppcmi (только out).

Метод IContextMenu.InvokeCommand, вероятно, самый сложный метод, поскольку ему требуется поддерживать четыре различных способа перенаправления команд.

function TCompositeContextMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;
var
lpicix: PCMInvokeCommandInfoEx;
pcmi: PContextMenuInfo;
fUnicode: Boolean;
idCmd: UINT_PTR;
icmi: UINT;
pszVerbWFake: LPCWSTR;
ppszVerbW: PPWideChar;
pszVerbOrig: LPCSTR;
pszVerbWOrig: LPCWSTR;
begin
lpicix := Pointer(@lpici);
fUnicode := (lpici.cbSize >= SizeOf(TCMInvokeCommandInfoEx)) and
((lpici.fMask and CMIC_MASK_UNICODE) <> 0);
if fUnicode then
idCmd := UINT_PTR(lpicix.lpVerbW)
else
idCmd := UINT_PTR(lpici.lpVerb);

if not IS_INTRESOURCE(idCmd) then
begin
for icmi := 0 to m_ccmi - 1 do
begin
Result := m_rgcmi[icmi].pcm.InvokeCommand(lpici);
if SUCCEEDED(Result) then
Exit;
end;
Result := E_INVALIDARG;
Exit;
end;

Result := ReduceOrdinal(idCmd, pcmi);
if FAILED(Result) then
Exit;

if fUnicode then
ppszVerbW := @lpicix.lpVerbW
else
ppszVerbW := @pszVerbWFake;
pszVerbOrig := lpici.lpVerb;
pszVerbWOrig := ppszVerbW^;

lpici.lpVerb := LPCSTR(idCmd);
ppszVerbW^ := LPCWSTR(idCmd);

Result := pcmi.pcm.InvokeCommand(lpici);

lpici.lpVerb := pszVerbOrig;
ppszVerbW^ := pszVerbWOrig;
end;

После некоторых предварительных телодвижений по поиску идентификатора команды, мы передаём команду обработчику в три шага.

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

Во-вторых, если команду нам передали как число, то мы просим ReduceOrdinal определить, которому контекстному меню оно принадлежит.

В-третьих, мы изменяем запись TCMInvokeCommandInfo, так чтобы она стала пригодной для использования вложенным обработчиком контекстного меню. Это означает изменение поля lpVerb и, возможно, поля lpVerbW, чтобы они содержали новые смещения, относительно вложенного обработчика, а не относительно нас, как контейнера. Это немного усложняется тем фактом, что Unicode-действие lpVerbW может не существовать. Мы скрываем это за локальной переменной pszVerbWFake, которая подсовывается, если у нас нет настоящего lpVerbW.

Okей, теперь, когда вы увидели основную идею по распределению вызовов команд во вложенные обработчики контекстных меню, остальное должно быть относительно простым.

function TCompositeContextMenu.GetCommandString(idCmd: UINT_PTR; uType: UINT;
pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HResult;
var
icmi: UINT;
pcmi: PContextMenuInfo;
begin
if not IS_INTRESOURCE(idCmd) then
begin
for icmi := 0 to m_ccmi - 1 do
begin
Result := m_rgcmi[icmi].pcm.GetCommandString(idCmd, uType, pwReserved, pszName, cchMax);
if Result = S_OK then
Exit;
end;

if (uType = GCS_VALIDATEA) or (uType = GCS_VALIDATEW) then
Result := S_FALSE
else
Result := E_INVALIDARG;
Exit;
end;

Result := ReduceOrdinal(idCmd, pcmi);
if FAILED(Result) then
Exit;

Result := pcmi.pcm.GetCommandString(idCmd, uType, pwReserved, pszName, cchMax);
end;

Метод GetCommandString следует тому же трёх-шаговому образцу, что и InvokeCommand.

Во-первых, мы передаём любую строковую команду напрямую, вызывая вложенные обработчики, пока кто-нибудь не примет её. Если никто не примет - мы возвращаем ошибку (обратите внимание на специальный случай с GCS_VALIDATE, который ожидает S_FALSE, а не код ошибки).

Во-вторых, если команда задана числом, то просим ReduceOrdinal определить, какому вложенному меню она принадлежит.

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

Последние методы будут проще в реализации со следующей вспомогательной функцией:

function IContextMenu_HandleMenuMsg2(const pcm: IContextMenu; uMsg: UINT; wParam: WPARAM; lParam: LPARAM; out plResult: LRESULT): HRESULT;
var
pcm2: IContextMenu2;
pcm3: IContextMenu3;
begin
Result := pcm.QueryInterface(IID_IContextMenu3, pcm3);
if SUCCEEDED(Result) then
Result := pcm3.HandleMenuMsg2(uMsg, wParam, lParam, plResult)
else
begin
Result := pcm.QueryInterface(IID_IContextMenu2, pcm2);
if SUCCEEDED(Result) then
begin
plResult := 0;
Result := pcm2.HandleMenuMsg(uMsg, wParam, lParam);
end;
end;
end;

Эта вспомогательная функция принимает интерфейс IContextMenu и пытается вызвать IContextMenu3.HandleMenuMsg2; если ей это не удаётся, она пытается вызвать IContextMenu2.HandleMenuMsg; а если и это не получается, то она сдаётся.

С этой функцией, мы с лёгкостью можем реализовать два оставшихся метода.

function TCompositeContextMenu.HandleMenuMsg(uMsg: UINT; wParam: WPARAM; lParam: LPARAM): HResult;
var
lres: LRESULT; // выбрасываем
begin
Result := HandleMenuMsg2(uMsg, wParam, lParam, lres);
end;

Метод IContextMenu2.HandleMenuMsg - просто заглушка, перенаправляющая работу методу IContextMenu3.HandleMenuMsg2:

function TCompositeContextMenu.HandleMenuMsg2(uMsg: UINT; wParam: WPARAM;
lParam: LPARAM; out plResult: LRESULT): HResult;
var
icmi: UINT;
begin
for icmi := 0 to m_ccmi - 1 do
begin
Result := IContextMenu_HandleMenuMsg2(m_rgcmi[icmi].pcm, uMsg, wParam, lParam, plResult);
if SUCCEEDED(Result) then
Exit;
end;
Result := E_NOTIMPL;
end;

А метод IContextMenu3.HandleMenuMsg2 просто пробегает по списку обработчиков контекстных меню, спрашивая каждого, не хочет ли он выполнить эту команду, останавливаясь, когда кто-нибудь согласится.

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

function GetCompositeContextMenuForFile(hwnd: HWND; pszPath: LPCWSTR; riid: TGUID; out ppv): HRESULT;
var
rgpcm: array[0..1] of IContextMenu;
CCM: TCompositeContextMenu;
begin
Pointer(ppv) := nil;

Result := GetUIObjectOfFile(hwnd, pszPath, IID_IContextMenu, rgpcm[0]);
if SUCCEEDED(Result) then
begin
rgpcm[1] := TTopContextMenu.Create;
CCM := TCompositeContextMenu.Create;
CCM.Initialize(rgpcm);
Result := CCM.QueryInterface(riid, ppv);
end;
end;

Эта функция строит составное меню, создавая два обработчика контекстных меню, а затем соединяя их в одно меню, используя наш класс-контейнер. Мы можем использовать эту функцию, чтобы внести изменение буквально в одну строчку в OnContextMenu, которую мы меняли в последний раз:

procedure TForm1.FormContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
var
pcm: IContextMenu;
Menu: HMENU;
Info: TCMInvokeCommandInfoEx;
Pt: TPoint;
iCmd: Integer;
begin
Handled := True;

Pt := MousePos;
if (Pt.X = -1) and (Pt.Y = -1) then
begin
Pt.X := 0;
Pt.Y := 0;
end;
Windows.ClientToScreen(Handle, Pt);

if SUCCEEDED(GetCompositeContextMenuForFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
...

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

Ценность этого подхода, по сравнению с методом из части 9, в том, что вы больше не должны координировать изменения (customization) контекстного меню из нескольких частей кода. С предыдущей техникой, вы должны быть уверены, что код, который обновляет подсказки к пунктам меню, должен быть согласован с кодом, который добавляет эти самые пункты меню.

А с новым методом, все эти изменения хранятся в одном месте (в контекстном меню "Top", которое является вложенным в наше композитное контекстное меню), так что оконной процедуре не нужно знать, какие модификации были сделаны в контекстном меню. Это становится ещё более ценным, если у вас есть несколько точек вызова меню, в одних вы показываете меню без изменений, в других - с одним или более изменениями. Централизация знаний о модификациях упрощает дизайн.

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

Существуют и другие вещи, которые вы можете делать с контекстными меню, но я хочу оставить вам их для самостоятельного экспериментирования. Например, вы можете использовать метод IContextMenu.GetCommandString для пробежки по меню и получения language-independent имени команды для каждого пункта. Это удобно, если вы хотите, например, удалить пункт "delete": вы можете найти команду, чьё language-independent имя будет "delete". Это имя не меняется, когда вы меняете язык; оно всегда будет на английском.

И, как мы уже заметили ранее, вам нужно быть готовыми к тому, что многие обработчики контекстных меню не реализуют правильно метод IContextMenu.GetCommandString, так что, скорее всего, вам наверняка встретятся команды, для которых вы просто не сможете получить имя. Them's the breaks.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.