Версия для печати
ComboBox с автозавершением (AutoComplete) по подстроке
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1387Андрей Чистяков
дата публикации 19-11-2008 10:59ComboBox с автозавершением (AutoComplete) по подстроке Постановка задачи
В интернете достаточно много информации по реализации функции автозавершения вводимого в TextBox или ComboBox текста — так, как это сделано в диалоговом окне "Пуск / Выполнить". Данную функцию можно реализовать с помощью интерфейсов IAutoComplete/IAutoComplete2. Эти интерфейсы позволяют формировать список вариантов автозавершения, подбирая те строки, которые начинаются с введенного в поле редактирования текста, но не позволяют выбирать те строки, которые содержат введенный текст в любом месте строки (см. рис. 1) — во всяком случае в интернете я не нашел информации по этому вопросу. Поэтому пришлось делать свое выпадающее окно автозавершения.
Рисунок1В целом выпадающее окно должно вести себя точно так же, как и стандартное выпадающее окно ComboBox'а (это окно создается системой, его класс называется "ComboLBox"):
- ComboBox не должен терять фокус при выпадении окна.
- Окно должно пропадать, когда фокус уходит с ComboBox'а; когда происходит переключение на другое окно; когда пользователь двигает форму, содержащую ComboBox.
- Выпадающее окно не должно получать фокус, т.е. когда пользователь выбирает какой-либо пункт списка автозавершения, либо просто прокручивает этот список мышью, заголовок формы, содержащей ComboBox, должен оставаться активным.
Второй пункт реализуется достаточно просто. Пункты первый и третий взаимосвязаны, и их я реализовать долгое время не мог. Родное выпадающее окно комбобокса (ComboLBox) судя по всему работает с применением захвата ввода от мыши (SetCapture/ReleaseCapture). Когда я попытался сделать свое выпадающее окно аналогичным образом, практически все получилось как задумано (комбобокс не терял фокус, форма не становилась неактивной), за единственным исключением — не работала полоса прокрутки в списке (в вопросе ¹ 57188 на Круглом столе тоже обсуждается эта проблема). Наконец, решение было найдено здесь. На основе этой статьи был сделан компонент TACComboBox. Тема показалась мне актуальной, поэтому я решил опубликовать результаты своих изысканий.
Описание компонента
Компонент имеет следующие published-свойства:
property ACItems: TStrings; — список вариантов автозавершения; из строк, содержащихся в этом списке формируется список автозавершения, в зависимости от значения свойства ACType.
type TAutoCompleteType = (actSimple, actSubString, actCustom);
property ACType: TAutoCompleteType; — определяет алгоритм, по которому заполняется список автозавершения.
- при ACType=actSimple в список автозавершения помещаются строки из ACItems, которые начинаются с введенного в поле комбобокса текста;
- при ACType=actSubString в список автозавершения помещаются строки из ACItems, которые содержат введенный в поле комбобокса текст;
- при ACType=actCustom для каждого элемента списка ACItems вызывается обработчик события OnCheckString, в котором определяется, следует ли добавлять очередной элемент в список автозавершения. Если обработчик устанавливает параметр AddString в True, элемент добавляется, если в False — не добавляется.
В принципе, для формирования списка автозавершения можно не пользоваться списком, заданным в свойстве ACItems. Для этого нужно написать наследника класса TACComboBox, в котором переопределить protected-метод PrepareACStrings. В параметре AText этому методу передается введенный в поле комбобокса текст; метод должен заполнить список FDropDown.Items (подробнее см. пример, прилагаемый к статье).
Опишу некоторые детали реализации компонента; подробные комментарии есть в исходниках к статье.
- Итак, делаем выпадающее окно. Возможность изменения размеров окна автозавершения, как это сделано в IAutoComplete, мне не нужна, поэтому в качестве выпадающего окна вполне подойдет обычный ListBox.
type TDropDownListBox = class(TListBox) protected procedure CreateParams(var Params: TCreateParams); override; end; procedure TDropDownListBox.CreateParams(var Params: TCreateParams); begin inherited CreateParams(Params); Params.ExStyle:=WS_EX_TOOLWINDOW; Params.WndParent:=GetDesktopWindow; Params.Style:=WS_CHILD or WS_BORDER or WS_CLIPSIBLINGS or WS_OVERLAPPED or WS_VSCROLL; end;Такой ListBox будет работать именно так, как нужно: не ловит фокус, при этом полоса прокрутки работает.
- Объявляем наследника класса TComboBox:
type TACComboBox = class(TComboBox) private FDropDown: TDropDownListBox; public procedure ShowAC; procedure HideAC(ApplySelection: Boolean); end;FDropDown — ссылка на окно автозавершения, создается/разрушается в конструкторе/деструкторе TACComboBox, подробнее см. исходники, прилагаемые к статье.
Метод ShowAC показывает выпадающее окно:
procedure TACComboBox.ShowAC; var P: TPoint; Cnt: Integer; begin // если текст в комбобоксе ='', то прячем окно автозавершения (если оно было показано) if Text='' then begin HideAC(False); exit; end; // заполняем список автозавершения (подробнее см. исходники) PrepareACStrings(Text); Cnt:=FDropDown.Items.Count; // если подходящих вариантов автозавершения нет, прячем окно автозавершения if Cnt=0 then begin HideAC(False); exit; end; // будем показывать список автозавершения такого размера, чтобы в нем // помещалось не более пяти строк. Если вариантов автозавершения больше пяти, // будет показана вертикальная полоса прокрутки if Cnt>5 then Cnt:=5; FDropped:=True; // если было показано "родное" выпадающее окно комбобокса, прячем его SendMessage(Handle, CB_SHOWDROPDOWN, 0, 0); // показываем окно автозавершения под комбобоксом. Вообще говоря, // было бы правильным сделать, чтобы это окно показывалось над комбобоксом, // если комбобокс находится слишком близко к нижнему краю экрана P.X:=1; P.Y:=Height-1; P:=ClientToScreen(P); SetWindowPos(FDropDown.Handle, HWND_TOPMOST, P.X, P.Y, Width-GetSystemMetrics(SM_CXVSCROLL)-2, Cnt*FDropDown.ItemHeight+2, SWP_SHOWWINDOW); end;Метод HideAC прячет список автозавершения. Если ApplySelection=False, окно просто прячется, если True — прячется, а выбранная строка помещается в комбобокс:
procedure TACComboBox.HideAC(ApplySelection: Boolean); var I: Integer; begin ShowWindow(FDropDown.Handle, SW_HIDE); if ApplySelection then begin I:=FDropDown.ItemIndex; if I<>-1 then begin Text:=FDropDown.Items[I]; SelectAll; end; end; FDropped:=False; end;- Нужно, чтобы при перемещении формы, на которой лежит комбобокс, окно автозавершения пряталось. Это реализуется путем подмены оконной процедуры формы и перехватом сообщений WM_WINDOWPOSCHANGING/WM_WINDOWPOSCHANGED. Мне самому такое решение не очень нравится, возможно, форма рассылает какие-то нотификационные сообщения дочерним контролам, уведомляя их об изменении своего положения, но я таких сообщений не нашел.
Остальные методы классов на мой взгляд понятны, подробности см. в исходниках.
Отмазки
Отмазка №1. Все это затевалось в основном для того, чтобы сделать AutoComplete с возможностью подбора вариантов по подстроке. Я исходил из предположения, что интерфейсы IAutoComplete/IAutoComplete2 этого сделать не могут. Если окажется, что они таки могут, и я изобрел очередной велосипед в — простите великодушно, не кидайте в меня камень, лучше киньте ссылку, по которой есть такая информация. Впрочем, даже в этом случае, надеюсь, из моей реализации все равно можно извлечь какую-то пользу — например, можно в списке вариантов подсвечивать набранный текст, как это показано на рисунке в начале статьи.
Отмазка №2. В принципе, можно в качестве выпадающего окна использовать не только листбокс. На мой взгляд, тема компонентов типа "поле ввода + кнопка + выпадающее окно" достаточно актуальна. Я попробовал на скорую руку вместо листбокса использовать календарь, на первый взгляд все работало как задумано. Но, с другой стороны, возможны и проблемы, связанные с тем, что фокус остается в комбобоксе и не переходит к выпадающему окну, поэтому как будет себя вести, например, TStringGrid с активным InplaceEdit'ом в роли выпадающего окна, я предсказывать не берусь.
Отмазка №3. Данные исходики я рассматриваю скорее как первый набросок, чем как окончательный вариант компонента. Всесторонне обкатать компонент в боевом проекте я еще не успел, поэтому наверняка где-то что-то можно добавить/оптимизировать.
Исходники
К статье прилагаю архив. Компонент находится в файле _ACCombo.pas, устанавливается обычным образом (Component/Install component…). Демо-проект не требует установки компонента в IDE, там компонент создается в Run-Time, поэтому можно просто открыть .dpr и нажать F9, чтобы посмотреть, как оно работает. Исходники на Delphi 7, но, насколько мне известно, я не использовал никаких особенностей компилятора данной версии, которые помешали бы пользоваться компонентом в других версиях Delphi.
Также, в соответствии с правилами Королевства, следует упомянуть и поблагодарить Владимира Папаева — для примера мне нужна была функция для представления числа прописью, я воспользовался его разработкой.
К статье прилагаются файлы: