简体   繁体   中英

Losing merged cells border while editing Excel file with openpyxl

I have two sheets in an Excel file and the first one is a cover sheet which I don't need to edit. There are a few merged cells in the cover sheet, and when I edit the file using openpyxl, without even touching the cover sheet, I lose borders from the merged cells. I am using load_workbook('excel file') to load the Excel file and saving it with a different filename.

Is there any way to fix this problem?

Actual solution is to patch the libraries code by including this snippet after including the library, it fixes the problem. (Note: don't worry about missing definitions, eg COORD_RE, ie the patch is self-contained)

from itertools import product
import types
import openpyxl
from openpyxl import worksheet
from openpyxl.utils import range_boundaries


def patch_worksheet():
    """This monkeypatches Worksheet.merge_cells to remove cell deletion bug
    https://bitbucket.org/openpyxl/openpyxl/issues/365/styling-merged-cells-isnt-working
    Thank you to Sergey Pikhovkin for the fix
    """

    def merge_cells(self, range_string=None, start_row=None, start_column=None, end_row=None, end_column=None):
        """ Set merge on a cell range.  Range is a cell range (e.g. A1:E1)
        This is monkeypatched to remove cell deletion bug
        https://bitbucket.org/openpyxl/openpyxl/issues/365/styling-merged-cells-isnt-working
        """
        if not range_string and not all((start_row, start_column, end_row, end_column)):
            msg = "You have to provide a value either for 'coordinate' or for\
            'start_row', 'start_column', 'end_row' *and* 'end_column'"
            raise ValueError(msg)
        elif not range_string:
            range_string = '%s%s:%s%s' % (get_column_letter(start_column),
                                          start_row,
                                          get_column_letter(end_column),
                                          end_row)
        elif ":" not in range_string:
            if COORD_RE.match(range_string):
                return  # Single cell, do nothing
            raise ValueError("Range must be a cell range (e.g. A1:E1)")
        else:
            range_string = range_string.replace('$', '')

        if range_string not in self._merged_cells:
            self._merged_cells.append(range_string)


        # The following is removed by this monkeypatch:

        # min_col, min_row, max_col, max_row = range_boundaries(range_string)
        # rows = range(min_row, max_row+1)
        # cols = range(min_col, max_col+1)
        # cells = product(rows, cols)

        # all but the top-left cell are removed
        #for c in islice(cells, 1, None):
            #if c in self._cells:
                #del self._cells[c]

    # Apply monkey patch
    worksheet.Worksheet.merge_cells = merge_cells
patch_worksheet()

Source https://bitbucket.org/openpyxl/openpyxl/issues/365/styling-merged-cells-isnt-working

I know this is old but I got the same issue and the patch didn't work for me so I found a work around by using the function in the documentation that adds a style for merged cells and then looping on all the merged cells and calling the function on each range

from openpyxl import load_workbook
from openpyxl.styles import Border, Side, PatternFill, Font, GradientFill, 
Alignment

def style_range(ws, cell_range, border=Border(), fill=None, font=None, 
alignment=None):
    """
    Apply styles to a range of cells as if they were a single cell.

    :param ws:  Excel worksheet instance
    :param range: An excel range to style (e.g. A1:F20)
    :param border: An openpyxl Border
    :param fill: An openpyxl PatternFill or GradientFill
    :param font: An openpyxl Font object
    """

    top = Border(top=border.top)
    left = Border(left=border.left)
    right = Border(right=border.right)
    bottom = Border(bottom=border.bottom)

    first_cell = ws[cell_range.split(":")[0]]
    if alignment:
        ws.merge_cells(cell_range)
        first_cell.alignment = alignment

    rows = ws[cell_range]
    if font:
        first_cell.font = font

    for cell in rows[0]:
        cell.border = cell.border + top
    for cell in rows[-1]:
        cell.border = cell.border + bottom

    for row in rows:
        l = row[0]
        r = row[-1]
        l.border = l.border + left
        r.border = r.border + right
        if fill:
            for c in row:
                c.fill = fill

file = 'file.xlsx'

wb = load_workbook(file)
ws = wb['Table 1']

thin = Side(border_style="thin", color="000000")
border = Border(top=thin, left=thin, right=thin, bottom=thin)

for range in ws.merged_cells.ranges:
    style_range(ws, str(range), border=border)

wb.save('newExcel.xlsx')

For me this code work perfectly:

wb = load_workbook(template_path)
ws = wb.active
for merged_cells in ws.merged_cells.ranges:
style = ws.cell(merged_cells.min_row, merged_cells.min_col)._style
for col in range(merged_cells.min_col, merged_cells.max_col + 1):
    for row in range(merged_cells.min_row, merged_cells.max_row + 1): 
        ws.cell(row, col)._style = style

I simply find all merged cells and apply style of left top cell to the rest of the cells.

If you are using openpyxl 2.4, the solution offered by micki_mouse may not work. I managed to make it work by tweaking it this way:

from openpyxl import load_workbook
from openpyxl.utils import range_boundaries

wb = load_workbook(template_path)
ws = wb.active
for merged_cells in ws.merged_cell_ranges:
    min_col, min_row, max_col, max_row = range_boundaries(merged_cells)
    style = ws.cell(row=min_row, column=min_col)._style
    for col in range(min_col, max_col + 1):
        for row in range(min_row, max_row + 1):
        ws.cell(row=row, column=col)._style = style

Basically, you have to use "merged_cell_ranges" (deprecated in 2.5.0-b1 ) to correctly access the merged cells within the worksheet. You also must specify explicitely in the cell constructor the row and column

Simple method

  1. Go to the folder C:\\Python\\Lib\\site-packages\\ openpyxl\\worksheet folder\\

  2. Open worksheet.py file

  3. In merge_cells function comment the following lines

    min_col, min_row, max_col, max_row = range_boundaries(range_string) rows = range(min_row, max_row+1) cols = range(min_col, max_col+1) cells = product(rows, cols) all but the top-left cell are removed for c in islice(cells, 1, None): if c in self._cells: del self._cells[c]
  4. Save it.

That's all , it works for me.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM