Технология автоматического объединения ячеек

Ячейки некоторых таблиц иногда требуется объединять для большей наглядности и аккуратного внешнего вида. Рассмотрим, как это можно делать, на примере журнала посещений регистратуры.

Обратите внимание, только часть столбцов содержат объединённые по вертикали ячейки. Это столбцы "Время", "Клиент" и "Специалист". Объединяются ячейки, имеющие одинаковое содержимое. Ячейки в других столбцах объединять не нужно.

1. В начале функции, формирующей web-страницу журнала посещений, описываем структуру, содержащую значения объединяемых полей. Надо сказать, что в структуре присутствуют также данные столбца "Дата", но этот столбец обычно не используется, поэтому на скриншоте не отображён.

string s;
int iCycle;
struct ROW
{
	char szSort [1024]; // для сортировки

	int iVisitNum;
	int iVisit;
	int iClientNum;
	int iClient;

	// дата
	char sz1 [5 +1];
	int iSpan1;
	// время
	char sz2 [5 +1];
	int iSpan2;
	// ФИО клиента
	char sz3 [100];
	int iSpan3;
	// специалист
	int i4;
	int iSpan4;
};
ROW curr_row,next_row;
std::auto_ptr<RING> rows (new RING("fnAppReg(QUERY *): rows",1024*1024));
int iQty;
int iCycleRow;

2. В цикле производится выборка актуальных записей из базы данных посещений для отображения в журнале. Производится заполнение структуры curr_row и размещение этих данных в динамическом массиве rows с начальным размером 1 мегабайт.

// ***************************
// * поиск посещений по дате *
// ***************************
for (iCycle = 0; iCycle < iVisitsQty; iCycle++)
{
	if (visit [iCycle]->fnGetDelete())
		continue;
	if (visit [iCycle]->fnGetServiceType() == -4) // виртуальная услуга
		continue;
	if (visit [iCycle]->fnGetPlannedTreatmentOnly())
		continue;
	if (config.bRegShowFutureVisits) // показывать в журнале будущие посещения
	{
		if (fnCompareDate(visit [iCycle]->fnGetDate(),szDate) == -1)
			continue;
	}
	else
	{
		if (strcmp(visit [iCycle]->fnGetDate(),szDate))
			continue;
	}
	// фильтрация по специалисту
	if (iSpec != -1)
		if (visit [iCycle]->fnGetSpec() != iSpec)
			continue;
	// если запрещено просматривать чужие записи
	if (! config.bOtherPrefixView)
		// проверяем авторство записей
		if (strcmp(query->szPrefix,visit [iCycle]->fnGetPrefix()))
			continue;

	// заполняем структуру для сортировки
	iClientNum = visit [iCycle]->fnGetClientNum();
	iClient = visit [iCycle]->fnGetClientIndex();
	if (iClient == -1)
		continue;
	// критерий сортировки журнала посещений
	switch (iTuningSortReg)
	{
		case 1: // сортировка по времени
		// формат "ГГГГММДД_время_фамилия имя отчество_услуга"
		sprintf (curr_row.szSort,"%04u%02u%02u_%s_%s %s %s_%s",
			fnGetYear(visit [iCycle]->fnGetDate()),
			fnGetMonth(visit [iCycle]->fnGetDate()),
			fnGetDay(visit [iCycle]->fnGetDate()),
			visit [iCycle]->fnGetTime(),
			client [iClient]->fnGetFamily(),
			client [iClient]->fnGetName(),
			client [iClient]->fnGetName2(),
			fnGetServiceName(visit [iCycle]->fnGetServiceType()));
		break;

		case 2: // сортировка по фамилиям
		// формат "ГГГГММДД_фамилия имя отчество_время_услуга"
		sprintf (curr_row.szSort,"%04u%02u%02u_%s %s %s_%s_%s",
			fnGetYear(visit [iCycle]->fnGetDate()),
			fnGetMonth(visit [iCycle]->fnGetDate()),
			fnGetDay(visit [iCycle]->fnGetDate()),
			client [iClient]->fnGetFamily(),
			client [iClient]->fnGetName(),
			client [iClient]->fnGetName2(),
			visit [iCycle]->fnGetTime(),
			fnGetServiceName(visit [iCycle]->fnGetServiceType()));
		break;
	}
	curr_row.iVisitNum = visit [iCycle]->fnGetNum();
	curr_row.iVisit = iCycle;
	// iClientNum сохраняется, чтобы отличать Анонимного
	curr_row.iClientNum = iClientNum;
	curr_row.iClient = iClient;

	// дата
	if (config.bRegShowFutureVisits)
	{
		sprintf(curr_row.sz1,"%02u/%02u",
			fnGetDay(visit [iCycle]->fnGetDate()),
			fnGetMonth(visit [iCycle]->fnGetDate()));
		curr_row.iSpan1 = 1;
	}

	// время
	strcpy(curr_row.sz2,visit [iCycle]->fnGetTime());
	curr_row.iSpan2 = 1;

	// ФИО клиента
	sprintf(curr_row.sz3,"%s %s %s",
		client [iClient]->fnGetFamily(),
		client [iClient]->fnGetName(),
		client [iClient]->fnGetName2());
	curr_row.iSpan3 = 1;

	// специалист
	curr_row.i4 = visit [iCycle]->fnGetSpec();
	curr_row.iSpan4 = 1;

	rows->fnPush((char *) &curr_row,sizeof (curr_row));
}

3. При необходимости производится сортировка записей в массиве rows.

// **************
// * сортировка *
// **************
if (iTuningSortReg)
	rows->fnSort(0);

4. Для всех объединяемых столбцов пишутся циклы сканирования ячеек на предмет совпадения содержимого. Циклы обычно немного отличаются, часть столбцов содержат строки, часть содержат числовые значения.

// ****************************************************
// * циклы сканирования ячеек таблицы для объединения *
// ****************************************************
iQty = rows->fnGetQty() -1;

// дата
for (iCycle = 0; iCycle < iQty; iCycle++)
{
	rows->fnPop((char *) &curr_row,iCycle);

	for (iCycleRow = iCycle +1; iCycleRow <= iQty; iCycleRow++)
	{
		rows->fnPop((char *) &next_row,iCycleRow);
		if (! strcmp(curr_row.sz1,next_row.sz1))
		{
			curr_row.iSpan1++;
			next_row.iSpan1 = 0;
			rows->fnReplace((char *) &next_row,iCycleRow,sizeof(next_row));
		}
		else
			break;
	}
	if (curr_row.iSpan1 > 1)
	{
		rows->fnReplace((char *) &curr_row,iCycle,sizeof(curr_row));
		iCycle += curr_row.iSpan1 -1;
	}
}

// время
for (iCycle = 0; iCycle < iQty; iCycle++)
{
	rows->fnPop((char *) &curr_row,iCycle);

	for (iCycleRow = iCycle +1; iCycleRow <= iQty; iCycleRow++)
	{
		rows->fnPop((char *) &next_row,iCycleRow);
		if (! strcmp(curr_row.sz2,next_row.sz2))
		{
			curr_row.iSpan2++;
			next_row.iSpan2 = 0;
			rows->fnReplace((char *) &next_row,iCycleRow,sizeof(next_row));
		}
		else
			break;
	}
	if (curr_row.iSpan2 > 1)
	{
		rows->fnReplace((char *) &curr_row,iCycle,sizeof(curr_row));
		iCycle += curr_row.iSpan2 -1;
	}
}

// ФИО клиента
for (iCycle = 0; iCycle < iQty; iCycle++)
{
	rows->fnPop((char *) &curr_row,iCycle);

	for (iCycleRow = iCycle +1; iCycleRow <= iQty; iCycleRow++)
	{
		rows->fnPop((char *) &next_row,iCycleRow);
		if (! strcmp(curr_row.sz3,next_row.sz3))
		{
			curr_row.iSpan3++;
			next_row.iSpan3 = 0;
			rows->fnReplace((char *) &next_row,iCycleRow,sizeof(next_row));
		}
		else
			break;
	}
	if (curr_row.iSpan3 > 1)
	{
		rows->fnReplace((char *) &curr_row,iCycle,sizeof(curr_row));
		iCycle += curr_row.iSpan3 -1;
	}
}

// специалист
for (iCycle = 0; iCycle < iQty; iCycle++)
{
	rows->fnPop((char *) &curr_row,iCycle);

	for (iCycleRow = iCycle +1; iCycleRow <= iQty; iCycleRow++)
	{
		rows->fnPop((char *) &next_row,iCycleRow);
		if (curr_row.i4 == next_row.i4)
		{
			curr_row.iSpan4++;
			next_row.iSpan4 = 0;
			rows->fnReplace((char *) &next_row,iCycleRow,sizeof(next_row));
		}
		else
			break;
	}
	if (curr_row.iSpan4 > 1)
	{
		rows->fnReplace((char *) &curr_row,iCycle,sizeof(curr_row));
		iCycle += curr_row.iSpan4 -1;
	}
}

5. Ниже пример формирования столбцов с объединёнными по вертикали ячейками. Переменные iSpan1, iSpan2, iSpan3 и iSpan4 в каждой структуре содержат количество объединяемых ячеек или 0, если ячейка в текущей строке таблицы не описывается.

// *****************************************
// * формирование таблицы с тегами rowspan *
// *****************************************
iQty = rows->fnGetQty();
for (iCycle = 0; iCycle < iQty; iCycle++)
{
	rows->fnPop((char *) &curr_row,iCycle);

	s = "<tr>";

	// ********
	// * дата *
	// ********
	if (config.bRegShowFutureVisits)
	{
		if (curr_row.iSpan1)
		{
			s += "<td";
			if (curr_row.iSpan1 > 1)
			{
				sprintf(szTmp," rowspan=%i",curr_row.iSpan1);
				s += szTmp;
			}
			s += ">";
			s += curr_row.sz1;
			s += "</td>";
		}
	}

	// *********
	// * время *
	// *********
	if (curr_row.iSpan2)
	{
		s += "<td";
		if (curr_row.iSpan2 > 1)
		{
			sprintf(szTmp," rowspan=%i",curr_row.iSpan2);
			s += szTmp;
		}
		s += ">";
		s += curr_row.sz2;
		s += "</td>";
	}

	// **********
	// * клиент *
	// **********
	if (curr_row.iSpan3)
	{
		s += "<td";
		if (curr_row.iSpan3 > 1)
		{
			sprintf(szTmp," rowspan=%i",curr_row.iSpan3);
			s += szTmp;
		}
		s += ">";

		if (! curr_row.iClientNum)
		{
			// анонимный клиент
			s += client [curr_row.iClient]->fnGetFamily();
		}
		else
		{
			// ФИО клиента
			s += "<a href=\"/client_edit?dst=reg&client_num=";
			sprintf(szTmp,"%i",client [curr_row.iClient]->fnGetNum());
			s += szTmp;
			s += "\">";
			s += curr_row.sz3;
			s += "</a>";
		}
		s += "</td>";
	}

	// **************
	// * специалист *
	// **************
	if (config.bInputSpec)
	{
		if (curr_row.iSpan4)
		{
			s += "<td";
			if (config.bRegJournalValignTop)
				s += " valign=top";
			if (curr_row.iSpan4 > 1)
			{
				sprintf(szTmp," rowspan=%i",curr_row.iSpan4);
				s += szTmp;
			}
			s += ">";
			s += fnGetSpecFIO(curr_row.i4);
			s += "</td>";
		}
	}

Другие, не объединяемые ячейки отображаются как обычно, на основе содержащихся в структуре уникальных номеров и индексов посещения и клиента.

На главную © Д.С.Кузьмин