Андрей Чистяков дата публикации 19-11-2008 10:59 ComboBox с автозавершением (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.
Также, в соответствии с правилами Королевства, следует упомянуть и поблагодарить Владимира Папаева — для примера мне нужна была функция для представления числа прописью, я воспользовался его разработкой.
К статье прилагаются файлы:
[TComboBox]
Обсуждение материала [ 10-08-2016 13:50 ] 11 сообщений |