Книги

4. Стандартные элементы управления (вторая часть)


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

Примечание: В данной главе слово "элемент" используется в двух различных смыслах: как элемент списка (item), и как элемент управления Windows (control), в том числе и сам список. Надеюсь, по контексту прозрачно, какой элемент имеется в виду.


4.1 Элемент управления "список" (класс "LISTBOX")

Начнем с того, что создадим самый простой список и заполним его строками.

. . . . .
const
  ID_LISTBOX_SINGLE = $101;
. . . . .
 ctl := CreateWindowEx (WS_EX_CLIENTEDGE,
                        'LISTBOX',
                        '',
                        WS_CHILD or WS_VISIBLE or WS_VSCROLL or
                        LBS_HASSTRINGS or LBS_NOTIFY,
                        10,10,
                        135,100,
                        Wnd,
                        ID_LISTBOX_SINGLE,
                        HInstance,
                        nil
                        );
. . . . .
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Первая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Вторая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Третья строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Четвертая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Пятая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Шестая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Седьмая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Восьмая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Девятая строка')));
 SendMessage (ctl, LB_ADDSTRING, 0, longint(pchar('Десятая строка')));
. . . . .

Вообще-то, стиль LBS_HASSTRINGS можно было и не указывать - он подразумевается по умолчанию, если не указаны стили LBS_OWNERDRAWxxx. Следует обратить внимание на стиль WS_VSCROLL, который создает вертикальную полосу прокрутки. У полос прокрутки в списках есть две особенности:

  • Горизонтальные полосы прокрутки не создаются (очень интересно, почему?);
  • Вертикальная полоса прокрутки видна только тогда, когда в ней есть необходимость (т.е. не все строки помещаются по высоте элемента), если не указан стиль LBS_DISABLENOSCROLL - тогда она показывается всегда, а когда не нужна - серой - недоступной.

Стиль LBS_NOTIFY требуется для того, чтобы мы могли получать сообщения когда пользователь будет взаимодействовать с нашими списками.

Добавление строки производится при помощи специального сообщения LB_ADDSTRING. При этом в параметре wParam передается 0, а в lParam - указатель на добавляемую строку, которая должна оканчиваться нулевым символом (ASCIIZ, в Паскале - тип PChar).

Если не брать во внимание варианты списка с ручной отрисовкой, то остается еще один - список со множественным выбором. Отличается стилем LBS_EXTENDEDSEL или LBS_MULTIPLESEL. В первом случае (второй список в прилагаемом примере) возможен множественный выбор при помощи клавиш Shift и Ctrl; а во втором - клик мышью устанавливает выделение на текущий элемент, если он не выделен, и снимает с уже выделенного, не меняя при этом состояния других элементов списка.


4.2 Выпадающие (комбинированные) списки (класс "COMBOBOX")

Комбинированные списки предствляют собой сочетание списка для выбора и поля редактирования.

Создание комбинированных списков практически аналогично созданию списков обычных за исключением того, что:

  1. Используется класс "COMBOBOX" (как можно было догадаться);
  2. Используются соответствующие стили окна;
  3. Указываемая высота относится к суммарной высоте поля редактирования и выпадающего списка, высота поля формируется системой автоматически;
  4. Добавление строк производится не сообщением LB_ADDSTRING, а CB_ADDSTRING.

Существует три основных разновидности комбинированных списков:

  • Простой комбинированный список - стиль CBS_SIMPLE - список элементов виден всегда. В нашем примере такого нет.
  • Выпадающий список - стиль CBS_DROPDOWN - первый пример.
  • Выпадающий список без возможности редактирования - только выбор - стиль CBS_DROPDOWNLIST - второй пример.

4.3 Работа со списками

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

Параметры сообщения WM_COMMAND:

  • wNotifyCode - HiWord (wParam) - код события;
  • wID - LoWord (wParam) - идентификатор элемента;
  • hwndCtl - lParam - дескриптор окна элемента.

Для примера напишем обработку, которая при изменении выделения в первом списке загонит соответствующую строку в заголовок главного окна.

. . . . .
procedure ProcessCommand(Wnd : HWnd; Code : word; ID : word; Ctl : HWnd);
 var
   idx : dword;
   len : dword;
   str : pchar;
 begin
 case ID of
      ID_LISTBOX_SINGLE : case Code of
                               LBN_SELCHANGE : begin
                                               idx := SendMessage (Ctl, LB_GETCURSEL, 0, 0);
                                               len := SendMessage (Ctl, LB_GETTEXTLEN, idx, 0);
                                               getMem (str, len + 1);
                                               SendMessage (Ctl, LB_GETTEXT, idx, longint(str));
                                               SendMessage (Wnd, WM_SETTEXT, 0, longint(str));
                                               freeMem (str)
                                               end
                               end
      end
 end;
. . . . .
// внутри оконной функции главного окна
      WM_COMMAND : ProcessCommand (Wnd, HiWord (wParam), LoWord(wParam), HWnd(lParam));
. . . . .

Итак, по порядку. Сначала мы определяем, что сообщение пришло именно от нужного нам списка, то есть - ID_LISTBOX_SINGLE. Затем анализируем код события. Кроме LBN_SELCHANGE здесь могут быть следующие значения:

  • LBN_DBLCLK - двойной щелчок по списку;
  • LBN_KILLFOCUS - потеря списком фокуса ввода;
  • LBN_SELCANCEL - снято выделение с одного из элементов списка;
  • LBN_SETFOCUS - список получил фокус ввода.

Обратим внимание на отсутствие события на простой клик. Дело в том, что простой клик обязательно должен привести к одному из двух событий - SELCANCEL или SELCHANGE.

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

  1. Сообщение LB_GETCURSEL параметров не имеет, возвращаемое значение - индекс текущего выделения. Заметим, что правильно работает это сообщение только для списков с одиночным выбором, для списка с множественным выбором будет возвращен индекс элемента на котором находится курсор (обычно это такая пунктирная рамка), но для этой цели лучше использовать сообщение LB_GETCARETINDEX. Для списков множественного выбора можно проверить состояние каждого элемента сообщением LB_GETSEL.
  2. Сообщение LB_GETTEXTSEL использует один параметр - индекс элемента, передаваемый в wParam. Возвращает длину строки соответствующего элемента.
  3. Для того, чтобы получить сам текст элемента, используется сообщение LB_GETTEXT. wParam - индекс элемента, lParam - адрес буфера, куда строка будет помещена.
  4. Общим для всех окон сообщением WM_SETTEXT устанавливаем новый заголовок.

Конечно действия, которые можно совершать со списками, не ограничиваются получением названия текущей строки. Однако, их количество достаточно велико и у меня нет никакого желания здесь их разбирать. Рекомендую раздел SDK, который называется "List Box Messages".

4.4 Работа с выпадающим списком

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

. . . . .
ID_COMBO_DROPDOWN : case Code of
                         CBN_EDITCHANGE : begin
                                          len := SendMessage (Ctl, WM_GETTEXTLENGTH, 0, 0);
                                          getMem (str, len + 1);
                                          SendMessage (Ctl, WM_GETTEXT, len + 1, longint(str));
                                          if not (SendMessage (Ctl, CB_FINDSTRING, 0, longint(str)) = CB_ERR)
                                             then begin
                                                  SendMessage (Ctl, CB_SELECTSTRING, 0, longint(str));
                                                  SendMessage (Ctl, CB_SETEDITSEL, 0, MAKELPARAM (len, -1))
                                                  end;
                                          freeMem (str)
                                          end
                         end
. . . . .

Итак, во-первых - мы обрабатываем событие CBN_EDITCHANGE, которое возникает при изменении текста поля ввода пользователем.

Во-вторых, для работы с текстом в поле ввода мы используем стандартные сообщения WM_GETTEXT и WM_GETTEXTLENGTH. А вот дальше - пошла специфика:

  • Сообщение CB_FINDSTRING позволяет нам определить, существует ли в списке строка, начинающаяся с введенных нами символов. Если не существует, то подставлять нечего.
  • Сообщение CB_SELECTSTRING работает аналогично CB_FINDSTRING, но при этом еще и выбирает соответствующую строку.
  • Сообщение CB_SETEDITSEL позволяет выделить в поле ввода произвольную подстроку. В этом сообщении HiWord (lParam) должно быть равно конечной позиции выделения (-1 для того, чтобы выделить все до конца строки, как в нашем случае), а LoWord (lParam) - начальной позиции. Функция MAKEPARAM() соответствует сишному макросу и попросту делает из двух значений типа Word одно типа LongInt.

В нашем примере есть одна существенная проблема - не обрабатываются нажатия Delete и BackSpace. Это происходит потому, что они удаляют в первую очередь выделенную часть, после чего строка заново находится, подставляется и выделяется. Для того, чтобы их корректно обрабатывать требуется значительное усложнение примера - например, хранение промежуточной строки и снятие выделения вместе с хвостом в случае, если начало строки не изменилось... Хотя наиболее корректный способ - написать класс-наследник от "COMBOBOX", или реализовать соответствующую функциональность с нуля - чтобы можно было адекватно реагировать на любые клавиши.

Аналогично предыдущему, для изучения всех возможностей комбинированных списков рекомендую раздел SDK "Combo Box Messages" и смежные.


4.5 Резюме

Мы вкратце рассмотрели элементы управления, которые позволяют реализовать выбор одной или нескольких альтернатив - списки и комбинированные списки. Пример, описанный в настоящей главе можно найти в архиве. А выглядит он так:



Рис. 4-1. Списки

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links