Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Элемент управления Grid

Anna | 24.06.2014 | нет комментариев
Табличные элементы управления (традиционно в их наименовании присутствуют слова Table либо Grid) обширно применяются при разработке GUI. Так получилось, что на работе мы используем С и MFC для разработки пользовательского интерфейса. В начале мы применяли CGridCtrl — общедоступную и достаточно вестимую реализацию грида. Но с некоторого времени он перестал нас устраивать и возникла на свет собственная разработка. Идеями, лежащими в основе нашей реализации, я хочу с вами тут поделиться. Есть задумка сделать open source план (скорее каждого под Qt). Следственно данную заметку дозволено рассматривать как «Proof Of Concept». Конструктивная критика и примечания приветствуются.

Поводы, по которым меня не устраивают существующие реализации я опущу (это тема для отдельной заметки).
Планы у нас инженерно-научные, с богатой графикой, и списки и таблицы применяются повсюду. Следственно новейший грид должен был обеспечивать эластичную кастомизацию, отличное быстродействие и минимальное потребление памяти при показе крупных объемов информации. При разработке я усердствовал придерживаться дальнейшим правилом: реализуй функциональность максимально обобщенно и абстрактно, но не во урон удобству применения и оптимальности работы. Безусловно, это правило двойственно, но насколько мне удалось соблюсти равновесие — судить вам.Дабы с чего-то начать, давайте испробуем дать определение элементу управления grid. Для сохранения общности дозволено сказать, что grid — это визуальный элемент, тот, что разбивает пространство на строки и столбцы. В итоге получается сетка ячеек (место пересечения строк и столбцов), внутри которых отображается некоторая информация. Таким образом у грида дозволено различить два компонента: конструкцию и данные. Конструкция грида определяет как мы будем разбивать пространство на строки и столбцы, а данные описывают, собственно, то, что мы хотим видеть в получившихся ячейках.

Как мы определили выше, конструкция грида (дозволено сказать топология) описывается строками и столбцами. Строки и столбцы — объекты дюже схожие. Дозволено сказать неразличимые, только одни разбивают плоскость по горизонтали, а другие по вертикали. Но делают они это идентичным образом. Тут мы теснее подходим к достаточно крошечной и самодостаточной сущности, которую дозволено оформить в C класс. Я назвал такой класс Lines (по русски дозволено определить как Линии либо Полосы). Данный класс будет определять комплект линий (строк либо столбцов). Углубляться и определять класс для отдельной линии нет необходимости. Класс получится маленьким и нефункциональным. Таким образом Lines будет определять свойства комплекта строк либо стобцов и операции, которые над ними дозволено изготавливать:

  • Основное качество Count — число линий, из которых состоит Lines
  • Всякая линия может менять свой размер (строка высоту, а столбец — ширину)
  • Линии дозволено переупорядочивать (строки сортировать, столбцам менять порядок)
  • Линии дозволено скрывать (делать заметными для пользователя)

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

class Lines
{
public:
    Lines(UINT_t count = 0);

    UINT_t GetCount() const { return m_count; }
    void SetCount(UINT_t count);

     UINT_t GetLineSize(UINT_t line) const;
     void SetLineSize(UINT_t line, UINT_t size);

    bool IsLineVisible(UINT_t line) const;
    void SetLineVisible(UINT_t line, bool visible);

    template <typename Pred> void Sort(const Pred& pred);

    const vector<UINT_t>& GetPermutation() const;
    void SetPermutation(const vector<UINT_t>& permutation);

    UINT_t GetAbsoluteLineID(UINT_t visibleLine) const;
    UINT_t GetVisibleLineID(UINT_t absoluteLine) const;

    Event_t<void(const Lines&, unsigned)> changed;

private:
    UINT_t m_count;
    vector<UINT_t> m_linesSize;
    vector<bool> m_linesVisible;
};

Комментарии и некоторые служебные функции и поля опущены для наглядности.
Вы можете подметить, что в классе есть функции GetAbsoluteLineID и GetVisibleLineID. Так как мы разрешаем перемешивать и скрывать линии, то безусловный и видный индекс линии различаются. Верю картинка наглядно показывает эту обстановку.

Также необходимо сделать пояснение по поводу строки

Event_t<void(const Lines&, unsigned)> changed;

Тут определён сигнал (так он именуется в Qt либо boost). С появлением С 11 и std::function, дозволено легко написать примитивную реализацию signals/slots, Дабы не зависеть от внешних библиотек. В данном случае мы определили эвент в классе Lines, и к нему дозволено подключать всякую функцию либо функтор. Скажем грид подключается к этому эвенту и получает оповещение, когда экземпляр Lines меняется.

Таким образом конструкция грида у нас представлена двумя экземплярами Lines:

private:
    Lines m_rows;
    Lines m_columns;

Переходим к данным. Каким образом давать гриду информацию о том, какие данные он будет отображать и как их отображать? Тут теснее всё изобретено до нас — я воспользовался триадой MVC (Model-View-Controller). Начнем с элемента View. Так же как класс Lines определяет не одну линию, а целый комплект, определим класс View как кое-что, что отображает какие-то однородные данные в некотором подмножестве ячеек грида. Скажем, у нас в первом столбце будет отображаться текст. Это обозначает, что мы обязаны сделать объект, тот, что может отображать текстовые данные и тот, что может говорить, что отображаться эти данные обязаны в первой колонке. Так как данные у нас могут отображаться различные и в различных местах, то отменнее реализовать эти функции в различных классах. Назовем класс, тот, что может отображать данные, собственно View, а класс, тот, что может говорить где данные отображать Range (комплект ячеек). Передавая в грид два экземпляра этих классов, мы как раз указываем что и где отображать.

Давайте подробнее остановимся на классе Range. Это ошеломительно небольшой и сильный класс. Его основная задача — стремительно отвечать на вопрос, входит ли определенная ячейка в него либо нет. По сути это интерфейс с одной функцией:

class Range
{
public:
    virtual bool HasCell(CellID cell) const = 0;
};

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

class RangeAll
{
public:
    bool HasCell(CellID cell) const override { return true; }
};
class RangeColumn
{
public:
    RangeColumn(UINT_t column): m_column(column) {}
    bool HasCell(CellID cell) const override { return cell.column == m_column; }
private:
    UINT_t m_column;
};

1-й класс определяет комплект из всех ячеек, а 2-й — комплект из одного определенного столбца.

Для класса View осталась одна функция — отрисуй данные в ячейке. На самом деле для полновесной работы View должен уметь отвечать еще на пару вопросов:

  • Сколько нужно места, что бы отобразить данные (скажем Дабы колонкам установить ширину, довольную для отображения текста — режим Fit)
  • Дай текстовое представление данных (Дабы скопировать в буфер обмена как текст либо отобразить в tooltip)
class View
{
public:
    virtual void Draw(DrawContext& dc, Rect rect, CellID cell) const = 0;
    virtual Size GetSize(DrawContext& dc, CellID cell) const = 0;
    virtual bool GetText(CellID cell, INTENT intent, String& text) const = 0;
};

А что, если мы хотим отрисовать различные типы данных в одной и той же ячейке? Скажем нарисовать иконку и рядом текст либо нарисовать чекбокс и рядом текст. Не хотелось бы для этих комбинаций реализовывать обособленный тип View. Давайте позволим в одной ячейке показывать несколько View, только необходим класс, тот, что говорит как поместить определенный View в ячейке.

class Layout
{
public:
    virtual void LayoutView(DrawContext& dc, const View* view, Rect& cellRect, Rect& viewRect) const = 0;
};

Для наглядности разглядим пример в котором в первом столбце отображаются чекбоксы и текст. Во втором столбце представлены радио-кнопки, квадратики с цветом и текстовое представление цвета. И еще в одной ячейке есть звёздочка.

Скажем для чекбокса мы будем применять LayoutLeft, тот, что спросит у View его размер и «откусит» прямоугольник надобного размера от прямоугольника ячейки. А для текста мы будем применять LayoutAll, к которому в параметре cellRect перейдет теснее усеченный прямоугольник ячейки. LayoutAll не будет спрашивать размер у своего View, а легко «заберет» все доступное пространство ячейки. Дозволено напридумывать много различных пригодных Layouts, которые будут комбинироваться с всякими View.

Возвратимся к классу Grid, для которого мы хотели задавать данные. Получается, что беречь мы можем тройки <Range, View, Layout>, которые определяют в каких ячейках, каким образом отображать данные, плюс как эти данные обязаны быть расположены внутри ячейки. Выходит класс Grid у нас выгляд_permark!Так же как и для отрисовки, для работы с мышью нам необходимы только видимые ячейки. Добавим в класс GridCache функции обработки мыши. По расположению курсора мыши определим какая ячейка (CacheCell) находится под ней. Дальше в ячейке для всех View, в чей прямоугольник попала мышь, забираем Controller и вызываем у него соответствующий способ. Если способ возвратил true — прекращаем обход Views. Данная схема работает довольно стремительно. При этом нам пришлось в класс View добавить ссылку на Controller.

Осталось разобраться с классом Model. Он необходим как образец адаптер. Его основная цель — предоставить данные для View в «комфортном» виде. Давайте разглядим пример. У нас есть ViewText тот, что может рисовать текст. Что бы его нарисовать в определенной ячейке, данный текст нужно для ячейки запросить у объекта ModelText, тот, что, в свою очередь, лишь интерфейс, а его определенная реализация знает откуда текст взять. Вот приблизительная реализация класса ViewText:

class ViewText: public View
{
public:
    ViewText(ModelText model): m_model(model) {}
    void Draw(DrawContext& dc, Rect rect, CellID cell) const override
    {
         const String& text = model->GetText(cell);
         dc.DrawText(text, rect);
    }
private:
    ModelText m_model;
};

Таким образом нетрудно угадать какой интерфейс должен быть у ModelText:

class ModelText: public Model
{
public:
    virtual const String& GetText(CellID cell) const = 0;
    virtual void SetText(CellID cell, const String& text) = 0;
};

Обратите внимание, мы добавили сеттер для того, что бы им мог воспользоваться контроллер. На практике особенно Зачастую применяется реализация ModelTextCallback

class ModelTextCallback: public ModelText
{
public:
    function<const String&(CellID)> getCallback;
    function<void(CellID, const String&)> setCallback;

    const String& GetText(CellID cell) const override { return getCallback(cell); }
    void SetText(CellID cell, const String& text) override { if (setCallback) setCallback(cell, text); }
};

Эта модель разрешает при инициализации грида назначить лямбда функции доступа к настоящим данным.
Ну а что же всеобщего у моделей для различных данных: ModelText, ModelInt, ModelBool …? В всеобщем-то ничего, исключительное, что про них всех дозволено сказать, что они обязаны сообщать все заинтересованные объекте о том, что данные изменились. Таким образом базовый класс Model у нас примет дальнейший вид:

class Model
{
public:
    virtual ~Model() {}
    Event_t<void(Model)> changed;
};

В результате наш грид разбился на уйма маленьких классов, всякий из которых исполняет Отчетливо определенную небольшую задачу. С одной стороны может показаться, что для реализации грида представлено слишком много классов. Но, с иной стороны, классы получиличь маленькими и примитивными, с Отчетливыми взаимосвязями, что упрощает осознавание кода и сокращает его трудность. При этом всевозможные комбинации преемников классов Range, Layout, View, Controller и Model дают дюже огромную вариативность. Применение лямбда функций для ModelCallback разрешают легко и стремительно объединять грид с данными.

В дальнейшей заметке я опишу как реализовать стандартную функциональность грида: selection, sorting, column/row resize, printing, как добавить заголовок (фиксированные верхние строки и левые столбцы).
Раскрою маленький секрет — все что описано в данной статье теснее довольно для реализации вышеперечисленного. Если какую-то функциональность я пропустил, пожалуйста, пишите в комментариях и я опишу их реализацию в дальнейшей статье.

Напоследок покажу несколько примеров, как применяется грид у нас в планах.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB