diff --git a/CHANGELOG.md b/CHANGELOG.md index e08d72d..550ce76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ -## New -Sheet::isName($name): bool -- Case-insensitive name checking +## v.5.6 Excel::setActiveSheet($name): Excel -- Set active (default) sheet by case-insensitive name +Sheet::isName($name): bool -- Case-insensitive name checking +Sheet::setPrintArea($range): Sheet +Sheet::setPrintTopRows($rows): Sheet +Sheet::setPrintLeftColumns($cols): Sheet +Sheet::setPrintGridlines($bool): Sheet ## v.5.5 diff --git a/README.md b/README.md index f12134d..6cf0153 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Jump To: * [Define Named Ranges](/docs/02-sheets.md#define-named-ranges) * [Freeze Panes and Autofilter](/docs/02-sheets.md#freeze-panes-and-autofilter) * [Setting Active Cells](/docs/02-sheets.md#setting-active-cells) + * [Print Settings](/docs/02-sheets.md#print-settings) * [Writing](/docs/03-writing.md) * [Writing Row by Row vs Direct](/docs/03-writing.md#writing-row-by-row-vs-direct) * [Direct Writing To Cells](/docs/03-writing.md#direct-writing-to-cells) diff --git a/docs/02-sheets.md b/docs/02-sheets.md index e594bf0..2982277 100644 --- a/docs/02-sheets.md +++ b/docs/02-sheets.md @@ -286,7 +286,16 @@ $sheet->writeCell('=Value*Rate'); ``` -### Setting Active Cells +### Setting Active Sheet and Cells + +You can select active (default) sheet in workbook + +```php +// Set active (default) sheet by case-insensitive name +$excel->setActiveSheet($name); +``` + +To select active cell on specified sheet use the following code: ```php // Selecting one active cell @@ -296,4 +305,30 @@ $sheet1->setActiveCell('B2'); $sheet1->setActiveCell('B2:C3'); ``` +### Print settings + +Specify printing area + +```php +$sheet->setPrintArea('A2:F3,A8:F10'); +``` + +To repeat specific rows/columns at top/left of a printing page, use the following code: + +```php +$sheet->setPrintTopRows('1')->setPrintLeftColumns('A'); +``` + +The following code is an example of how to repeat row 1 to 5 on each printed page: + +```php +$sheet->setPrintTopRows('1:5'); +``` + +To show/hide gridlines when printing, use the following code: + +```php +$sheet->setPrintGridlines(true); +``` + Returns to [README.md](/README.md) diff --git a/src/FastExcelWriter/Excel.php b/src/FastExcelWriter/Excel.php index 4f73285..2a6f8a4 100644 --- a/src/FastExcelWriter/Excel.php +++ b/src/FastExcelWriter/Excel.php @@ -137,6 +137,8 @@ class Excel implements InterfaceBookWriter protected array $bookViews = []; + protected array $definedNames = []; + /** @var bool */ protected bool $isRightToLeft = false; @@ -1176,7 +1178,10 @@ public function makeSheet(string $sheetName = null): Sheet } $key = mb_strtolower($sheetName); if (!isset($this->sheets[$key])) { - $this->sheets[$key] = static::createSheet($sheetName); + $sheet = static::createSheet($sheetName); + $sheet->localSheetId = count($this->sheets); + $this->sheets[$key] = $sheet; + $this->sheets[$key]->excel = $this; $this->sheets[$key]->key = $key; $this->sheets[$key]->index = count($this->sheets); @@ -1238,8 +1243,10 @@ public function getSheet($index = null): ?Sheet * Removes the first sheet of index omitted * * @param int|string|null $index + * + * @return $this */ - public function removeSheet($index = null): void + public function removeSheet($index = null): Excel { if (null === $index) { array_shift($this->sheets); @@ -1259,6 +1266,12 @@ public function removeSheet($index = null): void } unset($this->sheets[$key]); } + $localSheetId = 0; + foreach ($this->sheets as $sheet) { + $sheet->localSheetId = $localSheetId++; + } + + return $this; } /** @@ -1291,6 +1304,70 @@ public function addNamedRange(string $range, string $name): Excel ExceptionRangeName::throwNew('Sheet name not defined in range address'); } + /** + * @param string $name + * @param string $range + * @param array|null $attributes + * + * @return $this + */ + public function addDefinedName(string $name, string $range, ?array $attributes = []): Excel + { + $attributes = array_replace(['name' => Writer::xmlSpecialChars($name)], $attributes); + if ($name === '_xlnm.Print_Area' && isset($attributes['localSheetId'])) { + // add print area + foreach ($this->definedNames as $key => $definedName) { + if ($definedName['_attr']['name'] === $name && isset($definedName['localSheetId']) && $definedName['localSheetId'] === $attributes['localSheetId']) { + $this->definedNames[$key]['_value'] .= $range; + return $this; + } + } + $this->definedNames[] = [ + '_value' => $range, + '_attr' => $attributes, + ]; + } + elseif ($name === '_xlnm.Print_Titles' && isset($attributes['localSheetId'])) { + // set print title + foreach ($this->definedNames as $key => $definedName) { + if ($definedName['_attr']['name'] === $name && isset($definedName['localSheetId']) && $definedName['localSheetId'] === $attributes['localSheetId']) { + unset($this->definedNames[$key]); + } + } + $this->definedNames[] = [ + '_value' => $range, + '_attr' => $attributes, + ]; + } + else { + $this->definedNames[$name] = [ + '_value' => $range, + '_attr' => $attributes, + ]; + } + + return $this; + } + + /** + * @return array + */ + public function getDefinedNames(): array + { + $result = $this->definedNames; + foreach ($this->sheets as $sheet) { + if ($sheet->absoluteAutoFilter) { + $filterRange = $sheet->absoluteAutoFilter . ':' . Excel::cellAddress($sheet->rowCountWritten, $sheet->colCountWritten, true); + $fullAddress = "'" . $sheet->sanitizedSheetName . "'!" . $filterRange; + $result[] = [ + '_value' => $fullAddress, + '_attr' => ['name' => '_xlnm._FilterDatabase', 'localSheetId' => $sheet->localSheetId, 'hidden' => '1'], + ]; + } + } + return $result; + } + /** * @param string $imageBlob * diff --git a/src/FastExcelWriter/Sheet.php b/src/FastExcelWriter/Sheet.php index 7897ada..701fb54 100644 --- a/src/FastExcelWriter/Sheet.php +++ b/src/FastExcelWriter/Sheet.php @@ -36,6 +36,9 @@ class Sheet implements InterfaceSheetWriter /** @var int Index of the sheet */ public int $index; + /** @var int Local ID of the sheet */ + public int $localSheetId; + /** @var string Key of the sheet */ public string $key; @@ -145,6 +148,10 @@ class Sheet implements InterfaceSheetWriter // bottom sheet nodes protected array $bottomNodesOptions = []; + protected array $printAreas = []; + protected string $printTopRows = ''; + protected string $printLeftColumns = ''; + /** * Sheet constructor @@ -155,6 +162,7 @@ public function __construct(string $sheetName) { $this->setName($sheetName); $this->bottomNodesOptions = [ + 'printOptions' => [], 'pageMargins' => [ 'left' => '0.5', 'right' => '0.5', @@ -2311,6 +2319,40 @@ protected function _rangeDimension(string $cellAddress, ?int $colOffset = 1, ?in return $dimension; } + /** + * @param string|array $range1 + * @param string|array $range2 + * + * @return bool + */ + protected function _checkIntersection($range1, $range2): bool + { + $dim1 = isset($range1['rowNum1'], $range1['colNum1']) ? $range1 : $this->_rangeDimension($range1); + $dim2 = isset($range2['rowNum1'], $range2['colNum1']) ? $range2 : $this->_rangeDimension($range2); + if ( + ((($dim1['rowNum1'] >= $dim2['rowNum1']) && ($dim1['rowNum1'] <= $dim2['rowNum2'])) + || (($dim1['rowNum2'] >= $dim2['rowNum1']) && ($dim1['rowNum2'] <= $dim2['rowNum2']))) + && ((($dim1['colNum1'] >= $dim2['colNum1']) && ($dim1['colNum1'] <= $dim2['colNum2'])) + || (($dim1['colNum2'] >= $dim2['colNum1']) && ($dim1['colNum2'] <= $dim2['colNum2']))) + ) { + return true; + } + + return false; + } + + /** + * @param string|array $range + * + * @return string + */ + protected function _fullRangeAddress($range): string + { + $dim = isset($range['range']) ? $range : $this->_rangeDimension($range); + + return "'" . $this->sanitizedSheetName . "'!" . $dim['range']; + } + /** * @param string|array|null $cellAddress * @param mixed $value @@ -2927,7 +2969,7 @@ public function withLastRow(): Sheet */ public function withRange($range): Sheet { - $dimension = self::_rangeDimension($range); + $dimension = $this->_rangeDimension($range); if ($dimension['rowNum1'] <= $this->rowCountWritten) { throw new Exception('Row number must be greater than written rows'); } @@ -2945,6 +2987,7 @@ public function withRange($range): Sheet /** * Define named range + * addNamedRange('B3:C5', 'Demo') * * @param string $range * @param string $name @@ -2954,7 +2997,7 @@ public function withRange($range): Sheet public function addNamedRange(string $range, string $name): Sheet { if ($range) { - $dimension = self::_rangeDimension($range); + $dimension = $this->_rangeDimension($range); } else { $cell1 = Excel::cellAddress($this->lastTouch['area']['row_idx1'] + 1, $this->lastTouch['area']['col_idx1'] + 1, true); @@ -2990,6 +3033,8 @@ public function addNamedRange(string $range, string $name): Sheet $this->namedRanges[] = ['range' => $dimension['absAddress'], 'name' => $name]; $this->_setDimension($dimension['rowNum1'], $dimension['colNum1']); $this->_setDimension($dimension['rowNum2'], $dimension['colNum2']); + + $this->excel->addDefinedName($name, $this->_fullRangeAddress($dimension)); } return $this; @@ -3030,7 +3075,7 @@ public function addNote($cell, $comment = null, array $noteStyle = []): Sheet $cell = Excel::cellAddress($rowIdx + 1, $colIdx + 1); } else { - $dimension = self::_rangeDimension($cell); + $dimension = $this->_rangeDimension($cell); $cell = $dimension['cell1']; $rowIdx = $dimension['rowIndex']; $colIdx = $dimension['colIndex']; @@ -3130,7 +3175,7 @@ public function addImage(string $cell, string $imageFile, ?array $imageStyle = [ $cell = Excel::cellAddress($rowIdx + 1, $colIdx + 1); } else { - $dimension = self::_rangeDimension($cell); + $dimension = $this->_rangeDimension($cell); $cell = $dimension['cell1']; $rowIdx = $dimension['rowIndex']; $colIdx = $dimension['colIndex']; @@ -3665,6 +3710,107 @@ public function pagePaperWidth($paperWidth): Sheet return $this; } + /** + * @param string $range + * + * @return $this + */ + public function setPrintArea(string $range): Sheet + { + if (strpos($range, ',')) { + $ranges = explode(',', $range); + } + elseif (strpos($range, ';')) { + $ranges = explode(';', $range); + } + else { + $ranges = [$range]; + } + $address = ''; + foreach ($ranges as $range) { + $dimension = $this->_rangeDimension($range); + // checking intersections + foreach ($this->printAreas as $printArea) { + if ($this->_checkIntersection($dimension, $printArea)) { + throw new Exception('Print areas should not overlap (' . $printArea['localRange'] . ' & ' . $dimension['localRange'] . ')'); + } + } + $this->printAreas[] = $dimension; + if ($address) { + $address .= ','; + } + $address .= $this->_fullRangeAddress($dimension); + } + $this->excel->addDefinedName('_xlnm.Print_Area', $address, ['localSheetId' => $this->localSheetId]); + + return $this; + } + + /** + * @param string|null $rowsAtTop + * @param string|null $colsAtLeft + * + * @return $this + */ + public function setPrintTitles(?string $rowsAtTop, ?string $colsAtLeft = null): Sheet + { + $rowsTitle = $colsTitle = null; + if ($rowsAtTop && preg_match('/(\d+)(:(\d+))?/', $rowsAtTop, $m)) { + $rowsTitle = "'" . $this->sanitizedSheetName . "'!" . (empty($m[3]) ? '$' . $m[1] . ':$' . $m[1] : '$' . $m[1] . ':$' . $m[3]); + } + if ($colsAtLeft && preg_match('/([A-Z]+)(:([A-Z]+))?/', strtoupper($colsAtLeft), $m)) { + $colsTitle = "'" . $this->sanitizedSheetName . "'!" . (empty($m[3]) ? '$' . $m[1] . ':$' . $m[1] : '$' . $m[1] . ':$' . $m[3]); + } + if ($rowsTitle || $colsTitle) { + $address = ''; + if ($colsTitle) { + $address = $colsTitle; + } + if ($rowsTitle) { + $address .= ($address ? ',' : '') . $rowsTitle; + } + $this->excel->addDefinedName('_xlnm.Print_Titles', $address, ['localSheetId' => $this->localSheetId]); + } + + return $this; + } + + /** + * @param string $range + * + * @return $this + */ + public function setPrintTopRows(string $range): Sheet + { + $this->printTopRows = $range; + + return $this->setPrintTitles($this->printTopRows, $this->printLeftColumns); + } + + /** + * @param string $range + * + * @return $this + */ + public function setPrintLeftColumns(string $range): Sheet + { + $this->printLeftColumns = $range; + + return $this->setPrintTitles($this->printTopRows, $this->printLeftColumns); + } + + /** + * @param bool $bool + * + * @return $this + */ + public function setPrintGridlines(bool $bool): Sheet + { + $this->setBottomNodeOption('printOptions', 'gridLines', $bool ? '1' : '0'); + + return $this; + } + /** * @return array|array[] */ @@ -3779,6 +3925,7 @@ public function getBottomNodesOptions(): array { // need specified order for some nodes $order = [ + 'printOptions', 'pageMargins', 'pageSetup', 'drawing', diff --git a/src/FastExcelWriter/Writer/Writer.php b/src/FastExcelWriter/Writer/Writer.php index a77c6ee..60cca1a 100644 --- a/src/FastExcelWriter/Writer/Writer.php +++ b/src/FastExcelWriter/Writer/Writer.php @@ -1700,25 +1700,18 @@ protected function _buildWorkbookXML(Excel $excel): string $xmlText .= ''; $xmlText .= ''; - $definedNames = ''; - $i = 0; foreach ($sheets as $sheet) { $xmlText .= ''; - if ($sheet->absoluteAutoFilter) { - $filterRange = $sheet->absoluteAutoFilter . ':' . Excel::cellAddress($sheet->rowCountWritten, $sheet->colCountWritten, true); - $definedNames .= ''; - } - $i++; } $xmlText .= ''; - foreach ($sheets as $sheet) { - foreach ($sheet->getNamedRanges() as $range) { - $definedNames .= '\'' . $sheet->sanitizedSheetName . '\'!' . $range['range'] . ''; - } - } + $definedNames = $excel->getDefinedNames(); if ($definedNames) { - $xmlText .= '' . $definedNames . ''; + $xmlText .= ''; + foreach ($definedNames as $item) { + $xmlText .= '' . $item['_value'] . ''; + } + $xmlText .= ''; } else { $xmlText .= ''; diff --git a/tests/FastExcelWriterTest.php b/tests/FastExcelWriterTest.php index 6ef6cb5..12a35ce 100644 --- a/tests/FastExcelWriterTest.php +++ b/tests/FastExcelWriterTest.php @@ -230,6 +230,10 @@ public function testExcelWriter1() $sheet->setValue('C9', 'C9'); $sheet->writeCell('replace C9'); + $sheet->setAutofilter(); + $sheet->addNamedRange('b2:c3', 'b2c3'); + $sheet->setPrintArea('a2:f2,a4:f4')->setPrintTitles('1', 'a:b'); + $this->excelReader = $this->saveCheckRead($excel, $testFileName); $this->cells = $this->excelReader->readRows(false, null, true);