Шпаргалка по DevExpress cxGrid и Delphi 7

При выборке большого количества записей можно нарваться на ошибку недостатка памяти, хотя сама cxGrid с легкостью обрабатывает в несколько раз больше записей даже на 32-битной версии, достаточно лишь заменить стандартный менеджер памяти Delphi на «Fast Memory Manager», в моем случае FastMM4 отлично помог расширить возможности, увеличив предельную выборку в 5 раз.

Некоторые приемы работы с компонентом cxGrid, собранные из разных источников и полезные лично мне. (Обновлено 12.12.2022)

Внешний вид

Программную раскраску ячеек можно делать в событиях OnCustomDrawCell

if (AViewInfo.GridRecord.Values[TableViewColumn1.Index])=1 then begin
	ACanvas.Brush.Color := clRed;
	ACanvas.Font.Style := [fsBold];
end;

Если требуется закрасить только отдельные колонки (а не всю строку), то раскрашиваемую колонку в обработчике OnCustomDrawCell можно определить так:

if TableView.Columns[AViewInfo.Item.Index].DataBinding.FieldName='COLUMN_1' then
или
if AViewInfo.Item = TableViewCOLUMN_1 then

Единственно нужно учитывать что подобная раскраска не выводится при экспорте в Excel а также бывает неудачно выбирается цвет выделения. Для этого нужно красить исключительно присваивая заранее определенный “стиль” в обработчиках типа “TableViewStylesGet***Style”:

procedure TMoneyMoveDetailForm.TableViewStylesGetContentStyle(Sender: TcxCustomGridTableView; ARecord: TcxCustomGridRecord; AItem: TcxCustomGridTableItem; out AStyle: TcxStyle);
begin
	if not ARecord.IsData then Exit;
	// (AItem as TcxGridDBBandedColumn) если нужно понять в какой колонке мы находимся
	if (ARecord.Values[TableViewID_MONEY.Index])=null then begin
		AStyle :=cxStyleBold;
	end;
end;

Вывод изображения или иконки в ячейке

procedure TMsgForm.TableViewIMAGECustomDrawCell(Sender: TcxCustomGridTableView; ACanvas: TcxCanvas; AViewInfo: TcxGridTableDataCellViewInfo; var ADone: Boolean);
Var
	r : TRect;
	iImageIndex : Integer;
begin
	If (AViewInfo.GridRecord.Values[TableViewIMAGE.Index] = 1) then begin
		R := AViewInfo.Bounds;
		ACanvas.Brush.Color := AViewInfo.Params.Color;
		ACanvas.FillRect(R);

		// draw the image.
		R := AViewInfo.Bounds;
		Inc(r.Top,1);
		ACanvas.DrawImage(ImageList,r.Left,r.Top,20,True);
		ADone := True ;
	end;
end;

Но можно сделать и так:

procedure TFilesForm.TableViewIconCustomDrawCell(
Sender: TcxCustomGridTableView; ACanvas: TcxCanvas;
AViewInfo: TcxGridTableDataCellViewInfo; var ADone: Boolean);
begin
    inherited;
    with AViewInfo.ClientBounds do
        cxImageList.Draw(ACanvas.Canvas, Left + 1, Top + 1, 0);
    ADone := True;
end;

Есть еще вариант — установить в Properties ячейки компонент ButtonEdit, в котором можно добавить одну или более кнопок, а если навесить на кнопку Action то она станет еще и кликабельна. Правда только при условии включенного редактирования. Недостаток метода — ячейка получается только текст.

Раскраска разных уровней группировки разным цветом

procedure TPozSkladForm.TableViewStylesGetGroupStyle( Sender: TcxGridTableView; ARecord: TcxCustomGridRecord; ALevel: Integer; out AStyle: TcxStyle);
begin
	if ALevel=0 then AStyle := cxStyleL0;  // $00A0A0A0
	if ALevel=1 then AStyle := cxStyleL1;  // clSilver
	if ALevel=2 then AStyle := cxStyleL2;  // $00E0E0E0
end;

Ощутимый минус раскраски через стили (Style) в том что курсор перекрывает раскраску и ее не видно. Обойти это можно если в обработчике TableViewCustomDrawCell например делать выделение строки самому (для режима CellSelect=True):

  if AViewInfo.GridRecord.Selected then begin
    if AViewInfo.Selected then begin // под инверсным курсором сделаем белый шрифт
      ACanvas.Font.Color := clWhite;
    end else begin // остальную строку выделим синим шрифтом, чтобы не сбивать фоновую раскраску ячеек
      ACanvas.Font.Color := clBlue;
      ACanvas.Font.Style := [fsBold];
    end;
  end;

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

Отображение данных (фильтры, группировки, сортировки)

В TcxGrid скрыть группировку GroupByBox и убрать область над гридом в которой написано «drag a column header here to group by ..»

TableView.OptionsView.GroupByBox := false;

Программно задать фильтр

TableView.DataController.Filter.BeginUpdate;
TableView.DataController.Filter.Root.Clear;
TableView.DataController.Filter.Root.AddItem(TableViewColumn1, cxFilter.foLike, BegString+"%", BegString+"%");
TableView.DataController.Filter.Active:=true;
TableView.DataController.Filter.EndUpdate

Причем если добавляется более одного условия, то перед добавлением очередного желательно указать булевый тип (по умолчанию будет AND), но можно сделать OR — TableView.DataController.Filter.Root.BoolOperatorKind := fboOr;

Сохранение позиции курсора в гриде после обновления

TableView.BeginUpdate();
r := TableView.Controller.TopRowIndex;
f := TableView.Controller.FocusedRowIndex;
GridQuery.Close; // тут открываем-закрываем грид
GridQuery.Open;
TableView.Controller.TopRowIndex := r;
TableView.Controller.FocusedRowIndex := f;
TableView.EndUpdate;

Программно развернуть или свернуть группы

TableView.DataController.Groups.FullCollapse; // Свернуть
TableView.DataController.Groups.FullExpand; // Развернуть
TableView.DataController.Options := TableView.DataController.Options + [dcoGroupsAlwaysExpanded]; // Закрепить развернутое состояние

Программно задать или убрать группировку

TableViewColumn1.GroupIndex := 1; // задать группировку по колонке
TableView.DataController.Groups.ClearGrouping; // Убрать группировку

Доступ к данным и метаданным грида

 пробежаться в цикле по всем видимым строкам:

for I:=0 to cxGrid.DataController.FilteredRecordCount - 1 do begin
cxGrid.DataController.Values[cxGrid.DataController.FilteredRecordIndex[i],YourColumnName.Index])

Цикл по всем выделенным строкам с получением идентификатора каждой

for i := 0 to GridView1.Controller.SelectedRecordCount-1 do begin
ID_RASHODD := GridView1.DataController.Values[GridView1.Controller.SelectedRecords[i].RecordIndex, GridView1ID_RASHODD.Index];
end;

Получить колонку грида по ее имени

TableView1.GetColumnByFieldName('FIELD_NAME')

Для таблиц типа TcxGridTableView или TcxGridBandedTableView (без «DB») данные можно добавлять и удалять вручную. Делать это можно например так:

// Удаление всех строк из таблицы
while TableView.DataController.RowCount>0 do
  TableView.DataController.DeleteRecord(0);

// Добавление строки данных и заполнение ячеек значениями согласно их типу
r := TableView.DataController.AppendRecord;
TableView.DataController.SetValue(r, TableViewColumn1.Index, 'test1');
TableView.DataController.SetValue(r, TableViewColumn2.Index, 'test2');
TableView.DataController.SetValue(r, TableViewColumn3.Index, 12345);

 Редактирование данных пользователем

Организация списка ComboBox в ячейке:

в Properties ставим  ComboBox, заполняем список при событиях AfterScroll дата-сета, если это пытаться делать в onInitPopUp то список вываливается предыдущий (в комментариях подсказали что я делал не правильно). Пример заполнения:

TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;
while not LookupQuery.Eof do begin
	TcxComboBoxProperties(TableViewCNAME.Properties).Items.Add(LookupQuery['CNAME']);
	LookupQuery.Next;
end;

выбор пользователя проверяется и используется в методе OnValidate примерно так:

if LookupQuery.Locate('CNAME',DisplayValue,[]) then begin
	// тут заносим в базу
	TableView.BeginUpdate;
	// тут обновляем таблицу, если нужно
	TableView.EndUpdate;
end else begin
	ErrorText := 'Ошибка выбора';
	Error := True;
end;
TcxComboBoxProperties(TableViewCNAME.Properties).Items.Clear;

Редактирование числа в гриде:

в Properties ставим CalcEdit а в обработчике OnEditValueChanged юзаем (Sender as TcxCalcEdit).Value и текущее положение в дата-сете

Отловить правый клик на определенной колонке:

в событии OnCellClick:

if ACellViewInfo.Item.Name='DzTableViewNRM_NAME' then
if AButton=mbRight then

Сделать функцию копирования ячейки в буфер обмена

Добыть значение из текущей ячейки можно так := <TcxGridDBTableView>.Controller.FocusedRow.Values[FocusedColumn.Index] но если вдруг у вас в таблице есть колонки с LookupComboBox то вместо значения которые вы видите глазами у вас будет ключ. Поэтому функция добычи значения из ячейки должна это учесть и использовать .GetDisplayLookupText, которая скрыта и для доступа нужно унаследовать класс TcxLookupComboBoxProperties у себя в объявлениях:

type
  TcxLookupComboBoxPropertiesAccess = class(TcxLookupComboBoxProperties); // для доступа к .GetDisplayLookupText

// далее в коде

with <TcxGridDBTableView>.Controller do begin
  if FocusedColumn.Properties is TcxLookupComboboxProperties then begin
    Clipboard.AsText := (TcxLookupComboBoxPropertiesAccess(FocusedColumn.Properties).GetDisplayLookupText(FocusedRow.Values[FocusedColumn.Index]));
  end else begin
    Clipboard.AsText := FocusedRow.Values[FocusedColumn.Index];
  end;
end;

Определить где курсор по событию MouseUp/MouseDown:

procedure TPlanForm.TableView1MouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  AHitTest: TcxCustomGridHitTest;
begin
  inherited;
  AHitTest := (Sender as TcxGridSite).GridView.ViewInfo.GetHitTest(X,Y); // Может быть одно из // TcxGridColumnHeaderHitTest, TcxGridRecordCellHitTest, TcxGridGroupByBoxHitTest
  if AHitTest is TcxGridColumnHeaderHitTest then begin
    if TcxGridColumnHeaderHitTest(AHitTest).Column = TableView1COLUMN1 then begin
      // Определили что кликнули по заголовку первой колонки (но ставить здесь сортировку плохая идея, если что)
    end;
  end;
end;

Обработка отметки чек-бокса в ячейке. В событии OnChange:

if (Sender as TcxCheckBox).Checked then

Узнать в какой колонке происходит редактирование:

TableView.VisibleColumns[TableView.Controller.FocusedColumnIndex].DataBinding.FieldName

Изменение соседних ячеек при обработке редактирования в  OnEditValueChanged:

TableView.DataController.<strong>SetEditValue</strong>(TableViewFIELD_NAME.Index, new_Value, evsValue );

надо сказать этот способ не всегда работает почему-то, но можно и по другому:

TableView1.DataController.DataSource.DataSet.Edit;
TableView1.DataController.DataSource.DataSet.FieldByName('FIELD_NAME').Value := new_Value;
TableView1.DataController.DataSource.DataSet.Post;

Использование drag&drop для перетаскивания на строки таблицы

procedure TForm1.cxGrid2DBTableView1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
// Начало перетаскивания из таблицы-источника (впрочем можно просто включить Auto)
    if ((Button = mbLeft) and (ssAlt in Shift)) or (ssRight in Shift) then
        if (TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y).HitTestCode in [htCell, htRecord])
            then TcxGridSite(Sender).BeginDrag(False);
end;

var
  ARecordIndex: Integer;
procedure TForm1.cxGrid2DBTableView1StartDrag(Sender: TObject; var DragObject: TDragObject);
var
  AGridView: TcxGridDBTableView;
begin
// Достаем ARecordIndex записи которую перетаскиваем
  AGridView := TcxGridSite(Sender).GridView as TcxGridDBTableView;
  ARecordIndex := AGridView.Controller.FocusedRecordIndex;
end;

procedure TForm1.cxGrid2DBTableView1DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean);
begin
// Разрешения приема

    Accept := Source is TcxDragControlObject;
end;

procedure TForm1.cxGrid2DBTableView1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
// Выведем текст из строки на которой сделали Drop
    self.Caption := TcxGridRecordCellHitTest(TcxGridSite(Sender).ViewInfo.GetHitTest(X, Y)).GridRecord.Values[cxGrid1DBTableView1FULLNAME.Index];
end;

 Изменение таблицы в рантайме

Добавление колонки в рантайме:

var
	XCol : TcxGridDBBandedColumn;
begin
	XCol := TableView.CreateColumn;
	XCol.Position.BandIndex := 1;
	XCol.DataBinding.FieldName:='quant'+sl[i];
	XCol.DataBinding.ValueType := 'Float';
	XCol.Caption := ss[i];
	XCol.Width := 50;
	XCol.Tag := StrToInt(sl[i]);
	XCol.Summary.FooterKind := skSum;
	XCol.Summary.GroupFooterKind := skSum;
	XCol.Summary.GroupKind := skSum;
	XCol.Summary.FooterFormat := '0.##';
	XCol.Summary.GroupFooterFormat := '0.##';
	XCol.Summary.GroupFormat := '0.##';

Удаление колонки в рантайме:

TableView.Columns[i].Destroy;

Добавление бандов (bands):

b0 := TableView.Bands.Add;
b0.Position.BandIndex := b.Index;

Задать произвольную формулу расчета FooterSummary

Задать произвольную формулу расчета FooterSummary ячейки с использованием других FooterSummary ячеек можно в событии DataControllerSummaryAfterSummary. В событии OnGetText самой ячейки это сделать не выйдет, так как на момент вызова Summary еще не рассчитаны.

procedure T*****Form.TableViewDataControllerSummaryAfterSummary(ASender: TcxDataSummary);
var
  itog_summa, kmsumma :Variant;
begin
  inherited;
  itog_summa := ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewITOG_SUMMA)];
  kmsumma := ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewKMSUMMA)];
  ASender.FooterSummaryValues[ASender.FooterSummaryItems.IndexOfItemLink(TableViewPERCENT)] := itog_summa + kmsumma;
end;

Что касается подобного расчета для GroupSummary, здесь придется рекурсивно пройтись по всем Level и на каждом просчитать нужную формулу для всех GroupSummary. Пример реализации.

Глюки

Да, у такого сложного компонента их не может не быть, и иногда с ними можно столкнуться. Например в некоторых случаях у меня в Footer при суммировании иногда терялись старшие разряды, вместо 14.8 например показывается 4.8, и не зависит от заданного формата. В таких случаях пришлось написать собственную подбивку суммы и вызывать ее в событии OnGetText на FooterSummary:

procedure T****Form.TableViewTcxGridDBDataControllerTcxDataSummaryFooterSummaryItems3GetText(
Sender: TcxDataSummaryItem; const AValue: Variant; AIsFooter: Boolean;
var AText: String);
  var
    s : Double;
    i : Integer;
begin
  s := 0;
  for i:=0 to TableView.DataController.FilteredRecordCount - 1 do begin
    try
    s := s + TableView.DataController.GetValue(TableView.DataController.FilteredRecordIndex[i], Sender.Field.Index);
    except
    end
  end;
  AText := FormatFloat(Sender.Format, s);
end;

TcxLookupComboBox

Показать подсказку в самом поле ввода, (.TextHint) Оказывается данное свойство у компонента есть, но оно скрыто. Для его открытия необходимо в объявлениях унаследовать класс:

type
  TcxLookupComboBoxAccess = class(TcxLookupComboBox); // для доступа к скрытому полю .TextHint

// А уже например в OnCreate выполнить присвоение подсказки:

TcxLookupComboBoxAccess(MyLookupCoboBox).TextHint := 'Текст подсказки';

Добавить свою дополнительную кнопку в поле ввода, например для функции открытия справочника или редактирования. Это также будет работать для всего перечня контролов, у которых есть Properties (Здесь список контролов)

with MycxLookupComboBox.Properties do begin
    Images := cxImageList;
    Result := Buttons.Add;
    Result.Default := True;
    Result.Kind := bkGlyph;
    Result.LeftAlignment := False;
    Result.Action := Action; // Это действие, которое будет выполняться
end;

2 комментария к “Шпаргалка по DevExpress cxGrid и Delphi 7”

  1. pashagoldenberg

    Спасибо вам за статью.
    Но есть замечание относительно пункта «Редактирование данных пользователем», подпункт «Организация списка ComboBox в ячейке». На самом деле в OnInitPopup тоже можно сделать выпадающий список и он будет актуальным, нужно просто обращаться не к столбцу, а к Sender’-у. Что-то типа этого:

    procedure TrdbXXX.YYYPropertiesInitPopup(
      Sender: TObject);
    begin
      if XQuery.Active then
      begin
        (Sender as TcxComboBox).Properties.Items.Clear;
        XQuery.First;
        while notXQuery.Eof do
        begin
          (Sender as TcxComboBox).Properties.Items.Add(XQuery['FieldName']);
          XQuery.Next;
        end;
      end;
    end;

    a7in: Спасибо за замечание!

  2. Отличная статья! У меня задача: заменить значения чисел на текстовые значения. Спасибо.

Оставьте комментарий

Ваш адрес email не будет опубликован.