суббота, 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.

пятница, 14 августа 2009 г.

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

Это перевод How to host an IContextMenu, part 10 - Composite extensions - groundwork. Автор: Реймонд Чен. Предупреждение: этот пост сильно отличается от оригинала, т.к. вырезано много кода, который реализуется самой Delphi.

Вы могли удивляться: почему это интерфейс IContextMenu работает со смещениями идентификаторов меню вместо того, чтобы просто использовать сами идентификаторы напрямую.

Причина: поддержка нечто, что я называю "компоновка" (compositing).

У вас может быть несколько расширений контекстных меню, которые вы бы хотели скомбинировать в одно большое расширение меню. Оболочка (shell) делает это постоянно. Например, контекстное меню, с которым мы всё это время играли, на самом деле представляет собой композицию нескольких совершенно раздельных расширений меню: статические действия (verbs) из реестра, плюс все COM расширения типа "Отправить", "Открыть с помощью", и любые другие, которые были добавлены установленными вами программами (типа анти-вирусов или архиваторов).

Так что перед тем, как мы напишем свою компоновку, нам надо бы найти второе меню для соединения с первым. Вот простой кусок кода, который делает именно это, реализуя две команды: "Top" и "Next", которые ничего особо интересного не делают.

type
TTopContextMenu = class(TInterfacedObject, IInterface, IContextMenu)
private
m_cids: UINT;
function ValidateCommand(idCmd: UINT_PTR; fUnicode: Boolean; out puOffset: UINT): HRESULT;
function Top(const lpici: CMINVOKECOMMANDINFO): HRESULT;
function Next(const lpici: CMINVOKECOMMANDINFO): 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;
end;

Объявление класса не особо интересно. Мы не выполняем свою прорисовку меню, поэтому нам не нужны интерфейсы IContextMenu2 или IContextMenu3.

У нас есть две команды. Вместо использования имён типа 0 и 1, давайте дадим им читабельные имена.

const
TOPCMD_TOP = 0;
TOPCMD_NEXT = 1;
TOPCMD_MAX = 2;

А вот кусок кода, который поможет нам в обслуживании этих команд.

type
TCommandInfo = record
pszNameA: LPCSTR;
pszNameW: LPCWSTR;
pszHelpA: LPCSTR;
pszHelpW: LPCWSTR;
end;

const
c_rgciTop: array[0..1] of TCommandInfo = (
(pszNameA: 'Top'; pszNameW: 'Top';
pszHelpA: 'The top command'; pszHelpW: 'The top command'),
(pszNameA: 'next'; pszNameW: 'next';
pszHelpA: 'The next command'; pszHelpW: 'The next command'));

Удобно, что наши значения TOPCMD_* служат индексами в массиве c_rgciTop.

Ну, Delphi берёт на себя скучную часть: реализацию IInterface/IUnknown (и все ошибки, что вы могли в ней сделать) - поэтому далее у нас начинается интересная часть: реализация IContextMenu.QueryContextMenu. На что требуется обратить внимание в коде ниже:
  • Проверка наличия свободного места между idCmdFirst и idCmdLast сложна из-за того, что idCmdLast включает в себя последнюю точку - именно поэтому у нас появляется +1. Вот ещё одна причина, чтобы предпочитать диапазоны без включения верхних границ.
  • Если установлен флаг CMF_DEFAULTONLY, то мы вообще не добавляем пункты меню, потому что ни один из них не является пунктом меню по-умолчанию.
function TTopContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst,
idCmdLast, uFlags: UINT): HRESULT;
begin
m_cids := 0;

if idCmdLast - idCmdFirst + 1 >= (TOPCMD_MAX and (not (uFlags and CMF_DEFAULTONLY))) then
begin
InsertMenu(Menu, indexMenu + TOPCMD_TOP, MF_BYPOSITION, idCmdFirst + TOPCMD_TOP, 'Top');
InsertMenu(Menu, indexMenu + TOPCMD_NEXT, MF_BYPOSITION, idCmdFirst + TOPCMD_NEXT, 'Next');
m_cids := TOPCMD_MAX;
end;

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

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

function strcmpiA_invariant(psz1: LPCSTR; psz2: LPCSTR): Integer;
begin
Result := CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE, psz1, -1, psz2, -1) - CSTR_EQUAL;
end;

function strcmpiW_invariant(psz1: LPCWSTR; psz2: LPCWSTR): Integer;
begin
Result := CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, psz1, -1, psz2, -1) - CSTR_EQUAL;
end;

Эти функции похожи на обычные функции сравнения, но они используют инвариантную локаль (invariant locale), поскольку они будут использованы для сравнения канонических строк, а не чего-то, имеющего смысл для пользователя.

Теперь у нас всё готово для написания центральной вспомогательной функции для контекстного меню: нужно сообразить, о какой команде идёт речь.

Комманды могут передаваться в интерфейс IContextMenu (a) по номеру или по имени, и: (b) как ANSI или как Unicode. Суммарно получаем три способа или четыре, смотря как вы предпочитаете считать: "ANSI по номеру" и "Unicode по номеру" - это одно и то же или нет.

function TTopContextMenu.ValidateCommand(idCmd: UINT_PTR; fUnicode: Boolean;
out puOffset: UINT): HRESULT;
var
X: UINT_PTR;
begin
if not IS_INTRESOURCE(idCmd) then
begin
if fUnicode then
begin
idCmd := TOPCMD_MAX;
for X := 0 to TOPCMD_MAX - 1 do
if strcmpiW_invariant(LPCWSTR(X), c_rgciTop[X].pszNameW) = 0 then
Break;
end
else
begin
idCmd := TOPCMD_MAX;
for X := 0 to TOPCMD_MAX - 1 do
begin
if strcmpiA_invariant(LPCSTR(X), c_rgciTop[X].pszNameA) = 0 then
Break;
end;
end
end;

if idCmd < m_cids then
begin
puOffset := idCmd;
Result := S_OK;
Exit;
end;

Result := E_INVALIDARG;
end;

Эта вспомогательная функция принимает параметр "что-то" в форме UINT_PTR и флаг, который указывает, ANSI это "что-то" или же Unicode. Функция сама проверяет является ли "что-то" строкой или же числом. Если это строка, то функция преобразует её в число - с помощью просмотра таблицы команд (с подходящей кодировкой), используя наши вспомогательные функции сравнения. Заметьте, что если строка не найдена, то idCmd будет равно TOPCMD_MAX, что является недопустимым значением (а поэтому будет штатно обработано проверками на ошибки).

После (возможно неудачного) конвертирования в число, результат проверяется на допустимость; если он корректен, то число возвращается из функции для дальнейшей обработки.

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

function TTopContextMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HRESULT;
var
lpicix: PCMInvokeCommandInfoEx;
fUnicode: Boolean;
idCmd: UINT;
begin
lpicix := PCMInvokeCommandInfoEx(@lpici);
fUnicode := (lpici.cbSize >= SizeOf(CMINVOKECOMMANDINFOEX)) and
((lpici.fMask and CMIC_MASK_UNICODE) <> 0);
if fUnicode then
Result := ValidateCommand(UINT_PTR(lpicix^.lpVerbW), fUnicode, idCmd)
else
Result := ValidateCommand(UINT_PTR(lpici.lpVerb), fUnicode, idCmd);
if SUCCEEDED(Result) then
begin
case idCmd of
TOPCMD_TOP: Result := Top(lpici);
TOPCMD_NEXT: Result := Next(lpici);
else
Result := E_INVALIDARG;
end;
end;
end;

В этом коде вопрос "Тут три случая или четыре?" разрешается в пользу "четыре". У нас есть две формы записи CMINVOKECOMMANDINFO: базовая (только ANSI) и расширенная (которая добавляет поддержку Unicode).

Если у нас запись CMINVOKECOMMANDINFOEX и установлен флаг CMIC_MASK_UNICODE, то тогда вместо ANSI-полей должны использоваться Unicode-поля записи CMINVOKECOMMANDINFOEX.

Это означает, что у нас действительно 4 сценария:

  • ANSI строка в поле lpVerb.
  • Число в поле lpVerb.
  • Unicode строка в поле lpVerbW.
  • Число в поле lpVerbW.

После проверки ANSI у нас параметр или же Unicode, мы просим ValidateCommand выполнить проверку команды (verb) и сконвертировать её в число (TOPCMD_*), которое мы будем использовать в case-операторе для вызова действий.

Отсутствие реализации вызова команды по имени - это чрезвычано частый недосмотр в реализациях обработчиком контекстных меню. Из-за этого люди не смогут вызывать ваши команды (verb) программно.

"Почему меня должно волновать, что люди могут вызывать мои действия программно?"

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

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

Ой, постойте, я думаю, что нам надо реализовать наши операции. В конце концов, они не делают ничего выдающегося.

function TTopContextMenu.Top(const lpici: CMINVOKECOMMANDINFO): HRESULT;
begin
MessageBox(lpici.hwnd, 'Top', 'Title', MB_OK);
Result := S_OK;
end;

function TTopContextMenu.Next(const lpici: CMINVOKECOMMANDINFO): HRESULT;
begin
MessageBox(lpici.hwnd, 'Next', 'Title', MB_OK);
Result := S_OK;
end;

Оставшийся метод - это IContextMenu.GetCommandString, который, вероятно, наибольшее количество людей реализуют неправильно, потому что последствия кривых реализаций не видны сразу самому реализующему. Косяки вылезают только у людей, которые пытаются использовать контекстное меню программно.

function TTopContextMenu.GetCommandString(idCmd: UINT_PTR; uType: UINT;
pwReserved: PUINT; pszName: LPSTR; cchMax: UINT): HRESULT;
var
id: UINT;
begin
Result := ValidateCommand(idCmd, (uType and GCS_UNICODE) <> 0, id);
if FAILED(Result) then
begin
if (uType = GCS_VALIDATEA) or (uType = GCS_VALIDATEW) then
Result := S_FALSE;
Exit;
end;

case uType of
GCS_VERBA:
begin
lstrcpynA(pszName, c_rgciTop[id].pszNameA, cchMax);
Result := S_OK;
Exit;
end;

GCS_VERBW:
begin
lstrcpynW(LPWSTR(pszName), c_rgciTop[id].pszNameW, cchMax);
Result := S_OK;
Exit;
end;

GCS_HELPTEXTA:
begin
lstrcpynA(pszName, c_rgciTop[id].pszHelpA, cchMax);
Result := S_OK;
Exit;
end;

GCS_HELPTEXTW:
begin
lstrcpynW(LPWSTR(pszName), c_rgciTop[id].pszHelpW, cchMax);
Result := S_OK;
Exit;
end;

GCS_VALIDATEA,
GCS_VALIDATEW:
begin
Result := S_OK; // Всё, что они хотели - это просто проверка
Exit;
end;
end;

Result := E_NOTIMPL;
end;

Тут мы снова используем метод ValidateCommand для выполнения всей сложной работы по проверке команды, переданной в параметре idCmd, с подсказкой по интерпретации в флаге GCS_UNICODE параметра uType.

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

Если же команда верна, то мы возвращаем запрашиваемую информацию, что реализуется простым оператором case.

Окей, теперь, когда у нас есть это полноценное контекстное меню, мы можем немножко поиграться с ним. Возьмите программу из части 6 или части 9 и сделайте такие изменения в методе 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);

pcm := TTopContextMenu.Create;
try
Menu := CreatePopupMenu;
if Menu <> 0 then
...

Теперь мы получаем наше контекстное меню не вызовом функции GetUIObjectOfFile, а просто создавая реализующий его объект TTopContextMenu. Поскольку наш объект реализует IContextMenu, то весь оставшийся код остаётся без изменений.

Когда вы запустите программу, обратите внимание, что работают даже подсказки к пунктам меню.

Ах, вот сила использования интерфейсов вместо объектов: мы можете полностью изменить реализацию, выбросив на свалку старый объект, а весь код даже ничего и не заменит, до тех пор, пока сам интерфейс не будет изменён.

Окей, сегодня был длинный день, и мы просто делали подготовку - писали код, который понадобится завтра. Никаких прорывов, никаких "ага!" - просто печать кода. Прочитай описание метода, пойми, что надо сделать, и сделай это.

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

четверг, 13 августа 2009 г.

Как управлять IContextMenu, часть 9 - добавление своих команд

Это перевод How to host an IContextMenu, part 9 - Adding custom commands. Автор: Реймонд Чен.

Чтобы изменить меню, вам не всегда нужно реализовывать своё расширение меню.

Параметры indexMenu, idCmdFirst и idCmdLast в методе IContextMenu.QueryContextMenu позволяют вам, серверу, контролировать где в контекстном меню IContextMenu будет вставлять свои команды. Чтобы продемонстрировать это, давайте добавим две свои собственные команды в наше контекстное меню, со скучными именами "Top" и "Bottom".

Во-первых, нам нужно зарезервировать место в идентификаторах нашего меню, так что давайте отрежем кусочек для наших команд:

const 
SCRATCH_QCM_FIRST = 1;
SCRATCH_QCM_LAST = $6FFF;
IDM_TOP = $7000;
IDM_BOTTOM = $7001;

Мы зарезервировали $1000 команд для себя, позволяя IContextMenu играть только с командами от 1 до $6FFF (мы также могли откусить кусок от начала, увеличивая SCRATCH_QCM_FIRST вместо уменьшения SCRATCH_QCM_LAST).
Вернитесь назад к программе из части 6 и сделайте такие изменения:

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(GetUIObjectOfFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
if Menu <> 0 then
try
if InsertMenu(Menu, 0, MF_BYPOSITION, IDM_TOP, 'Top') and
InsertMenu(Menu, 1, MF_BYPOSITION, IDM_BOTTOM, 'Bottom') and
SUCCEEDED(pcm.QueryContextMenu(Menu, 1, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL)) then
begin
g_pcm := Pcm;
Pcm.QueryInterface(IID_IContextMenu2, g_pcm2);
Pcm.QueryInterface(IID_IContextMenu3, g_pcm3);
try
iCmd := Integer(TrackPopupMenuEx(Menu, TPM_RETURNCMD, Pt.X, Pt.y, Handle, nil));
finally
g_pcm3 := nil;
g_pcm2 := nil;
g_pcm := nil;
end;
if iCmd = IDM_TOP then
MessageBox(Handle, 'Top', 'Custom', MB_OK)
else
if iCmd = IDM_BOTTOM then
MessageBox(Handle, 'Bottom', 'Custom', MB_OK)
else
if iCmd > 0 then
begin
FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.fMask := CMIC_MASK_UNICODE or CMIC_MASK_PTINVOKE;
if GetKeyState(VK_CONTROL) < 0 then
Info.fMask := Info.fMask or CMIC_MASK_CONTROL_DOWN;
if GetKeyState(VK_SHIFT) < 0 then
Info.fMask := Info.fMask or CMIC_MASK_SHIFT_DOWN;
Info.hwnd := Handle;
Info.lpVerb := MAKEINTRESOURCEA(iCmd - SCRATCH_QCM_FIRST);
Info.lpVerbW := MAKEINTRESOURCEW(iCmd - SCRATCH_QCM_FIRST);
Info.nShow := SW_SHOWNORMAL;
Info.ptInvoke := Pt;
SetLastError(pcm.InvokeCommand(PCMInvokeCommandInfo(@Info)^));
if GetLastError <> 0 then
RaiseLastOSError;
end;

end;
finally
DestroyMenu(menu);
end;
finally
pcm := nil;
end;
end;

Прежде, чем вызывать IContextMenu.QueryContextMenu, мы добавляем наши собственные команды в меню (поскольку мы используем идентификаторы меню не из диапазона, что мы передаём в IContextMenu.QueryContextMenu, то у нас не возникают конфликты), и потом вызываем IContextMenu.QueryContextMenu, передавая новый уменьшенный диапазон, также указывая позицию для вставки равную 1 вместо 0.

Когда мы передаём меню в IContextMenu.QueryContextMenu, оно выглядит так:

Top
Bottom

Передавая 1-цу как точку для вставки, мы говорим обработчику контекстного меню, что он должен вставлять свои команды в позицию 1 (сдвигая пункты меню по позициям 1 и выше).

Top

... новые пункты меню ...
 
Bottom

После показа этого расширенного контекстного меню, мы проверяем, какую команду выбрал пользователь: либо одну из наших (мы обрабатываем её сразу) или же одну из вставленных команд обработчика меню (мы передаём вызов самому обработчику).

среда, 12 августа 2009 г.

Как управлять IContextMenu, часть 8 - оптимизируем вызов команды по-умолчанию

Это перевод How to host an IContextMenu, part 8 - Optimizing for the default command. Автор: Реймонд Чен.

В программе, что мы написали прошлый раз, можно сделать небольшое улучшение.

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

CMF_DEFAULTONLY
Этот флаг устанавливается, когда пользователь активирует действие по-умолчанию, обычно с помощью двойного щелчка мышью. Этот флаг подсказывает расширению меню ничего не добавлять, если оно не изменяет пункт меню по-умолчанию в меню. Например, расширители всплывающих меню или обработчики drag-and-drop не должны добавлять своих пунктов меню, если этот флаг включён. Расширитель пространства имён (namespace) должен добавлять в меню только пункт по-умолчанию (если таковой есть).

Как следует из этого текста из MSDN, этот флаг указывает реализации IContextMenu, что она должна волноваться только о команде по-умолчанию.

procedure TForm1.FormContextPopup(Sender: TObject; MousePos: TPoint;
var Handled: Boolean);
var
pcm: IContextMenu;
Menu: HMENU;
Info: TCMInvokeCommandInfo;
Id: UINT;
begin
Handled := True;

if SUCCEEDED(GetUIObjectOfFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
if Menu <> 0 then
try
if SUCCEEDED(pcm.QueryContextMenu(Menu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_DEFAULTONLY)) then
begin
Id := GetMenuDefaultItem(Menu, 0, 0);
if Id <> UINT(-1) then
begin
FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.hwnd := Handle;
Info.lpVerb := MAKEINTRESOURCEA(Id - SCRATCH_QCM_FIRST);
SetLastError(pcm.InvokeCommand(Info));
if GetLastError <> 0 then
RaiseLastOSError;
end;
end;
finally
DestroyMenu(Menu);
end;
finally
pcm := nil;
end;
end;

С этим изменением на моей машине время выполнения вызова IContextMenu.QueryContextMenu сократилось со 100 мс до 50 мс. Выигрыш на вашей машине может быть иным. Это сильно зависит от того, сколько расширителей меню установлено на вашей машине и сколько из них действительно учитывают флаг CMF_DEFAULTONLY.

(И этот пример показывает, как важно людям, реализующим интерфейс IContextMenu, учитывать все флаги. Если ваш обработчик контекстого меню не учитывает флаг CMF_DEFAULTONLY, то вы будете частью этой проблемы).

вторник, 11 августа 2009 г.

Как управлять IContextMenu, часть 7 - вызов пункта меню по-умолчанию

Это перевод How to host an IContextMenu, part 7 - Invoking the default verb. Автор: Реймонд Чен.

Когда мы в последний раз оставили нашего героя, мы задумывались, как же вызвать действие по-умолчанию программно. Теперь, когда мы узнали так много про интерактивный случай с IContextMenu, мы можем использовать эту информацию для работы в полностью невизуальном случае.

Ключевой момент здесь - использование описателя HMENU для идентификации пункта меню по-умолчанию и просто прямой его вызов. Вернитесь к программке из пункта 1 и сделайте такие изменения:

procedure TForm1.FormContextPopup(Sender: TObject; MousePos: TPoint;
var Handled: Boolean);
var
pcm: IContextMenu;
Menu: HMENU;
Info: TCMInvokeCommandInfo;
Id: UINT;
begin
Handled := True;

if SUCCEEDED(GetUIObjectOfFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
if Menu <> 0 then
try
if SUCCEEDED(pcm.QueryContextMenu(Menu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL)) then
begin
Id := GetMenuDefaultItem(Menu, 0, 0);
if Id <> UINT(-1) then
begin
FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.hwnd := Handle;
Info.lpVerb := MAKEINTRESOURCEA(Id - SCRATCH_QCM_FIRST);
SetLastError(pcm.InvokeCommand(Info));
if GetLastError <> 0 then
RaiseLastOSError;
end;
end;
finally
DestroyMenu(Menu);
end;
finally
pcm := nil;
end;
end;

Мы добавили вызов функции GetMenuDefaultItem для получения элемента по-умолчанию и установку действия (verb) в форме смешения идентификатора меню (т.е. мы вычитаем начальную точку, которую мы передали в IContextMenu.QueryContextMenu).

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

понедельник, 10 августа 2009 г.

Что означает выделенный жирным шрифтом пункт меню?

Это перевод What does boldface on a menu mean? Автор: Реймонд Чен.

Во вногих контекстных меню вы можете видеть, как один пункт меню выделен жирным шрифтом. Например, если вы щёлкните правой кнопкой по текстовому файлу, скорее всего, вы увидите пункт "Открыть" в самом начале меню, который выделен жирным. Что означает эта подсветка жирным шрифтом?

воскресенье, 9 августа 2009 г.

Как управлять IContextMenu, часть 6 - отображение подсказок меню

Это перевод How to host an IContextMenu, part 6 - Displaying menu help. Автор: Реймонд Чен.

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

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

Это перевод How to host an IContextMenu, part 5 - Handling menu messages. Автор: Реймонд Чен.

Одним из багов, который вылез сразу при нашей первой попытке показать меню - пункты "Открыть с помощью" и "Отправить" не работают.

Причина в том, что эти подменю имеют отложенное создание (что объясняет, почему они пустые, когда вы разворачиваете их) и owner-drawn (что вы не могли заметить из-за первой проблемы, но просто поверьте мне на слово).

Вот тут и используются методы IContextMenu2.HandleMenuMsg и IContextMenu3.HandleMenuMsg2.

Историческое примечание: метод IContextMenu2.HandleMenuMessage находится в своём собственном интерфейсе, а не в базовом IContextMenu, потому что он был поздно добавлен при разработке Windows 95, поэтому решили, что безопаснее объявить новый интерфейс, чем заставить всех, кто писал расширения оболочки Windows 95, переписывать свой код. Метод IContextMenu3.HandleMenuMessage2 был добавлен в Internet Explorer 4 (мне кажется), когда стало ясно, что способность расширителя контекстного меню переопределять возвращаемое значение обработчика сообщения была необходимой для поддержки keyboard accessibility в контекстных меню owner-drawn.

uses
..., ActiveX, JwaShlObj;

type
TForm1 = class(TForm)
...
private
{ Private declarations }
g_pcm2: IContextMenu2;
g_pcm3: IContextMenu3;
end;

Эти две новые переменные отслеживают интерфейсы IContextMenu2 и IContextMenu3 активного всплывающего меню. Нам нужно инициализировать и очистить их вокруг нашего вызова TrackPopupMenuEx:

        Pcm.QueryInterface(IID_IContextMenu2, g_pcm2);
Pcm.QueryInterface(IID_IContextMenu3, g_pcm3);
try
iCmd := Integer(TrackPopupMenuEx(Menu, TPM_RETURNCMD, Pt.X, Pt.y, Handle, nil));
finally
g_pcm3 := nil;
g_pcm2 := nil;
end;

И, наконец, нам нужно вызывать методы HandleMenuMsg/HandleMenuMsg2 в оконной процедуре:

type
TForm1 = class(TForm)
...
protected
{ Protected declarations }
procedure WndProc(var Message: TMessage); override;
end;

procedure TForm1.WndProc(var Message: TMessage);
begin
if Assigned(g_pcm3) then
begin
if SUCCEEDED(g_pcm3.HandleMenuMsg2(Message.Msg, Message.wParam, Message.lParam, Message.Result)) then
Exit;
end
else
if Assigned(g_pcm2) then
if SUCCEEDED(g_pcm2.HandleMenuMsg(Message.Msg, Message.wParam, Message.lParam)) then
begin
Message.Result := 0;
Exit;
end;
inherited;
end;

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

Запустите пример с этими изменениями и заметьте, что пункты меню "Открыть с помощью" и "Отправить" теперь работают, как ожидалось.

В следующий раз: получение подсказок к пунктам меню.

пятница, 7 августа 2009 г.

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

Это перевод How to host an IContextMenu, part 4 - Key context. Автор: Реймонд Чен.

Ещё одним багом, который вы могли заметить в нашей первой попытке показать контекстное меню, является то, что действие команды "Удалить" не зависит от того, держите ли вы клавишу Shift. Вспомните, что удерживание Shift-а изменяет поведение команды удаления, заставляя её удалять файл сразу, без помещения его в Корзину. Но в нашем примере, эта команда всегда предлагает переместить файл в Корзину, даже если вы зажали Shift.

Вы можете увидеть эту разницу в формулировке запроса диалога и его иконке. Если операция - перемещение файла в Корзину, у вас будет иконка Корзины и в тексте вас спросят, хотите ли вы отправить файл в Корзину. Если же операция будет удалением, то вы получите иконку с крестиком, а текст спросит вас, хотите ли вы удалить файл навсегда.

Чтобы передать эту информацию в контекстное меню, вам надо передавать состояния кнопок в запись TCMInvokeCommandInfoEx.

          FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.fMask := CMIC_MASK_UNICODE or CMIC_MASK_PTINVOKE;
if GetKeyState(VK_CONTROL) < 0 then
Info.fMask := Info.fMask or CMIC_MASK_CONTROL_DOWN;
if GetKeyState(VK_SHIFT) < 0 then
Info.fMask := Info.fMask or CMIC_MASK_SHIFT_DOWN;

Добавьте это изменение и заметьте, что теперь диалоги будут учитывать состояние кнопок.

Предупреждение: прежде чем играться с примером, убедитесь, что у вас включены предупреждения на удаление, иначе вы по-настоящему удалите свой файл clock.avi! Чтобы поиграть с командой удаления, вы также можете изменить программу так, что она будет работать с файлом, удаление которого вас устроит.

Упражнение: есть ещё одно место, где состояние кнопок влияет на контекстное меню, а именно: удерживание Shift во время правого клика включает "дополнительные действия" (extended verbs). Эти действия используются редко и поэтому не появляются в контекстном меню по умолчанию, чтобы не мешаться. Домашнее задание: добавьте поддержку дополнительных действий в наш пример.

В следующий раз мы разберёмся с самыми очевидными багами.

четверг, 6 августа 2009 г.

Как управлять IContextMenu, часть 3 - место вызова

Это перевод How to host an IContextMenu, part 3 - Invocation location. Автор: Реймонд Чен.

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

Вы говорите это установкой флага CMIC_MASK_PTINVOKE в поле fMask и указывая точку вызова в поле ptInvoke записи TCMInvokeCommandInfoEx.

          FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.fMask := CMIC_MASK_UNICODE or CMIC_MASK_PTINVOKE;
Info.hwnd := Handle;
Info.lpVerb := MAKEINTRESOURCEA(iCmd - SCRATCH_QCM_FIRST);
Info.lpVerbW := MAKEINTRESOURCEW(iCmd - SCRATCH_QCM_FIRST);
Info.nShow := SW_SHOWNORMAL;
Info.ptInvoke := Pt;

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

В следующий раз мы избавимся ещё от одной проблемы с нашей программой.

среда, 5 августа 2009 г.

Как управлять IContextMenu, часть 2 – показ контекстного меню

Это перевод How to host an IContextMenu, part 2 - Displaying the context menu. Автор: Реймонд Чен.

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

Программирование означает, что иногда вам нужно сложить вместе несколько частей

Это перевод Programming means that sometimes you have to snap two blocks together. Автор: Реймонд Чен.

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

Есть ли какой-то API или быстрый способ узнать, в каком окне находится мышь?

Я ответил: "LEGO Group не делает кубики для всех возможных объектов. Иногда вам просто надо взять две детали LEGO и сложить их вместе. Вот несколько интересных кусочков: GetCursorPos, WindowFromPoint."

Спасибо за ответ. Но WindowFromPoint даёт мне окно объекта по точке. Но мне нужно окно верхнего уровня, содержащее в себе курсор.

Отлично, тогда просто возьмите другой кусочек.

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

Нет, постойте, копирование кода в программу означает сложение блоков кода вместе. Наверное они в конце просто просят готовую программу.

вторник, 4 августа 2009 г.

Подводные камни в обработке WM_CONTEXTMENU

Это перевод Pitfalls in handling the WM_CONTEXTMENU message. Автор: Реймонд Чен.

Прежде чем продолжить нашу дискуссию о IContextMenu, я хотел бы сделать шаг в сторону, чтобы обсудить сообщение WM_CONTEXTMENU.

понедельник, 3 августа 2009 г.

Как управлять IContextMenu, часть 1 - вступительное слово

Это перевод How to host an IContextMenu, part 1 - Initial foray. Автор: Реймонд Чен.

Большая часть документации описывает, как встроиться в структуру контекстного меню оболочки (shell) и быть context menu provider. Если вы будете читать документацию с другой стороны, то вы также узнаете, как управлять (host) контекстным меню. Это первая часть из запланированных 11 частей. Да, одинадцать частей - прошу прощения у тех парней, кто заходит сюда послушать посты о истории. Я попробую разбавлять серию случайными постами.

IContextMenu используется следующим образом:
  • Создание.
  • IContextMenu.QueryContextMenu. Это инициализирует контекстное меню. Во время этого вызова, контекстное меню решает, какие элементы нужно отображать, основываясь на переданных вами флагах.
  • Показать меню или выбрать его команду иным способом, используя IContextMenu.GetCommandString, IContextMenu2.HandleMenuMsg и IContextMenu3.HandleMenuMsg2 для эмуляции взаимодействия пользователя.
  • IContextMenu.InvokeCommand. Этот вызов выполняет команду.
Подробности этих действий объясняются в Creating Context Menu Handlers с точки зрения реализатора (implementor) IContextMenu.
Оболочка сначала вызывает IContextMenu.QueryContextMenu. Она передаёт дескриптор HMENU, который метод может использовать для добавления элементов в контекстное меню. Если пользователь выбирает одну из команд, вызывается IContextMenu.GetCommandString для получения строки-подсказки, которая будет показана в статусной строке Microsoft Windows Explorer. Если пользователь щёлкает по одному из пунктов меню, оболочка вызывает IContextMenu.InvokeCommand. Тогда обработчик может выполнить действия команды.
Прочитайте это с другой стороны и вы увидите, что вам нужно сделать, чтобы самому использовать (host) IContextMenu:
Управляющий IContextMenu сначала вызывает IContextMenu.QueryContextMenu. Он передаёт дескриптор HMENU, который метод может использовать для добавления элементов в контекстное меню. Если пользователь выбирает одну из команд, вызывается IContextMenu.GetCommandString для получения строки-подсказки, которая будет показана в строке статуса вызывающего. Если пользователь щёлкает по одному из пунктов меню, управляющий IContextMenu вызывает IContextMenu.InvokeCommand. Тогда обработчик может выполнить действия команды.
Изучение последствий этой новой интерпретации документации контекстного меню будет нашим вниманием в течение следующих нескольких недель.

Окей, давайте начнем. Начнем, как всегда, с пустого VCL-приложения. Я буду предполагать, что вы уже знакомы с пространством имён оболочки (shell namespace) и pidl, так что я сфокусируюсь на проблеме контекстного меню.

uses
ActiveX, JwaShlObj;

function SHBindToParent(pidl: PItemIDList; const riid: TIID; out ppv; out ppidlLast: PItemIDList): HResult; stdcall; external 'shell32.dll' name 'SHBindToParent';

function GetUIObjectOfFile(wnd: HWND; const pszPath: WideString; const riid: TGUID; out ppv): HRESULT;
var
pidl: PItemIDList;
sfgao: SFGAOF;
psf: IShellFolder;
pidlChild: PItemIDList;
begin
DWord(ppv) := 0;
Result := SHParseDisplayName(PWideChar(pszPath), nil, pidl, 0, sfgao);
if SUCCEEDED(Result) then
try
Result := SHBindToParent(pidl, IID_IShellFolder, psf, pidlChild);
if SUCCEEDED(Result) then
try
Result := psf.GetUIObjectOf(wnd, 1, pidlChild, riid, nil, ppv);
finally
psf := nil;
end;
finally
CoTaskMemFree(pidl);
end;
end;

Эта простая функция берёт путь и получает UI-объект оболочки (shell) для него. Мы конвертируем путь в PIDL с помощью SHParseDisplayName, затем привязываемся (bind) к родителю PIDL с помощью SHBindToParent, после чего запрашиваем у родителя UI-объект с помощью IShellFolder.GetUIObjectOf. Я предполагаю, что у вас достаточно опыта работы с пространством имён, чтобы этот код не вызывал проблем.

(Вспомогательные функции типа SHParseDisplayName и SHBindToParent не делают ничего, чего бы вы не смогли сделать сами. Они просто экономят вам время на печать кода. Когда вы начнёте использовать пространство имён оболочки постоянно, вы соберёте собственную библиотеку небольших вспомогательных функций, типа этих).

Для нашего первого раза, всё, что мы сделаем - просто вызовем действие (verb) "Play" на файле, когда пользователь щёлкнет правой кнопкой (почему правой? Потому что в следующей версии мы покажем контекстное меню).

procedure TForm1.FormContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
const
SCRATCH_QCM_FIRST = 1;
SCRATCH_QCM_LAST = $7FFF;
var
pcm: IContextMenu;
Menu: HMENU;
Info: TCMInvokeCommandInfo;
begin
Handled := True;

if SUCCEEDED(GetUIObjectOfFile(Handle, 'C:\Windows\clock.avi', IID_IContextMenu, pcm)) then
try
Menu := CreatePopupMenu;
if Menu <> 0 then
try
if SUCCEEDED(pcm.QueryContextMenu(Menu, 0, SCRATCH_QCM_FIRST, SCRATCH_QCM_LAST, CMF_NORMAL)) then
begin
FillChar(Info, SizeOf(Info), 0);
Info.cbSize := SizeOf(info);
Info.hwnd := Handle;
Info.lpVerb := 'play';
SetLastError(pcm.InvokeCommand(info));
if GetLastError <> 0 then
RaiseLastOSError;
end;
finally
DestroyMenu(Menu);
end;
finally
pcm := nil;
end;
end;

Как указано в списке выше, сначала мы создаём IContextMenu, затем инициализируем его вызовом IContextMenu.QueryContextMenu. Заметьте, что даже хотя мы не собираемся показывать контекстное меню, нам всё ещё нужно создать popup-меню, потому что этого требует IContextMenu.QueryContextMenu. Однако, мы вообще не будем показывать получившееся меню; вместо того, чтобы просить пользователя сделать выбор из меню, мы сами делаем его за пользователя, выбирая действие "Play", заполняя запись TCMInvokeCommandInfo и вызывая его.

Но как мы узнаем, что действие (verb) будет именно "Play"? В нашем случае мы это знаем потому что мы жёстко зашили в код путь к файлу "clock.avi" и знаем, что AVI файлы имеют действие "Play". Но, конечно же, это не сработает в общем случае (прим. пер.: если вы запускаете пример, то убедитесь, что у вас есть этот файл или введите туда любое другое имя и посмотрите, какие действия есть для этого типа файлов. Кроме того, даже для AVI-файлов действия могут быть переназначены, если какой-то проигрыватель менял ассоциации). Прежде чем вызывать действие по-умолчанию (default verb), давайте сначала сделаем щаг попроще и попросим пользователя выбрать действие для вызова. Вообще-то это упражнение отвлечёт от нашего пути, но в конце-концов мы вернёмся к выбору действия по-умолчанию.

Если код выше - это всё, что вам было нужно (вызвать фиксированное действие для файла), то тогда вам не нужно проходить через остальные действия для контекстного меню. Код выше эквивалентен вызову функции ShellExecuteEx, передавая ей флаг SEE_MASK_INVOKEIDLIST для указания того, что вы хотите, чтобы вызов (invoke) шёл через IContextMenu.

воскресенье, 2 августа 2009 г.

Иногда вы не можете читать текст под курсором

Это перевод Sometimes you can't read the text under the cursor. Автор: Реймонд Чен.

Ранее я написал, как вы можете получать текст под курсором мыши, и вы могли заметить, что эта программа даёт смешанные результаты. Она работает отлично с одними программами, но не с другими.

Это зависит от программы. Некоторые программы были написаны с большим прицелом на читалки экранов, чем другие программы. К примеру, Internet Explorer всегда имел отличную поддержку Active Accessibility, потому что просмотр web - это отличный способ для людей с физическими ограничениями для участия в мире вокруг них.

А другие программы не слишком хорошо делают эту работу. К примеру, VCL программы не обрабатывают сообщение WM_GETOBJECT, поэтому и поддержка у них весьма ограничена (к примеру, вы не сможете прочитать Label).

Поэтому, то, как хорошо будет работать Active Accessibility для конкретной программы, сильно зависит от того, как много усилий автор программы вложил её поддержку.

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

Как получить текст под курсором (указателем мыши)

Это перевод How to retrieve text under the cursor (mouse pointer). Автор: Реймонд Чен.

Microsoft Active Accessibility - это технология, которая предоставляет информацию об объектах на экране для вспомогательных средств, таких как читалки экранов (screen readers). Но это не означает, что только читалки экранов могут использовать её.