03 апреля 2011

Жизнь без скроллбаров

Начну с того, что меня всегда удивляет использование в демках и конкурсных работах стандартного vcl. Как же так? Мы рисуем сцену с большим количеством объектов, красивым фоном, эффектами, физически взаимодействующими телами, с помощью графических движков (вроде GlScene, HGE и других), а элементарные кнопки, чекбоксы оставляем на отрисовку устаревающему vcl? Думаю дело в том, что красивый, простой и работающий гуи под GlScene - большая редкость, и поэтому многие просто вставляют в игру стандартные кнопки - кинуть на форму TButton умеют все. С другой стороны, возможно, те, кто начинают делать свой графический гуи, хотят сразу построить навороченный интерфейс с панелями, формами, выводом тысяч символов текста и т.д. Надеюсь, это рабочий подход к программированию, но не мой. Мне всегда кажется, что нужно начинать с чего-то маленького, тогда сразу станут видимыми границы, которых хочется достичь. В общем, я буду создавать, описывать и выкладывать в своем журнале простые элементы гуи, чтобы ими можно было воспользоваться в любую минуту. Обычно одним из самых неприятных моментов в гуи являются скроллбары, поэтому поступим хитро и попробуем организовать прокрутку длинного списка без скроллбаров вообще. Собственно, отсюда и заголовок сообщения - "Жизнь без скроллбаров", это возможно, просто и приятно. Приступим!

Вдохновение черпал из этого урока, и наш с вами результат будет выглядеть вот так:


Видео динамики прокрутки можно посмотреть здесь, исходники + ехе качаем отсюда.

Конечно, прокручивающийся список мы вынесем в отдельный класс TSimpleListBox, его описание у меня раздулось до такого:

  TSimpleListBox = class (TGLBaseSceneObject)
  protected
    fMatLib: TGLMaterialLibrary;
    fFont: TGLCustomBitmapFont;
    fItems: TList;

    fYPosition: Single; // it's delta y for all list
    fHeight: Integer;
    fDeltaHeight: Integer; // for each Item
    fMaxVisibleCount: Integer;
  protected
    fMainDummy: TGlDummyCube;
    fItemHudText: TGlHudText;
    fPanelBack: TGLHudSprite;
    fPanelFront: TGLHudSprite;
    fPanelSelection: TGLHudSprite;

    fMousePosition: TVector;
    Procedure InitHuds;
    Procedure MainInit(aMatLib: TGLMaterialLibrary; aFont: TGLCustomBitmapFont);
    Function GetItemByMouse: Integer;
  public
    Function GetItemByIndex(aIndex: Integer): TSimpleListItem;
    Function AddItem(aItem: TSimpleListItem): Integer;
    Function AddItemText(const aText: WideString): TSimpleListItem;
    Procedure SetMousePosition(aX, aY: Integer);
    Procedure DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean); override;
    procedure DoProgress(const progressTime: TProgressTimes); override;
    Constructor Create(AOwner: TComponent; aMatLib: TGLMaterialLibrary; aFont: TGLCustomBitmapFont); reintroduce;
    Constructor CreateAsChild(aParentOwner: TGLBaseSceneObject; aMatLib: TGLMaterialLibrary; aFont: TGLCustomBitmapFont);
    Destructor Destroy; override;
  end;

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

fListBox := TSimpleListBox.CreateAsChild(fMainDummy, fMatLib, fWinFont);

Вот такое волшебство будет нам доступно!
Следующим шагом рисуем элементы нашего списка:


Стоит заметить, что никаких clip-механизмов (чтобы рисовать объекты только в заданной области, обрезая края) мы использовать не будем, а просто оставим небольшую полоску, "загораживающую" вылезающие элементы сверху и снизу. Если внешнюю панельку рисовать раньше элементов списка, то выглядеть все это дело будет вот так кошмарно:


Ну и немножко кода, чтобы не расслабляться:

Procedure TSimpleListBox.DoProgress(const progressTime: TProgressTimes);
var
  dy: Single;
  i, item: Integer;
begin
  dy := (fMousePosition[1] - (Position.Y - fHeight));
  if dY < 0 then
    dY := 0;
  if dY > fHeight * 2 then
    dY := fHeight * 2;

  if fItems.Count >= fMaxVisibleCount then
    fYPosition := fYPosition + progressTime.deltaTime * 3 * (-dY * (fItems.Count - fMaxVisibleCount) * fDeltaheight / fHeight / 2 - fYPosition)
  else
    fYPosition := 0;

  for i := 0 to fItems.Count - 1 do
    with GetItemByIndex(i) do
      fSelectAlpha := fSelectAlpha - progressTime.deltaTime * 2;

  item := GetItemByMouse;
  if (item >= 0) and (item < fItems.Count) then
    GetItemByIndex(item).fSelectAlpha := 1;
end;

Исходя из позиции мышки, мы начинаем изменять значение fYPosition плавным образом, чтобы наш список начал элегантно прокручиваться. Заодно изменяем значение альфы всех элементов списка.

Ну и отрисовываем наше добро:

Procedure TSimpleListBox.DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean);
var
  i: Integer;
  item: TSimpleListItem;
  dY: Integer;
begin
  dY := Round(-fHeight + fYPosition);
  fPanelFront.Position.X := Position.X;
  fPanelFront.Position.Y := Position.Y;
  fPanelBack.Position.X := Position.X;
  fPanelBack.Position.Y := Position.Y - 10;
  fPanelSelection.Position.X := Position.X;
  fItemHudText.Position.X := Position.X - 80;

  fPanelBack.DoRender(ARci, true, false);

  for i := 0 to fItems.Count - 1 do
    if(i < fItems.Count)
    and(dY + fDeltaHeight * i <  fHeight + fDeltaHeight / 2)
    and(dY + fDeltaHeight * i > -fHeight - fDeltaHeight / 2)
    then
    begin
      fPanelSelection.Position.Y := Position.Y + dY + fDeltaHeight * i;
      fItemHudText.Position.Y := Position.Y + dY + fDeltaHeight * i;

      item := GetItemByIndex(i);
      fItemHudText.Text := item.fText;
      with fPanelSelection.Material.GetActualPrimaryMaterial do
        FrontProperties.Diffuse.Alpha := item.fSelectAlpha;
      fPanelSelection.DoRender(ARci, true, false);
      fItemHudText.DoRender(ARci, true, false);
    end;

  fPanelFront.DoRender(ARci, true, false);
end;

Как мы видим, сначала выставляются всем панелькам позиции, а затем в правильном порядке вызывается для всех них DoRender() - элементы начинают показываться на экране!

Теперь в основной программе остается только:
  • создать шрифт (у нас это происходит в InitFont()),
  • загрузить материалы (смотрим на InitMaterials())
  • создать сам объект-список (fListBox := TSimpleListBox.CreateAsChild(...))
  • заполнить его элементами (fListBox.AddItemText())
  • постоянно обновлять позицию мыши (fListBox.SetMousePosition(MPos.x, MPos.y))
Возможно, последний пункт стоит самостоятельно выполнять в DoProgress(), но я пока оставил как есть... Видео для ленивых и не желающих качать демо:


Для полноценной работы также необходимо ловить нажатие клавиши мышки, чтобы уметь выбирать элементы списка - но это мы оставим "на потом", ведь для сегодняшней стартовой демки хватит того, что есть!

Вот, собственно, и все...
Кстати, завел новый репозиторий, заметили? Буду там складировать всякие демки, вроде текущей :)

Сообщения, схожие по тематике:

9 коммент.:

  1. Спасибо, Лампоголовый, классные вещи пишешь)))

    ОтветитьУдалить
  2. вот это 5+
    очень понравилось!
    чуток бы смягчить это все. а то ничего не видно, когда скролл идет с начала списка в самый конец.
    вот это зачет!!)

    ОтветитьУдалить
  3. зачетная картинка сопровождает пост

    ОтветитьУдалить
  4. Класно!
    Это хорошая идея писать свои наследники базового объекта сцены, но вероятно нужен хотя бы средний скил в работе с графикой что бы это делать.
    ЗЫ: задний фон хорош.

    ОтветитьУдалить
  5. gltrinix, благодарю! рад, что понравилось!

    Aero, благодарю! по поводу "смягчить" - имеешь ввиду, чтобы прокрутка была медленнее? я просто скорость подбирал так, чтобы было не долго ждать от последнего до первого элемента... возможно, стоит еще подкрутить! благодарю за замечание!

    по поводу картинки - да, скорее всего это не последнее сообщение о гуи))

    Yarov - благодарю! по поводу наследников от объекта сцены - момент двоякий на самом деле... еще одна трудность при переносе на другой рендер, в принципе... но плюс большой, поэтому остановился пока на таком варианте))
    скилл - дело наживное, в любой момент можно прокачаться до желаемого результата!

    если у кого будут идеи или примеры интересных элементов гуи - кидайте, буду рад попробовать реализовать на GlScene :)

    ОтветитьУдалить
  6. А я подчерпнул для себя то, что удобней писать свой метод CreateAsChild, а не Init.

    ОтветитьУдалить
  7. Только вот обратил внимание, что интерфейс похож то на яФоновский :)
    http://delphimax.wordpress.com/2010/12/15/iphone-gui-from-a-delphi-perspective/

    ОтветитьУдалить
  8. soofX, да, CreateAsChild удобен:)

    Yarov, iPhone'овцы вообще молодцы, много вкусного придумали! правда я действительно искал всякие флешовые примеры и наткнулся на тот, который вначале сообщения... то есть, как говорится, "все украдено до нас"))

    ОтветитьУдалить