Digs - Персональная территория

Авторский проект Артема Глазкова
? 
        Версия для печати (цвет)  

Использование материалов
Заметка #20
11 сентября 2006

Создаем ComboBox


    Что и зачем
     На работе постоянно приходится заниматься графикой: схемы, графики. Соответственно, как и во всех нормальных продуктах должны быть настройки «всего на свете». Начиная с некоторого момента начало напрягать написание кучи окон для настройки пера, заливки и шрифта. В итоге придумал написать гибридный комбобокс. Он отличается от обычного бОльшим числом кнопок. Это позволяет быстро настроить соответствующее нечто, не открывая дополнительных окон.
    Вот, к примеру, на рисунке изображен компонент настройки TPen. В основной части компонента отображается линия, далее идет кнопка, при нажатии которой выпадает список цветов, затем кнопка настройки типа линии. Завершает все это SpinButton, который настраивает толщину линии.
    Итак, как написать такой combobox и как вообще пишутся любые комбобоксы?

    Задача и взаимодействие
    Требуется написать два универсальных класса, на основе которых в дальнейшем можно будет создать нужные нам компоненты. Первый класс (TMyCustomComboBox) это предок компонента, второй (TMyCustomComboList) – предок выпадающего списка.
    Наш компонент будет взаимодействовать со списком через ссылку (FDropList). Так как списков может быть несколько (хотя текущий только один), то список будет создаваться в момент появления на экран и иметь возможность разрушаться при его сворачивании. Почему возможность? Потому что после сворачивания могут потребоваться некоторые действия со списком. Например, проверка некоторых свойств. В этом случае его ненужно разрушать. Позже, при открытии следующего списка, старый будет автоматически удален. И так как в этом случае по не пустой ссылке нельзя будет определить видимость списка, то будет заведена определенная переменная (FListVisible).
    Процедура создания списка будет не виртуальной (DropDown), но в нее будет передаваться ссылка на класс создаваемого списка, а также будет определена виртуальная процедура (PrepareList), которая этот список настроит перед показом на экран.
    После всего выше сказанного, получаем такое начальное описание классов:

TMyCustomComboListClass = class of TMyCustomComboList;

TMyCustomComboList = class(TCustomControl)
end;

TMyCustomComboBox = class(TCustomControl)
private
  FDropList: TMyCustomComboList;
  FListVisible : boolean;
protected
  procedure   DropDown(AClass : TMyCustomComboListClass);
  procedure   PrepareList; virtual;
  procedure   CloseUp(AFreeItem : Boolean = true); virtual;
  procedure   FreeDropList;
public
  property    ListVisible : boolean read FListVisible;
end;

    Здесь функция CloseUp сворачивает список и удаляет его как объект, если параметр AFreeItem равен true. Функция FreeDropList удаляет список, если он еще не удален и присваивает переменной FDropList значение nil.
    Под подготовкой списка я понимаю заполнение параметров, а также настройку размеров. Как правило, у списка ширина такая же, как и у компонента, поэтому метод PrepareList:

procedure TMyCustomComboBox.PrepareList;
begin
  FDropList.Width := Width;
end;

    Стиль для списка и разворачивание
    Со стилем для списка особых проблем нет. Список должен быть всплывающим окном. Процедура параметров такая:

procedure TMyCustomComboList.CreateParams(var Params: TCreateParams);
begin
  inherited;
  with Params do begin
    Style := WS_POPUP; //окно всплывающее
    ExStyle := WS_EX_TOOLWINDOW; //инструментальное окно
    WindowClass.Style := CS_SAVEBITS; //под окном сохраняется изображение
  end;
end;

    А вот для показа надо знать один нюанс: всплывшее окно (список) не должно получить фокус ввода ни при каких обстоятельствах. Оно не принадлежит тому окну, в котором лежит combobox. Если допустить получение фокуса, то окно с combobox станет не активным. А это как минимум отображение неактивного заголовка.
    Для того чтобы список не получил фокус ввода, достаточно запретить его активацию с помощью мыши (хотя сообщения от нее и будут приходить списку). Для этого добавим такую процедуру:

procedure TMyCustomComboList.WMMouseActivate(var Message: TMessage);// message WM_MOUSEACTIVATE
begin
  Message.Result := MA_NOACTIVATE;
end;
    
    Теперь отображение списка:

procedure TMyCustomComboBox.DropDown(AClass : TMyCustomComboListClass);
var P : TPoint;
    x,y  :Integer;
begin
  FreeDropList;
  FDropList := AClass.Create(Self);
  FDropList.Parent := Self;
  PrepareList;
  //переводим координаты для списка из оконных в экранные
  P := Parent.ClientToScreen(Point(Left, Top));
  x := Min(Max(P.X, 0),Screen.Width-FDropList.Width);
  y := P.Y + Height;
  //если combobox слишком низко, то список отображаем выше его
  if y+FDropList.Height>Screen.Height then y := P.Y - FDropList.Height;
  //отображаем окно без активации
  SetWindowPos(FDropList.Handle, HWND_TOP,x,y , 0, 0,
    SWP_NOSIZE or SWP_NOACTIVATE or SWP_SHOWWINDOW);
  FListVisible := true;
  Invalidate;
end;
    
    Так как события от клавиатуры все же должны поступать списку, будем их анализировать в компоненте и оправлять списку сами:

procedure TMyCustomComboBox.KeyDown(var Key: Word; Shift: TShiftState);
begin
  if (Key <> 0) and ListVisible then FDropList.KeyDown(Key, Shift)
  else inherited;
end;
    
    Если предполагается, что список будет так же обрабатывать KeyUp и KeyPress, то нужно дописать соответствующие процедуры.
    Плохим моментом того, что список не получает фокус является то, что в него нет смысла помещать какие либо элементы управления. Они все равно не будут работать. Таким образом, если нам в выпадающем списке нужно сделать, к примеру, checkbox, то придется самим его нарисовать и обработать события.

    Фокус
    Пара событий, которые нужно обработать: WMKillFocus и CMExit. Ничего сложного: нужно просто закрыть список. Например:

procedure TMyCustomComboBox.WMKillFocus(var Message: TWMKillFocus);// message WM_KILLFOCUS;
begin
  CloseUp(false);
  Inherited;
end;
    
    Обратите внимание, что здесь используется «щадящий» режим закрытия списка. Т.е. сам список как объект не разрушается. Это используется, например, в случаях, когда из списка было вызвано диалоговое окно. После выхода из процедуры диалогового окна, будет возврат в процедуру принадлежащую списку. А это значит что список должен оставаться «живым».
    Еще одно событие: CMCancelMode. Данное событие вызывается, когда всплывает диалоговое окно, либо какое-то окно или что-то еще производит захват мыши. При этом необязательно оно заберет фокус. Такая ситуация возникает, например, когда пользователь перетаскивает окно или изменяет его размеры. Разумеется, в этом случае список также нужно свернуть. Но здесь нужно еще проверить, не является ли сам компонент или его список инициатором данного события:

procedure TMyCustomComboBox.CMCancelMode(var Message: TCMCancelMode);// message CM_CANCELMODE;
begin
  if (Message.Sender <> Self) and (Message.Sender <> FDropList) then CloseUp;
end;

     Обработка Escape
    Довольно часто встречается ситуация, когда форма программируется таким образом, чтобы при нажатии Escape она закрывалась. Ну, вы помните, устанавливаем свойство KeyPreview и пишем обработку. Наша задача, в случае, когда список виден, закрыть его и не допустить чтобы событие ушло форме (чтобы она не закрылась).
    Начнем с того, что заглянем в исходный текст TWinControl. Не долго поразбиравшись выясняется, что для каждого типа сообщения от клавиатуры существует три процедуры. На примере KeyDown: сначала событие Windows приводит к вызову процедуры WMKeyDown. Далее эта процедура вызывает функцию DoKeyDown, а та в свою очередь вызывает виртуальную KeyDown. Проверка флага KeyPreview происходит в функции DoKeyDown, которая не является виртуальной. Следовательно, нужно писать обработку WMKeyDown. Вот здесь нужно помнить про одно обстоятельство: при нажатии клавиши Escape вырабатывается два события. Сначала WMKeyDown, а затем WMChar. Чтобы избежать обработки клавиши формой придется обе их переписать.

procedure TMyCustomComboBox.WMKeyDown(var Message: TWMKeyDown);// message WM_KEYDOWN;
begin
  if (Message.CharCode<>VK_ESCAPE) or not ListVisible
    then inherited;
end;

procedure TMyCustomComboBox.WMChar(var Message: TWMChar);// message WM_CHAR;
begin
  if (Message.CharCode=VK_ESCAPE) and ListVisible then CloseUp
  else inherited;
end;
    
    Чтобы не заводить дополнительных переменных, я не допускаю дальнейшую обработку клавиши в первой процедуре и сворачиваю список во второй.

    Рисуем
    Собственно, рисовать особо нечего. Единственное что нужно, это окантовку (свойство Bordered будет отвечать за ее наличие), а также завести свойство, которое будет содержать положение и размеры клиентской области (ClientRect). В процедуре Paint клиентскую область не рисуем, так как наследники в любом случае будут ее заполнять.

procedure TMyCustomComboBox.SetBordered(const Value: boolean);
begin
  FBordered := Value;
  Resize;
  Invalidate;
end;

procedure TMyCustomComboBox.Paint;
begin
  if FBordered then DrawFrameControl(Canvas.Handle, Rect(0,0,Width,Height), 0, DFCS_PUSHED);
end;

procedure TMyCustomComboBox.Resize;
begin
  inherited;
  if FBordered
    then FClientRect := Rect(2,2,Width-2,Height-2)
    else FClientRect := Rect(0,0,Width,Height);
end;

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



© 2005-16, Powered By Digs (Написать письмо, vk)