Красивые списки...

Достаточно часто при написании программы требуется реализовать какой-либо список. В Delphi для этого, как правило, используется стандартный компонент TListBox. Однако заказчик (часто – в лице собственного самолюбия программиста) может потребовать сделать этот список цветным. Таким, например, как в небезызвестном проигрывателе Winamp. Вот тут-то многие начинающие программисты и заходят в тупик. Многие – но не мы с вами!

Задача
Итак, сегодня перед нами – следующая задачка: реализовать список, внешний видом не уступающий плейлистам известных проигрывателей, пользуясь только стандартными компонентами Delphi.

Решение
Для начала бросим на форму один TListBox (см. рисунок 1, слева). Для удобства назовем его PlayList (вы можете использовать любое другое название, но в таком случае придётся изменять мой код). Свойство Style измените на lbOwnerDrawVariable – это позволит отрисовывать внутри ListBox’а произвольную графику. Собственно отрисовка выполняется посредством метода PlayListDrawItem, представленного в листинге 1. Там же приведены список переменных и функция, их инициализирующая. Желающие сразу же приступить к делу могут скопировать этот код в проект Delphi и откомпилировать (работоспособность проверена в Delphi 7). Для тех же, кто что-то не понял, я дам необходимые пояснения.
Итак, в первую очередь мы объявляем переменные, отвечающие за внешний вид пунктов списка в различных ситуациях. Я использовал четыре основных состояния: активный невыделенный, активный выделенный, неактивный выделенный, неактивный невыделенный. По аналогии можно описать и другие состояния, но, как показывает практика, и этого более чем достаточно (в качестве примера я использовал упрощенный фрагмент кода из своего плагина AmpView1).

Следующий шаг – инициализация переменных. Для начала необходимо создать все объекты класса TFont, поскольку их автоматическая инициализация компилятором не производится. Затем задаём значения всех переменных, отвечающих за внешний вид. При компиляции этого примера будьте внимательны: я использую нестандартные функции преобразования типов. Поскольку у вас этих функций нет, а их рассмотрение не входит в рамки данной статьи, эту часть кода вам придётся изменять. Впрочем, ничего сложного здесь нет – используемые типы стандартны, и работа с ними не составит труда даже для начинающего программиста. Последняя строчка в данной процедуре – задание значения переменой ActiveItem, отвечающей за порядковый номер активного пункта списка. К примеру, в проигрывателе это будет номер трека, воспроизводимого в данный момент.

Наконец, мы добрались до основной процедуры. Здесь главное — понять суть, и именно в этом я постараюсь помочь.
Начнём с самого начала.
Наша процедура вызывается программой для прорисовки каждого (!) из пунктов списка (TListBox.Item). Это – очень важно; при прорисовке больших списков эта процедура может вызываться сотни и даже тысячи раз (в зависимости от конкретного числа строк в списке).
Строка «with Control as TListBox do» сообщает компилятору, что объект, над которым выполняются нижеследующие операции, принадлежит классу TListBox. Этим мы избавляем себя от необходимости явного обращения к компоненту PlayList, как представителю данного класса.
Далее мы инициализируем цвет основной кисти канвы. Именно этим цветом будет отрисовываться основной фон нашего списка.
И вот теперь – первое ветвление. Его смысл очень прост: если текущий пункт – это пункт активный, то выполняем первый блок кода, иначе – второй блок. Поскольку оба эти блока идентичны (за исключением подставляемый переменных, отвечающих за внешний вид пункта), я поясню суть на примере первого из них.
Итак, сперва мы проверяем состояние (State) пункта. Если выясняется, что пункт выделен (odSelected), то берём шрифты/цвета из набора Sel..., иначе – из обычного набора.
На этом ветвления заканчиваются (разумеется, при необходимости вы можете увеличить их число; главное – принцип).
Далее необходимо выделить на канве область, в которой будет отрисовываться необходимый нам текст (делается это при помощи метода канвы FillRect).
Ну вот, подготовка завершена. Теперь можно присвоить строковой переменной text необходимое значение и отрисовать прямоугольник с заданными нами параметрами (обращаемся к ещё одному методу канвы – TextOut). Обратите внимание, что текст, выводимый здесь, может отличаться от изначально определённого в соответствующем значении Items[]. Этим можно пользоваться, например, для включения/выключения нумерации строк (достаточно добавить в код, отвечающий за формирование строки, подходящее условие). Впрочем, это уже дело вашей фантазии.
Последнее, что мы сделаем в этой процедуре, – избавимся от рамки фокуса. Эта рамка стандартна для всех оконных элементов Windows, однако в полностью графическом объекте, подобном нашему, она, мягко говоря, неуместна. Для избавления от неё можно использовать, как минимум, два способа. Первый – модификация исходников TListBox (читай – изменение VCL). Сделать это несложно, но у такого способа есть один недостаток: рамка исчезнет во всех ваших проектах. Можно конечно, скопировать часть исходников VCL, но это – тоже не лучший выход. Гораздо проще воспользоваться одной особенностью WinAPI (а фокусный прямоугольник рисуется именно его средствами): если нарисовать рамку поверх уже существующей, то две рамки взаимно уничтожат друг друга. Зная, что рамка рисуется точно по границам определенного ранее прямоугольника (Rect), мы нарисуем ещё одну, воспользовавшись функцией DrawFocusRect.
Ну вот и всё, наш список готов. Если всё сделано правильно, то должно получиться что-то подобное рисунку 1 (справа). Я использовал цвета, характерные для небезызвестного проигрывателя – iTunes, у вас они могут быть другими.

Заключение
Как видите, используя только стандартные средства Delphi, можно сделать список, практически не уступающий по своим графическим возможностям таким зубрам, как Winamp и iTunes. Разумеется, это – только интерфейсная часть списка. Впрочем, проявив немного фантазии, можно добиться и функционального сходства (см. рисунок 1).

// Листниг 1
var
  BackColor: integer;
  BackColor2: integer;
  PlayFont: TFont;
  MainFont: TFont;
  SelFont: TFont;
  SelPlayFont: TFont;
  SelBackColor: integer;
  SelPlayBackColor: integer;
  ActiveItem: integer;

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Создание объектов
  MainFont:=TFont.Create;
  SelFont:=TFont.Create;
  SelPlayFont:=TFont.Create;
  PlayFont:=TFont.Create;
  // Инициализация переменных
  StrToFont(‘#000000|Arial|8|nnnn’, MainFont);
  BackColor:=HexToInt(‘#FFFFFF’);
  BackColor2:=HexToInt(‘#EDF3FE’);
  StrToFont(‘#FFFFFF|Arial|8|nnnn’, SelFont);
  StrToFont(‘#000000|Arial|8|ynnn’, PlayFont);
  StrToFont(‘#FFFFFF|Arial|8|ynnn’, SelPlayFont);
  SelBackColor:=HexToInt(‘#3D80DF’);
  SelPlayBackColor:=HexToInt(‘#3D80DF’);
  // Выбор активного пункта
  ActiveItem:=2;
end;

procedure TForm1.PlayListDrawItem(Control: TWinControl; Index: Integer; Rect: TRect; State: TOwnerDrawState);
var
 text: string;
begin
  // Начало отрисовки
  with Control as TListBox do
  begin
   Canvas.Brush.Color:=BackColor;
   // Отрисовка активного пункта
    if index=ActiveItem
     then
      begin
       // Если пункт выделен
       if (odSelected in State) and Focused
        then
         begin
          Canvas.Font:=SelPlayFont;
          Canvas.Brush.Color:=SelPlayBackColor;
         end
        else
         begin
          Canvas.Font:=PlayFont;
          if (Index mod 2)=0
           then Canvas.Brush.Color:=BackColor
           else Canvas.Brush.Color:=BackColor2;
         end;
      end
    // Для всех остальных пунктов
     else
      begin
       // Если пункт выделен
       if (odSelected in State) and Focused
        then
         begin
          Canvas.Font:=SelFont;
          Canvas.Brush.Color:=SelBackColor;
         end
        else
         begin
          Canvas.Font:=MainFont;
          if (Index mod 2)=0
           then Canvas.Brush.Color:=BackColor
           else Canvas.Brush.Color:=BackColor2;
         end;
      end;

    // Подготовка области вывода
    Canvas.FillRect(Rect);

    // Формирование текста
    text:=PlayList.Items[index];

    // Вывод текста на канву
    Canvas.TextOut(Rect.Left + 2, Rect.Top, text);
    // Перекрытие рамки фокуса
    if odFocused in State then DrawFocusRect(Canvas.Handle, Rect);
  end;
end;

Рекомендуем почитать: