Так уж случилось, что мне пришлось довольно много времени уделить разработке GUI компонент (контролов). Наиболее известные/популярные технологии в .NET это : WinForms — враперы для стандартных контролов Windows OS и новый и красивый WPF (уже не так уж новый, и далеко не такой красивый... вы уж мне поверьте :( ). Но не будем об грустном :), ведь под XNA стандартных контролов нет (хотя есть много готовых и бесплатных решений) и нам будет надо делать самим.
Хочу вас сразу застеречь, что написанное является опысом моих решений стоящих перед до мной вопросом и не является окончательной реализацией GUI (уверен найдется что-то что я не учел).
Введение
Создание Gui элетента делится на две важных для нас части — рендеринг и логика. В свою очередь рендеринг делится на рисование и компоновки элемента, а логика на роботу с вводом, обработка данных и т.д. На схеме показаны только те части о которых пойдет речь.
1) Layout
Наиболее сложный вопрос при создании интерфейса — правильная компоновка. Конечно что ее можно сделать вручную задав размеры и позицию каждому элементу, но в динамическом интерфейсе такой вариант неприемлем. В WPFе этим делом занимаются компоновочные элементы (GuiContainer). Реализуется компоновка в два шага
•измерение нужных размеров
•расстановка элементов
Сначала компоновочные элементы измеряют свои под-элементы, потом размещают в зависимости от реализированой логики.
Чтобы сделать первое у элемента есть методы
public Point Measure(Point point)protectedvirtual Point OnMeasure(Point point)
Первый реализует базовую логику работы с пропертями Padding ( отступ изнутри ), Margin ( отступ снаружи ), Width и Height (ширина и высота элемента). Проперти Width и Height имеют тип int? и могут быть не заданными. В таком случае размер считается по контенту. Второй метод служат для измерения контента элемента (тест, картинка, под-елементы).
Как и с Measure, первый реализует базовую логику работы Padding и Margin, а также пропертями HAlignment и VAlignment (смещение элемента относительно области расстановки). Второй метод – для расстановки контента.
Пример. В качестве примера рассмотрим реализацию класса StackPanel. Этот контейнер умеет размещать элементы один за другим в горизонтальном или вертикальном порядке (StackPanel.Orientation). В методе OnMeasure нам надо посчитать размер его контента – под-элементов. Пусть Orientation равен Horizontal, тогда ширина равна суме всех ширин элементов, а высота – наибольшая высота элементов. Реализация метода OnLayout похожа, только теперь мы уже размещаем элементы в нужном месте.
Домашнее задание – реализировать WrapPanel, унаследованную от GuiContainer. Этот контейнер должен размещать элементы в ряд (как буквы в текстовом редакторе). Если ряд выходить за границы элемента, мы начинаем новую строку.
2) Drawing
Пожалуй наиболее легкий но и самый не определенный вопрос.Есть много способов для того чтобы отрисовать элемент. Для начала выберем чем же мы будем отрисовывать. Простой вариант — графические примитивы.Это довольно просто и удобно, но создать сложный и красивый интерфейс примитивами почти невозможно. Потому я сразу перехожу к варианту с текстурами и SpriteBatch. Текстурами можно отрисовать почти любой интерфейс, хотя нет такой «вседозволенности». Для отрисовки у нас есть виртуальный метод
publicvirtualvoid Draw(SpriteBatch sprite)
Так как каждый элемент у нас должен иметь свою внешность, стандартной реализации у него нет (кроме компоновочных элементов которые отрисовывают свои под-элементы). А это значит что для каждого элемента вам надо будет самим реализовать такой метод.
Но все же чтобы упростить нам задачу с рисованием, мы добавим новый абстрактный класс — GuiDrawable. Этот объект представляет собой некий примитив, способный отрисоватся в указанную область. У него есть единственный абстрактный метод,
public abstract void Draw(SpriteBatch spriteBatch, Rectangle rect, Color color);
но возможно в дальнейшем вы захотите расширить функционал добавив например метод для рисования с поворотом, и т.д.
Класс TextureDrawable демонстрирует роботу простого GuiDrawable. Он просто обрисовывает текстуру в прямоугольник. Но зачем нам еще один класс, если можно просто отрисовать текстуру? Проблема в том, что иногда надо простой текстуры не достаточно. Например если рисовать одной текстурой элементы разных размеров, то они растягиваются и получается не очень красиво. Пример решения данной проблемы класс — BorderDrawable. Этот класс рисует текстуру таким образом, что растягивается только ее центральная часть, Вертикальные и горизонтальные края растягиваются только в своем направлении. Углы остаются без изменений в углах прямоугольника. Вы можете реализовать угодную вам реализацию этого класса.
Домашнее задание - реализовать класс анимации унаследованный от GuiDrawable. Пусть в классе будет массив текстур, и на каждом вызове Draw рисуем следующею. Когда прошли все текстуры, начинаем заново.
3) Logic / Input
А собственно вопрос логики мы решать не будем, так как и внешность, так и логика у каждого Gui элемента своя. Для ее реализации у нас есть простой виртуальный метод
publicvirtualvoid Update(GameTime gameTime)
который вы реализуете для каждого елемента по своему (или не реализуете).
Но мы решим вопрос с вводом. Для этого у нас есть компонента InputComponent(для доступа к компоненте, мы добавляем ее в сервисы). Все что он делает — это на каждом апдейте сохраняет текущее и предыдущее состояние мыши и клавиатуры (реализацию геймпада я не делал). Также есть несколько пропертей для проверки состояния мыши. Это все что нам надо.
Обработка ввода у нас будет осуществляется методами
public virtual void OnMouse\Keyboard <что-то там>(InputComponent input)
Единственым вопросом который остался - какой элемент должен оброблять ввод?
3a) Mouse
Мышь у нас будет оброблять элемент над которым она находится или элемент который ее захватил (например кнопка). Для реализации первого у каждого элемента есть метод
publicvirtual GuiElement HitTest(int x, int y)
который возвращает элемент по которому попал курсор мыши. В базовой реализации, элемент возвращает самого себя. В базовой реализации контейнеров, возвращает также и свои под-элементы.
класса GuiManager. Элемент который захватил мышь будет обрабатывать все изменения мыши пока не будет вызван метод ReleaseMouse.
3b) Keyboard
С обработкой клавиатуры все проще, ее обробляет элемент на котором есть фокус. Элемент на котором должен быть фокус задается пропертей
public GuiElement FocusedElement
класса GuiManager. котором должен быть фокус задается пропертей. Есть несколько способов передачи фокуса : по нажатию какой нибудь клавиши, по нажатию мыши, в результате выполнения некого условия логики GUI. В примере, фокус использует только элемент TextEditBox по нажатию мыши на элементе.
Домашнее задание - реализовать элемент TrackBar\ScrolBar унаследованный от GuiElement. Вам будет нужно добавить две проперти типа GuiDrawable для ползунка и фону. При нажатии на ползунку элемент должен захватить мышь.
ContentElement
ContentElement – особый класс элементов, конечный контент которых не определен. Поэтому в качестве контента в нем служит его единственный под-элемент.
Пример. Клас ContentButton – реализует элемент «кнопка». Но так как в кнопке может быть текст или рисунок, или то и другое вместе (или еще что-то), то контентом его есть неопределенным. Унаследовал это от ContentElement в зависимости от случая мы можем поместить в него или элемент текста (TextBlock) или элемент картинки (ImageBlock).
GuiManager
Класс GuiManager – компонента которая отрисовывает и апдейтит элементы GUI. Для добавления элементов есть пропертя GuiElement RootElement. Так как это не коллекция, то можно задать только один элемент. В большинстве случаев этим элементом есть компоновочный элемент.
GuiManager.Update
В методе void Update(GameTime gameTime) заключена вся логика работы менеджера, поэтому очень важно понять принцип его работы. В этом нам поможет маленькая диаграмма.
На вопрос – «почему обработка ввода и апдейт элементов идет первым», ответ – «потому что во время апдейта или обработка ввода, возможно надо пересчитать все заново, как например в редакторе текста».
Оптимизация
Возможны несколько вариантов оптимизации, вот два из них :
оптимизировать Layout перерасчетом не всего дерева, а только тех ветвей которые надо пересчитать (например если у контейнера установлен размер вручную, то надо пересчитать размер только его под-элементов)
оптимизировать прорисовку отрисовав все на текстуру и перерисовывать только по надобности.
Заключение
Вот и все, я старался не делать очень много кода (ну не очень получилось :/ ). Хочется верить что это поможет вам в реализации своего полноценного GUI.