Сохранить qtablewidget в excel

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

import xlrd                                                      # pip install xlrd

# XlsxWriter - это модуль Python для создания файлов Excel XLSX.
from xlsxwriter.workbook import Workbook                         # pip install XlsxWriter

from PyQt5.Qt import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.centralWidget = QWidget(self)            
        self.setCentralWidget(self.centralWidget) 
        menuBar = self.menuBar()
        fileMenu = QMenu("&File", self)
        menuBar.addMenu(fileMenu)
        self.actionOpen = QAction("Open", self)
        self.actionSave = QAction("Save", self)        
        fileMenu.addAction(self.actionOpen)
        fileMenu.addAction(self.actionSave)
        self.actionOpen.triggered.connect(self.fileOpen)
        self.actionSave.triggered.connect(self.fileSave)

        self.tableWidget = QTableWidget() 
        _data = {
            '0': ["Hello", "", ""],
            '1': ["", "World", ""],
            '2': ["item 1", "item 2", "item 3"],
            '3': ["hello", "world", "777"]
        }
        self.tableWidget.setColumnCount(len(_data[list(_data.keys())[0]]))
        self.tableWidget.setRowCount(len(_data))                                       
        self.tableWidget.verticalHeader().setMinimumSectionSize(1)
        for row, data in _data.items():
            for column, value in enumerate(data):
                self.tableWidget.setItem(int(row), column, QTableWidgetItem(value))
        
        self.main_layout = QGridLayout(self.centralWidget)
        self.main_layout.addWidget(self.tableWidget)        

    def fileSave(self):
        fileName, ok = QFileDialog.getSaveFileName(
            self,
            "Сохранить файл",
            ".",
            "All Files(*.xlsx)"
        )
        if not fileName:
            return 

        _list = []
        model = self.tableWidget.model()
        for row in range(model.rowCount()):
            _r = []
            for column in range(model.columnCount()):
                _r.append("{}".format(model.index(row, column).data() or ""))
            _list.append(_r)
        print(fileName)
        
        workbook = Workbook(fileName)
        worksheet = workbook.add_worksheet() 

        for r, row in enumerate(_list):
            for c, col in enumerate(row):
                worksheet.write(r, c, col)        
        workbook.close()  
        msg = QMessageBox.information(
            self, 
            "Success!", 
            f"Данные сохранены в файле: n{fileName}"
        )            

    def fileOpen(self):
        fileName, ok = QFileDialog.getOpenFileName(
            self, 
            'Open file',
            '.',
            "PersonDoc files (*.xlsx)"
        )
        if not fileName:
            return
            
        data = xlrd.open_workbook(fileName)
        table = data.sheets()[0]
        nrows = table.nrows
        ncols = table.ncols
 
        self.tableWidget.clear()
        self.tableWidget.setRowCount(nrows)
        self.tableWidget.setColumnCount(ncols)

        for i in range(nrows):
            for j in range(ncols):
                if isinstance(table.row_values(i)[j], str) == False:
                    newitem = str(table.row_values(i)[j])
                else:
                    newitem = table.row_values(i)[j]
                newitem = QTableWidgetItem(newitem)
                self.tableWidget.setItem(i,j,newitem)


if __name__=="__main__":
    import sys
    app = QApplication(sys.argv)
    w = MainWindow()
    w.resize(600, 400)
    w.show()
    sys.exit(app.exec_())

введите сюда описание изображения



Buy Me a Coffee? Your support is much appreciated!

PayPal Me: https://www.paypal.me/jiejenn/5
Venmo: @Jie-Jenn

Source Code:

import sys
from PyQt6.QtWidgets import QApplication, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QHBoxLayout, QVBoxLayout
from PyQt6.QtCore import Qt
import pandas as pd


class MyApp(QWidget):
    def __init__(self):
        super().__init__()
        self.window_width, self.window_height = 700, 500
        self.resize(self.window_width, self.window_height)

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.table = QTableWidget()
        layout.addWidget(self.table)

        self.button = QPushButton('&Export To Excel', clicked=self.exportToExcel)
        layout.addWidget(self.button)

        self.loadData()

    def exportToExcel(self):
        columnHeaders = []

        # create column header list
        for j in range(self.table.model().columnCount()):
            columnHeaders.append(self.table.horizontalHeaderItem(j).text())

        df = pd.DataFrame(columns=columnHeaders)

        # create dataframe object recordset
        for row in range(self.table.rowCount()):
            for col in range(self.table.columnCount()):
                df.at[row, columnHeaders[col]] = self.table.item(row, col).text()

        df.to_excel('Dummy File XYZ.xlsx', index=False)
        print('Excel file exported')

    def loadData(self):
        self.headerLabels = list('ABCDEFG')

        n = 3000
        self.table.setRowCount(n)
        self.table.setColumnCount(len(self.headerLabels))
        self.table.setHorizontalHeaderLabels(self.headerLabels)

        for row in range(n):
            for col in range(len(self.headerLabels)):
                item = QTableWidgetItem('Cell {0}-{1}'.format(self.headerLabels[col], row + 1))
                self.table.setItem(row, col, item)

        self.table.resizeColumnsToContents()
        self.table.resizeRowsToContents()

if __name__ == '__main__':
    # don't auto scale when drag app to a different monitor.
    # QGuiApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    
    app = QApplication(sys.argv)
    app.setStyleSheet('''
        QWidget {
            font-size: 17px;
        }
    ''')
    
    myApp = MyApp()
    myApp.show()

    try:
        sys.exit(app.exec())
    except SystemExit:
        print('Closing Window...')

Спасибо за советы. Попробовал реализовать через ActiveX, но строчка выделенная жирным шрифтом, где я получаю указатель на список листов, способствует вылету приложения(т.е. оно запускается, но в процессе работы вылетает). В чем может быть причина? Заранее спасибо

    QAxObject* excel = new QAxObject( «Excel.Application», 0 );
      excel->dynamicCall(«SetVisible(bool)»,true);
      QAxObject *workbooks = excel->querySubObject( «Workbooks» );
      QAxObject *workbook = workbooks->querySubObject( «Open(const QString&)», «C:\Users\user\Desktop\A.xls» );
      QAxObject *sheets = workbook->querySubObject( «Sheets» );
//      QAxObject *StatSheet = sheets->querySubObject( «Item(const QVariant&)», QVariant(«stat») );
//      StatSheet->dynamicCall( «Select()» );
//      QAxObject *range = StatSheet->querySubObject( «Range(const QVariant&)», QVariant( Qstring(«A1:A1»)));
//      range->dynamicCall( «Clear()» ); // на всякий случай очищаем эту ячейк
//      range->dynamicCall( «SetValue(const QVariant&)», QVariant(5) );//записываем в эту ячейку число 5

Время на прочтение
9 мин

Количество просмотров 15K

В предыдущей части я рассказывал о создании модуля для запуска SQL-запросов и оболочки, в которой эти модули запускаются. После недолгой работы с запросами возникает очевидный вопрос — а как воспользоваться результатом выборки, кроме как посмотреть на экране?

Для этого стоит сделать дополнительные инструменты экспорта и копирования данных. Экспортировать будем в файл в формате Excel, а копировать в системный буфер в формате HTML.

Но для начала прилепим к нашему главному окну панель инструментов.

Панель инструментов

Напомню, что наше приложение призвано быть простым, универсальным и расширяемым. Чтобы тулбар тоже сделать универсальным и расширяемым, вынесем его определение в файл конфигурации, а выполняемые функции будут находиться во внешних модулях, явно не импортируемых в модуле тулбара. Таким образом добавление новой кнопки и функции сведется к прописыванию их в конфигурационном файле и добавлению модуля в каталог программы.

Файл toolbar.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import importlib

class ToolBar(QToolBar):
    def __init__(self, iniFile, parent=None):
        super(ToolBar, self).__init__(parent)
        ini = QSettings(iniFile, QSettings.IniFormat)
        ini.setIniCodec("utf-8")
        ini.beginGroup("Tools")
        for key in sorted(ini.childKeys()):
            v = ini.value(key)
            title = v[0]
            params = v[1:]
            a = self.addAction(title)
            a.params = params
            a.triggered.connect(self.execAction)
        ini.endGroup()

    def execAction(self):
        try:
            params = self.sender().params
            module = importlib.import_module(params[0])
            if len(params) < 2: func = "run()"
            else: func = params[1]
            win = self.focusTaskWindow()
            exec("module.%s(win)" % func)
        except:
            print(str(sys.exc_info()[1]))
        return

    def focusTaskWindow(self):
        try:
            return QApplication.instance().focusedTaskWindow()
        except:
            return None

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = ToolBar("tools.ini")
    flags = Qt.Tool | Qt.WindowDoesNotAcceptFocus # | ex.windowFlags()
    ex.setWindowFlags(flags)
    ex.show()
    sys.exit(app.exec_())

Для панелей инструментов в Qt есть готовый класс QToolBar, от него породим свой ToolBar. Сейчас нам достаточно одного тулбара, но заложимся на возможность добавления в программу нескольких панелей. Каждой панели нужен свой конфигурационный файл со своим набором кнопок, поэтому имя файла будем передавать параметром при создании тулбара.
Конфигурационный файл будет традиционно в формате Ini и кодировке UTF-8.

class ToolBar(QToolBar):
    def __init__(self, iniFile, parent=None):
        super(ToolBar, self).__init__(parent)
        ini = QSettings(iniFile, QSettings.IniFormat)
        ini.setIniCodec("utf-8")

Синтаксис определения кнопок в наших руках, в простейшем случае нам нужны три вещи:

— текст на кнопке
— модуль, содержащий функцию кнопки
— функция кнопки

Определимся, что функция кнопки принимает один параметр — текущее дочернее окно. Что именно будет делать модуль с ним — задача модуля кнопки, а задача тулбара ограничивается только его вызовом.

Создадим такой файл tools.ini:

[Tools]
001=Export to Excel,exportview,"exportToExcel"
002=Copy as HTML,exportview,"copyAsHtml"

Теперь в питоне разбираем определения из Ini-файла:

        ini.beginGroup("Tools")
        # Перебираем переменные в алфавитном порядке
        for key in sorted(ini.childKeys()):
            # Здесь мы получим list, т.к. ini позволяет указать 
            # список значений, разделенных запятыми
            v = ini.value(key)
            title = v[0]
            params = v[1:]
            # создадим на панели кнопку и QAction, отвечающий за нее
            a = self.addAction(title) 
            # остаток списка со второго элемента [модуль, функция] сохраним в QAction
            a.params = params 
            # для всех кнопок у нас будет один метод выполнения
            a.triggered.connect(self.execAction) 
        ini.endGroup()

Метод выполнения, назначенный всем кнопкам, будет импортировать нужный модуль и вызывать из него назначенную кнопке функцию. Чтобы нам не прописывать каждый модуль в перечне импорта тулбара, воспользуемся библиотекой importlib. Осталось только узнать, что за кнопка была нажата и от какого QAction пришел сигнал — за это отвечает стандартный метод QObject.sender(), далее возьмем сохраненные в нем параметры и сделаем то, что задумано в модуле (что бы это ни было).

    def execAction(self):
        try:
            params = self.sender().params
            module = importlib.import_module(params[0])
            func = params[1]
            win = self.focusTaskWindow()
            exec("module.%s(win)" % func)
        except:
            print(str(sys.exc_info()[1]))
        return

Осталось добавить нашу панель в наше главное окно (модуль tasktree.py)

        self.tools = ToolBar("tools.ini",self)
        self.addToolBar(self.tools)

Можем запустить и проверить, появилась ли панель:

Может быть не так симпатично, как на первой картинке, главное, что работает.

Модуль функций инструментов

Теперь самое время сделать модуль с функциями кнопок. Модуль у нас будет один, потому что функции экспорта и копирования будут работать с одним источником данных и по одинаковым правилам, нет смысла разносить их по разным модулям.

Файл exportview.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import datetime
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import xlsxwriter

class ob():
   def test(self):
      return 1

def exportToExcel(win):
   if win == None:
      print("No focused window")
      return
   view = focusItemView(win)
   title = win.windowTitle() + '.xlsx'
   if view == None:
      print("No focused item view")
      return

   # Create a workbook and add a worksheet.
   fileName = QFileDialog.getSaveFileName(None, 'Save Excel file', title,'Excel files (*.xlsx)')
   if fileName == ('',''): return

   indexes = view.selectionModel().selectedIndexes()
   if len(indexes) == 0:
      indexes = view.selectAll()
      indexes = view.selectionModel().selectedIndexes()
   model = view.model()
   d = sortedIndexes(indexes)
   headers = { col:model.headerData(col, Qt.Horizontal) for col in d.columns }
   minRow = min(d.rows)
   minCol = min(d.columns)
   try:
      workbook = xlsxwriter.Workbook(fileName[0])
      worksheet = workbook.add_worksheet()
      bold = workbook.add_format({'bold': True})
      dateFormat = 'dd.MM.yyyy'
      date = workbook.add_format({'num_format': dateFormat})
      realCol = 0
      for col in d.columns:
         worksheet.write(0, realCol, headers[col], bold)
         realRow = 1
         for row in d.rows:
            if (row, col) in d.indexes:
               try:
                  v = d.indexes[(row,col)].data(Qt.EditRole)
                  if isinstance(v, QDateTime):
                     if v.isValid() and v.toPyDateTime() > datetime.datetime(1900,1,1):
                        v = v.toPyDateTime()
                        worksheet.write_datetime(realRow, realCol, v, date)
                     else:
                        v = v.toString(dateFormat)
                        worksheet.write(realRow, realCol, v)
                  else:
                     worksheet.write(realRow, realCol, v)
               except:
                  print(str(sys.exc_info()[1]))
            realRow += 1
         realCol += 1
      workbook.close()
   except:
      QMessageBox.critical(None,'Export error',str(sys.exc_info()[1]))
      return

def copyAsHtml(win):
   if win == None:
      print("No focused window")
      return
   view = focusItemView(win)
   if view == None:
      print("No focused item view")
      return
   indexes = view.selectedIndexes()
   if len(indexes) == 0:
      indexes = view.selectAll()
      indexes = view.selectedIndexes()
   if len(indexes) == 0:
      return;
   model = view.model()
   try:
      d = sortedIndexes(indexes)
      html = '<table><tbody>n'
      headers = { col:model.headerData(col, Qt.Horizontal) for col in d.columns }
      html += '<tr>' 
      for c in d.columns:
         html += '<th>%s</th>' % headers[c]
      html += '</tr>n' 
      for r in d.rows:
         html += '<tr>' 
         for c in d.columns:
            if (r, c) in d.indexes:
               v = d.indexes[(r,c)].data(Qt.DisplayRole)
               html += '<td>%s</td>' % v
            else:
               html += '<td></td>'
         html += '</tr>' 
      html += '</tbody></table>'
      mime = QMimeData()
      mime.setHtml(html)
      clipboard = QApplication.clipboard()
      clipboard.setMimeData(mime)
   except:
      QMessageBox.critical(None,'Export error',str(sys.exc_info()[1]))

def sortedIndexes(indexes):
    d = ob()
    d.indexes = { (i.row(), i.column()):i for i in indexes }
    d.rows = sorted(list(set([ i[0] for i in d.indexes ])))
    d.columns = sorted(list(set([ i[1] for i in d.indexes ])))
    return d

def headerNames(model, minCol, maxCol):
    headers = dict()
    for col in range(minCol, maxCol+1):
        headers[col] = model.headerData(col, Qt.Horizontal)
    return headers

def focusItemView(win):
    if win == None: return None
    w = win.focusWidget()
    if w != None and isinstance(w, QTableView):
        return w
    views = win.findChildren(QTableView)
    if type(views) == type([]) and len(views)>0:
        return views[0]
    return None

Наши функции будут работать с таблицами данных QTableView, который мы использовали в модулях для просмотра результатов запроса. Чтобы сохранить независимость модулей, определять нужный компонент будем «на лету» — либо это текущий выбранный (focused) компонент QTableView в текущем окне, либо первый попавшийся нужного класса среди дочерних элементов текущего окна.

def focusItemView(win):
    if win == None: return None
    w = win.focusWidget()
    if w != None and isinstance(w, QTableView):
        return w
    views = win.findChildren(QTableView)
    if type(views) == type([]) and len(views)>0:
        return views[0]
    return None

Из таблицы получаем список выбранных ячеек. Если ничего не выбрано, то принудительно выбираем всё.

   indexes = view.selectionModel().selectedIndexes()
   if len(indexes) == 0:
      indexes = view.selectAll()
      indexes = view.selectionModel().selectedIndexes()
   if len(indexes) == 0:
      return;

Наверное, вы уже в курсе, что в Qt вы не получаете массив данных напрямую, вместо этого вы работаете с индексами в модели. Индекс QModelIndex представляет собой простую структуру и указывает на конкретную позицию данных (строку row() и столбец column(), а в иерархии указание на индекс родителя parent()). Получив индекс, можно из него получить сами данные методом data().

Мы получили список индексов выбранных ячеек в модели, но индексы в этом списке следуют в том порядке, в котором пользователь их выделял, а не отсортированные по строкам и столбцам. Нам же удобнее будет работать не со списком, а с словарем (позиция → индекс) и сортированными списками задействованных строк и столбцов.

def sortedIndexes(indexes):
    d = ob() # объект-пустышка
    d.indexes = { (i.row(), i.column()):i for i in indexes }
    d.rows = sorted(list(set([ i[0] for i in d.indexes ])))
    d.columns = sorted(list(set([ i[1] for i in d.indexes ])))
    return d

Еще стоит учесть, что QTableView по умолчанию позволяет выделять несвязанные ячейки, потому в списке индексов могут быть ячейки, практически случайно расположенные:

Поэтому в d.rows есть каждая использованная строка, в d.columns есть каждый использованный столбец, но их сочетание необязательно есть в d.indexes.

Еще нам для пущей красоты нужен перечень наименований столбцов, который выводятся в QTableView. Получим их из модели методом headerData:

   headers = { col:model.headerData(col, Qt.Horizontal) for col in d.columns }

До сих пор код для экспорта и копирования был одинаковым, но теперь пошли различия.

Экспорт в Excel

Для экспорта в файлы Excel я воспользовался пакетом xlsxwriter. Он устанавливается, как обычно, через pip:

pip3 install xlsxwriter

Документация пакета вполне подробная и понятная, с примерами, поэтому останавливаться на нем не буду. Суть в том, что запись идет по ячейкам, адресуемым по номеру строки и столбца. Если нужно дополнительное форматирование, то нужно определить стиль и указывать его при записи ячейки.

Имя xlsx-файла, в который будем экспортировать, запросим у пользователя, у Qt есть такая функция. В PyQt функция возвращает список из выбранного имени файла и использованного фильтра. Если вернулся список из пустых строк, то это означает, что пользователь отказался от выбора.

   fileName = QFileDialog.getSaveFileName(None, 'Save Excel file', title,'Excel files (*.xlsx)')
   if fileName == ('',''): return

Собственно экспорт:

      workbook = xlsxwriter.Workbook(fileName[0])
      worksheet = workbook.add_worksheet()
      bold = workbook.add_format({'bold': True})
      dateFormat = 'dd.MM.yyyy'
      date = workbook.add_format({'num_format': dateFormat})
      realCol = 0
      for col in d.columns:
         worksheet.write(0, realCol, headers[col], bold)
         realRow = 1
         for row in d.rows:
            if (row, col) in d.indexes:
               try:
                  v = d.indexes[(row,col)].data(Qt.EditRole)
                  if isinstance(v, QDateTime):
                     if v.isValid() and v.toPyDateTime() > datetime.datetime(1900,1,1):
                        v = v.toPyDateTime()
                        worksheet.write_datetime(realRow, realCol, v, date)
                     else:
                        v = v.toString(dateFormat)
                        worksheet.write(realRow, realCol, v)
                  else:
                     worksheet.write(realRow, realCol, v)
               except:
                  print(str(sys.exc_info()[1]))
            realRow += 1
         realCol += 1
      workbook.close()

Танцы вокруг QDateTime добавлены из-за разного понимания даты/времени в Python, Qt и Excel — во-первых, пакет xlsxwriter умеет работать с питоновским datetime, но не умеет с QDateTime из Qt, поэтому приходится дополнительно его конвертировать специальной функцией toPyDateTime; во-вторых, Excel умеет работать только с датами с 01.01.1900, а всё, что было до этого времени для Excel — просто строка.

Результат экспорта в Excel:

Копирование в системный буфер в формате HTML

Не всегда нужен отдельный файл с выборкой, часто, особенно когда данных немного, удобнее скопировать их в табличном виде в системный буфер (clipboard), а затем вставить в нужное место, будь то Excel, Word, редактор веб-страниц или что-то другое.

Наиболее универсальным способом копирования табличных данных через буфер — это обычный формат HTML. В Windows, *nix и MacOS сильно разные способы работы с буфером (не говоря о том, что их несколько), поэтому хорошо, что Qt скрывает от нас детали реализации.

Всё, что нам нужно — создать объект QMimeData, заполнить его через метод setHtml фрагментом HTML-разметки, и отдать в системный clipboard, который доступен через QApplication

      mime = QMimeData()
      mime.setHtml(html)
      clipboard = QApplication.clipboard()
      clipboard.setMimeData(mime)

Таблицу собираем построчно, начиная с заголовков.

      html = '<table><tbody>n'
      headers = { col:model.headerData(col, Qt.Horizontal) for col in d.columns }
      html += '<tr>' 
      for c in d.columns:
         html += '<th>%s</th>' % headers[c]
      html += '</tr>n' 
      for r in d.rows:
         html += '<tr>' 
         for c in d.columns:
            if (r, c) in d.indexes:
               v = d.indexes[(r,c)].data(Qt.DisplayRole)
               html += '<td>%s</td>' % v
            else:
               html += '<td></td>'
         html += '</tr>' 
      html += '</tbody></table>'

Результат, вставленный в Word:

Здесь границы таблицы видны только благодаря включенной в Word настройке «Показывать границы текста«, на самом деле они невидимы. Чтобы таблица копировалась с явными границами, нужно изменить стиль таблицы в тэге table. Предоставляю это сделать вам.

Заключение

Итак, мы получили способ добавления в наш инструмент новых функций, причем функции добавляются и работают независимо от того, какими источниками данных мы будем пользоваться и как их отображать — модули, работающие с данными, ничего не знают о тулбарах и их функциях, тулбары не связаны ни с модулями данных, ни с функциями кнопок, а функции кнопок, не зная ни о тулбарах, ни о модулях данных, просто пытаются обработать текущий визуальный компонент известным им способом.

Исходники, использованные в примерах, как и ранее, выложены на github под лицензией MIT.

Начало — Точим себе инструмент на PyQt
Продолжение — Режем XML по разметке XQuery


Slot_ExportData()
{
	QString filepath = QFileDialog::getSaveFileName(this, tr("Save as..."),
		QString(), tr("EXCEL files (*.xls);;HTML-Files (*.txt);;"));
 
	if (filepath != "")
	{
		int row = m_pTable->rowCount();
		int col = m_pTable->columnCount();
		QList<QString> list;
		// Add the column headings  
		QString HeaderRow;
		for (int i = 0; i < col; i++)
		{
			HeaderRow.append(m_pTable->horizontalHeaderItem(i)->text() + "t");
		}
		list.push_back(HeaderRow);
		for (int i = 0; i < row; i++)
		{
			QString rowStr = "";
			for (int j = 0; j < col; j++){
				rowStr += m_pTable->item(i, j)->text() + "t";
			}
			list.push_back(rowStr);
		}
		QTextEdit textEdit;
		for (int i = 0; i < list.size(); i++)
		{
			textEdit.append(list.at(i));
		}
 
		QFile file(filepath);
		if (file.open(QFile::WriteOnly | QIODevice::Text))
		{
			QTextStream ts(&file);
			 ts.setCodec ( "GB2312"); // this place everyone decide for themselves whether to use "utf-8"
			ts << textEdit.document()->toPlainText();
			file.close();
		}
		 // will export table, this step we choose not to
		m_pTable->clearContents();
		m_pTable->setRowCount(0);
	}
	
}

Понравилась статья? Поделить с друзьями:
  • Сохранить pages в word онлайн
  • Сохранить google doc в excel
  • Сохранить excel строки в файл
  • Сохранить excel свои разделители
  • Сохранить excel как через vba