Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4470.html
 

Практика #2. Настройка таблицы.

Опубликовано 10.05.2011 г. 17:08

Данная статья является продолжением предыдущей и будет посвящена механизму настройки вида таблицы данных.

Вернемся к концу предыдущей статьи. Мы определились, что после переключения элемента данных в списке, необходимо настроить вид таблицы представления данных. Что для этого необходимо? Нужно знать количество строк и столбцов. Будем полагать что первая строка и столбец фиксированы, и содержат заголовки, поэтому их необходимо заполнить. Так что элемент данных TCustomDataItem должен предоставлять 4 свойства: количество строк и столбцов (целое), заголовки строк и столбцов (строка). Формировать коллекцию заголовков будут конечно же конечные классы, но предоставлять доступ к элементам коллекции должен базовый класс. В данном случае опять удобно использовать обобщенный список объектов:
THeaderCollection = TObjectList<THeaderItem>;
Таким образом у нас будет две подобных коллекции - для строк и столбцов. Что должен представлять из себя элемент коллекции? Безусловно необходим сам заголовок - строка. В принципе этого достаточно, но давайте смотреть на вещи более широко и наделим каждый элемент своим числовым идентификатором. Базовый класс TCustomDataItem будет редактировать элемент по номеру его столбца и строки в таблице, а конечный класс будет получать уже соответствующий идентификатор столбца или строки. Это может оказаться весьма удобным, особенно если нам понадобится поменять столбцы местами. Так что определим элемент заголовка следующим образом:
    THeaderItem = class(TObject)
      strict private
        FId : integer;
        FTitle : string;
       public
         constructor Create(aId : integer; aTitle : string);
         property id : integer read FId;
         property title : string read FTitle;
    end;
Значения идентификатора и заголовка передаются в параметрах конструктора, что является вполне обычной практикой программирования. Свойства id & title являются readonly, что вполне логично. Единожды их установив при создании, мы не можем больше их изменить, и этим гарантируем, что никто другой их также не изменит. В принципе надо полагать, что все данные которые мы будем изменять, будут часто содержать какие то однообразные коллекции заголовков. Поэтому мы можем ввести вспомогательный класс, для создания типичных коллекций. Например, это может быть коллекция годов. Так что давайте создадим такой класс. Отмечу, что в нем мы будем использовать классовые методы, что позволит нам вызывать методы создания коллекций без создания экземпляра класса.
    TDefaultHeaderCollections = class(TObject)
        class function getYearCollection(const StartYear, EndYear : integer) : THeaderCollection;
    end;
...
class function TDefaultHeaderCollections.getYearCollection(const StartYear, EndYear: integer): THeaderCollection;
var hi : THeaderItem;
    i : integer;
begin
    result := THeaderCollection.Create();

    for i:= startYear to endYear do  begin
        hi := THeaderItem.Create(i,  intToStr(i) );
        result.Add(hi);
    end;
end;
Набор таких методов будет весьма ограничен. Заметьте, идентификатор элемента - это значение самого года. И это будет удобно. Если я в конечном классе создал коллекцию, где идентификатор - значение года, то и при вызове, например, функции сохранения я буду получать не индекс элемента коллекции, а именно нужный идентификатор, что упростит мне реализацию конечных классов. Моя процедура заполнения таблицы теперь принимает следующий вид:
procedure TDataEditForm.UpdateGridState;
var c,r : integer;
begin
    dataGrid.BeginUpdate();
    try
        //заголовки
        with DataGrid do begin
            RowCount := FixedRows + FCurrentItem.RowCount;
            ColCount := FixedCols + FCurrentItem.ColCount;

            for c := 0 to FCurrentITem.ColCount - 1 do begin
                cells[fixedCols + c, 0] := FCurrentItem.ColHeaders[c];
            end;
            for r := 0 to FCurrentItem.RowCount - 1 do begin
                cells[0, fixedRows + r] := FCurrentItem.RowHeaders[r];
            end;

            //содержимое
            for c := fixedCols to fixedCols + FCurrentItem.ColCount - 1 do begin
                for r := fixedRows to fixedRows + FCurrentItem.RowCount -1 do begin
                    cells[c, r]  := FCurrentItem.Cells[ItemCol[c], itemRow[r]].value;
                    Colors[c, r] := CellColors[ FCurrentItem.CellState[itemCol[c], itemRow[r]] ];
                end;
            end;
        end;
    finally
        dataGrid.EndUpdate();
        dataGrid.AutoSizeColumns(true);
    end;
end;
Я использую в работе таблицы TMS. Обратите внимание, обычно таблицы содержат свойства fixedCols & fixedRows, в большинстве случаев, наверное, эти значения равны 1. В результате часто когда вы обходите таблицу (обычные ячейки), то пишете цикл вида
for c := 1 to ColCount -1 do 
Не стоит такого делать. Всегда для этих целей начинайте цикл с fixedCols. В дальнейшем, при возможном увеличении числа фиксированных столбцов или строк, вы избавите себя от лишней работы. Также братите внимание, часто различные компоненты, или списки имеют два метода - BeginUpdate & EndUpdate, позволяющие изменять элементы не вызывая сопутствующих событий. Например для списка это может быть вызов события "Добавлен Элемент", при работе с таблицой это может быть ее перерисовка. Так что, если вы производите какие либо массовые действия с коллекциями, используйте данные методы, если не требуется обратного. У обычной таблицы как вы можете заметить нет свойства Colors, но вы можете его добавить и без использования наследования - используя классы помощники (class helper). Впрочем такая техника расширения функционала класса особо не рекомендуется к использованию, поэтому используйте ее к месту. Заметим также что выбранный элемент FCurrentItem содержит такие свойства как ColCount & RowCount, ColHeaders & RowHeaders. При этом первые два целочисленные, вторые же предоставляют доступ по индексу, скрывая от нас коллекции THeaderCollection - не надо показывать наружу то, что там не требуется. Свойства с индексированным доступом определяются следующим образом:
        property ColHeaders[i : integer] : string read getHeader;
где функция getHeader имеет параметр - i и возвращает искомую строку. Вспомним не так часто используемый вариант описания свойств, который может быть удобен - указание индекса свойства.
property PropName[i : integer] : ReturnType index X read getMetod write SetMethod
При такой записи методы get & set должны иметь дополнительный параметр - индекс. В действительности, зачем нам для ColCount & rowCount писать два отдельных метода получения данных - объединим их в одном, воспользовавшись индексом свойства. При этом также мы можем поступить и с самими значениями ColHeaders & RowHeaders. А для того чтобы использовать одинаковую "нотацию" давайте добавим вложенный тип данных, описывающий к кому мы обращаемся - колонке или столбцу. Теперь я приведу часть класаа TCustomDataItem отвечающую за заголовки строк и столбцов:
 TCustomDataItem = class(TObject)
      strict private
        type
            THeaderType = (htColumn, htRow);
        function getHeader(i : integer; cr : THeaderType): string;
        function getHeaderCount(cr : THeaderType) : integer;
      strict protected
        FColHeaders : THeaderCollection;
        FRowHeaders : THeaderCollection;

        procedure InitHeaders(); virtual; abstract;
      public
        property ColHeaders[i : integer] : string index 0 read getHeader;
        property RowHeaders[i : integer] : string index 1 read getHeader;
        property ColCount : integer index 0 read getHeaderCount;
        property RowCount : integer index 1 read getHeaderCount;
    end;
На что стоит обратить еще раз внимание:
  1. Используем для внутренних нужд вложенный тип данных. В связи с этим он объявлен в секции private
  2. Использование свойств с индексом
  3. метод создания коллекций заголовков InitHeaders будет реализован в конечных классах, поэтому он объявлен в секции protected как abstract-ный метод. Коллекции FColHeaders & FRowHeaders также в секции Protected, чтобы конечные классы имели к ним доступ.
Теперь как же выглядят методы getHeader & getHeaderCount ? Заметьте, параметр - индекс свойства в самом описании свойства указывается как число, но в описании метода уже описан как THeaderType.
function TCustomDataItem.getHeader(i: integer; cr: THeaderType): string;
begin
    case cr of
        htColumn : result := FColHeaders[i].title;
        htRow    : result := FRowHeaders[i].title;
    end;
end;
Мы не перехватываем возможное исключение при выходе за рамки коллекции, нам это и не требуется. Хотя если мы вводим свои собственные классы исключений, то это как раз то место, где стоит это сделать. Можете заметить также, чтоб мы могли бы не использовать два отдельных члена класса FColHeaders & FRowHeaders, а определить массив по числу элементов множества THeaderType:
FHeaders : array[THeaderType] of THeaderCollection
Но этот прием не очень целесообразен в данном случае, однако помните, такой ход возможен, и часто бывает очень удобен.

небольшой итог

мы вспомнили как можно применить такие инструменты как классы-помощники, вспомогательные классы с классовыми функциями, вложенные типы данных, индексированные свойства. Вспомнили также и о том, что инкапсуляция это не только доступ к членам класса через методы, но и сокрытие лишних сведений (Col/RowHeaders[i]), а члены класса надо размещать ровно в той области видимости, в которой это необходимо. Содержание:
  1. Практика #1. Постановка задачи.
  2. Практика #2. Настройка таблицы.
  3. Практика #3. Абстракция
  4. Практика #4. Обобщения и кэширование.
  5. Практика #5. Конечная реализация
Метки:  ООП 

Комментарии

Андрей
12.05.2011 в 16:33
Спасибо!

Тема мне очень интересна, только не получается это все реализовать в форме. Возможно я слишком начинающий :) Нельзя ли выложить рабочий код? Непонятно, что есть Birthrate и что есть TCustomDataItem и т.д. если Вы пишете, чтобы помочь другим, неплохо было бы дать им возможность почуствовать как это работает.

Спасибо за труд.
ter
12.05.2011 в 17:11
TCustomDataItem это тема как раз следующей статьи (: - базовый класс для представления "элемента" данных для редактирования.
а TBirthrateDataItem это конечный класс (рождаемость), он появится наверное в самом конце серии статей (:
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно