Недавно я приводил список ближайших добавлений в наш замечательный сундучок простых gui-элементов, созданных в связке Delphi+GlScene. Так вот первым в списке был круговой выбор элементов или как еще его называют "радиальное меню". Его и делаем сегодня! Применений у такого объекта множество:
|
Итак, мы делаем вот такой список, отображаемый по кругу:
Механика такова: пользователь наводит курсор мыши на объект - и по кругу появляются доступные действия в виде кнопок. В общем, дело безхитростное - такое нужно садиться и программировать! Первым делом определяемся с сеттингом демки, а раз меню круглое и объекты все круглые - я решил, что данный элемент отлично впишется в космический сеттинг с планетами впереди и темным глубоким фоном позади...
Немного погуглив, нашлись подходящие картинки планет и космоса. Общее наполнение в итоге стало выглядеть так:
Радиальное меню
Ну и сразу за программирование радиального меню! Описание класса получилось довольно объемное:
TRadialGroup = class (TGlHudSprite) protected fElements: TList; fShowSpeed: Single; fHideSpeed: Single; fVisible: Boolean; fMousePos: TVector; fSelected: Integer; fBasePosition: TVector; fGroupRadius: Single; fOnMouseClick: TOnMouseClick; fLButtonDown: Boolean; fTweener: TTweener; protected Function GetItem(aIndex: Integer): TRadialElement; Function GetItemRect(aIndex: Integer): TVector; public property ShowSpeed: Single read fShowSpeed write fShowSpeed; property HideSpeed: Single read fHideSpeed write fHideSpeed; property GroupRadius: Single read fGroupRadius write fGroupRadius; property OnMouseClick: TOnMouseClick read fOnMouseClick write fOnMouseClick; Procedure Show; Procedure Hide; Procedure AddElements(fMaterialNames: Array of String); Function GetSelected: Integer; Procedure SetMousePos(aMousePos: TVector); Procedure DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean); override; Procedure DoProgress(const progressTime: TProgressTimes); override; Constructor Create(AOwner: TComponent); override; end;
В принципе, ничего серьезного, но строчек много получилось, сам не ожидал... Пройдусь по основным моментам:
- В Show() и Hide() методах запускаются tween'ы для каждой кнопки с небольшой задержкой между друг другом. Таким образом можно достичь появления кнопок одной за другой, как бы последовательно. Мне показалось, что так красивее :)
- GetItemRect(aIndex: Integer) выдает вектор-прямоугольник для элемента под номером aIndex. Для того, чтобы добраться до размеров текстуры, приходится переключать материал и уже по значениям ширины и высоты текстуры строить вектор результата:
Material.LibMaterialName := GetItem(aIndex).fMaterialName; Angle := pi/2 + aIndex * pi * 2 / fElements.Count; w := Material.GetActualPrimaryTexture.Image.Width; h := Material.GetActualPrimaryTexture.Image.Height; Result[0] := fBasePosition[0] + fGroupRadius * cos(Angle) - w/2; Result[1] := fBasePosition[1] + fGroupRadius * sin(Angle) - h/2; Result[2] := Result[0] + w; Result[3] := Result[1] + h;
- В перекрытом DoRender() теперь дело за малым: по значениям из GetItemRect() правильно позиционировать спрайт, выставить ему альфу и размеры... Все это умещается в таком маленьком коде:
for i := 0 to fElements.Count - 1 do begin r := GetItemRect(i); Position.x := (r[0] + r[2]) / 2; Position.y := (r[1] + r[3]) / 2; Width := (r[2] - r[0]) * GetItem(i).fScale; Height := (r[3] - r[1]) * GetItem(i).fScale; Material.GetActualPrimaryMaterial.FrontProperties.Diffuse.Alpha := GetItem(i).fAlpha; inherited DoRender(ARci, true, false); end
- Ну а DoProgress() как всегда занимается апдейтом состояний - находится подсвеченный элемент, вызывается для него tween'инг размеров; а также проверяется состояние кнопок мыши, чтобы при необходимости вызвать событие OnMouseClick()
- На мой взгляд очень удобно работать с получившимся классом снаружи: для добавления элементов достаточно вызвать метод AddElements() со списком материалов, которые будут отображаться в качестве элементов самого меню. То есть пишим одну строку
fGroup.AddElements(['gallery', 'audio', 'default', 'chat']);
и в наше меню добавляются четыре кнопки-элемента. Все эти кнопки обрабатываются в вышеописанных методах. Ничего больше делать не нужно.
Итак, настройка такого радиального меню занимает всего несколько строк:
fGroup := TRadialGroup.CreateAsChild(fMainDummy); fGroup.AddElements(['chat', 'gallery', 'chat']); fGroup.Position.SetPoint(fPlanet.Position.AsVector); fGroup.Material.MaterialLibrary := fMatLib; fGroup.OnMouseClick := OnItemMouseClick; fGroup.GroupRadius := 90;
Последняя строка (присвоение GroupRadius) необходима для установки, на каком расстоянии от центра будут располагаться кнопки. Остается только передавать положение курсора мышки, чтобы меню смогло корректно подсвечивать свои элементы:
GetCursorPos(Mpos); MPos := fViewer.ScreenToClient(MPos); fGroup.SetMousePos(VectorMake(MPos.x, MPos.y, 0));
Планета-кнопка
Для того, чтобы на экране отобразить сразу несколько планет с доступными для них радиальными менюшками, я решил создать небольшой support-класс TPlanetButton, описание которого получилось тоже довольно увестистое:
TOnMouseOver = Procedure (aSelf: TGlHudSprite) of object; TOnMouseOut = Procedure (aSelf: TGlHudSprite) of object;
TPlanetButton = class (TGlHudSprite) protected fMousePos: TVector; fSelected: Boolean; fOverRadius: Single; fOutRadius: Single; fAnimationTime: Single; protected fOnMouseOver: TOnMouseOver; fOnMouseOut: TOnMouseOut; public property OnMouseOver: TOnMouseOver read fOnMouseOver write fOnMouseOver; property OnMouseOut: TOnMouseOut read fOnMouseOut write fOnMouseOut; property OverRadius: Single read fOverRadius write fOverRadius; property OutRadius: Single read fOutRadius write fOutRadius; Function IsHit(aPosition: TVector): Boolean; Procedure SetMousePos(aMousePos: TVector); Procedure DoProgress(const progressTime: TProgressTimes); override; Constructor Create(AOwner: TComponent); override; end;
По сути такая кнопка ничего не умеет, за исключением вызова событий OnMouseOver и OnMouseOut при движениях мыши по своей области.
Итог
Вот и все...Выглядит это вот так:
Наводим на любую планету - показывается соответствующее меню. Каждый элемент немного меняет размеры при движениях по ним мыши. При клике - показывается хинт о том, по какому элементу нажали.
Как всегда скачать демку с исходниками можно отсюда; либо можно обновиться через svn, в котором сейчас всего 12 ревизий :)
p.s. Очень хочется выложить видео с демонстрацией того, как это все анимировано, но у меня к сожалению сейчас слишком медленный интернет для этого...
p.p.s Появилась возможность выложить видео - с радостью добавляю!
Ух) красиво получилось =) буду ждать Drag'n'Drop.
ОтветитьУдалитьДавно хотел спросить:
Сколько примерно уходит времени на подобную демку? такое чувство будто.. будто сел написал за пару минут и готово.
И откуда берутся подобные формулы - "Angle := pi/2 + aIndex * pi * 2 / fElements.Count;"? как они выводятся?
Как-как) Садишся да выводиш формулу какую надо) Потом параметры корректируеш и смотриш на результат.
ОтветитьУдалитьЭто быстрее чем что-то готовое под частные случаи искать.
В этом-то ничего сверхестественного, а вот время написания правда интересно.
Ulop, благодарю!
ОтветитьУдалитьпо времени точно не знаю - никогда не засекал... наверно часа 3-4, может больше)) могу в следующий раз замерить!
по поводу формулы - мне нужно отобразить элементы по кругу! так как я хочу, чтобы кнопки были симметричны относительно вертикали, тогда я выбрал, чтобы первому элементу соответствовал угол pi/2. всего fElements.Count элементов, которые нужно расположить от pi/2 до pi/2 + 2*pi (обойдем полный круг).
выход обычная линейная зависимость:
K*X + B, в нашем случае:
Pi/2 + aIndex * (Pi*2) / fElements.Count
где pi/2 - это B, сдвиг на четверть круга, чтобы первая кнопка была внизу по центру.
Pi*2/fElements.Count - есть K, а именно указывается, что всего нужно пройти Pi*2, но количество элементов fElements.Count на которые мы и делим...
прикидывая в голове можно быстро построить цепочку для 4ех элементов (индексы идут от 0 до 3):
0: Pi/2 + 0 * Pi*2 / 4 = Pi/2
1: Pi/2 + 1 * Pi*2 / 4 = Pi
2: Pi/2 + 2 * Pi*2 / 4 = 3*Pi/2
3: Pi/2 + 3 * Pi*2 / 4 = 2*Pi
Monax,
ок, в следующий раз постараюсь замерить время))
Спасибо за пояснение) вот что значит математика) всё оказалось намного проще
ОтветитьУдалитьLampogolovii, подскажите, почему при запуске успешно скомпилированного мной любого вашего исходника у меня возникает "Access violation...".
ОтветитьУдалитьGLScene 5593 (Delphi 2010)
Ulop, здорово, что все понятно! а то я не всегда могу вразумительно объяснить математические выкладки...
ОтветитьУдалитьАнонимный, хм... а какой у тебя снимок сцены? и что там на строке 5593?
Что за снимок сцены? 5593 - это ревизия GLScene.
ОтветитьУдалитьВот например демки для начинающих: http://glscene.ru/download.php?view.566 (по описания, вроде ваши). Там в комментариях человек пишет, что у него запускается только TextLEDAlpha, остальные выдают "Access violation...".
И у меня так же, TextLEDAlpha - нормально, остальное компилируется, но ошибка при запуске
Анонимный,
ОтветитьУдалитьхм... интересно! а у тебя есть аська? можешь кинуть мне ее на
lampogolovii[собака]mail.ru
так быстрее локализуем проблему =)