25 апреля 2011

GUI: Панелька из 9ти кусочков...

Недавно один из форумчан glscene.ru спрашивал о способе отображения панельки с изменяемой шириной. Простым масштабированием (Scale) спрайта правильного эффекта не всегда можно добиться. Например, нам нужны закругленные края или небольшая тень от панельки. При масштабировании эти эффекты безобразно увеличатся и будут портить всю картинку. Как же быть в случае, если заранее ширина или высота такой панельки неизвестна и просто нарисовать png-шку нужного размеры мы не сможем? На ум приходит собирать итоговую картинку из нескольких элементов - углы, края и серединка. Вот такую панельку, состоящую из 9ти кусочков мы и будем сегодня делать на GlScene.

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

  TSimplePanel = class (TGLHudSprite)
  protected
    fCorner, fHalfCorner: Integer;
    Procedure MainInit;
  public
    Function NCHitTest(aPosition: TVector): TNCSizing;
    Function IsHit(aPosition: TVector): Boolean;
    Procedure DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean); override;
    Constructor Create(AOwner: TComponent); reintroduce;
    Constructor CreateAsChild(aParentOwner: TGLBaseSceneObject);
  end;

где NCHitTest() и IsHit() нужны для возможности изменения размеров панельки при помощи мыши в realtime-режиме!
Заглянем в главный метод DoRender() и найдем для себя базовый метод отрисовки 9ти кусочков нашей панельки:

Procedure TSimplePanel.DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean);
var
  left, top: integer;
  eWidth, eHeight: Single;
  ePosition: TVector;
  Procedure RenderHud(x, y, w, h: integer;  a, b, c, d: Single);
  begin
    Position.SetPoint(left + x, top + y, 0);
    with Material.GetActualPrimaryTexture do
    begin
      MappingMode := tmmObjectLinear;
      MappingSCoordinates.SetVector(1 / w * a, 0, 0, b);
      MappingTCoordinates.SetVector(0, 1 / h * c, 0, d);
    end;
    Width := w;
    Height := h;
    inherited DoRender(ARci, true, false);
  end;
var
  w, h: integer;
begin
  eWidth := Width;
  eHeight := Height;
  ePosition := Position.AsVector;
  
  left := Round(Position.x - Trunc(Width / 2));
  top  := Round(Position.y - Trunc(Height / 2));
  w := (Round(Width)  - fCorner * 2) div 2;
  h := (Round(Height) - fCorner * 2) div 2;
  RenderHud(fHalfCorner,                   fHalfCorner, fCorner, fCorner, 1/4, 1/8, 1/4, 7/8);
  // ... и так далее для всех 9ти элементов панели
  RenderHud(fCorner + w,                   fHalfCorner, 2 * w,   fCorner, 1/2, 0.5, 1/4, 7/8);
  RenderHud(fCorner + 2 * w + fHalfCorner, fHalfCorner, fCorner, fCorner, 1/4, 7/8, 1/4, 7/8);

  RenderHud(fHalfCorner,                   h + fCorner, fCorner, 2 * h, 1/4, 1/8, 1/2, 0.5);
  RenderHud(fCorner + w,                   h + fCorner, 2 * w,   2 * h, 0.5, 0.5, 1/2, 0.5);
  RenderHud(fCorner + 2 * w + fHalfCorner, h + fCorner, fCorner, 2 * h, 1/4, 7/8, 1/2, 0.5);

  RenderHud(fHalfCorner,                   2 * h + fCorner + fHalfCorner, fCorner, fCorner, 1/4, 1/8, 1/4, 1/8);
  RenderHud(fCorner + w,                   2 * h + fCorner + fHalfCorner, 2 * w,   fCorner, 1/2, 0.5, 1/4, 1/8);
  RenderHud(fCorner + 2 * w + fHalfCorner, 2 * h + fCorner + fHalfCorner, fCorner, fCorner, 1/4, 7/8, 1/4, 1/8);

  Width := eWidth;
  Height := eHeight;
  Position.SetPoint(ePosition);
end;

Выглядит ужасно, согласен. Но это код из разряда "один раз написал и забыл", поэтому пока трогать и рефакторить его не собираюсь. По сути запоминаем текущие размеры панельки, ренедерим необходимые элементы, и выставляем исходные размеры.

Теперь об использовании данного класса...
Достаточно по обыкновению определить основные свойства и на экране появится симпатичная панелька:

fPanel := TSimplePanel.CreateAsChild(fMainDummy);
  fPanel.Material.MaterialLibrary := fMatLib;
  fPanel.Material.LibMaterialName := 'white';
  fPanel.Position.SetPoint(600, 200, 0);
  fPanel.Width := 200;
  fPanel.Height := 200;
В нашем случае будет что-то вроде такого


Для красоты и интерактивности я позволил себе добавить список различных "скинов". Сам список - это элемент, который мы создали ранее. Поэтому демка принимает следующий вид:



Код самой программы вроде нетруден, поэтому я в дополнение просто приведу методы, которые решил добавить в качестве украшения - панельку можно resize-ить за стороны в любой момент! А также таскать саму панельку за любое произвольное место с зажатой левой кнопкой мыши.
Весь механизм по сути заключается в двух методах (UpdateMoving и UpdateSizing):

Procedure TfrmMain.UpdateSizing(MPos: TVector; DeltaPos: TVector);
begin
  if not fDoSizing then
  begin
    fSizing := fPanel.NCHitTest(MPos);
    if (fSizing = cs_Left) or (fSizing = cs_Right) then
      fViewer.Cursor := crSizeWE
    else if (fSizing = cs_Top) or (fSizing = cs_Bottom) then
      fViewer.Cursor := crSizeNS
    else
      fViewer.Cursor := crDefault;
  end
  else
  begin
    if fSizing = cs_Left then
    begin
      fPanel.Width := fPanel.Width - Trunc(DeltaPos[0] / 2) * 2;
      fPanel.Position.X := fPanel.Position.X + Trunc(DeltaPos[0] / 2);
    end
    else if fSizing = cs_Right then
    begin
      fPanel.Width := fPanel.Width + Trunc(DeltaPos[0] / 2) * 2;
      fPanel.Position.X := fPanel.Position.X + Trunc(DeltaPos[0] / 2);
    end
    else if fSizing = cs_Top then
    begin
      fPanel.Height := fPanel.Height - Trunc(DeltaPos[1] / 2) * 2;
      fPanel.Position.Y := fPanel.Position.Y + Trunc(DeltaPos[1] / 2);
    end
    else if fSizing = cs_Bottom then
    begin
      fPanel.Height := fPanel.Height + Trunc(DeltaPos[1] / 2) * 2;
      fPanel.Position.Y := fPanel.Position.Y + Trunc(DeltaPos[1] / 2);
    end;
  end;
end;

Procedure TfrmMain.UpdateMoving(MPos: TVector; DeltaPos: TVector);
begin
  if fMousePressed and fPanel.IsHit(MPos) and not fDoSizing then
    with fPanel.Position do
    begin
      SetPoint(x + DeltaPos[0], y + DeltaPos[1], 0);

      if x <= 380 then
        x := 380
      else if x > 700 then
        x := 700;

      if y <= 100 then
        y := 100
      else if y > 500 then
        y := 500;
    end;
end;

Видео происходящего:


Думаю, на этом все, как обычно исходники качаем отсюда...
Всем удачи!

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

3 коммент.:

  1. gltrinix, всегда рад! если будут идеи еще каких гуи-элементов - буду очень признателен...

    ОтветитьУдалить
  2. Корни подхода вечно живут в web-верстке под css <3.0. Поэтому геймдевелоперам, имеющим опыт верстки, такой подход практически родной :)

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