16+
ComputerPrice
НА ГЛАВНУЮ СТАТЬИ НОВОСТИ О НАС




Яндекс цитирования


Версия для печати

Модуль поиска не установлен.

И в дополнение - автозаполнение...

25.02.2004

Роман Батищев

Хотя в практике изложения своих сокровенных мыслей употребление очень личного местоимения "Я" принято считать дурным тоном, избежать этого, порой, никак не удается. Вот, как сейчас, к примеру - хочется обратиться к собственному опыту, и как о нем рассказать иначе, как не от первого лица?

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

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

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

Увиденное же было как deja vue, породившее даже не вопрос, а какое-то чувство, больше похожее на эмоции, которые нельзя выразить: "До каких пор мы будем спотыкаться об одни и те же задачи, решенные до нас, за нас и для нас?"

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

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

Кстати, тем, кто не знаком с COM, но очень хотел бы разобраться в ней, можно порекомендовать замечательную книгу "Сущность технологии COM" очень хорошего автора Дональда Бокса. Ее приобретение, как правило, позволяет избежать дополнительных затрат на покупку еще одной, более толковой книги по интересующей тематике, как это нередко случается.

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

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

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

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

Итак, чтобы получить в свое распоряжение всю мощь функций автозаполнения, скрытую в недрах Windows, необходимо поближе познакомиться с COM-объектом, чье физическое имя скрыто за константой CLSID_AutoComplete.

Объект выставляет два интерфейса, IAutoComplete и IAutoComplete2, причем второй является расширением первого, наследуя от него.

Описание интерфейса IAutoComplete содержит два метода, Enable() и Init():

HRESULT Enable(BOOL fEnable);

HRESULT Init(HWND hwndEdit, IUnknown *punkACL, LPCOLESTR pwszRegKeyPath, LPCOLESTR pwszQuickComplete);

Метод Enable() позволяет "включать" и "выключать" автозаполнение. В качестве единственного параметра передается 32-рязрядное значение, трактуемое как логическая величина. Ее ненулевое значение говорит о необходимости включить автозаполнение, которое является активным по умолчанию уже при создании объекта. Поэтому вызывать Enable(), как правило, приходится для того, чтобы отключить автозаполнение, либо включить снова, после того как оно было предварительно выключено. В случае успешного выполнения функция возвращает S_OK, в случае ошибки - ее код.

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

Для тех, кто, увлекшись MFC и VCL, забыл, что такое дескриптор окна, остается напомнить, что это то, что в MFC возвращается методом GetSafeHWND(), а в VCL обозначено в подсказке словом Handle.

Текстовое поле может существовать не только само по себе, но и как часть другого органа управления, например, комбинированного списка. В этом случае также возможно включение для него автозаполнения, если тот управляющий элемент, в который оно внедрено, позволяет получить дескриптор поля ввода, как это делает, например, ComboBoxEx в ответ на сообщение CBEM_GETEDITCONTROL, иначе придется изворачиваться самому, придумывая обходные пути через использование EnumChildWindows() или что-нибудь в этом роде, это уже у кого на что фантазии хватит.

Второй параметр - это указатель на интерфейс IUnknown объекта, предоставляющего список строк для автозаполнения. Этот объект заслуживает особого внимания и будет рассмотрен чуть позже более подробно.

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

Эта строка не представляет собой ничего таинственного для тех, кто хотя бы раз в своей практике обращался к функциям sprintf() или wsprintf(). Она должна содержать спецификатор %s, заменяемый при нажатии пользователем комбинации Ctrl+Enter текстом, содержащимся в текстовом поле. К примеру, если введен текст "Вася Пупкин" (не правда ли, этот герой стал уже хрестоматийным), а строка формата определена как "Привет, %s!", то после того, как пользователь вдавит в клавиатуру указанные кнопки, текст в поле ввода будет преобразован в "Привет, Вася Пупкин!". Чтобы исключить в своем приложении возможность использования такого форматированного дополнения, достаточно передать в функцию нулевые указатели.

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

Остается заметить, что OLESTR служит псевдонимом для обозначения строк в формате Unicode, принятом в качестве стандарта для COM, что позволило избежать необходимости поддержки двух версий библиотеки API, как это имеет место быть в случае с системными функциями Windows. Те программисты, которым хватает для выражения своих мыслей восьми двоичных разрядов на каждый символ, должны знать, что они не обязаны изменять своим привычкам, так как в Win32 API предусмотрены функции MultiByteToWideChar(), преобразующей строку из 8-битных символов в Unicode, и WideCharToMultiByte(), делающей все в точности до наоборот. Все, что нужно сделать, используя эти функции для корректной обработки национальных символов, это правильно указать кодовую страницу.

Наследуемый от IAutoComplete интерфейс IAutoComplete2 предлагает два метода, при помощи которых можно управлять поведением объекта автозаполнения. Это методы GetOptions() и SetOptions():

HRESULT GetOptions(DWORD *pdwFlags);
HRESULT SetOptions(DWORD dwFlags);

Параметр dwFlags метода SetOptions() представляет собой набор битовых флагов, среди которых можно отметить два значения, управляющих установкой режимов автозаполнения:

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

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

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

Вызов GetOptions(), в свою очередь, позволяет получить сведения о включенных для объекта опциях. Соответствующая битовая комбинация помещается в переменную, адрес которой передается в качестве единственного параметра метода.

Обе функции строго следуют принятым в COM соглашениям, возвращая в качестве свидетельства успешного завершения S_OK или OLE-код ошибки в случае неудачи.

Следующий пример кода показывает создание экземпляра объекта автозаполнения и запрос у него указателя на интерфейс IAutoComplete (не забудьте перед этим вызвать CoInitialize() или CoInitializeEx() где-нибудь в функции инициализации своего приложения):

IAutoComplete *pAutoComplete = NULL;
HRESULT hr = CoCreateInstance(CLSID_AutoComplete, NULL, CLSCTX_INPROC_SERVER, IID_IAutoComplete, (void **)&pAutoComplete);
if (SUCCEEDED(hr)) // если вызов завершился без ошибок,
{
// то здесь можно что-то сделать
// ...
}

Теперь необходимо создать объект источника строк для операции автозаполнения, который уже упоминался вскользь при описании аргументов метода Init() интерфейса IAutoComplete. Этот объект, ответственный за предоставление объекту автозаполнения списка доступных пользователю строк, должен экспортировать интерфейс IEnumString. Его описание включает стандартный для всех IEnum-интерфейсов набор методов: Next(), Skip(), Reset() и Clone().

Программист может использовать для своих нужд экземпляры классов, предоставляемых в его распоряжение оболочкой. В их число входит класс с идентификатором CLSID_ACLHistory, предоставляющий доступ к списку адресов в Интернете, посещенных пользователем, CLSID_ACLMRU со своим списком недавно открывавшихся файлов, и CLSID_ACLListISF, дающий возможность выбирать из имен объектов в так называемом пространстве имен оболочки, включая названия файлов и каталогов, в том числе виртуальных.

Следующий фрагмент демонстрирует создание для использования в качестве источника экземпляра класса CLSID_ACLListISF:

IUnknown *pStringSource = NULL;
hr = CoCreateInstance(CLSID_ACLListISF, NULL, CLSCTX_INPROC_SERVER, IID_IACList, (void **)&pStringSource);

В качестве идентификатора запрашиваемого у объекта интерфейса в вызове CoCreateInstance() указан IID_IACList, который служит для повышения скорости автозаполнения в тех случаях, когда списки строк слишком велики, что ставит вопрос об организации своего рода хэш-таблицы, позволяющей не просматривать весь список целиком, а отбирать для просмотра только его часть.

Программист, реализующий свой собственный объект источника строк, может не включать в число экспортируемых им интерфейсов IACList, равно как и IACList2, методы которого предназначены для управления поведением списка. Для таких объектов обязательным является только определение методов IEnumString.

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

После того как были созданы объект автозаполнения и объект-источник строк, необходимо вызвать метод Init() первого из них, после чего для каждого может быть вызван метод Release(). Объект автозаполнения останется связан с текстовым полем даже после освобождения.

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

pAutoComplete->Init(hEdit, pStringSource, NULL, NULL);
pAutoComplete-Release();
pStringSource->Release();

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

К примеру, так можно установить режим автозаполнения:

IAutoComplete2 *pAC = NULL;
hr = pAutoComplete->QueryInterface (IID_IAutoComplete2, (void **)&pAC);
if (SUCCEEDED(hr))
{
/* установить режим, в котором пользователю будет предоставлена возможность выбирать строки из выпадающего списка */
pAC->SetOptions (ACO_AUTOSUGGEST);
pAC-Release();
}

А вот так можно повлиять на избирательность списка строк, предоставляемых пользователю:

IACList2 *pACList = NULL;
if (SUCCEEDED(pStringSource->QueryInterface(IID_ACList2, (void **)&pACList))
{
/* установить флаг, позволяющий перечислять только те объекты, которые являются частью физической файловой системы, а не расположены в виртуальных каталогах */
pACList->SetOptions(ACLO_FILESYSONLY);
pACList->Release();
}

Так можно организовать работу объекта автозаполнения, использующего для обращения к списку возможных строк один источник. А что делать, если программист хочет позволить пользователю выбирать из нескольких списков одновременно?

Итак, повторимся. Сначала мы создаем экземпляр класса CLSID_AutoComplete, как это было показано в самом первом примере кода. Затем...

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

IObjMgr *pObjectManager = NULL;
if (SUCCEEDED(CoCreateInstance(CLSID_ACLMulti, NULL, CLSCTX_INPROC_SERVER, IID_IObjMgr, (void **)&pObjectManager))
{
/* менеджер создан, теперь очередь за созданием списков */
IUnknown *pSrc1 = NULL;
if (SUCCEEDED(CoCreateInstance(CLSID_xxx, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pSrc1))
{
/* добавить объект источника в общую "кучу" */
pObjectManager->Append(pSrc1);
}
}

Очевидно, что забавная константа CLSID_xxx во втором вызове CoCreateInstance() лишь обозначает идентификатор класса, экземпляр которого планирует создать программист. Этот вызов в паре с вызовом метода Append() менеджера объектов должен быть выполнен для создания и подключения каждого из списков.

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

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

Целью предложенного вниманию читателей изложения виделось посвящение программистов в одну маленькую тайну оболочки, рассказ о том, что есть в системе кое-что, что может облегчить им жизнь, и прозрачный намек на то, что есть и другие маленькие секреты, которые, на самом деле, не скрыты за семью печатями, а просто недостаточно широко анонсированы. И естественно, что в рамки статьи не могло, да и не должно было уместиться объяснение всех моментов работы с объектами COM, ориентированное на тех из читателей, кто пока еще не знаком с этой технологией, о чьих достоинствах и недостатках уже поутихли жаркие споры. И кто ж тому виной, что оболочка не предоставляет более простого способа реализовать в своем приложении функцию автозаполнения? Однако, такой способ все-таки есть!

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

Речь идет об использовании функции SHAutoComplete(), вызов которой позволяет включить автозаполнение для поля ввода, дескриптор которого указывается в качестве первого параметра вызова. При этом пользователь сможет выбирать только из URL'ов и путей в файловой системе:

HRESULT SHAutoComplete(HWND hwndEdit, DWORD dwFlags);

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

Стоит заметить, что ряд опций, определяемых при вызове SHAutoComplete(), узнать о которых более подробно можно из документации, тесно связан со свойствами обозревателя, и их определение программистом способно изменить некоторые настройки Internet Explorer'а, сохраняемые в реестре.

Как и другие COM-функции, SHAutoComplete() возвращает в случае ошибки соответствующий OLE-код и S_OK при успешном завершении своей работы.

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



статьи
статьи
 / 
новости
новости
 / 
контакты
контакты