524 lines
16 KiB
Python
524 lines
16 KiB
Python
|
# encoding: utf-8
|
||
|
|
||
|
"""Table-related objects such as Table and Cell."""
|
||
|
|
||
|
from __future__ import absolute_import, division, print_function, unicode_literals
|
||
|
|
||
|
from pptx.compat import is_integer
|
||
|
from pptx.dml.fill import FillFormat
|
||
|
from pptx.oxml.table import TcRange
|
||
|
from pptx.shapes import Subshape
|
||
|
from pptx.text.text import TextFrame
|
||
|
from pptx.util import lazyproperty
|
||
|
|
||
|
|
||
|
class Table(object):
|
||
|
"""A DrawingML table object.
|
||
|
|
||
|
Not intended to be constructed directly, use
|
||
|
:meth:`.Slide.shapes.add_table` to add a table to a slide.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, tbl, graphic_frame):
|
||
|
super(Table, self).__init__()
|
||
|
self._tbl = tbl
|
||
|
self._graphic_frame = graphic_frame
|
||
|
|
||
|
def cell(self, row_idx, col_idx):
|
||
|
"""Return cell at *row_idx*, *col_idx*.
|
||
|
|
||
|
Return value is an instance of |_Cell|. *row_idx* and *col_idx* are
|
||
|
zero-based, e.g. cell(0, 0) is the top, left cell in the table.
|
||
|
"""
|
||
|
return _Cell(self._tbl.tc(row_idx, col_idx), self)
|
||
|
|
||
|
@lazyproperty
|
||
|
def columns(self):
|
||
|
"""
|
||
|
Read-only reference to collection of |_Column| objects representing
|
||
|
the table's columns. |_Column| objects are accessed using list
|
||
|
notation, e.g. ``col = tbl.columns[0]``.
|
||
|
"""
|
||
|
return _ColumnCollection(self._tbl, self)
|
||
|
|
||
|
@property
|
||
|
def first_col(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the first
|
||
|
column should be formatted differently, as for a side-heading column
|
||
|
at the far left of the table.
|
||
|
"""
|
||
|
return self._tbl.firstCol
|
||
|
|
||
|
@first_col.setter
|
||
|
def first_col(self, value):
|
||
|
self._tbl.firstCol = value
|
||
|
|
||
|
@property
|
||
|
def first_row(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the first
|
||
|
row should be formatted differently, e.g. for column headings.
|
||
|
"""
|
||
|
return self._tbl.firstRow
|
||
|
|
||
|
@first_row.setter
|
||
|
def first_row(self, value):
|
||
|
self._tbl.firstRow = value
|
||
|
|
||
|
@property
|
||
|
def horz_banding(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the rows of
|
||
|
the table should appear with alternating shading.
|
||
|
"""
|
||
|
return self._tbl.bandRow
|
||
|
|
||
|
@horz_banding.setter
|
||
|
def horz_banding(self, value):
|
||
|
self._tbl.bandRow = value
|
||
|
|
||
|
def iter_cells(self):
|
||
|
"""Generate _Cell object for each cell in this table.
|
||
|
|
||
|
Each grid cell is generated in left-to-right, top-to-bottom order.
|
||
|
"""
|
||
|
return (_Cell(tc, self) for tc in self._tbl.iter_tcs())
|
||
|
|
||
|
@property
|
||
|
def last_col(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the last
|
||
|
column should be formatted differently, as for a row totals column at
|
||
|
the far right of the table.
|
||
|
"""
|
||
|
return self._tbl.lastCol
|
||
|
|
||
|
@last_col.setter
|
||
|
def last_col(self, value):
|
||
|
self._tbl.lastCol = value
|
||
|
|
||
|
@property
|
||
|
def last_row(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the last
|
||
|
row should be formatted differently, as for a totals row at the
|
||
|
bottom of the table.
|
||
|
"""
|
||
|
return self._tbl.lastRow
|
||
|
|
||
|
@last_row.setter
|
||
|
def last_row(self, value):
|
||
|
self._tbl.lastRow = value
|
||
|
|
||
|
def notify_height_changed(self):
|
||
|
"""
|
||
|
Called by a row when its height changes, triggering the graphic frame
|
||
|
to recalculate its total height (as the sum of the row heights).
|
||
|
"""
|
||
|
new_table_height = sum([row.height for row in self.rows])
|
||
|
self._graphic_frame.height = new_table_height
|
||
|
|
||
|
def notify_width_changed(self):
|
||
|
"""
|
||
|
Called by a column when its width changes, triggering the graphic
|
||
|
frame to recalculate its total width (as the sum of the column
|
||
|
widths).
|
||
|
"""
|
||
|
new_table_width = sum([col.width for col in self.columns])
|
||
|
self._graphic_frame.width = new_table_width
|
||
|
|
||
|
@property
|
||
|
def part(self):
|
||
|
"""
|
||
|
The package part containing this table.
|
||
|
"""
|
||
|
return self._graphic_frame.part
|
||
|
|
||
|
@lazyproperty
|
||
|
def rows(self):
|
||
|
"""
|
||
|
Read-only reference to collection of |_Row| objects representing the
|
||
|
table's rows. |_Row| objects are accessed using list notation, e.g.
|
||
|
``col = tbl.rows[0]``.
|
||
|
"""
|
||
|
return _RowCollection(self._tbl, self)
|
||
|
|
||
|
@property
|
||
|
def vert_banding(self):
|
||
|
"""
|
||
|
Read/write boolean property which, when true, indicates the columns
|
||
|
of the table should appear with alternating shading.
|
||
|
"""
|
||
|
return self._tbl.bandCol
|
||
|
|
||
|
@vert_banding.setter
|
||
|
def vert_banding(self, value):
|
||
|
self._tbl.bandCol = value
|
||
|
|
||
|
|
||
|
class _Cell(Subshape):
|
||
|
"""Table cell"""
|
||
|
|
||
|
def __init__(self, tc, parent):
|
||
|
super(_Cell, self).__init__(parent)
|
||
|
self._tc = tc
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
"""|True| if this object proxies the same element as *other*.
|
||
|
|
||
|
Equality for proxy objects is defined as referring to the same XML
|
||
|
element, whether or not they are the same proxy object instance.
|
||
|
"""
|
||
|
if not isinstance(other, type(self)):
|
||
|
return False
|
||
|
return self._tc is other._tc
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
if not isinstance(other, type(self)):
|
||
|
return True
|
||
|
return self._tc is not other._tc
|
||
|
|
||
|
@lazyproperty
|
||
|
def fill(self):
|
||
|
"""
|
||
|
|FillFormat| instance for this cell, providing access to fill
|
||
|
properties such as foreground color.
|
||
|
"""
|
||
|
tcPr = self._tc.get_or_add_tcPr()
|
||
|
return FillFormat.from_fill_parent(tcPr)
|
||
|
|
||
|
@property
|
||
|
def is_merge_origin(self):
|
||
|
"""True if this cell is the top-left grid cell in a merged cell."""
|
||
|
return self._tc.is_merge_origin
|
||
|
|
||
|
@property
|
||
|
def is_spanned(self):
|
||
|
"""True if this cell is spanned by a merge-origin cell.
|
||
|
|
||
|
A merge-origin cell "spans" the other grid cells in its merge range,
|
||
|
consuming their area and "shadowing" the spanned grid cells.
|
||
|
|
||
|
Note this value is |False| for a merge-origin cell. A merge-origin
|
||
|
cell spans other grid cells, but is not itself a spanned cell.
|
||
|
"""
|
||
|
return self._tc.is_spanned
|
||
|
|
||
|
@property
|
||
|
def margin_left(self):
|
||
|
"""
|
||
|
Read/write integer value of left margin of cell as a |Length| value
|
||
|
object. If assigned |None|, the default value is used, 0.1 inches for
|
||
|
left and right margins and 0.05 inches for top and bottom.
|
||
|
"""
|
||
|
return self._tc.marL
|
||
|
|
||
|
@margin_left.setter
|
||
|
def margin_left(self, margin_left):
|
||
|
self._validate_margin_value(margin_left)
|
||
|
self._tc.marL = margin_left
|
||
|
|
||
|
@property
|
||
|
def margin_right(self):
|
||
|
"""
|
||
|
Right margin of cell.
|
||
|
"""
|
||
|
return self._tc.marR
|
||
|
|
||
|
@margin_right.setter
|
||
|
def margin_right(self, margin_right):
|
||
|
self._validate_margin_value(margin_right)
|
||
|
self._tc.marR = margin_right
|
||
|
|
||
|
@property
|
||
|
def margin_top(self):
|
||
|
"""
|
||
|
Top margin of cell.
|
||
|
"""
|
||
|
return self._tc.marT
|
||
|
|
||
|
@margin_top.setter
|
||
|
def margin_top(self, margin_top):
|
||
|
self._validate_margin_value(margin_top)
|
||
|
self._tc.marT = margin_top
|
||
|
|
||
|
@property
|
||
|
def margin_bottom(self):
|
||
|
"""
|
||
|
Bottom margin of cell.
|
||
|
"""
|
||
|
return self._tc.marB
|
||
|
|
||
|
@margin_bottom.setter
|
||
|
def margin_bottom(self, margin_bottom):
|
||
|
self._validate_margin_value(margin_bottom)
|
||
|
self._tc.marB = margin_bottom
|
||
|
|
||
|
def merge(self, other_cell):
|
||
|
"""Create merged cell from this cell to *other_cell*.
|
||
|
|
||
|
This cell and *other_cell* specify opposite corners of the merged
|
||
|
cell range. Either diagonal of the cell region may be specified in
|
||
|
either order, e.g. self=bottom-right, other_cell=top-left, etc.
|
||
|
|
||
|
Raises |ValueError| if the specified range already contains merged
|
||
|
cells anywhere within its extents or if *other_cell* is not in the
|
||
|
same table as *self*.
|
||
|
"""
|
||
|
tc_range = TcRange(self._tc, other_cell._tc)
|
||
|
|
||
|
if not tc_range.in_same_table:
|
||
|
raise ValueError("other_cell from different table")
|
||
|
if tc_range.contains_merged_cell:
|
||
|
raise ValueError("range contains one or more merged cells")
|
||
|
|
||
|
tc_range.move_content_to_origin()
|
||
|
|
||
|
row_count, col_count = tc_range.dimensions
|
||
|
|
||
|
for tc in tc_range.iter_top_row_tcs():
|
||
|
tc.rowSpan = row_count
|
||
|
for tc in tc_range.iter_left_col_tcs():
|
||
|
tc.gridSpan = col_count
|
||
|
for tc in tc_range.iter_except_left_col_tcs():
|
||
|
tc.hMerge = True
|
||
|
for tc in tc_range.iter_except_top_row_tcs():
|
||
|
tc.vMerge = True
|
||
|
|
||
|
@property
|
||
|
def span_height(self):
|
||
|
"""int count of rows spanned by this cell.
|
||
|
|
||
|
The value of this property may be misleading (often 1) on cells where
|
||
|
`.is_merge_origin` is not |True|, since only a merge-origin cell
|
||
|
contains complete span information. This property is only intended
|
||
|
for use on cells known to be a merge origin by testing
|
||
|
`.is_merge_origin`.
|
||
|
"""
|
||
|
return self._tc.rowSpan
|
||
|
|
||
|
@property
|
||
|
def span_width(self):
|
||
|
"""int count of columns spanned by this cell.
|
||
|
|
||
|
The value of this property may be misleading (often 1) on cells where
|
||
|
`.is_merge_origin` is not |True|, since only a merge-origin cell
|
||
|
contains complete span information. This property is only intended
|
||
|
for use on cells known to be a merge origin by testing
|
||
|
`.is_merge_origin`.
|
||
|
"""
|
||
|
return self._tc.gridSpan
|
||
|
|
||
|
def split(self):
|
||
|
"""Remove merge from this (merge-origin) cell.
|
||
|
|
||
|
The merged cell represented by this object will be "unmerged",
|
||
|
yielding a separate unmerged cell for each grid cell previously
|
||
|
spanned by this merge.
|
||
|
|
||
|
Raises |ValueError| when this cell is not a merge-origin cell. Test
|
||
|
with `.is_merge_origin` before calling.
|
||
|
"""
|
||
|
if not self.is_merge_origin:
|
||
|
raise ValueError(
|
||
|
"not a merge-origin cell; only a merge-origin cell can be sp" "lit"
|
||
|
)
|
||
|
|
||
|
tc_range = TcRange.from_merge_origin(self._tc)
|
||
|
|
||
|
for tc in tc_range.iter_tcs():
|
||
|
tc.rowSpan = tc.gridSpan = 1
|
||
|
tc.hMerge = tc.vMerge = False
|
||
|
|
||
|
@property
|
||
|
def text(self):
|
||
|
"""Unicode (str in Python 3) representation of cell contents.
|
||
|
|
||
|
The returned string will contain a newline character (``"\\n"``) separating each
|
||
|
paragraph and a vertical-tab (``"\\v"``) character for each line break (soft
|
||
|
carriage return) in the cell's text.
|
||
|
|
||
|
Assignment to *text* replaces all text currently contained in the cell. A
|
||
|
newline character (``"\\n"``) in the assigned text causes a new paragraph to be
|
||
|
started. A vertical-tab (``"\\v"``) character in the assigned text causes
|
||
|
a line-break (soft carriage-return) to be inserted. (The vertical-tab character
|
||
|
appears in clipboard text copied from PowerPoint as its encoding of
|
||
|
line-breaks.)
|
||
|
|
||
|
Either bytes (Python 2 str) or unicode (Python 3 str) can be assigned. Bytes can
|
||
|
be 7-bit ASCII or UTF-8 encoded 8-bit bytes. Bytes values are converted to
|
||
|
unicode assuming UTF-8 encoding (which correctly decodes ASCII).
|
||
|
"""
|
||
|
return self.text_frame.text
|
||
|
|
||
|
@text.setter
|
||
|
def text(self, text):
|
||
|
self.text_frame.text = text
|
||
|
|
||
|
@property
|
||
|
def text_frame(self):
|
||
|
"""
|
||
|
|TextFrame| instance containing the text that appears in the cell.
|
||
|
"""
|
||
|
txBody = self._tc.get_or_add_txBody()
|
||
|
return TextFrame(txBody, self)
|
||
|
|
||
|
@property
|
||
|
def vertical_anchor(self):
|
||
|
"""Vertical alignment of this cell.
|
||
|
|
||
|
This value is a member of the :ref:`MsoVerticalAnchor` enumeration or
|
||
|
|None|. A value of |None| indicates the cell has no explicitly
|
||
|
applied vertical anchor setting and its effective value is inherited
|
||
|
from its style-hierarchy ancestors.
|
||
|
|
||
|
Assigning |None| to this property causes any explicitly applied
|
||
|
vertical anchor setting to be cleared and inheritance of its
|
||
|
effective value to be restored.
|
||
|
"""
|
||
|
return self._tc.anchor
|
||
|
|
||
|
@vertical_anchor.setter
|
||
|
def vertical_anchor(self, mso_anchor_idx):
|
||
|
self._tc.anchor = mso_anchor_idx
|
||
|
|
||
|
@staticmethod
|
||
|
def _validate_margin_value(margin_value):
|
||
|
"""
|
||
|
Raise ValueError if *margin_value* is not a positive integer value or
|
||
|
|None|.
|
||
|
"""
|
||
|
if not is_integer(margin_value) and margin_value is not None:
|
||
|
tmpl = "margin value must be integer or None, got '%s'"
|
||
|
raise TypeError(tmpl % margin_value)
|
||
|
|
||
|
|
||
|
class _Column(Subshape):
|
||
|
"""Table column"""
|
||
|
|
||
|
def __init__(self, gridCol, parent):
|
||
|
super(_Column, self).__init__(parent)
|
||
|
self._gridCol = gridCol
|
||
|
|
||
|
@property
|
||
|
def width(self):
|
||
|
"""
|
||
|
Width of column in EMU.
|
||
|
"""
|
||
|
return self._gridCol.w
|
||
|
|
||
|
@width.setter
|
||
|
def width(self, width):
|
||
|
self._gridCol.w = width
|
||
|
self._parent.notify_width_changed()
|
||
|
|
||
|
|
||
|
class _Row(Subshape):
|
||
|
"""Table row"""
|
||
|
|
||
|
def __init__(self, tr, parent):
|
||
|
super(_Row, self).__init__(parent)
|
||
|
self._tr = tr
|
||
|
|
||
|
@property
|
||
|
def cells(self):
|
||
|
"""
|
||
|
Read-only reference to collection of cells in row. An individual cell
|
||
|
is referenced using list notation, e.g. ``cell = row.cells[0]``.
|
||
|
"""
|
||
|
return _CellCollection(self._tr, self)
|
||
|
|
||
|
@property
|
||
|
def height(self):
|
||
|
"""
|
||
|
Height of row in EMU.
|
||
|
"""
|
||
|
return self._tr.h
|
||
|
|
||
|
@height.setter
|
||
|
def height(self, height):
|
||
|
self._tr.h = height
|
||
|
self._parent.notify_height_changed()
|
||
|
|
||
|
|
||
|
class _CellCollection(Subshape):
|
||
|
"""Horizontal sequence of row cells"""
|
||
|
|
||
|
def __init__(self, tr, parent):
|
||
|
super(_CellCollection, self).__init__(parent)
|
||
|
self._tr = tr
|
||
|
|
||
|
def __getitem__(self, idx):
|
||
|
"""Provides indexed access, (e.g. 'cells[0]')."""
|
||
|
if idx < 0 or idx >= len(self._tr.tc_lst):
|
||
|
msg = "cell index [%d] out of range" % idx
|
||
|
raise IndexError(msg)
|
||
|
return _Cell(self._tr.tc_lst[idx], self)
|
||
|
|
||
|
def __iter__(self):
|
||
|
"""Provides iterability."""
|
||
|
return (_Cell(tc, self) for tc in self._tr.tc_lst)
|
||
|
|
||
|
def __len__(self):
|
||
|
"""Supports len() function (e.g. 'len(cells) == 1')."""
|
||
|
return len(self._tr.tc_lst)
|
||
|
|
||
|
|
||
|
class _ColumnCollection(Subshape):
|
||
|
"""Sequence of table columns."""
|
||
|
|
||
|
def __init__(self, tbl, parent):
|
||
|
super(_ColumnCollection, self).__init__(parent)
|
||
|
self._tbl = tbl
|
||
|
|
||
|
def __getitem__(self, idx):
|
||
|
"""
|
||
|
Provides indexed access, (e.g. 'columns[0]').
|
||
|
"""
|
||
|
if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst):
|
||
|
msg = "column index [%d] out of range" % idx
|
||
|
raise IndexError(msg)
|
||
|
return _Column(self._tbl.tblGrid.gridCol_lst[idx], self)
|
||
|
|
||
|
def __len__(self):
|
||
|
"""
|
||
|
Supports len() function (e.g. 'len(columns) == 1').
|
||
|
"""
|
||
|
return len(self._tbl.tblGrid.gridCol_lst)
|
||
|
|
||
|
def notify_width_changed(self):
|
||
|
"""
|
||
|
Called by a column when its width changes. Pass along to parent.
|
||
|
"""
|
||
|
self._parent.notify_width_changed()
|
||
|
|
||
|
|
||
|
class _RowCollection(Subshape):
|
||
|
"""Sequence of table rows"""
|
||
|
|
||
|
def __init__(self, tbl, parent):
|
||
|
super(_RowCollection, self).__init__(parent)
|
||
|
self._tbl = tbl
|
||
|
|
||
|
def __getitem__(self, idx):
|
||
|
"""
|
||
|
Provides indexed access, (e.g. 'rows[0]').
|
||
|
"""
|
||
|
if idx < 0 or idx >= len(self):
|
||
|
msg = "row index [%d] out of range" % idx
|
||
|
raise IndexError(msg)
|
||
|
return _Row(self._tbl.tr_lst[idx], self)
|
||
|
|
||
|
def __len__(self):
|
||
|
"""
|
||
|
Supports len() function (e.g. 'len(rows) == 1').
|
||
|
"""
|
||
|
return len(self._tbl.tr_lst)
|
||
|
|
||
|
def notify_height_changed(self):
|
||
|
"""
|
||
|
Called by a row when its height changes. Pass along to parent.
|
||
|
"""
|
||
|
self._parent.notify_height_changed()
|