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 .= '\'' . $sheet->sanitizedSheetName . '\'!' . $filterRange . '';
- }
- $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);