Что такое грид и когда его использовать?
Грид - это набор объектов ФоксПро, позволяющий представлять данные в виде таблицы-списка, который можно просматривать в двух направлениях при помощи полос прокрутки. Грид состоит из главного объекта грида и набора колонок. Каждая колонка, в свою очередь, должна иметь объект заголовка и контрол, который позволяет редактировать и отображать данные в колонке грида (по умолчанию это TextBox). Грид выглядит как прямоугольная область с линейной решеткой, заголовками вверху каждой колонки, полосами прокрутки и некоторыми другими полезными вещами, такими как метки записей, метки удаления записи, полоска для разделения грида на две части.
Но на самом деле, отображение данных не настолько свободно, как, например, в таблице Excel. Грид требует набор данных (псевдоним), чтобы отобразить что-то. В пределах одной колонки сложно организовать отображение различных типов данных для различных строк. Грид имеет одинаковый размер всех строк, и колонка имеет одинаковую ширину для всех строк. Существуют также некоторые ограничения, которые могут выглядеть странно, пока не вспомнить о том, что грид на самом деле является контролом, построенным на коде комманды BROWSE, поддерживаемой из более ранних версий ФоксПро. Это может ответить на много вопросов по поводу странного поведения грида и его свойств. Несмотря на все хлопоты с ним связанные, грид - весьма полезный контрол и очень популярен среди программистов ФоксПро, потому существует также много обходных путей и решений для гридов, которые позволяют пробиться сквозь ограничения и создавать действительно замечательные вещи. Ограничения, связанные со странностями грида, не должны быть ключевым аспектом в принятии решения об использовании грида в приложении.
Грид полезен для отображения (просмотра) данных в компактной форме (большое количество данных на одну страницу). Грид хорош для выпадающего списка или для отдельной страницы формы - списка для поиска и навигации по данным. Не очень хорошо использовать грид как основное средство навигации по данным, т.к. грид занимает большое место на форме. Но часто грид является основным средством навигации и управления по данным на самой основной (начальной) форме - на грид налагается большое количество функциональности для вызова других форм для редактирования данных.
Грид полезен для любых регулярных данных только для чтения. Плохая идея использовать грид для редактирования данных. Редактирование данных гридом хорошо использовать и легко организовать только в приложениях административного типа. Редактирование данных при помощи грида имеет много проблем, если пытаться пробиться сквозь ограничения и странности грида в ФоксПро. Но, несмотря на это, такие формы данных, как редактирование списка элементов накладной или ордера, все-таки более естественны для пользователя, если они представлены в виде списка-таблицы (грида) для редактирования. Такие случаи - исключение, но не правило, не ставьте гриды на каждой форме только потому что очень просто редактировать данные в них. Если использовать гриды повсюду, то бысто обнаружите себя в проблемах, т.к. грид - довольно сложное средство, которое может быстро выйти из-под контроля. Надо будет после этого потратить много усилий, чтобы решить проблему, найти решение и потом далее исравлять проблемы, следующие за ними. Обычно данные редактируются при помощи формы с набором контролов для каждого поля данных и набора средств для навигации по данным, записи/отмены изменения и некоторых других средств специфических для конкретной формы. Организация такой формы при помощи грида - это прямой путь потратить дополнительное время. Конечно, если у вас есть время и вам нравится поиграть со странностями грида (кто кого?), грид - прекрасный контрол для проведения времени на поиск и изобретение решений и обходных путей.
Автоматическая перепривязака колонок грида
Одна из странностей грида, которая проявляется довольно часто, это автоматическая перепривязка колонок грида к полям набора данных. Можно обнаружить, что грид отображает не те данные, которые вы ему указали через свойство ControlSource в колонках. В дополнение, данные в колонках отображены в порядке физического размещения полей в наборе данных, несмотря на то, что начальный порядок следования колонок был задан иным. Почему так?
- Потому что свойство RecordSource грида изменилось во время дизайна. После этого свойство ControlSource всех колонок очищается, что часто упускается программистом. После запуска формы такой грид сам назначит поля колонкам. Для такого случая делайте резервную копию всех значений этого свойства, в основном используют свойство Comment колонки.
- Потому что свойство RecordSource грида изменилось во время выполнения. Если это не порсто ошибочное повторное изменение, то надо сохранять значения ControlSource для всех колонок, а после присвоения набора данных - восстанавливать.
- Возможно что грид был перестроен, об этом - в следующей части этой статьи.
Это все не так досадно, как побочные эффекты после такого поведения грида. В основном, нет никакой разницы, пока грид не имеет сложной функциональности и построения. Например, если значения колонки отображались при помощи выражения - выражение будет утрачено, и только поле будет отображаться Например, вместо значения PADR(mMyMemoField,200) поле покажет "Memo" в колонке грида). В гриде появляются нежелательные поля, такие как поле главного ключа таблицы, про которое пользователь не имеет никакого представления. Но самый опасный случай, когда в гриде есть некоторые необычные контролы. Простой пример - в гриде есть CheckBox, и грид решил привязать колонку с ним с полю символьного типа, ФоксПро выдает ошибку несовпадения типов при первом же обновлении изображения грида: 'Type is not supported by this control'.
Несмотря на все это, все-таки бывают ситуации, когда набор данных грида надо изменять в любом случае, например, чтобы избежать перестройки грида. Ниже приведены примеры кода, как сохранить значения ControlSource всех колонок грида при помощи свойства Comment колонки, и как восстановить их.
&& сохранить ControlSource каждой колонки with {grid} local nColumnIndex for m.nColumnIndex = 1 to .ColumnCount .Columns(m.nColumnIndex).Comment = .Columns(m.nColumnIndex).ControlSource endfor endwith && восстановить ControlSource каждой колонки with {grid} local nColumnIndex for m.nColumnIndex = 1 to .ColumnCount if !empty(.Columns(m.nColumnIndex).Comment) .Columns(m.nColumnIndex).ControlSource = .Columns(m.nColumnIndex).Comment endif endfor endwith
Здесь {grid} это ссылка на объект грида.
Хорошая идея сделать эти куски кода методами класса грида в Вашем приложении. Так же, хорошо вызывать восстановление значений ControlSource в событии Init грида на случай, если программист случайно изменил или спровоцировал изменение (просто нажал Enter на свойстве RecordSource в редакторе свойств) во время дизайна (для этого надо в дизайне делать копию значений).
Для этого можно выполнить следующий код (идея принадлежит Cetin Basoz из UniversalThread):
******************************************************************** * Description.......: GridBuilder - allows to build a grid in design-time * Calling Samples...: * Parameter List....: * Created by........: Cetin Basoz * Modified by.......: ******************************************************************** *custom builder not registered aselobj(arrObj) for each oObj in arrObj if upper(oObj.baseclass)='GRID' for each oColumn in oObj.columns oColumn.Comment = oColumn.ControlSource next endif next
Важное примечание: не делайте никакого обновления любого видимого объекта грида до того, как все значения ControlSource будут восстановленны. Иначе может выдаваться сообщение типа 'Type is not supported by control', как описано выше.
Автоматическая перестройка грида
Когда-нибудь случалось, что ваш грид не хочет вести себя так, как вы ему указали во время дизайна? Контролы утрачены в колонках после запуска? Код событий в колонках, заголовках или контролах не запускается? Перестройка грида - это полное уничтожение всех колонок и контролов в гриде и создание их заново, используя стандартные контролы ФоксПро со всеми значениями свойств по умолчанию. Это утрата всех значений, свойств, кода методов и событий контролов в колонках. Свойство CurrentControl принимает значение "Text1" - контрол TextBox в колонке по умолчанию. Заголовки утрачены и приняли значения названий полей набора данных. Как правило, это катастрофа, когда не понимаешь, что случилось с гридом.
Перестройка случается в нескольких случаях, которые описанны ниже с соответствующими решениями для них.
1. Грид перестраивает себя всегда, если набор данных для этого грида, указанный в RecordSource, закрывается. Если это -представление (View), то обычно перестройка не возникает по команде Requery(). Если это запрос, то переприсвоение другого запроса или закрытие набора данных -результата предыдущего запроса, тоже запускает перестройку. Также перестройка возникает, если было использовано SQL Pass-Through (SPT) функции для выборки в набор данных для грида, и этот запрос запущен заново для выборки в тот же набор данных (т.е. перечитывание данных через SPT запускает реконструкцию, тогда как для представления requery() не запускает ее).
Чтоб избежать реконструкции, когда обновляется набор данных грида, до того, как делать любые действия, описанные выше, надо присвоить пустую строку свойству RecordSource грида (но только не строку с одним пропуском - " ", а именно пустую - ""). Проверяйте Ваш код аккуратно, чтоб запрограммировать это в правильном порядке действий с гридом, и ничто другое не испортит его. (Строку кода, запускающую перестройку грида, можно поймать, поставив SET STEP ON в событии BeforeRowColChange грида.) После того, как набор данных для грида подготовлен заново, надо присвоить название (Alias) набора данных или запрос гриду заново. Перестройка при таком способе не возникает, но происходит автоматическая перепривязка колонок грида после присвоения набора данных.
Пример кода:
* сохранить ControlSource каждой колонки ........... {grid}.RecordSource = "" * изменения в наборах данных для грида, закрывание/открывание заново или SPT запрос ........... * Восстановить набор данных для грида {grid}.RecordSource = "{НазваниеНабораДанных}" * restore the control sources of columns here ...........
Здесь {grid} - это ссылка на объект грида,
{НазваниеНабораДанных} - это псевдоним (Alias) набора данных или текст запроса. В наиболее общем случае, как набор данных для грида псевдоним можно выбрать из значений свойства ControlSource колонок, т.к. ФоксПро всегда подставляет автоматически псевдоним к названию поля в колонке, приводя значение к формату "псевдоним.поле" несмотря на то, что во время дизайна программист указал только поле.
Все замечания относительно автоматической перепривязки колонок к полям относятся и к этому случаю.
Существует и другой способ отменить перестройку грида - при помощи события BeforeRowColChange в гриде. Это событие запускается всегда перед тем, как грид перестроится. Оно запускается в любом случае: когда набор данных грида закрывается, SPT-запрос заново выбирает данные в набор данных грида; несмотря на то, что грид невидим, неактивен и независимо от настроек грида. Самое чудесное то, что комманда NODEFAULT, используемая в этом событии на протяжении всего времени обновления набора данных, отменяет перестройку грида вообще, но следует пользоваться этим очень аккуратно, т.к. могут возникать нежелательные побочные эффекты.
Вот пример кода для обновления набора данных грида таким способом:
thisform.GridRefreshing = .T. && сообщить, что данные будут обновляться ... выполнить обновление данных thisform.Grid.RecordSource = thisform.Grid.RecordSource thisform.Refresh && или grid.refresh DOEVENTS && если нужно - просто протестируйте без этого && после этого момента грид перестает запускать перестройку thisform.GridRefreshing = .F.
В событии BeforeRowColChange грида такой код:
if PEMStatus(thisform,"GridRefreshing",5) AND thisform.GridRefreshing nodefault && отменить перестройку на протяжении обновления данных return endif
Можно поставить этот код в класс грида для организации этой функциональности, тогда свойство GridRefreshing можно перенести в грид.
Иногда после такого способа нужно поставить фокус за пределы грида и обратно, так как текущая клетка грида может показывать звездочки ('*******').
К сожалению, невозможно определить причину запуска события BeforeRowColChange, чтоб различить, когда запускается перестройка грида, и когда это событие запускается перед сменой текущей клетки в гриде или получения фокуса гридом. Просто используйте свойство-флажок для этого, как в примере. Конечно, если у вас есть время, можно организовать класс грида с покрывающей его прозрачной прямоугольной областью, чтоб выловить все события мышки до того, как событие грида BeforeRowColChange запустится. Это также требует ловить события клавиатуры в событии KeyPress формы при активном гриде (свойство KeyPreview формы должно быть .T. для этого), и надо заключить грид в объект контэйнера, чтоб поймать момент получения фокуса гридом.
Оба подхода имеют существенный недостаток: необходимы дополнительные строки кода для грида во всех местах в коде, которые могут вызвать перестройку. Если эти места находятся во многих формах и классах, то весьма трудно их все обнаружить; и в дополнение необходимо во всех этих местах кода иметь ссылку на грид. Также, при динамическом обновлении данных грида при помощи представления, при переходе на использование удаленного доступа к БД проблемы будут при использовании функций SQL Pass-Through, поскольку обновление данных ими запускает перестройку (тогда как представления - нет, но представления для удаленного доступа (Remote View) не всегда подходят). Проблемы возникают в основном из-за того, что программист, не зная об перестройке грида или не предусматривая ее, использует для представления комманду requery() в разных местах кода и оставляет их так разбросанными не думая, что это плохо. Когда осуществляется перевод кода на работу через SQL Pass-Through, надо искать все эти места, и, в дополнение, добавлять код для отмены перестройки грида при необходимости. Если это в других классах или формах, то это может стать большой затратой времени. Совет: разместите обновление данных, открывание и закрывание наборов данных и код всех других действий с данными в одном месте - методах формы или объекта доступного нескольким формам. Всегда предполагайте, что некоторый код работы с данными может в будущем требовать дополнительные строки кода, даже если это просто вызов комманды requery(). В будущем это окупится - надо будет вносить изменения только в одном месте вместо поиска всюду, где только можно, в случае таких изменений. Перестройка грида - один из таких случаев. Например, создайте класс грида, который также будет содержать методы для работы с данными, отображаемыми гридом. При помощи ссылки на объект такого грида обновление данных будет вызываться одним вызовом метода объекта во всех формах, где нужно обновление этих данных. В случае изменений в коде надо будет вносить изменения только в одном месте - этом методе грида. Ну ладно, скорее всего, вы не будете строить приложений, которые потребуют таких сложностей...
Будучи разочарованы поведением грида, программисты также часто создают временный набор данных (курсор) и используют его как набор данных для грида, потом обновляют данные в нем путем удаления старых записей и копирования новых. Если такой курсор уже есть в программе, то обновлять его содержимое таким способом несложно. Но есть небольшая проблема - вертикальная полоса прокрутки показывает общее количество записей, которое намного больше, чем реально отображаемое (из-за множества записей помеченных на удаление).
2. Перестройка грида запускается также когда грид инициализируется, но свойство RecordSource пустое или набор данных для грида не существует (еще не открыт или запрос не запускался). В таком случае грид перестраивает себя, используя текущий псевдоним как набор данных, если таковой открыт в текущей рабочей области (или остается пустым, если не открыт, но все колонки уничтожаются все равно). Это случается часто: если надо открыть набор данных в зависимости от параметров позже чем в событии Load формы, то используйте следующую технику.
В событии Load создайте пустой курсор при помощи комманды CREATE CURSOR, используя ту же структуру, что и для набора данных грида под тем же именем, набор данных грида должен быть указан на этот курсор (на это имя). Во время открывания реальных данных грид уже может быть инициализирован, потому используйте решения из п.1. для того, чтоб закрыть временный курсор и открыть реальные данные. Другое решение - поставить объект на форму, событие Init которого запускается до того как событие Init грида.
Второй подход - добавлять грид к форме на лету. Создайте класс грида со всеми необходимыми настройками и колонками и не кладите его на форму во время дизайна. В коде Init формы используйте AddObject() или NewObject(), чтоб добавить его к форме или контэйнеру на форме после того, как набор данных для грида приготовлен.
3. Грид перестраивается, если установить количество колонок в 0 или -1. Я надеюсь, Вы никогда этого не делаете, не правда ли? 😉 Все-таки иногда это может быть использовано в простых административных формах, которые позволяют открыть любую таблицу для ее просмотра в гриде. Но из-за перестройки такой грид может иметь ограниченную функциональность, или вся функциональность в таком случае должна быть размещена в классах, которые используются после перестройки способом добавления их к гриду и замещения объектов по умолчанию. К сожалению, не существует другого способа задать свои классы и настройки для колонок, которые создаются ФоксПро автоматичски.
4. Грид перестраивается, когда набор данных выходит за пределы видимости грида. Это часто случается, когда набор данных открыт в одной сессии данных, а грид реально инициализирован в другой сессии данных, потому когда грид пробует обновить свое изображение, другая сессия данных является текущей, в которой этот набор данных не существует. Это может также возникнуть и в других ситуациях при активном переключении программистом сессий данных.
Перестройка грида не может быть отменена путем создания и хранения ссылок на каждую колонку грида (объект не уничтожается, если на него существует ссылка где-то в переменной) - грид просто отторгает объекты колонок от себя и очищает много свойств. Создание класса грида уже с колонками и его использование также не предотвращает перестройку (класс с объектами не позволяет удалять объекты в нем там, где этот класс используется как объект, и грид не исключение, но перестройка грида - исключение).
Также существует популярный обход перестройки грида вообще - создание грида программным образом динамически и пересоздание его после обновления данных. Создайте класс грида со всеми настройками. Программным образом удаляйте его с формы перед обновлением данных и добавляйте заново после обновления. Это требует некоторых проверок в таком коде, программного присвоения значений некоторым свойствам грида и т.п. Этот способ плох тем, что он требует создания своего класса грида для каждого частного случая, знать название этого класса, создавать программу для добавления каждого грида - много кода и объектов программирования, которых можно избежать.
Можно также создавать полностью грид и все его колонки, объекты и настройки при помощи одного только кода. После перестройки нужно запускать этот код каждый раз заново. Этот подход хорош только в случае, если перестройка нужна, например, если колонки грида динамично добавляются/убираются (нельзя создать класс грида) или для административных целей - для просмотра любой таблицы в гриде и в то же время иметь некоторую функциональность, такую как редактирование memo-полей в контроле EditBox, сортировка по нажатию на заголовок и т.п.
Блокировка (замораживание) колонки, чтоб она была всегда видима, несмотря на прокрутку
Если надо заблокировать (заморозить) некоторую колонку или колонки грида, чтоб они были всегда видны слева даже после горизонтальной прокрутки, первая мысль приходит - это разделить грид на две части: левая показывает первые колонки, которые нам нужно, а правая используется для прокрутки. Но для пользователя это выглядит странно и требует объяснений: эти все лишние полосы прокрутки, слева часто больше, чем одна колонка и т.п. ...
Грид имеет хорошое свойство "LeftColumn", содержащее порядковый номер колонки, которая сейчас отображена как самая левая колонка среди отображаемых колонок грида при текущей горизонтальной прокрутке. Это значение изменяется при прокрутке. Можно его использовать, чтобы переместить нужную нам колонку на место левой отображаемой позиции, в событии Scrolled грида, заблокировав эту колонку таким образом:
if nDirection>3 this.Columns(1).ColumnOrder = this.LeftColumn endif И, для автоматической прокрутки по перемещению текущей клетки грида, в событии AfterRowColChange добавьте строку: this.Columns(1).ColumnOrder = this.LeftColumn
Теперь первая колонка грида всегда отображается первой!
Советы для грида
- -При использовании запроса как набора данных для грида, всегда добавляйте "INTO CURSOR :" в конце строки запроса, иначе будет появляться окно Browse для отображения результата запроса при инициализации формы или в момент когда этот запрос выполнится.
- - Колонка грида позволяет использовать выражение в качестве ControlSource! Колонка должна быть только для чтения (ReadOnly = .T.), результат выражения должен быть одного типа и постоянной длины для всех колонок, иначе могут возникнуть всяческие странности, звездочки и другие эффекты вплоть до фатальных ошибок в самом ФоксПро. Если все же надо отображать данные разных типов в одной колонке, используйте контэйнер или несколько контролов в одной колонке.
- - Чтоб добавить во время дизайна новый контрол в колонку грида, выберите колонку в редакторе свойств, выберите окно редактирования класса или формы путем нажатия мышкой на заголовке окна, выберите нужный класс в панели классов и нажмите мышкой на гриде.
- - Чтоб удалить контрол из колонки грида, выберите этот контрол в редакторе свойств, выберите окно редактирования класса или формы путем нажатия мышкой на заголовке окна, затем нажмите клавишу "Del".
- - Если настроить грид не показывать горизонтальные линии решетки грида, серые или другие линии на их месте при HighlightRow=.F. это то, что находится прямо под гридом (в основном это поверхность формы, потому линии в основном серые). Чтоб избавиться от такого "мусора", положите под грид прямоугольную область, закрашенную в цвет колонок грида.
- - Можно изменить изображение курсора мышки для колонки грида путем установки нужных свойств в событии MouseMove колонки.
Предупреждения для грида
Эти строки дают некоторые предостережения для программистов, которые используют гриды...
- - Помните, что изменение набора данных для грида в режиме дизайна требует также запоминание значений ControlSource для всех колонок. После изменения свойства RecordSource они все очищаются! Обычно копия делается в свойство Comment колонки.
- - Не оставляйте свойство RecordSource грида пустым до инициализации грида.
- - События и свойства объекта внутри колонки грида используются только для текущей клетки грида, если свойство Sparse колонки равно .T. Если это свойство равно .F., только свойства используются для отображения строк в колонке, тогда как события по-прежнему работают только для текущей клетки.
- - Событие Scrolled запускается только при прокрутке с помощью полос прокрутки. Если грид прокручивается при помощи клавиатуры, программным образом или в других случаях, то это событие не запускается. Прокрутку таким образом надо ловить в событии AfterRowColChange, DoScroll и тестировать много (есть случаи, когда грид прокручивается и при других действиях без изменения текущей клетки, например, в некоторых случаях при работе с заголовком колонки).
- - Свойства RelativeRow и ActiveRow принимают корректные значения только, если грид активен. ActiveRow равен нулю, если текущая запись выходит за пределы области отображения грида.
VFP гриды, следующие части
Следующие статьи будут обсуждать модель событий грида, что такое покрывающая прозрачная область для грида и как ее использовать, сила свойств Dynamic* колонки, точное позиционирование объекта над клеткой грида, советы для колонок и заголовков грида, сортировка грида, индикатор сортировки и многое другое.
перевод Влад Гринчишин